carthagent 1.0.7 → 1.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/beta-rewards-e2e-plan.md +48 -0
  4. package/docs/beta-rewards-e2e-results.md +84 -0
  5. package/docs/cloud-onboarding-acceptance.md +65 -0
  6. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  7. package/docs/glyph-review/test-isolation-v1.md +26 -0
  8. package/docs/glyph-review/typecheck-base.log +1 -0
  9. package/docs/glyph-review/typecheck-v4.log +1 -0
  10. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  11. package/docs/help-dispatch-review.md +69 -0
  12. package/docs/launch-readiness-live-results.md +65 -0
  13. package/docs/legacy-cli.md +150 -0
  14. package/docs/live-theme-plugin-api.md +18 -0
  15. package/docs/native-cli.md +102 -0
  16. package/docs/native-compatibility.md +16 -0
  17. package/docs/native-daily-quests.md +132 -0
  18. package/docs/native-glyph-pty.md +59 -0
  19. package/docs/native-manual-testing.md +336 -0
  20. package/docs/native-slash-acceptance-handoff.md +49 -0
  21. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  22. package/docs/native-terminal-performance.md +310 -0
  23. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  24. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  25. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  34. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  35. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  36. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  37. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  38. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  39. package/docs/native-terminal-semantics.md +89 -0
  40. package/docs/native-upgrade.md +175 -0
  41. package/docs/opencode-composed-source-review.md +89 -0
  42. package/docs/opencode-migration.md +225 -9
  43. package/docs/opencode-owned-runtime.md +241 -0
  44. package/docs/opencode-runtime.md +12 -0
  45. package/docs/opencode-theme-provider-proof/README.md +55 -0
  46. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  47. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  48. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  49. package/docs/owned-host-controls-proof.md +39 -0
  50. package/docs/owned-runtime-release-manifest.md +42 -0
  51. package/docs/owned-visual-clear-proof.md +40 -0
  52. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  53. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  54. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  55. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  61. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  62. package/docs/release-notes-beta.1.md +60 -0
  63. package/docs/release-notes-beta.2.md +60 -0
  64. package/docs/release-notes-native-draft.md +73 -0
  65. package/docs/release-notes-preview.10.md +121 -0
  66. package/docs/release-notes-preview.11.md +76 -0
  67. package/docs/release-notes-preview.12.md +55 -0
  68. package/docs/release-notes-preview.13.md +68 -0
  69. package/docs/release-notes-preview.9.md +130 -0
  70. package/docs/source-preview.md +120 -0
  71. package/evaluate.mjs +28 -1
  72. package/lib/budget.mjs +1 -1
  73. package/lib/cloud/client.mjs +13 -7
  74. package/lib/codegraph.mjs +1 -1
  75. package/lib/delivery-review.mjs +19 -6
  76. package/lib/delivery.mjs +3 -2
  77. package/lib/environment.mjs +8 -0
  78. package/lib/guidance.mjs +1 -1
  79. package/lib/opencode/cli-runtime.mjs +1188 -0
  80. package/lib/opencode/cli.mjs +253 -0
  81. package/lib/opencode/credentials.mjs +63 -8
  82. package/lib/opencode/demo.mjs +53 -0
  83. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  84. package/lib/opencode/evaluation.mjs +32 -12
  85. package/lib/opencode/headless.mjs +22 -5
  86. package/lib/opencode/provider-policy.mjs +23 -0
  87. package/lib/opencode/review.mjs +37 -16
  88. package/lib/opencode/runtime-artifact.mjs +335 -0
  89. package/lib/opencode/runtime-github-transport.mjs +121 -0
  90. package/lib/opencode/runtime-release.json +36 -0
  91. package/lib/opencode/runtime.mjs +299 -20
  92. package/lib/opencode/service.mjs +77 -35
  93. package/lib/opencode/session-options.mjs +6 -2
  94. package/lib/opencode/update.mjs +222 -0
  95. package/lib/opencode/workspace.mjs +1 -1
  96. package/lib/planning-advice.mjs +2 -2
  97. package/lib/providers/experiential.mjs +2 -2
  98. package/lib/providers/names.mjs +1 -1
  99. package/lib/tui/framing.mjs +1 -80
  100. package/lib/tui/models.mjs +1 -1
  101. package/lib/update.mjs +10 -1
  102. package/package.json +33 -7
  103. package/plugins/carthagent/README.md +23 -0
  104. package/plugins/carthagent/src/bootstrap.ts +1 -1
  105. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  106. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  107. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  108. package/plugins/carthagent/src/index.ts +71 -24
  109. package/plugins/carthagent/src/rpc.ts +51 -6
  110. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  111. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  112. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  113. package/plugins/carthagent/src/server/delivery.ts +227 -112
  114. package/plugins/carthagent/src/server/framing.mjs +80 -0
  115. package/plugins/carthagent/src/server/policy.ts +1 -1
  116. package/plugins/carthagent/src/server/providers.ts +65 -36
  117. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  118. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  119. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  120. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  121. package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
  122. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  123. package/plugins/carthagent/src/testing.ts +5 -0
  124. package/plugins/carthagent/src/tui.tsx +506 -0
  125. package/plugins/carthagent/src/ui/client.ts +127 -0
  126. package/plugins/carthagent/src/ui/commands.ts +70 -0
  127. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  128. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  129. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  130. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  131. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  132. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  133. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  134. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  135. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  136. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  137. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  138. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  139. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  140. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  141. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  142. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  143. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  144. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  145. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  146. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  147. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  148. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  149. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  150. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  151. package/plugins/carthagent/tests/commands.test.ts +107 -0
  152. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  153. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  154. package/plugins/carthagent/themes/carthage.json +67 -0
  155. package/plugins/carthagent/tui.ts +26 -1
  156. package/script/ctg/build-owned-runtime.sh +166 -0
  157. package/tests/budget-engine.test.mjs +4 -3
  158. package/tests/cli.test.mjs +52 -96
  159. package/tests/cloud-client.test.mjs +2 -2
  160. package/tests/daily-evidence.test.mjs +62 -0
  161. package/tests/delivery-review.test.mjs +57 -2
  162. package/tests/delivery.test.mjs +22 -1
  163. package/tests/experiential-extension.test.mjs +2 -2
  164. package/tests/experiential.test.mjs +1 -1
  165. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  166. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  167. package/tests/installed-runtime-contract.test.mjs +43 -0
  168. package/tests/native-cli-host-smoke.mjs +457 -0
  169. package/tests/native-daily-editor-focus.test.mjs +213 -0
  170. package/tests/native-daily-editor.test.mjs +43 -0
  171. package/tests/native-daily-quests-layout.test.mjs +55 -0
  172. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  173. package/tests/native-daily-quests-ui.test.mjs +778 -0
  174. package/tests/native-daily-source.test.mjs +525 -0
  175. package/tests/native-demo-ui.test.mjs +48 -0
  176. package/tests/native-evaluation-admission.test.mjs +24 -0
  177. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  178. package/tests/native-fixture-runtime.test.mjs +111 -0
  179. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  180. package/tests/native-glyph-pty.test.mjs +107 -0
  181. package/tests/native-manual-projects.test.mjs +170 -0
  182. package/tests/native-model-command.test.mjs +67 -0
  183. package/tests/native-owned-runtime-env.test.mjs +140 -0
  184. package/tests/native-paired-guards.test.mjs +464 -0
  185. package/tests/native-provider-policy.test.mjs +33 -0
  186. package/tests/native-pty-invocation.test.mjs +114 -0
  187. package/tests/native-pty-runtime.test.mjs +45 -0
  188. package/tests/native-resume-session.test.mjs +46 -0
  189. package/tests/native-review-consumer.test.mjs +286 -0
  190. package/tests/native-runtime-identity.test.mjs +214 -0
  191. package/tests/native-select-mouse.test.mjs +23 -0
  192. package/tests/native-ship-guidance.test.mjs +16 -0
  193. package/tests/native-ship-parity.test.mjs +526 -0
  194. package/tests/native-ship-plan.test.mjs +85 -0
  195. package/tests/native-ship-rpc.test.mjs +83 -0
  196. package/tests/native-ui-commands.test.mjs +6 -0
  197. package/tests/native-voyages-layout.test.mjs +28 -0
  198. package/tests/native-voyages-navigation.test.mjs +169 -0
  199. package/tests/native-voyages-rpc.test.mjs +397 -0
  200. package/tests/native-voyages-ui.test.mjs +307 -0
  201. package/tests/opencode-atomic-write.test.mjs +55 -0
  202. package/tests/opencode-cli.test.mjs +1312 -0
  203. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  204. package/tests/opencode-delivery.test.mjs +16 -0
  205. package/tests/opencode-providers.test.mjs +248 -20
  206. package/tests/opencode-runtime.test.mjs +597 -8
  207. package/tests/opencode-session-controls.test.mjs +15 -0
  208. package/tests/opencode-ship-rpc.test.mjs +71 -0
  209. package/tests/opencode-update.test.mjs +475 -0
  210. package/tests/opencode-workspace.test.mjs +51 -2
  211. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  212. package/tests/release-workflow.test.mjs +46 -0
  213. package/tests/runtime-artifact.test.mjs +269 -0
  214. package/tests/runtime-development-trust.test.mjs +231 -0
  215. package/tests/runtime-github-transport.test.mjs +132 -0
  216. package/tests/runtime-token-env.test.mjs +23 -0
  217. package/tests/server.test.mjs +3 -1
  218. package/tests/ship-telemetry.test.mjs +1 -1
  219. package/tests/source-preview.test.mjs +253 -0
  220. package/tests/test_native_terminal_probe.py +76 -0
  221. package/tests/test_native_terminal_semantics.py +232 -0
  222. package/tests/tui.test.mjs +2 -2
  223. package/tests/update.test.mjs +42 -5
  224. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  225. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  226. package/tests/workspace-live.test.mjs +127 -0
  227. package/tools/benchmark/diagnose-request.mjs +59 -36
  228. package/tools/benchmark/eval-common.mjs +3 -3
  229. package/tools/benchmark/evaluators.test.mjs +4 -4
  230. package/tools/benchmark/guard.test.mjs +35 -2
  231. package/tools/benchmark/guard.ts +20 -7
  232. package/tools/benchmark/planning-startup.test.mjs +30 -34
  233. package/tools/benchmark/runner.mjs +12 -7
  234. package/tools/benchmark/runner.test.mjs +23 -7
  235. package/tools/benchmark/upstream-error.test.mjs +16 -2
  236. package/tools/generate-owned-runtime-release.mjs +217 -0
  237. package/tools/installed-runtime-contract.mjs +16 -0
  238. package/tools/manual-runtime-manifest.mjs +39 -0
  239. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  240. package/tools/native-fixture-stop-diagnostics.mjs +45 -0
  241. package/tools/native-fixture.mjs +199 -40
  242. package/tools/native-glyph-helpers.py +127 -0
  243. package/tools/native-glyph-pty.py +144 -0
  244. package/tools/native-manual-projects.mjs +259 -0
  245. package/tools/native-manual.mjs +338 -0
  246. package/tools/native-model-picker-acceptance.py +306 -0
  247. package/tools/native-owned-runtime-env.sh +47 -0
  248. package/tools/native-paired-guards.mjs +282 -0
  249. package/tools/native-paired-manifest.mjs +72 -0
  250. package/tools/native-paired-orchestration.sh +51 -0
  251. package/tools/native-plugin-readiness.mjs +33 -0
  252. package/tools/native-plugin-readiness.test.mjs +53 -0
  253. package/tools/native-pty-invocation.mjs +26 -0
  254. package/tools/native-pty-runtime.mjs +83 -0
  255. package/tools/native-pty-smoke.py +147 -24
  256. package/tools/native-public-repo-shell-result.mjs +13 -0
  257. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  258. package/tools/native-runtime-identity.mjs +201 -0
  259. package/tools/native-slash-acceptance-env.py +38 -0
  260. package/tools/native-terminal-semantics.py +882 -0
  261. package/tools/owned-shell-observer/index.ts +16 -0
  262. package/tools/owned-shell-observer/package.json +1 -0
  263. package/tools/owned-visual-clear-env.py +48 -0
  264. package/tools/owned-visual-clear-model.py +21 -0
  265. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  266. package/tools/prepare-native-manual.mjs +122 -0
  267. package/tools/run-native-terminal-paired.sh +403 -0
  268. package/tools/runtime-token-env.mjs +8 -0
  269. package/tools/seed-native-switch-fixture.mjs +79 -0
  270. package/tools/test-native-controls.mjs +3 -1
  271. package/tools/test-native-daily-quests.mjs +65 -0
  272. package/tools/test-native-default-cli.mjs +106 -0
  273. package/tools/test-native-delivery-host.mjs +102 -20
  274. package/tools/test-native-evaluation.mjs +7 -3
  275. package/tools/test-native-glyph-pty.mjs +94 -0
  276. package/tools/test-native-host.mjs +91 -65
  277. package/tools/test-native-outcomes.mjs +3 -1
  278. package/tools/test-native-package.mjs +206 -38
  279. package/tools/test-native-performance.mjs +26 -0
  280. package/tools/test-native-product.mjs +90 -0
  281. package/tools/test-native-pty.mjs +34 -9
  282. package/tools/test-native-public-repo.mjs +224 -0
  283. package/tools/test-native-refresh-reentry.mjs +106 -0
  284. package/tools/test-native-report.mjs +27 -20
  285. package/tools/test-native-review.mjs +75 -0
  286. package/tools/test-native-runtime.mjs +120 -0
  287. package/tools/test-native-ship-models.mjs +176 -0
  288. package/tools/test-native-ship-walkthrough.mjs +96 -0
  289. package/tools/test-native-slash-commands.mjs +125 -0
  290. package/tools/test-native-slash-commands.py +242 -0
  291. package/tools/test-native-swe-host.mjs +224 -55
  292. package/tools/test-native-tasks.mjs +77 -0
  293. package/tools/test-native-terminal-performance.py +887 -0
  294. package/tools/test-native-terminal-readiness.sh +85 -0
  295. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  296. package/tools/test-native-visual-clear-installed.mjs +48 -0
  297. package/tools/test-owned-host-controls.mjs +238 -0
  298. package/tools/test-owned-opencode-runtime.mjs +35 -0
  299. package/tools/test-owned-visual-clear-env.py +120 -0
  300. package/tools/test-owned-visual-clear.py +373 -0
  301. package/tools/verify-draft-runtime-gate.mjs +138 -0
  302. package/tools/verify-owned-runtime-release.mjs +114 -0
  303. package/tools/verify-release-version.mjs +21 -1
@@ -50,12 +50,12 @@ test('parallel per-session admissions reserve the final slot once, replay IDs id
50
50
  // A duplicate delivery of the admitted call ID is idempotent, including
51
51
  // after a plugin restart which discards in-memory state.
52
52
  const admittedID=state.storage.get(key('session-a')).find(row=>row.id.startsWith('call-')&&row.admitted).id;
53
- await assert.rejects(before({id:admittedID,tool:'read',sessionID:'session-a'}),/budget exhausted/);
53
+ await assert.rejects(before({id:admittedID,tool:'read',sessionID:'session-a'}),/invocation ID was already reserved/);
54
54
  assert.equal(state.storage.get(key('session-a')).filter(row=>row.admitted).length,2);
55
55
  await dispose();
56
56
  const other=harness(state.storage);
57
57
  dispose=await guard(other.ctx);
58
- await assert.rejects(other.hooks.get('tool:execute.before')({id:admittedID,tool:'read',sessionID:'session-a'}),/budget exhausted/);
58
+ await assert.rejects(other.hooks.get('tool:execute.before')({id:admittedID,tool:'read',sessionID:'session-a'}),/invocation ID was already reserved/);
59
59
  await other.hooks.get('tool:execute.before')({id:'neighbor',tool:'read',sessionID:'session-b'});
60
60
  assert.equal(other.storage.get(key('session-b')).filter(row=>row.admitted).length,1);
61
61
  await assert.rejects(other.hooks.get('tool:execute.before')({id:'new-after-restart',tool:'read',sessionID:'session-a'}),/budget exhausted/);
@@ -73,6 +73,39 @@ test('parallel per-session admissions reserve the final slot once, replay IDs id
73
73
  }
74
74
  });
75
75
 
76
+ test('Code Mode same-tool calls sharing an execute ID fail closed as ambiguous, not exhausted',async()=>{
77
+ const temp=mkdtempSync(join(tmpdir(),'benchmark-guard-codemode-'));
78
+ const old={limit:process.env.BENCH_TOOL_LIMIT,log:process.env.BENCH_GUARD_LOG,home:process.env.CTG_OPENCODE_HOME};
79
+ process.env.BENCH_TOOL_LIMIT='3';process.env.BENCH_GUARD_LOG=join(temp,'guard.jsonl');process.env.CTG_OPENCODE_HOME=temp;
80
+ const state=harness();let dispose=await guard(state.ctx);
81
+ try{
82
+ const before=state.hooks.get('tool:execute.before');
83
+ const invoke=(tool,id='execute-1')=>before({id,tool,sessionID:'codemode-session'});
84
+ await invoke('execute');await invoke('carthagent.delivery_plan');
85
+ let nestedExecutions=0;
86
+ const executeNested=async()=>{await invoke('carthagent.delivery_plan');nestedExecutions++;};
87
+ const repeated=await Promise.allSettled([executeNested(),executeNested()]);
88
+ assert.equal(repeated.filter(result=>result.status==='rejected').length,2);
89
+ for(const result of repeated)if(result.status==='rejected'){
90
+ assert.match(result.reason.message,/attribution unavailable/);
91
+ assert.doesNotMatch(result.reason.message,/exhausted/);
92
+ }
93
+ assert.equal(nestedExecutions,0,'ambiguous calls must be rejected at the pre-execution hook boundary');
94
+ assert.equal(state.storage.get(key('codemode-session')).filter(row=>row.admitted).length,2,'ambiguous repeated calls must not consume or appear admitted against the remaining slot');
95
+ assert.equal(state.storage.get(key('codemode-session')).length,2,'ambiguous calls do not create inferred reservations');
96
+ await invoke('carthagent.delivery_check','execute-2');
97
+ assert.deepEqual(state.storage.get(key('codemode-session')).map(row=>({tool:row.tool,admitted:row.admitted})),[
98
+ {tool:'execute',admitted:true},{tool:'carthagent.delivery_plan',admitted:true},{tool:'carthagent.delivery_check',admitted:true},
99
+ ]);
100
+ const log=readFileSync(process.env.BENCH_GUARD_LOG,'utf8').trim().split('\n').map(JSON.parse);
101
+ assert.deepEqual(log.map(row=>row.tool),['execute','carthagent.delivery_plan','carthagent.delivery_check']);
102
+ }finally{
103
+ await dispose();
104
+ for(const[name,value]of Object.entries({BENCH_TOOL_LIMIT:old.limit,BENCH_GUARD_LOG:old.log,CTG_OPENCODE_HOME:old.home}))if(value===undefined)delete process.env[name];else process.env[name]=value;
105
+ rmSync(temp,{recursive:true,force:true});
106
+ }
107
+ });
108
+
76
109
  test('failed durable reservation is fail-closed and a cancelled queued call consumes no slot',async()=>{
77
110
  const temp=mkdtempSync(join(tmpdir(),'benchmark-guard-fail-'));
78
111
  const old={limit:process.env.BENCH_TOOL_LIMIT,log:process.env.BENCH_GUARD_LOG,home:process.env.CTG_OPENCODE_HOME};
@@ -92,17 +92,30 @@ export default async function guard(ctx: any): Promise<() => Promise<void>> {
92
92
  }));
93
93
  await register(ctx.tool.hook('execute.before', async (event: any) => {
94
94
  let blocked = false;
95
+ let rejectionReason: string | undefined;
95
96
  let attempts = 0;
96
97
  await serializeSession(event.sessionID, async () => {
97
98
  if (disposed || !cancellationEventsAvailable || cancelledSessions.has(event.sessionID)) throw Error('Benchmark tool admission cancelled before reservation.');
98
- const currentCallKey = callKey(event.sessionID, event.id);
99
+ const currentCallKey = callKey(event.sessionID, `${event.id}:${event.tool}`);
99
100
  const previous = await ctx.storage.get(guardLedger(event.sessionID));
100
101
  const rows = Array.isArray(previous) ? previous : [];
101
- const existing = rows.find(row => row && typeof row === 'object' && row.id === event.id);
102
+ // OpenCode Code Mode dispatches nested namespace tools using the parent
103
+ // execute call ID. Include the actual tool name so the outer execute
104
+ // reservation cannot collide with its nested Carthagent invocation.
105
+ const reservationKey = event.tool === 'execute' ? event.id : `${event.id}:${event.tool}`;
106
+ const existing = rows.find(row => row && typeof row === 'object' && (row.reservationKey === reservationKey || (!row.reservationKey && row.id === event.id && row.tool === event.tool)));
102
107
  if (existing) {
103
108
  if (existing.tool !== event.tool || typeof existing.admitted !== 'boolean') throw Error('Conflicting benchmark tool reservation ID.');
104
109
  attempts = rows.filter(row => row && typeof row === 'object' && row.admitted === true).length;
105
- blocked = true; // Duplicate invocation never executes twice.
110
+ blocked = true;
111
+ // The pinned Code Mode hook contract reuses the parent execute ID
112
+ // for nested calls and provides no distinct invocation ID. A second
113
+ // call to the same nested tool is therefore ambiguous, not proof of
114
+ // a replay and not evidence that the budget is exhausted.
115
+ const parentExecute = rows.some(row => row && typeof row === 'object' && row.id === event.id && row.tool === 'execute');
116
+ rejectionReason = parentExecute && event.tool !== 'execute'
117
+ ? 'Benchmark tool invocation attribution unavailable: nested Code Mode calls share the parent execute ID; refusing ambiguous execution.'
118
+ : 'Benchmark tool invocation ID was already reserved; refusing duplicate execution.';
106
119
  } else {
107
120
  attempts = rows.filter(row => row && typeof row === 'object' && row.admitted === true).length;
108
121
  blocked = attempts >= limit;
@@ -110,16 +123,16 @@ export default async function guard(ctx: any): Promise<() => Promise<void>> {
110
123
  // Persist a reservation (or rejection) while holding both the
111
124
  // per-session queue and inter-process file lock. No tool is admitted
112
125
  // until this durable write has succeeded.
113
- await ctx.storage.set(guardLedger(event.sessionID), [...rows, { id:event.id, tool:event.tool, admitted:!blocked, at:Date.now() }]);
126
+ await ctx.storage.set(guardLedger(event.sessionID), [...rows, { id:event.id, tool:event.tool, reservationKey, admitted:!blocked, at:Date.now() }]);
114
127
  attempts += Number(!blocked);
115
128
  appendFileSync(process.env.BENCH_GUARD_LOG!, JSON.stringify({ type:'tool-attempt', sessionID:event.sessionID, callID:event.id, tool:event.tool, attempts, blocked })+'\n');
116
129
  }
117
- if (!existing) admitted.set(currentCallKey, !blocked);
130
+ admitted.set(currentCallKey, !blocked);
118
131
  });
119
- if (blocked) throw Error('Benchmark tool budget exhausted before execution.');
132
+ if (blocked) throw Error(rejectionReason ?? 'Benchmark tool budget exhausted before execution.');
120
133
  }));
121
134
  await register(ctx.tool.hook('execute.after', async (event: any) => {
122
- const currentCallKey=callKey(event.sessionID,event.id);
135
+ const currentCallKey=callKey(event.sessionID,`${event.id}:${event.tool}`);
123
136
  if (admitted.get(currentCallKey) === false) return;
124
137
  if (event.status !== 'completed' || event.tool !== 'delivery_finish') return;
125
138
  const raw = event.result.content.filter((item: any) => item.type === 'text').map((item: any) => item.text).join('\n');
@@ -1,4 +1,4 @@
1
- // Offline scripted provider: exercises actual installed pi plumbing, not model efficacy.
1
+ // Offline scripted provider: exercises the installed native OpenCode engine, not model efficacy.
2
2
  import test from 'node:test';
3
3
  import assert from 'node:assert/strict';
4
4
  import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
@@ -7,41 +7,37 @@ import { join } from 'node:path';
7
7
  import { runCandidate } from './runner.mjs';
8
8
  import { initializeCampaign } from './campaign.mjs';
9
9
  import { MODEL } from './gateway.mjs';
10
- import { fixtureDesign, fixtureReview } from '../../tests/helpers/tested-design.mjs';
11
10
 
12
11
  test('installed engine traverses discovery guide and full delivery with offline scripted responses', async t => {
13
- const root=mkdtempSync(join(tmpdir(),'ctg-startup-engine-'));t.after(()=>rmSync(root,{recursive:true,force:true}));
14
- const campaignPath=join(root,'campaign.json');initializeCampaign(campaignPath,{historicalCandidate:0,historicalSmoke:0});
15
- const requirement='Writes the exact expected file';
16
- const plan={goal:'Implement fixture output',assumptions:[],artifacts:['.'],outputs:['output.txt'],steps:[{id:'build',title:'Write and verify',checks:['output']}],acceptance:[{requirement,checks:['output']}],checks:[{id:'output',kind:'test',argv:[process.execPath,'-e',"require('node:assert/strict').equal(require('node:fs').readFileSync('output.txt','utf8'),'ok')"],timeoutSeconds:5}]};plan.design=fixtureDesign(plan);
17
- const lastTool=payload=>{
18
- const raw=payload.messages.filter(m=>m.role==='tool').at(-1)?.content;
19
- return JSON.parse(typeof raw==='string'?raw:raw.map(b=>b.text||'').join('\n'));
20
- };
21
- const sequence=[...Array.from({length:8},()=>()=>({name:'read',args:{path:'TASK.md'}})),
22
- ()=>({name:'delivery_design',args:{action:'guide'}}),
23
- ()=>({name:'delivery_plan',args:plan}),
24
- ()=>({name:'delivery_design',args:{action:'validate'}}),
25
- ()=>({name:'delivery_design',args:{action:'inspect'}}),
26
- p=>({name:'delivery_design',args:{action:'review',review:{...fixtureReview(plan),captureId:lastTool(p).result.id}}}),
27
- ()=>({name:'delivery_design',args:{action:'approve'}}),
28
- ()=>({name:'write',args:{path:'output.txt',content:'ok'}}),
29
- ()=>({name:'delivery_check',args:{id:'all'}}),
30
- ()=>({name:'delivery_progress',args:{step:'build',status:'done'}}),
31
- ()=>({name:'delivery_review',args:{action:'inspect'}}),
32
- p=>({name:'delivery_review',args:{action:'record',captureId:lastTool(p).id,coverage:[{requirement,assertions:'Exact file content asserted by Node strict equality'}],probes:['Declared output check exited zero'],findings:[],limitations:['Scripted fixture; no semantic review claim']}}),
33
- ()=>({name:'delivery_finish',args:{status:'verified',review:'Reviewed fixture evidence',launch:'n/a',limitations:['Offline scripted provider']}}),
12
+ const root=mkdtempSync(join(tmpdir(),'ctg-startup-engine-'));t.after(()=>{if(process.env.CTG_KEEP_BENCHMARK_FAILURES!=='1')rmSync(root,{recursive:true,force:true});});
13
+ const campaignPath=join(root,'campaign.json');initializeCampaign(campaignPath,{historicalCandidate:0,historicalSmoke:0,smokeLimit:1});
14
+ const requirement='Writes the exact expected file';
15
+ const plan={goal:'Implement fixture output',assumptions:[],artifacts:['.'],outputs:['output.txt'],steps:[{id:'build',title:'Write and verify',checks:['output']}],acceptance:[{requirement,checks:['output']}],checks:[{id:'output',kind:'test',argv:[process.execPath,'-e',"require('node:assert/strict').equal(require('node:fs').readFileSync('output.txt','utf8'),'ok')"],timeoutSeconds:5}]};
16
+ const sequence=[...Array.from({length:8},()=>()=>({name:'read',args:{path:'TASK.md'}})),
17
+ ()=>({name:'delivery_design',args:{action:'guide'}}),
18
+ ()=>({name:'delivery_plan',args:plan}),
19
+ ()=>({name:'shell',args:{command:"printf 'ok' > output.txt"}}),
20
+ ()=>({name:'delivery_check',args:{id:'all'}}),
21
+ ()=>({name:'delivery_progress',args:{step:'build',status:'done'}}),
22
+ ()=>({name:'delivery_review',args:{action:'inspect'}}),
23
+ p=>{const reviewed=JSON.parse(p.messages.filter(message=>message.role==='tool').at(-1).content);return {name:'delivery_review',args:{action:'record',captureId:reviewed.id,coverage:[{requirement,assertions:'Declared Node strict-equality check verified exact output content'}],probes:['Declared output check completed successfully'],findings:[],limitations:['Offline scripted fixture; not model efficacy']}};},
24
+ ()=>({name:'delivery_finish',args:{status:'verified',change:'Completed the scripted fixture task.',review:'Reviewed fixture evidence',launch:'n/a',limitations:['Offline scripted provider']}}),
34
25
  ];
35
- let i=0,sawCheckpoint=false;
36
- const r=await runCandidate('smoke','harness',{smoke:true,key:'dummy',root,campaignPath,promptOverride:'Implement a fixture output.txt containing exactly ok. Use the delivery contract and finish verified.',fetchImpl:async(_url,options)=>{
37
- const payload=JSON.parse(options.body);sawCheckpoint ||= JSON.stringify(payload.messages).includes('Pre-plan checkpoint: 8 discovery');
38
- const call=i<sequence.length?sequence[i++](payload):null;
39
- return new Response('data: '+JSON.stringify({id:'offline',model:MODEL,choices:[{index:0,delta:{role:'assistant',content:call?'':'SMOKE_OK',...(call?{tool_calls:[{index:0,id:'call'+i,type:'function',function:{name:call.name,arguments:JSON.stringify(call.args)}}]}:{})},finish_reason:call?'tool_calls':'stop'}],usage:{cost:0,prompt_tokens:1,completion_tokens:1}})+'\n\ndata: [DONE]\n\n');
40
- }});
41
- assert.equal(r.smokePassed,true);assert.equal(i,sequence.length);assert.ok(sawCheckpoint);
26
+ let i=0,sawCheckpoint=false,advertised=[],requestTrace=[];
27
+ const r=await runCandidate('smoke','harness',{smoke:true,key:'dummy',root,campaignPath,promptOverride:'Implement a fixture output.txt containing exactly ok. Use the delivery contract and finish verified.',fetchImpl:async(_url,options)=>{
28
+ const payload=JSON.parse(options.body);advertised=payload.tools.map(tool=>tool.function?.name).filter(Boolean);requestTrace.push({turn:i,tools:advertised,messages:payload.messages.map(message=>({role:message.role,tool_calls:message.tool_calls?.map(call=>call.function?.name),tool_call_id:message.tool_call_id,content:typeof message.content==='string'?message.content.slice(-120):undefined}))});sawCheckpoint ||= JSON.stringify(payload.messages).includes('Pre-plan checkpoint: 8 discovery');
29
+ const call=i<sequence.length?sequence[i++](payload):null;
30
+ if(call&&call.name!=='read'&&call.name!=='shell'&&call.name!=='execute'){call.args={code:`return await tools.carthagent[${JSON.stringify(call.name)}](${JSON.stringify(call.args)})`};call.name='execute';}
31
+ if(call){const name=advertised.find(candidate=>candidate===call.name||candidate.endsWith(`_${call.name}`)||candidate.endsWith(`.${call.name}`));if(!name)throw Error(`Scripted tool ${call.name} is not advertised: ${advertised.join(', ')}`);call.name=name;}
32
+ return new Response('data: '+JSON.stringify({id:'offline',model:MODEL,choices:[{index:0,delta:{role:'assistant',content:call?'':'SMOKE_OK',...(call?{tool_calls:[{index:0,id:'call'+i,type:'function',function:{name:call.name,arguments:JSON.stringify(call.args)}}]}:{})},finish_reason:call?'tool_calls':'stop'}],usage:{cost:i<=8?0:.001,prompt_tokens:1,completion_tokens:1}})+'\n\ndata: [DONE]\n\n');
33
+ }});
42
34
  const events=readFileSync(join(root,'smoke-harness/events.jsonl'),'utf8').trim().split('\n').map(JSON.parse);
43
- assert.deepEqual(events.filter(e=>e.type==='tool_execution_end'&&e.isError),[]);
44
- assert.equal(readFileSync(join(root,'smoke-harness/workspace/output.txt'),'utf8'),'ok');
45
- const finish=events.findLast(e=>e.type==='tool_execution_end'&&e.toolName==='delivery_finish');
46
- assert.match(JSON.stringify(finish.result),/verified/);
35
+ assert.equal(r.smokePassed,true,JSON.stringify({result:r,providerCalls:i,sawCheckpoint,requestTrace,events}));assert.equal(i,sequence.length);assert.ok(sawCheckpoint);
36
+ const failures=events.filter(e=>e.type==='session.tool.success'&&e.data?.metadata?.error);
37
+ assert.deepEqual(failures,[],JSON.stringify(failures));
38
+ const completedTools=events.flatMap(e=>e.type==='session.tool.success'?(e.data?.metadata?.toolCalls||[]).filter(call=>call.status==='completed'):[ ]);
39
+ assert.ok(completedTools.some(call=>call.tool==='carthagent.delivery_finish'), 'native delivery_finish did not complete');
40
+ assert.equal(readFileSync(join(root,'smoke-harness/workspace/output.txt'),'utf8'),'ok');
41
+ const finish=events.findLast(e=>e.type==='session.tool.success'&&e.data?.metadata?.toolCalls?.some(call=>call.tool==='carthagent.delivery_finish'&&call.status==='completed'));
42
+ assert.match(JSON.stringify(finish.data.content),/verified/);
47
43
  });
@@ -25,10 +25,10 @@ export function childEnv(config, gateway, log) {
25
25
  const home = isolatedHome(config);
26
26
  return safeEnv({ CTG_OPENCODE_HOME: home, CTG_OPENCODE_URL: process.env.CTG_OPENCODE_URL || '', BENCH_GATEWAY: gateway, BENCH_ADVISOR_LOG: log, BENCH_GUARD_LOG: join(dirname(config),'guard.jsonl') }, home);
27
27
  }
28
- async function waitForService(url, { timeoutMs = 30000, signal } = {}) {
28
+ async function waitForService(url, { timeoutMs = 30000, signal, headers = {} } = {}) {
29
29
  const deadline = Date.now() + timeoutMs;
30
30
  while (Date.now() < deadline && !signal?.aborted) {
31
- try { const response = await fetch(`${url}/api/info`, { signal: AbortSignal.timeout(1000) }); if (response.ok) return; } catch {}
31
+ try { const response = await fetch(`${url}/api/info`, { headers, signal: AbortSignal.timeout(1000) }); if (response.ok) return; } catch {}
32
32
  await new Promise(resolve => setTimeout(resolve, 150));
33
33
  }
34
34
  throw Error('Isolated OpenCode 2 service readiness timeout');
@@ -48,9 +48,10 @@ async function startNativeService({ workspace, config, mode, arm, smoke, toolLim
48
48
  const provider = arm === 'plain' ? 'benchmark-fixture' : 'benchmark';
49
49
  const model = arm === 'plain' ? 'fixture' : MODEL;
50
50
  const plugins = [join(REPO,'lib/opencode/evaluation-budget-plugin'), ...(arm === 'plain' ? [] : [join(REPO,'tools/benchmark/native-plugin')])];
51
+ if (smoke && arm !== 'plain') plugins.push({ package:join(REPO,'plugins/carthagent'), options:{deliveryOptions:{workflow:'standard'}} });
51
52
  const configJson = {
52
53
  '$schema':'https://opencode.ai/config.json', update:'disable', model:`${provider}/${model}`,
53
- plugins:plugins.filter((_,index)=>mode!=='smoke' || index===0),
54
+ plugins,
54
55
  providers:{[provider]:{
55
56
  package:'@opencode/ai/providers/openai-compatible',
56
57
  settings:{baseURL:`${gateway}/v1`,apiKey:apiKey || 'fixture-no-key'},
@@ -72,7 +73,7 @@ async function startNativeService({ workspace, config, mode, arm, smoke, toolLim
72
73
  const deadline=Date.now()+15000;
73
74
  while(Date.now()<deadline){
74
75
  const loaded=await listPlugins(service,request).catch(()=>null);
75
- const expected = ['ctg.evaluation-budget', ...(arm === 'plain' ? [] : ['ctg.benchmark'])];
76
+ const expected = ['ctg.evaluation-budget', ...(plugins.includes(join(REPO,'tools/benchmark/native-plugin')) ? ['ctg.benchmark'] : []), ...(plugins.some(plugin => typeof plugin === 'object' && plugin.package === join(REPO,'plugins/carthagent')) ? ['carthagent'] : [])];
76
77
  const active = expected.every(id=>loaded?.some(plugin=>plugin.id===id&&plugin.state?.status==='active'));
77
78
  const failure=loaded?.find(plugin=>expected.includes(plugin.id)&&plugin.state?.status==='failed');
78
79
  if(active) break;
@@ -98,7 +99,11 @@ export async function runCandidate(task, arm, { smoke = false, key, root = ROOT,
98
99
  mkdirSync(root, { recursive: true }); mkdirSync(run); mkdirSync(workspace); mkdirSync(config);
99
100
  if (!Number.isInteger(toolLimit) || toolLimit < 1 || toolLimit > 120) throw Error('Invalid tool limit');
100
101
  const toolsReceipt = provisionTools(config);
101
- const gateway = await startGateway({ key: apiKey || 'fixture-only-no-upstream', limit: smoke ? 0.08 : 2.79, allowJev: arm === 'jev', receiptPath: join(run, 'gateway.jsonl'), fetchImpl, account: campaignAccount(campaignPath, smoke ? 'smoke' : 'candidate', run) });
102
+ // Scripted offline smoke transcripts may need many zero-cost turns. Give only
103
+ // that injected fixture enough reservation headroom; production smoke remains
104
+ // under its normal $0.08 gateway ceiling and native session cost ceiling.
105
+ const gatewayLimit = smoke ? (fetchImpl ? 3.1 : 0.08) : 2.79;
106
+ const gateway = await startGateway({ key: apiKey || 'fixture-only-no-upstream', limit:gatewayLimit, allowJev: arm === 'jev', receiptPath: join(run, 'gateway.jsonl'), fetchImpl, account: campaignAccount(campaignPath, smoke ? 'smoke' : 'candidate', run) });
102
107
  let fixtureService, nativeService;
103
108
  try {
104
109
  const env = childEnv(config, gateway.url, join(run,'advisor.jsonl'));
@@ -112,13 +117,13 @@ export async function runCandidate(task, arm, { smoke = false, key, root = ROOT,
112
117
  else if (!process.env.CTG_OPENCODE_URL) nativeService = await startNativeService({workspace,config,mode:arm,arm,smoke,toolLimit,gateway:gateway.url,apiKey,serviceManager});
113
118
  env.CTG_OPENCODE_URL = process.env.CTG_OPENCODE_URL || nativeService?.endpoint || fixtureService?.url;
114
119
  env.CTG_OPENCODE_AUTHORIZATION = nativeService ? nativeService.auth.authorization : '';
115
- const args = { task, arm, smoke, runtime: ENGINE_RUNTIME, openCodeService: nativeService ? 'dedicated-isolated-service' : process.env.CTG_OPENCODE_URL ? 'explicit-isolated-endpoint' : 'injected-fixture', toolsReceipt, harnessCommit: spawnSync('git',['rev-parse','HEAD'],{cwd:REPO,encoding:'utf8'}).stdout.trim(), ceilings: { seconds: smoke ? 180 : 900, tools:toolLimit, usd:smoke ? .08 : 2.79 }, startedAt:new Date().toISOString() };
120
+ const args = { task, arm, smoke, runtime: ENGINE_RUNTIME, openCodeService: nativeService ? 'dedicated-isolated-service' : process.env.CTG_OPENCODE_URL ? 'explicit-isolated-endpoint' : 'injected-fixture', toolsReceipt, harnessCommit: spawnSync('git',['rev-parse','HEAD'],{cwd:REPO,encoding:'utf8'}).stdout.trim(), ceilings: { seconds: smoke ? 180 : 900, tools:toolLimit, usd:gatewayLimit }, startedAt:new Date().toISOString() };
116
121
  writeFileSync(join(run,'invocation.json'), JSON.stringify(args,null,2));
117
122
  let stopReason = null; const events = [], started = Date.now();
118
123
  const controller = new AbortController();
119
124
  const timer = setTimeout(() => { stopReason = 'wall-time'; controller.abort(); }, (smoke ? 180 : 900) * 1000);
120
125
  let result;
121
- try { await waitForService(env.CTG_OPENCODE_URL,{signal:controller.signal}); result = await nativeRunner({ directory: workspace, prompt, systemPrompt: arm === 'plain' ? '' : `Use the Carthagent delivery workflow. Required checks: ${JSON.stringify({ toolLimit })}.`, provider:arm === 'plain' ? 'benchmark-fixture' : 'benchmark', model:arm === 'plain' ? 'fixture' : MODEL, timeoutSeconds: smoke ? 180 : 900, maxTurns: toolLimit, maxCost: smoke ? .08 : 2.79, signal: controller.signal, baseUrl: env.CTG_OPENCODE_URL, headers:nativeService?.auth || {}, onEvent: event => { events.push(event); appendFileSync(join(run,'events.jsonl'),JSON.stringify(event)+'\n'); } }); }
126
+ try { await waitForService(env.CTG_OPENCODE_URL,{signal:controller.signal,headers:nativeService?.auth || {}}); result = await nativeRunner({ directory: workspace, prompt, systemPrompt: arm === 'plain' ? '' : `Use the Carthagent delivery workflow. Required checks: ${JSON.stringify({ toolLimit })}.`, provider:arm === 'plain' ? 'benchmark-fixture' : 'benchmark', model:arm === 'plain' ? 'fixture' : MODEL, timeoutSeconds: smoke ? 180 : 900, maxTurns: toolLimit, maxCost: smoke ? .08 : 2.79, signal: controller.signal, baseUrl: env.CTG_OPENCODE_URL, headers:nativeService?.auth || {}, onEvent: event => { events.push(event); appendFileSync(join(run,'events.jsonl'),JSON.stringify(event)+'\n'); } }); }
122
127
  catch (error) { result = { exitCode:1, errors:[error.message], turns:0, productiveTurns:0, tools:{} }; }
123
128
  finally { clearTimeout(timer); }
124
129
  if (result.outcome !== 'succeeded') {
@@ -7,7 +7,7 @@ import { initializeCampaign } from './campaign.mjs';
7
7
  import { runCandidate, terminalStatus } from './runner.mjs';
8
8
  import { createFreeze, verifyFreeze, frozenFiles } from './freeze.mjs';
9
9
  import { sha } from './runtime.mjs';
10
- import { nativeFixture } from '../native-fixture.mjs';
10
+ import { nativeFixture, streamText } from '../native-fixture.mjs';
11
11
  const mk = () => mkdtempSync(join(tmpdir(), 'benchmark-runner-'));
12
12
 
13
13
  test('native terminal classification does not mistake process success for model success', () => {
@@ -56,15 +56,19 @@ test('benchmark refuses arbitrary service endpoints before creating a run', asyn
56
56
 
57
57
  test('native benchmark plugin loads in OpenCode 2 with both session and tool hooks', async () => {
58
58
  const root=mk();
59
+ let probeSent=false;
60
+ let markerFile;
59
61
  const host=await nativeFixture({
60
62
  plugin:join(process.cwd(),'lib/opencode/evaluation-budget-plugin'),
61
- plugins:[join(process.cwd(),'tools/benchmark/native-plugin'),join(process.cwd(),'plugins/carthagent')],
63
+ // Keep this admission probe independent of the delivery plugin's plan gate.
64
+ plugins:[join(process.cwd(),'tools/benchmark/native-plugin')],
62
65
  env:{BENCH_TOOL_LIMIT:'1',BENCH_RETRY_LIMIT:'0',BENCH_GUARD_LOG:join(root,'guard.jsonl'),BENCH_ADVISOR_LOG:join(root,'advisor.jsonl'),BENCH_GATEWAY:'http://127.0.0.1:1',CTG_EVALUATION_MAX_TURNS:'4',CTG_EVALUATION_MAX_COST:'1'},
63
66
  respond(body,res) {
64
- if(JSON.stringify(body.messages).includes('CTG_PARALLEL_GUARD_PROBE')) {
65
- const shell=body.tools.find(tool=>tool.function?.name==='shell');
66
- assert.ok(shell,'native host must advertise shell for the concurrent tool admission probe');
67
- const calls=Array.from({length:8},(_,index)=>({index,id:`parallel-${index}`,type:'function',function:{name:'shell',arguments:JSON.stringify({command:`printf parallel-${index}`})}}));
67
+ if(!probeSent && JSON.stringify(body.messages).includes('CTG_PARALLEL_GUARD_PROBE')) {
68
+ probeSent=true;
69
+ const read=body.tools.find(tool=>tool.function?.name==='read');
70
+ assert.ok(read,'native host must advertise read for the concurrent tool admission probe');
71
+ const calls=Array.from({length:8},(_,index)=>({index,id:`parallel-${index}`,type:'function',function:{name:'read',arguments:JSON.stringify({path:markerFile})}}));
68
72
  const chunk=(delta,finish_reason=null)=>`data: ${JSON.stringify({id:'parallel-guard-probe',object:'chat.completion.chunk',created:1,model:'fixture',choices:[{index:0,delta,finish_reason}]})}\n\n`;
69
73
  res.writeHead(200,{'content-type':'text/event-stream'});
70
74
  res.end(`${chunk({role:'assistant',tool_calls:calls})}${chunk({},'tool_calls')}data: [DONE]\n\n`);
@@ -80,13 +84,25 @@ test('native benchmark plugin loads in OpenCode 2 with both session and tool hoo
80
84
  const response=await host.client.session.create({location:host.location,model:host.model,title:'hook initialization probe'},host.request());
81
85
  assert.ok(response.id);
82
86
  const session=await host.client.session.create({location:host.location,model:host.model,title:'parallel admission probe',metadata:{'ctg.evaluation.budget':{maxTurns:4,maxCost:1}}},host.request());
87
+ const marker='CTG_BENCHMARK_ADMITTED_READ_RESULT';
88
+ markerFile=join(host.cwd,'read-admission-proof.txt');
89
+ writeFileSync(markerFile,marker);
83
90
  await host.client.session.prompt({sessionID:session.id,text:'CTG_PARALLEL_GUARD_PROBE'},host.request());
84
91
  await host.client.session.wait({sessionID:session.id},host.request());
85
- assert.ok(existsSync(join(root,'guard.jsonl')),`${host.output}\n${JSON.stringify(host.requests)}`);
92
+ assert.ok(existsSync(join(root,'guard.jsonl')),`${JSON.stringify(host.providerErrors.map(error=>error.stack??String(error)))}\n${JSON.stringify(await host.client.message.list({sessionID:session.id},host.request()))}`);
93
+ assert.deepEqual(host.providerErrors,[],'offline provider requests must complete without fixture errors');
86
94
  const rows=readFileSync(join(root,'guard.jsonl'),'utf8').trim().split('\n').map(JSON.parse).filter(row=>row.sessionID===session.id);
87
95
  assert.equal(rows.length,8,JSON.stringify(rows));
88
96
  assert.equal(rows.filter(row=>!row.blocked).length,1,JSON.stringify(rows));
89
97
  assert.equal(rows.filter(row=>row.blocked).length,7,JSON.stringify(rows));
98
+ const admittedCallID=rows.find(row=>!row.blocked).callID;
99
+ const toolResults=host.requests.flatMap(request=>request.messages).filter(message=>message.role==='tool');
100
+ const admittedResult=toolResults.find(message=>message.tool_call_id===admittedCallID);
101
+ assert.ok(admittedResult?.content?.includes(marker), 'the admitted call must return the unique marker file content');
102
+ for(const row of rows.filter(row=>row.blocked)) {
103
+ const rejectedResult=toolResults.find(message=>message.tool_call_id===row.callID);
104
+ assert.ok(rejectedResult?.content?.toLowerCase().includes('budget'), `blocked call ${row.callID} must return its budget rejection`);
105
+ }
90
106
  const ledger=await host.client.session.context({sessionID:session.id},host.request());
91
107
  assert.ok(ledger.at(-1)?.outcome,JSON.stringify(ledger.at(-1)));
92
108
  } finally { await host.close(); }
@@ -1,10 +1,24 @@
1
1
  import test from 'node:test';import assert from 'node:assert/strict';
2
2
  import {upstreamError}from'./upstream-error.mjs';import{startGateway,MODEL}from'./gateway.mjs';
3
- import{reconstruct,inspectRequest}from'./diagnose-request.mjs';import{existsSync}from'node:fs';
3
+ import{inspectCapturedRequest,inspectRequest}from'./diagnose-request.mjs';import{mkdtempSync,writeFileSync,rmSync}from'node:fs';import{tmpdir}from'node:os';import{join}from'node:path';
4
4
  test('bounded error receipts redact reflected credentials and preserve diagnostic fields',async()=>{
5
5
  const secret='private+key/test';const r=await upstreamError(Response.json({error:{code:400,message:'Invalid tool history: '+secret,metadata:{provider_name:'fixture',raw:'Bearer token '+encodeURIComponent(secret)}}},{status:400,headers:{'x-request-id':'req123'}}),{secrets:[secret]});assert.equal(r.status,400);assert.equal(r.requestId,'req123');assert.equal(r.provider,'fixture');assert.match(r.message,/Invalid tool history/);assert.ok(!JSON.stringify(r).includes(secret));assert.ok(!JSON.stringify(r).includes(encodeURIComponent(secret)));
6
6
  let canceled=false;const over=await upstreamError(new Response(new ReadableStream({start(c){c.enqueue(new TextEncoder().encode('x'.repeat(17000)));},cancel(){canceled=true;}}),{status:400}));assert.equal(canceled,true);assert.equal(over.bodyExcerpt,'[Oversized upstream error omitted]');
7
7
  const html=await upstreamError(new Response('<html>bad request</html>',{status:400}));assert.match(html.bodyExcerpt,/bad request/);
8
8
  });
9
9
  test('gateway records upstream rejection without replay or credential leak',async()=>{let calls=0;const g=await startGateway({key:'reflected-secret',fetchImpl:async()=>{calls++;return Response.json({error:{code:400,message:'Invalid input reflected-secret'}},{status:400});}});try{for(let i=0;i<2;i++){const r=await fetch(g.url+'/v1/chat/completions',{method:'POST',body:JSON.stringify({model:MODEL,messages:[]})});await r.text();assert.equal(r.status,400);}assert.equal(calls,1);assert.match(g.receipts.find(r=>r.type==='http-failure').message,/Invalid input/);assert.ok(!JSON.stringify(g.receipts).includes('reflected-secret'));assert.equal(g.budget.reserved,.15);}finally{await g.close();}});
10
- test('offline reconstruction matches original failed multi-turn wire payload, detects orphan results',async(t)=>{const run='/home/baissi/benchmarks/deepseek-jev-v3/voxel-plain';if(!existsSync(run)){t.skip('Historical session not installed');return;}const {payload,report}=await reconstruct(run);assert.equal(report.hashMatches,true);assert.equal(report.toolCalls,11);assert.equal(report.toolResults,11);assert.deepEqual(report.issues,[]);payload.messages.push({role:'tool',tool_call_id:'missing',content:'x'});assert.ok(inspectRequest(payload).issues.some(s=>s.includes('unmatched')));});
10
+ test('offline native request inspection validates captured OpenAI-compatible payloads without replay',async()=>{
11
+ const dir=mkdtempSync(join(tmpdir(),'ctg-native-request-diagnostic-'));
12
+ try{
13
+ const capture=join(dir,'request.json');
14
+ const payload={model:MODEL,messages:[{role:'assistant',tool_calls:[{id:'read-1',type:'function',function:{name:'read',arguments:JSON.stringify({path:'src/main.rs'})}}]},{role:'tool',tool_call_id:'read-1',content:'ok'}],tools:[{type:'function',function:{name:'read'}}],stream:true,max_tokens:16384,reasoning:{effort:'high'}};
15
+ writeFileSync(capture,JSON.stringify(payload));
16
+ const inspected=await inspectCapturedRequest(capture);
17
+ assert.deepEqual(inspected.report.issues,[]);assert.equal(inspected.report.toolCalls,1);assert.equal(inspected.report.toolResults,1);
18
+ assert.equal(inspected.report.sourceHash,inspected.report.normalizedHash);
19
+ assert.equal(JSON.stringify(inspected.report).includes('.pi/'),false);
20
+ const orphan={...payload,messages:[...payload.messages,{role:'tool',tool_call_id:'missing',content:'x'}]};
21
+ assert.ok(inspectRequest(orphan).issues.some(issue=>issue.includes('unmatched tool result')));
22
+ assert.ok(inspectRequest({...payload,messages:[{role:'assistant',tool_calls:[{id:'bad',function:{arguments:'{'}}]}]}).issues.some(issue=>issue.includes('invalid tool arguments')));
23
+ }finally{rmSync(dir,{recursive:true,force:true});}
24
+ });
@@ -0,0 +1,217 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { link, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { readPinnedOwnedRuntimeInputs } from './pinned-owned-runtime-inputs.mjs';
8
+
9
+ export const REQUIRED_RUNTIME_TARGETS = Object.freeze({
10
+ 'linux-x64': ['linux', 'x64', 'opencode-linux-x64.tar.gz', 'cli-linux-x64/bin/opencode'],
11
+ 'linux-arm64': ['linux', 'arm64', 'opencode-linux-arm64.tar.gz', 'cli-linux-arm64/bin/opencode'],
12
+ 'darwin-x64': ['darwin', 'x64', 'opencode-darwin-x64.tar.gz', 'cli-darwin-x64/bin/opencode'],
13
+ 'darwin-arm64': ['darwin', 'arm64', 'opencode-darwin-arm64.tar.gz', 'cli-darwin-arm64/bin/opencode'],
14
+ 'windows-x64': ['windows', 'x64', 'opencode-windows-x64.tar.gz', 'cli-windows-x64/bin/opencode.exe'],
15
+ });
16
+
17
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
18
+ const [releaseVersion, assetsDirectory, repoRoot = process.cwd()] = process.argv.slice(2);
19
+ if (!releaseVersion || !assetsDirectory) {
20
+ console.error('Usage: node tools/generate-owned-runtime-release.mjs <release-version> <assets-directory> [repo-root]');
21
+ process.exit(2);
22
+ }
23
+ const manifest = await generateOwnedRuntimeRelease({ releaseVersion, assetsDirectory, repoRoot });
24
+ process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
25
+ }
26
+
27
+ /**
28
+ * Verify five build outputs through the production USTAR acquisition parser,
29
+ * then emit a candidate outer trust manifest. Pinned build records must be
30
+ * made by the native build job from its binary before it is archived; records
31
+ * are claims until the acquisition module verifies them against archive bytes.
32
+ */
33
+ export async function generateOwnedRuntimeRelease({
34
+ releaseVersion,
35
+ assetsDirectory,
36
+ repoRoot,
37
+ outputPath = path.join(path.dirname(path.resolve(assetsDirectory)), 'runtime-release.json.candidate'),
38
+ acquireRuntime,
39
+ }) {
40
+ validateReleaseVersion(releaseVersion);
41
+ const directory = path.resolve(assetsDirectory);
42
+ const root = path.resolve(repoRoot ?? process.cwd());
43
+ const provenance = await readPinnedBuildInputs(root);
44
+ const acquireOwnedRuntime = acquireRuntime ?? (await import('../lib/opencode/runtime-artifact.mjs')).acquireOwnedRuntime;
45
+
46
+ const entries = await readdir(directory, { withFileTypes: true });
47
+ if (entries.some((entry) => !entry.isFile())) {
48
+ throw new Error('Assets directory may contain only regular archive and build-record files');
49
+ }
50
+ const files = entries.map((entry) => entry.name).sort();
51
+ const expectedFiles = Object.values(REQUIRED_RUNTIME_TARGETS)
52
+ .flatMap(([, , archiveName]) => [archiveName, `${archiveName}.build.json`])
53
+ .sort();
54
+ if (JSON.stringify(files) !== JSON.stringify(expectedFiles)) {
55
+ throw new Error(`Assets directory must contain exactly the five runtime archives and their build records: ${expectedFiles.join(', ')}`);
56
+ }
57
+
58
+ const cacheRoot = await mkdtemp(path.join(os.tmpdir(), 'ctg-runtime-manifest-'));
59
+ const assets = {};
60
+ try {
61
+ for (const [key, [platform, arch, name, executablePath]] of Object.entries(REQUIRED_RUNTIME_TARGETS)) {
62
+ const archivePath = path.join(directory, name);
63
+ const archiveBytes = await readFile(archivePath);
64
+ if (!archiveBytes.length) throw new Error(`${name} is empty`);
65
+ const archiveSha256 = sha256(archiveBytes);
66
+ const record = JSON.parse(await readFile(path.join(directory, `${name}.build.json`), 'utf8'));
67
+ validateBuildRecord(record, { key, platform, arch, provenance });
68
+ const expectedExecutableSha256 = record.executableSha256.toLowerCase();
69
+
70
+ // The fetch shim exposes only the exact local archive requested by the
71
+ // approved acquisition API. No network request or release upload occurs.
72
+ const fetchImpl = async (url) => {
73
+ const requestedName = decodeURIComponent(new URL(url).pathname.split('/').at(-1) ?? '');
74
+ if (requestedName !== name) return new Response('not found', { status: 404 });
75
+ return new Response(archiveBytes, { status: 200, headers: { 'content-length': String(archiveBytes.length) } });
76
+ };
77
+ const verifiedRoot = await acquireOwnedRuntime({
78
+ releaseVersion,
79
+ platform,
80
+ arch,
81
+ assetName: name,
82
+ baseUrl: 'https://runtime-manifest-fixture.invalid/assets',
83
+ fetchImpl,
84
+ cacheRoot,
85
+ expectedArchiveSha256: archiveSha256,
86
+ expectedExecutableSha256,
87
+ expectedSourceRevision: provenance.sourceRevision,
88
+ expectedPatchSha256: provenance.patchSha256,
89
+ expectedLockSha256: provenance.lockSha256,
90
+ expectedCompatibilityVersion: provenance.compatibilityVersion,
91
+ expectedRuntimeVersion: provenance.runtimeVersion,
92
+ expectedChannel: `ctg.${provenance.compatibilityVersion}`,
93
+ });
94
+ const verifiedExecutable = await readFile(path.join(verifiedRoot, executablePath));
95
+ if (sha256(verifiedExecutable) !== expectedExecutableSha256) {
96
+ throw new Error(`Acquisition returned an unexpected executable for ${key}`);
97
+ }
98
+ await validateRuntimeTree(verifiedRoot, { key, executablePath, runtimeVersion: provenance.runtimeVersion });
99
+ assets[key] = { name, archiveSha256, executableSha256: expectedExecutableSha256 };
100
+ }
101
+ } finally {
102
+ await rm(cacheRoot, { recursive: true, force: true });
103
+ }
104
+
105
+ const manifest = {
106
+ version: 1,
107
+ releaseVersion,
108
+ compatibilityVersion: provenance.compatibilityVersion,
109
+ runtimeVersion: provenance.runtimeVersion,
110
+ sourceRevision: provenance.sourceRevision,
111
+ patchSha256: provenance.patchSha256,
112
+ lockSha256: provenance.lockSha256,
113
+ assets,
114
+ };
115
+ const destination = path.resolve(outputPath);
116
+ await mkdir(path.dirname(destination), { recursive: true });
117
+ const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
118
+ try {
119
+ await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
120
+ // Link is atomic and fails if the candidate already exists; the generator
121
+ // never silently replaces a previously reviewed candidate.
122
+ await link(temporary, destination);
123
+ } finally {
124
+ await rm(temporary, { force: true }).catch(() => {});
125
+ }
126
+ return manifest;
127
+ }
128
+
129
+ function validateReleaseVersion(value) {
130
+ if (typeof value !== 'string' || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.test(value)) {
131
+ throw new TypeError('releaseVersion must be a canonical semantic version without a v prefix');
132
+ }
133
+ }
134
+
135
+ async function readPinnedBuildInputs(repoRoot) {
136
+ const recipePath = path.join(repoRoot, 'script/ctg/build-owned-runtime.sh');
137
+ const recipe = await readFile(recipePath, 'utf8');
138
+ const pinnedInputs = await readPinnedOwnedRuntimeInputs(repoRoot);
139
+ const runtime = recipe.match(/OPENCODE_VERSION=([^\s]+)\s+OPENCODE_CHANNEL=ctg\.([^\s]+)/);
140
+ if (!runtime || runtime[2] !== runtime[1].replace(/^0\.0\.0-ctg\./, '')) {
141
+ throw new Error('Build recipe must pin a matching CTG compatibility/runtime version');
142
+ }
143
+ const provenance = {
144
+ sourceRevision: pinnedInputs.sourceRevision,
145
+ lockSha256: pinnedInputs.lockSha256,
146
+ patchSha256: pinnedInputs.patchSha256,
147
+ compatibilityVersion: runtime[2],
148
+ runtimeVersion: runtime[1],
149
+ };
150
+ if (!/^[a-f0-9]{40}$/i.test(provenance.sourceRevision)) throw new Error('Build recipe source revision is not a commit SHA');
151
+ for (const field of ['lockSha256', 'patchSha256']) {
152
+ if (!/^[a-f0-9]{64}$/.test(provenance[field])) throw new Error(`Build recipe ${field} is not a SHA-256 digest`);
153
+ }
154
+ return provenance;
155
+ }
156
+
157
+ function validateBuildRecord(record, { key, platform, arch, provenance }) {
158
+ if (!record || record.version !== 1 || record.target !== key
159
+ || record.platform !== platform || record.arch !== arch
160
+ || record.sourceRevision !== provenance.sourceRevision
161
+ || record.patchSha256?.toLowerCase() !== provenance.patchSha256
162
+ || record.lockSha256?.toLowerCase() !== provenance.lockSha256
163
+ || record.compatibilityVersion !== provenance.compatibilityVersion
164
+ || record.runtimeVersion !== provenance.runtimeVersion
165
+ || typeof record.executableSha256 !== 'string'
166
+ || !/^[a-f0-9]{64}$/i.test(record.executableSha256)) {
167
+ throw new Error(`Build record for ${key} does not match the pinned recipe and target`);
168
+ }
169
+ }
170
+
171
+ async function validateRuntimeTree(root, { key, executablePath, runtimeVersion }) {
172
+ const expectedDirectories = new Set(['', `cli-${key}`, `cli-${key}/bin`]);
173
+ const expectedFiles = new Set([
174
+ 'LICENSE',
175
+ 'SHA256SUMS',
176
+ 'ctg-runtime.json',
177
+ executablePath,
178
+ `cli-${key}/package.json`,
179
+ ]);
180
+ const actualDirectories = new Set();
181
+ const actualFiles = new Set();
182
+ const visit = async (directory, relative) => {
183
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
184
+ const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
185
+ if (entry.isSymbolicLink()) throw new Error(`Runtime archive contains a symlink: ${childRelative}`);
186
+ if (entry.isDirectory()) {
187
+ actualDirectories.add(childRelative);
188
+ await visit(path.join(directory, entry.name), childRelative);
189
+ } else if (entry.isFile()) actualFiles.add(childRelative);
190
+ else throw new Error(`Runtime archive contains a non-regular member: ${childRelative}`);
191
+ }
192
+ };
193
+ await visit(root, '');
194
+ for (const member of actualDirectories) {
195
+ if (!expectedDirectories.has(member)) throw new Error(`Runtime archive contains an unexpected directory: ${member}`);
196
+ }
197
+ for (const member of actualFiles) {
198
+ if (!expectedFiles.has(member)) throw new Error(`Runtime archive contains an unexpected file: ${member}`);
199
+ }
200
+ for (const member of expectedDirectories) {
201
+ if (member && !actualDirectories.has(member)) throw new Error(`Runtime archive is missing directory: ${member}`);
202
+ }
203
+ for (const member of expectedFiles) {
204
+ if (!actualFiles.has(member)) throw new Error(`Runtime archive is missing file: ${member}`);
205
+ }
206
+ const packageMetadata = JSON.parse(await readFile(path.join(root, `cli-${key}/package.json`), 'utf8'));
207
+ const [, platform, arch] = key.match(/^(linux|darwin|windows)-(x64|arm64)$/) ?? [];
208
+ const packageOs = platform === 'windows' ? 'win32' : platform;
209
+ if (packageMetadata.name !== `@opencode/cli-${key}`
210
+ || packageMetadata.version !== runtimeVersion
211
+ || packageMetadata.os?.length !== 1 || packageMetadata.os[0] !== packageOs
212
+ || packageMetadata.cpu?.length !== 1 || packageMetadata.cpu[0] !== arch) {
213
+ throw new Error(`Runtime archive package metadata does not match ${key} and ${runtimeVersion}`);
214
+ }
215
+ }
216
+
217
+ function sha256(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
@@ -0,0 +1,16 @@
1
+ /** Shared installed-default resolution contract, kept small for offline regression tests. */
2
+ export async function resolveInstalledRuntime(resolveOpenCode, runtimeIdentity, manifestPath) {
3
+ const options = { runtimeReleaseManifestPath: manifestPath };
4
+ const binary = await resolveOpenCode(options);
5
+ const identity = await runtimeIdentity(binary, options);
6
+ return { binary, identity, options };
7
+ }
8
+
9
+ export function assertTerminalSemanticsProof(proof, { binary, identity, manifest }) {
10
+ if (proof.status !== 'PASS') throw new Error('controlled terminal harness did not pass all configured checks');
11
+ if (proof.binaryResolvedPath !== binary) throw new Error('PTY proof did not identify the default-resolved runtime executable');
12
+ if (proof.binarySha256 !== identity.binarySha256) throw new Error('PTY proof executable digest differs from trusted owned identity');
13
+ if (proof.binaryVersion !== `opencode v${manifest.runtimeVersion}`) throw new Error('PTY proof version differs from packaged runtime manifest');
14
+ if (proof.physicalOSClipboardVerified !== false) throw new Error('controlled terminal model cannot claim physical OS clipboard verification');
15
+ if (proof.controlledHostClipboardModel !== true) throw new Error('PTY proof is missing controlled terminal host model scope');
16
+ }