From c454c6a54cbed620a69f0d1b17d7b4a6127f14e3 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 7 Sep 2026 04:25:26 +0200 Subject: [PATCH] openai-consumer: the static-key model-access consumer (ADR 0050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The consumer half of the OTHER model-access shape. Where anthropic-consumer receives a refreshed access token, this receives one operator-supplied API key the mesh sealed to it and the host unsealed at its secret path — no manager, no refresh, no usage. It writes the key where an OpenAI/Codex client reads it: an OPENAI_API_KEY env file and the publicly-known Codex auth.json. Pure node, no SDK import — the simplest a model-access consumer gets. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- modules/openai-consumer/apply/index.ts | 42 +++++++++++++++++++++ modules/openai-consumer/credentials.ts | 32 ++++++++++++++++ modules/openai-consumer/module.json | 51 ++++++++++++++++++++++++++ modules/openai-consumer/package.json | 11 ++++++ modules/openai-consumer/tsconfig.json | 15 ++++++++ 5 files changed, 151 insertions(+) create mode 100644 modules/openai-consumer/apply/index.ts create mode 100644 modules/openai-consumer/credentials.ts create mode 100644 modules/openai-consumer/module.json create mode 100644 modules/openai-consumer/package.json create mode 100644 modules/openai-consumer/tsconfig.json diff --git a/modules/openai-consumer/apply/index.ts b/modules/openai-consumer/apply/index.ts new file mode 100644 index 0000000..08eed62 --- /dev/null +++ b/modules/openai-consumer/apply/index.ts @@ -0,0 +1,42 @@ +// The consumer's scheduled run: take the API key the mesh delivered and write it where an OpenAI or +// Codex client reads it (novox/hq ADR 0050, the static-key half). The key arrives sealed-then-unsealed +// at the module's secret path — the host opened it with this node's private key; this process reads +// plaintext. There is no manager, no refresh, and nothing to strip: a static-key credential is one +// value, delivered unchanged. +// +// What the host delivers, per the manifest: +// secrets.model-access -> a file holding the sealed-then-unsealed API key (host-unsealed). +// binds.model-access -> a JSON file of the non-secret facts the licence serves (which licence, +// model). Not needed to write the key; read only for a log line. +// +// Runs as `mesh-tools run` (no broker) on a schedule, so it is idempotent: same key in, same files out. + +import { readFileSync } from "node:fs"; + +import { deliver } from "../credentials.js"; + +function required(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`${name} is not set — the consumer runtime was deployed without it`); + return v; +} + +function main(): void { + const key = readFileSync(required("MESH_MODEL_ACCESS_SECRET_FILE"), "utf8").trim(); + if (!key) { + // Nothing was delivered — which reads exactly like a credential that never arrived, so it is said + // rather than written as an empty key file a client would take for a valid login. + throw new Error("[openai-consumer] the delivered API key is empty; nothing was written"); + } + + const envFile = process.env.MESH_OPENAI_ENV_FILE ?? `${home()}/.config/openai/openai.env`; + const authFile = process.env.MESH_OPENAI_CREDENTIALS_FILE ?? `${home()}/.codex/auth.json`; + deliver(envFile, authFile, key); + console.error(`[openai-consumer] wrote OPENAI_API_KEY to ${envFile} and ${authFile}`); +} + +function home(): string { + return process.env.HOME ?? "/root"; +} + +main(); diff --git a/modules/openai-consumer/credentials.ts b/modules/openai-consumer/credentials.ts new file mode 100644 index 0000000..c1c4875 --- /dev/null +++ b/modules/openai-consumer/credentials.ts @@ -0,0 +1,32 @@ +// Writing the delivered OpenAI API key where an OpenAI/Codex client reads it — the consumer half of +// model-access for a STATIC-KEY vendor (novox/hq ADR 0050). Unlike the refreshable-grant consumer, +// there is nothing to strip: the credential is a single operator-supplied key the mesh sealed to this +// holder and the host unsealed at the module's secret path. This process reads that plaintext and +// writes it, and only it — no manager, no refresh token, no rotation. +// +// It is written two ways, for two clients: an `OPENAI_API_KEY=` env file (what most OpenAI +// tooling and the OpenAI SDK read), and the publicly-known Codex API-key `auth.json` +// (`{ "OPENAI_API_KEY": "" }`). Both are atomic and 0600 — a partial credential must never be +// read as a whole one. + +import { writeFileSync, renameSync, mkdirSync } from "node:fs"; +import { dirname } from "node:path"; + +/** Atomic write-then-rename at 0600, creating the parent directory if needed. */ +export function atomicWrite(path: string, content: string): void { + mkdirSync(dirname(path), { recursive: true }); + const tmp = `${path}.tmp`; + writeFileSync(tmp, content, { mode: 0o600 }); + renameSync(tmp, path); +} + +/** The Codex API-key auth file shape — the public, documented form of `~/.codex/auth.json`. */ +export function authJson(key: string): string { + return JSON.stringify({ OPENAI_API_KEY: key }, null, 2) + "\n"; +} + +/** Write the delivered key to both an env file and the Codex auth.json. */ +export function deliver(envFile: string, authFile: string, key: string): void { + atomicWrite(envFile, `OPENAI_API_KEY=${key}\n`); + atomicWrite(authFile, authJson(key)); +} diff --git a/modules/openai-consumer/module.json b/modules/openai-consumer/module.json new file mode 100644 index 0000000..8cabeac --- /dev/null +++ b/modules/openai-consumer/module.json @@ -0,0 +1,51 @@ +{ + "module": "openai-consumer", + "version": "1", + "capabilities": [ + "container-runtime" + ], + "requires": [ + "model-access" + ], + "binds": { + "model-access": "/var/lib/openai-consumer/model.json" + }, + "secrets": { + "model-access": "/var/lib/openai-consumer/api-key" + }, + "resources": [ + { + "id": "state", + "type": "directory", + "path": "/var/lib/openai-consumer", + "mode": "0700" + }, + { + "id": "config", + "type": "directory", + "path": "/var/lib/openai-consumer/config", + "mode": "0700" + }, + { + "id": "apply", + "type": "container", + "name": "mesh-openai-consumer-apply", + "image": "mesh-runtime-openai-consumer@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "network": "host", + "schedule": "*/5 * * * *", + "args": [ + "run", + "/app/modules/openai-consumer/dist/apply/index.js" + ], + "volumes": [ + "/var/lib/openai-consumer:/run/state" + ], + "env": { + "MESH_MODEL_ACCESS_SECRET_FILE": "/run/state/api-key", + "MESH_MODEL_ACCESS_BIND_FILE": "/run/state/model.json", + "MESH_OPENAI_ENV_FILE": "/run/state/config/openai.env", + "MESH_OPENAI_CREDENTIALS_FILE": "/run/state/config/auth.json" + } + } + ] +} diff --git a/modules/openai-consumer/package.json b/modules/openai-consumer/package.json new file mode 100644 index 0000000..3237e5b --- /dev/null +++ b/modules/openai-consumer/package.json @@ -0,0 +1,11 @@ +{ + "name": "@novox/module-openai-consumer", + "version": "0.1.0", + "description": "openai-consumer — the consumer side of model-access for a STATIC-KEY vendor (ADR 0050): writes the delivered OpenAI API key where an OpenAI/Codex client reads it (OPENAI_API_KEY env + the Codex auth.json). No manager, no refresh, no usage — the static-key half of the same interface.", + "type": "module", + "private": true, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/modules/openai-consumer/tsconfig.json b/modules/openai-consumer/tsconfig.json new file mode 100644 index 0000000..35a455f --- /dev/null +++ b/modules/openai-consumer/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": [ + "credentials.ts", + "apply/index.ts" + ] +} -- 2.54.0