From a354f782bc5f8220a3467ba59aed3fbe3c0572c4 Mon Sep 17 00:00:00 2001 From: Noah-Bytes Date: Tue, 21 Jul 2026 18:51:38 +0800 Subject: [PATCH] feat(config): align config.json with claude-openmax / openmax-bridge format Migrate the external config from the old { cws, codex, bridge, org } shape to the bridge format { enabled, server, agent, cf_access, orgs } so per-org access policy (dm + group) can be expressed and passed to the SDK's CwsAgentBridge as orgConfigs. Preserves codex-openmax's runtime-specific codex:{bin,cwd} and bridge:{localHttpPort} blocks. - config.ts: parse the new shape, expose camelCase runtime fields (server/agent), org_id-keyed orgs map -> array with full access, keep codex + local port; env overrides + strict validation retained. - cli.ts: build orgConfigs from the orgs map (full access, enabled filter), thread cf_access into token/http/ws; device_id/app_version from agent.*. - onboarding.ts: emit the new shape with a COMPLETE access block (dmPolicy/dmAllowFrom/groupPolicy/groups); both credential paths. - config.example.json: bridge-format template + codex fields. - tests: config/onboarding updated to the new shape. Co-Authored-By: Claude Opus 4.8 --- config.example.json | 46 +++++++++++++++-- src/cli.ts | 57 +++++++++++++-------- src/config.ts | 111 +++++++++++++++++++++++++++++++++++----- src/onboarding.ts | 49 ++++++++++++------ test/config.test.ts | 64 ++++++++++++++++++++--- test/onboarding.test.ts | 30 +++++++++-- 6 files changed, 293 insertions(+), 64 deletions(-) diff --git a/config.example.json b/config.example.json index d754580..9871e7d 100644 --- a/config.example.json +++ b/config.example.json @@ -1,5 +1,45 @@ { - "cws": { "bffUrl": "https://openmax.com", "wsUrl": "wss://openmax.com/ws", "identityId": "", "apiKey": "" }, - "codex": { "bin": "codex", "cwd": "." }, - "bridge": { "localHttpPort": 8787 } + "enabled": true, + "server": { + "bff_url": "https://your-cws-core.example.com", + "ws_url": "wss://your-cws-comm.example.com/cws-comm", + "frontend_base_path": "/workspace" + }, + "agent": { + "identity_id": "", + "api_key": "cwsk_replace_me", + "device_id": "codex-openmax-device-1", + "app_version": "codex-openmax/0.1.0" + }, + "cf_access": { + "client_id": "", + "client_secret": "" + }, + "orgs": { + "019f0000-0000-7000-8000-000000000000": { + "enabled": true, + "org_id": "019f0000-0000-7000-8000-000000000000", + "org_name": "My Org", + "owner": { "member_id": "", "name": "" }, + "self": { "member_id": "", "name": "Codex", "display_name": "" }, + "access": { + "dmPolicy": "owner", + "dmAllowFrom": [], + "groupPolicy": "allowlist", + "groups": { + "019f0000-0000-7000-8000-conversation1": { + "mode": "mention", + "allowFrom": ["*"] + } + } + } + } + }, + "codex": { + "bin": "codex", + "cwd": "." + }, + "bridge": { + "localHttpPort": 8787 + } } diff --git a/src/cli.ts b/src/cli.ts index 37eb2a2..b9c48c6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,8 +9,8 @@ // graceful SIGINT/SIGTERM. import { execFileSync } from "node:child_process"; import * as fs from "node:fs"; -import { readFileSync } from "node:fs"; import { buildConfig, writeConfigFile, type FetchLike, type OnboardInput } from "./onboarding.js"; +import { loadConfig, type AppConfig } from "./config.js"; function usage(): never { console.error(`usage: @@ -57,9 +57,10 @@ async function cmdInit(args: string[]): Promise { } catch { console.error("init: WARNING — `codex` binary not found on PATH; install it before `start`"); } - const org = (config.org as { self?: { display_name?: string } }).self?.display_name ?? "?"; + const orgs = config.orgs as Record; + const self = Object.values(orgs)[0]?.self?.display_name ?? "?"; // Machine-readable success line. No secrets: display name + org only. - console.log(JSON.stringify({ ok: true, org: input.org_id, self: org, codex: codexOk })); + console.log(JSON.stringify({ ok: true, org: input.org_id, self, codex: codexOk })); } catch (e) { console.error(`init: ${e instanceof Error ? e.message : String(e)}`); process.exit(1); @@ -67,37 +68,48 @@ async function cmdInit(args: string[]): Promise { } async function cmdStart(): Promise { - let cfg: Record; + let config: AppConfig; try { - cfg = JSON.parse(readFileSync("config.json", "utf8")) as Record; - } catch { - console.error("start: ./config.json missing or invalid — run `codex-openmax init` first"); + config = loadConfig(); + } catch (e) { + console.error(`start: ${e instanceof Error ? e.message : String(e)} — run \`codex-openmax init\` first`); process.exit(1); } - if (!(cfg.org as { org_id?: string } | undefined)?.org_id) { - console.error("start: config.json lacks an org block — re-run init"); + if (!config.orgs.length) { + console.error("start: config.json has no orgs — re-run init"); process.exit(1); } // The SDK ships plain JS/ESM with no type declarations; construct via dynamic import. const sdk = (await import("@openmaxai/openmax-agent-sdk")) as Record; const { createSdkCwsBridge } = await import("./bridge/sdk-bridge.js"); const { main } = await import("./index.js"); - const cws = cfg.cws as { bffUrl: string; wsUrl: string; apiKey: string }; - const org = cfg.org as { org_id: string; self: { member_id: string } }; + const { server, agent, cfAccess, orgs } = config; const log = (...a: unknown[]) => console.log(new Date().toISOString(), ...a); const logger = { info: log, warn: log, error: log, debug: () => {}, log }; + // The SDK's cfAccessHeaders() reads `cfg.cf_access.{client_id,client_secret}` (WRAPPED), + // so the bare block must be wrapped as { cf_access: ... }; env COCO_CF_ACCESS_* still wins + // inside the SDK. Omitted entirely when no cf_access is configured. + const cfAccessWrapped = cfAccess ? { cf_access: cfAccess } : undefined; + // enabled:false opts an org out (mirrors claude-openmax / the openmax component). + const activeOrgs = orgs.filter((o) => o.enabled !== false); + const defaultOrgId = () => activeOrgs[0]?.org_id ?? orgs[0].org_id; const tokenManager = new sdk.TokenManager({ - apiKey: cws.apiKey, - coreUrl: cws.bffUrl, + apiKey: agent.apiKey, + coreUrl: server.bffUrl, + ...(cfAccessWrapped ? { cfAccess: cfAccessWrapped } : {}), storage: sdk.memoryStorage(), - resolveDefaultOrgId: () => org.org_id, + resolveDefaultOrgId: defaultOrgId, logger, }); const http = new sdk.CwsHttpClient({ - baseUrl: cws.bffUrl, - apiKey: cws.apiKey, + baseUrl: server.bffUrl, + apiKey: agent.apiKey, + deviceId: agent.deviceId, + clientVersion: agent.appVersion, + ...(cfAccessWrapped ? { cfAccess: cfAccessWrapped } : {}), + frontendBasePath: server.frontendBasePath, tokenManager, - resolveDefaultOrgId: () => org.org_id, + resolveDefaultOrgId: defaultOrgId, logger, }); const bridge = createSdkCwsBridge( @@ -105,15 +117,20 @@ async function cmdStart(): Promise { new sdk.CwsAgentBridge({ http, tokenManager, - ws: { baseUrl: cws.wsUrl, deviceId: `codex-openmax-${org.self.member_id.slice(-6)}`, clientVersion: "codex-openmax/0.0.1" }, - orgConfigs: [cfg.org], + ws: { + baseUrl: server.wsUrl, + deviceId: agent.deviceId, + clientVersion: agent.appVersion, + ...(cfAccessWrapped ? { cfAccess: cfAccessWrapped } : {}), + }, + orgConfigs: activeOrgs, providers: { logger, inbound: { deliver } }, callbacks: { syncSelf: async () => ({ nameReady: true }) }, reporters: { metrics: false }, }), ); const handle = await main(bridge); - log(`[codex-openmax] online — adapter on :${handle.port}, org=${org.org_id}`); + log(`[codex-openmax] online — adapter on :${handle.port}, orgs=${activeOrgs.map((o) => o.org_id).join(",")}`); const stop = async () => { log("[codex-openmax] stopping…"); await handle.stop(); diff --git a/src/config.ts b/src/config.ts index d7405bc..92b8ca1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,23 +1,98 @@ -// Config load/validate: CWS credentials, codex binary, local HTTP port. +// Config load/validate: CWS server + agent credentials (bridge / openmax-mirrored +// format), per-org access policy, PLUS codex-openmax runtime-specific fields (the +// codex binary + the local HTTP/wake port). +// +// The on-disk config.json now mirrors the claude-openmax / openmax-bridge shape so +// the server/agent/orgs blocks are portable between the sibling adapters, with two +// codex-only additions (`codex`, `bridge`) that have no analog there: +// +// { +// "enabled": true, +// "server": { "bff_url", "ws_url", "frontend_base_path" }, +// "agent": { "identity_id", "api_key", "device_id", "app_version" }, +// "cf_access": { "client_id", "client_secret" }, // optional (test/CF envs) +// "orgs": { "": { // KEYED by org_id +// "enabled", "org_id", "org_name", +// "owner": { "member_id", "name" }, +// "self": { "member_id", "name", "display_name" }, +// "access": { "dmPolicy", "dmAllowFrom", "groupPolicy", "groups" } +// } }, +// "codex": { "bin", "cwd" }, // codex-openmax ONLY +// "bridge": { "localHttpPort" } // codex-openmax ONLY (local /wake server) +// } +// // Secrets come from config.json / env and are never committed (see .gitignore, config.example.json). // P0 finding: app-server speaks JSONL over stdio (no ws listener in codex-cli 0.136.0), // so we configure the binary to spawn, not a URL — see docs/p0-spike-findings.md. import { readFileSync } from "node:fs"; +const DEFAULT_APP_VERSION = "codex-openmax/0.1.0"; +const DEFAULT_FRONTEND_BASE_PATH = "/workspace"; +const DEFAULT_DEVICE_ID = "codex-openmax-device"; +const DEFAULT_LOCAL_HTTP_PORT = 8787; + +/** Per-org access policy — full bridge shape (DM + group). Passed VERBATIM to the + * SDK's CwsAgentBridge as part of each orgConfig, so the SDK can enforce group + * access policy (the old `{dmPolicy}`-only shape could not express this). */ +export interface OrgAccess { + dmPolicy?: string; + dmAllowFrom?: string[]; + groupPolicy?: string; + groups?: Record; +} + +/** One org, in the bridge / openmax-mirrored shape handed straight to the SDK + * (orgConfigs). Kept snake_case because the SDK reads these keys directly. */ +export interface OrgConfig { + enabled?: boolean; + org_id: string; + org_name?: string; + owner?: { member_id: string; name: string }; + self: { member_id: string; name?: string; display_name?: string }; + access: OrgAccess; +} + export interface AppConfig { - cws: { bffUrl: string; wsUrl: string; identityId: string; apiKey: string }; + enabled?: boolean; + server: { bffUrl: string; wsUrl: string; frontendBasePath: string }; + agent: { identityId: string; apiKey: string; deviceId: string; appVersion: string }; + cfAccess?: { client_id: string; client_secret: string }; + orgs: OrgConfig[]; + // codex-openmax runtime-specific (no analog in claude-openmax / openmax): codex: { bin: string; cwd: string }; bridge: { localHttpPort: number }; } const REQUIRED = [ - ["cws.bffUrl", (c: AppConfig) => c.cws.bffUrl], - ["cws.wsUrl", (c: AppConfig) => c.cws.wsUrl], - ["cws.identityId", (c: AppConfig) => c.cws.identityId], - ["cws.apiKey", (c: AppConfig) => c.cws.apiKey], + ["server.bff_url", (c: AppConfig) => c.server.bffUrl], + ["server.ws_url", (c: AppConfig) => c.server.wsUrl], + ["agent.identity_id", (c: AppConfig) => c.agent.identityId], + ["agent.api_key", (c: AppConfig) => c.agent.apiKey], ["codex.bin", (c: AppConfig) => c.codex.bin], ] as const; +/** Normalize the org_id-keyed on-disk `orgs` map into an array of orgConfigs. Each + * org's `access` block is passed through as-is (defaulting to {} when absent) so the + * full DM + group policy the config carries reaches the SDK unchanged. */ +function parseOrgs(raw: Record): OrgConfig[] { + const orgsRaw = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}; + const orgs: OrgConfig[] = []; + for (const [orgIdKey, org] of Object.entries(orgsRaw)) { + if (!org || typeof org !== "object") continue; + const org_id = (org as any).org_id || orgIdKey; + if (!org_id) continue; + orgs.push({ + ...((org as any).enabled !== undefined ? { enabled: (org as any).enabled } : {}), + org_id, + org_name: (org as any).org_name || "", + owner: (org as any).owner || { member_id: "", name: "" }, + self: (org as any).self || { member_id: "", name: "", display_name: "" }, + access: (org as any).access || {}, + }); + } + return orgs; +} + /** * Load config from a JSON file (path arg, else $CODEX_OPENMAX_CONFIG, else ./config.json), * then apply env overrides, then validate required fields. Throws on missing/invalid. @@ -32,19 +107,31 @@ export function loadConfig(path?: string): AppConfig { // default path missing is tolerated — env-only config is valid } + const cfAccessRaw = raw.cf_access as { client_id?: string; client_secret?: string } | undefined; const cfg: AppConfig = { - cws: { - bffUrl: process.env.CWS_BFF_URL ?? raw.cws?.bffUrl ?? "", - wsUrl: process.env.CWS_WS_URL ?? raw.cws?.wsUrl ?? "", - identityId: process.env.CWS_IDENTITY_ID ?? raw.cws?.identityId ?? "", - apiKey: process.env.CWS_API_KEY ?? raw.cws?.apiKey ?? "", + ...(raw.enabled !== undefined ? { enabled: raw.enabled } : {}), + server: { + bffUrl: process.env.CWS_BFF_URL ?? raw.server?.bff_url ?? "", + wsUrl: process.env.CWS_WS_URL ?? raw.server?.ws_url ?? "", + frontendBasePath: raw.server?.frontend_base_path ?? DEFAULT_FRONTEND_BASE_PATH, + }, + agent: { + identityId: process.env.CWS_IDENTITY_ID ?? raw.agent?.identity_id ?? "", + apiKey: process.env.CWS_API_KEY ?? raw.agent?.api_key ?? "", + deviceId: raw.agent?.device_id ?? DEFAULT_DEVICE_ID, + appVersion: raw.agent?.app_version ?? DEFAULT_APP_VERSION, }, + // Only surface cf_access when it actually carries a client_id — an empty + // { client_id: "", client_secret: "" } block (as in config.example.json) stays + // inert so we never emit empty CF-Access headers. + ...(cfAccessRaw?.client_id ? { cfAccess: { client_id: cfAccessRaw.client_id, client_secret: cfAccessRaw.client_secret ?? "" } } : {}), + orgs: parseOrgs(raw.orgs), codex: { bin: process.env.CODEX_BIN ?? raw.codex?.bin ?? "codex", cwd: process.env.CODEX_CWD ?? raw.codex?.cwd ?? process.cwd(), }, bridge: { - localHttpPort: Number(process.env.BRIDGE_HTTP_PORT ?? raw.bridge?.localHttpPort ?? 8787), + localHttpPort: Number(process.env.BRIDGE_HTTP_PORT ?? raw.bridge?.localHttpPort ?? DEFAULT_LOCAL_HTTP_PORT), }, }; diff --git a/src/onboarding.ts b/src/onboarding.ts index ba9a062..757dfbd 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -227,7 +227,7 @@ export interface OnboardInput { /** Full init pipeline → the config.json object (same shape the P1 stack already runs on). * Dispatches on which credential shape `input` supplies — see OnboardInput. Validates fully - * before any network call either way (config.ts demands cws.identityId; a config missing it + * before any network call either way (config.ts demands agent.identity_id; a config missing it * would be written only for `start` to reject it). */ export async function buildConfig(fetchFn: FetchLike, input: OnboardInput): Promise> { const { api_key: apiKey, identity_id: identityId, invitation_id: invitationId, invitation_token: invitationToken } = input; @@ -241,16 +241,7 @@ export async function buildConfig(fetchFn: FetchLike, input: OnboardInput): Prom if (!identityId) throw new Error("missing required field: identity_id"); const jwt = await exchangeAgentToken(fetchFn, input.bff_url, apiKey, input.org_id); const self = await fetchSelf(fetchFn, input.bff_url, jwt); - return { - cws: { bffUrl: input.bff_url, wsUrl: input.ws_url, identityId, apiKey }, - codex: { bin: "codex", cwd: "." }, - bridge: { localHttpPort: input.local_http_port ?? 8787 }, - org: { - org_id: input.org_id, - self: { member_id: self.memberId, display_name: self.displayName }, - access: { dmPolicy: "open" }, - }, - }; + return assembleConfig(input, identityId, apiKey, input.org_id, self); } // Self-register shape: register -> identity-only JWT -> accept invitation -> org JWT. @@ -263,15 +254,39 @@ export async function buildConfig(fetchFn: FetchLike, input: OnboardInput): Prom // caller-supplied input.org_id — trust the accept response over the reference value. const orgJwt = await exchangeAgentToken(fetchFn, input.bff_url, registered.apiKey, accepted.orgId); const self = await fetchSelf(fetchFn, input.bff_url, orgJwt); + return assembleConfig(input, registered.identityId, registered.apiKey, accepted.orgId, self); +} + +/** Assemble the config.json object in the bridge / openmax-mirrored shape (see + * config.ts for the full field map), keeping the codex-openmax runtime-specific + * blocks (`codex`, `bridge`). The per-org `access` block is emitted COMPLETE + * ({dmPolicy, dmAllowFrom, groupPolicy, groups}) with claude-openmax's onboarding + * defaults — a private-by-default posture the old `{dmPolicy:"open"}` could not + * express — so an operator can widen it later without hand-editing the shape. */ +function assembleConfig(input: OnboardInput, identityId: string, apiKey: string, orgId: string, self: SelfInfo): Record { return { - cws: { bffUrl: input.bff_url, wsUrl: input.ws_url, identityId: registered.identityId, apiKey: registered.apiKey }, + enabled: true, + server: { bff_url: input.bff_url, ws_url: input.ws_url, frontend_base_path: "/workspace" }, + agent: { + identity_id: identityId, + api_key: apiKey, + // Global device id derived from the resolved member_id (preserves the old + // per-org derivation as a stable, human-recognizable default). + device_id: `codex-openmax-${self.memberId.slice(-6)}`, + app_version: "codex-openmax/0.1.0", + }, + orgs: { + [orgId]: { + enabled: true, + org_id: orgId, + org_name: "", + owner: { member_id: "", name: "" }, + self: { member_id: self.memberId, name: self.displayName, display_name: self.displayName }, + access: { dmPolicy: "owner", dmAllowFrom: [], groupPolicy: "allowlist", groups: {} }, + }, + }, codex: { bin: "codex", cwd: "." }, bridge: { localHttpPort: input.local_http_port ?? 8787 }, - org: { - org_id: accepted.orgId, - self: { member_id: self.memberId, display_name: self.displayName }, - access: { dmPolicy: "open" }, - }, }; } diff --git a/test/config.test.ts b/test/config.test.ts index fa14ce3..a7070ff 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -25,35 +25,82 @@ function tmpConfig(obj: unknown): string { return p; } +// Bridge / openmax-mirrored shape + codex-openmax runtime-specific blocks (codex, bridge). const VALID = { - cws: { bffUrl: "https://openmax.com", wsUrl: "wss://openmax.com/ws", identityId: "id_1", apiKey: "cwsk_x" }, + enabled: true, + server: { bff_url: "https://openmax.com", ws_url: "wss://openmax.com/ws", frontend_base_path: "/workspace" }, + agent: { identity_id: "id_1", api_key: "cwsk_x", device_id: "dev_1", app_version: "codex-openmax/9.9.9" }, + cf_access: { client_id: "cf_id", client_secret: "cf_secret" }, + orgs: { + org_1: { + enabled: true, + org_id: "org_1", + org_name: "Org One", + owner: { member_id: "", name: "" }, + self: { member_id: "m_1", name: "Codex", display_name: "Codex" }, + access: { dmPolicy: "owner", dmAllowFrom: [], groupPolicy: "allowlist", groups: { conv_1: { mode: "mention", allowFrom: ["*"] } } }, + }, + }, codex: { bin: "codex", cwd: "/tmp" }, bridge: { localHttpPort: 8787 }, }; -describe("loadConfig (P1 MVP)", () => { - it("loads a valid config file", () => { +describe("loadConfig (bridge-format)", () => { + it("loads a valid config file and maps the bridge shape to the runtime shape", () => { const p = tmpConfig(VALID); const c = loadConfig(p); - expect(c.cws.apiKey).toBe("cwsk_x"); + expect(c.server.bffUrl).toBe("https://openmax.com"); + expect(c.server.wsUrl).toBe("wss://openmax.com/ws"); + expect(c.server.frontendBasePath).toBe("/workspace"); + expect(c.agent.identityId).toBe("id_1"); + expect(c.agent.apiKey).toBe("cwsk_x"); + expect(c.agent.deviceId).toBe("dev_1"); + expect(c.agent.appVersion).toBe("codex-openmax/9.9.9"); + expect(c.cfAccess).toEqual({ client_id: "cf_id", client_secret: "cf_secret" }); expect(c.codex.bin).toBe("codex"); expect(c.bridge.localHttpPort).toBe(8787); rmSync(p, { force: true }); }); + it("parses the org_id-keyed orgs map into an array carrying the FULL access block (dm + group)", () => { + const p = tmpConfig(VALID); + const c = loadConfig(p); + expect(c.orgs).toHaveLength(1); + const org = c.orgs[0]; + expect(org.org_id).toBe("org_1"); + expect(org.self.member_id).toBe("m_1"); + // The whole point of the alignment: group access policy survives into orgConfigs. + expect(org.access).toEqual({ dmPolicy: "owner", dmAllowFrom: [], groupPolicy: "allowlist", groups: { conv_1: { mode: "mention", allowFrom: ["*"] } } }); + rmSync(p, { force: true }); + }); + + it("keys an org by its map key when the inner org_id is omitted", () => { + const p = tmpConfig({ ...VALID, orgs: { org_from_key: { self: { member_id: "m" }, access: {} } } }); + const c = loadConfig(p); + expect(c.orgs[0].org_id).toBe("org_from_key"); + rmSync(p, { force: true }); + }); + + it("leaves cfAccess undefined when cf_access has an empty client_id (inert example block)", () => { + const p = tmpConfig({ ...VALID, cf_access: { client_id: "", client_secret: "" } }); + const c = loadConfig(p); + expect(c.cfAccess).toBeUndefined(); + rmSync(p, { force: true }); + }); + it("env overrides file values", () => { const p = tmpConfig(VALID); process.env.CWS_API_KEY = "cwsk_from_env"; process.env.BRIDGE_HTTP_PORT = "9999"; const c = loadConfig(p); - expect(c.cws.apiKey).toBe("cwsk_from_env"); + expect(c.agent.apiKey).toBe("cwsk_from_env"); expect(c.bridge.localHttpPort).toBe(9999); rmSync(p, { force: true }); }); it("throws listing every missing required field", () => { - const p = tmpConfig({ cws: { bffUrl: "https://x", wsUrl: "wss://x" }, codex: {}, bridge: {} }); - expect(() => loadConfig(p)).toThrow(/missing required field\(s\).*cws\.identityId.*cws\.apiKey/s); + const p = tmpConfig({ server: { bff_url: "https://x", ws_url: "wss://x" }, agent: {}, codex: {}, bridge: {} }); + expect(() => loadConfig(p)).toThrow(/missing required field\(s\).*agent\.identity_id.*agent\.api_key/s); rmSync(p, { force: true }); }); @@ -75,6 +122,7 @@ describe("loadConfig (P1 MVP)", () => { // no path arg + no CODEX_OPENMAX_CONFIG → default ./config.json absent → tolerated, env supplies all const c = loadConfig(); expect(c.codex.bin).toBe("codex"); - expect(c.cws.apiKey).toBe("cwsk_env"); + expect(c.agent.apiKey).toBe("cwsk_env"); + expect(c.orgs).toEqual([]); }); }); diff --git a/test/onboarding.test.ts b/test/onboarding.test.ts index 2f1f3cc..1a7e7f1 100644 --- a/test/onboarding.test.ts +++ b/test/onboarding.test.ts @@ -171,10 +171,21 @@ describe("onboarding: buildConfig pipeline (api_key + identity_id)", () => { local_http_port: 9999, }); expect(cfg).toEqual({ - cws: { bffUrl: "https://x.test", wsUrl: "wss://x.test/ws", identityId: "id_direct", apiKey: "sk_direct" }, + enabled: true, + server: { bff_url: "https://x.test", ws_url: "wss://x.test/ws", frontend_base_path: "/workspace" }, + agent: { identity_id: "id_direct", api_key: "sk_direct", device_id: "codex-openmax-m_9", app_version: "codex-openmax/0.1.0" }, + orgs: { + org_1: { + enabled: true, + org_id: "org_1", + org_name: "", + owner: { member_id: "", name: "" }, + self: { member_id: "m_9", name: "codex-bot", display_name: "codex-bot" }, + access: { dmPolicy: "owner", dmAllowFrom: [], groupPolicy: "allowlist", groups: {} }, + }, + }, codex: { bin: "codex", cwd: "." }, bridge: { localHttpPort: 9999 }, - org: { org_id: "org_1", self: { member_id: "m_9", display_name: "codex-bot" }, access: { dmPolicy: "open" } }, }); expect(calls.some((c) => c.url.includes("/accept"))).toBe(false); // no agent-side invitation redemption exists }); @@ -221,10 +232,21 @@ describe("onboarding: buildConfig pipeline (invitation_id + invitation_token, se local_http_port: 9999, }); expect(cfg).toEqual({ - cws: { bffUrl: "https://x.test", wsUrl: "wss://x.test/ws", identityId: "id_registered", apiKey: "sk_registered" }, + enabled: true, + server: { bff_url: "https://x.test", ws_url: "wss://x.test/ws", frontend_base_path: "/workspace" }, + agent: { identity_id: "id_registered", api_key: "sk_registered", device_id: "codex-openmax-m_9", app_version: "codex-openmax/0.1.0" }, + orgs: { + org_from_accept: { + enabled: true, + org_id: "org_from_accept", + org_name: "", + owner: { member_id: "", name: "" }, + self: { member_id: "m_9", name: "codex-bot", display_name: "codex-bot" }, + access: { dmPolicy: "owner", dmAllowFrom: [], groupPolicy: "allowlist", groups: {} }, + }, + }, codex: { bin: "codex", cwd: "." }, bridge: { localHttpPort: 9999 }, - org: { org_id: "org_from_accept", self: { member_id: "m_9", display_name: "codex-bot" }, access: { dmPolicy: "open" } }, }); // full sequence: register, identity-only exchange, accept, org-scoped exchange, /me expect(calls.filter((c) => c.url.includes("/auth/register/agent"))).toHaveLength(1);