@bitkyc08/opencodex 2.19.0 → 2.21.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 (130) hide show
  1. package/gui/dist/assets/index-BOFeam5a.js +76 -0
  2. package/gui/dist/assets/index-Xq49CY8F.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic.ts +2 -28
  6. package/src/adapters/google.ts +31 -3
  7. package/src/adapters/mimo-free.ts +17 -0
  8. package/src/adapters/openai-chat.ts +392 -40
  9. package/src/adapters/registry.ts +144 -0
  10. package/src/adapters/responses-tool-schema.ts +67 -0
  11. package/src/bridge.ts +15 -2
  12. package/src/chat/inbound.ts +13 -7
  13. package/src/claude/gateway-cache.ts +41 -4
  14. package/src/cli/claude.ts +3 -2
  15. package/src/cli/combo.ts +3 -0
  16. package/src/cli/dispatch.ts +8 -0
  17. package/src/cli/export-command.ts +2 -2
  18. package/src/cli/help.ts +2 -0
  19. package/src/cli/index.ts +3 -2
  20. package/src/cli/lab.ts +135 -1
  21. package/src/cli/minimax.ts +491 -0
  22. package/src/cli/models-runtime.ts +22 -1
  23. package/src/cli/models.ts +67 -2
  24. package/src/cli/opencode.ts +2 -1
  25. package/src/cli/registry.ts +22 -2
  26. package/src/clients/config-export.ts +125 -7
  27. package/src/codex/app-server-processes.ts +57 -2
  28. package/src/codex/app-server-restart-service.ts +232 -0
  29. package/src/codex/catalog/aggregation.ts +10 -1
  30. package/src/codex/catalog/effort.ts +15 -3
  31. package/src/codex/catalog/parsing.ts +3 -1
  32. package/src/codex/catalog/provider-fetch.ts +45 -5
  33. package/src/codex/catalog/sync.ts +74 -4
  34. package/src/codex/convergence.ts +2 -0
  35. package/src/combos/index.ts +1 -0
  36. package/src/combos/request.ts +30 -0
  37. package/src/combos/types.ts +6 -0
  38. package/src/config.ts +52 -0
  39. package/src/generated/compatibility-version.json +241 -85
  40. package/src/images/loop.ts +26 -6
  41. package/src/integrations/registry.ts +7 -0
  42. package/src/lab/conformance/jcs.ts +42 -2
  43. package/src/lab/conformance/negative-controls.ts +6 -2
  44. package/src/lab/conformance/runner.ts +16 -5
  45. package/src/lab/fabric/observe.ts +49 -14
  46. package/src/lab/index.ts +16 -0
  47. package/src/lab/ledger/purge.ts +152 -83
  48. package/src/lab/ledger/store.ts +168 -54
  49. package/src/lab/observe/from-conformance.ts +8 -6
  50. package/src/lab/observe/from-live.ts +8 -2
  51. package/src/lab/paths.ts +23 -0
  52. package/src/lab/public/bundle.ts +217 -0
  53. package/src/lab/public/community-authority.ts +175 -0
  54. package/src/lab/public/community-files.ts +29 -0
  55. package/src/lab/public/community.ts +479 -0
  56. package/src/lab/public/file-safety.ts +155 -0
  57. package/src/lab/public/ids.ts +26 -0
  58. package/src/lab/public/index.ts +16 -0
  59. package/src/lab/public/mutation-lock.ts +424 -0
  60. package/src/lab/public/operator.ts +353 -0
  61. package/src/lab/public/origin-purge.ts +79 -0
  62. package/src/lab/public/origin.ts +203 -0
  63. package/src/lab/public/privacy.ts +143 -0
  64. package/src/lab/public/private-file.ts +261 -0
  65. package/src/lab/public/project.ts +124 -0
  66. package/src/lab/public/purge-test-fault.ts +21 -0
  67. package/src/lab/public/purge.ts +223 -0
  68. package/src/lab/public/registry.ts +44 -0
  69. package/src/lab/public/revocation.ts +252 -0
  70. package/src/lab/public/signature.ts +219 -0
  71. package/src/lab/public/storage.ts +105 -0
  72. package/src/lab/public/strict-json.ts +206 -0
  73. package/src/lab/public/time.ts +26 -0
  74. package/src/lab/public/types.ts +172 -0
  75. package/src/lab/public/validate.ts +391 -0
  76. package/src/lib/codex-restart-contract.ts +120 -0
  77. package/src/lib/lab-activation.ts +109 -47
  78. package/src/lib/lab-live-pinned-sender.ts +16 -5
  79. package/src/lib/pinned-http.ts +70 -16
  80. package/src/lib/self-launch-argv.ts +15 -0
  81. package/src/lib/state-store-registrations.ts +2 -0
  82. package/src/lib/upstream-reachability.ts +4 -0
  83. package/src/lib/windows-elevation.ts +10 -1
  84. package/src/providers/derive.ts +24 -4
  85. package/src/providers/registry.ts +14 -4
  86. package/src/providers/request-pacing.ts +310 -0
  87. package/src/providers/service-tier.ts +143 -0
  88. package/src/providers/static-model-discovery.ts +86 -0
  89. package/src/reasoning-effort.ts +27 -1
  90. package/src/responses/custom-tool-compat.ts +4 -1
  91. package/src/responses/parser.ts +7 -1
  92. package/src/responses/provider-opaque-metadata.ts +73 -0
  93. package/src/responses/schema.ts +6 -0
  94. package/src/router.ts +23 -6
  95. package/src/routing/capability.ts +4 -2
  96. package/src/routing/compatibility/behavior.ts +5 -1
  97. package/src/server/adapter-resolve.ts +2 -32
  98. package/src/server/auth-cors.ts +50 -6
  99. package/src/server/chat-completions.ts +74 -36
  100. package/src/server/chat-native-sse.ts +331 -0
  101. package/src/server/chat-native.ts +371 -0
  102. package/src/server/management/combo-routes.ts +16 -2
  103. package/src/server/management/config-routes.ts +6 -4
  104. package/src/server/management/context.ts +17 -0
  105. package/src/server/management/lab-routes.ts +181 -19
  106. package/src/server/management/model-routes.ts +76 -2
  107. package/src/server/management/model-rows.ts +8 -0
  108. package/src/server/management/provider-capability-config.ts +48 -0
  109. package/src/server/management/provider-routes.ts +76 -4
  110. package/src/server/management/system-restart.ts +4 -2
  111. package/src/server/management/system-routes.ts +38 -0
  112. package/src/server/relay.ts +17 -3
  113. package/src/server/responses/compact.ts +4 -1
  114. package/src/server/responses/core.ts +257 -46
  115. package/src/server/responses/empty-completion-guard.ts +276 -0
  116. package/src/server/responses/fetch-helpers.ts +35 -4
  117. package/src/server/responses/pacing-overload.ts +13 -0
  118. package/src/server/responses/policy-fallback.ts +16 -2
  119. package/src/server/responses/terminal-guard.ts +1 -1
  120. package/src/server/responses/upstream-error.ts +5 -0
  121. package/src/server/responses.ts +17 -2
  122. package/src/server/system-env.ts +1 -1
  123. package/src/types.ts +84 -5
  124. package/src/update/index.ts +6 -5
  125. package/src/update/job.ts +5 -6
  126. package/src/update/notify.ts +5 -3
  127. package/src/usage/log.ts +11 -1
  128. package/src/web-search/loop.ts +21 -5
  129. package/gui/dist/assets/index-CQ7bIKee.css +0 -1
  130. package/gui/dist/assets/index-D_JUZLEC.js +0 -76
@@ -14,8 +14,9 @@ import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/
14
14
  import { existsSync } from "node:fs";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { createAdapterEventQueue } from "../adapters/run-turn-queue";
17
- import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuationState, OcxRequestOptions, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
17
+ import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuationState, OcxProviderOpaqueToolCallMetadata, OcxRequestOptions, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
18
18
  import { namespacedToolName, toolChoiceToolPredicate } from "../types";
19
+ import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
19
20
  import type { AttemptRecoveryKind } from "../usage/log";
20
21
  import { bridgeToResponsesSSE } from "../bridge";
21
22
  import { clearableDeadline, idleDeadline } from "../lib/abort";
@@ -93,6 +94,11 @@ interface ImageCall {
93
94
  id: string;
94
95
  name: string;
95
96
  args: string;
97
+ /**
98
+ * Provider-opaque metadata from the originating part (issue #1735). Stored PER CALL: a
99
+ * signature belongs to one specific part, so parallel calls must not share one value.
100
+ */
101
+ providerMetadata?: OcxProviderOpaqueToolCallMetadata;
96
102
  }
97
103
 
98
104
  /**
@@ -109,13 +115,13 @@ function scanEventsForImageCall(events: AdapterEvent[], toolNames: Set<string>):
109
115
  const calls: ImageCall[] = [];
110
116
  const passthrough: AdapterEvent[] = [];
111
117
  let hasRealToolCall = false;
112
- let pending: { name: string; id: string; argsBuf: string; events: AdapterEvent[] } | null = null;
118
+ let pending: { name: string; id: string; argsBuf: string; events: AdapterEvent[]; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
113
119
  const flushPending = (): void => {
114
120
  if (!pending) return;
115
121
  if (toolNames.has(pending.name)) {
116
122
  // Unterminated image call still carries buffered args — fulfill so malformed JSON
117
123
  // becomes a normal tool_result error instead of silently vanishing.
118
- calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf });
124
+ calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf, providerMetadata: pending.providerMetadata });
119
125
  } else {
120
126
  passthrough.push(...pending.events);
121
127
  hasRealToolCall = true;
@@ -125,14 +131,14 @@ function scanEventsForImageCall(events: AdapterEvent[], toolNames: Set<string>):
125
131
  for (const e of events) {
126
132
  if (e.type === "tool_call_start") {
127
133
  flushPending();
128
- pending = { name: e.name, id: e.id, argsBuf: "", events: [e] };
134
+ pending = { name: e.name, id: e.id, argsBuf: "", events: [e], providerMetadata: e.providerMetadata };
129
135
  } else if (e.type === "tool_call_delta" && pending) {
130
136
  pending.argsBuf += e.arguments;
131
137
  pending.events.push(e);
132
138
  } else if (e.type === "tool_call_end" && pending) {
133
139
  pending.events.push(e);
134
140
  if (toolNames.has(pending.name)) {
135
- calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf });
141
+ calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf, providerMetadata: pending.providerMetadata });
136
142
  } else {
137
143
  passthrough.push(...pending.events);
138
144
  hasRealToolCall = true;
@@ -247,6 +253,8 @@ export interface ImageBridgeDeps {
247
253
  stallTimeoutSec?: number;
248
254
  /** Provider-specific fetch (e.g. xAI transport wrapper). Falls back to global fetch. */
249
255
  fetchImpl?: typeof globalThis.fetch;
256
+ /** Reserve the routed provider's next request-start slot before each adapter dispatch. */
257
+ waitForRequestSlot?: (signal?: AbortSignal) => Promise<void>;
250
258
  /** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
251
259
  onUsage?: (usage: OcxUsage | undefined) => void;
252
260
  /**
@@ -380,6 +388,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
380
388
  // expose buildRequest/fetchResponse/parseStream to the bridge, so collect their events through
381
389
  // an AdapterEventQueue and wrap them in a pseudo-response whose parseStream replays them.
382
390
  if (adapter.runTurn) {
391
+ await deps.waitForRequestSlot?.(signal);
383
392
  const queue = createAdapterEventQueue({
384
393
  onBacklogExceeded: () => internalAbort.abort("runTurn backlog exceeded"),
385
394
  });
@@ -457,6 +466,11 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
457
466
  }
458
467
 
459
468
  let headerDeadline = clearableDeadline(connectTimeoutMs, signal);
469
+ const paceThenResetHeaderDeadline = async (): Promise<void> => {
470
+ headerDeadline.clear();
471
+ await deps.waitForRequestSlot?.(signal);
472
+ headerDeadline = clearableDeadline(connectTimeoutMs, signal);
473
+ };
460
474
  try {
461
475
  /**
462
476
  * Build and fetch one image-bridge iteration on the given adapter, under the iteration
@@ -487,6 +501,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
487
501
  let response: Response;
488
502
  try {
489
503
  if (requestAdapter.fetchResponse) {
504
+ await paceThenResetHeaderDeadline();
490
505
  deps.onAttemptSend?.(recovery);
491
506
  response = await requestAdapter.fetchResponse(request, {
492
507
  abortSignal: headerDeadline.signal,
@@ -496,7 +511,8 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
496
511
  });
497
512
  } else {
498
513
  response = await fetchWithResetRetry(
499
- (retryRecovery) => {
514
+ async (retryRecovery) => {
515
+ await paceThenResetHeaderDeadline();
500
516
  // Record every helper-driven send (the callback runs for the first attempt and
501
517
  // each connection-reset replay); preserve the caller's recovery kind
502
518
  // (rate-limit-429 / key-429) when the retry layer supplies none.
@@ -861,6 +877,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
861
877
  id: call.id,
862
878
  name: call.name,
863
879
  arguments: args,
880
+ // Clone per call: parallel media calls each keep their own signature.
881
+ ...(cloneProviderOpaqueToolCallMetadata(call.providerMetadata)
882
+ ? { providerMetadata: cloneProviderOpaqueToolCallMetadata(call.providerMetadata) }
883
+ : {}),
864
884
  })),
865
885
  ],
866
886
  timestamp: now,
@@ -20,6 +20,8 @@ import {
20
20
  hermesHomeDir,
21
21
  kimiConfigPath,
22
22
  kimiHomeDir,
23
+ mcodeConfigPath,
24
+ mcodeHomeDir,
23
25
  ompAgentDir,
24
26
  ompModelsConfigPath,
25
27
  opencodeGlobalConfigPath,
@@ -112,6 +114,11 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
112
114
  sourcePreservingYaml: { path: ["llm-pi-ai", "providers", "opencodex"] },
113
115
  writerLock: { suffix: ".lock" },
114
116
  },
117
+ mcode: {
118
+ id: "mcode",
119
+ configPath: (env = process.env, home = homedir()) => mcodeConfigPath(env, home),
120
+ detectDir: (env = process.env, home = homedir()) => mcodeHomeDir(env, home),
121
+ },
115
122
  };
116
123
 
117
124
  export const INTEGRATION_CLIENT_IDS: readonly IntegrationClientId[] =
@@ -1,5 +1,40 @@
1
1
  /** RFC 8785 JSON Canonicalization Scheme (JCS) for deterministic equality. */
2
2
 
3
+ function assertValidUnicodeScalarString(value: string): void {
4
+ for (let index = 0; index < value.length; index += 1) {
5
+ const code = value.charCodeAt(index);
6
+ if (code >= 0xd800 && code <= 0xdbff) {
7
+ const next = value.charCodeAt(index + 1);
8
+ if (!(next >= 0xdc00 && next <= 0xdfff)) {
9
+ throw new TypeError("jcsStringify: lone UTF-16 surrogate is not valid Unicode");
10
+ }
11
+ index += 1;
12
+ continue;
13
+ }
14
+ if (code >= 0xdc00 && code <= 0xdfff) {
15
+ throw new TypeError("jcsStringify: lone UTF-16 surrogate is not valid Unicode");
16
+ }
17
+ }
18
+ }
19
+
20
+ function stringifyJcsString(value: string): string {
21
+ assertValidUnicodeScalarString(value);
22
+ return JSON.stringify(value);
23
+ }
24
+
25
+ function assertDenseJsonArray(value: readonly unknown[]): void {
26
+ for (let index = 0; index < value.length; index += 1) {
27
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
28
+ throw new TypeError("jcsStringify: sparse arrays / array holes are not representable in JCS");
29
+ }
30
+ }
31
+ for (const key of Object.keys(value)) {
32
+ if (!/^(?:0|[1-9]\d*)$/.test(key) || Number(key) >= value.length) {
33
+ throw new TypeError("jcsStringify: arrays with extra enumerable properties are not representable in JCS");
34
+ }
35
+ }
36
+ }
37
+
3
38
  export function jcsStringify(value: unknown): string {
4
39
  if (value === undefined) throw new TypeError("jcsStringify: undefined is not representable in JCS");
5
40
  if (value === null || typeof value === "boolean") return JSON.stringify(value);
@@ -7,14 +42,19 @@ export function jcsStringify(value: unknown): string {
7
42
  if (!Number.isFinite(value)) throw new TypeError("jcsStringify: non-finite numbers are not representable in JCS");
8
43
  return JSON.stringify(value);
9
44
  }
10
- if (typeof value === "string") return JSON.stringify(value);
45
+ if (typeof value === "string") return stringifyJcsString(value);
11
46
  if (Array.isArray(value)) {
47
+ assertDenseJsonArray(value);
12
48
  return `[${value.map(jcsStringify).join(",")}]`;
13
49
  }
14
50
  if (typeof value === "object") {
51
+ const prototype = Object.getPrototypeOf(value);
52
+ if (prototype !== Object.prototype && prototype !== null) {
53
+ throw new TypeError("jcsStringify: only plain JSON objects are representable in JCS");
54
+ }
15
55
  const obj = value as Record<string, unknown>;
16
56
  const keys = Object.keys(obj).sort();
17
- return `{${keys.map((k) => `${JSON.stringify(k)}:${jcsStringify(obj[k])}`).join(",")}}`;
57
+ return `{${keys.map((key) => `${stringifyJcsString(key)}:${jcsStringify(obj[key])}`).join(",")}}`;
18
58
  }
19
59
  throw new TypeError(`jcsStringify: unsupported value type ${typeof value}`);
20
60
  }
@@ -50,11 +50,15 @@ export const NEGATIVE_CONTROL_FIXTURES: Array<{
50
50
  },
51
51
  {
52
52
  id: "negative.tool-result-order",
53
- defect: "invalid tool-result ordering",
53
+ defect: "invalid tool-result correlation after chat-history repair",
54
54
  mutate: (c) => ({
55
55
  ...c,
56
56
  id: "negative.tool-result-order",
57
- assertions: [{ id: "result", operator: "tool_result_correlates", selector: "/upstream/requests", expected: { call: "/client/response/toolCalls/0/id", result: "/upstream/requests/1/json/messages/1/tool_call_id" }, required: true }],
57
+ // Chat history hardening closes the unmatched call with a synthetic result, then
58
+ // re-emits the orphan result behind a synthetic assistant call. Inspect the actual
59
+ // supplied result at the end of that repaired pair rather than the synthetic close,
60
+ // otherwise the negative control accidentally validates the repair and passes.
61
+ assertions: [{ id: "result", operator: "tool_result_correlates", selector: "/upstream/requests", expected: { call: "/client/response/toolCalls/0/id", result: "/upstream/requests/1/json/messages/3/tool_call_id" }, required: true }],
58
62
  fixture: {
59
63
  ...c.fixture,
60
64
  bytesUtf8: JSON.stringify({
@@ -1,7 +1,7 @@
1
1
  import { discoverScenarios, loadCaseAuthority } from "./manifest";
2
2
  import { runScenario } from "./executor";
3
- import { buildNegativeControls } from "./negative-controls";
4
- import type { ScenarioRunResult } from "./types";
3
+ import { baseCaseForNegativeControl, buildNegativeControls } from "./negative-controls";
4
+ import type { CaseRecord, ScenarioRunResult } from "./types";
5
5
  import { CL01_SUITES } from "./types";
6
6
 
7
7
  export interface ConformanceRunSummary {
@@ -19,6 +19,8 @@ export interface NegativeControlRunSummary extends ConformanceRunSummary {
19
19
  rejected: number;
20
20
  }
21
21
 
22
+ type ScenarioRunner = (caseRecord: CaseRecord) => Promise<ScenarioRunResult>;
23
+
22
24
  export async function runConformanceSuite(
23
25
  suites: readonly string[] = CL01_SUITES,
24
26
  ): Promise<ConformanceRunSummary> {
@@ -33,15 +35,24 @@ export async function runConformanceSuite(
33
35
  return { total: results.length, passed, failed: results.length - passed, results };
34
36
  }
35
37
 
36
- export async function runNegativeControls(): Promise<NegativeControlRunSummary> {
38
+ export async function runNegativeControls(
39
+ execute: ScenarioRunner = runScenario,
40
+ ): Promise<NegativeControlRunSummary> {
37
41
  const authority = loadCaseAuthority();
38
42
  const scenarios = buildNegativeControls(discoverScenarios(authority));
39
43
  if (scenarios.length === 0) throw new Error("harness_failure: no negative controls discovered");
40
44
  const results: ScenarioRunResult[] = [];
41
45
  for (const scenario of scenarios) {
42
- results.push(await runScenario(scenario));
46
+ const baseCase = baseCaseForNegativeControl(scenario.id, authority.cases);
47
+ const executionScenario = baseCase ? { ...scenario, id: baseCase.id } : scenario;
48
+ const result = await execute(executionScenario);
49
+ results.push({ ...result, scenarioId: scenario.id });
43
50
  }
44
- const rejected = results.filter((r) => !r.passed).length;
51
+ const rejected = results.filter((r) => (
52
+ !r.passed
53
+ && r.classification === "protocol_failure"
54
+ && r.secondaryCode === "deterministic_assertion"
55
+ )).length;
45
56
  return {
46
57
  total: results.length,
47
58
  passed: rejected,
@@ -8,11 +8,11 @@ import {
8
8
  OUTCOMES,
9
9
  } from "../constants";
10
10
  import { FAILURE_CLASSIFICATIONS } from "../conformance/types";
11
- import { fixtureDigest, isSha256Hex, jcsStringify } from "../digest";
11
+ import { fixtureDigest, isSha256Hex, jcsStringify, subjectIdForSubject } from "../digest";
12
12
  import type { ObservationEvent, RouteSubjectV1, TaskSubjectV1 } from "../events/types";
13
13
  import { LabValidationError } from "../events/errors";
14
14
  import { assignEventId, validateSubject } from "../events/validate";
15
- import { appendLabEventIfAbsent } from "../ledger/store";
15
+ import { withLedgerMutation } from "../ledger/store";
16
16
  import { ensureLabDirs } from "../paths";
17
17
  import {
18
18
  FABRIC_EVIDENCE_LAYER,
@@ -172,6 +172,11 @@ function validateFabricVerifier(raw: Record<string, unknown>): void {
172
172
 
173
173
  /** Validate non-negative usage counters on a producer outcome. */
174
174
  function validateFabricUsage(raw: Record<string, unknown>): void {
175
+ for (const key of Object.keys(raw)) {
176
+ if (!(USAGE_KEYS as readonly string[]).includes(key)) {
177
+ throw new FabricTaskError(`unknown usage field ${key}`, "malformed_producer_outcome", "harness");
178
+ }
179
+ }
175
180
  for (const key of USAGE_KEYS) {
176
181
  assertNonNegativeIntegerField(raw, key);
177
182
  }
@@ -179,6 +184,11 @@ function validateFabricUsage(raw: Record<string, unknown>): void {
179
184
 
180
185
  /** Validate non-negative limit fields on a producer outcome. */
181
186
  function validateFabricLimits(raw: Record<string, unknown>): void {
187
+ for (const key of Object.keys(raw)) {
188
+ if (!(LIMIT_KEYS as readonly string[]).includes(key)) {
189
+ throw new FabricTaskError(`unknown limit field ${key}`, "malformed_producer_outcome", "harness");
190
+ }
191
+ }
182
192
  for (const key of LIMIT_KEYS) {
183
193
  assertNonNegativeIntegerField(raw, key);
184
194
  }
@@ -262,12 +272,18 @@ export function assertFabricOutcomeV1(raw: unknown): FabricTaskOutcomeV1 {
262
272
  } catch (error) {
263
273
  wrapValidationError(error);
264
274
  }
275
+ if (jcsStringify(taskSubjectObj) !== jcsStringify(taskSubject)) {
276
+ throw new FabricTaskError("taskSubject contains undeclared fields", "malformed_producer_outcome", "harness");
277
+ }
278
+ if (jcsStringify(routeSubjectObj) !== jcsStringify(routeSubject)) {
279
+ throw new FabricTaskError("routeSubject contains undeclared fields", "malformed_producer_outcome", "harness");
280
+ }
265
281
  if (!routeSubjectsMatch(routeSubject, taskSubject.routeSubject)) {
266
282
  throw new FabricTaskError("contradictory route subjects", "layer_subject_mismatch", "harness");
267
283
  }
268
284
 
269
- assertStringField(obj, "taskClassId");
270
- assertStringField(obj, "taskClassVersion");
285
+ const taskClassId = assertStringField(obj, "taskClassId");
286
+ const taskClassVersion = assertStringField(obj, "taskClassVersion");
271
287
  const subjectId = assertStringField(obj, "subjectId");
272
288
  if (!isSha256Hex(subjectId)) {
273
289
  throw new FabricTaskError("malformed producer outcome: subjectId", "malformed_producer_outcome", "harness");
@@ -277,16 +293,36 @@ export function assertFabricOutcomeV1(raw: unknown): FabricTaskOutcomeV1 {
277
293
  if (!isSha256Hex(taskFixtureDigest) || !isSha256Hex(verifierManifestDigest)) {
278
294
  throw new FabricTaskError("malformed producer outcome: digest field", "malformed_producer_outcome", "harness");
279
295
  }
280
- assertStringField(obj, "fabricCompatibilityVersion");
296
+ const fabricCompatibilityVersion = assertStringField(obj, "fabricCompatibilityVersion");
281
297
  const sandboxProfileDigest = assertStringField(obj, "sandboxProfileDigest");
282
298
  if (!isSha256Hex(sandboxProfileDigest)) {
283
299
  throw new FabricTaskError("malformed producer outcome: sandboxProfileDigest", "malformed_producer_outcome", "harness");
284
300
  }
285
- assertIntegerField(obj, "startedAt");
286
- assertIntegerField(obj, "completedAt");
301
+ if (subjectId !== subjectIdForSubject(taskSubject)) {
302
+ throw new FabricTaskError("subjectId does not match taskSubject", "malformed_producer_outcome", "harness");
303
+ }
304
+ if (
305
+ taskClassId !== taskSubject.taskClassId
306
+ || taskClassVersion !== taskSubject.taskClassVersion
307
+ || taskFixtureDigest !== taskSubject.taskFixtureDigest
308
+ || verifierManifestDigest !== taskSubject.verifierManifestDigest
309
+ || fabricCompatibilityVersion !== taskSubject.fabricCompatibilityVersion
310
+ || sandboxProfileDigest !== taskSubject.sandboxProfileDigest
311
+ ) {
312
+ throw new FabricTaskError("task identity fields do not match taskSubject", "malformed_producer_outcome", "harness");
313
+ }
314
+ const startedAt = assertNonNegativeIntegerField(obj, "startedAt");
315
+ const completedAt = assertNonNegativeIntegerField(obj, "completedAt");
316
+ if (completedAt < startedAt) {
317
+ throw new FabricTaskError("invalid execution timestamps", "malformed_producer_outcome", "harness");
318
+ }
287
319
  validateFabricLimits(assertPlainObject(obj.limits, "limits"));
288
320
  validateFabricUsage(assertPlainObject(obj.usage, "usage"));
289
- validateFabricVerifier(assertPlainObject(obj.verifier, "verifier"));
321
+ const verifier = assertPlainObject(obj.verifier, "verifier");
322
+ validateFabricVerifier(verifier);
323
+ if (verifier.manifestDigest !== verifierManifestDigest) {
324
+ throw new FabricTaskError("verifier manifest digest does not match outcome", "malformed_producer_outcome", "harness");
325
+ }
290
326
  if (!OUTCOMES.includes(obj.outcome as typeof OUTCOMES[number])) {
291
327
  throw new FabricTaskError("malformed producer outcome: outcome", "malformed_producer_outcome", "harness");
292
328
  }
@@ -324,9 +360,6 @@ export function observationFromFabricOutcome(
324
360
  if (routeSubject.subjectKind !== "route") {
325
361
  throw new FabricTaskError("nested route subject required", "layer_subject_mismatch", "harness");
326
362
  }
327
- if (!Number.isInteger(outcome.startedAt) || !Number.isInteger(outcome.completedAt) || outcome.completedAt < outcome.startedAt) {
328
- throw new FabricTaskError("invalid execution timestamps", "malformed_producer_outcome", "harness");
329
- }
330
363
 
331
364
  const paths = ensureLabDirs(opts.configDir);
332
365
  const ownsStore = !opts.artifactStore;
@@ -430,9 +463,11 @@ function persistFabricOutcome(
430
463
  const ownsStore = !opts.artifactStore;
431
464
  const store = opts.artifactStore ?? createArtifactStore(paths.artifactsDir);
432
465
  try {
433
- const { event } = observationFromFabricOutcome(outcome, { ...opts, artifactStore: store });
434
- appendLabEventIfAbsent(paths.ledgerPath, event);
435
- return { event, ledgerPath: paths.ledgerPath };
466
+ return withLedgerMutation(paths.ledgerPath, (ledger) => {
467
+ const { event } = observationFromFabricOutcome(outcome, { ...opts, artifactStore: store });
468
+ ledger.appendIfAbsent(event);
469
+ return { event, ledgerPath: paths.ledgerPath };
470
+ });
436
471
  } finally {
437
472
  if (ownsStore) store.close();
438
473
  }
package/src/lab/index.ts CHANGED
@@ -36,3 +36,19 @@ export * from "./subject/installation-salt";
36
36
  export { CL03_LIVE_SUITES } from "./conformance/types";
37
37
  export * from "./query";
38
38
  export * from "./automation";
39
+ export * from "./public/types";
40
+ export {
41
+ exportLocalPublicEvidence,
42
+ importCommunityEvidenceFile,
43
+ importCommunityEvidenceValue,
44
+ listCommunityEvidenceContext,
45
+ previewLocalPublicEvidence,
46
+ summarizePublicEvidenceVerification,
47
+ verifyPublicEvidenceFile,
48
+ type LocalPublicExportV1,
49
+ type LocalPublicPreviewV1,
50
+ type PublicOperatorExclusionReason,
51
+ type PublicOperatorExclusionV1,
52
+ type PublicVerificationSummaryV1,
53
+ } from "./public/operator";
54
+ export { PublicEvidenceValidationError } from "./public/validate";