@bitkyc08/opencodex 2.10.1 → 2.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/bin/ocx.mjs +18 -9
  2. package/gui/dist/assets/index-BKVqyYqT.js +70 -0
  3. package/gui/dist/assets/{index-Cd6_PBKn.css → index-Ca_3269W.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  6. package/gui/dist/provider-icons/openai.svg +1 -1
  7. package/package.json +1 -1
  8. package/src/adapters/command-code.ts +453 -0
  9. package/src/adapters/google.ts +3 -0
  10. package/src/cli/claude.ts +37 -22
  11. package/src/cli/index.ts +16 -3
  12. package/src/cli/launcher-context.ts +77 -0
  13. package/src/codex/admission.ts +1 -1
  14. package/src/codex/app-server-processes.ts +44 -1
  15. package/src/codex/catalog/sync.ts +22 -3
  16. package/src/codex/catalog-write-serialization.ts +1 -1
  17. package/src/codex/codex-write-lock.ts +1 -1
  18. package/src/codex/convergence-types.ts +1 -1
  19. package/src/codex/desired-state.ts +27 -7
  20. package/src/codex/history-job.ts +1 -1
  21. package/src/codex/history-lock.ts +1 -1
  22. package/src/codex/history-worker.ts +1 -1
  23. package/src/codex/internal/history-writer.ts +1 -1
  24. package/src/codex/transition-state.ts +1 -1
  25. package/src/codex/user-identity.ts +1 -1
  26. package/src/config.ts +6 -1
  27. package/src/integrations/config-io.ts +1 -1
  28. package/src/integrations/journal.ts +1 -1
  29. package/src/integrations/merge.ts +1 -1
  30. package/src/integrations/ownership.ts +1 -1
  31. package/src/integrations/registry.ts +1 -1
  32. package/src/integrations/serialize.ts +1 -1
  33. package/src/integrations/state.ts +1 -1
  34. package/src/integrations/store.ts +1 -1
  35. package/src/integrations/writer.ts +1 -1
  36. package/src/lib/bounded-body.ts +3 -1
  37. package/src/lib/bun-runtime.ts +21 -17
  38. package/src/lib/bun-stream-caps.ts +1 -1
  39. package/src/lib/local-management-attestation.ts +51 -0
  40. package/src/lib/shadow-call.ts +4 -4
  41. package/src/oauth/command-code.ts +239 -0
  42. package/src/oauth/health.ts +46 -2
  43. package/src/oauth/index.ts +38 -3
  44. package/src/providers/command-code-efforts.ts +85 -0
  45. package/src/providers/google-vertex-location.ts +14 -0
  46. package/src/providers/registry.ts +138 -26
  47. package/src/routing/capability.ts +1 -0
  48. package/src/server/adapter-resolve.ts +3 -0
  49. package/src/server/auth-cors.ts +6 -1
  50. package/src/server/index.ts +44 -2
  51. package/src/server/management/integration-routes.ts +1 -1
  52. package/src/server/management/native-integration-routes.ts +2 -2
  53. package/src/server/responses/core.ts +13 -9
  54. package/src/storage/scanner.ts +1 -1
  55. package/src/types.ts +6 -0
  56. package/src/usage/log.ts +1 -1
  57. package/gui/dist/assets/index-ChZQsmBY.js +0 -70
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Trusted facts captured by the plain-Node npm launcher before Bun auto-loads
3
+ * project dotenv files. The random proof travels in argv while the context
4
+ * travels in the environment, so a project `.env` cannot forge the pair during
5
+ * an ordinary `ocx ...` invocation.
6
+ */
7
+ export const NODE_LAUNCH_CONTEXT_ENV = "OCX_NODE_LAUNCH_CONTEXT";
8
+ export const NODE_LAUNCH_PROOF_PREFIX = "--ocx-internal-launch-proof=";
9
+
10
+ export const ANTHROPIC_PARENT_ENV_SLOTS = [
11
+ "ANTHROPIC_API_KEY",
12
+ "ANTHROPIC_AUTH_TOKEN",
13
+ "ANTHROPIC_BASE_URL",
14
+ ] as const;
15
+
16
+ export type AnthropicParentEnvSlot = typeof ANTHROPIC_PARENT_ENV_SLOTS[number];
17
+
18
+ export type TrustedNodeLaunchContext = {
19
+ anthropicEnvSlots: readonly AnthropicParentEnvSlot[];
20
+ };
21
+
22
+ let trustedContext: TrustedNodeLaunchContext | null = null;
23
+
24
+ function isLaunchProof(value: string): boolean {
25
+ return /^[A-Za-z0-9_-]{43}$/.test(value);
26
+ }
27
+
28
+ /** Consume the internal proof before normal CLI argument parsing. */
29
+ export function initializeNodeLauncherContext(
30
+ argv: string[] = process.argv,
31
+ env: NodeJS.ProcessEnv = process.env,
32
+ ): TrustedNodeLaunchContext | null {
33
+ const proofArgs: string[] = [];
34
+ for (let index = argv.length - 1; index >= 2; index -= 1) {
35
+ const value = argv[index];
36
+ if (!value?.startsWith(NODE_LAUNCH_PROOF_PREFIX)) continue;
37
+ proofArgs.push(value.slice(NODE_LAUNCH_PROOF_PREFIX.length));
38
+ argv.splice(index, 1);
39
+ }
40
+
41
+ const raw = env[NODE_LAUNCH_CONTEXT_ENV];
42
+ delete env[NODE_LAUNCH_CONTEXT_ENV];
43
+ // Older launchers used this unauthenticated marker. Never let a project
44
+ // dotenv resurrect it as a trusted provenance channel.
45
+ delete env.OCX_PRE_BUN_ANTHROPIC_ENV;
46
+ trustedContext = null;
47
+
48
+ if (proofArgs.length !== 1 || !raw || raw.length > 2048) return null;
49
+ const proof = proofArgs[0]!;
50
+ if (!isLaunchProof(proof)) return null;
51
+
52
+ try {
53
+ const parsed = JSON.parse(raw) as {
54
+ version?: unknown;
55
+ proof?: unknown;
56
+ anthropicEnvSlots?: unknown;
57
+ };
58
+ if (parsed.version !== 1 || parsed.proof !== proof || !Array.isArray(parsed.anthropicEnvSlots)) {
59
+ return null;
60
+ }
61
+ const allowed = new Set<string>(ANTHROPIC_PARENT_ENV_SLOTS);
62
+ const slots = parsed.anthropicEnvSlots.filter(
63
+ (slot): slot is AnthropicParentEnvSlot => typeof slot === "string" && allowed.has(slot),
64
+ );
65
+ if (slots.length !== parsed.anthropicEnvSlots.length || new Set(slots).size !== slots.length) {
66
+ return null;
67
+ }
68
+ trustedContext = { anthropicEnvSlots: slots };
69
+ return trustedContext;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function trustedNodeLauncherContext(): TrustedNodeLaunchContext | null {
76
+ return trustedContext;
77
+ }
@@ -16,7 +16,7 @@
16
16
  * READS ONLY. Nothing here creates a directory, a database, or a marker: an
17
17
  * admission that manufactures the state it is admitting cannot refuse.
18
18
  *
19
- * Design record: devlog/_plan/260804_codex_write_substrate/040_ownership_convergence.md.
19
+ * Design record: devlog/_fin/260804_codex_write_substrate/040_ownership_convergence.md.
20
20
  */
21
21
  import { createHash } from "node:crypto";
22
22
  import { existsSync, readFileSync } from "node:fs";
@@ -407,7 +407,9 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
407
407
  return matched;
408
408
  }
409
409
 
410
- export function formatStaleCodexAppServerWarning(processes: readonly CodexAppServerProcess[]): string {
410
+ export function formatStaleCodexAppServerWarning(
411
+ processes: readonly { pid: number }[],
412
+ ): string {
411
413
  const pids = processes.map(process => process.pid).join(", ");
412
414
  return (
413
415
  `WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
@@ -754,3 +756,44 @@ export function afterCatalogWriteHandleAppServers(
754
756
  }
755
757
  return { processes, warned: false, restart, hint };
756
758
  }
759
+
760
+ /**
761
+ * Startup-safe counterpart to {@link afterCatalogWriteHandleAppServers} (#1046).
762
+ *
763
+ * Service startup rewrites the catalog and the models cache, but an app-server
764
+ * that booted earlier keeps an in-memory model list — Codex builds a static
765
+ * manager from the catalog once and never rereads the file — so the picker shows
766
+ * a roster that no longer exists on disk. Every check a user runs reads the file;
767
+ * the picker renders memory.
768
+ *
769
+ * Two things this deliberately does NOT do, both of which the `--restart-codex`
770
+ * path does:
771
+ *
772
+ * - It never signals anything. Killing an app-server on an unattended boot would
773
+ * interrupt whatever turn the user has in flight. A human typing
774
+ * `ocx sync --restart-codex` is consenting to that; a login is not.
775
+ * - It never warns about a merely-running app-server. It asks the mtime
776
+ * classifier whether one is actually stale, so a boot with Codex open and a
777
+ * current catalog stays quiet.
778
+ *
779
+ * Failure is swallowed: startup synchronization is best-effort and must not stop
780
+ * the proxy from coming up.
781
+ *
782
+ * The memoized state is dropped first. {@link collectCodexAppServerCatalogState}
783
+ * caches for 5s when every io field is defaulted, so a `fresh` reading taken
784
+ * before the write would otherwise be replayed after it and this would stay
785
+ * silent about the very staleness it exists to report.
786
+ */
787
+ export function warnIfStaleCodexAppServersAfterStartupWrite(
788
+ options: { log?: Pick<Console, "error">; io?: CodexAppServerProcessIo } = {},
789
+ ): { warned: boolean } {
790
+ try {
791
+ resetCodexAppServerCatalogStateCache();
792
+ const status = collectCodexAppServerCatalogState(options.io ?? {});
793
+ if (status.state !== "stale") return { warned: false };
794
+ options.log?.error(formatStaleCodexAppServerWarning(status.processes));
795
+ return { warned: true };
796
+ } catch {
797
+ return { warned: false };
798
+ }
799
+ }
@@ -11,7 +11,6 @@ import { modelInList } from "../../types";
11
11
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
12
12
  import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
- import { getProviderRegistryEntry } from "../../providers/registry";
15
14
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
16
15
  import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
17
16
  import { identifyRoutedModel } from "../../adapters/identity";
@@ -200,6 +199,26 @@ export function isExactComboCatalogModel(
200
199
  return model !== undefined && exactComboSlugs.has(catalogModelSlug(model));
201
200
  }
202
201
 
202
+ /**
203
+ * Friendly Codex-picker label for a routed `provider/model` slug. Command Code's two config
204
+ * ids differ by a single dash (`command-code` vs `commandcode`), so relabel them to the
205
+ * lowercase-dash style the opencode presets use: `commandcode-auth/x` and `commandcode-api/x`.
206
+ * The model-id portion also carries a redundant `<vendor>-` prefix (`deepseek-deepseek-v4-flash`)
207
+ * that is dropped for display. All other providers keep the raw slug exactly as before.
208
+ */
209
+ function routedDisplayName(slug: string): string {
210
+ const slash = slug.indexOf("/");
211
+ if (slash <= 0) return slug;
212
+ const provider = slug.slice(0, slash);
213
+ let model = slug.slice(slash + 1);
214
+ if (provider === "command-code" || provider === "commandcode") {
215
+ const m = model.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
216
+ if (m && model.startsWith(`${m[1]}-${m[1]}-`)) model = model.slice(m[1]!.length + 1);
217
+ return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${model}`;
218
+ }
219
+ return slug;
220
+ }
221
+
203
222
  export function deriveEntry(
204
223
  template: RawEntry | null,
205
224
  slug: string,
@@ -220,7 +239,7 @@ export function deriveEntry(
220
239
  if (template) {
221
240
  const e = JSON.parse(JSON.stringify(template)) as RawEntry;
222
241
  e.slug = slug;
223
- e.display_name = slug;
242
+ e.display_name = routedDisplayName(slug);
224
243
  e.description = desc;
225
244
  e.priority = priority;
226
245
  e.visibility = "list";
@@ -271,7 +290,7 @@ export function deriveEntry(
271
290
  }
272
291
  // Fallback when no template is available (best-effort; strict parser may need more).
273
292
  const entry: RawEntry = {
274
- slug, display_name: slug, description: desc,
293
+ slug, display_name: routedDisplayName(slug), description: desc,
275
294
  shell_type: "shell_command", visibility: "list", supported_in_api: true,
276
295
  priority, base_instructions: "You are a helpful coding assistant.",
277
296
  ...(isRouted ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
@@ -22,7 +22,7 @@
22
22
  * advances the native pair. Order is `N -> K -> C`; there is no `C -> K` and no
23
23
  * `K -> N` (`005_contract.md:660-762`).
24
24
  *
25
- * Design record: devlog/_plan/260804_codex_write_substrate/005_contract.md §3.
25
+ * Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md §3.
26
26
  */
27
27
  import { chmodSync, lstatSync, realpathSync } from "node:fs";
28
28
 
@@ -26,7 +26,7 @@
26
26
  * Lock order is `N -> C`. C is `withConfigMutationLockSync`, entered while N is
27
27
  * held and released before N commits. There is no `C -> N`.
28
28
  *
29
- * Design record: devlog/_plan/260804_codex_write_substrate/030_lock_protocol.md.
29
+ * Design record: devlog/_fin/260804_codex_write_substrate/030_lock_protocol.md.
30
30
  */
31
31
  import { AsyncLocalStorage } from "node:async_hooks";
32
32
  import { createHash } from "node:crypto";
@@ -6,7 +6,7 @@
6
6
  * of the record schema, the /api/sync contract, and the convergence entry
7
7
  * point; the contract is centralized here so a consumer can only import.
8
8
  *
9
- * Design record: devlog/_plan/260804_codex_write_substrate/005_contract.md
9
+ * Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md
10
10
  * Audit trail: 006, 007, 008, 009, 010 audit syntheses in the same unit.
11
11
  *
12
12
  * TYPES ONLY. WP8b deliberately rewires nothing: WP9 supplies the first
@@ -27,7 +27,17 @@ import type { OcxClientIntegrationsConfig, OcxConfig } from "../types";
27
27
  export type DurableIntentClientId = keyof OcxClientIntegrationsConfig;
28
28
 
29
29
  /** Injectable for tests; production passes the real sync. */
30
- export type CodexStartupSync = (port: number) => Promise<unknown>;
30
+ /**
31
+ * The startup sync result the caller needs to decide whether anything was
32
+ * actually written (#1046). It used to be `unknown`, so "a write happened" was
33
+ * not observable at the startup boundary and no post-write action could be
34
+ * gated on it.
35
+ */
36
+ export interface CodexStartupSyncOutcome {
37
+ catalogWritten: boolean;
38
+ cacheSynced: boolean;
39
+ }
40
+ export type CodexStartupSync = (port: number) => Promise<CodexStartupSyncOutcome | undefined>;
31
41
 
32
42
  export type CodexDesiredStateResult =
33
43
  | { readonly ok: true; readonly status: "committed" | "unchanged"; readonly enabled: boolean }
@@ -143,19 +153,29 @@ export function setGrokIntegrationEnabled(enabled: boolean): CodexDesiredStateRe
143
153
  * Swallowing the user's decision was not.
144
154
  *
145
155
  * Returns whether the sync ran, so a caller — or a test — can tell "skipped
146
- * because the user turned it off" from "ran and quietly failed".
156
+ * because the user turned it off" from "ran and quietly failed", plus what it
157
+ * wrote when it did run (#1046 — the caller warns about stale app-servers only
158
+ * after a real write).
147
159
  */
148
160
  export async function syncCodexOnStartIfEnabled(
149
161
  port: number,
150
162
  config: Pick<OcxConfig, "clientIntegrations">,
151
163
  sync: CodexStartupSync = defaultStartupSync,
152
- ): Promise<boolean> {
153
- if (!codexIntegrationEnabled(config)) return false;
154
- await sync(port).catch(() => {});
155
- return true;
164
+ ): Promise<{ ran: boolean; catalogWritten: boolean; cacheSynced: boolean }> {
165
+ if (!codexIntegrationEnabled(config)) {
166
+ return { ran: false, catalogWritten: false, cacheSynced: false };
167
+ }
168
+ // The `.catch` is deliberate and stays: a failure to APPLY must not stop the
169
+ // proxy from coming up. A failed sync simply reports no writes.
170
+ const outcome = await sync(port).catch(() => undefined);
171
+ return {
172
+ ran: true,
173
+ catalogWritten: outcome?.catalogWritten === true,
174
+ cacheSynced: outcome?.cacheSynced === true,
175
+ };
156
176
  }
157
177
 
158
- async function defaultStartupSync(port: number): Promise<unknown> {
178
+ async function defaultStartupSync(port: number): Promise<CodexStartupSyncOutcome> {
159
179
  const { syncModelsToCodex } = await import("./sync");
160
180
  return syncModelsToCodex(port);
161
181
  }
@@ -14,7 +14,7 @@
14
14
  * exception crossing back into a route that already persisted its mutation — is
15
15
  * how a successful change gets reported as a 500.
16
16
  *
17
- * Design record: devlog/_plan/260804_codex_write_substrate/020_history_isolation.md.
17
+ * Design record: devlog/_fin/260804_codex_write_substrate/020_history_isolation.md.
18
18
  */
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { join } from "node:path";
@@ -27,7 +27,7 @@
27
27
  * So every history mutator asks this module at runtime whether the permit it was
28
28
  * handed is still live for the state database it is about to write.
29
29
  *
30
- * Design record: devlog/_plan/260804_codex_write_substrate/005_contract.md §6.
30
+ * Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md §6.
31
31
  */
32
32
  import { chmodSync, lstatSync, realpathSync } from "node:fs";
33
33
 
@@ -21,7 +21,7 @@
21
21
  * at import time, so a request that leaned on those constants would silently
22
22
  * address the wrong home.
23
23
  *
24
- * Design record: devlog/_plan/260804_codex_write_substrate/020_history_isolation.md.
24
+ * Design record: devlog/_fin/260804_codex_write_substrate/020_history_isolation.md.
25
25
  */
26
26
  import { withHistoryWriteSerialization } from "./history-lock";
27
27
  import {
@@ -21,7 +21,7 @@
21
21
  * root. Readers and probes stay in `history-provider.ts`; nothing in the CLI, the
22
22
  * server, the guardian, `inject.ts` or `sync.ts` may reach these symbols.
23
23
  *
24
- * Design record: devlog/_plan/260804_codex_write_substrate/020_history_isolation.md.
24
+ * Design record: devlog/_fin/260804_codex_write_substrate/020_history_isolation.md.
25
25
  */
26
26
  import { assertHistoryWritePermit, type HistoryWritePermit } from "../history-lock";
27
27
  import {
@@ -7,7 +7,7 @@
7
7
  * UPDATE, and the opaque one-shot capability backed by an already-open
8
8
  * `BEGIN IMMEDIATE` transaction.
9
9
  *
10
- * Design record: devlog/_plan/260804_codex_write_substrate/005_contract.md §1.
10
+ * Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md §1.
11
11
  */
12
12
  import { randomUUID } from "node:crypto";
13
13
  import { chmodSync, lstatSync, realpathSync } from "node:fs";
@@ -6,7 +6,7 @@
6
6
  * could therefore coordinate through different databases. The namespace is
7
7
  * keyed only by the effective uid/SID and the canonical CODEX_HOME.
8
8
  *
9
- * Design record: devlog/_plan/260804_codex_write_substrate/005_contract.md §7.
9
+ * Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md §7.
10
10
  */
11
11
  import { createHash } from "node:crypto";
12
12
  import {
package/src/config.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  } from "./lib/windows-secret-acl";
39
39
  import { recordOwnedConfigPath } from "./lib/config-ownership";
40
40
  import { assertNotRealHomeUnderTest } from "./lib/test-home-guard";
41
+ import { isLocalAttestationSecret } from "./lib/local-management-attestation";
41
42
  import { providerDestinationConfigError } from "./lib/destination-policy";
42
43
  import { redactSecretString } from "./lib/redact";
43
44
  import { openRouterRoutingConfigError } from "./providers/openrouter-routing";
@@ -2608,18 +2609,22 @@ export type RuntimePortState = {
2608
2609
  pid: number;
2609
2610
  port: number;
2610
2611
  hostname?: string;
2612
+ /** Per-process proof key; protected by the config directory and never served. */
2613
+ attestationSecret?: string;
2611
2614
  };
2612
2615
 
2613
2616
  function isValidRuntimePortState(value: unknown): value is RuntimePortState {
2614
2617
  if (!value || typeof value !== "object") return false;
2615
2618
  const state = value as Record<string, unknown>;
2616
2619
  const hostnameOk = state.hostname === undefined || typeof state.hostname === "string";
2620
+ const attestationOk = state.attestationSecret === undefined || isLocalAttestationSecret(state.attestationSecret);
2617
2621
  return Number.isSafeInteger(state.pid)
2618
2622
  && Number(state.pid) > 0
2619
2623
  && Number.isInteger(state.port)
2620
2624
  && Number(state.port) > 0
2621
2625
  && Number(state.port) <= 65535
2622
- && hostnameOk;
2626
+ && hostnameOk
2627
+ && attestationOk;
2623
2628
  }
2624
2629
 
2625
2630
  export function writeRuntimePort(state: RuntimePortState): void {
@@ -5,7 +5,7 @@
5
5
  * needs it too, and a reader that disagreed with the writer about what counts
6
6
  * as an absent file is exactly how an unreadable config gets overwritten.
7
7
  *
8
- * Design of record: devlog/_plan/260802_client_toggle_api/021 §5-6.
8
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §5-6.
9
9
  */
10
10
  import { mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
11
11
  import type { ConfigFormat } from "../clients/config-export";
@@ -10,7 +10,7 @@
10
10
  * verbatim — so they go through `atomicWriteFile`, which applies 0600 plus
11
11
  * Windows ACL hardening.
12
12
  *
13
- * Design of record: devlog/_plan/260802_client_toggle_api/021 §4.
13
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §4.
14
14
  */
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { appendFileSync, existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
@@ -7,7 +7,7 @@
7
7
  * remove, and inferring ownership from a name is how a config editor destroys
8
8
  * work it did not create.
9
9
  *
10
- * Design of record: devlog/_plan/260802_client_toggle_api/031_wp3_writer_impl.md.
10
+ * Design of record: devlog/_fin/260802_client_toggle_api/031_wp3_writer_impl.md.
11
11
  */
12
12
  import type { ManagedContribution } from "../clients/config-export";
13
13
 
@@ -7,7 +7,7 @@
7
7
  * conflating them is what lets a foreign edit read as ordinary drift — which
8
8
  * would then be silently overwritten.
9
9
  *
10
- * Design of record: devlog/_plan/260802_client_toggle_api/021 §2.
10
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §2.
11
11
  */
12
12
  import { createHash } from "node:crypto";
13
13
  import { mkdirSync, readFileSync } from "node:fs";
@@ -6,7 +6,7 @@
6
6
  * client's config. This one says where it lives, how to tell whether the client
7
7
  * is installed at all, and whether a remote bind is safe for it.
8
8
  *
9
- * Design of record: devlog/_plan/260802_client_toggle_api/021 §1.
9
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §1.
10
10
  */
11
11
  import { homedir } from "node:os";
12
12
  import { join } from "node:path";
@@ -21,7 +21,7 @@
21
21
  * might misread — and the writer turns the failure into a structured refusal
22
22
  * rather than an exception.
23
23
  *
24
- * Design of record: devlog/_plan/260802_client_toggle_api/011_wp1_builders.md.
24
+ * Design of record: devlog/_fin/260802_client_toggle_api/011_wp1_builders.md.
25
25
  */
26
26
 
27
27
  export type ConfigFormat = "json" | "yaml" | "toml" | "json5";
@@ -6,7 +6,7 @@
6
6
  * be reported as ordinary drift. Getting that wrong would let `disable` delete
7
7
  * a user's own edits.
8
8
  *
9
- * Design of record: devlog/_plan/260802_client_toggle_api/021 §3.
9
+ * Design of record: devlog/_fin/260802_client_toggle_api/021 §3.
10
10
  */
11
11
  import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
12
12
  import type { OcxConfig } from "../types";
@@ -8,7 +8,7 @@
8
8
  * bookkeeping seams to that same store so the two can never point at different
9
9
  * roots.
10
10
  *
11
- * Design of record: devlog/_plan/260802_client_toggle_api/006 §Config-dir seam.
11
+ * Design of record: devlog/_fin/260802_client_toggle_api/006 §Config-dir seam.
12
12
  */
13
13
  import {
14
14
  appendOperation,
@@ -7,7 +7,7 @@
7
7
  * file is not in a state we can reason about. A disable that deletes work the
8
8
  * user did after us would be worse than never shipping the feature.
9
9
  *
10
- * Design of record: devlog/_plan/260802_client_toggle_api/030 and 031.
10
+ * Design of record: devlog/_fin/260802_client_toggle_api/030 and 031.
11
11
  */
12
12
  import { dirname } from "node:path";
13
13
  import { EXPORT_CLIENTS, type ExportModel } from "../clients/config-export";
@@ -23,6 +23,8 @@ export interface BoundedBodyOptions {
23
23
  totalTimeoutMs?: number;
24
24
  /** Deadline between non-empty raw chunks. Exposed for focused tests. */
25
25
  inactivityTimeoutMs?: number;
26
+ /** Deadline for the first non-empty raw chunk. Defaults to inactivityTimeoutMs. */
27
+ firstByteTimeoutMs?: number;
26
28
  }
27
29
 
28
30
  export interface BoundedBodyResult {
@@ -129,7 +131,7 @@ export async function readBoundedResponseBody(
129
131
  let cancelReason: unknown;
130
132
  const total = timeoutPromise(options.totalTimeoutMs ?? BOUNDED_BODY_TIMEOUT_MS, TOTAL_TIMEOUT);
131
133
  let inactivity = timeoutPromise(
132
- options.inactivityTimeoutMs ?? BOUNDED_BODY_TIMEOUT_MS,
134
+ options.firstByteTimeoutMs ?? options.inactivityTimeoutMs ?? BOUNDED_BODY_TIMEOUT_MS,
133
135
  INACTIVITY_TIMEOUT,
134
136
  );
135
137
 
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { createRequire } from "node:module";
14
14
  import { realpathSync } from "node:fs";
15
- import { dirname, join, resolve } from "node:path";
15
+ import { dirname, join } from "node:path";
16
16
  import { isRealBunBinary } from "./bun-binary-validator.mjs";
17
17
 
18
18
  export { isRealBunBinary };
@@ -108,19 +108,23 @@ export function withProcessRuntimeProvenance(
108
108
  * exact executable, otherwise what this executable actually is.
109
109
  */
110
110
  function currentRuntimeProvenance(env: NodeJS.ProcessEnv): DurableBunRuntime {
111
- const claimed = reportedBunRuntimeSource(env);
112
- const claimedPath = env[BUN_RUNTIME_PATH_ENV]?.trim();
113
- if (claimed && claimedPath && samePath(claimedPath, process.execPath)) {
114
- return { path: process.execPath, source: claimed, overrideEnv: BUN_OVERRIDE_ENV };
115
- }
111
+ const recorded = recordedCurrentRuntime(env);
112
+ if (recorded) return recorded;
116
113
  // No marker that describes this binary: report what is running. One resolution
117
114
  // supplies both halves so the pair can never disagree.
118
- const runtime = durableBunRuntime();
115
+ const runtime = unmarkedDurableBunRuntime();
119
116
  return samePath(runtime.path, process.execPath)
120
117
  ? runtime
121
118
  : { path: process.execPath, source: "process", overrideEnv: BUN_OVERRIDE_ENV };
122
119
  }
123
120
 
121
+ function recordedCurrentRuntime(env: NodeJS.ProcessEnv): DurableBunRuntime | null {
122
+ const source = reportedBunRuntimeSource(env);
123
+ const path = env[BUN_RUNTIME_PATH_ENV]?.trim();
124
+ if (!source || !path || !samePath(path, process.execPath)) return null;
125
+ return { path, source, overrideEnv: BUN_OVERRIDE_ENV };
126
+ }
127
+
124
128
  /**
125
129
  * Same file, allowing for the aliases a path can pick up between launch and relaunch:
126
130
  * symlinks/junctions, mapped drives, and Windows case differences. Falls back to a
@@ -154,21 +158,21 @@ export function bundledBunPath(): string | null {
154
158
  }
155
159
  }
156
160
 
157
- export function overrideBunPath(): string | null {
158
- const value = process.env[BUN_OVERRIDE_ENV]?.trim();
159
- if (!value) return null;
160
- const resolved = resolve(value);
161
- return isRealBunBinary(resolved) ? resolved : null;
162
- }
163
-
164
- export function durableBunRuntime(): DurableBunRuntime {
165
- const override = overrideBunPath();
166
- if (override) return { path: override, source: "override", overrideEnv: BUN_OVERRIDE_ENV };
161
+ function unmarkedDurableBunRuntime(): DurableBunRuntime {
167
162
  const bundled = bundledBunPath();
168
163
  if (bundled) return { path: bundled, source: "bundled", overrideEnv: BUN_OVERRIDE_ENV };
169
164
  return { path: process.execPath, source: "process", overrideEnv: BUN_OVERRIDE_ENV };
170
165
  }
171
166
 
167
+ export function durableBunRuntime(): DurableBunRuntime {
168
+ // A durable artifact must use the runtime selected BEFORE Bun auto-loaded a
169
+ // project dotenv. The Node launcher and owned service/shim launchers stamp the
170
+ // selected source/path pair; it is accepted only when it names this exact
171
+ // running executable. Re-reading OPENCODEX_BUN_PATH here would let a project
172
+ // `.env` persist an arbitrary executable into a shim or service.
173
+ return recordedCurrentRuntime(process.env) ?? unmarkedDurableBunRuntime();
174
+ }
175
+
172
176
  /**
173
177
  * Bun path to bake into durable artifacts (launchd/systemd/Task Scheduler and
174
178
  * the Codex auto-start shim). Prefer the bundled binary — it lives under the
@@ -10,7 +10,7 @@
10
10
  * safety pin. Darwin no-rewrite traffic stays on tee
11
11
  * for `auto` regardless of runtime capability and reaches eager relay only via
12
12
  * explicit `streamMode: "eager-relay"` opt-in (see
13
- * devlog/_plan/260731_macos_rss_retention/100_darwin_eager_optin.md).
13
+ * devlog/_fin/260731_macos_rss_retention/100_darwin_eager_optin.md).
14
14
  *
15
15
  * Prerelease conservatism: a version carrying a prerelease suffix (e.g.
16
16
  * `1.4.0-canary.3`) is NEVER treated as fixed even when its numeric triple
@@ -0,0 +1,51 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+
3
+ export const LOCAL_ATTESTATION_CHALLENGE_HEADER = "x-opencodex-attestation-challenge";
4
+ export const LOCAL_ATTESTATION_PROOF_HEADER = "x-opencodex-attestation-proof";
5
+
6
+ const BASE64URL_256 = /^[A-Za-z0-9_-]{43}$/;
7
+
8
+ export function isLocalAttestationSecret(value: unknown): value is string {
9
+ return typeof value === "string" && BASE64URL_256.test(value);
10
+ }
11
+
12
+ export function createLocalAttestationSecret(): string {
13
+ return randomBytes(32).toString("base64url");
14
+ }
15
+
16
+ export function createLocalAttestationChallenge(): string {
17
+ return randomBytes(32).toString("base64url");
18
+ }
19
+
20
+ function attestationPayload(challenge: string, pid: number, port: number): string | null {
21
+ if (!BASE64URL_256.test(challenge)) return null;
22
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
23
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
24
+ return `opencodex-local-management-v1\n${challenge}\n${pid}\n${port}`;
25
+ }
26
+
27
+ export function createLocalAttestationProof(
28
+ secret: string,
29
+ challenge: string,
30
+ pid: number,
31
+ port: number,
32
+ ): string | null {
33
+ if (!isLocalAttestationSecret(secret)) return null;
34
+ const payload = attestationPayload(challenge, pid, port);
35
+ if (!payload) return null;
36
+ return createHmac("sha256", secret).update(payload).digest("base64url");
37
+ }
38
+
39
+ export function verifyLocalAttestationProof(
40
+ secret: string,
41
+ challenge: string,
42
+ pid: number,
43
+ port: number,
44
+ proof: string | null,
45
+ ): boolean {
46
+ const expected = createLocalAttestationProof(secret, challenge, pid, port);
47
+ if (!expected || !proof || !BASE64URL_256.test(proof)) return false;
48
+ const expectedBytes = Buffer.from(expected);
49
+ const actualBytes = Buffer.from(proof);
50
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
51
+ }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Shadow-call intercept source models.
3
3
  *
4
- * Codex's hard-coded helper model is not stable across client versions: it was
5
- * `gpt-5.4-mini` up to 0.144.x and became `gpt-5.6-luna` in 0.145.0. The
6
- * intercept therefore matches a prefix SET, and every surface that names the
4
+ * Codex 0.145.0+ uses `gpt-5.6-luna` for helper calls. Older clients through
5
+ * 0.144.x used `gpt-5.4-mini`; operators supporting them can restore that
6
+ * prefix with the `sourceModels` override. Every surface that names the
7
7
  * intercepted model (management API, GUI badges/tooltips, CLI) reads it from
8
8
  * here instead of hard-coding a slug that goes stale on the next client bump.
9
9
  */
10
- export const DEFAULT_SHADOW_SOURCE_MODELS = ["gpt-5.4-mini", "gpt-5.6-luna"] as const;
10
+ export const DEFAULT_SHADOW_SOURCE_MODELS = ["gpt-5.6-luna"] as const;
11
11
 
12
12
  /** Normalize a persisted `sourceModels` override; falls back to the defaults. */
13
13
  export function shadowSourceModels(configured?: unknown): string[] {