@aiwg/cli 2026.10.1 → 2026.10.3

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 (166) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +210 -4
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +75 -27
  38. package/dist/src/decision/driver.js +3 -1
  39. package/dist/src/decision/entry.js +38 -6
  40. package/dist/src/decision/heldout/collector.js +10 -1
  41. package/dist/src/decision/index.js +1 -0
  42. package/dist/src/decision/interop.js +60 -8
  43. package/dist/src/decision/ledger.js +839 -0
  44. package/dist/src/decision/validate.js +6 -3
  45. package/dist/src/effects/dispatch-verifier.js +252 -0
  46. package/dist/src/effects/index.js +1 -0
  47. package/dist/src/features/installer.js +2 -2
  48. package/dist/src/flow/graph-metadata.js +5 -0
  49. package/dist/src/gates/providers/loader.js +3 -1
  50. package/dist/src/installation/manager-command.mjs +1 -1
  51. package/dist/src/installation/manager.mjs +17 -2
  52. package/dist/src/lint/runner.js +2 -2
  53. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  54. package/dist/src/mcp/cli.mjs +587 -77
  55. package/dist/src/mcp/config-file.mjs +279 -0
  56. package/dist/src/mcp/config-layers.mjs +348 -0
  57. package/dist/src/mcp/credentials.mjs +484 -0
  58. package/dist/src/mcp/grok-build-config.mjs +16 -26
  59. package/dist/src/mcp/helpers.mjs +106 -2
  60. package/dist/src/mcp/omp-config.mjs +50 -29
  61. package/dist/src/mcp/profiles.js +97 -23
  62. package/dist/src/mcp/profiles.mjs +99 -27
  63. package/dist/src/mcp/registry.js +264 -57
  64. package/dist/src/mcp/registry.mjs +198 -45
  65. package/dist/src/mcp/server.mjs +403 -20
  66. package/dist/src/mcp/toml-string.mjs +9 -0
  67. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  68. package/dist/src/mcp/tool-filters.mjs +357 -0
  69. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  70. package/dist/src/mcp/tools/decision.mjs +21 -3
  71. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  72. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  73. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  74. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  75. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  76. package/dist/src/memory/project-registry.js +2 -2
  77. package/dist/src/mission-protocol/codecs.js +639 -1
  78. package/dist/src/mission-protocol/types.js +110 -0
  79. package/dist/src/mission-protocol/worker-status.js +2 -0
  80. package/dist/src/models/config-loader.js +70 -13
  81. package/dist/src/models/grok-build-models.js +93 -41
  82. package/dist/src/models/model-capabilities.v1.json +3091 -24
  83. package/dist/src/models/model-discovery.js +862 -63
  84. package/dist/src/models/provider-policy.js +625 -11
  85. package/dist/src/models/qualification-digest.mjs +17 -0
  86. package/dist/src/models/resolver.js +259 -0
  87. package/dist/src/models/worker-direct-readiness.js +529 -0
  88. package/dist/src/models/worker-dispatch-offer.js +169 -0
  89. package/dist/src/models/worker-health-readiness.js +131 -0
  90. package/dist/src/models/worker-installation.mjs +879 -0
  91. package/dist/src/models/wrapper-route.js +18 -0
  92. package/dist/src/ops/registry.js +9 -14
  93. package/dist/src/plugins/standalone-packager.js +4 -3
  94. package/dist/src/process/bounded-child-process.mjs +387 -0
  95. package/dist/src/process/cli-command.mjs +75 -0
  96. package/dist/src/providers/capability-matrix.js +8 -0
  97. package/dist/src/providers/capability-matrix.yaml +270 -0
  98. package/dist/src/providers/grok-build-headless.js +43 -21
  99. package/dist/src/providers/provider-definitions.js +9 -1
  100. package/dist/src/providers/provider-definitions.mjs +9 -2
  101. package/dist/src/serve/executor-registry.js +16 -24
  102. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  103. package/dist/src/serve/mission-conductor.js +9 -0
  104. package/dist/src/serve/shared-host-scheduler.js +2 -0
  105. package/dist/src/serve/worker-invocation.js +538 -0
  106. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  107. package/dist/src/sessions/adapter-registry.js +45 -0
  108. package/dist/src/sessions/adapters/generic.js +37 -3
  109. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  110. package/dist/src/sessions/adapters/grokbot.js +423 -24
  111. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  112. package/dist/src/sessions/adapters/muse.js +68 -29
  113. package/dist/src/sessions/adapters/pi.js +1 -0
  114. package/dist/src/sessions/batch-contracts.js +13 -1
  115. package/dist/src/sessions/batch-import.js +23 -42
  116. package/dist/src/sessions/candidates.js +47 -11
  117. package/dist/src/sessions/contracts.js +35 -0
  118. package/dist/src/sessions/coverage-preflight.js +134 -0
  119. package/dist/src/sessions/discovery.js +3 -0
  120. package/dist/src/sessions/grok-build-cli.js +67 -5
  121. package/dist/src/sessions/importer.js +24 -8
  122. package/dist/src/sessions/index.js +2 -0
  123. package/dist/src/sessions/repository.js +54 -13
  124. package/dist/src/sessions/workspace-discovery.js +99 -37
  125. package/dist/src/skills/adapters/clawhub.js +10 -8
  126. package/dist/src/skills/adapters/openclaw.js +10 -7
  127. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  128. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  129. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  130. package/dist/src/smiths/mcpsmith/example.js +1 -1
  131. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  132. package/dist/src/storage/backends/logseq.js +68 -7
  133. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  134. package/dist/src/testing/generators/test-code-generator.js +57 -30
  135. package/dist/src/uhp/client.js +11 -1
  136. package/dist/src/uhp/mission.js +5 -0
  137. package/dist/src/update/checker.mjs +5 -5
  138. package/dist/src/update/notifier.mjs +54 -2
  139. package/dist/src/writing/fidelity.js +18 -1
  140. package/package.json +18 -1
  141. package/schemas/mission-v1.schema.json +2429 -0
  142. package/schemas/models/model-catalog.v1.schema.json +301 -0
  143. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  144. package/schemas/models/model-policy.v1.schema.json +14 -0
  145. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  146. package/tools/_resolve-impl.mjs +39 -0
  147. package/tools/agents/deploy-agents.mjs +15 -2
  148. package/tools/agents/providers/base.mjs +49 -32
  149. package/tools/agents/providers/grokbot.mjs +11 -12
  150. package/tools/agents/providers/hermes.mjs +6 -3
  151. package/tools/agents/providers/omp.mjs +5 -2
  152. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  153. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  154. package/tools/providers/omp-transport.mjs +192 -0
  155. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  156. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  157. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  158. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  159. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  160. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  161. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  162. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  163. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  164. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  165. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  166. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -2,7 +2,6 @@ import { DecisionValidationError, validateDecisionValue, validateDistribution }
2
2
  import { canonicalJson } from '../../security/artifact-trust.js';
3
3
  import { partitionProjectedState } from '../projection.js';
4
4
  import { isTraceparent } from '../telemetry/context.js';
5
- import { runInNewContext } from 'node:vm';
6
5
  import { lookup } from 'node:dns/promises';
7
6
  import { isIP } from 'node:net';
8
7
  import { request as httpsRequest } from 'node:https';
@@ -12,7 +11,7 @@ export const JEV_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
12
11
  const MAX_HEADER_BYTES = 32 * 1024;
13
12
  const MAX_BODY_BYTES = 1024 * 1024;
14
13
  const MAX_REQUEST_ID_LENGTH = 128;
15
- const MAX_PARSE_MS = 50;
14
+ const MAX_PARSE_DEPTH = 64;
16
15
  /** Safe resolver taxonomy; messages are intentionally discarded at this boundary. */
17
16
  export class JevCredentialError extends Error {
18
17
  category;
@@ -45,6 +44,7 @@ export class JevDecisionAdapter {
45
44
  this.region = options.region?.trim() ? options.region : null;
46
45
  this.observeResponseBody = options.observeResponseBody;
47
46
  }
47
+ /** Used only for non-2xx error bodies, where there is no separate successful parse to stay consistent with. */
48
48
  observeResponse(body) {
49
49
  if (!this.observeResponseBody)
50
50
  return;
@@ -61,6 +61,30 @@ export class JevDecisionAdapter {
61
61
  this.observeResponseBody(body, { actualModel: typeof parsed.model === 'string' && parsed.model ? parsed.model : null,
62
62
  usage });
63
63
  }
64
+ /**
65
+ * Derives the observer's model/usage from a value `parseBoundedJson` has already produced (or `undefined`
66
+ * on a failed parse) -- never from a second, independent parse of the same bytes. `parseBoundedJson` runs
67
+ * inside a bounded-time VM context (`MAX_PARSE_MS`); calling it twice on identical bytes let one call
68
+ * succeed while the other spuriously hit that wall-clock bound under host contention, so the reported
69
+ * metadata could disagree with the actual parsed answer even though both described one response.
70
+ */
71
+ observationMetadataOf(parsed) {
72
+ let record = {};
73
+ try {
74
+ record = asRecord(parsed);
75
+ }
76
+ catch { /* A non-object (or absent) top-level value has no model; usage stays unknown. */ }
77
+ let usage = emptyUsage();
78
+ try {
79
+ usage = normalizeUsage(record.usage);
80
+ }
81
+ catch { /* Missing usage is unknown, including on auth errors. */ }
82
+ return { actualModel: typeof record.model === 'string' && record.model ? record.model : null, usage };
83
+ }
84
+ /** Observes exactly once, from the single parse attempt `parsed` already represents (successful or not). */
85
+ observeParsed(body, parsed) {
86
+ this.observeResponseBody?.(body, this.observationMetadataOf(parsed));
87
+ }
64
88
  async capabilities() {
65
89
  return {
66
90
  answerKinds: ['choice', 'ordinal-score', 'truth-probability'],
@@ -191,9 +215,7 @@ export class JevDecisionAdapter {
191
215
  try {
192
216
  parsedError = parseBoundedJson(errorBody);
193
217
  }
194
- catch (error) {
195
- if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT')
196
- throw error;
218
+ catch {
197
219
  parsedError = null;
198
220
  }
199
221
  if (parsedError && typeof parsedError === 'object' && !Array.isArray(parsedError)) {
@@ -217,8 +239,15 @@ export class JevDecisionAdapter {
217
239
  let parsed;
218
240
  try {
219
241
  const encoded = await readBoundedBody(response, signal);
220
- this.observeResponse(encoded);
221
- parsed = parseBoundedJson(encoded);
242
+ // One parse attempt; the observer reads its outcome (success or failure) before answer validation,
243
+ // so an invalid answer still gets model/usage checked, and a parse failure is reported as unknown
244
+ // to both the observer and the main path -- never divergently.
245
+ try {
246
+ parsed = parseBoundedJson(encoded);
247
+ }
248
+ finally {
249
+ this.observeParsed(encoded, parsed);
250
+ }
222
251
  if (request.signal.aborted)
223
252
  return externalInterruption(request, 'terminal-response', metadata);
224
253
  if (deadline.aborted)
@@ -334,8 +363,14 @@ export class JevDecisionAdapter {
334
363
  }
335
364
  try {
336
365
  const encoded = await readBoundedBody(response, signal);
337
- this.observeResponse(encoded);
338
- const parsed = parseBoundedJson(encoded);
366
+ // One parse attempt; see the single-request `evaluate()` for why observation never re-parses.
367
+ let parsed;
368
+ try {
369
+ parsed = parseBoundedJson(encoded);
370
+ }
371
+ finally {
372
+ this.observeParsed(encoded, parsed);
373
+ }
339
374
  const normalized = normalizeBatchResponse(requests, parsed).map(answer => ({
340
375
  questionId: answer.questionId,
341
376
  observation: { ...answer.observation, ...metadata },
@@ -574,19 +609,21 @@ function safeRequestId(value) {
574
609
  return typeof value === 'string' && value.length > 0 && value.length <= MAX_REQUEST_ID_LENGTH
575
610
  && /^[\x21-\x7e]+$/.test(value) && !value.includes(',') ? value : null;
576
611
  }
577
- function parseBoundedJson(text) {
578
- rejectDuplicateAnswerKeys(text);
579
- return structuredClone(runInNewContext('JSON.parse(input)', { input: text }, { timeout: MAX_PARSE_MS }));
580
- }
581
- /** Detect duplicate keys in the response's answer map before JSON.parse erases them. */
582
- function rejectDuplicateAnswerKeys(text) {
583
- const marker = /"answers"\s*:\s*\{/g.exec(text);
584
- if (!marker)
585
- return;
586
- let index = marker.index + marker[0].length;
587
- let depth = 1;
588
- const keys = new Set();
589
- while (index < text.length && depth > 0) {
612
+ /** @internal Exported from this module for direct admission and CPU-cost evidence, not the public barrel. */
613
+ export function parseBoundedJson(text) {
614
+ // Bound work, not elapsed time: a VM timeout includes host scheduling delay.
615
+ // The stream reader also enforces this bound, before UTF-8 decoding.
616
+ if (text.length > MAX_BODY_BYTES || Buffer.byteLength(text, 'utf8') > MAX_BODY_BYTES) {
617
+ throw new DecisionValidationError('Jev response too large');
618
+ }
619
+ // Scan iteratively before allocation by JSON.parse. Total key text/count is
620
+ // bounded by body bytes; no recursive walk or suffix copying is needed.
621
+ // String/key spans are disjoint; skip the whitespace consumed by lookahead.
622
+ // Each code unit is visited once. Key slicing/decoding/hashing sum to
623
+ // O(body bytes), as does native parsing.
624
+ const containers = [];
625
+ let index = 0;
626
+ while (index < text.length) {
590
627
  const char = text[index];
591
628
  if (char === '"') {
592
629
  const start = index++;
@@ -600,20 +637,31 @@ function rejectDuplicateAnswerKeys(text) {
600
637
  else if (next === '"')
601
638
  break;
602
639
  }
603
- if (depth === 1 && /^\s*:/.test(text.slice(index))) {
640
+ let next = index;
641
+ while (next < text.length && /[\x20\t\r\n]/.test(text[next]))
642
+ next += 1;
643
+ const keys = containers.at(-1);
644
+ if (keys && text[next] === ':') {
604
645
  const key = JSON.parse(text.slice(start, index));
605
646
  if (keys.has(key))
606
- throw new DecisionValidationError('duplicate Jev answer key');
647
+ throw new DecisionValidationError('duplicate Jev response key');
607
648
  keys.add(key);
608
649
  }
650
+ index = next;
609
651
  continue;
610
652
  }
611
- if (char === '{' || char === '[')
612
- depth += 1;
653
+ if (char === '{' || char === '[') {
654
+ if (containers.length >= MAX_PARSE_DEPTH)
655
+ throw new DecisionValidationError('Jev response nesting too deep');
656
+ containers.push(char === '{' ? new Set() : null);
657
+ }
613
658
  else if (char === '}' || char === ']')
614
- depth -= 1;
659
+ containers.pop();
615
660
  index += 1;
616
661
  }
662
+ // Native parsing checks the complete JSON grammar, with no reviver or code
663
+ // execution. Input bytes and nesting bound its work and allocated graph.
664
+ return JSON.parse(text);
617
665
  }
618
666
  function headersWithinLimit(headers) {
619
667
  let bytes = 0;
@@ -3,8 +3,10 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { askDecision, setupJev, DecisionAskUsageError, readDecisionContextFile, validateDecisionAskInput, } from './ask.js';
6
+ import { recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, } from './ledger.js';
7
+ import { resolveActiveProvider } from '../cli/provider-resolution.js';
6
8
  import { assertDecisionEvaluateDispatcherConfig, decisionPatternPacks, getDecisionPatternPack, governedPatternArtifacts, listDecisionPatterns, patternInputSchema, patternOutputSchema, planLiveDecisionPattern, runOfflineDecisionPattern, validateAgainstSchema, validateDecisionDocument, validateDefinition, validateRuleset, } from './index.js';
7
- export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, };
9
+ export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, resolveActiveProvider, };
8
10
  const DECISION_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
9
11
  const VALIDATION_TARGETS = ['request', 'definition', 'ruleset', 'binding'];
10
12
  const OFFLINE_ADAPTER = 'agentic/code/addons/decision-engine/examples/fixture-jev-adapter.mjs';
@@ -16,10 +16,42 @@ export class EntryAdmissionError extends Error {
16
16
  this.name = 'EntryAdmissionError';
17
17
  }
18
18
  }
19
+ /**
20
+ * Starts an admission-budget clock, feature-detecting
21
+ * `process.threadCpuUsage()` once for this reading. `elapsedAdmissionMs`
22
+ * below reads the *type* of what this returns rather than re-detecting the
23
+ * feature, so one admission check always uses one clock end to end.
24
+ *
25
+ * Per-thread CPU time measures only the work the admitting call itself did
26
+ * on this thread, so it is immune both to host scheduling contention
27
+ * pausing this thread between reads (the root cause of #3047's false
28
+ * `time-budget` rejections of legitimate input under host load) and to
29
+ * other threads' or background GC's CPU time landing on this same process
30
+ * (which a process-wide `process.cpuUsage()` clock would still charge here
31
+ * -- not a safe substitute for the same reason `performance.now()` was not).
32
+ *
33
+ * `process.threadCpuUsage()` was added in Node v22.19.0 (per the installed
34
+ * `@types/node` `@since` annotation) and is absent on this project's
35
+ * `engines.node` floor (`>=20.0.0` in package.json) and on Node
36
+ * 22.0.0-22.18.x. On a runtime without it, this falls back to the
37
+ * pre-#3047 monotonic wall-clock guard as an explicitly documented
38
+ * compatibility fallback: it restores the original host-load sensitivity on
39
+ * those older runtimes, it does not resolve it there.
40
+ */
41
+ export function admissionClockStart() {
42
+ return typeof process.threadCpuUsage === 'function' ? process.threadCpuUsage() : performance.now();
43
+ }
44
+ /** Elapsed time since `start` (from `admissionClockStart`), in milliseconds. */
45
+ export function elapsedAdmissionMs(start) {
46
+ if (typeof start === 'number')
47
+ return performance.now() - start;
48
+ const diff = process.threadCpuUsage(start);
49
+ return (diff.user + diff.system) / 1000;
50
+ }
19
51
  /** Check the object graph before JSON serialization, schema validation or digesting. */
20
52
  export function admitEntry(value, limits = DEFAULT_ENTRY_LIMITS) {
21
53
  const counts = { entries: 0, properties: 0, bytes: 0, estimatedMemoryBytes: 0 };
22
- const started = performance.now();
54
+ const started = admissionClockStart();
23
55
  const encoder = new TextEncoder();
24
56
  const stack = new Set();
25
57
  const fail = (reason) => { throw new EntryAdmissionError(reason, counts); };
@@ -32,7 +64,7 @@ export function admitEntry(value, limits = DEFAULT_ENTRY_LIMITS) {
32
64
  fail('memory-budget');
33
65
  };
34
66
  const visit = (item, depth) => {
35
- if (performance.now() - started > limits.timeMs)
67
+ if (elapsedAdmissionMs(started) > limits.timeMs)
36
68
  fail('time-budget');
37
69
  if (++counts.entries > limits.entries)
38
70
  fail('entry-count');
@@ -107,7 +139,7 @@ export function parseDecisionJson(source, limits = DEFAULT_ENTRY_LIMITS) {
107
139
  throw new EntryAdmissionError('serialized-bytes', { bytes });
108
140
  if (bytes * 4 > limits.memoryBytes)
109
141
  throw new EntryAdmissionError('memory-budget', { bytes });
110
- const started = performance.now();
142
+ const started = admissionClockStart();
111
143
  let value;
112
144
  try {
113
145
  value = JSON.parse(source);
@@ -118,7 +150,7 @@ export function parseDecisionJson(source, limits = DEFAULT_ENTRY_LIMITS) {
118
150
  const document = parseDocument(source, { uniqueKeys: true });
119
151
  if (document.errors.length)
120
152
  throw new EntryAdmissionError('duplicate-or-invalid-key', { bytes });
121
- if (performance.now() - started > limits.timeMs)
153
+ if (elapsedAdmissionMs(started) > limits.timeMs)
122
154
  throw new EntryAdmissionError('time-budget', { bytes });
123
155
  admitEntry(value, limits);
124
156
  return value;
@@ -130,7 +162,7 @@ export function parseDecisionYaml(source, limits = DEFAULT_ENTRY_LIMITS) {
130
162
  throw new EntryAdmissionError('serialized-bytes', { bytes });
131
163
  if (bytes * 4 > limits.memoryBytes)
132
164
  throw new EntryAdmissionError('memory-budget', { bytes });
133
- const started = performance.now();
165
+ const started = admissionClockStart();
134
166
  const document = parseDocument(source, { uniqueKeys: true });
135
167
  if (document.errors.length)
136
168
  throw new EntryAdmissionError('duplicate-or-invalid-key', { bytes });
@@ -160,7 +192,7 @@ export function parseDecisionYaml(source, limits = DEFAULT_ENTRY_LIMITS) {
160
192
  }
161
193
  };
162
194
  inspect(document.contents, 0);
163
- if (performance.now() - started > limits.timeMs)
195
+ if (elapsedAdmissionMs(started) > limits.timeMs)
164
196
  throw new EntryAdmissionError('time-budget', { bytes, entries });
165
197
  const value = document.toJS();
166
198
  admitEntry(value, limits);
@@ -250,6 +250,12 @@ export async function collectHeldoutStudy(options) {
250
250
  // No raw provider exceptions, model IDs or request IDs cross this boundary when a secret matches.
251
251
  const raw = observation;
252
252
  const metadata = wireMetadata;
253
+ // `metadata` is `jev.ts`'s observer, reading model/usage off the exact same single parse that
254
+ // produced `raw` (never a second, independent parse of the same bytes -- see jev.ts `observeParsed`).
255
+ // The two can therefore no longer disagree on a successful response; preferring `metadata` here
256
+ // only matters when `raw` is a failure (e.g. an otherwise-parseable but invalid answer), so the
257
+ // model/usage bound checks below still see the wire-observed values jev.ts's `failure()` does not
258
+ // carry.
253
259
  const observed = raw && metadata ? { ...raw, actualModel: metadata.actualModel, usage: metadata.usage } : raw;
254
260
  const completed = receipt;
255
261
  const captures = QUALIFICATION_PRIVACY_SURFACES.map(surface => ({ surface,
@@ -275,7 +281,10 @@ export async function collectHeldoutStudy(options) {
275
281
  else if (observed?.usage.costUsd !== null && observed?.usage.costUsd !== undefined && observed.usage.costUsd * 1_000_000 > reserveMicros)
276
282
  outcome = 'price-bound';
277
283
  else if (observed?.status === 'success') {
278
- if (observed.actualModel !== a.servedModel)
284
+ // A null model is unknown, not a mismatch: it must complete neither as 'ok' nor as 'served-model'.
285
+ if (observed.actualModel === null)
286
+ outcome = 'model-unknown';
287
+ else if (observed.actualModel !== a.servedModel)
279
288
  outcome = 'served-model';
280
289
  else if (observed.usage.inputTokens === null || observed.usage.outputTokens === null)
281
290
  outcome = 'usage-unknown';
@@ -54,6 +54,7 @@ export * from './issue-triage/index.js';
54
54
  export * from './routing/index.js';
55
55
  export * from './interop.js';
56
56
  export * from './ask.js';
57
+ export * from './ledger.js';
57
58
  export * from './jev-credentials.js';
58
59
  export { collectHeldoutStudy, scoreHeldoutStudy } from './heldout/collector.js';
59
60
  export { heldoutDigest, heldoutExecutionDigest, planHeldoutCollection, validateHeldoutBundle, heldoutRowsInScope } from './heldout/contract.js';
@@ -38,7 +38,8 @@ const FORBIDDEN_CONTROL_FIELDS = new Set([
38
38
  'calibration', 'executor', 'backend', 'acceptance', 'artifactPin', 'actionAuthorization',
39
39
  ]);
40
40
  const SENSITIVE_LOG_KEY = /(?:raw|body|secret|token|credential|private|locator|state|authorization|password|key)/iu;
41
- const SENSITIVE_LOG_VALUE = /(?:bearer\s+\S+|secret|token|file:\/\/|\/home\/|private|[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})/iu;
41
+ const SENSITIVE_LOG_VALUE = /(?:bearer\s+\S+|secret|token|file:\/\/|\/home\/|private)/iu;
42
+ const MAX_LOG_VALUE_LENGTH = 65_536;
42
43
  const SUPPORTED_DMN_ELEMENTS = new Set(['definitions', 'decision', 'informationRequirement', 'requiredDecision', 'decisionTable', 'input', 'inputExpression', 'output', 'rule', 'inputEntry', 'outputEntry', 'text']);
43
44
  export function decisionInteropDefaultBounds(overrides = {}) {
44
45
  return { ...DEFAULT_BOUNDS, ...overrides };
@@ -580,7 +581,11 @@ function sanitizeJson(value, path, redaction) {
580
581
  redaction.removed.push(`${path}.${key}`);
581
582
  continue;
582
583
  }
583
- result[key] = sanitizeJson(child, `${path}.${key}`, redaction);
584
+ // Keep ordinary objects while preserving __proto__ as an own JSON data key.
585
+ Object.defineProperty(result, key, {
586
+ value: sanitizeJson(child, `${path}.${key}`, redaction),
587
+ enumerable: true, writable: true, configurable: true,
588
+ });
584
589
  }
585
590
  return result;
586
591
  }
@@ -595,10 +600,16 @@ function projectOpaInput(input, allowlist, redaction) {
595
600
  for (const pointer of allowlist) {
596
601
  if (!pointer.startsWith('/'))
597
602
  throw new DecisionInteropError('opa-input-projection', 'OPA input projection allowlist entries must be JSON Pointers');
603
+ const path = pointer.slice(1).split('/').map(part => part.replace(/~1/gu, '/').replace(/~0/gu, '~'));
604
+ // Only __proto__ is forbidden in pointers; constructor/prototype are valid data keys.
605
+ // Own-property traversal and data-property writes keep ordinary projection objects safe.
606
+ if (path.some(segment => segment === '__proto__')) {
607
+ throw new DecisionInteropError('opa-input-projection', 'OPA input projection cannot contain __proto__ segments');
608
+ }
598
609
  const resolved = resolveJsonPointer(input, pointer);
599
610
  if (!resolved.found)
600
611
  continue;
601
- setProjectedValue(output, pointer.slice(1).split('/').map(part => part.replace(/~1/gu, '/').replace(/~0/gu, '~')), sanitizeJson(resolved.value, `$.input${pointer}`, redaction));
612
+ setProjectedValue(output, path, sanitizeJson(resolved.value, `$.input${pointer}`, redaction));
602
613
  }
603
614
  return output;
604
615
  }
@@ -606,17 +617,58 @@ function setProjectedValue(target, path, value) {
606
617
  let cursor = target;
607
618
  path.forEach((segment, index) => {
608
619
  if (index === path.length - 1) {
609
- cursor[segment] = value;
620
+ Object.defineProperty(cursor, segment, {
621
+ value, enumerable: true, writable: true, configurable: true,
622
+ });
610
623
  return;
611
624
  }
612
- const existing = cursor[segment];
613
- if (!existing || typeof existing !== 'object' || Array.isArray(existing))
614
- cursor[segment] = {};
625
+ const existing = Object.hasOwn(cursor, segment) ? cursor[segment] : undefined;
626
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
627
+ Object.defineProperty(cursor, segment, {
628
+ value: {}, enumerable: true, writable: true, configurable: true,
629
+ });
630
+ }
615
631
  cursor = cursor[segment];
616
632
  });
617
633
  }
618
634
  function isSensitiveValue(value) {
619
- return typeof value === 'string' && SENSITIVE_LOG_VALUE.test(value);
635
+ if (typeof value !== 'string')
636
+ return false;
637
+ // Redact oversized values in full so a sensitive suffix cannot escape inspection.
638
+ if (value.length > MAX_LOG_VALUE_LENGTH)
639
+ return true;
640
+ if (SENSITIVE_LOG_VALUE.test(value))
641
+ return true;
642
+ let localLength = 0;
643
+ let domainLength = -1;
644
+ let suffixLength = -1;
645
+ for (const character of value) {
646
+ const letter = /[A-Z]/iu.test(character);
647
+ if (character === '@') {
648
+ domainLength = localLength > 0 ? 0 : -1;
649
+ suffixLength = -1;
650
+ }
651
+ else if (domainLength >= 0) {
652
+ if (/[A-Z0-9.-]/iu.test(character)) {
653
+ if (character === '.')
654
+ suffixLength = domainLength > 0 ? 0 : -1;
655
+ else if (letter && suffixLength >= 0) {
656
+ suffixLength++;
657
+ if (suffixLength >= 2)
658
+ return true;
659
+ }
660
+ else
661
+ suffixLength = -1;
662
+ domainLength++;
663
+ }
664
+ else {
665
+ domainLength = -1;
666
+ suffixLength = -1;
667
+ }
668
+ }
669
+ localLength = /[A-Z0-9._%+-]/iu.test(character) ? localLength + 1 : 0;
670
+ }
671
+ return false;
620
672
  }
621
673
  function assertUnique(values, label) {
622
674
  const seen = new Set();