Merge pull request 'openai-consumer: the static-key model-access consumer (ADR 0050)' (#18) from feat/openai-access into main
This commit was merged in pull request #18.
This commit is contained in:
@@ -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();
|
||||||
@@ -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=<key>` env file (what most OpenAI
|
||||||
|
// tooling and the OpenAI SDK read), and the publicly-known Codex API-key `auth.json`
|
||||||
|
// (`{ "OPENAI_API_KEY": "<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));
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user