@bitkyc08/opencodex 2.12.0 → 2.13.0

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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -4,6 +4,7 @@ import { dirname } from "node:path";
4
4
  import { labInstallationSaltPath, labRoot } from "../paths";
5
5
 
6
6
  const SALT_BYTES = 32;
7
+ const installationSaltCache = new Map<string, Uint8Array>();
7
8
  const UNSUPPORTED_DIRECTORY_FSYNC_CODES = new Set(["EINVAL", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
8
9
 
9
10
  function readSaltFile(path: string): Uint8Array {
@@ -14,6 +15,25 @@ function readSaltFile(path: string): Uint8Array {
14
15
  return new Uint8Array(bytes);
15
16
  }
16
17
 
18
+ function cacheSalt(path: string, salt: Uint8Array): Uint8Array {
19
+ const cached = new Uint8Array(salt);
20
+ installationSaltCache.set(path, cached);
21
+ return new Uint8Array(cached);
22
+ }
23
+
24
+ /** Read the existing local fingerprint salt without creating Lab state. */
25
+ export function readExistingInstallationSalt(configDir?: string): Uint8Array | null {
26
+ const path = labInstallationSaltPath(configDir);
27
+ const cached = installationSaltCache.get(path);
28
+ if (cached) return new Uint8Array(cached);
29
+ try {
30
+ return cacheSalt(path, readSaltFile(path));
31
+ } catch (error) {
32
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
33
+ throw error;
34
+ }
35
+ }
36
+
17
37
  function removeStagingFile(path: string): void {
18
38
  try { unlinkSync(path); }
19
39
  catch (error) {
@@ -45,7 +65,7 @@ export function readInstallationSalt(configDir?: string): Uint8Array {
45
65
  const root = labRoot(configDir);
46
66
  mkdirSync(root, { recursive: true, mode: 0o700 });
47
67
 
48
- try { return readSaltFile(path); }
68
+ try { return cacheSalt(path, readSaltFile(path)); }
49
69
  catch (error) {
50
70
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
51
71
  }
@@ -67,10 +87,10 @@ export function readInstallationSalt(configDir?: string): Uint8Array {
67
87
  linkSync(stagingPath, path);
68
88
  try { fsyncDirectory(dirname(path)); }
69
89
  catch { throw new Error("harness_failure: installation salt directory fsync failed"); }
70
- return new Uint8Array(salt);
90
+ return cacheSalt(path, salt);
71
91
  } catch (error) {
72
92
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
73
- return readSaltFile(path);
93
+ return cacheSalt(path, readSaltFile(path));
74
94
  }
75
95
  } finally {
76
96
  if (fd !== undefined) {
@@ -78,4 +98,4 @@ export function readInstallationSalt(configDir?: string): Uint8Array {
78
98
  }
79
99
  removeStagingFile(stagingPath);
80
100
  }
81
- }
101
+ }
@@ -0,0 +1,80 @@
1
+ import { createHash } from "node:crypto";
2
+ import { jcsStringify } from "../digest";
3
+ import type { ProtocolSubjectV1 } from "../events/types";
4
+ import type { ProtocolExecutionContextV1 } from "../conformance/types";
5
+
6
+ /** Frozen CL-01 protocol compatibility identity. */
7
+ export const PROTOCOL_COMPATIBILITY_VERSION = "protocol-v1";
8
+
9
+ /** Adapter identity used by the deterministic protocol harness for a wire protocol. */
10
+ export function protocolAdapterForUpstreamProtocol(protocol: string): string {
11
+ switch (protocol) {
12
+ case "openai-responses":
13
+ return "openai-responses";
14
+ case "anthropic-messages":
15
+ return "anthropic";
16
+ case "openai-chat":
17
+ return "openai-chat";
18
+ default:
19
+ throw new Error(`unsupported protocol identity: ${protocol}`);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Exact CL-01 behavior fingerprint for one protocol execution context.
25
+ *
26
+ * `effectiveAdapter` is optional so the harness keeps its historical mapping,
27
+ * while routing consumers may require an exact production adapter match. A
28
+ * provider-specific adapter that merely speaks an OpenAI-shaped wire therefore
29
+ * cannot borrow generic protocol evidence unless CL-01 actually produced the
30
+ * same adapter identity.
31
+ */
32
+ export function protocolBehaviorFingerprintV1(
33
+ ctx: ProtocolExecutionContextV1,
34
+ effectiveAdapter = protocolAdapterForUpstreamProtocol(ctx.upstreamProtocol),
35
+ ): string {
36
+ const values = {
37
+ schemaVersion: 1,
38
+ resolverVersion: 1,
39
+ values: {
40
+ "wire.adapter": {
41
+ source: "lab_forced",
42
+ value: effectiveAdapter,
43
+ },
44
+ "wire.upstreamProtocol": {
45
+ source: "lab_forced",
46
+ value: ctx.upstreamProtocol,
47
+ },
48
+ "runtime.arch": {
49
+ source: "lab_forced",
50
+ value: process.arch,
51
+ },
52
+ "runtime.bunVersion": {
53
+ source: "lab_forced",
54
+ value: process.versions.bun ?? Bun.version,
55
+ },
56
+ "runtime.platform": {
57
+ source: "lab_forced",
58
+ value: process.platform,
59
+ },
60
+ },
61
+ };
62
+ return createHash("sha256").update(jcsStringify(values)).digest("hex");
63
+ }
64
+
65
+ /** Build the exact ProtocolSubjectV1 consumed by CL-01 projection. */
66
+ export function buildProtocolSubjectV1(
67
+ ctx: ProtocolExecutionContextV1,
68
+ effectiveAdapter = protocolAdapterForUpstreamProtocol(ctx.upstreamProtocol),
69
+ ): ProtocolSubjectV1 {
70
+ return Object.freeze({
71
+ subjectSchemaVersion: 1,
72
+ subjectKind: "protocol",
73
+ opencodexCompatibilityVersion: PROTOCOL_COMPATIBILITY_VERSION,
74
+ effectiveAdapter,
75
+ inboundProtocol: ctx.inboundProtocol,
76
+ upstreamProtocol: ctx.upstreamProtocol,
77
+ surface: ctx.surface,
78
+ behaviorFingerprint: protocolBehaviorFingerprintV1(ctx, effectiveAdapter),
79
+ });
80
+ }
@@ -34,6 +34,7 @@ export function buildRouteSubjectV1(
34
34
  routeContext: LabRouteContext,
35
35
  destination: LabDestinationV1,
36
36
  configDir?: string,
37
+ installationSalt?: Uint8Array | string,
37
38
  ): RouteSubjectV1 {
38
39
  if (!routeContext.providerInstanceKey) throw new Error("harness_failure: provider instance identity is required");
39
40
  if (!/^[0-9a-f]{64}$/.test(routeContext.opencodexCompatibilityVersion)) {
@@ -44,7 +45,7 @@ export function buildRouteSubjectV1(
44
45
  if (adapterValue !== routeContext.effectiveAdapter || protocolValue !== routeContext.upstreamProtocol) {
45
46
  throw new Error("harness_failure: behavior resolver output does not match exact route");
46
47
  }
47
- const salt = readInstallationSalt(configDir);
48
+ const salt = installationSalt ?? readInstallationSalt(configDir);
48
49
  const providerInstanceFingerprint = localFingerprint("providerInstance", routeContext.providerInstanceKey, salt);
49
50
  const dependencies = canonicalDependencies(routeContext.dependencies);
50
51
  const subject: RouteSubjectV1 = {
@@ -0,0 +1,7 @@
1
+ import type { TrustedFabricPatchExecutor } from "../lab/fabric/types";
2
+ import { isHostIssuedFabricPatchExecutor } from "./fabric-task-host";
3
+
4
+ /** Trusted exact-route fabric patch executor recognition facade. */
5
+ export function isTrustedFabricPatchExecutor(value: unknown): value is TrustedFabricPatchExecutor {
6
+ return isHostIssuedFabricPatchExecutor(value);
7
+ }
@@ -0,0 +1,29 @@
1
+ import type { FabricPatchExecutor, FabricPatchExecutorInput, TrustedFabricPatchExecutor } from "../lab/fabric/types";
2
+
3
+ const HOST_ISSUED_EXECUTORS = new WeakSet<object>();
4
+
5
+ /**
6
+ * Trusted host integration boundary for CL-07 exact-route fabric patch production.
7
+ *
8
+ * @internal host integration only
9
+ */
10
+ export function createHostIssuedFabricPatchExecutor(
11
+ executorModulePath: string,
12
+ execute: FabricPatchExecutor,
13
+ ): TrustedFabricPatchExecutor {
14
+ const capability: TrustedFabricPatchExecutor = Object.freeze({
15
+ executorModulePath,
16
+ execute: (input: FabricPatchExecutorInput) => Promise.resolve(execute(input)),
17
+ });
18
+ HOST_ISSUED_EXECUTORS.add(capability);
19
+ return capability;
20
+ }
21
+
22
+ /** Internal recognition check consumed by the read-only authority facade. */
23
+ export function isHostIssuedFabricPatchExecutor(value: unknown): value is TrustedFabricPatchExecutor {
24
+ if (typeof value !== "object" || value === null || !HOST_ISSUED_EXECUTORS.has(value as object)) return false;
25
+ const capability = value as TrustedFabricPatchExecutor;
26
+ return typeof capability.executorModulePath === "string"
27
+ && capability.executorModulePath.length > 0
28
+ && typeof capability.execute === "function";
29
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Production host integration for CL-03 trusted exact-route execution.
3
+ *
4
+ * Issues a host-recognized `TrustedLabRouteExecutor` that routes provider traffic
5
+ * through the CL-03 credential lease, pinned transport, and observation normalization
6
+ * pipeline. Secret material stays outside Lab modules.
7
+ *
8
+ * @internal host integration only
9
+ */
10
+ import { resolveEnvValue } from "../config";
11
+ import {
12
+ getValidAccessTokenSnapshot,
13
+ OAuthLoginRequiredError,
14
+ UnsupportedOAuthProviderError,
15
+ } from "../oauth";
16
+ import type { OcxConfig } from "../types";
17
+ import { createCredentialLease } from "../lab/live/credential-lease";
18
+ import {
19
+ liveUpstreamRequestPath,
20
+ normalizeLabLiveTransportObservation,
21
+ } from "../lab/live/executor";
22
+ import { createPinnedTransport } from "../lab/live/transport";
23
+ import { TransportError } from "../lab/live/transport";
24
+ import type { LabRouteContext } from "../lab/live/types";
25
+ import { createLabAuthorizedPinnedSender } from "./lab-live-pinned-sender";
26
+ import { createHostIssuedLabRouteExecutor } from "./lab-live-host";
27
+ import type { TrustedLabRouteExecutor } from "../lab/live/types";
28
+
29
+ const CREDENTIAL_HEADER = /(authorization|api[-_]?key|token|secret|credential|cookie)/i;
30
+
31
+ export interface ProductionLabRouteExecutorDeps {
32
+ configDir?: string;
33
+ loadConfig: () => OcxConfig;
34
+ }
35
+
36
+ async function buildLabProviderAuthHeaders(
37
+ routeContext: LabRouteContext,
38
+ config: OcxConfig,
39
+ ): Promise<Record<string, string>> {
40
+ const provider = config.providers?.[routeContext.providerId];
41
+ if (!provider || provider.disabled === true) {
42
+ throw new TransportError("auth_blocked", "provider unavailable");
43
+ }
44
+ if (provider.authMode === "forward") {
45
+ throw new TransportError("auth_blocked", "forward auth unsupported for lab probes");
46
+ }
47
+ if (provider.authMode === "local") {
48
+ throw new TransportError("auth_blocked", "local provider unsupported for lab probes");
49
+ }
50
+
51
+ const headers: Record<string, string> = {
52
+ Accept: "application/json",
53
+ "Content-Type": "application/json",
54
+ };
55
+
56
+ if (provider.authMode === "oauth") {
57
+ let protocol: string;
58
+ try {
59
+ protocol = new URL(routeContext.baseUrl).protocol;
60
+ } catch {
61
+ throw new TransportError("auth_blocked", "invalid OAuth provider destination");
62
+ }
63
+ if (protocol !== "https:") {
64
+ throw new TransportError("auth_blocked", "OAuth lab probes require HTTPS");
65
+ }
66
+ try {
67
+ const snapshot = await getValidAccessTokenSnapshot(routeContext.providerId);
68
+ headers.Authorization = `Bearer ${snapshot.accessToken}`;
69
+ } catch (error) {
70
+ if (error instanceof OAuthLoginRequiredError || error instanceof UnsupportedOAuthProviderError) {
71
+ throw new TransportError("auth_blocked", "oauth unavailable");
72
+ }
73
+ // Non-terminal refresh failures (network, lock contention, provider transient errors)
74
+ // are harness/infrastructure failures rather than evidence that credentials are invalid.
75
+ throw new TransportError("harness_failure", "oauth refresh unavailable");
76
+ }
77
+ } else {
78
+ const apiKey = resolveEnvValue(provider.apiKey)?.trim();
79
+ if (!apiKey) throw new TransportError("auth_blocked", "missing api key");
80
+ if (provider.adapter === "anthropic" && provider.apiKeyTransport === "x-api-key") {
81
+ headers["x-api-key"] = apiKey;
82
+ } else {
83
+ headers.Authorization = `Bearer ${apiKey}`;
84
+ }
85
+ }
86
+
87
+ if (provider.headers) {
88
+ for (const [name, value] of Object.entries(provider.headers)) {
89
+ if (CREDENTIAL_HEADER.test(name)) continue;
90
+ const resolved = resolveEnvValue(value);
91
+ headers[name] = typeof resolved === "string" ? resolved : value;
92
+ }
93
+ }
94
+
95
+ return headers;
96
+ }
97
+
98
+ /** Create the process-local trusted CL-03 route executor for Lab automation dispatch. */
99
+ export function createProductionLabRouteExecutor(
100
+ deps: ProductionLabRouteExecutorDeps,
101
+ ): TrustedLabRouteExecutor {
102
+ return createHostIssuedLabRouteExecutor(async (input) => {
103
+ if (!input.initiatingRequest) {
104
+ throw new TransportError("harness_failure", "live scenario missing initiating request");
105
+ }
106
+ const config = deps.loadConfig();
107
+ const lease = createCredentialLease({
108
+ destination: input.destination,
109
+ transportId: "lab-automation",
110
+ budget: input.limits.maxRequests,
111
+ });
112
+ const authHeaders = await buildLabProviderAuthHeaders(input.routeContext, config);
113
+ const sender = createLabAuthorizedPinnedSender(async () => authHeaders);
114
+ const transport = createPinnedTransport({
115
+ destination: input.destination,
116
+ lease,
117
+ sender,
118
+ limits: input.limits,
119
+ transportId: "lab-automation",
120
+ });
121
+ const path = liveUpstreamRequestPath(input.routeContext.upstreamProtocol);
122
+ const response = await transport.request({
123
+ method: "POST",
124
+ path,
125
+ body: input.initiatingRequest,
126
+ signal: input.signal,
127
+ });
128
+ return normalizeLabLiveTransportObservation(input.routeContext, response);
129
+ });
130
+ }
@@ -0,0 +1,100 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { isLocalAttestationSecret } from "./local-management-attestation";
3
+
4
+ export const LOCAL_MANAGEMENT_EXPECTED_PID_HEADER = "x-opencodex-local-expected-pid";
5
+ export const LOCAL_MANAGEMENT_NONCE_HEADER = "x-opencodex-local-nonce";
6
+ export const LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER = "x-opencodex-local-expires-at";
7
+ export const LOCAL_MANAGEMENT_CAPABILITY_HEADER = "x-opencodex-local-capability";
8
+ export const LOCAL_MANAGEMENT_CAPABILITY_TTL_MS = 10_000;
9
+
10
+ export const LOCAL_MANAGEMENT_READ_PATHS = {
11
+ codexAccounts: "/api/codex-auth/accounts",
12
+ systemMemory: "/api/system/memory",
13
+ } as const;
14
+
15
+ export type LocalManagementReadPath =
16
+ typeof LOCAL_MANAGEMENT_READ_PATHS[keyof typeof LOCAL_MANAGEMENT_READ_PATHS];
17
+
18
+ const BASE64URL_256 = /^[A-Za-z0-9_-]{43}$/;
19
+ const LOCAL_READ_METHOD = "GET";
20
+
21
+ export type ExpectedLocalManagementPid =
22
+ | { kind: "absent" }
23
+ | { kind: "invalid" }
24
+ | { kind: "present"; pid: number };
25
+
26
+ export function parseExpectedLocalManagementPid(value: string | null): ExpectedLocalManagementPid {
27
+ if (value === null) return { kind: "absent" };
28
+ if (!/^[1-9]\d*$/.test(value)) return { kind: "invalid" };
29
+ const pid = Number(value);
30
+ return Number.isSafeInteger(pid) ? { kind: "present", pid } : { kind: "invalid" };
31
+ }
32
+
33
+ function isLocalManagementReadPath(path: string): path is LocalManagementReadPath {
34
+ return path === LOCAL_MANAGEMENT_READ_PATHS.codexAccounts
35
+ || path === LOCAL_MANAGEMENT_READ_PATHS.systemMemory;
36
+ }
37
+
38
+ function localReadCapabilityPayload(
39
+ nonce: string,
40
+ method: string,
41
+ path: string,
42
+ pid: number,
43
+ port: number,
44
+ expiresAt: number,
45
+ ): string | null {
46
+ if (!BASE64URL_256.test(nonce)) return null;
47
+ if (method !== LOCAL_READ_METHOD || !isLocalManagementReadPath(path)) return null;
48
+ if (!Number.isSafeInteger(pid) || pid <= 0) return null;
49
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) return null;
50
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= 0) return null;
51
+ return `opencodex-local-management-read-v1\n${nonce}\n${method}\n${path}\n${pid}\n${port}\n${expiresAt}`;
52
+ }
53
+
54
+ /** Process-scoped authorization for one allowlisted local management GET. */
55
+ export function createLocalManagementReadCapability(
56
+ secret: string,
57
+ nonce: string,
58
+ method: string,
59
+ path: string,
60
+ pid: number,
61
+ port: number,
62
+ expiresAt: number,
63
+ ): string | null {
64
+ if (!isLocalAttestationSecret(secret)) return null;
65
+ const payload = localReadCapabilityPayload(nonce, method, path, pid, port, expiresAt);
66
+ if (!payload) return null;
67
+ return createHmac("sha256", secret).update(payload).digest("base64url");
68
+ }
69
+
70
+ export function verifyLocalManagementReadCapability(
71
+ secret: string,
72
+ nonce: string | null,
73
+ method: string,
74
+ path: string,
75
+ pid: number,
76
+ port: number,
77
+ expiresAt: number,
78
+ capability: string | null,
79
+ now = Date.now(),
80
+ ): boolean {
81
+ if (!nonce || !capability || !BASE64URL_256.test(capability)) return false;
82
+ if (
83
+ !Number.isSafeInteger(now)
84
+ || expiresAt <= now
85
+ || expiresAt > now + LOCAL_MANAGEMENT_CAPABILITY_TTL_MS
86
+ ) return false;
87
+ const expected = createLocalManagementReadCapability(
88
+ secret,
89
+ nonce,
90
+ method,
91
+ path,
92
+ pid,
93
+ port,
94
+ expiresAt,
95
+ );
96
+ if (!expected) return false;
97
+ const expectedBytes = Buffer.from(expected);
98
+ const actualBytes = Buffer.from(capability);
99
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
100
+ }
@@ -0,0 +1,71 @@
1
+ type Cleanup = () => void;
2
+
3
+ type ServerResourceOwner = {
4
+ token: symbol;
5
+ cleanups: Set<Cleanup>;
6
+ };
7
+
8
+ export type ServerResourceOwnerLease = {
9
+ release(): void;
10
+ };
11
+
12
+ const owners: ServerResourceOwner[] = [];
13
+
14
+ function currentOwner(): ServerResourceOwner | undefined {
15
+ return owners.at(-1);
16
+ }
17
+
18
+ /** Acquire ownership for resources created while one server instance starts. */
19
+ export function acquireServerResourceOwner(): ServerResourceOwnerLease {
20
+ const owner: ServerResourceOwner = {
21
+ token: Symbol("server-resource-owner"),
22
+ cleanups: new Set(),
23
+ };
24
+ owners.push(owner);
25
+
26
+ let released = false;
27
+ return {
28
+ release() {
29
+ if (released) return;
30
+ released = true;
31
+ const index = owners.findIndex(candidate => candidate.token === owner.token);
32
+ if (index !== -1) owners.splice(index, 1);
33
+ for (const cleanup of [...owner.cleanups]) {
34
+ try {
35
+ cleanup();
36
+ } catch {
37
+ // Resource cleanup is best-effort and one failed hook must not retain siblings.
38
+ }
39
+ }
40
+ owner.cleanups.clear();
41
+ },
42
+ };
43
+ }
44
+
45
+ /** Attach cleanup to the server instance currently being started. */
46
+ export function registerCurrentServerResourceCleanup(cleanup: Cleanup): () => void {
47
+ const owner = currentOwner();
48
+ if (!owner) return () => {};
49
+ owner.cleanups.add(cleanup);
50
+ let attached = true;
51
+ return () => {
52
+ if (!attached) return;
53
+ attached = false;
54
+ owner.cleanups.delete(cleanup);
55
+ };
56
+ }
57
+
58
+ /** Test-only reset for isolated lifecycle tests. */
59
+ export function resetServerResourceOwnershipForTests(): void {
60
+ for (const owner of [...owners].reverse()) {
61
+ for (const cleanup of [...owner.cleanups]) {
62
+ try {
63
+ cleanup();
64
+ } catch {
65
+ // Test reset must leave no retained owner even when a synthetic cleanup throws.
66
+ }
67
+ }
68
+ owner.cleanups.clear();
69
+ }
70
+ owners.length = 0;
71
+ }
@@ -21,6 +21,13 @@ type TrustedSystemDirectoryResolver = () => string;
21
21
  type IsUserAnAdmin = () => number;
22
22
  type WindowsElevationProbe = () => boolean | null;
23
23
 
24
+ export class WindowsSystemDirectoryFfiUnavailableError extends Error {
25
+ constructor() {
26
+ super("Failed to load GetSystemDirectoryW from kernel32.dll.");
27
+ this.name = "WindowsSystemDirectoryFfiUnavailableError";
28
+ }
29
+ }
30
+
24
31
  let getSystemDirectoryWFn: GetSystemDirectoryW | null | undefined;
25
32
  let trustedSystemDirectoryResolverForTests: TrustedSystemDirectoryResolver | null = null;
26
33
  let isUserAnAdminFn: (() => boolean) | null | undefined;
@@ -114,7 +121,7 @@ export function resolveTrustedWindowsSystemDirectory(): string {
114
121
  }
115
122
  const getSystemDirectoryW = loadGetSystemDirectoryW();
116
123
  if (!getSystemDirectoryW) {
117
- throw new Error("Failed to load GetSystemDirectoryW from kernel32.dll.");
124
+ throw new WindowsSystemDirectoryFfiUnavailableError();
118
125
  }
119
126
 
120
127
  let size = 260;
@@ -20,11 +20,71 @@
20
20
  * would mean passing an absolute deadline through the runner interface.
21
21
  */
22
22
 
23
- import { resolveTrustedWindowsPowerShellExe } from "./windows-elevation";
23
+ import { existsSync } from "node:fs";
24
+ import { win32 as windowsPath } from "node:path";
25
+
26
+ import {
27
+ resolveTrustedWindowsPowerShellExe,
28
+ WindowsSystemDirectoryFfiUnavailableError,
29
+ } from "./windows-elevation";
24
30
 
25
31
  const SID_PATTERN = /^S-1-(?:\d+-)+\d+$/i;
26
32
  const SID_EXPRESSION =
27
33
  "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value";
34
+ const DEFAULT_WINDOWS_ARM64_POWERSHELL = windowsPath.join(
35
+ "C:\\Windows\\System32",
36
+ "WindowsPowerShell",
37
+ "v1.0",
38
+ "powershell.exe",
39
+ );
40
+
41
+ type PrincipalExecutableResolution = Readonly<{
42
+ platform: NodeJS.Platform;
43
+ arch: string;
44
+ resolveTrusted: () => string;
45
+ pathExists: (path: string) => boolean;
46
+ }>;
47
+
48
+ /**
49
+ * Bun's Windows ARM64 build cannot currently execute the `bun:ffi` call used
50
+ * by the general System32 resolver. The ACL identity lookup has a narrower
51
+ * authority than the elevation helpers: it starts one non-elevated PowerShell
52
+ * command and accepts only a SID-shaped result. Keep its fallback equally
53
+ * narrow by using the fixed, OS-protected default installation path only.
54
+ *
55
+ * Environment and PATH lookup are deliberately absent. A Windows installation
56
+ * outside C:\\Windows keeps failing closed rather than executing a binary chosen
57
+ * by caller-controlled `SystemRoot`, `WINDIR`, or `PATH` values.
58
+ */
59
+ function resolveWindowsPrincipalPowerShellExecutable(
60
+ resolution: PrincipalExecutableResolution = {
61
+ platform: process.platform,
62
+ arch: process.arch,
63
+ resolveTrusted: resolveTrustedWindowsPowerShellExe,
64
+ pathExists: existsSync,
65
+ },
66
+ ): string {
67
+ try {
68
+ return resolution.resolveTrusted();
69
+ } catch (error) {
70
+ if (
71
+ !(error instanceof WindowsSystemDirectoryFfiUnavailableError) ||
72
+ resolution.platform !== "win32" ||
73
+ resolution.arch !== "arm64" ||
74
+ !resolution.pathExists(DEFAULT_WINDOWS_ARM64_POWERSHELL)
75
+ ) {
76
+ throw error;
77
+ }
78
+ return DEFAULT_WINDOWS_ARM64_POWERSHELL;
79
+ }
80
+ }
81
+
82
+ /** Test-only dependency-injected view of the Windows ARM64 executable boundary. */
83
+ export function resolveWindowsPrincipalPowerShellExecutableForTests(
84
+ resolution: PrincipalExecutableResolution,
85
+ ): string {
86
+ return resolveWindowsPrincipalPowerShellExecutable(resolution);
87
+ }
28
88
 
29
89
  export interface WindowsPrincipalLookupResult {
30
90
  success: boolean;
@@ -52,7 +112,7 @@ const POWERSHELL_ARGS = [
52
112
  ] as const;
53
113
 
54
114
  function windowsPrincipalPowerShellCommand(): string[] {
55
- return [resolveTrustedWindowsPowerShellExe(), ...POWERSHELL_ARGS];
115
+ return [resolveWindowsPrincipalPowerShellExecutable(), ...POWERSHELL_ARGS];
56
116
  }
57
117
 
58
118
  /** Test-only readback of the exact trusted executable and static arguments. */
package/src/lib/winsw.ts CHANGED
@@ -17,7 +17,7 @@ import { createHash } from "node:crypto";
17
17
  import { execFileSync } from "node:child_process";
18
18
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { join, resolve } from "node:path";
20
+ import { join, resolve, win32 } from "node:path";
21
21
  import { expandUserPath, getConfigDir, loadConfig } from "../config";
22
22
  import { recordOwnedConfigPath } from "./config-ownership";
23
23
  import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "./bun-runtime";
@@ -63,6 +63,11 @@ function currentCodexHomeAbsolute(): string {
63
63
  return raw ? resolve(expandUserPath(raw)) : join(homedir(), ".codex");
64
64
  }
65
65
 
66
+ function windowsServicePathAbsolute(raw: string): string {
67
+ const expanded = expandUserPath(raw);
68
+ return win32.isAbsolute(expanded) ? win32.normalize(expanded) : resolve(expanded);
69
+ }
70
+
66
71
  export interface WinswEntry {
67
72
  bun: string;
68
73
  /** Provenance of `bun`, resolved together with it so the two can never disagree. */
@@ -103,6 +108,7 @@ export function buildWinswXml(entry: WinswEntry, env: NodeJS.ProcessEnv = proces
103
108
  ` <env name="OCX_API_TOKEN_FILE" value="${xmlEscape(serviceApiTokenFilePath())}"/>`,
104
109
  ` <env name="PATH" value="${xmlEscape(env.PATH ?? "")}"/>`,
105
110
  env.CODEX_HOME?.trim() ? ` <env name="CODEX_HOME" value="${xmlEscape(currentCodexHomeAbsolute())}"/>` : null,
111
+ env.CODEX_SQLITE_HOME?.trim() ? ` <env name="CODEX_SQLITE_HOME" value="${xmlEscape(windowsServicePathAbsolute(env.CODEX_SQLITE_HOME.trim()))}"/>` : null,
106
112
  ` <env name="OPENCODEX_HOME" value="${xmlEscape(getConfigDir())}"/>`,
107
113
  aclTimeout ? ` <env name="OPENCODEX_ACL_TIMEOUT_MS" value="${xmlEscape(aclTimeout)}"/>` : null,
108
114
  ].filter((line): line is string => Boolean(line));
@@ -394,4 +400,4 @@ export function winswStatusSummary(): string {
394
400
  export function defaultWinswEntry(cliDir: string): WinswEntry {
395
401
  const runtime = durableBunRuntime();
396
402
  return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(cliDir, "cli", "index.ts") };
397
- }
403
+ }