Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/server/management/oauth-account-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ function validateKeyName(
}

export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;

if (url.pathname === "/api/accounts/events" && req.method === "GET") {
const { accountSelectionStream } = await import("./account-selection-stream");
Expand All @@ -151,6 +151,17 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
const provider = (body.provider ?? "").trim().toLowerCase();
if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
// Meta Muse login imports a credential from the user's macOS Keychain and
// persists it in OpenCodex. A raw management token proves administrative
// access, not that a person acknowledged that credential move and its ToS
// risk. The dashboard warning therefore needs this matching server-side gate;
// headers are not evidence because an admin-token holder can forge them.
if (provider === "meta-muse" && principal !== "gui-session") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the advertised Meta Muse CLI login path

When an operator runs ocx login meta-muse or ocx account login meta-muse, src/cli/account-auth.ts posts to this endpoint using the admin token, so this condition now always returns 403 before the existing CLI consent warning, Keychain import, or manual-key prompt can run. This also contradicts src/oauth/meta-muse.ts, which explicitly directs users without another paste surface to run that command, and makes headless Meta Muse setup impossible. Either add a consent mechanism that the CLI can satisfy or remove/update the CLI fallback and its user-facing documentation and regression coverage.

AGENTS.md reference: AGENTS.md:L367-L372

Useful? React with 👍 / 👎.

return jsonResponse({
error: "Meta Muse import requires acknowledgement in the OpenCodex dashboard.",
code: "oauth_consent_required",
}, 403);
}
const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
if (namespaceCollision) return jsonResponse({ error: namespaceCollision }, 409);
const accountId = body.accountId?.trim();
Expand Down
29 changes: 29 additions & 0 deletions tests/oauth/oauth-public-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,35 @@ describe("legacy ChatGPT OAuth public-surface exclusion", () => {
expect(isPublicOAuthProvider("github-copilot")).toBe(true);
});

test("Meta Muse import requires a consent-bearing GUI session", async () => {
const cfg = config();
const request = () => new Request("http://localhost/api/oauth/login", {
method: "POST",
headers: {
"content-type": "application/json",
origin: "http://localhost",
"x-opencodex-gui-origin": "http://localhost",
"x-opencodex-csrf-token": "forgeable-without-a-session",
},
// A missing account makes a correctly admitted request stop before the
// platform-specific import, while still proving it passed the consent gate.
body: JSON.stringify({ provider: "meta-muse", accountId: "missing-slot" }),
});

for (const principal of [undefined, "admin-token", "gui-pair-capability"] as const) {
const response = await handleManagementAPI(request(), new URL(request().url), cfg, {}, principal);
expect(response?.status).toBe(403);
expect(await response?.json()).toEqual({
error: "Meta Muse import requires acknowledgement in the OpenCodex dashboard.",
code: "oauth_consent_required",
});
}

const admitted = await handleManagementAPI(request(), new URL(request().url), cfg, {}, "gui-session");
expect(admitted?.status).toBe(404);
expect(await admitted?.json()).toEqual({ error: "Unknown account for reauth" });
});

test("generic management OAuth endpoints reject chatgpt before touching login state", async () => {
const cfg = config();
const requests = [
Expand Down
Loading