@bitkyc08/opencodex 2.11.1 → 2.12.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 (175) hide show
  1. package/README.md +1 -1
  2. package/bin/ocx.mjs +4 -0
  3. package/gui/dist/assets/index-BNVYzdn0.css +1 -0
  4. package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/command-code.ts +94 -5
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-tool-schema.ts +80 -15
  10. package/src/adapters/google.ts +24 -7
  11. package/src/adapters/openai-chat.ts +233 -305
  12. package/src/adapters/openai-responses.ts +62 -0
  13. package/src/adapters/tool-catalog-nudge.ts +2 -11
  14. package/src/claude/outbound.ts +42 -14
  15. package/src/cli/claude.ts +61 -9
  16. package/src/cli/codex-shim-autorestore.ts +2 -0
  17. package/src/cli/codex-shim-readiness.ts +8 -1
  18. package/src/cli/config-command.ts +4 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +15 -0
  21. package/src/cli/index.ts +27 -11
  22. package/src/cli/lab.ts +367 -0
  23. package/src/cli/provider.ts +9 -1
  24. package/src/clients/config-export.ts +100 -3
  25. package/src/codex/app-server-processes.ts +11 -8
  26. package/src/codex/auth-api.ts +92 -28
  27. package/src/codex/auth-collision.ts +5 -3
  28. package/src/codex/catalog/metadata.ts +7 -3
  29. package/src/codex/catalog/provider-fetch.ts +191 -47
  30. package/src/codex/catalog.ts +2 -2
  31. package/src/codex/history-job.ts +83 -18
  32. package/src/codex/history-migration-guardian.ts +9 -16
  33. package/src/codex/history-provider.ts +178 -1
  34. package/src/codex/history-transition.ts +5 -5
  35. package/src/codex/history-worker.ts +22 -2
  36. package/src/codex/native-profile-store.ts +1 -1
  37. package/src/codex/plan.ts +15 -0
  38. package/src/codex/quota.ts +6 -6
  39. package/src/codex/routing.ts +3 -3
  40. package/src/codex/subagent-model-fallback.ts +246 -22
  41. package/src/codex/user-identity.ts +63 -12
  42. package/src/config.ts +265 -32
  43. package/src/images/loop.ts +3 -1
  44. package/src/images/plan.ts +23 -13
  45. package/src/integrations/omp-yaml-source.ts +225 -0
  46. package/src/integrations/ownership.ts +5 -5
  47. package/src/integrations/state.ts +45 -6
  48. package/src/integrations/writer.ts +38 -3
  49. package/src/lab/artifacts/sanitize.ts +586 -0
  50. package/src/lab/artifacts/secure-fs.ts +475 -0
  51. package/src/lab/artifacts/store.ts +310 -0
  52. package/src/lab/conformance/assertion.ts +325 -0
  53. package/src/lab/conformance/digest.ts +22 -0
  54. package/src/lab/conformance/executor.ts +741 -0
  55. package/src/lab/conformance/fixture-provider.ts +27 -0
  56. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  57. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  58. package/src/lab/conformance/harness-budget.ts +47 -0
  59. package/src/lab/conformance/index.ts +5 -0
  60. package/src/lab/conformance/jcs.ts +24 -0
  61. package/src/lab/conformance/json-pointer.ts +39 -0
  62. package/src/lab/conformance/manifest.ts +180 -0
  63. package/src/lab/conformance/mcp-stub.ts +179 -0
  64. package/src/lab/conformance/negative-controls.ts +160 -0
  65. package/src/lab/conformance/observation.ts +355 -0
  66. package/src/lab/conformance/runner.ts +57 -0
  67. package/src/lab/conformance/sse-normalize.ts +59 -0
  68. package/src/lab/conformance/suite-manifest.ts +78 -0
  69. package/src/lab/conformance/types.ts +191 -0
  70. package/src/lab/constants.ts +126 -0
  71. package/src/lab/digest.ts +64 -0
  72. package/src/lab/events/errors.ts +9 -0
  73. package/src/lab/events/limits.ts +117 -0
  74. package/src/lab/events/types.ts +228 -0
  75. package/src/lab/events/validate.ts +780 -0
  76. package/src/lab/index.ts +36 -0
  77. package/src/lab/ledger/artifact-refs.ts +127 -0
  78. package/src/lab/ledger/invalidation.ts +136 -0
  79. package/src/lab/ledger/purge.ts +241 -0
  80. package/src/lab/ledger/store.ts +263 -0
  81. package/src/lab/live/credential-lease.ts +53 -0
  82. package/src/lab/live/destination.ts +155 -0
  83. package/src/lab/live/executor.ts +311 -0
  84. package/src/lab/live/inert-tools.ts +56 -0
  85. package/src/lab/live/manifest.ts +85 -0
  86. package/src/lab/live/mcp-loopback.ts +57 -0
  87. package/src/lab/live/runner.ts +19 -0
  88. package/src/lab/live/sandbox.ts +61 -0
  89. package/src/lab/live/suite-manifest.ts +41 -0
  90. package/src/lab/live/transport.ts +116 -0
  91. package/src/lab/live/types.ts +197 -0
  92. package/src/lab/observe/from-conformance.ts +344 -0
  93. package/src/lab/observe/from-live.ts +103 -0
  94. package/src/lab/paths.ts +65 -0
  95. package/src/lab/projection/rebuild.ts +477 -0
  96. package/src/lab/projection/schema.ts +122 -0
  97. package/src/lab/projection/verdicts.ts +438 -0
  98. package/src/lab/projection/verification.ts +332 -0
  99. package/src/lab/query/catalog.ts +72 -0
  100. package/src/lab/query/connection.ts +107 -0
  101. package/src/lab/query/constants.ts +4 -0
  102. package/src/lab/query/cursor.ts +132 -0
  103. package/src/lab/query/dto-map.ts +277 -0
  104. package/src/lab/query/errors.ts +22 -0
  105. package/src/lab/query/index.ts +34 -0
  106. package/src/lab/query/queries.ts +444 -0
  107. package/src/lab/query/types.ts +266 -0
  108. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  109. package/src/lab/subject/installation-salt.ts +81 -0
  110. package/src/lab/subject/route-subject.ts +73 -0
  111. package/src/lib/bounded-body.ts +104 -0
  112. package/src/lib/lab-live-execution-authority.ts +13 -0
  113. package/src/lib/lab-live-host.ts +30 -0
  114. package/src/lib/lab-live-pinned-sender.ts +45 -0
  115. package/src/lib/pinned-http.ts +36 -7
  116. package/src/oauth/index.ts +20 -1
  117. package/src/oauth/local-token-detect.ts +11 -2
  118. package/src/oauth/login-cli.ts +38 -4
  119. package/src/providers/alibaba-region-migration.ts +1 -1
  120. package/src/providers/codex-capacity.ts +7 -4
  121. package/src/providers/command-code-efforts.ts +5 -2
  122. package/src/providers/derive.ts +105 -0
  123. package/src/providers/label.ts +1 -1
  124. package/src/providers/openai-tiers.ts +59 -3
  125. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  126. package/src/providers/quota.ts +67 -21
  127. package/src/providers/registry.ts +118 -5
  128. package/src/router.ts +15 -1
  129. package/src/routing/analytics.ts +8 -7
  130. package/src/routing/evaluator.ts +51 -5
  131. package/src/routing/profile.ts +10 -1
  132. package/src/routing/trace.ts +42 -6
  133. package/src/server/auth-cors.ts +15 -0
  134. package/src/server/background-lifecycle.ts +169 -0
  135. package/src/server/chat-completions.ts +18 -3
  136. package/src/server/claude-messages.ts +1 -0
  137. package/src/server/images.ts +78 -11
  138. package/src/server/index.ts +72 -18
  139. package/src/server/lifecycle.ts +6 -0
  140. package/src/server/live.ts +5 -1
  141. package/src/server/management/agent-settings-routes.ts +41 -24
  142. package/src/server/management/body.ts +6 -0
  143. package/src/server/management/config-routes.ts +51 -8
  144. package/src/server/management/lab-routes.ts +388 -0
  145. package/src/server/management/logs-usage-routes.ts +19 -1
  146. package/src/server/management/model-rows.ts +25 -11
  147. package/src/server/management/native-integration-routes.ts +3 -3
  148. package/src/server/management/provider-routes.ts +5 -0
  149. package/src/server/management/shared.ts +8 -1
  150. package/src/server/management/usage-summary-cache.ts +2 -0
  151. package/src/server/management/vision-sidecar-options.ts +125 -0
  152. package/src/server/management-api.ts +2 -0
  153. package/src/server/request-decompress.ts +106 -5
  154. package/src/server/responses/collaboration.ts +4 -1
  155. package/src/server/responses/core.ts +117 -31
  156. package/src/server/responses-json-events.ts +50 -12
  157. package/src/server/search.ts +23 -7
  158. package/src/server/ws-bridge.ts +3 -1
  159. package/src/service.ts +40 -20
  160. package/src/types.ts +74 -0
  161. package/src/update/job.ts +10 -2
  162. package/src/usage/cost.ts +0 -0
  163. package/src/usage/expected-prices.ts +9 -0
  164. package/src/usage/log.ts +9 -9
  165. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  166. package/src/usage/user-cost-overlays.ts +314 -0
  167. package/src/vision/describe.ts +2 -2
  168. package/src/vision/eligibility.ts +229 -0
  169. package/src/vision/index.ts +20 -2
  170. package/src/web-search/executor.ts +2 -2
  171. package/src/web-search/index.ts +4 -2
  172. package/src/web-search/loop.ts +3 -1
  173. package/src/web-search/parse.ts +72 -3
  174. package/gui/dist/assets/index-BA1dgo4q.css +0 -1
  175. package/gui/dist/assets/index-DDZpgzKk.js +0 -70
@@ -0,0 +1,116 @@
1
+ import type { FailureClassification } from "../conformance/types";
2
+ import { assertLeaseScope, LabCredentialError } from "./credential-lease";
3
+ import type {
4
+ LabCredentialLeaseV1, LabDestinationV1, LabPinnedSender, LabTransport, LabTransportRequest,
5
+ LabTransportResponse, LiveRunConfig, TransportErrorCode,
6
+ } from "./types";
7
+
8
+ export interface MockTransportEntry {
9
+ matchPath?: string | RegExp;
10
+ status: number;
11
+ headers?: Record<string, string>;
12
+ body: string;
13
+ error?: TransportErrorCode;
14
+ }
15
+ export interface MockTransportOptions { entries?: MockTransportEntry[]; onRequest?: (req: LabTransportRequest) => void }
16
+
17
+ /** Test seam only. No live runner path creates this implicitly. */
18
+ export function createMockTransport(opts: MockTransportOptions = {}): LabTransport {
19
+ let callIndex = 0;
20
+ return {
21
+ async request(req): Promise<LabTransportResponse> {
22
+ opts.onRequest?.(req);
23
+ const entry = opts.entries?.[callIndex] ?? opts.entries?.[opts.entries.length - 1];
24
+ callIndex += 1;
25
+ if (!entry) throw new TransportError("harness_failure", "no mock transport entry");
26
+ if (entry.error) throw new TransportError(entry.error, entry.error);
27
+ if (entry.matchPath) {
28
+ const ok = typeof entry.matchPath === "string" ? req.path.includes(entry.matchPath) : entry.matchPath.test(req.path);
29
+ if (!ok) throw new TransportError("harness_failure", "mock path mismatch");
30
+ }
31
+ if (entry.status >= 300 && entry.status < 400) throw new TransportError("redirect_blocked", "redirect response");
32
+ return { status: entry.status, headers: { "content-type": "application/json", ...(entry.headers ?? {}) }, body: entry.body };
33
+ },
34
+ };
35
+ }
36
+
37
+ export interface PinnedTransportOptions {
38
+ destination: LabDestinationV1;
39
+ lease: LabCredentialLeaseV1;
40
+ sender: LabPinnedSender;
41
+ limits: LiveRunConfig;
42
+ transportId?: string;
43
+ }
44
+
45
+ function assertRelativeRequestPath(path: string): void {
46
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("://") || /[\r\n]/.test(path)) {
47
+ throw new TransportError("harness_failure", "request path attempted to widen destination");
48
+ }
49
+ }
50
+
51
+ function selectedPinnedAddress(destination: LabDestinationV1): { address: string; family: 4 | 6 } {
52
+ const row = destination.addresses.find((a) => a.family === 4) ?? destination.addresses[0];
53
+ if (!row) throw new TransportError("harness_failure", "approved destination has no address");
54
+ return { address: row.address, family: row.family };
55
+ }
56
+
57
+ /** Uses only the frozen approved address set. Secret injection is owned by the trusted sender, outside Lab. */
58
+ export function createPinnedTransport(opts: PinnedTransportOptions): LabTransport {
59
+ const transportId = opts.transportId ?? "default";
60
+ return {
61
+ async request(req): Promise<LabTransportResponse> {
62
+ assertRelativeRequestPath(req.path);
63
+ assertLeaseScope(opts.lease, opts.destination, transportId);
64
+ const inputBytes = new TextEncoder().encode(req.body ?? "").byteLength;
65
+ if (inputBytes > opts.limits.maxInputBytes) throw new TransportError("input_byte_limit", "request exceeds input byte budget");
66
+ opts.lease.consume();
67
+ const controller = new AbortController();
68
+ const timer = setTimeout(() => controller.abort(new TransportError("total_timeout", "live request total timeout")), opts.limits.totalTimeoutMs);
69
+ const onAbort = () => controller.abort(req.signal?.reason);
70
+ req.signal?.addEventListener("abort", onAbort, { once: true });
71
+ try {
72
+ const response = await opts.sender(opts.lease, opts.destination, selectedPinnedAddress(opts.destination), req, controller.signal, opts.limits);
73
+ const outputBytes = new TextEncoder().encode(response.body).byteLength;
74
+ if (outputBytes > opts.limits.maxOutputBytes) throw new TransportError("output_byte_limit", "response exceeds output byte budget");
75
+ if (response.status >= 300 && response.status < 400) throw new TransportError("redirect_blocked", "redirect response");
76
+ if (response.status === 401 || response.status === 403) throw new TransportError("auth_blocked", `HTTP ${response.status}`);
77
+ if (response.status === 429) throw new TransportError("quota_blocked", "HTTP 429");
78
+ if (response.status === 451) throw new TransportError("region_blocked", "HTTP 451");
79
+ if (response.status >= 500) throw new TransportError("provider_transient", `HTTP ${response.status}`);
80
+ return response;
81
+ } catch (error) {
82
+ if (error instanceof LabCredentialError) {
83
+ throw new TransportError(error.code === "budget_exhausted" ? "request_limit" : "harness_failure", error.message);
84
+ }
85
+ throw error;
86
+ } finally {
87
+ clearTimeout(timer);
88
+ req.signal?.removeEventListener("abort", onAbort);
89
+ }
90
+ },
91
+ };
92
+ }
93
+
94
+ export class TransportError extends Error {
95
+ override readonly name = "TransportError";
96
+ constructor(readonly code: TransportErrorCode, message: string) { super(message); }
97
+ }
98
+
99
+ export function classifyTransportError(error: unknown): { classification: FailureClassification; secondaryCode: string } {
100
+ const code = error instanceof TransportError ? error.code : undefined;
101
+ switch (code) {
102
+ case "auth_blocked": return { classification: "authentication_blocked", secondaryCode: code };
103
+ case "quota_blocked": return { classification: "quota_blocked", secondaryCode: code };
104
+ case "network_blocked": return { classification: "network_failure", secondaryCode: code };
105
+ case "region_blocked": return { classification: "region_blocked", secondaryCode: code };
106
+ case "provider_transient": return { classification: "provider_transient", secondaryCode: code };
107
+ case "connect_timeout": case "first_byte_timeout": case "inactivity_timeout": case "total_timeout":
108
+ return { classification: "timeout", secondaryCode: code };
109
+ case "request_limit": case "input_byte_limit": case "output_byte_limit": case "output_token_limit":
110
+ case "tool_call_limit": case "artifact_byte_limit": case "memory_limit": case "child_process_limit":
111
+ return { classification: "budget_exhausted", secondaryCode: code };
112
+ case "live_transport_required": case "untrusted_route_executor": case "redirect_blocked": case "destination_mismatch": case "host_sni_mismatch": case "harness_failure":
113
+ return { classification: "harness_failure", secondaryCode: code };
114
+ default: return { classification: "harness_failure", secondaryCode: "execution_error" };
115
+ }
116
+ }
@@ -0,0 +1,197 @@
1
+ import type { FailureClassification, NormalizedObservation } from "../conformance/types";
2
+ import type { RouteDependencyV1, RouteSubjectV1 } from "../events/types";
3
+
4
+ export interface LabDestinationV1 {
5
+ readonly scheme: "http" | "https";
6
+ readonly host: string;
7
+ readonly port: number;
8
+ readonly basePath: string;
9
+ readonly sniHost: string;
10
+ readonly addresses: ReadonlyArray<{ readonly address: string; readonly family: 4 | 6 }>;
11
+ readonly privateNetwork: boolean;
12
+ readonly fingerprint: string;
13
+ }
14
+
15
+ export type LabBehaviorSource =
16
+ | "request"
17
+ | "model_override"
18
+ | "provider_config"
19
+ | "registry_runtime_default"
20
+ | "generated_model_metadata"
21
+ | "global_config"
22
+ | "adapter_default"
23
+ | "lab_forced";
24
+
25
+ export interface LabBehaviorValue {
26
+ source: LabBehaviorSource;
27
+ value: unknown;
28
+ }
29
+
30
+ /** Authoritative effective values emitted by the production route/model/adapter resolver. */
31
+ export type LabBehaviorValues = Record<string, LabBehaviorValue>;
32
+
33
+ export interface LabRouteContext {
34
+ providerId: string;
35
+ /** Config-owner identity only; HMACed before it enters evidence. */
36
+ providerInstanceKey: string;
37
+ clientModelId: string;
38
+ upstreamModelId: string;
39
+ effectiveAdapter: string;
40
+ inboundProtocol: string;
41
+ upstreamProtocol: string;
42
+ surface: string;
43
+ baseUrl: string;
44
+ /** Generated compatibility hash, not package marketing version. */
45
+ opencodexCompatibilityVersion: string;
46
+ /** Closed, effective behavior inputs from the production resolver. */
47
+ behaviorValues: LabBehaviorValues;
48
+ allowPrivateNetwork?: boolean;
49
+ labRunApproval?: boolean;
50
+ requiredClaims?: string[];
51
+ availableHarnessFeatures?: string[];
52
+ dependencies?: RouteDependencyV1[];
53
+ }
54
+
55
+ export interface LiveRunConfig {
56
+ totalTimeoutMs: number;
57
+ connectTimeoutMs: number;
58
+ firstByteTimeoutMs: number;
59
+ inactivityTimeoutMs: number;
60
+ maxRequests: number;
61
+ maxInputBytes: number;
62
+ maxOutputBytes: number;
63
+ maxOutputTokens: number;
64
+ maxToolCalls: number;
65
+ maxMemoryBytes: number;
66
+ maxChildProcesses: number;
67
+ maxArtifacts: number;
68
+ perArtifactBytes: number;
69
+ aggregateArtifactBytes: number;
70
+ }
71
+
72
+ export type DnsResolver = (hostname: string) => Promise<Array<{ address: string; family: 4 | 6 }>>;
73
+
74
+ export type TransportErrorCode =
75
+ | "auth_blocked"
76
+ | "quota_blocked"
77
+ | "network_blocked"
78
+ | "region_blocked"
79
+ | "provider_transient"
80
+ | "redirect_blocked"
81
+ | "destination_mismatch"
82
+ | "host_sni_mismatch"
83
+ | "connect_timeout"
84
+ | "first_byte_timeout"
85
+ | "inactivity_timeout"
86
+ | "total_timeout"
87
+ | "request_limit"
88
+ | "input_byte_limit"
89
+ | "output_byte_limit"
90
+ | "output_token_limit"
91
+ | "tool_call_limit"
92
+ | "artifact_byte_limit"
93
+ | "memory_limit"
94
+ | "child_process_limit"
95
+ | "live_transport_required"
96
+ | "untrusted_route_executor"
97
+ | "harness_failure";
98
+
99
+ export interface LabTransportRequest {
100
+ method: "GET" | "POST";
101
+ path: string;
102
+ headers?: Record<string, string>;
103
+ body?: string;
104
+ signal?: AbortSignal;
105
+ }
106
+
107
+ export interface LabTransportResponse {
108
+ status: number;
109
+ headers: Record<string, string>;
110
+ body: string;
111
+ }
112
+
113
+ export type TransportRequest = LabTransportRequest;
114
+ export type TransportResponse = LabTransportResponse;
115
+
116
+ export interface LabTransport {
117
+ request(req: LabTransportRequest): Promise<LabTransportResponse>;
118
+ }
119
+
120
+ export const LAB_CREDENTIAL_LEASE = Symbol("LabCredentialLeaseV1");
121
+
122
+ /** Opaque capability. Scope metadata and secret material are deliberately not public fields. */
123
+ export interface LabCredentialLeaseV1 {
124
+ readonly [LAB_CREDENTIAL_LEASE]: true;
125
+ readonly remainingRequests: number;
126
+ consume(): void;
127
+ }
128
+
129
+ export interface LabPinnedAddress {
130
+ address: string;
131
+ family: 4 | 6;
132
+ }
133
+
134
+ /** Trusted provider/credential transport seam. Implementations own secret injection outside Lab. */
135
+ export type LabPinnedSender = (
136
+ lease: LabCredentialLeaseV1,
137
+ destination: LabDestinationV1,
138
+ pinned: LabPinnedAddress,
139
+ request: LabTransportRequest,
140
+ signal: AbortSignal,
141
+ limits: LiveRunConfig,
142
+ ) => Promise<LabTransportResponse>;
143
+
144
+ export interface LabRouteExecutorInput {
145
+ routeContext: LabRouteContext;
146
+ destination: LabDestinationV1;
147
+ routeSubject: RouteSubjectV1;
148
+ scenarioId: string;
149
+ initiatingRequest?: string;
150
+ limits: LiveRunConfig;
151
+ signal: AbortSignal;
152
+ environment: Readonly<Record<string, string>>;
153
+ }
154
+
155
+ /** Trusted exact-route execution function. The wrapper capability is issued only by the host integration. */
156
+ export type LabRouteExecutor = (input: LabRouteExecutorInput) => Promise<NormalizedObservation>;
157
+
158
+ export const REQUIRED_LAB_SANDBOX_BOUNDARIES = [
159
+ "destination_pinning",
160
+ "credential_binding",
161
+ "wall_clock",
162
+ "connect_timeout",
163
+ "first_byte_timeout",
164
+ "inactivity_timeout",
165
+ "request_budget",
166
+ "input_byte_budget",
167
+ "output_byte_budget",
168
+ "output_token_budget",
169
+ "tool_call_budget",
170
+ "memory_limit",
171
+ "child_process_limit",
172
+ "artifact_budget",
173
+ ] as const;
174
+ export type LabSandboxBoundary = typeof REQUIRED_LAB_SANDBOX_BOUNDARIES[number];
175
+
176
+ /** Opaque host-issued capability; runtime trust is established outside Lab. */
177
+ export interface TrustedLabRouteExecutor {
178
+ execute(input: LabRouteExecutorInput): Promise<NormalizedObservation>;
179
+ readonly enforcedBoundaries: readonly LabSandboxBoundary[];
180
+ }
181
+
182
+ export type LiveExecutionAuthority = "trusted_route" | "test_transport" | "none";
183
+
184
+ export interface LiveScenarioRunResult {
185
+ scenarioId: string;
186
+ suite: string;
187
+ startedAt: number;
188
+ completedAt: number;
189
+ passed: boolean;
190
+ classification: FailureClassification;
191
+ secondaryCode?: string;
192
+ assertionResults: import("../conformance/types").AssertionResult[];
193
+ diagnostics: string[];
194
+ routeSubject?: RouteSubjectV1;
195
+ transportError?: TransportErrorCode;
196
+ executionAuthority: LiveExecutionAuthority;
197
+ }
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Persistence seam: transform CL-01 conformance results into valid observation events.
3
+ * Deterministic harness execution stays separate from ledger persistence.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { createArtifactStore, type ArtifactStore } from "../artifacts/store";
7
+ import { sanitizeDiagnostic, truncateUtf8 } from "../artifacts/sanitize";
8
+ import {
9
+ LAB_EVENT_SCHEMA_VERSION,
10
+ LAB_PRODUCER,
11
+ LAB_PRODUCER_VERSION,
12
+ type ObservationOutcome,
13
+ } from "../constants";
14
+ import {
15
+ jcsStringify,
16
+ scenarioManifestDigest,
17
+ subjectIdForSubject,
18
+ suiteManifestDigest,
19
+ } from "../digest";
20
+ import type { ObservationEvent, ProtocolSubjectV1 } from "../events/types";
21
+ import { assignEventId } from "../events/validate";
22
+ import { appendLabEvent } from "../ledger/store";
23
+ import { ensureLabDirs } from "../paths";
24
+ import type {
25
+ CaseAuthority,
26
+ CaseRecord,
27
+ ProtocolExecutionContextV1,
28
+ ScenarioRunResult,
29
+ } from "../conformance/types";
30
+ import { expandScenario } from "../conformance/manifest";
31
+ import { suiteManifestObjectForCase } from "../conformance/suite-manifest";
32
+ import { fixtureDigest } from "../conformance/digest";
33
+ import { resolveProtocolExecutionContext } from "../conformance/executor";
34
+
35
+ const COMPAT_VERSION = "protocol-v1";
36
+
37
+ export interface PersistConformanceOptions {
38
+ configDir?: string;
39
+ recordedAt?: number;
40
+ /** Optional explicit override for the runner-provided execution timestamps. */
41
+ startedAt?: number;
42
+ completedAt?: number;
43
+ producerVersion?: string;
44
+ artifactStore?: ArtifactStore;
45
+ }
46
+
47
+ export interface PersistedConformanceObservation {
48
+ event: ObservationEvent;
49
+ ledgerPath: string;
50
+ }
51
+
52
+ function validateExecutionContext(
53
+ caseRecord: CaseRecord,
54
+ ctx: ProtocolExecutionContextV1,
55
+ ): ProtocolExecutionContextV1 {
56
+ const checks: Array<[string, string[], string]> = [
57
+ ["inboundProtocols", caseRecord.requirements.inboundProtocols, ctx.inboundProtocol],
58
+ ["upstreamProtocols", caseRecord.requirements.upstreamProtocols, ctx.upstreamProtocol],
59
+ ["surfaces", caseRecord.requirements.surfaces, ctx.surface],
60
+ ];
61
+ for (const [name, declared, actual] of checks) {
62
+ if (declared.length === 0) throw new Error(`case ${caseRecord.id} has empty ${name}`);
63
+ if (!declared.includes(actual)) {
64
+ throw new Error(`case ${caseRecord.id} execution context violates ${name}`);
65
+ }
66
+ }
67
+ return ctx;
68
+ }
69
+
70
+ function behaviorFingerprintForCase(
71
+ caseRecord: CaseRecord,
72
+ executionContext?: ProtocolExecutionContextV1,
73
+ ): string {
74
+ const ctx = validateExecutionContext(
75
+ caseRecord,
76
+ executionContext ?? resolveProtocolExecutionContext(caseRecord),
77
+ );
78
+ const adapter = upstreamAdapter(ctx.upstreamProtocol);
79
+ const values = {
80
+ schemaVersion: 1,
81
+ resolverVersion: 1,
82
+ values: {
83
+ "wire.adapter": {
84
+ source: "lab_forced",
85
+ value: adapter,
86
+ },
87
+ "wire.upstreamProtocol": {
88
+ source: "lab_forced",
89
+ value: ctx.upstreamProtocol,
90
+ },
91
+ "runtime.arch": {
92
+ source: "lab_forced",
93
+ value: process.arch,
94
+ },
95
+ "runtime.bunVersion": {
96
+ source: "lab_forced",
97
+ value: process.versions.bun ?? Bun.version,
98
+ },
99
+ "runtime.platform": {
100
+ source: "lab_forced",
101
+ value: process.platform,
102
+ },
103
+ },
104
+ };
105
+ return createHash("sha256").update(jcsStringify(values)).digest("hex");
106
+ }
107
+
108
+ function protocolSubject(caseRecord: CaseRecord, result: ScenarioRunResult): ProtocolSubjectV1 {
109
+ const ctx = validateExecutionContext(
110
+ caseRecord,
111
+ result.executionContext ?? resolveProtocolExecutionContext(caseRecord),
112
+ );
113
+ return {
114
+ subjectSchemaVersion: 1,
115
+ subjectKind: "protocol",
116
+ opencodexCompatibilityVersion: COMPAT_VERSION,
117
+ effectiveAdapter: upstreamAdapter(ctx.upstreamProtocol),
118
+ inboundProtocol: ctx.inboundProtocol,
119
+ upstreamProtocol: ctx.upstreamProtocol,
120
+ surface: ctx.surface,
121
+ behaviorFingerprint: behaviorFingerprintForCase(caseRecord, ctx),
122
+ };
123
+ }
124
+
125
+ function upstreamAdapter(protocol: string): string {
126
+ switch (protocol) {
127
+ case "openai-responses":
128
+ return "openai-responses";
129
+ case "anthropic-messages":
130
+ return "anthropic";
131
+ case "openai-chat":
132
+ return "openai-chat";
133
+ default:
134
+ throw new Error(`unsupported protocol identity: ${protocol}`);
135
+ }
136
+ }
137
+
138
+ function outcomeFromResult(result: ScenarioRunResult): ObservationOutcome {
139
+ if (result.passed) return "pass";
140
+ switch (result.classification) {
141
+ case "timeout":
142
+ case "budget_exhausted":
143
+ case "authentication_blocked":
144
+ case "quota_blocked":
145
+ case "region_blocked":
146
+ case "network_failure":
147
+ case "provider_transient":
148
+ return "blocked";
149
+ case "inconclusive":
150
+ case "harness_failure":
151
+ return "inconclusive";
152
+ case "protocol_failure":
153
+ case "capability_failure":
154
+ case "behavioral_failure":
155
+ return "fail";
156
+ default: {
157
+ const _never: never = result.classification;
158
+ throw new Error(`unmapped failure classification: ${String(_never)}`);
159
+ }
160
+ }
161
+ }
162
+
163
+ function requireExecutionTimes(
164
+ result: ScenarioRunResult,
165
+ opts: PersistConformanceOptions,
166
+ ): { startedAt: number; completedAt: number } {
167
+ const startedAt = opts.startedAt ?? result.startedAt;
168
+ const completedAt = opts.completedAt ?? result.completedAt;
169
+ if (!Number.isInteger(startedAt) || !Number.isInteger(completedAt)) {
170
+ throw new Error("real startedAt/completedAt are required for persisted conformance evidence");
171
+ }
172
+ if (startedAt < 0 || completedAt < startedAt) {
173
+ throw new Error("invalid persisted conformance execution timestamps");
174
+ }
175
+ return { startedAt, completedAt };
176
+ }
177
+
178
+ /**
179
+ * Build a valid protocol_conformance observation from one CL-01 scenario result.
180
+ * Does not append; use persistConformanceResult for ledger write.
181
+ */
182
+ export function observationFromConformanceResult(
183
+ result: ScenarioRunResult,
184
+ caseRecord: CaseRecord,
185
+ authority: CaseAuthority,
186
+ opts: PersistConformanceOptions = {},
187
+ ): { event: ObservationEvent; artifacts: ReturnType<ArtifactStore["put"]>[] } {
188
+ const paths = ensureLabDirs(opts.configDir);
189
+ const ownsStore = !opts.artifactStore;
190
+ const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
191
+ try {
192
+ const { startedAt, completedAt } = requireExecutionTimes(result, opts);
193
+ const recordedAt = opts.recordedAt ?? completedAt;
194
+
195
+ const expandedScenario = expandScenario(caseRecord, authority);
196
+ const scenarioDigest = scenarioManifestDigest(expandedScenario);
197
+ const suiteExpanded = suiteManifestObjectForCase(caseRecord, authority);
198
+ const suiteDigest = suiteManifestDigest(suiteExpanded);
199
+
200
+ const fixtureDigests: string[] = [];
201
+ const artifacts: ReturnType<ArtifactStore["put"]>[] = [];
202
+
203
+ const putFixture = (fixture: CaseRecord["fixture"]) => {
204
+ const bytes = new TextEncoder().encode(fixture.bytesUtf8);
205
+ const digest = fixtureDigest(bytes);
206
+ fixtureDigests.push(digest);
207
+ artifacts.push(
208
+ store.put({
209
+ artifactClass: "fixture",
210
+ payload: bytes,
211
+ expectedDigest: digest,
212
+ mediaType: fixture.mediaType,
213
+ }),
214
+ );
215
+ };
216
+ putFixture(caseRecord.fixture);
217
+ if (caseRecord.initiatingRequest) putFixture(caseRecord.initiatingRequest);
218
+
219
+ artifacts.push(
220
+ store.put({
221
+ artifactClass: "scenario_manifest",
222
+ payload: expandedScenario,
223
+ expectedDigest: scenarioDigest,
224
+ }),
225
+ );
226
+ artifacts.push(
227
+ store.put({
228
+ artifactClass: "suite_manifest",
229
+ payload: suiteExpanded,
230
+ expectedDigest: suiteDigest,
231
+ }),
232
+ );
233
+
234
+ const assertionReport = store.put({
235
+ artifactClass: "assertion_report",
236
+ payload: {
237
+ scenarioId: result.scenarioId,
238
+ passed: result.passed,
239
+ classification: result.classification,
240
+ assertions: result.assertionResults.map((a) => ({
241
+ id: a.id,
242
+ operator: a.operator,
243
+ required: a.required,
244
+ passed: a.passed,
245
+ observedSummary: a.observedSummary,
246
+ reason: a.reason,
247
+ })),
248
+ },
249
+ });
250
+ artifacts.push(assertionReport);
251
+
252
+ const subject = protocolSubject(caseRecord, result);
253
+ const subjectId = subjectIdForSubject(subject);
254
+ const outcome = outcomeFromResult(result);
255
+ const authorityLimits = authority.manifestDefaults.executionLimits;
256
+
257
+ const eventWithoutId = {
258
+ schemaVersion: LAB_EVENT_SCHEMA_VERSION,
259
+ eventKind: "observation" as const,
260
+ recordedAt,
261
+ producer: LAB_PRODUCER,
262
+ producerVersion: opts.producerVersion ?? LAB_PRODUCER_VERSION,
263
+ evidenceLayer: "protocol_conformance" as const,
264
+ scenarioId: caseRecord.id,
265
+ scenarioVersion: String(authority.manifestDefaults.version),
266
+ scenarioManifestDigest: scenarioDigest,
267
+ suiteId: caseRecord.suite,
268
+ suiteVersion: String(authority.manifestDefaults.suiteVersion),
269
+ suiteManifestDigest: suiteDigest,
270
+ fixtureDigests,
271
+ subject,
272
+ subjectId,
273
+ startedAt,
274
+ completedAt,
275
+ executionMode: "fixture" as const,
276
+ attempt: 1,
277
+ limits: { ...authorityLimits },
278
+ outcome,
279
+ assertions: result.assertionResults.map((a) => ({
280
+ id: a.id,
281
+ operator: a.operator,
282
+ required: a.required,
283
+ passed: a.passed,
284
+ expectedSummary: "see_assertion_report",
285
+ // Sanitize before truncating; see from-live.ts for the same boundary.
286
+ observedSummary: truncateUtf8(sanitizeDiagnostic(a.observedSummary), 512),
287
+ ...(a.reason ? { reason: a.reason } : {}),
288
+ })),
289
+ ...(caseRecord.expectedFailure
290
+ ? { expectedFailure: { ...caseRecord.expectedFailure } }
291
+ : {}),
292
+ environment: {
293
+ runtime: {
294
+ platform: process.platform,
295
+ arch: process.arch,
296
+ bunVersion: process.versions.bun ?? Bun.version,
297
+ },
298
+ },
299
+ artifactRefs: artifacts,
300
+ ...(result.passed
301
+ ? {}
302
+ : {
303
+ failure: {
304
+ class: result.classification,
305
+ code: result.secondaryCode ?? result.classification,
306
+ retryable: false,
307
+ attribution:
308
+ result.classification === "harness_failure"
309
+ ? ("harness" as const)
310
+ : ("opencodex" as const),
311
+ },
312
+ }),
313
+ };
314
+
315
+ const event = assignEventId(eventWithoutId) as ObservationEvent;
316
+ return { event, artifacts };
317
+ } finally {
318
+ if (ownsStore) store.close();
319
+ }
320
+ }
321
+
322
+ /** Transform → validate → append one CL-01 result into the canonical JSONL ledger. */
323
+ export function persistConformanceResult(
324
+ result: ScenarioRunResult,
325
+ caseRecord: CaseRecord,
326
+ authority: CaseAuthority,
327
+ opts: PersistConformanceOptions = {},
328
+ ): PersistedConformanceObservation {
329
+ const paths = ensureLabDirs(opts.configDir);
330
+ const ownsStore = !opts.artifactStore;
331
+ const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
332
+ try {
333
+ const { event } = observationFromConformanceResult(result, caseRecord, authority, {
334
+ ...opts,
335
+ artifactStore: store,
336
+ });
337
+ appendLabEvent(paths.ledgerPath, event);
338
+ return { event, ledgerPath: paths.ledgerPath };
339
+ } finally {
340
+ if (ownsStore) store.close();
341
+ }
342
+ }
343
+
344
+ export { behaviorFingerprintForCase };