From 30f98630278c8f5e055821a1ffb15b7858d7f423 Mon Sep 17 00:00:00 2001 From: Mathis Blanc Date: Fri, 24 Jul 2026 04:35:12 +0200 Subject: [PATCH] Add Codex Prompt Center --- README.md | 2 + docs/agent-configuration.md | 12 +- docs/features/codex-prompt-center.md | 244 + docs/features/context-hub.md | 11 +- docs/features/index.md | 5 +- docs/features/settings.md | 6 +- docs/product-overview.md | 11 +- schemas/codex-prompt-catalog-v1.schema.json | 1106 ++++ schemas/codex-prompt-overrides-v1.schema.json | 154 + ...ex-prompt-publication-state-v2.schema.json | 53 + ...odex-prompt-runtime-receipt-v2.schema.json | 110 + src/codex_prompt_center.mjs | 3888 ++++++++++++ src/context_room.mjs | 970 ++- test/codex_prompt_center.test.mjs | 5608 +++++++++++++++++ test/context_hub.test.mjs | 10 + test/context_room.test.mjs | 20 + test/shared_context.test.mjs | 5 + 17 files changed, 12176 insertions(+), 39 deletions(-) create mode 100644 docs/features/codex-prompt-center.md create mode 100644 schemas/codex-prompt-catalog-v1.schema.json create mode 100644 schemas/codex-prompt-overrides-v1.schema.json create mode 100644 schemas/codex-prompt-publication-state-v2.schema.json create mode 100644 schemas/codex-prompt-runtime-receipt-v2.schema.json create mode 100644 src/codex_prompt_center.mjs create mode 100644 test/codex_prompt_center.test.mjs diff --git a/README.md b/README.md index 95ef21c..bae08c6 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,8 @@ Rules that matter: - [Agent configuration](docs/agent-configuration.md): config fields, metadata, and agent setup. - [Shared context](docs/features/shared-context.md): generic shared repositories, proposals, partial acceptance, skills, freshness, and permissions. - [Context Hub](docs/features/context-hub.md): one UI for local-only, shared-only, and linked local-plus-shared projects. +- [Codex Prompt Center](docs/features/codex-prompt-center.md): edit runtime-published Codex prompt targets through exact private overlays, inspect per-process runtime-loaded proof, then quit Codex completely, reopen it, and create a new task. +- Codex Prompt Center contracts: [catalog/snapshot v1](schemas/codex-prompt-catalog-v1.schema.json), [overrides v1](schemas/codex-prompt-overrides-v1.schema.json), [publication state v2](schemas/codex-prompt-publication-state-v2.schema.json), and [runtime receipt v2](schemas/codex-prompt-runtime-receipt-v2.schema.json). - [Documentation research agent](docs/features/documentation-agent.md): working-agent launcher, dedicated documentation CLI, Codex isolation, evidence schema, and safety boundaries. - [Documentation lifecycle](docs/features/documentation-lifecycle.md): creation, scheduled audit, task proposal reuse, and local/shared/mixed routing. diff --git a/docs/agent-configuration.md b/docs/agent-configuration.md index 96857d3..bd2339d 100644 --- a/docs/agent-configuration.md +++ b/docs/agent-configuration.md @@ -4,8 +4,8 @@ context_room: scope: context-room status: current canonical_for: agent configuration - last_verified: 2026-07-21 - sources: [bin/context-room.mjs, src/context_room.mjs, src/shared_context.mjs, schemas/config.schema.json] + last_verified: 2026-07-23 + sources: [bin/context-room.mjs, src/context_room.mjs, src/codex_prompt_center.mjs, src/shared_context.mjs, schemas/config.schema.json, schemas/codex-prompt-catalog-v1.schema.json, schemas/codex-prompt-overrides-v1.schema.json, schemas/codex-prompt-publication-state-v2.schema.json, schemas/codex-prompt-runtime-receipt-v2.schema.json] --- # Agent configuration guide @@ -26,6 +26,14 @@ Appearance preferences are shared across every Context Room on the computer and Use the Settings screen to change the app theme, hidden-file visibility, or `Auto-open Git diff`. Project paths, review rules, scanners, templates, and hub cards remain local to `.context-room/config.json`. +Codex prompt overrides are also computer-wide, but they are not preferences and never belong in project configuration: + +```text +$CODEX_HOME/prompt-overrides/ +``` + +Use **Context Hub → Codex prompts** to inspect the runtime-published catalog and edit only targets marked `securityClass: local_user_editable`. Context Room writes the strict private `overrides.json` contract; a compatible Codex runtime regenerates the catalog and per-process runtime receipts. After a change, quit Codex completely (`⌘Q` on macOS), reopen it, then create a new task. See [Codex Prompt Center](features/codex-prompt-center.md). + The human-owned review gate is also stored separately: ```text diff --git a/docs/features/codex-prompt-center.md b/docs/features/codex-prompt-center.md new file mode 100644 index 0000000..d5e3d7d --- /dev/null +++ b/docs/features/codex-prompt-center.md @@ -0,0 +1,244 @@ +--- +context_room: + kind: canonical + scope: context-room + status: current + canonical_for: Codex Prompt Center + last_verified: 2026-07-23 + sources: [src/codex_prompt_center.mjs, src/context_room.mjs, schemas/codex-prompt-catalog-v1.schema.json, schemas/codex-prompt-overrides-v1.schema.json, schemas/codex-prompt-publication-state-v2.schema.json, schemas/codex-prompt-runtime-receipt-v2.schema.json, test/codex_prompt_center.test.mjs] +--- + +# Codex Prompt Center + +## Purpose + +Codex Prompt Center is the global Context Hub editor for prompt targets published by a compatible local Codex runtime. It does not guess which modes or model prompts exist. The installed runtime owns the catalog; Context Room renders every group and target it receives. + +Open Context Hub and select **Codex prompts**. The catalog is loaded only when this tab opens, so prompt discovery does not slow normal project startup. + +## Compatibility Prerequisite + +This integration currently targets OpenAI Codex tag [`rust-v0.145.0`](https://github.com/openai/codex/tree/rust-v0.145.0), source commit [`25af12f7e61572b0bc18ddb1008be543b91519b0`](https://github.com/openai/codex/commit/25af12f7e61572b0bc18ddb1008be543b91519b0), with the companion repository's [`patches/codex-prompt-overrides-rust-v0.145.0.patch`](https://github.com/Swarek/codex-default-no-assumptions/blob/main/patches/codex-prompt-overrides-rust-v0.145.0.patch) applied. A stock build does not publish this contract. Follow the [companion repository README](https://github.com/Swarek/codex-default-no-assumptions#readme) to install the Apple Command Line Tools that provide `/usr/bin/python3`, build, and install the compatible runtime, then fully restart Codex before opening Prompt Center. + +Installing the compatible runtime is the one-time prerequisite. After that, normal prompt changes happen in Context Room: save the override, fully restart Codex, and create a new task. Rebuilding is needed only when installing a different Codex version or changing the runtime patch itself. + +A compatible runtime creates: + +```text +$CODEX_HOME/prompt-overrides/catalog.json +``` + +The file must be private mode `0600`, use `schemaVersion: 1`, publish `runtimeVersion` and `groups`, and carry a valid typed `catalogRevision`. Prompt Center fails closed and remains read-only when that diagnostic file is absent, insecure, malformed, or from another protocol version. This file check proves compatibility only; the per-process receipt described below is still required to prove what a running Codex process loaded. + +## Editing Flow + +1. Select a runtime-published target. +2. Compare **Official**, **Effective after restart**, and **Runtime loaded**. +3. Edit the complete **Effective after restart** text. +4. Inspect the exact, non-normalized diff. +5. Select **Save override**. +6. Quit Codex completely (`⌘Q` on macOS), reopen it, then create a new task. + +**Restore official** always asks for confirmation, then removes the saved override for that target. It does not copy an old upstream prompt over the current catalog. + +The three surfaces have distinct meanings: + +- **Official** is the current baseline published by the installed Codex build. +- **Effective after restart** is the desired text after applying the current registry to the official baseline. A read-only target shadowed by explicit Codex configuration instead shows the runtime-published effective text. +- **Runtime loaded** is reconstructed per process from that process's immutable catalog snapshot plus its exact concrete target hash. It proves only what the local runtime resolved and loaded for that target. It does not prove that a collaboration mode was selected, that the target entered a particular task's assembled context, or that it was sent to a model. Receipt files never contain prompt text. + +The catalog is intentionally broader than the editor. Context Room discovers groups and statuses from the runtime rather than maintaining its own list. Every target belongs to one authority class: + +- **Editable here**: the runtime publishes an official baseline, an effective value, `editable: true`, `securityClass: local_user_editable`, an allowed editable runtime status, and one coherent override strategy. Only model-base, developer, local compaction, and selectable collaboration targets can enter this class. +- **Configurable elsewhere**: another local Codex configuration or task-history surface owns the value. Prompt Center shows the owner and read-only reason instead of competing with it. +- **Protected**: security-, protocol-, enforcement-, lifecycle-, or platform-coupled instructions are metadata-only. Prompt text and hashes are not published to Context Room. +- **Server-owned**: a remote service owns the instruction. The target is metadata-only and cannot be replaced locally. + +The platform-supplied system prompt and remote/server compaction instructions cannot be read or replaced through this local integration. Prompt Center shows only their metadata and read-only reason. This boundary is enforced by both the JSON catalog contract and Context Room's runtime checks; a forged `editable: true` flag is rejected. + +In the Codex 0.145.0 compatibility contract, Default and Plan are `selectable` and editable. Pair Programming and Execute are `dormant` and read-only. `selectable` describes a runtime-supported template; it does not assert which mode a task selected. Bundled, cached, and configured model targets can be editable when the runtime also marks them `local_user_editable`. + +Raw-provider model targets can also be `catalogued` before their runtime is active—for example, a Bedrock model discovered from provider configuration. `catalogued`, bundled, cached, and configured model targets can be editable only when the runtime also supplies the complete local-user-editable contract. + +## Exact Overlays + +Context Room stores an exact contextual replacement instead of a permanent copy of the whole upstream prompt: + +```json +{ + "targetId": "example/target", + "officialHash": "sha256:…", + "patches": [ + { + "before": "Exact official text", + "after": "Exact replacement text", + "expectedMatches": 1 + } + ], + "replacement": null +} +``` + +The editor first derives one deterministic contextual span from the longest shared prefix and suffix. If that span is ambiguous or cannot reproduce the draft exactly, it falls back to one bounded full-document patch. Codex still requires exactly one source match. If an upstream update changes the official hash or anchor, Prompt Center reports a conflict instead of silently loading stale instructions. + +The runtime publishes `overrideStrategy` for each editable target. A non-empty baseline normally uses `patch`; an intentionally empty baseline uses `replacement`. Context Room does not infer this rule from a target name. Targets without published official content cannot use the full-text editor because Context Room cannot derive a safe overlay. + +A wildcard model override always has `targetId: model/base/*`, `officialHash` omitted or `null`, one or more patches, and `replacement: null`; it cannot bind to one official model hash or replace multiple baselines wholesale. Its catalog row is read-only pattern metadata with `kind: model_base`, `runtimeStatus: pattern`, `securityClass: advanced_pattern`, `targetPattern: model/base/{modelSlug}`, and no prompt text or hash. `sourceTargetId` is either `null` or `model/base/*` when that wildcard override is active. Saving one concrete target back to its official text materializes an exact concrete no-op patch. Exact target precedence then neutralizes the wildcard for that model without deleting or weakening the wildcard for other models. + +Runtime-detected conflicts remain recoverable. The catalog publishes `overrideConflict: { code, message, sourceTargetId }` with one of the current codes `official_hash_mismatch`, `strategy_mismatch`, `patch_anchor_mismatch`, `effective_prompt_too_large`, or `target_became_personality_dependent`. Context Room shows the exact runtime message. Normal drift conflicts on a trusted editable target can be repaired by saving a corrected exact override or removing the conflicting source. A model target that became personality-dependent stays read-only, but its stale exact override remains removable so the user can migrate to a personality-qualified target. A stale runtime conflict disappears from the desired state as soon as the current manifest resolves cleanly; the runtime-loaded surface still requires a restart to change. + +An explicit Codex developer, compact, or model prompt configuration has higher priority than the registry. The runtime publishes that target as `shadowed_by_explicit_config`, `editable: false`, and `securityClass: config_shadowed`, with the explicit configuration in the effective surface. A resumed task can similarly publish `shadowed_by_session_history` with `securityClass: session_history` because its historical base prompt remains fixed for conversation consistency. Context Room never applies a saved registry override over either read-only value, but an exact stale registry entry remains removable. + +## Concurrency And Validation + +Every new validation and save request rechecks: + +- catalog revision; +- official hash; +- current override hash; +- target editability; +- target presence and catalog-published override strategy; +- `securityClass: local_user_editable` and an explicitly allowed editable runtime status; +- exact overlay result; +- 28 KiB UTF-8 prompt limit; +- 8,192-token estimate limit using `ceil(UTF-8 bytes / 4)`; +- 16 MiB aggregate manifest limit. +- manifest revision increment within JavaScript's maximum safe integer. + +A stale edit returns `409 Conflict`. The browser preserves only genuinely modified drafts while it loads the new baseline, so the user can compare and retry instead of losing text or overwriting another change. A manual catalog refresh and every successful global manifest mutation invalidate cached target details; untouched drafts reset to the newly published effective text, while modified drafts remain available for deliberate review against that new baseline. + +The editor shows live UTF-8 byte and estimated-token counts. A draft above either runtime limit cannot be saved. Above 1,000 estimated tokens, **Save override** requires an explicit high-context confirmation because that target can consume substantial context whenever the runtime uses it. The server requires the corresponding boolean acknowledgement too; a client cannot bypass the confirmation by calling the API directly. + +An override whose target disappeared, changed strategy, or became read-only is shown and counted as a conflict instead of making the whole Prompt Center unavailable. New saves remain blocked until an orphaned or structurally incompatible entry is removed, while **Restore official** remains available. Runtime-reported conflicts remain editable only when the runtime still publishes trusted editable authority; migration conflicts can deliberately be read-only and removable instead. + +After the atomic manifest rename succeeds, a later receipt-refresh failure cannot turn that commit into a false save failure. The API returns the committed target with `commitWarning`; the UI reports that the override was saved while its runtime-loaded status remains temporarily unavailable. + +Because several isolated Context Room servers can expose the same global Prompt Center, a private lock directory serializes the final revalidation and atomic manifest replacement across processes. Its `owner.json` binds the lock to a PID, that process instance's start time when available, and a random generation token. A verified live owner is never evicted because of age and produces `409`. + +Every owner or reclaimer whose recorded start time is `null` first receives a 30-second incomplete-record grace period. After that grace, Context Room compares the current process start with the owner record timestamp: a process that predates the record remains a live owner, while a valid later start proves a reused PID. If that start remains unavailable, malformed, or throws while the PID is alive, Context Room fails safe and keeps the lock blocking. + +Stale-lock recovery creates `.reclaim` inside that exact lock directory with exclusive `wx` creation. The reclaimer then rereads the directory, owner, and claim identities through the same paths. When every identity still matches, it atomically renames that generation to a unique private retired path before cleanup. Release uses the same generation-retirement transition after verifying its owner and the absence of a reclaim claim. A successor can therefore claim the canonical lock path immediately without being deleted by delayed cleanup of the retired generation; a second live or fresh reclaimer still receives `409`. + +Read-only targets remain visible with the runtime-provided reason. The UI does not hardcode `Default`, `Plan`, model names, or any other target ID. + +## Private Storage + +Prompt state belongs to Codex, not to a Context Room project: + +```text +$CODEX_HOME/prompt-overrides/ +├── catalog.json +├── overrides.json +├── last-known-good.json +├── .publication-state.json # runtime-owned publication ordering; read-only here +├── .context-room-write.lock/ +│ ├── owner.json +│ └── .reclaim # present only during stale-lock recovery +└── runtime/ + ├── .json + └── ..catalog.json +``` + +`$CODEX_HOME` defaults to `$HOME/.codex`. + +**.publication-state.json** is a schema-v2 file owned by the compatible Codex runtime. It serializes multi-process catalog publication through registry and owner generations and uses mode `0600`. Context Room reads it only to verify runtime receipts; it never writes, restores, deletes, or exposes it as an editable prompt. + +Security rules: + +- the storage root and owned directories use mode `0700`; +- state files use mode `0600`; +- reads reject roots or runtime directories that are not `0700`, and reject catalog, manifest, publication-state, receipt, or snapshot files that are not `0600`; +- sensitive files are opened once with `O_NOFOLLOW`, validated with `fstat` on that same descriptor, read with a hard byte bound, and decoded as fatal UTF-8 before JSON parsing; +- writes use a private temporary file, `fsync`, and atomic rename; +- before each changed save or restore, `last-known-good.json` receives an atomic private copy of the previous manifest; +- a manifest revision at `9007199254740991` is rejected before backup or rename because it cannot be incremented safely; +- symbolic storage roots, state files, and runtime receipts are refused; +- browser requests contain opaque target IDs, never filesystem paths; +- Prompt Center requires a loopback socket peer and the active loopback host, rejects an incompatible `Origin` or `Referer` and explicit cross-site requests, and permits local clients that omit those browser headers; +- every mutation, including one from a headerless local client, requires both the active project identity and the random page nonce; +- prompt text never enters `.context-room/config.json`, the review ledger, Git, runtime receipts, or logs; +- Context Room never edits `config.toml` automatically. + +Catalogs, immutable runtime snapshots, `overrides.json`, and `last-known-good.json` contain plaintext prompt content. Mode `0600` prevents access by other OS users, but any process running as the same user can still read it. Never put passwords, API keys, access tokens, private keys, or other secrets in a prompt override. + +**Restore official** removes the active override, but the previous text can remain in `last-known-good.json` and older runtime snapshots until they are replaced or garbage-collected. Context Room does not currently provide a destructive purge button. For a recoverable full reset, quit every Codex and Context Room process, move the entire `prompt-overrides` directory to a new private backup path beside it, restart the patched Codex build, and verify that it generated a fresh official catalog before recreating any wanted overrides. Keep that backup private and delete it only after independent verification. + +`catalog.json` publishes target metadata plus nullable `officialText`, `officialHash`, `effectiveText`, and `effectiveHash`, `overrideStrategy`, `securityClass`, optional `overrideConflict`, and optional source target ID. Protected and server-owned targets publish all four content fields as `null`. The catalog is the current discovery and editing surface; it is not evidence of what an older running process loaded. + +Context Room rejects missing or unknown catalog fields, mismatched text/hash presence, incoherent authority metadata, or an editable target without a complete effective value. An editable target with `sourceTargetId: null` must have an effective value exactly equal to its official baseline; any changed effective value requires explicit runtime-published override provenance. + +Context Room rebuilds the runtime's typed canonical JSON in fixed field order and recomputes `catalogRevision` before trusting the catalog. It does not trust `editable: true` alone: only one of the four allowed editable kinds with `securityClass: local_user_editable`, an allowed runtime status, full content, and the baseline-compatible override strategy opens the editor. + +Each verified Codex app-server process writes a schema-v2 hash-only receipt and references one immutable catalog snapshot. The receipt includes a positive JavaScript-safe `publicationGeneration` and the exact macOS process-generation identity `processStartIdentity: darwin-proc-bsdinfo-v1:::`. The boot-session UUID is lowercase `kern.bootsessionuuid`; `tvsec` is positive and `tvusec` comes from `proc_pidinfo(PROC_PIDTBSDINFO)`, zero-padded to six digits. `catalogFile` is a strict basename bound to the receipt PID and the snapshot's raw SHA-256. The snapshot must be a regular non-symlink file with mode `0600`; its raw hash, logical `catalogRevision`, and runtime version must match the receipt. Dead or non-Codex PIDs are ignored before their receipt is parsed. + +Receipt freshness is also bound to the current process instance. Context Room invokes only Apple's absolute `/usr/bin/python3` path with a minimal environment, never a `python3` resolved from `PATH`. Its `ctypes` helper validates the exact `proc_pidinfo` result size, PID, positive start second, and bounded microseconds; it also bounds the `kern.bootsessionuuid` buffer to 128 bytes before allocation and requires an ASCII canonical UUID before lowercasing it. The reader compares this identity with the receipt before opening the snapshot, then obtains it again after snapshot validation and requires the tuple to remain unchanged. If `/usr/bin/python3` is absent or not executable, or if exact identity is unavailable or changes, runtime-loaded state fails closed as unverified. Context Room also strictly requires the receipt load timestamp plus the receipt and snapshot modification times to be no older than the process start. There is no grace window: even a receipt from one second before the verified process start is rejected. + +Every snapshot read is bracketed by two reads each of the receipt and **.publication-state.json**. The verification order is process identity A, receipt R1, publication state S1, snapshot, receipt R2, publication state S2, then process identity B. R1 and R2 must be byte-identical. Both states must be strictly valid, and the owner generation for that receipt's PID must stay unchanged; unrelated `nextGeneration`, `globalOwnerGeneration`, registry-generation, or other PID owner changes do not invalidate the proof. Each relevant owner must equal the receipt's exact `publicationGeneration`. + +Publication state v2 separates instance registry age from receipt publication age. `runtimeRegistryGenerations[pid]` is the generation reserved when the current configuration instance takes ownership of that PID; `runtimeOwnerGenerations[pid]` changes for every receipt publication or tombstone. The two maps must contain exactly the same canonical decimal non-zero `u32` PID keys. All runtime generations are positive JavaScript-safe integers below `nextGeneration`, and each registry generation must be less than or equal to its owner generation. `globalOwnerGeneration` may be zero and must remain below `nextGeneration`. Context Room validates registry generations structurally, but only `runtimeOwnerGenerations[pid]` proves receipt freshness. A legacy v1 file remains unverified here until the compatible runtime atomically migrates it while reserving its next publication generation; Context Room never guesses the missing registry map. + +Before returning an aggregate, Context Room also validates one coherent batch for the exact PIDs whose receipts it will trust. It captures their initial receipt bytes, owner-generation vector, represented live-process set, and exact process identities; validates every snapshot; reads a strict final publication state; rereads every proved receipt; then reads the publication state and represented process set again. Each receipt's bytes and generation must still match its proof, the relevant owner vector must match the initial vector across both final state reads, and both the PID set and every process-start identity must remain unchanged. Churn in fields and owners outside the relevant owner vector is deliberately ignored. A batch race retries the entire batch once; a second race fails every otherwise verified member closed as unverified, so aggregation never combines stale runtime A with current runtime B. + +The publication protocol requires receipt bytes to be immutable for one `publicationGeneration`. Under the runtime's artifact lock, only the current or a newer registry generation may publish. It reserves the next unique owner generation, advances `nextGeneration`, and writes publication state with that owner before writing the content-addressed snapshot and then the receipt. A failure before receipt replacement therefore leaves the new owner as a tombstone; a failure after replacement may leave the fully written receipt verifiable. The runtime revalidates owner generation after publication, and an older registry generation cannot republish. Any later receipt publication, replacement, or tombstone must allocate a fresh owner generation for that PID. Context Room rereads receipt bytes and catches violations that overlap its verification window, but no sequence of filesystem reads can create a coherent batch against a writer that silently rewrites an already-read receipt without advancing its generation. + +If the runtime atomically publishes a newer relevant receipt or owner generation, changes process identity, or changes the represented process set during the first attempt, the reader retries once from the latest batch. A second relevant change within the bounded retry fails closed as unverified. Missing, unreadable, insecure, malformed, ownerless, or generation-mismatched publication state also fails closed as unverified. A runtime tombstone updates the owner generation before removing a receipt, so an in-flight old receipt cannot remain verified even if its immutable snapshot still exists. + +Runtime-loaded state is aggregated by target, not by global catalog, manifest, runtime revision, or collaboration-mode label. If every verified live process proves the same expected `effectiveHash`, the target is runtime loaded even when those processes use different catalog snapshots, manifests, or runtime versions. Different proved target hashes produce **Mixed runtime-loaded state** and the diagnostic **Different loaded prompt versions**. A missing, invalid, stale, or target-incomplete snapshot produces **Runtime-loaded state unavailable**. Older global catalog or manifest revisions remain visible as diagnostics but do not create a false target conflict. + +A globally stale manifest is also reported separately when the selected target's exact loaded hash already matches its desired hash, so unrelated changes do not create a false per-target restart warning. The compatible Codex runtime regenerates `catalog.json`, an immutable process snapshot, and its receipt during configuration loading and startup; resolving a model can add its model-specific target. Context Room owns only safe updates to `overrides.json`. No `config.toml` switch is required. + +The receipt field `activeOverrides` means that the runtime successfully resolved those concrete overrides. It must be the exact ordered set derived from the referenced immutable snapshot: every target with both a non-null source target ID and a non-null runtime effective hash, with the exact `targetId`, `sourceTargetId`, and `effectiveHash`. Missing, extra, reordered, or mismatched records invalidate the receipt. It is not a record of an active collaboration mode and is not task-level delivery evidence. + +## Runtime States + +Prompt Center can show: + +- `Official loaded by runtime` +- `Effective loaded by runtime` +- `Runtime loaded` +- `Pending next launch` +- `Restart required` +- `Mixed runtime-loaded state` +- `Runtime-loaded state unavailable` +- `Loaded prompt differs` +- `Catalogued` +- `Target pattern` +- `Conflict` +- `Codex not running` + +The restart instruction is deliberately explicit: **Quit Codex completely (`⌘Q` on macOS), reopen it, then create a new task.** Closing only a window is insufficient, and an existing task may retain previously injected context. The same guidance applies to a read-only target whose loaded prompt differs from the current runtime-published effective value: Prompt Center cannot edit that authority-owned value, but a full restart is still required to load its current form. + +## API + +The loopback server exposes: + +```text +GET /api/codex-prompts +GET /api/codex-prompts/target?id=… +POST /api/codex-prompts/validate +POST /api/codex-prompts/override +DELETE /api/codex-prompts/override +POST /api/codex-prompts/refresh +``` + +The server requires a loopback peer and the active loopback `Host`. It rejects an `Origin` or `Referer` that names another origin and rejects requests explicitly marked cross-site; headerless local clients remain supported. Every mutation must still send both the random `x-context-room-prompt-nonce` and the active `x-context-room-project` identity, which prevents a stale project tab or local client from mutating through a different Context Room process. Every Context Room page also sends a `frame-ancestors` policy limited to itself and the exact loopback ports of its known parent rooms, so Context Hub nesting works without allowing wildcard or external framing. + +## Durable Contracts + +The persisted JSON contracts are versioned independently from the UI: + +- [`schemas/codex-prompt-catalog-v1.schema.json`](../../schemas/codex-prompt-catalog-v1.schema.json): `catalog.json` and immutable `..catalog.json` snapshots; +- [`schemas/codex-prompt-overrides-v1.schema.json`](../../schemas/codex-prompt-overrides-v1.schema.json): `overrides.json`; +- [`schemas/codex-prompt-publication-state-v2.schema.json`](../../schemas/codex-prompt-publication-state-v2.schema.json): read-only **.publication-state.json** registry and publication generation ownership; +- [`schemas/codex-prompt-runtime-receipt-v2.schema.json`](../../schemas/codex-prompt-runtime-receipt-v2.schema.json): hash-only `.json` receipts. + +All four schemas reject unknown top-level fields. The compatible Codex runtime validates its own target allowlist; Context Room only saves targets present in the current runtime catalog. Context Room additionally enforces relationships JSON Schema cannot express directly: the runtime-compatible typed catalog revision, Unicode scalar validity, UTF-8 byte and estimated-token limits, unique override IDs, JavaScript-safe publication generations and strict owner ordering, exact receipt-owner equality across two stable publication-state reads, the exact Darwin process-generation identity, the PID embedded in `catalogFile`, the raw snapshot hash, the logical catalog revision, and the exact ordered `activeOverrides` snapshot set. + +## Source Map + +- `src/codex_prompt_center.mjs`: catalog normalization, exact overlays, optimistic concurrency, private storage, receipts, and provider. +- `src/context_room.mjs`: injectable API routes and lazy Context Hub interface. +- `schemas/codex-prompt-*.schema.json`: strict persisted catalog, manifest, publication-state, snapshot, and receipt contracts. +- `test/codex_prompt_center.test.mjs`: synthetic catalog, overlay, storage, receipt, API, and privacy tests. +- [Context Hub](context-hub.md): global navigation and project isolation. +- [Settings](settings.md): project and preference configuration that remains separate from prompt state. diff --git a/docs/features/context-hub.md b/docs/features/context-hub.md index 5de44cf..88c3510 100644 --- a/docs/features/context-hub.md +++ b/docs/features/context-hub.md @@ -4,8 +4,8 @@ context_room: scope: context-room status: current canonical_for: global Context Hub - last_verified: 2026-07-22 - sources: [src/context_hub.mjs, src/context_room.mjs, src/shared_context.mjs, bin/context-room.mjs, docs/features/shared-context.md] + last_verified: 2026-07-23 + sources: [src/context_hub.mjs, src/context_room.mjs, src/codex_prompt_center.mjs, src/shared_context.mjs, bin/context-room.mjs, docs/features/shared-context.md, docs/features/codex-prompt-center.md] --- # Context Hub @@ -48,7 +48,7 @@ Normal `init`, `setup`, and `start` flows register their initialized project aut `hub proposals` exposes the aggregated proposal index to agents and can filter by project or Codex task ID. `hub open` prints a deep link into the running Hub with the same focus. -## Inbox And Projects +## Inbox, Projects, And Codex Prompts **Inbox** combines work that may need attention: @@ -60,6 +60,8 @@ Normal `init`, `setup`, and `start` flows register their initialized project aut Repository-wide proposal scopes appear as a dedicated **Global skills** project. They stay searchable and filterable without being duplicated under every project that consumes them. +**Codex prompts** loads a compatible installed Codex runtime's global prompt catalog on demand. It groups every runtime-published target without hardcoding mode or model names, compares official, effective-after-restart, and runtime-loaded versions, and saves exact private overlays. Runtime receipts prove local resolution by target, not mode selection or task delivery. Prompt state is not project configuration and never enters the local or shared review workflow. See [Codex Prompt Center](codex-prompt-center.md). + Keyboard shortcuts inside the Hub: - `/`: focus search; @@ -77,7 +79,8 @@ The global registry lives at `$HOME/.context-room/hub/registry.json`. The runnin ## Source Map - `src/context_hub.mjs`: global project, shared-repository, and runtime registry. -- `src/context_room.mjs`: aggregate Hub state, inbox UI, project-room isolation, and exact review embedding. +- `src/context_room.mjs`: aggregate Hub state, inbox UI, prompt-center UI, project-room isolation, and exact review embedding. +- `src/codex_prompt_center.mjs`: runtime-published prompt catalog, private overlays, and load receipts. - `src/shared_context.mjs`: shared-only repository listing, proposal lifecycle signals, and exact review materialization. - `bin/context-room.mjs`: `context-room hub` commands and automatic registration. - [Shared context](shared-context.md): proposal, acceptance, skills, freshness, and permission contracts. diff --git a/docs/features/index.md b/docs/features/index.md index b556a74..6db8a2f 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -4,8 +4,8 @@ context_room: scope: context-room status: current canonical_for: features - last_verified: 2026-07-22 - sources: [README.md, docs/product-overview.md, bin/context-room.mjs, src/context_room.mjs, src/context_hub.mjs, src/doc_agent.mjs, src/shared_context.mjs, schemas/config.schema.json] + last_verified: 2026-07-23 + sources: [README.md, docs/product-overview.md, bin/context-room.mjs, src/context_room.mjs, src/context_hub.mjs, src/codex_prompt_center.mjs, src/doc_agent.mjs, src/shared_context.mjs, schemas/config.schema.json] --- # Features @@ -16,6 +16,7 @@ This folder explains Context Room by user-facing feature. Read this when changin - [Hub and navigation](hub-and-navigation.md) - [Context Hub](context-hub.md) +- [Codex Prompt Center](codex-prompt-center.md) - [File explorer and editor](file-explorer-and-editor.md) - [HTML visual documents](html-visual-documents.md) - [Review queue](review-queue.md) diff --git a/docs/features/settings.md b/docs/features/settings.md index 5d43b4a..90014c3 100644 --- a/docs/features/settings.md +++ b/docs/features/settings.md @@ -4,8 +4,8 @@ context_room: scope: context-room status: current canonical_for: settings - last_verified: 2026-07-20 - sources: [src/context_room.mjs, schemas/config.schema.json, docs/agent-configuration.md] + last_verified: 2026-07-23 + sources: [src/context_room.mjs, src/codex_prompt_center.mjs, schemas/config.schema.json, docs/agent-configuration.md, docs/features/codex-prompt-center.md] --- # Settings @@ -14,6 +14,8 @@ context_room: Settings separates project setup from computer-wide preferences without exposing every option at once. +Codex prompts are not a Settings category. They use the global [Codex Prompt Center](codex-prompt-center.md), whose versioned runtime catalog, conflict handling, and private storage are separate from project configuration and appearance preferences. + ## Categories - `Review`: simple watched paths, structured folder watch rules, and required-review paths. diff --git a/docs/product-overview.md b/docs/product-overview.md index f0dd3ff..f32edbb 100644 --- a/docs/product-overview.md +++ b/docs/product-overview.md @@ -5,7 +5,7 @@ context_room: status: current canonical_for: product overview last_verified: 2026-07-23 - sources: [README.md, bin/context-room.mjs, src/context_room.mjs, src/context_hub.mjs, src/doc_agent.mjs, src/shared_context.mjs, schemas/config.schema.json, schemas/doc-context.schema.json, schemas/shared-repository.schema.json, docs/agent-configuration.md] + sources: [README.md, bin/context-room.mjs, src/context_room.mjs, src/codex_prompt_center.mjs, src/context_hub.mjs, src/doc_agent.mjs, src/shared_context.mjs, schemas/config.schema.json, schemas/codex-prompt-catalog-v1.schema.json, schemas/codex-prompt-overrides-v1.schema.json, schemas/codex-prompt-publication-state-v2.schema.json, schemas/codex-prompt-runtime-receipt-v2.schema.json, schemas/doc-context.schema.json, schemas/shared-repository.schema.json, docs/agent-configuration.md] --- # Product Overview @@ -28,7 +28,8 @@ Projects that need cross-project documentation or skills can add the optional [S ## Main Surfaces - Hub: card-based navigation from `hubSections`. -- Context Hub: one computer-local inbox and project switcher for local review queues and shared proposals, while each opened project keeps an isolated server and identity. +- Context Hub: one computer-local inbox and project switcher for local review queues and shared proposals, plus a lazy global Codex Prompt Center; each opened project keeps an isolated server and identity. +- Codex Prompt Center: runtime-published prompt targets with official, effective-after-restart, and runtime-loaded views; exact overrides remain private to `$CODEX_HOME`, while protected and server-owned targets stay visible and read-only. - Explorer and editor: safe project text, with editing limited by `allowedPaths` and four explicit folder watch modes. - Changed files to review: Git-backed review queue, required review paths, project `AGENTS.md` files unless implicit review is disabled, and every skill exposed by Startup skills. - Startup context: project instruction files by default, with ancestor and global discovery available by opt-in. @@ -69,6 +70,7 @@ Feature-level docs live in [Features](features/index.md). - `context_room` metadata: optional Markdown frontmatter used by `doctor`, graph health, and briefs. - `~/.context-room/shared/registry.json`: user-approved source-repository and subpath bindings for generic shared context. - `$HOME/.context-room/hub/registry.json`: local project and shared-repository catalog used by the global Context Hub. +- `$CODEX_HOME/prompt-overrides/`: private Codex-owned prompt catalog, exact overrides, hash-only runtime receipts, and immutable per-process catalog snapshots; it is never project configuration. - `/.context-room/shared-repository.json`: versioned contract for a shared repository's branch and path layout. - `schemas/doc-context.schema.json`: structured evidence contract returned by the documentation research agent. @@ -78,14 +80,19 @@ Feature-level docs live in [Features](features/index.md). - `src/context_room.mjs`: server, file access, review queue, graph, brief, UI, and API. - `src/shared_context.mjs`: shared repository sync, snapshots, skill links, proposals, review materialization, and acceptance. - `src/context_hub.mjs`: global project/shared-repository registration and single-Hub runtime discovery. +- `src/codex_prompt_center.mjs`: generic Codex prompt catalog, exact overlays, private storage, optimistic concurrency, and per-process receipt/snapshot proof. - `src/doc_agent.mjs`: documentation-only corpus, section retrieval, Codex researcher invocation, and evidence packet rendering. - `src/codex_composer_bridge.mjs`: loopback-only insertion into the active Codex composer. - `src/doc_metadata.mjs`: Markdown metadata parsing. - `src/yaml_utils.mjs`: YAML helpers. - `schemas/config.schema.json`: config contract. +- `schemas/codex-prompt-catalog-v1.schema.json`: strict runtime catalog and immutable snapshot contract. +- `schemas/codex-prompt-overrides-v1.schema.json`: strict private override-manifest contract. +- `schemas/codex-prompt-runtime-receipt-v2.schema.json`: strict hash-only runtime receipt contract. - `schemas/shared-repository.schema.json`: shared repository manifest contract. - `schemas/doc-context.schema.json`: documentation research output contract. - `test/context_room.test.mjs`: CLI, config, review, startup scanner, and UI behavior tests. +- `test/codex_prompt_center.test.mjs`: synthetic prompt catalog, overlay, storage, receipt, privacy, and API tests. - `test/shared_context.test.mjs`: shared snapshots, skills, offline fallback, proposal scope, hash expiry, and partial-acceptance tests. - `test/doc_agent.test.mjs`: documentation corpus, retrieval, provenance, prompt boundaries, and Codex invocation tests. - `docs/agent-configuration.md`: detailed config guide. diff --git a/schemas/codex-prompt-catalog-v1.schema.json b/schemas/codex-prompt-catalog-v1.schema.json new file mode 100644 index 0000000..09f8331 --- /dev/null +++ b/schemas/codex-prompt-catalog-v1.schema.json @@ -0,0 +1,1106 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://unpkg.com/context-room@latest/schemas/codex-prompt-catalog-v1.schema.json", + "title": "Codex prompt catalog v1", + "description": "Strict schema for prompt-overrides/catalog.json and immutable runtime catalog snapshots.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "runtimeVersion", + "catalogRevision", + "groups" + ], + "properties": { + "schemaVersion": { + "const": 1 + }, + "runtimeVersion": { + "type": "string" + }, + "catalogRevision": { + "$ref": "#/$defs/hash" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/$defs/group" + } + } + }, + "$defs": { + "hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "nullableHash": { + "anyOf": [ + { + "$ref": "#/$defs/hash" + }, + { + "type": "null" + } + ] + }, + "nullableString": { + "type": [ + "string", + "null" + ] + }, + "overrideConflict": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message", + "sourceTargetId" + ], + "properties": { + "code": { + "enum": [ + "official_hash_mismatch", + "strategy_mismatch", + "patch_anchor_mismatch", + "effective_prompt_too_large", + "target_became_personality_dependent" + ] + }, + "message": { + "type": "string", + "minLength": 1 + }, + "sourceTargetId": { + "type": "string", + "minLength": 1 + } + } + }, + "target": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "kind", + "editable", + "runtimeStatus", + "officialHash", + "officialText", + "effectiveHash", + "effectiveText", + "targetPattern", + "sourceTargetId", + "readOnlyReason", + "overrideStrategy", + "overrideConflict", + "source", + "securityClass" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "kind": { + "enum": [ + "model_base", + "developer", + "compact", + "collaboration", + "protected", + "server_owned" + ] + }, + "editable": { + "type": "boolean" + }, + "runtimeStatus": { + "enum": [ + "active", + "available_local_only", + "bundled", + "cached", + "catalogued", + "configured", + "selectable", + "dormant", + "override_conflict", + "pattern", + "shadowed_by_explicit_config", + "shadowed_by_session_history", + "personality_dependent", + "protected", + "server_owned" + ] + }, + "officialHash": { + "$ref": "#/$defs/nullableHash" + }, + "officialText": { + "$ref": "#/$defs/nullableString" + }, + "effectiveHash": { + "$ref": "#/$defs/nullableHash" + }, + "effectiveText": { + "$ref": "#/$defs/nullableString" + }, + "targetPattern": { + "$ref": "#/$defs/nullableString" + }, + "sourceTargetId": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + }, + "readOnlyReason": { + "$ref": "#/$defs/nullableString" + }, + "overrideStrategy": { + "type": [ + "string", + "null" + ], + "enum": [ + "patch", + "replacement", + null + ] + }, + "overrideConflict": { + "anyOf": [ + { + "$ref": "#/$defs/overrideConflict" + }, + { + "type": "null" + } + ] + }, + "source": { + "type": "string" + }, + "securityClass": { + "enum": [ + "local_user_editable", + "dormant", + "advanced_pattern", + "config_shadowed", + "session_history", + "dynamic_assembly", + "enforcement_coupled", + "security_critical", + "contract_coupled", + "state_assembled", + "protocol_coupled", + "privacy_sensitive", + "dynamic_capability", + "authority_sensitive", + "runtime_generated", + "lifecycle_managed", + "history_semantics", + "executable_contract", + "configurable_elsewhere", + "server_owned" + ] + } + }, + "allOf": [ + { + "oneOf": [ + { + "properties": { + "officialHash": { + "$ref": "#/$defs/hash" + }, + "officialText": { + "type": "string" + } + } + }, + { + "properties": { + "officialHash": { + "type": "null" + }, + "officialText": { + "type": "null" + } + } + } + ] + }, + { + "oneOf": [ + { + "properties": { + "effectiveHash": { + "$ref": "#/$defs/hash" + }, + "effectiveText": { + "type": "string" + } + } + }, + { + "properties": { + "effectiveHash": { + "type": "null" + }, + "effectiveText": { + "type": "null" + } + } + } + ] + }, + { + "if": { + "properties": { + "editable": { + "const": true + } + }, + "required": [ + "editable" + ] + }, + "then": { + "properties": { + "kind": { + "enum": [ + "model_base", + "developer", + "compact", + "collaboration" + ] + }, + "runtimeStatus": { + "enum": [ + "active", + "available_local_only", + "bundled", + "cached", + "catalogued", + "configured", + "selectable", + "override_conflict" + ] + }, + "officialHash": { + "$ref": "#/$defs/hash" + }, + "officialText": { + "type": "string" + }, + "effectiveHash": { + "$ref": "#/$defs/hash" + }, + "effectiveText": { + "type": "string" + }, + "targetPattern": { + "type": "null" + }, + "readOnlyReason": { + "type": "null" + }, + "overrideStrategy": { + "enum": [ + "patch", + "replacement" + ] + }, + "securityClass": { + "const": "local_user_editable" + } + } + } + }, + { + "if": { + "properties": { + "securityClass": { + "const": "local_user_editable" + } + }, + "required": [ + "securityClass" + ] + }, + "then": { + "properties": { + "editable": { + "const": true + }, + "kind": { + "enum": [ + "model_base", + "developer", + "compact", + "collaboration" + ] + } + } + } + }, + { + "if": { + "properties": { + "editable": { + "const": true + }, + "officialText": { + "const": "" + } + }, + "required": [ + "editable", + "officialText" + ] + }, + "then": { + "properties": { + "overrideStrategy": { + "const": "replacement" + } + } + } + }, + { + "if": { + "properties": { + "editable": { + "const": true + }, + "officialText": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "editable", + "officialText" + ] + }, + "then": { + "properties": { + "overrideStrategy": { + "const": "patch" + } + } + } + }, + { + "if": { + "properties": { + "runtimeStatus": { + "const": "override_conflict" + } + }, + "required": [ + "runtimeStatus" + ] + }, + "then": { + "properties": { + "sourceTargetId": { + "type": "null" + }, + "overrideConflict": { + "$ref": "#/$defs/overrideConflict" + } + } + }, + "else": { + "properties": { + "overrideConflict": { + "type": "null" + } + } + } + }, + { + "if": { + "anyOf": [ + { + "properties": { + "kind": { + "const": "protected" + } + }, + "required": [ + "kind" + ] + }, + { + "properties": { + "runtimeStatus": { + "const": "protected" + } + }, + "required": [ + "runtimeStatus" + ] + }, + { + "properties": { + "securityClass": { + "enum": [ + "enforcement_coupled", + "security_critical", + "contract_coupled", + "state_assembled", + "protocol_coupled", + "privacy_sensitive", + "dynamic_capability", + "authority_sensitive", + "runtime_generated", + "lifecycle_managed", + "history_semantics", + "executable_contract", + "configurable_elsewhere" + ] + } + }, + "required": [ + "securityClass" + ] + } + ] + }, + "then": { + "properties": { + "kind": { + "const": "protected" + }, + "editable": { + "const": false + }, + "runtimeStatus": { + "const": "protected" + }, + "officialHash": { + "type": "null" + }, + "officialText": { + "type": "null" + }, + "effectiveHash": { + "type": "null" + }, + "effectiveText": { + "type": "null" + }, + "targetPattern": { + "type": "null" + }, + "sourceTargetId": { + "type": "null" + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "overrideConflict": { + "type": "null" + }, + "securityClass": { + "enum": [ + "enforcement_coupled", + "security_critical", + "contract_coupled", + "state_assembled", + "protocol_coupled", + "privacy_sensitive", + "dynamic_capability", + "authority_sensitive", + "runtime_generated", + "lifecycle_managed", + "history_semantics", + "executable_contract", + "configurable_elsewhere" + ] + } + } + } + }, + { + "if": { + "anyOf": [ + { + "properties": { + "kind": { + "const": "server_owned" + } + }, + "required": [ + "kind" + ] + }, + { + "properties": { + "runtimeStatus": { + "const": "server_owned" + } + }, + "required": [ + "runtimeStatus" + ] + }, + { + "properties": { + "securityClass": { + "const": "server_owned" + } + }, + "required": [ + "securityClass" + ] + } + ] + }, + "then": { + "properties": { + "kind": { + "const": "server_owned" + }, + "editable": { + "const": false + }, + "runtimeStatus": { + "const": "server_owned" + }, + "officialHash": { + "type": "null" + }, + "officialText": { + "type": "null" + }, + "effectiveHash": { + "type": "null" + }, + "effectiveText": { + "type": "null" + }, + "targetPattern": { + "type": "null" + }, + "sourceTargetId": { + "type": "null" + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "overrideConflict": { + "type": "null" + }, + "securityClass": { + "const": "server_owned" + } + } + } + }, + { + "if": { + "anyOf": [ + { + "properties": { + "id": { + "const": "model/base/*" + } + }, + "required": [ + "id" + ] + }, + { + "properties": { + "runtimeStatus": { + "const": "pattern" + } + }, + "required": [ + "runtimeStatus" + ] + }, + { + "properties": { + "securityClass": { + "const": "advanced_pattern" + } + }, + "required": [ + "securityClass" + ] + }, + { + "properties": { + "targetPattern": { + "type": "string" + } + }, + "required": [ + "targetPattern" + ] + } + ] + }, + "then": { + "properties": { + "id": { + "const": "model/base/*" + }, + "kind": { + "const": "model_base" + }, + "editable": { + "const": false + }, + "runtimeStatus": { + "const": "pattern" + }, + "officialHash": { + "type": "null" + }, + "officialText": { + "type": "null" + }, + "effectiveHash": { + "type": "null" + }, + "effectiveText": { + "type": "null" + }, + "targetPattern": { + "const": "model/base/{modelSlug}" + }, + "sourceTargetId": { + "enum": [ + "model/base/*", + null + ] + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "overrideConflict": { + "type": "null" + }, + "securityClass": { + "const": "advanced_pattern" + } + } + } + }, + { + "if": { + "anyOf": [ + { + "properties": { + "runtimeStatus": { + "const": "shadowed_by_explicit_config" + } + }, + "required": [ + "runtimeStatus" + ] + }, + { + "properties": { + "securityClass": { + "const": "config_shadowed" + } + }, + "required": [ + "securityClass" + ] + } + ] + }, + "then": { + "properties": { + "kind": { + "enum": [ + "model_base", + "developer", + "compact" + ] + }, + "editable": { + "const": false + }, + "runtimeStatus": { + "const": "shadowed_by_explicit_config" + }, + "officialHash": { + "$ref": "#/$defs/hash" + }, + "officialText": { + "type": "string" + }, + "effectiveHash": { + "$ref": "#/$defs/hash" + }, + "effectiveText": { + "type": "string" + }, + "targetPattern": { + "type": "null" + }, + "sourceTargetId": { + "type": "null" + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "overrideConflict": { + "type": "null" + }, + "securityClass": { + "const": "config_shadowed" + } + } + } + }, + { + "if": { + "anyOf": [ + { + "properties": { + "runtimeStatus": { + "const": "dormant" + } + }, + "required": [ + "runtimeStatus" + ] + }, + { + "properties": { + "securityClass": { + "const": "dormant" + } + }, + "required": [ + "securityClass" + ] + } + ] + }, + "then": { + "properties": { + "kind": { + "const": "collaboration" + }, + "editable": { + "const": false + }, + "runtimeStatus": { + "const": "dormant" + }, + "targetPattern": { + "type": "null" + }, + "sourceTargetId": { + "type": "null" + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "overrideConflict": { + "type": "null" + }, + "securityClass": { + "const": "dormant" + } + } + } + }, + { + "if": { + "anyOf": [ + { + "properties": { + "runtimeStatus": { + "const": "shadowed_by_session_history" + } + }, + "required": [ + "runtimeStatus" + ] + }, + { + "properties": { + "securityClass": { + "const": "session_history" + } + }, + "required": [ + "securityClass" + ] + } + ] + }, + "then": { + "properties": { + "kind": { + "const": "model_base" + }, + "editable": { + "const": false + }, + "runtimeStatus": { + "const": "shadowed_by_session_history" + }, + "officialHash": { + "$ref": "#/$defs/hash" + }, + "officialText": { + "type": "string" + }, + "effectiveHash": { + "$ref": "#/$defs/hash" + }, + "effectiveText": { + "type": "string" + }, + "targetPattern": { + "type": "null" + }, + "sourceTargetId": { + "type": "null" + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "overrideConflict": { + "type": "null" + }, + "securityClass": { + "const": "session_history" + } + } + } + }, + { + "if": { + "properties": { + "runtimeStatus": { + "const": "personality_dependent" + } + }, + "required": [ + "runtimeStatus" + ] + }, + "then": { + "properties": { + "kind": { + "const": "model_base" + }, + "editable": { + "const": false + }, + "officialHash": { + "$ref": "#/$defs/hash" + }, + "officialText": { + "type": "string" + }, + "effectiveHash": { + "$ref": "#/$defs/hash" + }, + "effectiveText": { + "type": "string" + }, + "targetPattern": { + "type": "null" + }, + "sourceTargetId": { + "type": "null" + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "overrideConflict": { + "type": "null" + }, + "securityClass": { + "const": "dynamic_assembly" + } + } + } + }, + { + "if": { + "properties": { + "securityClass": { + "const": "dynamic_assembly" + } + }, + "required": [ + "securityClass" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "runtimeStatus": { + "const": "personality_dependent" + } + }, + "required": [ + "runtimeStatus" + ] + }, + { + "properties": { + "runtimeStatus": { + "const": "override_conflict" + }, + "overrideConflict": { + "type": "object", + "properties": { + "code": { + "const": "target_became_personality_dependent" + } + }, + "required": [ + "code" + ] + } + }, + "required": [ + "runtimeStatus", + "overrideConflict" + ] + } + ] + } + }, + { + "if": { + "properties": { + "overrideConflict": { + "type": "object", + "properties": { + "code": { + "const": "target_became_personality_dependent" + } + }, + "required": [ + "code" + ] + } + }, + "required": [ + "overrideConflict" + ] + }, + "then": { + "properties": { + "kind": { + "const": "model_base" + }, + "editable": { + "const": false + }, + "officialHash": { + "$ref": "#/$defs/hash" + }, + "officialText": { + "type": "string" + }, + "effectiveHash": { + "$ref": "#/$defs/hash" + }, + "effectiveText": { + "type": "string" + }, + "targetPattern": { + "type": "null" + }, + "readOnlyReason": { + "type": "string", + "minLength": 1 + }, + "overrideStrategy": { + "type": "null" + }, + "securityClass": { + "const": "dynamic_assembly" + } + } + } + } + ] + }, + "group": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "targets" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/$defs/target" + } + } + } + } + }, + "$comment": "Prompt strings are additionally limited by the runtime to 28672 UTF-8 bytes and approximately 8192 tokens; JSON Schema cannot express UTF-8 byte length." +} diff --git a/schemas/codex-prompt-overrides-v1.schema.json b/schemas/codex-prompt-overrides-v1.schema.json new file mode 100644 index 0000000..b9fb131 --- /dev/null +++ b/schemas/codex-prompt-overrides-v1.schema.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://unpkg.com/context-room@latest/schemas/codex-prompt-overrides-v1.schema.json", + "title": "Codex prompt overrides v1", + "description": "Strict schema for prompt-overrides/overrides.json.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "revision", + "overrides" + ], + "properties": { + "schemaVersion": { + "const": 1 + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "overrides": { + "type": "array", + "items": { + "$ref": "#/$defs/override" + } + } + }, + "$defs": { + "hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "patch": { + "type": "object", + "additionalProperties": false, + "required": [ + "before", + "after", + "expectedMatches" + ], + "properties": { + "before": { + "type": "string", + "minLength": 1 + }, + "after": { + "type": "string" + }, + "expectedMatches": { + "const": 1 + } + } + }, + "override": { + "type": "object", + "additionalProperties": false, + "required": [ + "targetId", + "patches", + "replacement" + ], + "properties": { + "targetId": { + "type": "string", + "minLength": 1 + }, + "officialHash": { + "anyOf": [ + { + "$ref": "#/$defs/hash" + }, + { + "type": "null" + } + ] + }, + "patches": { + "type": "array", + "items": { + "$ref": "#/$defs/patch" + } + }, + "replacement": { + "type": [ + "string", + "null" + ] + } + }, + "allOf": [ + { + "oneOf": [ + { + "properties": { + "patches": { + "type": "array", + "minItems": 1 + }, + "replacement": { + "type": "null" + } + } + }, + { + "properties": { + "patches": { + "type": "array", + "maxItems": 0 + }, + "replacement": { + "type": "string" + } + } + } + ] + }, + { + "if": { + "properties": { + "targetId": { + "const": "model/base/*" + } + }, + "required": [ + "targetId" + ] + }, + "then": { + "properties": { + "officialHash": { + "type": "null" + }, + "replacement": { + "type": "null" + } + } + }, + "else": { + "required": [ + "officialHash" + ], + "properties": { + "officialHash": { + "$ref": "#/$defs/hash" + } + } + } + } + ] + } + }, + "$comment": "Each prompt string and every resolved effective prompt is additionally limited to 28672 UTF-8 bytes and approximately 8192 tokens. The runtime also validates the targetId allowlist and unique target IDs." +} diff --git a/schemas/codex-prompt-publication-state-v2.schema.json b/schemas/codex-prompt-publication-state-v2.schema.json new file mode 100644 index 0000000..9ec4642 --- /dev/null +++ b/schemas/codex-prompt-publication-state-v2.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://unpkg.com/context-room@latest/schemas/codex-prompt-publication-state-v2.schema.json", + "title": "Codex prompt publication state v2", + "description": "Strict schema for prompt-overrides/.publication-state.json.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "nextGeneration", + "globalOwnerGeneration", + "runtimeRegistryGenerations", + "runtimeOwnerGenerations" + ], + "properties": { + "schemaVersion": { + "const": 2 + }, + "nextGeneration": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "globalOwnerGeneration": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "runtimeRegistryGenerations": { + "type": "object", + "propertyNames": { + "pattern": "^[1-9][0-9]{0,9}$" + }, + "additionalProperties": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "runtimeOwnerGenerations": { + "type": "object", + "propertyNames": { + "pattern": "^[1-9][0-9]{0,9}$" + }, + "additionalProperties": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + } + }, + "$comment": "Context Room additionally requires runtimeRegistryGenerations and runtimeOwnerGenerations to have exactly the same keys; every key must be the canonical decimal representation of a non-zero u32 PID. globalOwnerGeneration must be lower than nextGeneration. Every runtime generation must be at least 1 and lower than nextGeneration, with runtimeRegistryGenerations[pid] less than or equal to runtimeOwnerGenerations[pid]. Batch receipt verification keeps only the owner-generation vector for PIDs with verified receipt proofs stable; registry generations are validated structurally but are not receipt-freshness keys. nextGeneration, globalOwnerGeneration, registry generations, and unrelated PID owners may advance independently. A conforming publisher allocates a fresh owner generation before every receipt publication or tombstone." +} diff --git a/schemas/codex-prompt-runtime-receipt-v2.schema.json b/schemas/codex-prompt-runtime-receipt-v2.schema.json new file mode 100644 index 0000000..080bbc8 --- /dev/null +++ b/schemas/codex-prompt-runtime-receipt-v2.schema.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://unpkg.com/context-room@latest/schemas/codex-prompt-runtime-receipt-v2.schema.json", + "title": "Codex prompt runtime receipt v2", + "description": "Strict schema for prompt-overrides/runtime/.json.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "pid", + "publicationGeneration", + "processStartIdentity", + "loadedAtUnixMs", + "runtimeVersion", + "manifestRevision", + "manifestHash", + "catalogFile", + "catalogHash", + "catalogRevision", + "activeOverrides" + ], + "properties": { + "schemaVersion": { + "const": 2 + }, + "pid": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "publicationGeneration": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "processStartIdentity": { + "type": "string", + "pattern": "^darwin-proc-bsdinfo-v1:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[1-9][0-9]*:[0-9]{6}$" + }, + "loadedAtUnixMs": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "runtimeVersion": { + "type": "string" + }, + "manifestRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "manifestHash": { + "anyOf": [ + { + "$ref": "#/$defs/hash" + }, + { + "type": "null" + } + ] + }, + "catalogFile": { + "type": "string", + "pattern": "^[1-9][0-9]*\\.[0-9a-f]{64}\\.catalog\\.json$" + }, + "catalogHash": { + "$ref": "#/$defs/hash" + }, + "catalogRevision": { + "$ref": "#/$defs/hash" + }, + "activeOverrides": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/activeOverride" + } + } + }, + "$defs": { + "hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "activeOverride": { + "type": "object", + "additionalProperties": false, + "required": [ + "targetId", + "sourceTargetId", + "effectiveHash" + ], + "properties": { + "targetId": { + "type": "string", + "minLength": 1 + }, + "sourceTargetId": { + "type": "string", + "minLength": 1 + }, + "effectiveHash": { + "$ref": "#/$defs/hash" + } + } + } + }, + "$comment": "publicationGeneration must exactly equal runtimeOwnerGenerations[String(pid)] in the relevant stable owner vector from codex-prompt-publication-state-v2. runtimeRegistryGenerations is validated structurally but is not a receipt-freshness key. Receipt bytes are immutable for one publicationGeneration; every publication, replacement, or tombstone requires a fresh owner generation. processStartIdentity must equal the lowercase kern.bootsessionuuid plus the exact Darwin proc_pidinfo(PROC_PIDTBSDINFO) start tuple before and after snapshot validation, and the complete represented PID-to-identity set must still match during final batch verification. Receipt bytes and publication generations are reread byte-exactly before aggregation. catalogFile must contain the same PID as pid and the raw SHA-256 hex from catalogHash. The immutable snapshot uses codex-prompt-catalog-v1.schema.json." +} diff --git a/src/codex_prompt_center.mjs b/src/codex_prompt_center.mjs new file mode 100644 index 0000000..3288dba --- /dev/null +++ b/src/codex_prompt_center.mjs @@ -0,0 +1,3888 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createHash, randomBytes } from "node:crypto"; +import { execFileSync } from "node:child_process"; + +export const CODEX_PROMPT_PROTOCOL_VERSION = 1; +export const CODEX_PROMPT_RECEIPT_VERSION = 2; +export const CODEX_PROMPT_PUBLICATION_STATE_VERSION = 2; +export const MAX_CODEX_PROMPT_BYTES = 28 * 1024; +export const MAX_CODEX_PROMPT_ESTIMATED_TOKENS = 8_192; +export const CODEX_PROMPT_HIGH_CONTEXT_CONFIRM_TOKENS = 1_000; +export const MAX_CODEX_PROMPT_MANIFEST_BYTES = 16 * 1_048_576; +export const MAX_CODEX_PROMPT_REQUEST_BYTES = 8 * MAX_CODEX_PROMPT_BYTES + 65_536; +export const CODEX_PROMPT_RESTART_MESSAGE = "Quit Codex completely (⌘Q on macOS), reopen it, then create a new task."; + +const CATALOG_FILE = "catalog.json"; +const OVERRIDES_FILE = "overrides.json"; +const LAST_KNOWN_GOOD_FILE = "last-known-good.json"; +const PUBLICATION_STATE_FILE = ".publication-state.json"; +const RUNTIME_DIR = "runtime"; +const WRITE_LOCK_FILE = ".context-room-write.lock"; +const WRITE_LOCK_OWNER_FILE = "owner.json"; +const WRITE_LOCK_RECLAIM_FILE = ".reclaim"; +const INCOMPLETE_WRITE_LOCK_GRACE_MS = 30_000; +const WRITE_LOCK_RECORD_MAX_BYTES = 4_096; +const WRITE_LOCK_PROCESS_START_FS_TOLERANCE_MS = 2_000; +const MAX_RUNTIME_RECEIPT_READ_ATTEMPTS = 2; +const MAX_CODEX_PROMPT_PUBLICATION_STATE_BYTES = 1_048_576; +const DARWIN_PROCESS_START_IDENTITY = /^darwin-proc-bsdinfo-v1:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):([1-9]\d*):([0-9]{6})$/; +const DARWIN_PROCESS_START_IDENTITY_SCRIPT = String.raw` +import ctypes +import sys +import uuid + +PROC_PIDTBSDINFO = 3 +MAXCOMLEN = 16 +MAX_BOOT_SESSION_UUID_BYTES = 128 + +class ProcBsdInfo(ctypes.Structure): + _fields_ = [ + ("pbi_flags", ctypes.c_uint32), + ("pbi_status", ctypes.c_uint32), + ("pbi_xstatus", ctypes.c_uint32), + ("pbi_pid", ctypes.c_uint32), + ("pbi_ppid", ctypes.c_uint32), + ("pbi_uid", ctypes.c_uint32), + ("pbi_gid", ctypes.c_uint32), + ("pbi_ruid", ctypes.c_uint32), + ("pbi_rgid", ctypes.c_uint32), + ("pbi_svuid", ctypes.c_uint32), + ("pbi_svgid", ctypes.c_uint32), + ("rfu_1", ctypes.c_uint32), + ("pbi_comm", ctypes.c_char * MAXCOMLEN), + ("pbi_name", ctypes.c_char * (2 * MAXCOMLEN)), + ("pbi_nfiles", ctypes.c_uint32), + ("pbi_pgid", ctypes.c_uint32), + ("pbi_pjobc", ctypes.c_uint32), + ("e_tdev", ctypes.c_uint32), + ("e_tpgid", ctypes.c_uint32), + ("pbi_nice", ctypes.c_int32), + ("pbi_start_tvsec", ctypes.c_uint64), + ("pbi_start_tvusec", ctypes.c_uint64), + ] + +def validate_process_info(pid, read_size, expected_size, info): + if ( + read_size != expected_size + or info.pbi_pid != pid + or info.pbi_start_tvsec <= 0 + or info.pbi_start_tvusec >= 1000000 + ): + raise SystemExit(1) + return info.pbi_start_tvsec, info.pbi_start_tvusec + +def canonical_boot_session_uuid(raw_bytes): + try: + raw_text = raw_bytes.decode("ascii") + canonical = str(uuid.UUID(raw_text)) + except (UnicodeDecodeError, ValueError, AttributeError): + raise SystemExit(1) + if raw_text.lower() != canonical: + raise SystemExit(1) + return canonical + +def read_process_start(pid): + info = ProcBsdInfo() + libproc = ctypes.CDLL("/usr/lib/libproc.dylib", use_errno=True) + libproc.proc_pidinfo.argtypes = [ + ctypes.c_int, + ctypes.c_int, + ctypes.c_uint64, + ctypes.c_void_p, + ctypes.c_int, + ] + libproc.proc_pidinfo.restype = ctypes.c_int + expected_size = ctypes.sizeof(info) + read_size = libproc.proc_pidinfo( + pid, + PROC_PIDTBSDINFO, + 0, + ctypes.byref(info), + expected_size, + ) + return validate_process_info(pid, read_size, expected_size, info) + +def read_boot_session_uuid(libc=None, create_buffer=ctypes.create_string_buffer): + if libc is None: + libc = ctypes.CDLL(None, use_errno=True) + libc.sysctlbyname.argtypes = [ + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_size_t), + ctypes.c_void_p, + ctypes.c_size_t, + ] + libc.sysctlbyname.restype = ctypes.c_int + boot_name = b"kern.bootsessionuuid" + boot_size = ctypes.c_size_t() + if libc.sysctlbyname(boot_name, None, ctypes.byref(boot_size), None, 0) != 0: + raise SystemExit(1) + capacity = boot_size.value + if capacity <= 0 or capacity > MAX_BOOT_SESSION_UUID_BYTES: + raise SystemExit(1) + boot_buffer = create_buffer(capacity) + returned_size = ctypes.c_size_t(capacity) + if ( + libc.sysctlbyname( + boot_name, + boot_buffer, + ctypes.byref(returned_size), + None, + 0, + ) != 0 + or returned_size.value <= 0 + or returned_size.value > capacity + ): + raise SystemExit(1) + raw_bytes = bytes(boot_buffer.raw[:returned_size.value]) + if raw_bytes.endswith(b"\0"): + raw_bytes = raw_bytes[:-1] + return canonical_boot_session_uuid(raw_bytes) + +def main(): + try: + pid = int(sys.argv[1]) + except (IndexError, TypeError, ValueError): + raise SystemExit(1) + if pid <= 0: + raise SystemExit(1) + start_tvsec, start_tvusec = read_process_start(pid) + boot_session = read_boot_session_uuid() + print( + f"darwin-proc-bsdinfo-v1:{boot_session}:" + f"{start_tvsec}:{start_tvusec:06d}" + ) + +if __name__ == "__main__": + main() +`; +const SECURE_READ_FLAGS = fs.constants.O_RDONLY + | (fs.constants.O_NOFOLLOW || 0) + | (fs.constants.O_CLOEXEC || 0) + | (fs.constants.O_NONBLOCK || 0); +const CATALOG_ROOT_KEYS = ["schemaVersion", "runtimeVersion", "catalogRevision", "groups"]; +const CATALOG_GROUP_KEYS = ["id", "label", "targets"]; +const CATALOG_TARGET_KEYS = [ + "id", + "label", + "kind", + "editable", + "runtimeStatus", + "officialHash", + "officialText", + "effectiveHash", + "effectiveText", + "targetPattern", + "sourceTargetId", + "readOnlyReason", + "overrideStrategy", + "overrideConflict", + "source", + "securityClass", +]; +const CATALOG_OVERRIDE_CONFLICT_KEYS = ["code", "message", "sourceTargetId"]; +const CATALOG_KINDS = new Set([ + "model_base", + "developer", + "compact", + "collaboration", + "protected", + "server_owned", +]); +const CATALOG_RUNTIME_STATUSES = new Set([ + "active", + "available_local_only", + "bundled", + "cached", + "catalogued", + "configured", + "selectable", + "dormant", + "override_conflict", + "pattern", + "shadowed_by_explicit_config", + "shadowed_by_session_history", + "personality_dependent", + "protected", + "server_owned", +]); +const CATALOG_SECURITY_CLASSES = new Set([ + "local_user_editable", + "dormant", + "advanced_pattern", + "config_shadowed", + "session_history", + "dynamic_assembly", + "enforcement_coupled", + "security_critical", + "contract_coupled", + "state_assembled", + "protocol_coupled", + "privacy_sensitive", + "dynamic_capability", + "authority_sensitive", + "runtime_generated", + "lifecycle_managed", + "history_semantics", + "executable_contract", + "configurable_elsewhere", + "server_owned", +]); +const CATALOG_OVERRIDE_CONFLICT_CODES = new Set([ + "official_hash_mismatch", + "strategy_mismatch", + "patch_anchor_mismatch", + "effective_prompt_too_large", + "target_became_personality_dependent", +]); +const EDITABLE_KINDS = new Set([ + "model_base", + "developer", + "compact", + "collaboration", +]); +const EDITABLE_RUNTIME_STATUSES = new Set([ + "active", + "available_local_only", + "bundled", + "cached", + "catalogued", + "configured", + "selectable", + "override_conflict", +]); +const PROTECTED_SECURITY_CLASSES = new Set([ + "enforcement_coupled", + "security_critical", + "contract_coupled", + "state_assembled", + "protocol_coupled", + "privacy_sensitive", + "dynamic_capability", + "authority_sensitive", + "runtime_generated", + "lifecycle_managed", + "history_semantics", + "executable_contract", + "configurable_elsewhere", +]); +function promptCenterError(message, statusCode = 400, code = "codex_prompt_request_failed", details = undefined) { + const error = new Error(message); + error.statusCode = statusCode; + error.code = code; + if (details !== undefined) error.details = details; + return error; +} + +function assertExactObjectShape(value, expectedKeys, label, { + statusCode = 503, + code = "codex_prompt_invalid_catalog", + optionalKeys = [], +} = {}) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw promptCenterError(`${label} must be an object`, statusCode, code); + } + const keys = Object.keys(value); + const missing = expectedKeys.filter((key) => !Object.hasOwn(value, key)); + const allowed = new Set([...expectedKeys, ...optionalKeys]); + const unknown = keys.filter((key) => !allowed.has(key)); + if (missing.length || unknown.length) { + throw promptCenterError( + `${label} has an invalid shape` + + (missing.length ? `; missing: ${missing.join(", ")}` : "") + + (unknown.length ? `; unsupported: ${unknown.join(", ")}` : ""), + statusCode, + code, + ); + } +} + +function sha256(value) { + const hash = createHash("sha256"); + if (Buffer.isBuffer(value)) hash.update(value); + else hash.update(String(value), "utf8"); + return `sha256:${hash.digest("hex")}`; +} + +function isSha256(value) { + return /^sha256:[0-9a-f]{64}$/.test(String(value || "")); +} + +function isDarwinProcessStartIdentity(value) { + return DARWIN_PROCESS_START_IDENTITY.test(String(value || "")); +} + +function stableJson(value) { + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +function hasLoneUtf16Surrogate(value) { + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index); + if (code >= 0xd800 && code <= 0xdbff) { + const next = value.charCodeAt(index + 1); + if (!(next >= 0xdc00 && next <= 0xdfff)) return true; + index += 1; + } else if (code >= 0xdc00 && code <= 0xdfff) { + return true; + } + } + return false; +} + +function assertUnicodeScalarStrings(value, label, { + statusCode = 422, + code = "codex_prompt_invalid_unicode", +} = {}) { + if (typeof value === "string") { + if (hasLoneUtf16Surrogate(value)) { + throw promptCenterError( + `${label} contains an unpaired UTF-16 surrogate`, + statusCode, + code, + ); + } + return; + } + if (Array.isArray(value)) { + value.forEach((item) => assertUnicodeScalarStrings(item, label, { statusCode, code })); + return; + } + if (value && typeof value === "object") { + Object.entries(value).forEach(([key, item]) => { + assertUnicodeScalarStrings(key, label, { statusCode, code }); + assertUnicodeScalarStrings(item, label, { statusCode, code }); + }); + } +} + +function byteLength(value) { + return Buffer.byteLength(String(value), "utf8"); +} + +export function estimateCodexPromptTokens(value) { + return Math.ceil(byteLength(value) / 4); +} + +function canonicalCatalogTarget(rawTarget = {}) { + return { + id: rawTarget.id, + label: rawTarget.label, + kind: rawTarget.kind, + editable: rawTarget.editable, + runtimeStatus: rawTarget.runtimeStatus, + officialHash: rawTarget.officialHash, + officialText: rawTarget.officialText, + effectiveHash: rawTarget.effectiveHash, + effectiveText: rawTarget.effectiveText, + targetPattern: rawTarget.targetPattern, + sourceTargetId: rawTarget.sourceTargetId, + readOnlyReason: rawTarget.readOnlyReason, + overrideStrategy: rawTarget.overrideStrategy, + overrideConflict: rawTarget.overrideConflict === null || rawTarget.overrideConflict === undefined + ? rawTarget.overrideConflict + : { + code: rawTarget.overrideConflict.code, + message: rawTarget.overrideConflict.message, + sourceTargetId: rawTarget.overrideConflict.sourceTargetId, + }, + source: rawTarget.source, + securityClass: rawTarget.securityClass, + }; +} + +export function computeCodexPromptCatalogRevision(rawCatalog) { + if (!rawCatalog || typeof rawCatalog !== "object" || Array.isArray(rawCatalog)) return ""; + const canonical = { + schemaVersion: rawCatalog.schemaVersion, + runtimeVersion: rawCatalog.runtimeVersion, + catalogRevision: "", + groups: Array.isArray(rawCatalog.groups) + ? rawCatalog.groups.map((group) => ({ + id: group?.id, + label: group?.label, + targets: Array.isArray(group?.targets) + ? group.targets.map(canonicalCatalogTarget) + : group?.targets, + })) + : rawCatalog.groups, + }; + return sha256(JSON.stringify(canonical)); +} + +function serializePromptState(value) { + assertUnicodeScalarStrings(value, "Codex prompt state"); + return `${JSON.stringify(value, null, 2)}\n`; +} + +function assertPromptSize(value, label = "Prompt") { + const size = byteLength(value); + if (size > MAX_CODEX_PROMPT_BYTES) { + throw promptCenterError( + `${label} exceeds the ${MAX_CODEX_PROMPT_BYTES}-byte limit`, + 413, + "codex_prompt_too_large", + { maxBytes: MAX_CODEX_PROMPT_BYTES, actualBytes: size }, + ); + } + const estimatedTokens = estimateCodexPromptTokens(value); + if (estimatedTokens > MAX_CODEX_PROMPT_ESTIMATED_TOKENS) { + throw promptCenterError( + `${label} is estimated at ${estimatedTokens} tokens, exceeding the ${MAX_CODEX_PROMPT_ESTIMATED_TOKENS}-token limit`, + 413, + "codex_prompt_too_many_estimated_tokens", + { + maxEstimatedTokens: MAX_CODEX_PROMPT_ESTIMATED_TOKENS, + estimatedTokens, + }, + ); + } +} + +function normalizePrompt(value, label) { + if (typeof value !== "string") { + throw promptCenterError(`${label} must be text`, 422, "codex_prompt_invalid_catalog"); + } + assertUnicodeScalarStrings(value, label); + assertPromptSize(value, label); + return value; +} + +function readBoundedFileDescriptor(descriptor, maxBytes) { + const chunks = []; + let totalBytes = 0; + while (totalBytes <= maxBytes) { + const remaining = maxBytes + 1 - totalBytes; + const chunk = Buffer.allocUnsafe(Math.min(64 * 1_024, remaining)); + const bytesRead = fs.readSync(descriptor, chunk, 0, chunk.length, null); + if (bytesRead === 0) break; + chunks.push(chunk.subarray(0, bytesRead)); + totalBytes += bytesRead; + } + return { + bytes: Buffer.concat(chunks, totalBytes), + tooLarge: totalBytes > maxBytes, + }; +} + +function readJsonFile(filePath, { + required = true, + maxBytes = MAX_CODEX_PROMPT_MANIFEST_BYTES, + includeBytes = false, + requiredMode = null, +} = {}) { + let descriptor; + try { + descriptor = fs.openSync(filePath, SECURE_READ_FLAGS); + } catch (error) { + if (!required && error.code === "ENOENT") return null; + if (error.code === "ENOENT") { + throw promptCenterError( + `Codex prompt data is unavailable: ${path.basename(filePath)} was not found`, + 503, + "codex_prompt_data_unavailable", + ); + } + if (["ELOOP", "EMLINK"].includes(error.code)) { + throw promptCenterError("Codex prompt storage must not contain symbolic links", 409, "codex_prompt_symlink_refused"); + } + throw error; + } + try { + const stat = fs.fstatSync(descriptor); + if (!stat.isFile()) { + throw promptCenterError(`Codex prompt data is not a regular file: ${path.basename(filePath)}`, 409, "codex_prompt_storage_invalid"); + } + if (requiredMode !== null && (stat.mode & 0o777) !== requiredMode) { + throw promptCenterError( + `Codex prompt data must use mode 0${requiredMode.toString(8)}: ${path.basename(filePath)}`, + 409, + "codex_prompt_storage_permissions_invalid", + ); + } + if (stat.size > maxBytes) { + throw promptCenterError( + `Codex prompt data exceeds the ${Math.floor(maxBytes / 1_048_576)} MiB file limit`, + 413, + "codex_prompt_data_too_large", + ); + } + const { bytes, tooLarge } = readBoundedFileDescriptor(descriptor, maxBytes); + if (tooLarge) { + throw promptCenterError( + `Codex prompt data exceeds the ${Math.floor(maxBytes / 1_048_576)} MiB file limit`, + 413, + "codex_prompt_data_too_large", + ); + } + let text; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch (error) { + throw promptCenterError( + `Codex prompt data is not valid UTF-8: ${path.basename(filePath)}`, + 503, + "codex_prompt_data_invalid", + { cause: error.message }, + ); + } + const value = JSON.parse(text); + return includeBytes ? { value, bytes, stat } : value; + } catch (error) { + if (typeof error?.code === "string" && error.code.startsWith("codex_prompt_")) throw error; + throw promptCenterError( + `Codex prompt data is invalid JSON: ${path.basename(filePath)}`, + 503, + "codex_prompt_data_invalid", + { cause: error.message }, + ); + } finally { + fs.closeSync(descriptor); + } +} + +function pathIsWithin(anchorPath, targetPath) { + const relative = path.relative(anchorPath, targetPath); + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); +} + +function promptStorageTrustAnchor(targetPath) { + const resolved = path.resolve(targetPath); + const trusted = [os.homedir(), os.tmpdir()] + .map((candidate) => path.resolve(candidate)) + .filter((candidate) => pathIsWithin(candidate, resolved)) + .sort((left, right) => right.length - left.length); + return trusted[0] || path.parse(resolved).root; +} + +function existingPathSegments(targetPath) { + const resolved = path.resolve(targetPath); + const anchor = promptStorageTrustAnchor(resolved); + const relative = path.relative(anchor, resolved); + const segments = relative ? relative.split(path.sep).filter(Boolean) : []; + const paths = [anchor]; + let current = anchor; + for (const segment of segments) { + current = path.join(current, segment); + paths.push(current); + } + return paths; +} + +export function assertNoPromptStorageSymlinks(targetPath) { + const candidates = existingPathSegments(targetPath); + for (const candidate of candidates) { + let stat; + try { + stat = fs.lstatSync(candidate); + } catch (error) { + if (error.code === "ENOENT") break; + throw error; + } + if (stat.isSymbolicLink()) { + throw promptCenterError( + `Codex prompt storage refuses symbolic links: ${candidate}`, + 409, + "codex_prompt_symlink_refused", + ); + } + } +} + +function ensurePrivateDirectory(directoryPath) { + assertNoPromptStorageSymlinks(directoryPath); + fs.mkdirSync(directoryPath, { recursive: true, mode: 0o700 }); + assertNoPromptStorageSymlinks(directoryPath); + const stat = fs.lstatSync(directoryPath); + if (!stat.isDirectory()) { + throw promptCenterError("Codex prompt storage root is not a directory", 409, "codex_prompt_storage_invalid"); + } + fs.chmodSync(directoryPath, 0o700); +} + +function assertPrivateDirectory(directoryPath, label, { required = true } = {}) { + let stat; + try { + stat = fs.lstatSync(directoryPath); + } catch (error) { + if (!required && error.code === "ENOENT") return false; + throw error; + } + if (stat.isSymbolicLink() || !stat.isDirectory()) { + throw promptCenterError(`${label} must be a regular directory`, 409, "codex_prompt_storage_invalid"); + } + if ((stat.mode & 0o777) !== 0o700) { + throw promptCenterError(`${label} must use mode 0700`, 409, "codex_prompt_storage_permissions_invalid"); + } + return true; +} + +export function secureAtomicWritePromptState(filePath, value) { + const directoryPath = path.dirname(filePath); + ensurePrivateDirectory(directoryPath); + assertNoPromptStorageSymlinks(filePath); + if (fs.existsSync(filePath) && !fs.lstatSync(filePath).isFile()) { + throw promptCenterError("Codex prompt state target is not a regular file", 409, "codex_prompt_storage_invalid"); + } + const serialized = serializePromptState(value); + const serializedBytes = byteLength(serialized); + if (serializedBytes > MAX_CODEX_PROMPT_MANIFEST_BYTES) { + throw promptCenterError( + "Codex prompt state exceeds the 16 MiB runtime manifest limit", + 413, + "codex_prompt_manifest_too_large", + { maxBytes: MAX_CODEX_PROMPT_MANIFEST_BYTES, actualBytes: serializedBytes }, + ); + } + const temporaryPath = path.join( + directoryPath, + `.${path.basename(filePath)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`, + ); + let descriptor; + let writeError = null; + try { + descriptor = fs.openSync(temporaryPath, "wx", 0o600); + fs.writeFileSync(descriptor, serialized, "utf8"); + fs.fsyncSync(descriptor); + } catch (error) { + writeError = error; + } + if (descriptor !== undefined) { + try { + fs.closeSync(descriptor); + } catch (error) { + if (!writeError) writeError = error; + } + } + if (writeError) { + try { + fs.unlinkSync(temporaryPath); + } catch (cleanupError) { + if (cleanupError.code !== "ENOENT") { + writeError.cleanupWarning = `Temporary prompt state cleanup failed: ${cleanupError.message}`; + } + } + throw writeError; + } + try { + fs.renameSync(temporaryPath, filePath); + } catch (error) { + try { + fs.unlinkSync(temporaryPath); + } catch (cleanupError) { + if (cleanupError.code !== "ENOENT") { + error.cleanupWarning = `Temporary prompt state cleanup failed: ${cleanupError.message}`; + } + } + throw error; + } + + const warnings = []; + try { + fs.chmodSync(filePath, 0o600); + } catch (error) { + warnings.push(`Prompt state committed, but permission finalization failed: ${error.message}`); + } + let directoryDescriptor; + try { + directoryDescriptor = fs.openSync(directoryPath, "r"); + fs.fsyncSync(directoryDescriptor); + } catch (error) { + warnings.push(`Prompt state committed, but directory durability sync failed: ${error.message}`); + } + if (directoryDescriptor !== undefined) { + try { + fs.closeSync(directoryDescriptor); + } catch (error) { + warnings.push(`Prompt state committed, but directory handle cleanup failed: ${error.message}`); + } + } + return { + filePath, + committed: true, + commitWarning: warnings.join(" "), + }; +} + +function promptWriteLockIdentity(stat) { + return `${String(stat.dev)}-${String(stat.ino)}`; +} + +function readPromptWriteLockRecord(recordPath, label) { + let descriptor; + try { + descriptor = fs.openSync(recordPath, SECURE_READ_FLAGS); + } catch (error) { + if (error.code === "ENOENT") return null; + if (["ELOOP", "EMLINK"].includes(error.code)) { + throw promptCenterError(`${label} is invalid`, 409, "codex_prompt_symlink_refused"); + } + throw error; + } + try { + const stat = fs.fstatSync(descriptor); + if (!stat.isFile() || (stat.mode & 0o777) !== 0o600 || stat.size > WRITE_LOCK_RECORD_MAX_BYTES) { + throw promptCenterError(`${label} is invalid`, 409, "codex_prompt_storage_invalid"); + } + let record = null; + try { + const { bytes, tooLarge } = readBoundedFileDescriptor(descriptor, WRITE_LOCK_RECORD_MAX_BYTES); + if (tooLarge) throw new SyntaxError("write-lock record is too large"); + const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + if ( + parsed + && typeof parsed === "object" + && !Array.isArray(parsed) + && Object.keys(parsed).length === 3 + && Number.isSafeInteger(parsed.pid) + && parsed.pid > 0 + && typeof parsed.token === "string" + && /^[0-9a-f]{32}$/.test(parsed.token) + && ( + parsed.processStartedAtUnixMs === null + || (Number.isSafeInteger(parsed.processStartedAtUnixMs) && parsed.processStartedAtUnixMs >= 0) + ) + ) { + record = { + pid: parsed.pid, + token: parsed.token, + processStartedAtUnixMs: parsed.processStartedAtUnixMs, + }; + } + } catch (error) { + if (!(error instanceof SyntaxError) && error?.code !== "ERR_ENCODING_INVALID_ENCODED_DATA") throw error; + } + return { + ...record, + pid: record?.pid || 0, + token: record?.token || "", + processStartedAtUnixMs: record?.processStartedAtUnixMs ?? null, + dev: stat.dev, + ino: stat.ino, + mtimeMs: stat.mtimeMs, + ageMs: Math.max(0, Date.now() - stat.mtimeMs), + }; + } finally { + fs.closeSync(descriptor); + } +} + +function readPromptWriteLock(lockPath) { + let stat; + try { + stat = fs.lstatSync(lockPath); + } catch (error) { + if (error.code === "ENOENT") return null; + throw error; + } + if (stat.isSymbolicLink() || !stat.isDirectory()) { + throw promptCenterError("Codex prompt write lock is not a regular directory", 409, "codex_prompt_symlink_refused"); + } + if ((stat.mode & 0o777) !== 0o700) { + throw promptCenterError("Codex prompt write lock must use mode 0700", 409, "codex_prompt_storage_permissions_invalid"); + } + const ownerPath = path.join(lockPath, WRITE_LOCK_OWNER_FILE); + const owner = readPromptWriteLockRecord(ownerPath, "Codex prompt write lock owner"); + return { + ...owner, + pid: owner?.pid || 0, + token: owner?.token || "", + processStartedAtUnixMs: owner?.processStartedAtUnixMs ?? null, + ownerDev: owner?.dev ?? null, + ownerIno: owner?.ino ?? null, + reclaim: readPromptWriteLockRecord( + path.join(lockPath, WRITE_LOCK_RECLAIM_FILE), + "Codex prompt write-lock reclaim claim", + ), + dev: stat.dev, + ino: stat.ino, + identity: promptWriteLockIdentity(stat), + ageMs: Math.max(0, Date.now() - stat.mtimeMs), + }; +} + +function promptWriteLockRecordIsLiveOrFresh(record, isPidAlive, getProcessStartUnixMs, { + ageMs = Number.isFinite(record?.mtimeMs) + ? Math.max(0, Date.now() - record.mtimeMs) + : record?.ageMs ?? Number.POSITIVE_INFINITY, +} = {}) { + if (!record) return false; + const ownerAlive = Boolean(record.pid && isPidAlive(record.pid)); + let currentProcessStartedAtUnixMs = null; + if (ownerAlive) { + try { + currentProcessStartedAtUnixMs = getProcessStartUnixMs(record.pid); + } catch {} + } + const hasVerifiedCurrentProcessStart = Boolean( + Number.isSafeInteger(currentProcessStartedAtUnixMs) + && currentProcessStartedAtUnixMs >= 0 + ); + if (ownerAlive && !hasVerifiedCurrentProcessStart) return true; + const verifiedLiveOwner = Boolean( + ownerAlive + && record.processStartedAtUnixMs !== null + && currentProcessStartedAtUnixMs === record.processStartedAtUnixMs + ); + const legacyLiveOwner = Boolean( + ownerAlive + && record.processStartedAtUnixMs === null + && Number.isFinite(record.mtimeMs) + && currentProcessStartedAtUnixMs <= record.mtimeMs + WRITE_LOCK_PROCESS_START_FS_TOLERANCE_MS + ); + const incompleteAndFresh = Boolean( + ageMs < INCOMPLETE_WRITE_LOCK_GRACE_MS + && ( + !record.pid + || record.processStartedAtUnixMs === null + ) + ); + return verifiedLiveOwner || legacyLiveOwner || incompleteAndFresh; +} + +function promptWriteLockIdentityMatches(left, right) { + return Boolean( + left + && right + && left.dev === right.dev + && left.ino === right.ino + && left.ownerDev === right.ownerDev + && left.ownerIno === right.ownerIno + && left.pid === right.pid + && left.token === right.token + && left.processStartedAtUnixMs === right.processStartedAtUnixMs + ); +} + +function removePromptWriteLockRecordIfOwned(handle) { + try { + const stat = fs.lstatSync(handle.path); + if (stat.dev !== handle.dev || stat.ino !== handle.ino) return false; + fs.unlinkSync(handle.path); + return true; + } catch (error) { + if (error.code === "ENOENT") return false; + throw error; + } +} + +function promptWriteLockDirectoryIdentityMatches(lockPath, expected) { + try { + const stat = fs.lstatSync(lockPath); + return Boolean( + stat.isDirectory() + && !stat.isSymbolicLink() + && stat.dev === expected.dev + && stat.ino === expected.ino + ); + } catch (error) { + if (error.code === "ENOENT") return false; + throw error; + } +} + +function preparePromptWriteLockRecord(lockPath, recordName, record) { + const temporaryName = `${recordName}.tmp.${process.pid}.${randomBytes(16).toString("hex")}`; + const temporaryPath = path.join(lockPath, temporaryName); + let descriptor; + let temporary = null; + try { + descriptor = fs.openSync(temporaryPath, "wx", 0o600); + const stat = fs.fstatSync(descriptor); + temporary = { + path: temporaryPath, + dev: stat.dev, + ino: stat.ino, + }; + fs.writeFileSync(descriptor, `${JSON.stringify(record)}\n`, "utf8"); + fs.fsyncSync(descriptor); + fs.closeSync(descriptor); + descriptor = undefined; + return temporary; + } catch (error) { + if (descriptor !== undefined) { + try { + fs.closeSync(descriptor); + } catch {} + } + if (temporary) { + try { + removePromptWriteLockRecordIfOwned(temporary); + } catch (cleanupError) { + error.cleanupWarning = combinePromptWarnings( + error.cleanupWarning, + `Temporary write-lock record cleanup failed: ${cleanupError.message}`, + ); + } + } + throw error; + } +} + +function retirePromptWriteLockGeneration(lockPath) { + const retiredPath = `${lockPath}.retired.${process.pid}.${randomBytes(16).toString("hex")}`; + fs.renameSync(lockPath, retiredPath); + return retiredPath; +} + +function cleanupRetiredPromptWriteLock(retiredPath, expectedOwner, expectedReclaim) { + const retired = readPromptWriteLock(retiredPath); + if ( + !promptWriteLockIdentityMatches(expectedOwner, retired) + || retired.reclaim?.dev !== expectedReclaim.dev + || retired.reclaim?.ino !== expectedReclaim.ino + ) { + throw promptCenterError( + "Codex prompt write lock changed generation during cleanup", + 409, + "codex_prompt_write_in_progress", + ); + } + if (retired.ownerDev !== null && retired.ownerIno !== null) { + const ownerRemoved = removePromptWriteLockRecordIfOwned({ + path: path.join(retiredPath, WRITE_LOCK_OWNER_FILE), + dev: retired.ownerDev, + ino: retired.ownerIno, + }); + if (!ownerRemoved) { + throw promptCenterError( + "Codex prompt write-lock owner changed during cleanup", + 409, + "codex_prompt_write_in_progress", + ); + } + } + const reclaimRemoved = removePromptWriteLockRecordIfOwned({ + path: path.join(retiredPath, WRITE_LOCK_RECLAIM_FILE), + dev: retired.reclaim.dev, + ino: retired.reclaim.ino, + }); + if (!reclaimRemoved) { + throw promptCenterError( + "Codex prompt write-lock reclaim claim changed during cleanup", + 409, + "codex_prompt_write_in_progress", + ); + } + fs.rmdirSync(retiredPath); +} + +function createPromptWriteReclaim(lockPath, expectedOwner, getProcessStartUnixMs) { + const reclaimPath = path.join(lockPath, WRITE_LOCK_RECLAIM_FILE); + const token = randomBytes(16).toString("hex"); + const initial = readPromptWriteLock(lockPath); + if ( + !promptWriteLockIdentityMatches(expectedOwner, initial) + || initial.reclaim + ) { + throw promptCenterError( + "Codex prompt write lock changed generation before reclaim publication", + 409, + "codex_prompt_write_in_progress", + ); + } + let processStartedAtUnixMs = null; + try { + const value = getProcessStartUnixMs(process.pid); + if (Number.isSafeInteger(value) && value >= 0) processStartedAtUnixMs = value; + } catch {} + const temporary = preparePromptWriteLockRecord( + lockPath, + WRITE_LOCK_RECLAIM_FILE, + { pid: process.pid, token, processStartedAtUnixMs }, + ); + let published = false; + try { + const beforePublication = readPromptWriteLock(lockPath); + if ( + !promptWriteLockIdentityMatches(expectedOwner, beforePublication) + || beforePublication.reclaim + ) { + throw promptCenterError( + "Codex prompt write lock changed generation before reclaim publication", + 409, + "codex_prompt_write_in_progress", + ); + } + fs.linkSync(temporary.path, reclaimPath); + published = true; + const afterPublication = readPromptWriteLock(lockPath); + if ( + !promptWriteLockIdentityMatches(expectedOwner, afterPublication) + || afterPublication.reclaim?.dev !== temporary.dev + || afterPublication.reclaim?.ino !== temporary.ino + || afterPublication.reclaim?.token !== token + || afterPublication.reclaim?.pid !== process.pid + || afterPublication.reclaim?.processStartedAtUnixMs !== processStartedAtUnixMs + ) { + throw promptCenterError( + "Codex prompt write lock changed generation during reclaim publication", + 409, + "codex_prompt_write_in_progress", + ); + } + removePromptWriteLockRecordIfOwned(temporary); + return { path: reclaimPath, dev: temporary.dev, ino: temporary.ino }; + } catch (error) { + if (published) { + try { + removePromptWriteLockRecordIfOwned({ + path: reclaimPath, + dev: temporary.dev, + ino: temporary.ino, + }); + } catch (cleanupError) { + error.cleanupWarning = combinePromptWarnings( + error.cleanupWarning, + `Published write-lock reclaim cleanup failed: ${cleanupError.message}`, + ); + } + } + try { + removePromptWriteLockRecordIfOwned(temporary); + } catch (cleanupError) { + error.cleanupWarning = combinePromptWarnings( + error.cleanupWarning, + `Temporary write-lock reclaim cleanup failed: ${cleanupError.message}`, + ); + } + throw error; + } +} + +function reclaimStalePromptWriteLock(lockPath, expectedOwner, isPidAlive, getProcessStartUnixMs) { + if (expectedOwner.reclaim) { + if (promptWriteLockRecordIsLiveOrFresh( + expectedOwner.reclaim, + isPidAlive, + getProcessStartUnixMs, + )) { + throw promptCenterError( + "Another Context Room is reclaiming Codex prompt storage. Retry after it finishes.", + 409, + "codex_prompt_write_in_progress", + ); + } + const current = readPromptWriteLock(lockPath); + if ( + !current + || current.dev !== expectedOwner.dev + || current.ino !== expectedOwner.ino + || current.reclaim?.dev !== expectedOwner.reclaim.dev + || current.reclaim?.ino !== expectedOwner.reclaim.ino + ) { + return false; + } + removePromptWriteLockRecordIfOwned({ + path: path.join(lockPath, WRITE_LOCK_RECLAIM_FILE), + dev: expectedOwner.reclaim.dev, + ino: expectedOwner.reclaim.ino, + }); + return false; + } + + let reclaim; + let retiredPath = null; + try { + reclaim = createPromptWriteReclaim(lockPath, expectedOwner, getProcessStartUnixMs); + } catch (error) { + if (["ENOENT", "EEXIST"].includes(error.code)) return false; + throw error; + } + try { + const claimed = readPromptWriteLock(lockPath); + if (!promptWriteLockIdentityMatches(expectedOwner, claimed)) return false; + if (claimed.reclaim?.dev !== reclaim.dev || claimed.reclaim?.ino !== reclaim.ino) return false; + retiredPath = retirePromptWriteLockGeneration(lockPath); + cleanupRetiredPromptWriteLock(retiredPath, claimed, reclaim); + return true; + } finally { + if (!retiredPath) removePromptWriteLockRecordIfOwned(reclaim); + } +} + +function acquirePromptWriteLock(storageRoot, isPidAlive, getProcessStartUnixMs) { + ensurePrivateDirectory(storageRoot); + const lockPath = path.join(storageRoot, WRITE_LOCK_FILE); + for (let attempt = 0; attempt < 4; attempt += 1) { + const token = randomBytes(16).toString("hex"); + try { + fs.mkdirSync(lockPath, { mode: 0o700 }); + } catch (error) { + if (error.code !== "EEXIST") throw error; + const owner = readPromptWriteLock(lockPath); + if (owner?.reclaim) { + reclaimStalePromptWriteLock(lockPath, owner, isPidAlive, getProcessStartUnixMs); + continue; + } + if (promptWriteLockRecordIsLiveOrFresh(owner, isPidAlive, getProcessStartUnixMs)) { + throw promptCenterError( + "Another Context Room is saving Codex prompts. Retry after it finishes.", + 409, + "codex_prompt_write_in_progress", + ); + } + if (!owner) continue; + reclaimStalePromptWriteLock(lockPath, owner, isPidAlive, getProcessStartUnixMs); + continue; + } + + const ownerPath = path.join(lockPath, WRITE_LOCK_OWNER_FILE); + const lockStat = fs.lstatSync(lockPath); + try { + let processStartedAtUnixMs = null; + try { + const value = getProcessStartUnixMs(process.pid); + if (Number.isSafeInteger(value) && value >= 0) processStartedAtUnixMs = value; + } catch {} + const temporary = preparePromptWriteLockRecord( + lockPath, + WRITE_LOCK_OWNER_FILE, + { pid: process.pid, token, processStartedAtUnixMs }, + ); + try { + if (!promptWriteLockDirectoryIdentityMatches(lockPath, lockStat)) { + throw promptCenterError( + "Codex prompt write lock changed generation before owner publication", + 409, + "codex_prompt_write_in_progress", + ); + } + fs.renameSync(temporary.path, ownerPath); + } catch (error) { + try { + removePromptWriteLockRecordIfOwned(temporary); + } catch (cleanupError) { + error.cleanupWarning = combinePromptWarnings( + error.cleanupWarning, + `Temporary write-lock owner cleanup failed: ${cleanupError.message}`, + ); + } + throw error; + } + const stat = fs.lstatSync(lockPath); + const published = readPromptWriteLock(lockPath); + if ( + stat.dev !== lockStat.dev + || stat.ino !== lockStat.ino + || published?.dev !== lockStat.dev + || published?.ino !== lockStat.ino + || published?.ownerDev !== temporary.dev + || published?.ownerIno !== temporary.ino + || published?.pid !== process.pid + || published?.token !== token + || published?.processStartedAtUnixMs !== processStartedAtUnixMs + ) { + throw promptCenterError( + "Codex prompt write lock changed generation during acquisition", + 409, + "codex_prompt_write_in_progress", + ); + } + return { + lockPath, + ownerPath, + token, + dev: stat.dev, + ino: stat.ino, + }; + } catch (error) { + // Leave a partially initialized generation in place. Removing the + // canonical path here cannot be atomically bound to the directory + // generation created above, so an interleaved successor could otherwise + // be deleted. The normal stale-lock protocol reclaims this generation + // after its incomplete-lock grace period. + throw error; + } + } + throw promptCenterError( + "Codex prompt storage could not acquire its private write lock", + 409, + "codex_prompt_write_in_progress", + ); +} + +function combinePromptWarnings(...warnings) { + return warnings.flat().filter(Boolean).join(" "); +} + +function releasePromptWriteLock(handle, getProcessStartUnixMs) { + let reclaim = null; + let retiredPath = null; + try { + const current = readPromptWriteLock(handle.lockPath); + if ( + !current + || current.reclaim + || current.dev !== handle.dev + || current.ino !== handle.ino + || current.token !== handle.token + ) { + return "Codex prompt save finished, but its write lock changed generation before cleanup."; + } + try { + reclaim = createPromptWriteReclaim(handle.lockPath, current, getProcessStartUnixMs); + } catch (error) { + if (["ENOENT", "EEXIST"].includes(error.code)) { + return "Codex prompt save finished, but its write lock changed generation before cleanup."; + } + throw error; + } + const claimed = readPromptWriteLock(handle.lockPath); + if ( + !promptWriteLockIdentityMatches(current, claimed) + || claimed.reclaim?.dev !== reclaim.dev + || claimed.reclaim?.ino !== reclaim.ino + ) { + return "Codex prompt save finished, but its write lock changed generation before cleanup."; + } + retiredPath = retirePromptWriteLockGeneration(handle.lockPath); + cleanupRetiredPromptWriteLock(retiredPath, claimed, reclaim); + return ""; + } catch (error) { + if (error.code === "ENOENT" && !retiredPath) return ""; + return `Codex prompt save finished, but write lock cleanup failed: ${error.message}`; + } finally { + if (reclaim && !retiredPath) removePromptWriteLockRecordIfOwned(reclaim); + } +} + +function withPromptWriteLock(storageRoot, isPidAlive, getProcessStartUnixMs, action) { + const handle = acquirePromptWriteLock(storageRoot, isPidAlive, getProcessStartUnixMs); + let result; + let actionError = null; + try { + result = action(); + } catch (error) { + actionError = error; + } + const cleanupWarning = releasePromptWriteLock(handle, getProcessStartUnixMs); + if (actionError) { + if (cleanupWarning) actionError.cleanupWarning = combinePromptWarnings(actionError.cleanupWarning, cleanupWarning); + throw actionError; + } + if (cleanupWarning && result && typeof result === "object" && !Array.isArray(result)) { + return { + ...result, + commitWarning: combinePromptWarnings(result.commitWarning, cleanupWarning), + }; + } + return result; +} + +function normalizeCatalogOverrideConflict(rawConflict, targetId) { + if (rawConflict === null) return null; + assertExactObjectShape( + rawConflict, + CATALOG_OVERRIDE_CONFLICT_KEYS, + `Override conflict for ${targetId}`, + ); + if ( + typeof rawConflict.code !== "string" + || typeof rawConflict.message !== "string" + || typeof rawConflict.sourceTargetId !== "string" + || !CATALOG_OVERRIDE_CONFLICT_CODES.has(rawConflict.code) + || !rawConflict.message + || !rawConflict.sourceTargetId + ) { + throw promptCenterError( + `Override conflict is incomplete for ${targetId}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + return { + code: rawConflict.code, + message: rawConflict.message, + sourceTargetId: rawConflict.sourceTargetId, + }; +} + +function normalizeCatalogTarget(rawTarget, group) { + assertExactObjectShape(rawTarget, CATALOG_TARGET_KEYS, "Codex prompt target"); + for (const [key, label] of [ + ["id", "id"], + ["label", "label"], + ["kind", "kind"], + ["runtimeStatus", "runtimeStatus"], + ["source", "source"], + ["securityClass", "securityClass"], + ]) { + if (typeof rawTarget[key] !== "string" || (["id", "label"].includes(key) && !rawTarget[key])) { + throw promptCenterError( + `Codex prompt target ${label} must be ${["id", "label"].includes(key) ? "non-empty " : ""}text`, + 503, + "codex_prompt_invalid_catalog", + ); + } + } + if (typeof rawTarget.editable !== "boolean") { + throw promptCenterError("Codex prompt target editable must be a boolean", 503, "codex_prompt_invalid_catalog"); + } + if ( + !CATALOG_KINDS.has(rawTarget.kind) + || !CATALOG_RUNTIME_STATUSES.has(rawTarget.runtimeStatus) + || !CATALOG_SECURITY_CLASSES.has(rawTarget.securityClass) + ) { + throw promptCenterError( + `Codex prompt target enum value is unsupported for ${rawTarget.id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + for (const key of [ + "officialHash", + "officialText", + "effectiveHash", + "effectiveText", + "targetPattern", + "sourceTargetId", + "readOnlyReason", + "overrideStrategy", + ]) { + if (rawTarget[key] !== null && typeof rawTarget[key] !== "string") { + throw promptCenterError( + `Codex prompt target ${key} must be text or null`, + 503, + "codex_prompt_invalid_catalog", + ); + } + } + + const id = rawTarget.id; + const hasOfficialContent = rawTarget.officialText !== null; + const official = hasOfficialContent + ? normalizePrompt(rawTarget.officialText, `Official prompt ${id}`) + : null; + const runtimeEffectiveText = rawTarget.effectiveText === null + ? null + : normalizePrompt(rawTarget.effectiveText, `Runtime effective prompt ${id}`); + const runtimeEffectiveHash = rawTarget.effectiveHash; + if ((rawTarget.officialText === null) !== (rawTarget.officialHash === null)) { + throw promptCenterError( + `Official prompt text and hash presence do not match for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ((rawTarget.effectiveText === null) !== (rawTarget.effectiveHash === null)) { + throw promptCenterError( + `Effective prompt text and hash presence do not match for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if (runtimeEffectiveHash !== null && !isSha256(runtimeEffectiveHash)) { + throw promptCenterError(`Effective hash is invalid for ${id}`, 503, "codex_prompt_invalid_catalog"); + } + if ( + runtimeEffectiveText !== null + && runtimeEffectiveHash !== null + && runtimeEffectiveHash !== sha256(runtimeEffectiveText) + ) { + throw promptCenterError( + `Effective hash does not match the catalog content for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const officialHash = rawTarget.officialHash; + if (officialHash !== null && !isSha256(officialHash)) { + throw promptCenterError(`Official hash is invalid for ${id}`, 503, "codex_prompt_invalid_catalog"); + } + if (hasOfficialContent && officialHash !== null && officialHash !== sha256(official)) { + throw promptCenterError( + `Official hash does not match the catalog content for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const runtimeStatus = rawTarget.runtimeStatus; + const lifecycle = ""; + const securityClass = rawTarget.securityClass; + const overrideConflict = normalizeCatalogOverrideConflict(rawTarget.overrideConflict, id); + if ((runtimeStatus === "override_conflict") !== Boolean(overrideConflict)) { + throw promptCenterError( + `Override conflict metadata does not match runtimeStatus for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const runtimeEditable = rawTarget.editable; + const trustedEditableContract = ( + EDITABLE_KINDS.has(rawTarget.kind) + && securityClass === "local_user_editable" + && EDITABLE_RUNTIME_STATUSES.has(runtimeStatus) + ); + if ( + runtimeEditable + && ( + !trustedEditableContract + || !hasOfficialContent + || runtimeEffectiveText === null + || rawTarget.readOnlyReason !== null + ) + ) { + throw promptCenterError( + `Editable target authority metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if (!runtimeEditable && securityClass === "local_user_editable") { + throw promptCenterError( + `Read-only target cannot use local_user_editable security metadata for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const editable = runtimeEditable && trustedEditableContract && hasOfficialContent; + if (editable && officialHash === null) { + throw promptCenterError(`Editable target ${id} requires an official hash`, 503, "codex_prompt_invalid_catalog"); + } + const overrideStrategy = rawTarget.overrideStrategy; + if (overrideStrategy !== null && !["patch", "replacement"].includes(overrideStrategy)) { + throw promptCenterError(`Override strategy is invalid for ${id}`, 503, "codex_prompt_invalid_catalog"); + } + if ( + editable + && overrideStrategy !== (official === "" ? "replacement" : "patch") + ) { + throw promptCenterError( + `Editable target override strategy does not match its official baseline for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const sourceTargetId = rawTarget.sourceTargetId; + if (sourceTargetId !== null && sourceTargetId.length === 0) { + throw promptCenterError( + `Codex prompt target sourceTargetId must be non-empty for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + editable + && sourceTargetId === null + && ( + runtimeEffectiveText !== official + || runtimeEffectiveHash !== officialHash + ) + ) { + throw promptCenterError( + `Editable target without override provenance must match its official baseline for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const modelPatternMetadata = ( + id === "model/base/*" + || runtimeStatus === "pattern" + || securityClass === "advanced_pattern" + || rawTarget.targetPattern !== null + ); + if ( + modelPatternMetadata + && ( + id !== "model/base/*" + || rawTarget.kind !== "model_base" + || runtimeStatus !== "pattern" + || runtimeEditable + || securityClass !== "advanced_pattern" + || rawTarget.officialHash !== null + || rawTarget.officialText !== null + || runtimeEffectiveHash !== null + || runtimeEffectiveText !== null + || rawTarget.targetPattern !== "model/base/{modelSlug}" + || ![null, "model/base/*"].includes(sourceTargetId) + || !rawTarget.readOnlyReason + || rawTarget.overrideStrategy !== null + || overrideConflict !== null + ) + ) { + throw promptCenterError( + `Model prompt pattern metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const protectedMetadata = ( + rawTarget.kind === "protected" + || runtimeStatus === "protected" + || PROTECTED_SECURITY_CLASSES.has(securityClass) + ); + if ( + protectedMetadata + && ( + rawTarget.kind !== "protected" + || runtimeStatus !== "protected" + || runtimeEditable + || !PROTECTED_SECURITY_CLASSES.has(securityClass) + || rawTarget.officialHash !== null + || rawTarget.officialText !== null + || runtimeEffectiveHash !== null + || runtimeEffectiveText !== null + || rawTarget.targetPattern !== null + || sourceTargetId !== null + || !rawTarget.readOnlyReason + || rawTarget.overrideStrategy !== null + || overrideConflict !== null + ) + ) { + throw promptCenterError( + `Protected prompt metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const serverOwnedMetadata = ( + rawTarget.kind === "server_owned" + || runtimeStatus === "server_owned" + || securityClass === "server_owned" + ); + if ( + serverOwnedMetadata + && ( + rawTarget.kind !== "server_owned" + || runtimeStatus !== "server_owned" + || runtimeEditable + || securityClass !== "server_owned" + || rawTarget.officialHash !== null + || rawTarget.officialText !== null + || runtimeEffectiveHash !== null + || runtimeEffectiveText !== null + || rawTarget.targetPattern !== null + || sourceTargetId !== null + || !rawTarget.readOnlyReason + || rawTarget.overrideStrategy !== null + || overrideConflict !== null + ) + ) { + throw promptCenterError( + `Server-owned prompt metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + ( + runtimeStatus === "shadowed_by_explicit_config" + || securityClass === "config_shadowed" + ) + && ( + runtimeStatus !== "shadowed_by_explicit_config" + || !["model_base", "developer", "compact"].includes(rawTarget.kind) + || runtimeEditable + || securityClass !== "config_shadowed" + || official === null + || officialHash === null + || runtimeEffectiveText === null + || runtimeEffectiveHash === null + || rawTarget.targetPattern !== null + || sourceTargetId !== null + || rawTarget.overrideStrategy !== null + || overrideConflict !== null + || !rawTarget.readOnlyReason + ) + ) { + throw promptCenterError( + `Explicit-config shadow metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + ( + runtimeStatus === "dormant" + || securityClass === "dormant" + ) + && ( + runtimeStatus !== "dormant" + || runtimeEditable + || securityClass !== "dormant" + || rawTarget.kind !== "collaboration" + || rawTarget.targetPattern !== null + || sourceTargetId !== null + || rawTarget.overrideStrategy !== null + || overrideConflict !== null + || !rawTarget.readOnlyReason + ) + ) { + throw promptCenterError( + `Dormant collaboration metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + ( + runtimeStatus === "shadowed_by_session_history" + || securityClass === "session_history" + ) + && ( + runtimeStatus !== "shadowed_by_session_history" + || runtimeEditable + || rawTarget.kind !== "model_base" + || securityClass !== "session_history" + || official === null + || officialHash === null + || rawTarget.targetPattern !== null + || sourceTargetId !== null + || rawTarget.overrideStrategy !== null + || overrideConflict !== null + || !rawTarget.readOnlyReason + || runtimeEffectiveText === null + || runtimeEffectiveHash === null + ) + ) { + throw promptCenterError( + `Session-history shadow metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + runtimeStatus === "personality_dependent" + && ( + runtimeEditable + || rawTarget.kind !== "model_base" + || securityClass !== "dynamic_assembly" + || official === null + || officialHash === null + || rawTarget.targetPattern !== null + || sourceTargetId !== null + || rawTarget.overrideStrategy !== null + || overrideConflict !== null + || !rawTarget.readOnlyReason + || runtimeEffectiveText === null + || runtimeEffectiveHash === null + ) + ) { + throw promptCenterError( + `Personality-dependent target metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + securityClass === "dynamic_assembly" + && ( + runtimeStatus !== "personality_dependent" + && overrideConflict?.code !== "target_became_personality_dependent" + ) + ) { + throw promptCenterError( + `Dynamic prompt assembly metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + overrideConflict + && ( + sourceTargetId !== null + || runtimeEffectiveText !== official + || runtimeEffectiveHash !== officialHash + ) + ) { + throw promptCenterError( + `Override conflict fallback is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + overrideConflict + && !editable + && ( + runtimeEditable + || !rawTarget.readOnlyReason + || overrideStrategy !== null + ) + ) { + throw promptCenterError( + `Read-only override conflict metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + if ( + overrideConflict?.code === "target_became_personality_dependent" + && ( + rawTarget.kind !== "model_base" + || runtimeEditable + || securityClass !== "dynamic_assembly" + || rawTarget.targetPattern !== null + ) + ) { + throw promptCenterError( + `Personality-dependent migration conflict metadata is invalid for ${id}`, + 503, + "codex_prompt_invalid_catalog", + ); + } + const editabilityReason = !runtimeEditable + ? "This runtime contract is read-only." + : !hasOfficialContent + ? "This target does not publish one official baseline, so the full-text editor cannot derive a safe exact overlay." + : !trustedEditableContract + ? "This target is not in the runtime's local-user-editable security class or editable runtime status." + : "This runtime contract is read-only."; + return { + id, + label: rawTarget.label, + description: "", + groupId: group.id, + groupLabel: group.label, + source: rawTarget.source, + kind: rawTarget.kind, + role: "", + lifecycle, + editable, + readOnlyReason: editable + ? "" + : rawTarget.readOnlyReason || editabilityReason, + official, + officialHash, + officialContentAvailable: hasOfficialContent, + runtimeEffectiveText, + runtimeEffectiveHash, + runtimeStatus, + targetPattern: rawTarget.targetPattern || "", + sourceTargetId, + overrideStrategy, + overrideConflict, + requiredPlaceholders: [], + securityClass, + }; +} + +export function normalizeCodexPromptCatalog(rawCatalog) { + if (!rawCatalog || typeof rawCatalog !== "object" || Array.isArray(rawCatalog)) { + throw promptCenterError("Codex prompt catalog must be an object", 503, "codex_prompt_invalid_catalog"); + } + assertUnicodeScalarStrings(rawCatalog, "Codex prompt catalog", { + statusCode: 503, + code: "codex_prompt_invalid_catalog", + }); + const protocolVersion = rawCatalog.schemaVersion; + if (protocolVersion !== CODEX_PROMPT_PROTOCOL_VERSION) { + throw promptCenterError( + `Unsupported Codex prompt protocol version: ${Number.isFinite(protocolVersion) ? protocolVersion : "missing"}`, + 503, + "codex_prompt_protocol_unsupported", + { supported: CODEX_PROMPT_PROTOCOL_VERSION, received: Number.isFinite(protocolVersion) ? protocolVersion : null }, + ); + } + assertExactObjectShape(rawCatalog, CATALOG_ROOT_KEYS, "Codex prompt catalog"); + if (typeof rawCatalog.runtimeVersion !== "string") { + throw promptCenterError("Codex prompt runtimeVersion must be text", 503, "codex_prompt_invalid_catalog"); + } + if (!isSha256(rawCatalog.catalogRevision)) { + throw promptCenterError("Codex prompt catalogRevision must be a SHA-256 hash", 503, "codex_prompt_invalid_catalog"); + } + if (!Array.isArray(rawCatalog.groups)) { + throw promptCenterError("Codex prompt catalog groups must be an array", 503, "codex_prompt_invalid_catalog"); + } + const calculatedRevision = computeCodexPromptCatalogRevision(rawCatalog); + if (rawCatalog.catalogRevision !== calculatedRevision) { + throw promptCenterError( + "Codex prompt catalogRevision does not match the typed catalog content", + 503, + "codex_prompt_invalid_catalog", + { expected: calculatedRevision, received: rawCatalog.catalogRevision }, + ); + } + const ids = new Set(); + const groups = rawCatalog.groups.map((rawGroup) => { + assertExactObjectShape(rawGroup, CATALOG_GROUP_KEYS, "Codex prompt catalog group"); + if (typeof rawGroup.id !== "string" || !rawGroup.id || typeof rawGroup.label !== "string" || !rawGroup.label) { + throw promptCenterError("Codex prompt catalog group id and label must be non-empty text", 503, "codex_prompt_invalid_catalog"); + } + if (!Array.isArray(rawGroup.targets)) { + throw promptCenterError("Codex prompt catalog group targets must be an array", 503, "codex_prompt_invalid_catalog"); + } + const id = rawGroup.id; + const label = rawGroup.label; + const targets = rawGroup.targets.map((target) => normalizeCatalogTarget(target, { id, label })); + for (const target of targets) { + if (ids.has(target.id)) { + throw promptCenterError(`Duplicate Codex prompt target: ${target.id}`, 503, "codex_prompt_invalid_catalog"); + } + ids.add(target.id); + } + return { id, label, targets }; + }); + return { + protocolVersion, + codexVersion: rawCatalog.runtimeVersion, + generatedAt: "", + catalogRevision: calculatedRevision, + groups, + }; +} + +export function readCodexPromptCatalog({ storageRoot = defaultCodexPromptStorageRoot(), catalog = null } = {}) { + if (catalog) return normalizeCodexPromptCatalog(catalog); + assertNoPromptStorageSymlinks(storageRoot); + assertPrivateDirectory(storageRoot, "Codex prompt storage"); + return normalizeCodexPromptCatalog(readJsonFile(path.join(storageRoot, CATALOG_FILE), { + requiredMode: 0o600, + })); +} + +function emptyOverrides() { + return { + schemaVersion: CODEX_PROMPT_PROTOCOL_VERSION, + revision: 0, + overrides: [], + manifestHash: null, + }; +} + +function normalizePatch(rawPatch, targetId) { + assertExactObjectShape( + rawPatch, + ["before", "after", "expectedMatches"], + `Patch for ${targetId}`, + { statusCode: 503, code: "codex_prompt_override_invalid" }, + ); + if (typeof rawPatch.before !== "string" || typeof rawPatch.after !== "string") { + throw promptCenterError( + `Patch before and after must be text for ${targetId}`, + 503, + "codex_prompt_override_invalid", + ); + } + const before = normalizePrompt(rawPatch.before, `Patch source for ${targetId}`); + const after = normalizePrompt(rawPatch.after, `Patch replacement for ${targetId}`); + const expectedMatches = rawPatch.expectedMatches; + if (!Number.isInteger(expectedMatches) || expectedMatches !== 1) { + throw promptCenterError(`Patch expectedMatches must be exactly 1 for ${targetId}`, 503, "codex_prompt_override_invalid"); + } + if (!before) { + throw promptCenterError(`Patch source must not be empty for ${targetId}`, 503, "codex_prompt_override_invalid"); + } + return { before, after, expectedMatches }; +} + +function normalizeOverride(rawOverride) { + assertExactObjectShape( + rawOverride, + ["targetId", "patches", "replacement"], + "Codex prompt override", + { + statusCode: 503, + code: "codex_prompt_override_invalid", + optionalKeys: ["officialHash"], + }, + ); + assertUnicodeScalarStrings(rawOverride, "Codex prompt override", { + statusCode: 503, + code: "codex_prompt_override_invalid", + }); + const targetId = rawOverride.targetId; + if (typeof targetId !== "string" || !targetId) { + throw promptCenterError("Every Codex prompt override requires a text targetId", 503, "codex_prompt_override_invalid"); + } + if (!Array.isArray(rawOverride.patches)) { + throw promptCenterError(`Override patches must be an array for ${targetId}`, 503, "codex_prompt_override_invalid"); + } + const officialHash = Object.hasOwn(rawOverride, "officialHash") ? rawOverride.officialHash : null; + if (officialHash !== null && typeof officialHash !== "string") { + throw promptCenterError(`Override officialHash must be text or null for ${targetId}`, 503, "codex_prompt_override_invalid"); + } + if (rawOverride.replacement !== null && typeof rawOverride.replacement !== "string") { + throw promptCenterError(`Override replacement must be text or null for ${targetId}`, 503, "codex_prompt_override_invalid"); + } + const patches = rawOverride.patches.map((patch) => normalizePatch(patch, targetId)); + const replacement = rawOverride.replacement === null + ? null + : normalizePrompt(rawOverride.replacement, `Replacement prompt ${targetId}`); + const normalized = { + targetId, + officialHash, + patches, + replacement, + }; + if (normalized.officialHash !== null && !isSha256(normalized.officialHash)) { + throw promptCenterError(`Override officialHash is invalid for ${targetId}`, 503, "codex_prompt_override_invalid"); + } + if (targetId !== "model/base/*" && normalized.officialHash === null) { + throw promptCenterError(`Override officialHash is required for ${targetId}`, 503, "codex_prompt_override_invalid"); + } + const usesPatches = patches.length > 0; + const usesReplacement = replacement !== null; + if ( + targetId === "model/base/*" + && ( + normalized.officialHash !== null + || !usesPatches + || usesReplacement + ) + ) { + throw promptCenterError( + "Override model/base/* must use patches with officialHash and replacement set to null", + 503, + "codex_prompt_override_invalid", + ); + } + if (usesPatches === usesReplacement) { + throw promptCenterError( + `Override ${targetId} must use either patches or replacement, but not both`, + 503, + "codex_prompt_override_invalid", + ); + } + return { ...normalized, overrideHash: sha256(stableJson(normalized)) }; +} + +function normalizeOverrides(rawOverrides, { manifestHash = null } = {}) { + if (!rawOverrides) return emptyOverrides(); + assertExactObjectShape( + rawOverrides, + ["schemaVersion", "revision", "overrides"], + "Codex prompt manifest", + { statusCode: 503, code: "codex_prompt_override_invalid" }, + ); + const protocolVersion = rawOverrides.schemaVersion; + if (protocolVersion !== CODEX_PROMPT_PROTOCOL_VERSION) { + throw promptCenterError("Unsupported Codex prompt override protocol", 503, "codex_prompt_protocol_unsupported"); + } + if (!Array.isArray(rawOverrides.overrides)) { + throw promptCenterError("Codex prompt manifest overrides must be an array", 503, "codex_prompt_override_invalid"); + } + const revision = rawOverrides.revision; + if (!Number.isSafeInteger(revision) || revision < 0) { + throw promptCenterError("Codex prompt manifest revision must be a non-negative safe integer", 503, "codex_prompt_override_invalid"); + } + const source = rawOverrides.overrides; + const seen = new Set(); + const overrides = source.map(normalizeOverride); + for (const override of overrides) { + if (seen.has(override.targetId)) { + throw promptCenterError(`Duplicate Codex prompt override: ${override.targetId}`, 503, "codex_prompt_override_invalid"); + } + seen.add(override.targetId); + } + return { + schemaVersion: protocolVersion, + revision, + overrides, + manifestHash, + }; +} + +function nextPromptManifestRevision(revision) { + if (!Number.isSafeInteger(revision) || revision < 0 || revision >= Number.MAX_SAFE_INTEGER) { + throw promptCenterError( + "Codex prompt manifest revision cannot be incremented safely", + 409, + "codex_prompt_manifest_revision_exhausted", + ); + } + return revision + 1; +} + +export function readCodexPromptOverrides({ storageRoot = defaultCodexPromptStorageRoot() } = {}) { + assertNoPromptStorageSymlinks(storageRoot); + if (!assertPrivateDirectory(storageRoot, "Codex prompt storage", { required: false })) { + return emptyOverrides(); + } + const record = readJsonFile(path.join(storageRoot, OVERRIDES_FILE), { + required: false, + includeBytes: true, + requiredMode: 0o600, + }); + if (!record) return emptyOverrides(); + return normalizeOverrides(record.value, { manifestHash: sha256(record.bytes) }); +} + +function countOccurrences(haystack, needle) { + let count = 0; + let offset = 0; + while (offset <= haystack.length - needle.length) { + const index = haystack.indexOf(needle, offset); + if (index < 0) break; + count += 1; + offset = index + Math.max(needle.length, 1); + } + return count; +} + +export function applyCodexPromptPatches(official, patches, { targetId = "" } = {}) { + let effective = official; + for (const [index, patch] of patches.entries()) { + const matches = countOccurrences(effective, patch.before); + if (matches !== patch.expectedMatches) { + throw promptCenterError( + `Override patch ${index + 1} for ${targetId || "this prompt"} expected ${patch.expectedMatches} match${patch.expectedMatches === 1 ? "" : "es"} but found ${matches}`, + 409, + "codex_prompt_override_conflict", + { targetId, patchIndex: index, expectedMatches: patch.expectedMatches, actualMatches: matches }, + ); + } + effective = effective.split(patch.before).join(patch.after); + assertPromptSize(effective, `Effective prompt ${targetId}`); + } + return effective; +} + +function commonPrefixLength(left, right) { + const limit = Math.min(left.length, right.length); + let index = 0; + while (index < limit && left[index] === right[index]) index += 1; + return index; +} + +function commonSuffixLength(left, right, prefixLength) { + const limit = Math.min(left.length, right.length) - prefixLength; + let length = 0; + while (length < limit && left[left.length - 1 - length] === right[right.length - 1 - length]) length += 1; + return length; +} + +function expandPatchAnchor(official, prefixLength, suffixLength) { + let start = prefixLength; + let end = official.length - suffixLength; + if (start === end) { + if (start > 0) start -= 1; + else if (end < official.length) end += 1; + } + while (start > 0 && official[start - 1] !== "\n") start -= 1; + while (end < official.length && official[end] !== "\n") end += 1; + if (end < official.length) end += 1; + if (start === end && official) end = official.length; + return { start, end }; +} + +export function createCodexPromptPatches(official, effective) { + if (official === effective) return []; + if (!official) { + throw promptCenterError("An empty official prompt cannot be overridden safely", 422, "codex_prompt_empty_official"); + } + const prefixLength = commonPrefixLength(official, effective); + const suffixLength = commonSuffixLength(official, effective, prefixLength); + const { start, end } = expandPatchAnchor(official, prefixLength, suffixLength); + const before = official.slice(start, end); + const effectiveEnd = effective.length - Math.max(0, official.length - end); + const after = effective.slice(start, effectiveEnd); + const contextual = { before, after, expectedMatches: 1 }; + if ( + before + && countOccurrences(official, before) === 1 + && applyCodexPromptPatches(official, [contextual]) === effective + ) { + return [contextual]; + } + return [{ before: official, after: effective, expectedMatches: 1 }]; +} + +function targetLookup(catalog) { + return new Map(catalog.groups.flatMap((group) => group.targets.map((target) => [target.id, target]))); +} + +function findTarget(catalog, targetId) { + const target = targetLookup(catalog).get(String(targetId || "")); + if (!target) throw promptCenterError("Unknown Codex prompt target", 404, "codex_prompt_target_not_found"); + return target; +} + +function validateRequiredPlaceholders(target, effective) { + const missing = target.requiredPlaceholders.filter((placeholder) => !effective.includes(placeholder)); + if (missing.length) { + throw promptCenterError( + `Required placeholder${missing.length === 1 ? "" : "s"} missing: ${missing.join(", ")}`, + 422, + "codex_prompt_placeholder_missing", + { targetId: target.id, missing }, + ); + } +} + +function currentOverrideForTarget(overrides, target) { + const exact = overrides.overrides.find((item) => item.targetId === target.id); + if (exact) return exact; + if (target.sourceTargetId) { + const source = overrides.overrides.find((item) => item.targetId === target.sourceTargetId); + if (source) return source; + } + if (target.editable && target.id.startsWith("model/base/") && target.id !== "model/base/*") { + return overrides.overrides.find((item) => item.targetId === "model/base/*") || null; + } + return null; +} + +function staleReadOnlyOverrideConflict(target, override) { + if (!override || target.editable || target.runtimeStatus === "pattern") return null; + return preferredOverrideConflict(target, override, { + code: "codex_prompt_override_target_read_only", + message: `This saved override cannot apply because ${target.id} is now read-only. Restore official to remove it.`, + }); +} + +function catalogOverrideIssues(catalog, overrides) { + const targets = targetLookup(catalog); + const issues = []; + for (const override of overrides.overrides) { + const target = targets.get(override.targetId); + if (!target) { + issues.push({ + targetId: override.targetId, + code: "codex_prompt_override_target_unknown", + message: `Saved override target is absent from the runtime catalog: ${override.targetId}`, + }); + continue; + } + if ( + target.overrideConflict?.code === "target_became_personality_dependent" + && target.overrideConflict.sourceTargetId === override.targetId + && !target.editable + ) { + continue; + } + const readOnlyConflict = staleReadOnlyOverrideConflict(target, override); + if (readOnlyConflict) { + issues.push({ + targetId: override.targetId, + ...readOnlyConflict, + }); + continue; + } + const expectedStrategy = target.overrideStrategy + || (target.runtimeStatus === "pattern" ? "patch" : null); + const actualStrategy = override.replacement === null ? "patch" : "replacement"; + if (!expectedStrategy || actualStrategy !== expectedStrategy) { + issues.push({ + targetId: override.targetId, + code: "codex_prompt_override_strategy_invalid", + message: `Saved override strategy ${actualStrategy} is incompatible with ${override.targetId}`, + expected: expectedStrategy, + received: actualStrategy, + }); + } + } + return issues; +} + +function assertNoCatalogOverrideIssues(catalog, overrides) { + const issues = catalogOverrideIssues(catalog, overrides); + if (issues.length) { + throw promptCenterError( + "Resolve incompatible or orphaned Codex prompt overrides before saving another change.", + 409, + "codex_prompt_manifest_conflict", + { issues }, + ); + } +} + +function orphanedCatalogTarget(override) { + return { + id: override.targetId, + label: override.targetId, + description: "Saved override whose target is absent from the installed runtime catalog.", + groupId: "orphaned-overrides", + groupLabel: "Orphaned overrides", + source: "", + kind: "orphaned_override", + role: "", + lifecycle: "", + editable: false, + readOnlyReason: "This target is no longer published. Restore it to remove the incompatible override.", + official: "", + officialHash: override.officialHash, + officialContentAvailable: false, + runtimeEffectiveText: null, + runtimeEffectiveHash: null, + runtimeStatus: "orphaned_override", + targetPattern: "", + sourceTargetId: null, + overrideStrategy: null, + overrideConflict: null, + requiredPlaceholders: [], + securityClass: "runtime_contract", + }; +} + +function catalogWithOrphanedOverrides(catalog, overrides) { + const targets = targetLookup(catalog); + const orphaned = overrides.overrides + .filter((override) => !targets.has(override.targetId)) + .map(orphanedCatalogTarget); + if (!orphaned.length) return catalog; + return { + ...catalog, + groups: [ + ...catalog.groups, + { id: "orphaned-overrides", label: "Orphaned overrides", targets: orphaned }, + ], + }; +} + +function baselineEffectiveWithoutRegistry(target) { + if (!target.editable) { + return { + effective: target.runtimeEffectiveText, + effectiveHash: target.runtimeEffectiveHash, + }; + } + if (!target.sourceTargetId && target.runtimeEffectiveText !== null) { + return { + effective: target.runtimeEffectiveText, + effectiveHash: target.runtimeEffectiveHash, + }; + } + return { + effective: target.official, + effectiveHash: target.officialHash, + }; +} + +function preferredOverrideConflict(target, override, fallback) { + if ( + target.overrideConflict + && override + && target.overrideConflict.sourceTargetId === override.targetId + ) { + return target.overrideConflict; + } + return fallback; +} + +function resolveOverride(target, override) { + if (target.runtimeStatus === "orphaned_override") { + return { + effective: null, + effectiveHash: null, + overrideHash: override?.overrideHash || "", + override: override || null, + overrideInherited: false, + conflict: override ? { + code: "codex_prompt_override_target_unknown", + message: "This saved override target is absent from the installed runtime catalog.", + } : null, + }; + } + if (!target.editable) { + const baseline = baselineEffectiveWithoutRegistry(target); + return { + ...baseline, + overrideHash: override?.overrideHash || "", + override: null, + overrideInherited: false, + conflict: staleReadOnlyOverrideConflict(target, override) || target.overrideConflict || null, + }; + } + if (!override) { + const baseline = baselineEffectiveWithoutRegistry(target); + return { + ...baseline, + overrideHash: "", + override: null, + overrideInherited: false, + conflict: null, + }; + } + if (override.officialHash !== null && override.officialHash !== target.officialHash) { + return { + effective: target.official, + effectiveHash: target.officialHash, + overrideHash: override.overrideHash, + override, + overrideInherited: override.targetId !== target.id, + conflict: preferredOverrideConflict(target, override, { + code: "codex_prompt_official_changed", + message: "The official prompt changed after this override was saved.", + }), + }; + } + try { + const usesReplacement = override.replacement !== null; + if ( + !target.overrideStrategy + || (target.overrideStrategy === "replacement" && !usesReplacement) + || (target.overrideStrategy === "patch" && usesReplacement) + ) { + throw promptCenterError( + `Saved override strategy no longer matches the runtime catalog for ${target.id}`, + 409, + "codex_prompt_override_strategy_changed", + ); + } + const effective = override.replacement === null + ? applyCodexPromptPatches(target.official, override.patches, { targetId: target.id }) + : override.replacement; + validateRequiredPlaceholders(target, effective); + const effectiveHash = sha256(effective); + return { + effective, + effectiveHash, + overrideHash: override.overrideHash, + override, + overrideInherited: override.targetId !== target.id, + conflict: null, + }; + } catch (error) { + return { + effective: target.official, + effectiveHash: target.officialHash, + overrideHash: override.overrideHash, + override, + overrideInherited: override.targetId !== target.id, + conflict: preferredOverrideConflict(target, override, { + code: error.code || "codex_prompt_override_conflict", + message: error.message, + }), + }; + } +} + +function defaultPidAlive(pid) { + if (!Number.isSafeInteger(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch (error) { + return error.code === "EPERM"; + } +} + +function defaultCodexRuntimeProcess(pid) { + try { + const command = execFileSync("/bin/ps", ["-p", String(pid), "-o", "command="], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 1_000, + }).trim(); + return /\bapp-server\b/.test(command) + && /(?:^|[/\s])codex(?:[\s-]|$)/i.test(command); + } catch { + return false; + } +} + +function defaultCodexRuntimeProcessStartUnixMs(pid) { + try { + const started = execFileSync("/bin/ps", ["-p", String(pid), "-o", "lstart="], { + encoding: "utf8", + env: { ...process.env, LC_ALL: "C" }, + stdio: ["ignore", "pipe", "ignore"], + timeout: 1_000, + }).trim(); + const startedAtUnixMs = Date.parse(started); + return Number.isFinite(startedAtUnixMs) && startedAtUnixMs >= 0 ? startedAtUnixMs : null; + } catch { + return null; + } +} + +function defaultCodexRuntimeProcessStartIdentity(pid) { + if (process.platform !== "darwin" || !Number.isSafeInteger(pid) || pid <= 0) return null; + try { + const identity = execFileSync("/usr/bin/python3", [ + "-c", + DARWIN_PROCESS_START_IDENTITY_SCRIPT, + String(pid), + ], { + encoding: "utf8", + env: { + LANG: "C", + LC_ALL: "C", + PATH: "/usr/bin:/bin:/usr/sbin:/sbin", + }, + stdio: ["ignore", "pipe", "ignore"], + timeout: 1_000, + }).trim(); + return isDarwinProcessStartIdentity(identity) ? identity : null; + } catch { + return null; + } +} + +export function codexPromptDarwinIdentityHelperSourceForTest() { + return DARWIN_PROCESS_START_IDENTITY_SCRIPT; +} + +function normalizeActiveOverrides(receipt) { + if (!Array.isArray(receipt.activeOverrides)) { + throw promptCenterError("Codex prompt runtime receipt has no activeOverrides array", 503, "codex_prompt_receipt_invalid"); + } + const records = []; + const hashes = {}; + const sources = {}; + for (const item of receipt.activeOverrides) { + assertExactObjectShape( + item, + ["targetId", "sourceTargetId", "effectiveHash"], + "Codex prompt runtime active override", + { statusCode: 503, code: "codex_prompt_receipt_invalid" }, + ); + const targetId = typeof item.targetId === "string" ? item.targetId : ""; + const sourceTargetId = typeof item.sourceTargetId === "string" ? item.sourceTargetId : ""; + const effectiveHash = typeof item.effectiveHash === "string" ? item.effectiveHash : ""; + if (!targetId || !sourceTargetId || Object.hasOwn(hashes, targetId) || Object.hasOwn(sources, targetId)) { + throw promptCenterError("Codex prompt runtime receipt contains an invalid or duplicate target", 503, "codex_prompt_receipt_invalid"); + } + if (!isSha256(effectiveHash)) { + throw promptCenterError("Codex prompt runtime receipt contains an invalid effective hash", 503, "codex_prompt_receipt_invalid"); + } + records.push({ targetId, sourceTargetId, effectiveHash }); + sources[targetId] = sourceTargetId; + hashes[targetId] = effectiveHash; + } + return { records, hashes, sources }; +} + +function expectedActiveOverrides(catalog) { + return catalog.groups.flatMap((group) => group.targets) + .filter((target) => target.sourceTargetId !== null && target.runtimeEffectiveHash !== null) + .map((target) => ({ + targetId: target.id, + sourceTargetId: target.sourceTargetId, + effectiveHash: target.runtimeEffectiveHash, + })); +} + +function assertActiveOverridesMatchSnapshot(actual, catalog) { + const expected = expectedActiveOverrides(catalog); + const matches = actual.length === expected.length && actual.every((item, index) => ( + item.targetId === expected[index].targetId + && item.sourceTargetId === expected[index].sourceTargetId + && item.effectiveHash === expected[index].effectiveHash + )); + if (!matches) { + throw promptCenterError( + "Codex prompt runtime activeOverrides do not exactly match the referenced catalog snapshot", + 503, + "codex_prompt_receipt_invalid", + { expected, received: actual }, + ); + } +} + +function rawCatalogRevision(rawCatalog) { + return computeCodexPromptCatalogRevision(rawCatalog); +} + +function runtimeSnapshotResult(runtimePath, { + pid, + catalogFile, + catalogHash, + catalogRevision, + processStartedAtUnixMs, + beforeRuntimeSnapshotRead, +}) { + try { + if (typeof beforeRuntimeSnapshotRead === "function") { + beforeRuntimeSnapshotRead({ pid, catalogFile }); + } + const snapshotPath = path.join(runtimePath, catalogFile); + const record = readJsonFile(snapshotPath, { + maxBytes: MAX_CODEX_PROMPT_MANIFEST_BYTES, + includeBytes: true, + requiredMode: 0o600, + }); + if (record.stat.mtimeMs < processStartedAtUnixMs) { + throw promptCenterError( + `Codex prompt runtime catalog predates process ${pid}: ${catalogFile}`, + 503, + "codex_prompt_runtime_catalog_stale", + ); + } + if (sha256(record.bytes) !== catalogHash) { + throw promptCenterError( + `Codex prompt runtime catalog hash does not match ${catalogFile}`, + 503, + "codex_prompt_runtime_catalog_invalid", + ); + } + if (rawCatalogRevision(record.value) !== catalogRevision) { + throw promptCenterError( + `Codex prompt runtime catalog has an invalid logical revision: ${catalogFile}`, + 503, + "codex_prompt_runtime_catalog_invalid", + ); + } + const catalog = normalizeCodexPromptCatalog(record.value); + if (catalog.catalogRevision !== catalogRevision) { + throw promptCenterError( + `Codex prompt runtime catalog revision does not match receipt ${pid}.json`, + 503, + "codex_prompt_runtime_catalog_invalid", + ); + } + return { catalog, error: null }; + } catch (error) { + return { + catalog: null, + error: { + code: error.code || "codex_prompt_runtime_catalog_invalid", + message: error.message, + }, + }; + } +} + +function unverifiedRuntimeReceipt( + pid, + processStartedAtUnixMs, + message, + processStartIdentity = null, + publicationGeneration = 0, +) { + return { + pid, + alive: true, + identityVerified: false, + processStartedAtUnixMs, + processStartIdentity, + publicationGeneration, + loadedAtUnixMs: 0, + codexVersion: "", + catalogFile: "", + catalogHash: "", + catalogRevision: "", + catalog: null, + catalogError: { + code: "codex_prompt_runtime_identity_unverified", + message, + }, + manifestRevision: 0, + manifestHash: null, + loadedHashes: {}, + sourceTargetIds: {}, + activeOverrides: [], + }; +} + +function normalizeRuntimeReceipt(receipt, { + entryName, + filenamePid, + runtimePath, + processStartedAtUnixMs, + processStartIdentity, + publicationState, + beforeRuntimeSnapshotRead, +}) { + const receiptKeys = [ + "schemaVersion", + "pid", + "publicationGeneration", + "processStartIdentity", + "loadedAtUnixMs", + "runtimeVersion", + "manifestRevision", + "manifestHash", + "catalogFile", + "catalogHash", + "catalogRevision", + "activeOverrides", + ]; + assertExactObjectShape(receipt, receiptKeys, `Codex prompt receipt ${entryName}`, { + statusCode: 503, + code: "codex_prompt_receipt_invalid", + }); + const schemaVersion = receipt.schemaVersion; + if (schemaVersion !== CODEX_PROMPT_RECEIPT_VERSION) { + throw promptCenterError( + `Unsupported Codex prompt receipt protocol in ${entryName}`, + 503, + "codex_prompt_protocol_unsupported", + ); + } + if (typeof receipt.runtimeVersion !== "string") { + throw promptCenterError( + `Codex prompt receipt runtimeVersion must be text in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + const pid = receipt.pid; + if (!Number.isSafeInteger(pid) || pid <= 0 || pid !== filenamePid) { + throw promptCenterError( + `Codex prompt receipt PID does not match ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + const publicationGeneration = receipt.publicationGeneration; + if (!Number.isSafeInteger(publicationGeneration) || publicationGeneration <= 0) { + throw promptCenterError( + `Codex prompt receipt has an invalid publicationGeneration in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + if (!isDarwinProcessStartIdentity(receipt.processStartIdentity)) { + throw promptCenterError( + `Codex prompt receipt has an invalid processStartIdentity in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + if (receipt.processStartIdentity !== processStartIdentity) { + return unverifiedRuntimeReceipt( + pid, + processStartedAtUnixMs, + `Codex prompt receipt belongs to another process generation: ${entryName}`, + processStartIdentity, + publicationGeneration, + ); + } + if (publicationState.runtimeOwnerGenerations[String(pid)] !== publicationGeneration) { + return unverifiedRuntimeReceipt( + pid, + processStartedAtUnixMs, + `Codex prompt receipt is not owned by the current publication generation: ${entryName}`, + processStartIdentity, + publicationGeneration, + ); + } + const catalogRevision = receipt.catalogRevision; + const catalogHash = receipt.catalogHash; + const catalogFile = receipt.catalogFile; + if ( + typeof catalogRevision !== "string" + || typeof catalogHash !== "string" + || typeof catalogFile !== "string" + || !isSha256(catalogRevision) + || !isSha256(catalogHash) + ) { + throw promptCenterError( + `Codex prompt receipt has an invalid catalog identity in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + const catalogFileMatch = /^([1-9]\d*)\.([0-9a-f]{64})\.catalog\.json$/.exec(catalogFile); + if ( + !catalogFileMatch + || Number(catalogFileMatch[1]) !== pid + || `sha256:${catalogFileMatch[2]}` !== catalogHash + ) { + throw promptCenterError( + `Codex prompt receipt has an invalid catalogFile in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + const loadedAtUnixMs = receipt.loadedAtUnixMs; + if (!Number.isSafeInteger(loadedAtUnixMs) || loadedAtUnixMs < 0 || loadedAtUnixMs > Date.now() + 300_000) { + throw promptCenterError( + `Codex prompt receipt has an invalid load timestamp in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + if (loadedAtUnixMs < processStartedAtUnixMs) { + return unverifiedRuntimeReceipt( + pid, + processStartedAtUnixMs, + `Codex prompt receipt predates the current process start: ${entryName}`, + ); + } + const manifestHash = receipt.manifestHash; + if (manifestHash !== null && typeof manifestHash !== "string") { + throw promptCenterError( + `Codex prompt receipt manifestHash must be text or null in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + if (manifestHash !== null && !isSha256(manifestHash)) { + throw promptCenterError( + `Codex prompt receipt has an invalid manifest hash in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + const manifestRevision = receipt.manifestRevision; + if (!Number.isSafeInteger(manifestRevision) || manifestRevision < 0) { + throw promptCenterError( + `Codex prompt receipt has an invalid manifest revision in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + const active = normalizeActiveOverrides(receipt); + const snapshot = runtimeSnapshotResult(runtimePath, { + pid, + catalogFile, + catalogHash, + catalogRevision, + processStartedAtUnixMs, + beforeRuntimeSnapshotRead, + }); + if (snapshot.catalog) { + if (receipt.runtimeVersion !== snapshot.catalog.codexVersion) { + throw promptCenterError( + `Codex prompt receipt runtimeVersion does not match its catalog snapshot in ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + ); + } + assertActiveOverridesMatchSnapshot(active.records, snapshot.catalog); + } + return { + pid, + alive: true, + identityVerified: true, + processStartedAtUnixMs, + processStartIdentity, + publicationGeneration, + loadedAtUnixMs, + codexVersion: receipt.runtimeVersion, + catalogFile, + catalogHash, + catalogRevision, + catalog: snapshot.catalog, + catalogError: snapshot.error, + manifestRevision, + manifestHash, + activeOverrides: active.records, + loadedHashes: active.hashes, + sourceTargetIds: active.sources, + }; +} + +function normalizeRuntimePublicationState(value) { + assertExactObjectShape( + value, + [ + "schemaVersion", + "nextGeneration", + "globalOwnerGeneration", + "runtimeRegistryGenerations", + "runtimeOwnerGenerations", + ], + "Codex prompt publication state", + { + statusCode: 503, + code: "codex_prompt_publication_state_invalid", + }, + ); + if (value.schemaVersion !== CODEX_PROMPT_PUBLICATION_STATE_VERSION) { + throw promptCenterError( + "Unsupported Codex prompt publication state protocol", + 503, + "codex_prompt_publication_state_invalid", + ); + } + const nextGeneration = value.nextGeneration; + const globalOwnerGeneration = value.globalOwnerGeneration; + if ( + !Number.isSafeInteger(nextGeneration) + || nextGeneration < 1 + || !Number.isSafeInteger(globalOwnerGeneration) + || globalOwnerGeneration < 0 + || Object.is(globalOwnerGeneration, -0) + || globalOwnerGeneration >= nextGeneration + ) { + throw promptCenterError( + "Codex prompt publication state contains an invalid generation", + 503, + "codex_prompt_publication_state_invalid", + ); + } + const rawRegistries = value.runtimeRegistryGenerations; + const rawOwners = value.runtimeOwnerGenerations; + if (!rawRegistries || typeof rawRegistries !== "object" || Array.isArray(rawRegistries)) { + throw promptCenterError( + "Codex prompt publication state runtimeRegistryGenerations must be an object", + 503, + "codex_prompt_publication_state_invalid", + ); + } + if (!rawOwners || typeof rawOwners !== "object" || Array.isArray(rawOwners)) { + throw promptCenterError( + "Codex prompt publication state runtimeOwnerGenerations must be an object", + 503, + "codex_prompt_publication_state_invalid", + ); + } + const registryKeys = Object.keys(rawRegistries); + const ownerKeys = Object.keys(rawOwners); + if ( + registryKeys.length !== ownerKeys.length + || registryKeys.some((pidKey) => !Object.hasOwn(rawOwners, pidKey)) + ) { + throw promptCenterError( + "Codex prompt publication state runtime generation maps must have identical PID keys", + 503, + "codex_prompt_publication_state_invalid", + ); + } + const runtimeRegistryGenerations = {}; + const runtimeOwnerGenerations = {}; + for (const pidKey of registryKeys) { + const pid = Number(pidKey); + const registryGeneration = rawRegistries[pidKey]; + const ownerGeneration = rawOwners[pidKey]; + if ( + !/^[1-9]\d*$/.test(pidKey) + || !Number.isSafeInteger(pid) + || pid < 1 + || pid > 0xffff_ffff + || String(pid) !== pidKey + || !Number.isSafeInteger(registryGeneration) + || registryGeneration < 1 + || registryGeneration >= nextGeneration + || !Number.isSafeInteger(ownerGeneration) + || ownerGeneration < 1 + || ownerGeneration >= nextGeneration + || registryGeneration > ownerGeneration + ) { + throw promptCenterError( + "Codex prompt publication state contains invalid runtime generations", + 503, + "codex_prompt_publication_state_invalid", + ); + } + runtimeRegistryGenerations[pidKey] = registryGeneration; + runtimeOwnerGenerations[pidKey] = ownerGeneration; + } + return { + schemaVersion: CODEX_PROMPT_PUBLICATION_STATE_VERSION, + nextGeneration, + globalOwnerGeneration, + runtimeRegistryGenerations, + runtimeOwnerGenerations, + }; +} + +function readRuntimePublicationStateRecord(storageRoot) { + try { + const record = readJsonFile(path.join(storageRoot, PUBLICATION_STATE_FILE), { + required: false, + maxBytes: MAX_CODEX_PROMPT_PUBLICATION_STATE_BYTES, + includeBytes: true, + requiredMode: 0o600, + }); + if (!record) { + return { + record: null, + error: { + code: "codex_prompt_publication_state_unavailable", + message: "Codex prompt publication state is unavailable.", + }, + }; + } + return { + record: { + ...record, + value: normalizeRuntimePublicationState(record.value), + }, + error: null, + }; + } catch (error) { + return { + record: null, + error: { + code: error.code || "codex_prompt_publication_state_invalid", + message: error.message, + }, + }; + } +} + +function readRuntimeReceiptRecord(receiptPath, entryName, processStartedAtUnixMs, { + required = true, +} = {}) { + let record; + try { + record = readJsonFile(receiptPath, { + required, + maxBytes: MAX_CODEX_PROMPT_MANIFEST_BYTES, + includeBytes: true, + requiredMode: 0o600, + }); + } catch (error) { + throw promptCenterError( + `Codex prompt runtime receipt is invalid: ${entryName}`, + 503, + "codex_prompt_receipt_invalid", + { cause: error.message }, + ); + } + if (!record) return { missing: true, stale: false, record: null }; + if (record.stat.mtimeMs < processStartedAtUnixMs) { + return { missing: false, stale: true, record: null }; + } + return { + missing: false, + stale: false, + record, + }; +} + +function safeReceiptPublicationGeneration(receipt) { + return Number.isSafeInteger(receipt?.publicationGeneration) + && receipt.publicationGeneration > 0 + ? receipt.publicationGeneration + : 0; +} + +function runtimeReceiptEntries(runtimePath) { + return fs.readdirSync(runtimePath, { withFileTypes: true }) + .filter((entry) => /^\d+\.json$/.test(entry.name)) + .sort((left, right) => left.name.localeCompare(right.name)); +} + +function publicationStateOwnsRuntimeReceiptProofs(publicationStateRead, proofs) { + return Boolean(publicationStateRead.record) + && proofs.every((proof) => ( + publicationStateRead.record.value.runtimeOwnerGenerations[String(proof.pid)] + === proof.publicationGeneration + )); +} + +function readRuntimeProcessIdentitySet({ + runtimePath, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartIdentity, +}) { + const identities = new Map(); + for (const entry of runtimeReceiptEntries(runtimePath)) { + const pid = Number(path.basename(entry.name, ".json")); + if (!Number.isSafeInteger(pid) || pid <= 0) continue; + let alive = false; + try { + alive = Boolean(isPidAlive(pid)); + } catch {} + if (!alive) continue; + let runtimeProcess = false; + try { + runtimeProcess = isRuntimeProcess(pid) === true; + } catch {} + if (!runtimeProcess) continue; + let processStartIdentity = null; + try { + processStartIdentity = getRuntimeProcessStartIdentity(pid); + } catch {} + identities.set( + pid, + isDarwinProcessStartIdentity(processStartIdentity) ? processStartIdentity : null, + ); + } + return identities; +} + +function sameRuntimeProcessIdentitySet(expected, actual) { + return expected.size === actual.size + && [...expected].every(([pid, identity]) => actual.get(pid) === identity); +} + +function runtimeReceiptBatchProofIsStable({ + storageRoot, + runtimePath, + initialPublicationStateRead, + initialProcessIdentities, + proofs, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartIdentity, + beforeRuntimeBatchReceiptRead, + batchAttempt, +}) { + if (!proofs.length) return true; + const publicationStateBefore = readRuntimePublicationStateRecord(storageRoot); + let stable = ( + publicationStateOwnsRuntimeReceiptProofs(initialPublicationStateRead, proofs) + && publicationStateOwnsRuntimeReceiptProofs(publicationStateBefore, proofs) + ); + for (const proof of proofs) { + if (typeof beforeRuntimeBatchReceiptRead === "function") { + try { + beforeRuntimeBatchReceiptRead({ + pid: proof.pid, + entryName: proof.entryName, + batchAttempt, + }); + } catch { + stable = false; + } + } + let currentRead = null; + try { + currentRead = readRuntimeReceiptRecord( + proof.receiptPath, + proof.entryName, + proof.processStartedAtUnixMs, + { required: false }, + ); + } catch { + stable = false; + continue; + } + if ( + currentRead.missing + || currentRead.stale + || !proof.receiptBytes.equals(currentRead.record.bytes) + || safeReceiptPublicationGeneration(currentRead.record.value) !== proof.publicationGeneration + ) { + stable = false; + } + } + const publicationStateAfter = readRuntimePublicationStateRecord(storageRoot); + if (!publicationStateOwnsRuntimeReceiptProofs(publicationStateAfter, proofs)) { + stable = false; + } + try { + const finalProcessIdentities = readRuntimeProcessIdentitySet({ + runtimePath, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartIdentity, + }); + if (!sameRuntimeProcessIdentitySet(initialProcessIdentities, finalProcessIdentities)) { + stable = false; + } + } catch { + stable = false; + } + return stable; +} + +export function readCodexPromptRuntimeReceipts({ + storageRoot = defaultCodexPromptStorageRoot(), + isPidAlive = defaultPidAlive, + isRuntimeProcess = defaultCodexRuntimeProcess, + getRuntimeProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity = defaultCodexRuntimeProcessStartIdentity, + beforeRuntimeSnapshotRead = null, + beforeRuntimeBatchReceiptRead = null, +} = {}) { + const runtimePath = path.join(storageRoot, RUNTIME_DIR); + assertNoPromptStorageSymlinks(runtimePath); + if (!assertPrivateDirectory(storageRoot, "Codex prompt storage", { required: false })) return []; + if (!assertPrivateDirectory(runtimePath, "Codex prompt runtime directory", { required: false })) return []; + for ( + let batchAttempt = 0; + batchAttempt < MAX_RUNTIME_RECEIPT_READ_ATTEMPTS; + batchAttempt += 1 + ) { + const entries = runtimeReceiptEntries(runtimePath); + const initialPublicationStateRead = readRuntimePublicationStateRecord(storageRoot); + const initialProcessIdentities = new Map(); + const proofs = []; + const receipts = entries.flatMap((entry) => { + const filenamePid = Number(path.basename(entry.name, ".json")); + if (!Number.isSafeInteger(filenamePid) || filenamePid <= 0 || !isPidAlive(filenamePid)) return []; + let identityVerified = false; + try { + identityVerified = isRuntimeProcess(filenamePid) === true; + } catch {} + if (!identityVerified) return []; + let processStartIdentity = null; + try { + processStartIdentity = getRuntimeProcessStartIdentity(filenamePid); + } catch {} + initialProcessIdentities.set( + filenamePid, + isDarwinProcessStartIdentity(processStartIdentity) ? processStartIdentity : null, + ); + if (!isDarwinProcessStartIdentity(processStartIdentity)) { + return [unverifiedRuntimeReceipt( + filenamePid, + null, + `Could not verify the exact start identity of Codex process ${filenamePid}.`, + )]; + } + let processStartedAtUnixMs = null; + try { + processStartedAtUnixMs = getRuntimeProcessStartUnixMs(filenamePid); + } catch {} + if (!Number.isFinite(processStartedAtUnixMs) || processStartedAtUnixMs < 0) { + return [unverifiedRuntimeReceipt( + filenamePid, + null, + `Could not verify the start time of Codex process ${filenamePid}.`, + processStartIdentity, + )]; + } + const receiptPath = path.join(runtimePath, entry.name); + const firstRead = readRuntimeReceiptRecord( + receiptPath, + entry.name, + processStartedAtUnixMs, + { required: false }, + ); + if (firstRead.missing) return []; + if (firstRead.stale) { + return [unverifiedRuntimeReceipt( + filenamePid, + processStartedAtUnixMs, + `Codex prompt receipt predates the current process start: ${entry.name}`, + processStartIdentity, + )]; + } + let currentRecord = firstRead.record; + let normalized = null; + for (let attempt = 0; attempt < MAX_RUNTIME_RECEIPT_READ_ATTEMPTS; attempt += 1) { + const publicationStateRead = readRuntimePublicationStateRecord(storageRoot); + let normalizationError = null; + if (publicationStateRead.record) { + try { + normalized = normalizeRuntimeReceipt(currentRecord.value, { + entryName: entry.name, + filenamePid, + runtimePath, + processStartedAtUnixMs, + processStartIdentity, + publicationState: publicationStateRead.record.value, + beforeRuntimeSnapshotRead, + }); + } catch (error) { + normalizationError = error; + } + } else { + normalized = unverifiedRuntimeReceipt( + filenamePid, + processStartedAtUnixMs, + publicationStateRead.error.message, + processStartIdentity, + safeReceiptPublicationGeneration(currentRecord.value), + ); + } + const latestRead = readRuntimeReceiptRecord( + receiptPath, + entry.name, + processStartedAtUnixMs, + { required: false }, + ); + const latestPublicationStateRead = readRuntimePublicationStateRecord(storageRoot); + if (latestRead.missing) { + normalized = unverifiedRuntimeReceipt( + filenamePid, + processStartedAtUnixMs, + `Codex prompt receipt was removed while its publication was being verified: ${entry.name}`, + processStartIdentity, + safeReceiptPublicationGeneration(currentRecord.value), + ); + break; + } + if (latestRead.stale) { + return [unverifiedRuntimeReceipt( + filenamePid, + processStartedAtUnixMs, + `Codex prompt receipt predates the current process start: ${entry.name}`, + processStartIdentity, + )]; + } + const latestRecord = latestRead.record; + const receiptStable = currentRecord.bytes.equals(latestRecord.bytes); + const publicationStateStable = ( + publicationStateRead.record + && latestPublicationStateRead.record + && publicationStateRead.record.value.runtimeOwnerGenerations[String(filenamePid)] + === latestPublicationStateRead.record.value.runtimeOwnerGenerations[String(filenamePid)] + ); + const publicationStateUnavailable = ( + !publicationStateRead.record + && !latestPublicationStateRead.record + ); + if (receiptStable && (publicationStateStable || publicationStateUnavailable)) { + if (normalizationError) throw normalizationError; + break; + } + if (attempt + 1 >= MAX_RUNTIME_RECEIPT_READ_ATTEMPTS) { + normalized = unverifiedRuntimeReceipt( + filenamePid, + processStartedAtUnixMs, + `Codex prompt receipt or publication state changed repeatedly while its catalog snapshot was being verified: ${entry.name}`, + processStartIdentity, + safeReceiptPublicationGeneration(latestRecord.value), + ); + break; + } + currentRecord = latestRecord; + } + let processStartIdentityAfter = null; + try { + processStartIdentityAfter = getRuntimeProcessStartIdentity(filenamePid); + } catch {} + if ( + !isDarwinProcessStartIdentity(processStartIdentityAfter) + || processStartIdentityAfter !== processStartIdentity + ) { + return [unverifiedRuntimeReceipt( + filenamePid, + processStartedAtUnixMs, + `Codex process ${filenamePid} changed generation while its prompt receipt was being verified.`, + processStartIdentityAfter, + )]; + } + if (normalized.identityVerified) { + proofs.push({ + pid: filenamePid, + entryName: entry.name, + receiptPath, + processStartedAtUnixMs, + publicationGeneration: normalized.publicationGeneration, + receiptBytes: currentRecord.bytes, + }); + } + return [normalized]; + }); + if (runtimeReceiptBatchProofIsStable({ + storageRoot, + runtimePath, + initialPublicationStateRead, + initialProcessIdentities, + proofs, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartIdentity, + beforeRuntimeBatchReceiptRead, + batchAttempt, + })) { + return receipts; + } + if (batchAttempt + 1 >= MAX_RUNTIME_RECEIPT_READ_ATTEMPTS) { + return receipts.map((receipt) => ( + receipt.identityVerified + ? unverifiedRuntimeReceipt( + receipt.pid, + receipt.processStartedAtUnixMs, + "Codex prompt runtime receipts changed repeatedly while a consistent batch was being verified.", + receipt.processStartIdentity, + receipt.publicationGeneration, + ) + : receipt + )); + } + } + return []; +} + +function runtimeTargetProof(receipt, targetId) { + if (!receipt.catalog) return { hash: "", text: null }; + const snapshotTarget = targetLookup(receipt.catalog).get(targetId); + if (!snapshotTarget) return { hash: "", text: null }; + if (snapshotTarget.sourceTargetId !== null && snapshotTarget.runtimeEffectiveHash !== null) { + const exactHash = receipt.loadedHashes[targetId] || ""; + const exactSource = receipt.sourceTargetIds[targetId] || ""; + if ( + exactHash !== snapshotTarget.runtimeEffectiveHash + || exactSource !== snapshotTarget.sourceTargetId + ) { + return { hash: "", text: null }; + } + return { + hash: exactHash, + text: snapshotTarget.runtimeEffectiveText, + }; + } + const snapshotHash = snapshotTarget.runtimeEffectiveHash || snapshotTarget.officialHash || ""; + const snapshotText = snapshotTarget.runtimeEffectiveHash === snapshotHash + ? snapshotTarget.runtimeEffectiveText + : snapshotTarget.officialHash === snapshotHash + ? snapshotTarget.official + : null; + return { + hash: snapshotHash, + text: snapshotText, + }; +} + +function loadedStateForTarget(target, resolved, receipts, catalog, overrides) { + const alive = receipts.filter((receipt) => receipt.alive); + if (target.runtimeStatus === "pattern") { + return { + status: "pattern", + loaded: null, + loadedHash: "", + processes: alive.length, + }; + } + if (!target.officialHash && !target.runtimeEffectiveHash && !resolved.override) { + return { + status: "catalogued", + loaded: null, + loadedHash: "", + processes: alive.length, + }; + } + if (!alive.length) { + return { status: resolved.override ? "pending_next_launch" : "not_running", loaded: null, loadedHash: "", processes: 0 }; + } + const manifestCurrent = alive.every((receipt) => ( + receipt.manifestRevision === overrides.revision + && receipt.manifestHash === overrides.manifestHash + )); + const catalogCurrent = alive.every((receipt) => receipt.catalogRevision === catalog.catalogRevision); + const identityVerified = alive.every((receipt) => receipt.identityVerified === true); + const proofs = alive.map((receipt) => runtimeTargetProof(receipt, target.id)); + if (proofs.some((proof) => !proof.hash)) { + return { + status: "unverified_runtime", + loaded: null, + loadedHash: "", + processes: alive.length, + manifestCurrent, + catalogCurrent, + identityVerified, + }; + } + const hashes = [...new Set(proofs.map((proof) => proof.hash))]; + const knownText = (hash) => { + const snapshotText = proofs.find((proof) => proof.hash === hash && proof.text !== null)?.text; + if (snapshotText !== undefined) return snapshotText; + if (hash === resolved.effectiveHash) return resolved.effective; + if (hash === target.runtimeEffectiveHash) return target.runtimeEffectiveText; + if (hash === target.officialHash) return target.official; + return null; + }; + if (hashes.length > 1) { + return { + status: "mixed_versions", + loaded: null, + loadedHash: "", + loadedHashes: hashes, + processes: alive.length, + manifestCurrent, + catalogCurrent, + identityVerified, + }; + } + const loadedHash = hashes[0] || ""; + if (!loadedHash) { + return { + status: "unverified_runtime", + loaded: null, + loadedHash: "", + processes: alive.length, + manifestCurrent, + catalogCurrent, + identityVerified, + }; + } + if (loadedHash === resolved.effectiveHash) { + const status = resolved.override + ? "loaded" + : loadedHash === target.officialHash + ? "official_loaded" + : "effective_loaded"; + return { + status, + loaded: knownText(loadedHash), + loadedHash, + processes: alive.length, + manifestCurrent, + catalogCurrent, + identityVerified, + }; + } + return { + status: target.editable ? "restart_required" : "loaded_differs", + loaded: knownText(loadedHash), + loadedHash, + processes: alive.length, + manifestCurrent, + catalogCurrent, + identityVerified, + }; +} + +function statusLabel(status) { + return ({ + conflict: "Conflict", + loaded: "Runtime loaded", + official_loaded: "Official loaded by runtime", + effective_loaded: "Effective loaded by runtime", + pending_next_launch: "Pending next launch", + not_running: "Codex not running", + mixed_versions: "Mixed runtime-loaded state", + unverified_runtime: "Runtime-loaded state unavailable", + restart_required: "Restart required", + loaded_differs: "Loaded prompt differs", + catalogued: "Catalogued", + pattern: "Target pattern", + })[status] || "Unknown"; +} + +function currentState({ catalog, overrides, receipts, target }) { + const override = currentOverrideForTarget(overrides, target); + const resolved = resolveOverride(target, override); + const loaded = loadedStateForTarget(target, resolved, receipts, catalog, overrides); + const status = resolved.conflict ? "conflict" : loaded.status; + return { + resolved, + loaded, + status, + statusLabel: statusLabel(status), + restartRequired: ["restart_required", "pending_next_launch", "mixed_versions", "loaded_differs"].includes(status), + catalogRevision: catalog.catalogRevision, + overridesRevision: overrides.revision, + }; +} + +export function readCodexPromptCenterSummary({ + storageRoot = defaultCodexPromptStorageRoot(), + catalog: suppliedCatalog = null, + isPidAlive = defaultPidAlive, + isRuntimeProcess = defaultCodexRuntimeProcess, + getRuntimeProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity = defaultCodexRuntimeProcessStartIdentity, +} = {}) { + const runtimeCatalog = readCodexPromptCatalog({ storageRoot, catalog: suppliedCatalog }); + const overrides = readCodexPromptOverrides({ storageRoot }); + const catalog = catalogWithOrphanedOverrides(runtimeCatalog, overrides); + const receipts = readCodexPromptRuntimeReceipts({ + storageRoot, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + }); + const groups = catalog.groups.map((group) => ({ + id: group.id, + label: group.label, + targets: group.targets.map((target) => { + const state = currentState({ catalog, overrides, receipts, target }); + return { + id: target.id, + label: target.label, + description: target.description, + source: target.source, + kind: target.kind, + role: target.role, + editable: target.editable, + readOnlyReason: target.readOnlyReason, + securityClass: target.securityClass, + runtimeStatus: target.runtimeStatus, + targetPattern: target.targetPattern, + sourceTargetId: target.sourceTargetId, + overrideStrategy: target.overrideStrategy, + overrideConflict: target.overrideConflict, + officialContentAvailable: target.officialContentAvailable, + status: state.status, + statusLabel: state.statusLabel, + hasOverride: Boolean(state.resolved.overrideHash), + overrideInherited: state.resolved.overrideInherited, + conflict: state.resolved.conflict, + officialHash: target.officialHash, + effectiveHash: state.resolved.effectiveHash, + loadedHash: state.loaded.loadedHash, + }; + }), + })); + return { + schemaVersion: catalog.protocolVersion, + codexVersion: catalog.codexVersion, + catalogRevision: catalog.catalogRevision, + overridesRevision: overrides.revision, + generatedAt: catalog.generatedAt, + groups, + summary: { + targets: groups.reduce((total, group) => total + group.targets.length, 0), + editable: groups.reduce((total, group) => total + group.targets.filter((target) => target.editable).length, 0), + overrides: overrides.overrides.length, + conflicts: groups.reduce((total, group) => total + group.targets.filter((target) => target.status === "conflict").length, 0), + restartRequired: groups.reduce((total, group) => total + group.targets.filter((target) => ( + ["restart_required", "pending_next_launch", "mixed_versions", "loaded_differs"].includes(target.status) + )).length, 0), + liveProcesses: receipts.filter((receipt) => receipt.alive).length, + }, + restartMessage: CODEX_PROMPT_RESTART_MESSAGE, + }; +} + +function promptTargetResponse(target, catalog, overrides, receipts, { commitWarning = "" } = {}) { + const state = currentState({ catalog, overrides, receipts, target }); + return { + ...target, + catalogRevision: catalog.catalogRevision, + overridesRevision: overrides.revision, + effective: state.resolved.effective, + effectiveHash: state.resolved.effectiveHash, + overrideHash: state.resolved.overrideHash, + overrideInherited: state.resolved.overrideInherited, + overrideSourceTargetId: state.resolved.override?.targetId || null, + patches: state.resolved.override?.patches || [], + replacement: state.resolved.override?.replacement ?? null, + loaded: state.loaded.loaded, + loadedHash: state.loaded.loadedHash, + loadedHashes: state.loaded.loadedHashes || [], + liveProcesses: state.loaded.processes, + runtimeIdentityVerified: state.loaded.identityVerified ?? null, + runtimeManifestCurrent: state.loaded.manifestCurrent ?? null, + runtimeCatalogCurrent: state.loaded.catalogCurrent ?? null, + status: state.status, + statusLabel: state.statusLabel, + conflict: state.resolved.conflict, + restartRequired: state.restartRequired, + restartMessage: CODEX_PROMPT_RESTART_MESSAGE, + ...(commitWarning ? { commitWarning } : {}), + }; +} + +export function readCodexPromptTarget(targetId, { + storageRoot = defaultCodexPromptStorageRoot(), + catalog: suppliedCatalog = null, + isPidAlive = defaultPidAlive, + isRuntimeProcess = defaultCodexRuntimeProcess, + getRuntimeProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity = defaultCodexRuntimeProcessStartIdentity, +} = {}) { + const runtimeCatalog = readCodexPromptCatalog({ storageRoot, catalog: suppliedCatalog }); + const overrides = readCodexPromptOverrides({ storageRoot }); + const catalog = catalogWithOrphanedOverrides(runtimeCatalog, overrides); + const receipts = readCodexPromptRuntimeReceipts({ + storageRoot, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + }); + const target = findTarget(catalog, targetId); + return promptTargetResponse(target, catalog, overrides, receipts); +} + +function assertCurrentDraft(payload, catalog, overrides, target) { + if (String(payload.catalogRevision || "") !== catalog.catalogRevision) { + throw promptCenterError( + "The Codex prompt catalog changed. Refresh before saving.", + 409, + "codex_prompt_catalog_stale", + { expected: catalog.catalogRevision, received: String(payload.catalogRevision || "") }, + ); + } + const receivedOfficialHash = payload.officialHash === null ? null : String(payload.officialHash || ""); + if (receivedOfficialHash !== target.officialHash) { + throw promptCenterError( + "The official Codex prompt changed. Refresh before saving.", + 409, + "codex_prompt_official_stale", + { expected: target.officialHash, received: receivedOfficialHash }, + ); + } + const currentOverride = currentOverrideForTarget(overrides, target); + const currentOverrideHash = currentOverride?.overrideHash || ""; + if (String(payload.overrideHash || "") !== currentOverrideHash) { + throw promptCenterError( + "This Codex prompt override changed in another editor. Refresh before saving.", + 409, + "codex_prompt_override_stale", + { expected: currentOverrideHash, received: String(payload.overrideHash || "") }, + ); + } + return currentOverride; +} + +export function validateCodexPromptDraft(payload, { + storageRoot = defaultCodexPromptStorageRoot(), + catalog: suppliedCatalog = null, +} = {}) { + const catalog = readCodexPromptCatalog({ storageRoot, catalog: suppliedCatalog }); + const overrides = readCodexPromptOverrides({ storageRoot }); + assertNoCatalogOverrideIssues(catalog, overrides); + const target = findTarget(catalog, payload?.targetId); + if (!target.editable) { + throw promptCenterError( + target.readOnlyReason || "This Codex prompt is read-only.", + 403, + "codex_prompt_read_only", + { targetId: target.id }, + ); + } + const currentOverride = assertCurrentDraft(payload || {}, catalog, overrides, target); + const effective = normalizePrompt(payload?.effective ?? "", `Effective prompt ${target.id}`); + validateRequiredPlaceholders(target, effective); + if (!target.officialContentAvailable && target.overrideStrategy !== "replacement") { + throw promptCenterError( + "The installed Codex build did not publish this target's official content, so Context Room cannot derive a safe exact overlay.", + 409, + "codex_prompt_official_content_unavailable", + { targetId: target.id }, + ); + } + const materializesInheritedOverride = Boolean( + currentOverride + && currentOverride.targetId !== target.id + && effective === target.official + ); + const replacement = target.overrideStrategy === "replacement" ? effective : null; + const patches = replacement === null + ? materializesInheritedOverride + ? [{ before: target.official, after: target.official, expectedMatches: 1 }] + : createCodexPromptPatches(target.official, effective) + : []; + const applied = replacement === null + ? (patches.length ? applyCodexPromptPatches(target.official, patches, { targetId: target.id }) : target.official) + : replacement; + if (applied !== effective) { + throw promptCenterError("The exact prompt overlay did not reproduce the draft", 422, "codex_prompt_overlay_invalid"); + } + return { + valid: true, + targetId: target.id, + changed: effective !== target.official || materializesInheritedOverride, + bytes: byteLength(effective), + estimatedTokens: estimateCodexPromptTokens(effective), + maxBytes: MAX_CODEX_PROMPT_BYTES, + maxEstimatedTokens: MAX_CODEX_PROMPT_ESTIMATED_TOKENS, + officialHash: target.officialHash, + effectiveHash: sha256(effective), + overrideHash: String(payload?.overrideHash || ""), + catalogRevision: catalog.catalogRevision, + overridesRevision: overrides.revision, + patches, + replacement, + }; +} + +function normalizedCommittedOverrides(value) { + return normalizeOverrides(value, { manifestHash: sha256(serializePromptState(value)) }); +} + +function persistedOverridesState(overrides) { + return { + schemaVersion: CODEX_PROMPT_PROTOCOL_VERSION, + revision: overrides.revision, + overrides: overrides.overrides.map(({ overrideHash: _overrideHash, ...override }) => override), + }; +} + +function backupCurrentOverrides(storageRoot, overrides) { + return secureAtomicWritePromptState( + path.join(storageRoot, LAST_KNOWN_GOOD_FILE), + persistedOverridesState(overrides), + ); +} + +function promptTargetResponseAfterCommit( + target, + catalog, + overrides, + storageRoot, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + commitWarnings = [], +) { + try { + const receipts = readCodexPromptRuntimeReceipts({ + storageRoot, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + }); + return promptTargetResponse(target, catalog, overrides, receipts, { + commitWarning: combinePromptWarnings(commitWarnings), + }); + } catch (error) { + return promptTargetResponse(target, catalog, overrides, [], { + commitWarning: combinePromptWarnings( + commitWarnings, + `Override commit succeeded, but runtime-loaded status could not be refreshed: ${error.message}`, + ), + }); + } +} + +export function writeCodexPromptOverride(payload, { + storageRoot = defaultCodexPromptStorageRoot(), + catalog: suppliedCatalog = null, + isPidAlive = defaultPidAlive, + isRuntimeProcess = defaultCodexRuntimeProcess, + getRuntimeProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity = defaultCodexRuntimeProcessStartIdentity, + getProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, +} = {}) { + return withPromptWriteLock(storageRoot, isPidAlive, getProcessStartUnixMs, () => { + const validation = validateCodexPromptDraft(payload, { storageRoot, catalog: suppliedCatalog }); + if ( + Object.hasOwn(payload || {}, "acknowledgeHighContext") + && typeof payload.acknowledgeHighContext !== "boolean" + ) { + throw promptCenterError( + "acknowledgeHighContext must be a boolean", + 422, + "codex_prompt_high_context_ack_invalid", + ); + } + if ( + validation.changed + && validation.estimatedTokens > CODEX_PROMPT_HIGH_CONTEXT_CONFIRM_TOKENS + && payload?.acknowledgeHighContext !== true + ) { + throw promptCenterError( + `Saving this prompt requires explicit confirmation because it is estimated at ${validation.estimatedTokens} tokens`, + 422, + "codex_prompt_high_context_ack_required", + { + threshold: CODEX_PROMPT_HIGH_CONTEXT_CONFIRM_TOKENS, + estimatedTokens: validation.estimatedTokens, + }, + ); + } + if (!validation.changed) { + return deleteCodexPromptOverrideUnlocked(payload, { + storageRoot, + catalog: suppliedCatalog, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + }); + } + const catalog = readCodexPromptCatalog({ storageRoot, catalog: suppliedCatalog }); + const overrides = readCodexPromptOverrides({ storageRoot }); + assertNoCatalogOverrideIssues(catalog, overrides); + const target = findTarget(catalog, validation.targetId); + assertCurrentDraft(payload || {}, catalog, overrides, target); + const nextOverride = { + targetId: validation.targetId, + officialHash: validation.officialHash, + patches: validation.patches, + replacement: validation.replacement, + }; + const nextOverrides = overrides.overrides + .filter((item) => item.targetId !== validation.targetId) + .map(({ overrideHash: _overrideHash, ...item }) => item); + nextOverrides.push(nextOverride); + nextOverrides.sort((left, right) => left.targetId.localeCompare(right.targetId)); + const nextState = { + schemaVersion: CODEX_PROMPT_PROTOCOL_VERSION, + revision: nextPromptManifestRevision(overrides.revision), + overrides: nextOverrides, + }; + const backupWrite = backupCurrentOverrides(storageRoot, overrides); + const manifestWrite = secureAtomicWritePromptState(path.join(storageRoot, OVERRIDES_FILE), nextState); + const committedOverrides = normalizedCommittedOverrides(nextState); + return promptTargetResponseAfterCommit( + target, + catalog, + committedOverrides, + storageRoot, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + [backupWrite.commitWarning, manifestWrite.commitWarning], + ); + }); +} + +function deleteCodexPromptOverrideUnlocked(payload, { + storageRoot = defaultCodexPromptStorageRoot(), + catalog: suppliedCatalog = null, + isPidAlive = defaultPidAlive, + isRuntimeProcess = defaultCodexRuntimeProcess, + getRuntimeProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity = defaultCodexRuntimeProcessStartIdentity, +} = {}) { + const runtimeCatalog = readCodexPromptCatalog({ storageRoot, catalog: suppliedCatalog }); + const overrides = readCodexPromptOverrides({ storageRoot }); + const catalog = catalogWithOrphanedOverrides(runtimeCatalog, overrides); + const target = findTarget(catalog, payload?.targetId); + const currentOverride = assertCurrentDraft(payload || {}, catalog, overrides, target); + if (currentOverride && currentOverride.targetId !== target.id) { + throw promptCenterError( + `This prompt inherits ${currentOverride.targetId}; restore that source target instead.`, + 409, + "codex_prompt_inherited_override", + { targetId: target.id, sourceTargetId: currentOverride.targetId }, + ); + } + const exists = overrides.overrides.some((item) => item.targetId === target.id); + if (!exists) { + return readCodexPromptTarget(target.id, { + storageRoot, + catalog: suppliedCatalog, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + }); + } + const nextState = { + schemaVersion: CODEX_PROMPT_PROTOCOL_VERSION, + revision: nextPromptManifestRevision(overrides.revision), + overrides: overrides.overrides + .filter((item) => item.targetId !== target.id) + .map(({ overrideHash: _overrideHash, ...item }) => item), + }; + const backupWrite = backupCurrentOverrides(storageRoot, overrides); + const manifestWrite = secureAtomicWritePromptState(path.join(storageRoot, OVERRIDES_FILE), nextState); + const committedOverrides = normalizedCommittedOverrides(nextState); + return promptTargetResponseAfterCommit( + target, + catalog, + committedOverrides, + storageRoot, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + [backupWrite.commitWarning, manifestWrite.commitWarning], + ); +} + +export function deleteCodexPromptOverride(payload, { + storageRoot = defaultCodexPromptStorageRoot(), + catalog: suppliedCatalog = null, + isPidAlive = defaultPidAlive, + isRuntimeProcess = defaultCodexRuntimeProcess, + getRuntimeProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity = defaultCodexRuntimeProcessStartIdentity, + getProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, +} = {}) { + return withPromptWriteLock( + storageRoot, + isPidAlive, + getProcessStartUnixMs, + () => deleteCodexPromptOverrideUnlocked(payload, { + storageRoot, + catalog: suppliedCatalog, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + }), + ); +} + +export function defaultCodexPromptStorageRoot() { + return path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "prompt-overrides"); +} + +export function createCodexPromptCenterProvider({ + storageRoot = defaultCodexPromptStorageRoot(), + catalog = null, + isPidAlive = defaultPidAlive, + isRuntimeProcess = defaultCodexRuntimeProcess, + getRuntimeProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity = defaultCodexRuntimeProcessStartIdentity, + getProcessStartUnixMs = defaultCodexRuntimeProcessStartUnixMs, +} = {}) { + const options = { + storageRoot, + catalog, + isPidAlive, + isRuntimeProcess, + getRuntimeProcessStartUnixMs, + getRuntimeProcessStartIdentity, + getProcessStartUnixMs, + }; + return Object.freeze({ + readSummary: () => readCodexPromptCenterSummary(options), + readTarget: (targetId) => readCodexPromptTarget(targetId, options), + validateDraft: (payload) => validateCodexPromptDraft(payload, options), + writeOverride: (payload) => writeCodexPromptOverride(payload, options), + deleteOverride: (payload) => deleteCodexPromptOverride(payload, options), + refresh: () => readCodexPromptCenterSummary(options), + }); +} diff --git a/src/context_room.mjs b/src/context_room.mjs index d3a01d6..b4d5b28 100644 --- a/src/context_room.mjs +++ b/src/context_room.mjs @@ -5,7 +5,7 @@ import net from "node:net"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; -import { createHash } from "node:crypto"; +import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; import { fileURLToPath } from "node:url"; import { Worker } from "node:worker_threads"; import { collectInlinePathReferences, parseDocMetadata, renderDocMetadataTemplateValues } from "./doc_metadata.mjs"; @@ -34,6 +34,13 @@ import { recordContextHubProjectOpened, registerContextHubProject, } from "./context_hub.mjs"; +import { + CODEX_PROMPT_HIGH_CONTEXT_CONFIRM_TOKENS, + MAX_CODEX_PROMPT_BYTES, + MAX_CODEX_PROMPT_ESTIMATED_TOKENS, + MAX_CODEX_PROMPT_REQUEST_BYTES, + createCodexPromptCenterProvider, +} from "./codex_prompt_center.mjs"; export { DOC_METADATA_KINDS, DOC_METADATA_STATUSES, parseDocMetadata } from "./doc_metadata.mjs"; @@ -56,6 +63,7 @@ export const AGENT_CONTEXT_FILE = `${CONFIG_DIR}/README.md`; const LEGACY_AGENT_CONTEXT_FILE = `${AGENT_CONTEXT_DIR}/README.md`; const AGENT_CONTEXT_ASSET_FILENAMES = [ "agent-configuration.md", + "features/codex-prompt-center.md", "html-visual-documents.md", "html-visual-patterns.md", "context-room-visual-components.html", @@ -5576,11 +5584,12 @@ export function syncContextRoomAgentContext(root = process.cwd()) { const targetRoot = path.join(projectRoot, AGENT_CONTEXT_DIR); const assets = [ [path.join(sourceRoot, "agent-configuration.md"), AGENT_CONTEXT_ASSET_FILENAMES[0]], - [path.join(sourceRoot, "features", "html-visual-documents.md"), AGENT_CONTEXT_ASSET_FILENAMES[1]], - [path.join(sourceRoot, "features", "html-visual-patterns.md"), AGENT_CONTEXT_ASSET_FILENAMES[2]], - [path.join(sourceRoot, "context-room-visual-components.html"), AGENT_CONTEXT_ASSET_FILENAMES[3]], - [path.join(sourceRoot, "context-room-data-visual-components.html"), AGENT_CONTEXT_ASSET_FILENAMES[4]], - [path.join(sourceRoot, "features", "shared-context.md"), AGENT_CONTEXT_ASSET_FILENAMES[5]], + [path.join(sourceRoot, "features", "codex-prompt-center.md"), AGENT_CONTEXT_ASSET_FILENAMES[1]], + [path.join(sourceRoot, "features", "html-visual-documents.md"), AGENT_CONTEXT_ASSET_FILENAMES[2]], + [path.join(sourceRoot, "features", "html-visual-patterns.md"), AGENT_CONTEXT_ASSET_FILENAMES[3]], + [path.join(sourceRoot, "context-room-visual-components.html"), AGENT_CONTEXT_ASSET_FILENAMES[4]], + [path.join(sourceRoot, "context-room-data-visual-components.html"), AGENT_CONTEXT_ASSET_FILENAMES[5]], + [path.join(sourceRoot, "features", "shared-context.md"), AGENT_CONTEXT_ASSET_FILENAMES[6]], ]; const missing = assets.filter(([source]) => !fs.existsSync(source)).map(([source]) => source); if (missing.length) throw new Error(`Context Room agent context is incomplete: ${missing.join(", ")}`); @@ -5702,9 +5711,32 @@ This compatibility file is generated by Context Room. ]; for (const [source, fileName] of assets) { const sourceContent = fs.readFileSync(source, "utf8"); - const relocatedContent = /[.]md$/i.test(fileName) + let relocatedContent = /[.]md$/i.test(fileName) ? sourceContent.replaceAll("../context-room-", "context-room-") : sourceContent; + if (fileName === "features/codex-prompt-center.md") { + relocatedContent = relocatedContent + .replaceAll( + "../../schemas/", + "https://unpkg.com/context-room@latest/schemas/", + ) + .replaceAll( + "](context-hub.md)", + "](https://unpkg.com/context-room@latest/docs/features/context-hub.md)", + ) + .replaceAll( + "](settings.md)", + "](https://unpkg.com/context-room@latest/docs/features/settings.md)", + ); + } + if (fileName === "features/shared-context.md") { + for (const linkedFeature of ["context-hub.md", "documentation-lifecycle.md", "review-queue.md"]) { + relocatedContent = relocatedContent.replaceAll( + `](${linkedFeature})`, + `](https://unpkg.com/context-room@latest/docs/features/${linkedFeature})`, + ); + } + } files.push([path.join(targetRoot, fileName), relocatedContent]); } @@ -7201,15 +7233,83 @@ async function listenContextRoomServer(server, port) { }); } +function contextRoomServerPort(server, configuredPort) { + const address = server.address(); + return address && typeof address === "object" ? Number(address.port) : Number(configuredPort); +} + +function normalizeFrameAncestorPorts(values = []) { + return [...new Set((Array.isArray(values) ? values : []).map(Number).filter((value) => ( + Number.isSafeInteger(value) && value >= 1 && value <= 65_535 + )))]; +} + +function frameAncestorsPolicy(ports = []) { + const sources = ["'self'"]; + for (const port of normalizeFrameAncestorPorts(ports)) { + sources.push(`http://127.0.0.1:${port}`, `http://localhost:${port}`); + } + return `frame-ancestors ${sources.join(" ")}`; +} + +function isLoopbackRemoteAddress(value) { + const address = String(value || "").toLowerCase(); + return address === "127.0.0.1" + || address === "::1" + || address === "::ffff:127.0.0.1"; +} + +function isLoopbackPromptAuthority(value, server, configuredPort) { + const authority = String(value || "").trim(); + if (!authority) return false; + try { + const parsed = new URL(`http://${authority}`); + const hostname = parsed.hostname.toLowerCase(); + const port = Number(parsed.port || 80); + return parsed.username === "" + && parsed.password === "" + && ["127.0.0.1", "localhost", "[::1]"].includes(hostname) + && port === contextRoomServerPort(server, configuredPort); + } catch { + return false; + } +} + +function isTrustedPromptOrigin(value, server, configuredPort) { + const raw = String(value || "").trim(); + if (!raw) return true; + try { + const parsed = new URL(raw); + return parsed.protocol === "http:" + && isLoopbackPromptAuthority(parsed.host, server, configuredPort); + } catch { + return false; + } +} + +function secretHeaderMatches(expected, received) { + const expectedBytes = Buffer.from(String(expected), "utf8"); + const receivedBytes = Buffer.from(String(received || ""), "utf8"); + return expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes); +} + +function isCodexPromptRequest(req) { + return new URL(req.url || "/", "http://localhost").pathname.startsWith("/api/codex-prompts"); +} + export function createMemoryServer({ root = process.cwd(), port = DEFAULT_PORT, globalPreferencesPath = null, codexComposerInsert = insertIntoActiveCodexComposer, codexReferenceInsert = insertFileReferenceIntoActiveCodexComposer, + codexPromptCenter = createCodexPromptCenterProvider(), registerInHub = false, + frameAncestorPorts = [], } = {}) { const projectId = contextRoomProjectId(root); + const promptMutationNonce = randomBytes(32).toString("base64url"); + const trustedFrameAncestorPorts = normalizeFrameAncestorPorts(frameAncestorPorts); if (registerInHub && !process.env.NODE_TEST_CONTEXT && !fs.existsSync(path.join(path.resolve(root), SHARED_REVIEW_CONFIG))) { try { const connection = readSharedProjectConnection(root); @@ -7226,11 +7326,41 @@ export function createMemoryServer({ if (lastSelectedPath) void readBackgroundFileTask("file-diff", root, { path: lastSelectedPath }).catch(() => {}); const stopBackgroundWatch = watchBackgroundInputs(root); const server = http.createServer(async (req, res) => { + const promptRequest = isCodexPromptRequest(req); + const requestMutation = !["GET", "HEAD", "OPTIONS"].includes(req.method || "GET"); + const trustedHost = isLoopbackPromptAuthority(req.headers.host, server, port); + const trustedPeer = isLoopbackRemoteAddress(req.socket.remoteAddress); + if (!trustedHost || !trustedPeer) { + sendJson(res, 403, { + error: "Context Room accepts requests only through its local loopback address.", + code: promptRequest ? "codex_prompt_untrusted_origin" : "context_room_untrusted_host", + }); + return; + } + if (promptRequest) { + const crossSite = String(req.headers["sec-fetch-site"] || "").toLowerCase() === "cross-site"; + const trustedOrigin = isTrustedPromptOrigin(req.headers.origin, server, port) + && isTrustedPromptOrigin(req.headers.referer, server, port); + if (!trustedOrigin || crossSite) { + sendJson(res, 403, { + error: "Codex Prompt Center accepts requests only from this local Context Room page.", + code: "codex_prompt_untrusted_origin", + }); + return; + } + if (requestMutation && !secretHeaderMatches(promptMutationNonce, req.headers["x-context-room-prompt-nonce"])) { + sendJson(res, 403, { + error: "Reload Context Room before changing Codex prompts.", + code: "codex_prompt_nonce_required", + }); + return; + } + } res.setHeader("x-context-room-project", projectId); const expectedProjectId = String(req.headers["x-context-room-project"] || "").trim(); - const browserMutation = !["GET", "HEAD", "OPTIONS"].includes(req.method || "GET") + const browserMutation = requestMutation && Boolean(req.headers["sec-fetch-site"] || req.headers.origin || req.headers.referer); - if (!expectedProjectId && browserMutation) { + if (!expectedProjectId && (browserMutation || (promptRequest && requestMutation))) { sendJson(res, 409, { error: "Reload this Context Room tab before changing project state.", code: "context_room_project_identity_required", @@ -7252,6 +7382,9 @@ export function createMemoryServer({ await routeRequest(req, res, root, globalPreferencesPath, { codexComposerInsert, codexReferenceInsert, + codexPromptCenter, + promptMutationNonce, + frameAncestorPorts: trustedFrameAncestorPorts, startSharedReview: async ({ proposal, repository = "" }) => { if (fs.existsSync(path.join(path.resolve(root), SHARED_REVIEW_CONFIG))) { throw sharedRequestError("This Context Room already represents an exact proposal review", 409, "shared_context_review_already_open"); @@ -7276,7 +7409,16 @@ export function createMemoryServer({ reviewAgentInstructions: false, }); const reviewPort = await selectAvailableContextRoomPort(DEFAULT_PORT, { allowFallback: true }); - const reviewRoom = createMemoryServer({ root: result.reviewRoot, port: reviewPort, globalPreferencesPath, registerInHub: false }); + const reviewRoom = createMemoryServer({ + root: result.reviewRoot, + port: reviewPort, + globalPreferencesPath, + registerInHub: false, + frameAncestorPorts: [ + ...trustedFrameAncestorPorts, + contextRoomServerPort(server, port), + ], + }); await listenContextRoomServer(reviewRoom.server, reviewPort); sharedReviewServers.add(reviewRoom.server); reviewRoom.server.once("close", () => sharedReviewServers.delete(reviewRoom.server)); @@ -7301,7 +7443,16 @@ export function createMemoryServer({ const existing = contextHubProjectRooms.get(project.id); if (existing) return existing; const projectPort = await selectAvailableContextRoomPort(DEFAULT_PORT, { allowFallback: true }); - const projectRoom = createMemoryServer({ root: project.root, port: projectPort, globalPreferencesPath, registerInHub: false }); + const projectRoom = createMemoryServer({ + root: project.root, + port: projectPort, + globalPreferencesPath, + registerInHub: false, + frameAncestorPorts: [ + ...trustedFrameAncestorPorts, + contextRoomServerPort(server, port), + ], + }); await listenContextRoomServer(projectRoom.server, projectPort); contextHubProjectServers.add(projectRoom.server); projectRoom.server.once("close", () => contextHubProjectServers.delete(projectRoom.server)); @@ -7320,6 +7471,7 @@ export function createMemoryServer({ sendJson(res, Number(error.statusCode) || 500, { error: error.message, ...(error.code ? { code: error.code } : {}), + ...(error.details !== undefined ? { details: error.details } : {}), }); } finally { if (requestInvalidatesBackgroundCaches(req)) invalidateBackgroundCaches(root, { explicit: true }); @@ -7336,19 +7488,26 @@ export function createMemoryServer({ closeBackgroundWorkers(root); clearBackgroundCacheState(root); }); - return { server, root, port, projectId }; + return { server, root, port, projectId, promptMutationNonce }; } async function routeRequest(req, res, root, globalPreferencesPath = null, { codexComposerInsert = insertIntoActiveCodexComposer, codexReferenceInsert = insertFileReferenceIntoActiveCodexComposer, + codexPromptCenter = createCodexPromptCenterProvider(), + promptMutationNonce = "", + frameAncestorPorts = [], startSharedReview = null, startContextHubProject = null, } = {}) { const url = new URL(req.url, `http://${req.headers.host || "localhost"}`); if (req.method === "GET" && url.pathname === "/") { - sendHtml(res, renderAppHtml()); + sendHtml( + res, + renderAppHtml({ codexPromptMutationNonce: promptMutationNonce }), + { frameAncestorPorts }, + ); return; } if (req.method === "GET" && url.pathname === "/api/shared-context") { @@ -7359,6 +7518,44 @@ async function routeRequest(req, res, root, globalPreferencesPath = null, { sendJson(res, 200, contextHubUiState(root)); return; } + if (req.method === "GET" && url.pathname === "/api/codex-prompts") { + sendJson(res, 200, await codexPromptCenter.readSummary()); + return; + } + if (req.method === "GET" && url.pathname === "/api/codex-prompts/target") { + sendJson(res, 200, await codexPromptCenter.readTarget(url.searchParams.get("id") || "")); + return; + } + if (req.method === "POST" && url.pathname === "/api/codex-prompts/validate") { + const body = await readJsonBody(req, { maxBytes: MAX_CODEX_PROMPT_REQUEST_BYTES }); + sendJson(res, 200, await codexPromptCenter.validateDraft(body)); + return; + } + if (req.method === "POST" && url.pathname === "/api/codex-prompts/override") { + const body = await readJsonBody(req, { maxBytes: MAX_CODEX_PROMPT_REQUEST_BYTES }); + sendJson(res, 200, await codexPromptCenter.writeOverride(body)); + return; + } + if (req.method === "DELETE" && url.pathname === "/api/codex-prompts/override") { + const body = await readJsonBody(req, { maxBytes: MAX_CODEX_PROMPT_REQUEST_BYTES }); + sendJson(res, 200, await codexPromptCenter.deleteOverride(body)); + return; + } + if (req.method === "POST" && url.pathname === "/api/codex-prompts/refresh") { + const body = await readJsonBody(req, { maxBytes: MAX_CODEX_PROMPT_REQUEST_BYTES }); + if (!body || typeof body !== "object" || Array.isArray(body)) { + throw sharedRequestError( + "Codex prompt refresh requires a JSON object", + 400, + "codex_prompt_refresh_invalid", + ); + } + if (Object.keys(body).length) { + throw sharedRequestError("Codex prompt refresh does not accept request fields", 400, "codex_prompt_refresh_invalid"); + } + sendJson(res, 200, await codexPromptCenter.refresh()); + return; + } if (req.method === "POST" && url.pathname === "/api/context-hub/refresh") { sendJson(res, 200, contextHubUiState(root)); return; @@ -8178,11 +8375,56 @@ function isBlockedPath(relPath) { || lowered.includes("id_ed25519"); } -async function readJsonBody(req) { - let raw = ""; - for await (const chunk of req) raw += chunk; - if (!raw) return {}; - return JSON.parse(raw); +function formatRequestBodyLimit(maxBytes) { + if (maxBytes < 1_048_576) return `${Math.ceil(maxBytes / 1_024)} KiB`; + const mebibytes = maxBytes / 1_048_576; + return `${Number.isInteger(mebibytes) ? mebibytes : mebibytes.toFixed(1)} MiB`; +} + +async function readJsonBody(req, { maxBytes = Number.POSITIVE_INFINITY } = {}) { + const chunks = []; + let receivedBytes = 0; + for await (const chunk of req) { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + receivedBytes += buffer.length; + if (receivedBytes > maxBytes) { + throw sharedRequestError( + `Request body exceeds the ${formatRequestBodyLimit(maxBytes)} limit`, + 413, + "request_body_too_large", + ); + } + chunks.push(buffer); + } + if (!chunks.length) return {}; + let text; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)); + } catch { + throw sharedRequestError( + "Request body must be valid UTF-8 JSON", + 400, + "request_json_invalid", + ); + } + let value; + try { + value = JSON.parse(text); + } catch { + throw sharedRequestError( + "Request body must be valid JSON", + 400, + "request_json_invalid", + ); + } + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw sharedRequestError( + "Request body must be a JSON object", + 400, + "request_json_object_required", + ); + } + return value; } function sendJson(res, status, payload) { @@ -8194,10 +8436,11 @@ function sendJson(res, status, payload) { res.end(body); } -function sendHtml(res, body) { +function sendHtml(res, body, { frameAncestorPorts = [] } = {}) { res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", + "content-security-policy": frameAncestorsPolicy(frameAncestorPorts), }); res.end(body); } @@ -8219,7 +8462,7 @@ export function renderReviewSummary(summary = {}) { '
' + changed + 'changed
'; } -export function renderAppHtml() { +export function renderAppHtml({ codexPromptMutationNonce = "" } = {}) { return String.raw` @@ -9489,6 +9732,41 @@ export function renderAppHtml() { .shared-proposal-review-empty { position: absolute; inset: 0; display: grid; place-items: center; padding: 30px; color: var(--muted); text-align: center; } .shared-proposal-frames, .shared-proposal-frame { width: 100%; height: 100%; min-height: 0; border: 0; background: var(--bg); } .shared-proposal-frame[hidden], .shared-proposal-review-empty[hidden] { display: none !important; } + .codex-prompt-center { min-width: 0; min-height: 0; height: 100%; overflow: auto; padding: 20px 22px 28px; background: var(--bg); } + .codex-prompt-center[hidden] { display: none !important; } + .codex-prompt-center-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; } + .codex-prompt-center-title { min-width: 0; } + .codex-prompt-center-title h2 { margin: 0; color: var(--text); font-size: 20px; line-height: 1.25; } + .codex-prompt-center-title p { max-width: 78ch; margin: 7px 0 0; color: var(--text-soft); font-size: 12px; line-height: 1.5; } + .codex-prompt-badges { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; } + .codex-prompt-badge { display: inline-flex; align-items: center; min-height: 24px; padding: 3px 8px; border-radius: 999px; background: var(--surface-floating-soft); color: var(--muted); font: 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; } + .codex-prompt-badge[data-status="loaded"], .codex-prompt-badge[data-status="official_loaded"], .codex-prompt-badge[data-status="effective_loaded"] { color: var(--good); } + .codex-prompt-badge[data-status="conflict"], .codex-prompt-badge[data-status="mixed_versions"] { color: var(--danger); } + .codex-prompt-badge[data-status="restart_required"], .codex-prompt-badge[data-status="pending_next_launch"], .codex-prompt-badge[data-status="loaded_differs"] { color: var(--accent-2); } + .codex-prompt-meta { display: flex; flex-wrap: wrap; gap: 5px 14px; margin-top: 12px; color: var(--muted); font: 10px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; } + .codex-prompt-notice { margin-top: 14px; padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--line)); border-radius: 8px; background: color-mix(in srgb, var(--accent) 7%, var(--panel)); color: var(--text-soft); font-size: 11px; line-height: 1.45; } + .codex-prompt-notice[data-error="true"] { border-color: color-mix(in srgb, var(--danger) 45%, var(--line)); color: var(--danger); } + .codex-prompt-surfaces { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 16px; } + .codex-prompt-surface { min-width: 0; display: grid; grid-template-rows: auto minmax(260px, 1fr); gap: 6px; } + .codex-prompt-surface > span { color: var(--muted); font: 10px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; } + .codex-prompt-surface-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; } + .codex-prompt-metrics { color: var(--muted); font: 9px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; } + .codex-prompt-metrics[data-warning="true"] { color: var(--accent-2); } + .codex-prompt-metrics[data-error="true"] { color: var(--danger); } + .codex-prompt-surface textarea { width: 100%; min-width: 0; min-height: 300px; resize: vertical; padding: 12px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; tab-size: 2; } + .codex-prompt-surface textarea[readonly] { background: var(--surface-sidebar); color: var(--text-soft); } + .codex-prompt-surface textarea:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 58%, transparent); outline-offset: 2px; } + .codex-prompt-diff { margin-top: 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-sidebar); } + .codex-prompt-diff summary { padding: 10px 12px; color: var(--text-soft); font-size: 11px; cursor: pointer; } + .codex-prompt-diff pre { max-height: 300px; margin: 0; padding: 12px; overflow: auto; border-top: 1px solid var(--line); color: var(--text-soft); font: 10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow-wrap: anywhere; } + .codex-prompt-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; } + .codex-prompt-actions .danger-action { color: var(--danger); } + .codex-prompt-action-status { color: var(--muted); font-size: 11px; } + .codex-prompt-card-kind { color: var(--muted); font: 9px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; text-transform: uppercase; } + .codex-prompt-card-status { margin-left: auto; color: var(--muted); font: 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; } + .codex-prompt-card-status[data-status="loaded"], .codex-prompt-card-status[data-status="official_loaded"], .codex-prompt-card-status[data-status="effective_loaded"] { color: var(--good); } + .codex-prompt-card-status[data-status="conflict"], .codex-prompt-card-status[data-status="mixed_versions"] { color: var(--danger); } + .codex-prompt-card-status[data-status="restart_required"], .codex-prompt-card-status[data-status="pending_next_launch"], .codex-prompt-card-status[data-status="loaded_differs"] { color: var(--accent-2); } @media (max-width: 900px) { .shared-proposal-workspace-head { grid-template-columns: auto minmax(0, 1fr) auto auto; } .context-hub-views { grid-column: 3; } @@ -9498,6 +9776,9 @@ export function renderAppHtml() { #sharedProposalProjectFilter { grid-column: 4; } .shared-proposal-workspace-body { grid-template-columns: minmax(280px, 340px) minmax(0, 1fr); } .shared-proposal-files { grid-template-columns: 1fr; } + .codex-prompt-surfaces { grid-template-columns: 1fr; } + .codex-prompt-surface { grid-template-rows: auto minmax(220px, auto); } + .codex-prompt-surface textarea { min-height: 220px; } } @media (max-width: 680px) { .shared-proposal-workspace-head { grid-template-columns: auto minmax(0, 1fr) auto; gap: 7px; padding: 8px; } @@ -9771,6 +10052,7 @@ export function renderAppHtml() {
+
+ + + + +
+ Exact diff · Official → Effective after restart +

+          
+
+ + + +
+