Skip to content
Merged
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
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,14 @@ they have been synchronized. See

In **Connect → API Keys**, every row of the key table has its own delete button, which asks for confirmation in place. Clicking a key can show its full value with a **Copy** button only from an independently authorized session.

### Applying a Codex CLI update over HTTP

The CLI-update HTTP API has no dashboard job UI. Its apply route requires a current explicitly paired browser session on trusted loopback ingress, matching Origin and CSRF headers, and `confirmed: true`. Automatic local sign-in, a remembered admin session, an admin token or Tailscale sign-in alone cannot authorize this action.

For standalone local pairing, open the dashboard at its configured literal `http://127.0.0.1:<port>` or `http://[::1]:<port>` origin. Use the existing local pairing form in **Connect → API Keys** or Remote Link setup: run the displayed `ocx gui pair --origin "<browser-origin>"` command locally, then redeem its one-time code in that form. Pairing does not apply an update; a subsequent confirmed API request is still required. A server configured with hostname `localhost`, a wildcard bind, or a connected-client role cannot issue this local pairing authority. Remote pairing does not remove the apply route's trusted-loopback requirement.

Without an eligible HTTP pairing path, use `ocx system codex-cli-update plan` followed by `ocx system codex-cli-update apply --plan-id <32-hex> --yes` locally. This uses the same durable job owner without a GUI session. Pairing proves authority to write the protected configuration home, not human identity or protection from unrestricted same-user processes.

### Reading an existing API key

Showing a stored key's full value requires a dashboard session established by explicit pairing or
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout-responses.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"responses-native-output-identity.test.ts": "responses",
"graceful-max-output-tokens.test.ts": "responses",
"graceful-max-output-tokens-exclusions.test.ts": "responses",
"advisor-web-search.test.ts": "advisor",
Expand Down
3 changes: 2 additions & 1 deletion skills/ocx/references/01_surface_observe-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ Drives no management route.
| `--request-id` | string | Optional stable retry key. |
| `--json` | boolean | Emit the public job status. |

- Local counterpart of POST /api/codex/cli-update/apply: invokes the same durable job owner with --yes confirmation; HTTP admission separately requires a trusted loopback GUI session.
- Local counterpart of POST /api/codex/cli-update/apply: invokes the same durable job owner with --yes confirmation; HTTP admission separately requires a current paired dashboard session on trusted loopback ingress, matching origin, CSRF and confirmed: true.
- Standalone HTTP pairing requires the configured literal 127.0.0.1 or ::1 origin; localhost aliases, wildcard binds and connected-client roles use the local CLI --yes path instead.
- Admission is stored under the protected current-user config root before the installer can run.
- Exit 0 requires an applied job; refused, not_applied, ambiguous and pending duplicate jobs exit 1, including with --json.
- Only POSIX npm-global managed installs are currently supported; Windows, shims and other managers remain refused.
Expand Down
2 changes: 1 addition & 1 deletion src/cli/capabilities-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ export const CAPABILITIES: readonly Capability[] = [
routes: [],
flags: [{ name: "--plan-id", value: "string", summary: "Required 32-hex id from the current plan." }, { name: "--yes", value: "boolean", summary: "Required confirmation." }, { name: "--request-id", value: "string", summary: "Optional stable retry key." }, { name: "--json", value: "boolean", summary: "Emit the public job status." }],
mutates: true, json: "envelope",
details: ["Local counterpart of POST /api/codex/cli-update/apply: invokes the same durable job owner with --yes confirmation; HTTP admission separately requires a trusted loopback GUI session.", "Admission is stored under the protected current-user config root before the installer can run.", "Exit 0 requires an applied job; refused, not_applied, ambiguous and pending duplicate jobs exit 1, including with --json.", "Only POSIX npm-global managed installs are currently supported; Windows, shims and other managers remain refused."],
details: ["Local counterpart of POST /api/codex/cli-update/apply: invokes the same durable job owner with --yes confirmation; HTTP admission separately requires a current paired dashboard session on trusted loopback ingress, matching origin, CSRF and confirmed: true.", "Standalone HTTP pairing requires the configured literal 127.0.0.1 or ::1 origin; localhost aliases, wildcard binds and connected-client roles use the local CLI --yes path instead.", "Admission is stored under the protected current-user config root before the installer can run.", "Exit 0 requires an applied job; refused, not_applied, ambiguous and pending duplicate jobs exit 1, including with --json.", "Only POSIX npm-global managed installs are currently supported; Windows, shims and other managers remain refused."],
usage: "ocx system codex-cli-update apply --plan-id <32-hex> --yes [--request-id <key>] [--json]",
},
{
Expand Down
3 changes: 2 additions & 1 deletion src/server/management/codex-cli-update-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export async function handleCodexCliUpdateRoutes(ctx: ManagementContext): Promis
}
if (ctx.url.pathname !== "/api/codex/cli-update/apply") return null;
if (ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405, ctx.req, ctx.config);
if (!hasHumanDashboardSession(ctx) || !ctx.trustedLoopbackIngress) {
if (!hasHumanDashboardSession(ctx) || !ctx.trustedLoopbackIngress
|| ctx.sessionControl?.isPaired(ctx.req, ctx.config) !== true) {
return jsonResponse({ error: "local_dashboard_confirmation_required" }, 403, ctx.req, ctx.config);
}
let body: unknown;
Expand Down
2 changes: 1 addition & 1 deletion src/server/management/route-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
{ method: "POST", path: "/api/codex/desktop-compatibility/settings", module: "server/management/desktop-compatibility-settings-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Saved native observation intent requires an operator-paired local GUI action and fresh field revision." } },
// server/management/codex-cli-update-routes
{ method: "GET", path: "/api/codex/cli-update/plan", module: "server/management/codex-cli-update-routes", mutates: false, exempt: { reason: "local-transport", why: "The CLI runs the same proof-bound plan builder locally; the authenticated route also supplies the dashboard plan." } },
{ method: "POST", path: "/api/codex/cli-update/apply", module: "server/management/codex-cli-update-routes", mutates: true, exempt: { reason: "session-only", why: "HTTP apply requires a confirmed trusted loopback GUI session; the CLI invokes the same durable job owner locally." } },
{ method: "POST", path: "/api/codex/cli-update/apply", module: "server/management/codex-cli-update-routes", mutates: true, exempt: { reason: "session-only", why: "HTTP apply requires a current paired dashboard session on trusted loopback ingress, matching origin, CSRF and confirmed: true; the local CLI uses --yes independently." } },
{ method: "GET", path: "/api/codex/cli-update/jobs/{jobId}", module: "server/management/codex-cli-update-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "The CLI status command reads the same protected job record directly and does not use job IDs as authorization." } },
{ method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true },
{ method: "PUT", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: true },
Expand Down
17 changes: 13 additions & 4 deletions src/server/responses/native-response-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ export function nativeResponseRecord(value: unknown): value is Record<string, un
return value !== null && typeof value === "object" && !Array.isArray(value);
}

const canonical = (item: unknown): string => Array.isArray(item) ? `[${item.map(canonical).join(",")}]`
: nativeResponseRecord(item) ? `{${Object.keys(item).sort().map(key => `${JSON.stringify(key)}:${canonical(item[key])}`).join(",")}}`
: JSON.stringify(item) ?? "null";
const fingerprint = (json: string): string => createHash("sha256").update(json).digest("hex");

/** Compare JSON content by value: object-key order is irrelevant, array order is not. */
export function nativeResponseFingerprint(value: unknown): string {
const canonical = (item: unknown): string => Array.isArray(item) ? `[${item.map(canonical).join(",")}]`
: nativeResponseRecord(item) ? `{${Object.keys(item).sort().map(key => `${JSON.stringify(key)}:${canonical(item[key])}`).join(",")}}`
: JSON.stringify(item) ?? "null";
return createHash("sha256").update(canonical(value)).digest("hex");
return fingerprint(canonical(value));
}

/** Keep transient body keys within the existing 64-byte digest payload bound. */
export function nativeResponseOutputIdentity(item: Record<string, unknown>): string {
if (typeof item.id === "string") return `id:${item.id}`;
const json = canonical(item);
return Buffer.byteLength(json, "utf8") <= 64 ? `json:${json}` : `body:${fingerprint(json)}`;
}
3 changes: 1 addition & 2 deletions src/server/responses/native-response-output.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { nativeResponseFingerprint as injectionFingerprint, nativeResponseRecord as record } from "./native-response-json";
import { nativeResponseFingerprint as injectionFingerprint, nativeResponseOutputIdentity as identity, nativeResponseRecord as record } from "./native-response-json";
type Frame = Record<string, unknown>;

/**
Expand All @@ -10,7 +10,6 @@ export function nativeResponseOutput(done: ReadonlyMap<number, Frame>, terminal:
const observed = [...done.entries()].sort((a, b) => a[0] - b[0]).map(([, item]) => item);
if (terminal == null || (Array.isArray(terminal) && !terminal.length)) return observed;
if (!Array.isArray(terminal) || terminal.some(item => !record(item))) throw new Error("Invalid native response output.");
const identity = (item: Frame) => typeof item.id === "string" ? `id:${item.id}` : `body:${injectionFingerprint(item)}`;
const positions = new Map<string, number>();
observed.forEach((item, index) => {
const key = identity(item);
Expand Down
2 changes: 1 addition & 1 deletion structure/cli-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ eligibility, drift refusal and recovery. No endpoint or credential is printed by
`ocx system codex-cli-update apply` durably admits and records the apply result under the protected config root; `--request-id` provides a retry key, and reusing it with a different plan conflicts. `ocx system codex-cli-update status <job-id>` reads the same path-free terminal DTO without a GUI token. A record left queued/running after owner restart is reported ambiguous with unknown owner state. Apply remains limited to the supported POSIX npm-global installation.
On Windows the same durable record and status read are served by the handle-bound job-store backend described in `codex-cli-update.md`.

The capability declarations document `ocx system codex-cli-update plan`, `apply` and `status` as the local counterparts of `GET /api/codex/cli-update/plan`, `POST /api/codex/cli-update/apply` and `GET /api/codex/cli-update/jobs/{jobId}`. They share the plan builder and durable job owner with the dashboard rather than sending HTTP management requests, so their driven-route lists remain empty. CLI apply requires `--yes`; HTTP apply separately requires explicit confirmation from a trusted loopback GUI session. The GUI parity test resolves these registered capabilities, checks their mutation classifications and documented resource counterparts, and includes all three resources in endpoint coverage.
The capability declarations document `ocx system codex-cli-update plan`, `apply` and `status` as the local counterparts of `GET /api/codex/cli-update/plan`, `POST /api/codex/cli-update/apply` and `GET /api/codex/cli-update/jobs/{jobId}`. They share the plan builder and durable job owner with the dashboard rather than sending HTTP management requests, so their driven-route lists remain empty. CLI apply requires `--yes`; HTTP apply separately requires explicit confirmation from a current paired dashboard session on trusted loopback ingress, with matching origin and CSRF. The GUI parity test resolves these registered capabilities, checks their mutation classifications and documented resource counterparts, and includes all three resources in endpoint coverage.

## Keyless CC-Switch provider import

Expand Down
4 changes: 3 additions & 1 deletion structure/codex-cli-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,9 @@ documented residual it always was.

`bin/ocx.mjs` includes the proof-bound Codex candidate snapshot for the published `start` command as well as the updater commands; this makes the production management plan work without accepting request-supplied candidate evidence. `src/codex/cli-update-inspection-context.ts` is the shared bridge from that Node snapshot to the installer inspector. Direct Bun/source launches have no trusted candidate and fail closed; neither path reconstructs write targets from display data. `src/codex/cli-update-job.ts` owns the bounded durable admission ledger shared by CLI and management. Request IDs are scoped to the protected config root and payload digest; exact retries return the same opaque job, conflicts refuse, and a full ledger refuses new admission. Every store operation requires a canonical same-owner config parent with safe write permissions and canonical ancestry to filesystem root; each ancestor must be owned by root or the current user, and shared-writable ancestors are refused unless root-owned and sticky. Only confirmed apply admission may create one absent config-root level, which is revalidated as owner-private. Plan and status reads never create it, and an existing root must pass the same canonical/no-link checks. This path-based ancestry check prevents untrusted other-user writable ancestors from redirecting store operations; it does not defend against concurrent tampering by the same user or root. Atomic updates use a private same-owner POSIX directory, no-follow reads and a short exclusive lock. Unresolved records are not expired. An in-process generation and revision own completion writes; after restart a queued/running record projects as `ambiguous` with `owner_state_unknown`, without claiming the old owner stopped. A `retained` lease state requires an execution reason that proves acquisition and intentional preservation; synthetic job errors, unverified release and release exceptions project as `unknown`. The path-free DTO separates owner state, installer child state, lease retention and overall lifetime uncertainty with fixed outcome/reason/cleanup codes.

`src/cli/codex-cli-update.ts` records confirmed CLI apply in that ledger and exposes `status <job-id>` from the same protected record, without requiring a GUI token. `src/server/management/codex-cli-update-routes.ts` exposes a proof-bound read-only plan, confirmed human dashboard session (`hasHumanDashboardSession`) plus trusted-loopback apply admission, and authenticated status. Plan and job reads use ordinary management admission. HTTP apply returns after durable admission; disconnect does not cancel the worker. Dashboard display data is never accepted back as a candidate, prefix, executable or managed-selection attestation. There is no dashboard job UI in this contract. `tests/server/persistent-dashboard-consent.test.ts` covers management admission and the apply service boundary.
`src/cli/codex-cli-update.ts` records confirmed CLI apply in that ledger and exposes `status <job-id>` from the same protected record, without requiring a GUI token. `src/server/management/codex-cli-update-routes.ts` exposes a proof-bound read-only plan, confirmed apply from a current paired dashboard session on trusted-loopback ingress, and authenticated status. Apply requires both `hasHumanDashboardSession` and `sessionControl.isPaired`; an automatically issued loopback session, remembered-admin session, raw admin token or Tailscale-identity session does not suffice. Origin, CSRF and `confirmed: true` checks remain required. Plan and job reads use ordinary management admission. HTTP apply returns after durable admission; disconnect does not cancel the worker. Dashboard display data is never accepted back as a candidate, prefix, executable or managed-selection attestation. There is no dashboard job UI in this contract. `tests/server/persistent-dashboard-consent.test.ts` covers refusal and a genuinely issued-and-redeemed local pairing session reaching one apply admission without running an installer.

Standalone pairing requires the configured literal `127.0.0.1` or `::1` loopback origin, as described in [Standalone pairing delivery](remote-link.md#standalone-pairing-delivery). A `localhost` alias, wildcard bind or connected-client role cannot obtain that local pairing authority; remote pairing does not bypass trusted-loopback ingress. Operators without an eligible HTTP pairing path use `ocx system codex-cli-update plan` and `ocx system codex-cli-update apply --plan-id <32-hex> --yes` locally. Pairing establishes protected-home write authority, not human identity or a boundary against unrestricted same-user or elevated processes; the local CLI remains independently available to that owner.

On Windows, `src/codex/cli-update-job-store-windows.ts` maps the same ledger contract onto NTFS handle semantics and the public admit/read/finish API in `src/codex/cli-update-job.ts` dispatches to it asynchronously. Each ancestor from the volume root is opened relative to its already-held parent — never a re-parsed path — and refused on any reparse attribute, then evaluated under the "trusted-ancestry" ACL policy (owner is the serving token or SYSTEM/Administrators/TrustedInstaller; untrusted grantees may hold read and, on admin-owned ancestors only, broad create-only rights). The config-root parent and a pre-existing config root are verified under the "write-private" policy and are never rehardened; only a directory the call itself creates is icacls-hardened and held to the strict "private" rule. File identity is the volume serial plus 128-bit file ID with single link and no pending delete, the dev:ino analog. Ledger writes go through a private hardened temp that is byte/ACL/identity-verified, renamed while its handle is held, re-read through the published name, and flushed at the directory. The admission mutex is a mkdir lock plus an owner record released only while both held pins still show this call identity and content, so a leftover lock blocks later admission exactly like POSIX. ACL snapshots are taken per phase through the shared bounded PowerShell read in `src/lib/windows-owner-acl.ts` (async variant), not per path. Pins bind each object to its verified state for the operation and the private DACL confines writers, but this is not a proof against concurrent mutation by the serving account or an elevated process, which sit inside the trust boundary as on POSIX. The backend is x64-only; cross-account (other-SID) and ARM64 behavior are unverified.

Expand Down
Loading
Loading