carthagent 1.0.7 → 1.1.0-preview.12

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 (275) hide show
  1. package/README.md +80 -79
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  4. package/docs/glyph-review/test-isolation-v1.md +26 -0
  5. package/docs/glyph-review/typecheck-base.log +1 -0
  6. package/docs/glyph-review/typecheck-v4.log +1 -0
  7. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  8. package/docs/help-dispatch-review.md +69 -0
  9. package/docs/legacy-cli.md +150 -0
  10. package/docs/live-theme-plugin-api.md +18 -0
  11. package/docs/native-cli.md +102 -0
  12. package/docs/native-compatibility.md +16 -0
  13. package/docs/native-daily-quests.md +132 -0
  14. package/docs/native-glyph-pty.md +59 -0
  15. package/docs/native-manual-testing.md +314 -0
  16. package/docs/native-slash-acceptance-handoff.md +49 -0
  17. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  18. package/docs/native-terminal-performance.md +310 -0
  19. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  20. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  21. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  22. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  23. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  24. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  31. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  32. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  33. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  35. package/docs/native-terminal-semantics.md +89 -0
  36. package/docs/native-upgrade.md +177 -0
  37. package/docs/opencode-composed-source-review.md +89 -0
  38. package/docs/opencode-migration.md +225 -9
  39. package/docs/opencode-owned-runtime.md +241 -0
  40. package/docs/opencode-runtime.md +12 -0
  41. package/docs/opencode-theme-provider-proof/README.md +55 -0
  42. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  43. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  44. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  45. package/docs/owned-host-controls-proof.md +39 -0
  46. package/docs/owned-runtime-release-manifest.md +42 -0
  47. package/docs/owned-visual-clear-proof.md +40 -0
  48. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  49. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  50. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  51. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  52. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  53. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  54. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  57. package/docs/release-notes-native-draft.md +73 -0
  58. package/docs/release-notes-preview.10.md +121 -0
  59. package/docs/release-notes-preview.11.md +76 -0
  60. package/docs/release-notes-preview.12.md +55 -0
  61. package/docs/release-notes-preview.9.md +130 -0
  62. package/evaluate.mjs +28 -1
  63. package/lib/budget.mjs +1 -1
  64. package/lib/codegraph.mjs +1 -1
  65. package/lib/delivery.mjs +3 -2
  66. package/lib/environment.mjs +8 -0
  67. package/lib/guidance.mjs +1 -1
  68. package/lib/opencode/cli-runtime.mjs +1171 -0
  69. package/lib/opencode/cli.mjs +252 -0
  70. package/lib/opencode/credentials.mjs +63 -8
  71. package/lib/opencode/demo.mjs +53 -0
  72. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  73. package/lib/opencode/evaluation.mjs +32 -12
  74. package/lib/opencode/headless.mjs +8 -4
  75. package/lib/opencode/provider-policy.mjs +23 -0
  76. package/lib/opencode/review.mjs +37 -16
  77. package/lib/opencode/runtime-artifact.mjs +335 -0
  78. package/lib/opencode/runtime-github-transport.mjs +121 -0
  79. package/lib/opencode/runtime-release.json +36 -0
  80. package/lib/opencode/runtime.mjs +298 -20
  81. package/lib/opencode/service.mjs +77 -35
  82. package/lib/opencode/session-options.mjs +6 -2
  83. package/lib/opencode/update.mjs +222 -0
  84. package/lib/opencode/workspace.mjs +1 -1
  85. package/lib/planning-advice.mjs +2 -2
  86. package/lib/tui/framing.mjs +1 -80
  87. package/lib/update.mjs +10 -1
  88. package/package.json +33 -7
  89. package/plugins/carthagent/README.md +23 -0
  90. package/plugins/carthagent/src/bootstrap.ts +1 -1
  91. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  92. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  93. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  94. package/plugins/carthagent/src/index.ts +71 -24
  95. package/plugins/carthagent/src/rpc.ts +51 -6
  96. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  97. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  98. package/plugins/carthagent/src/server/daily-source-rpc.ts +197 -0
  99. package/plugins/carthagent/src/server/delivery.ts +217 -106
  100. package/plugins/carthagent/src/server/framing.mjs +80 -0
  101. package/plugins/carthagent/src/server/policy.ts +1 -1
  102. package/plugins/carthagent/src/server/providers.ts +58 -30
  103. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  104. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  105. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  106. package/plugins/carthagent/src/server/ship-guidance.ts +39 -0
  107. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  108. package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
  109. package/plugins/carthagent/src/testing.ts +5 -0
  110. package/plugins/carthagent/src/tui.tsx +492 -0
  111. package/plugins/carthagent/src/ui/client.ts +125 -0
  112. package/plugins/carthagent/src/ui/commands.ts +70 -0
  113. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  114. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  115. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  116. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  117. package/plugins/carthagent/src/ui/daily-quests.tsx +232 -0
  118. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  119. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  120. package/plugins/carthagent/src/ui/native-review.d.mts +31 -0
  121. package/plugins/carthagent/src/ui/native-review.mjs +154 -0
  122. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  123. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  124. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  125. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  126. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  127. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  128. package/plugins/carthagent/src/ui/voyages-navigation.tsx +122 -0
  129. package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
  130. package/plugins/carthagent/src/ui/voyages.tsx +127 -0
  131. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  132. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  133. package/plugins/carthagent/src/voyages-rpc.ts +60 -0
  134. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  135. package/plugins/carthagent/tests/commands.test.ts +107 -0
  136. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  137. package/plugins/carthagent/tests/voyages-navigation.test.tsx +258 -0
  138. package/plugins/carthagent/themes/carthage.json +67 -0
  139. package/plugins/carthagent/tui.ts +26 -1
  140. package/script/ctg/build-owned-runtime.sh +166 -0
  141. package/tests/budget-engine.test.mjs +4 -3
  142. package/tests/cli.test.mjs +52 -96
  143. package/tests/daily-evidence.test.mjs +62 -0
  144. package/tests/delivery.test.mjs +22 -1
  145. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  146. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  147. package/tests/installed-runtime-contract.test.mjs +43 -0
  148. package/tests/native-cli-host-smoke.mjs +457 -0
  149. package/tests/native-daily-editor-focus.test.mjs +213 -0
  150. package/tests/native-daily-editor.test.mjs +43 -0
  151. package/tests/native-daily-quests-layout.test.mjs +53 -0
  152. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  153. package/tests/native-daily-quests-ui.test.mjs +778 -0
  154. package/tests/native-daily-source.test.mjs +426 -0
  155. package/tests/native-demo-ui.test.mjs +48 -0
  156. package/tests/native-evaluation-admission.test.mjs +24 -0
  157. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  158. package/tests/native-fixture-runtime.test.mjs +111 -0
  159. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  160. package/tests/native-glyph-pty.test.mjs +107 -0
  161. package/tests/native-manual-projects.test.mjs +170 -0
  162. package/tests/native-owned-runtime-env.test.mjs +140 -0
  163. package/tests/native-paired-guards.test.mjs +464 -0
  164. package/tests/native-provider-policy.test.mjs +31 -0
  165. package/tests/native-pty-invocation.test.mjs +114 -0
  166. package/tests/native-pty-runtime.test.mjs +45 -0
  167. package/tests/native-resume-session.test.mjs +46 -0
  168. package/tests/native-review-consumer.test.mjs +267 -0
  169. package/tests/native-runtime-identity.test.mjs +214 -0
  170. package/tests/native-ship-parity.test.mjs +526 -0
  171. package/tests/native-ship-plan.test.mjs +75 -0
  172. package/tests/native-ship-rpc.test.mjs +71 -0
  173. package/tests/native-ui-commands.test.mjs +6 -0
  174. package/tests/native-voyages-layout.test.mjs +28 -0
  175. package/tests/native-voyages-navigation.test.mjs +169 -0
  176. package/tests/native-voyages-rpc.test.mjs +395 -0
  177. package/tests/native-voyages-ui.test.mjs +277 -0
  178. package/tests/opencode-atomic-write.test.mjs +55 -0
  179. package/tests/opencode-cli.test.mjs +1312 -0
  180. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  181. package/tests/opencode-providers.test.mjs +245 -18
  182. package/tests/opencode-runtime.test.mjs +546 -8
  183. package/tests/opencode-session-controls.test.mjs +15 -0
  184. package/tests/opencode-ship-rpc.test.mjs +71 -0
  185. package/tests/opencode-update.test.mjs +475 -0
  186. package/tests/opencode-workspace.test.mjs +51 -2
  187. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  188. package/tests/release-workflow.test.mjs +46 -0
  189. package/tests/runtime-artifact.test.mjs +269 -0
  190. package/tests/runtime-development-trust.test.mjs +230 -0
  191. package/tests/runtime-github-transport.test.mjs +132 -0
  192. package/tests/runtime-token-env.test.mjs +23 -0
  193. package/tests/test_native_terminal_probe.py +76 -0
  194. package/tests/test_native_terminal_semantics.py +232 -0
  195. package/tests/update.test.mjs +36 -0
  196. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  197. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  198. package/tests/workspace-live.test.mjs +127 -0
  199. package/tools/benchmark/diagnose-request.mjs +59 -36
  200. package/tools/benchmark/eval-common.mjs +3 -3
  201. package/tools/benchmark/evaluators.test.mjs +4 -4
  202. package/tools/benchmark/guard.test.mjs +35 -2
  203. package/tools/benchmark/guard.ts +20 -7
  204. package/tools/benchmark/planning-startup.test.mjs +30 -34
  205. package/tools/benchmark/runner.mjs +12 -7
  206. package/tools/benchmark/runner.test.mjs +23 -7
  207. package/tools/benchmark/upstream-error.test.mjs +16 -2
  208. package/tools/generate-owned-runtime-release.mjs +217 -0
  209. package/tools/installed-runtime-contract.mjs +16 -0
  210. package/tools/manual-runtime-manifest.mjs +39 -0
  211. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  212. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  213. package/tools/native-fixture.mjs +199 -40
  214. package/tools/native-glyph-helpers.py +127 -0
  215. package/tools/native-glyph-pty.py +144 -0
  216. package/tools/native-manual-projects.mjs +259 -0
  217. package/tools/native-manual.mjs +338 -0
  218. package/tools/native-model-picker-acceptance.py +306 -0
  219. package/tools/native-owned-runtime-env.sh +47 -0
  220. package/tools/native-paired-guards.mjs +282 -0
  221. package/tools/native-paired-manifest.mjs +72 -0
  222. package/tools/native-paired-orchestration.sh +51 -0
  223. package/tools/native-plugin-readiness.mjs +33 -0
  224. package/tools/native-plugin-readiness.test.mjs +53 -0
  225. package/tools/native-pty-invocation.mjs +26 -0
  226. package/tools/native-pty-runtime.mjs +83 -0
  227. package/tools/native-pty-smoke.py +147 -24
  228. package/tools/native-public-repo-shell-result.mjs +13 -0
  229. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  230. package/tools/native-runtime-identity.mjs +201 -0
  231. package/tools/native-slash-acceptance-env.py +38 -0
  232. package/tools/native-terminal-semantics.py +876 -0
  233. package/tools/owned-shell-observer/index.ts +16 -0
  234. package/tools/owned-shell-observer/package.json +1 -0
  235. package/tools/owned-visual-clear-env.py +48 -0
  236. package/tools/owned-visual-clear-model.py +21 -0
  237. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  238. package/tools/prepare-native-manual.mjs +122 -0
  239. package/tools/run-native-terminal-paired.sh +403 -0
  240. package/tools/runtime-token-env.mjs +8 -0
  241. package/tools/seed-native-switch-fixture.mjs +79 -0
  242. package/tools/test-native-controls.mjs +3 -1
  243. package/tools/test-native-daily-quests.mjs +65 -0
  244. package/tools/test-native-default-cli.mjs +106 -0
  245. package/tools/test-native-delivery-host.mjs +100 -18
  246. package/tools/test-native-evaluation.mjs +7 -3
  247. package/tools/test-native-glyph-pty.mjs +94 -0
  248. package/tools/test-native-host.mjs +83 -61
  249. package/tools/test-native-outcomes.mjs +3 -1
  250. package/tools/test-native-package.mjs +206 -38
  251. package/tools/test-native-performance.mjs +26 -0
  252. package/tools/test-native-product.mjs +90 -0
  253. package/tools/test-native-pty.mjs +34 -9
  254. package/tools/test-native-public-repo.mjs +224 -0
  255. package/tools/test-native-refresh-reentry.mjs +106 -0
  256. package/tools/test-native-report.mjs +27 -20
  257. package/tools/test-native-review.mjs +75 -0
  258. package/tools/test-native-runtime.mjs +120 -0
  259. package/tools/test-native-ship-models.mjs +176 -0
  260. package/tools/test-native-ship-walkthrough.mjs +96 -0
  261. package/tools/test-native-slash-commands.mjs +125 -0
  262. package/tools/test-native-slash-commands.py +242 -0
  263. package/tools/test-native-swe-host.mjs +224 -55
  264. package/tools/test-native-tasks.mjs +77 -0
  265. package/tools/test-native-terminal-performance.py +887 -0
  266. package/tools/test-native-terminal-readiness.sh +85 -0
  267. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  268. package/tools/test-native-visual-clear-installed.mjs +48 -0
  269. package/tools/test-owned-host-controls.mjs +238 -0
  270. package/tools/test-owned-opencode-runtime.mjs +35 -0
  271. package/tools/test-owned-visual-clear-env.py +120 -0
  272. package/tools/test-owned-visual-clear.py +373 -0
  273. package/tools/verify-draft-runtime-gate.mjs +138 -0
  274. package/tools/verify-owned-runtime-release.mjs +114 -0
  275. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Bounded, non-timed real integration readiness check. This starts only the
5
+ # disposable loopback fixture and a single PTY TUI; it does not run samples.
6
+ if [[ $# -ne 6 || ( "$6" != baseline && "$6" != ctg ) ]]; then
7
+ printf 'Usage: %s SOURCE_CHECKOUT EXECUTION_SNAPSHOT_TAR HARNESS_WORKTREE BINARY MANIFEST baseline|ctg\n' "$0" >&2
8
+ exit 2
9
+ fi
10
+ source_checkout="$(cd "$1" && pwd)"
11
+ execution_snapshot="$(realpath "$2")"
12
+ harness_worktree="$(cd "$3" && pwd)"
13
+ binary="$(realpath "$4")"
14
+ manifest="$(realpath "$5")"
15
+ mode="$6"
16
+ owned_root="${CTG_OWNED_OPENCODE_RUNTIME:-}"
17
+ if [[ -z "$owned_root" ]]; then owned_root="$(dirname "$(dirname "$(dirname "$binary")")")"; fi
18
+ release_manifest="${CTG_TEST_RUNTIME_RELEASE_MANIFEST:-$harness_worktree/lib/opencode/runtime-release.json}"
19
+ source "$harness_worktree/tools/native-owned-runtime-env.sh"
20
+ configure_native_owned_runtime "$binary" "$owned_root" "$release_manifest"
21
+ source_commit="$(node -e 'process.stdout.write(JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8")).sourceCommit)' "$manifest")"
22
+ snapshot_sha="$(node -e 'process.stdout.write(JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8")).sourceSnapshotSha256)' "$manifest")"
23
+ execution_sha="$(node -e 'process.stdout.write(JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8")).executionSnapshotSha256)' "$manifest")"
24
+ actual_snapshot_sha="$(sha256sum "$execution_snapshot" | awk '{print $1}')"
25
+ if [[ "$actual_snapshot_sha" != "$execution_sha" ]]; then
26
+ printf 'Execution snapshot hash does not match retained manifest\n' >&2
27
+ exit 1
28
+ fi
29
+ work="$(mktemp -d /tmp/ctg-native-readiness.XXXXXX)"
30
+ chmod 700 "$work"
31
+ ready="$work/ready"
32
+ seeder_log="$work/seeder.log"
33
+ server_pid=''
34
+ fixture=''
35
+ cleanup() {
36
+ if [[ -n "$server_pid" ]]; then
37
+ kill -TERM "$server_pid" 2>/dev/null || true
38
+ for _ in $(seq 1 50); do kill -0 "$server_pid" 2>/dev/null || break; sleep 0.1; done
39
+ kill -KILL "$server_pid" 2>/dev/null || true
40
+ wait "$server_pid" 2>/dev/null || true
41
+ fi
42
+ if [[ -n "$fixture" && -f "$fixture" ]]; then
43
+ temp="$(node -e 'process.stdout.write(JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8")).temp)' "$fixture")"
44
+ if [[ "$temp" == /tmp/ctg-native-fixture-* && "$temp" != *..* ]]; then rm -rf -- "$temp"; fi
45
+ fi
46
+ if [[ "$work" == /tmp/ctg-native-readiness.* && "$work" != *..* ]]; then rm -rf -- "$work"; fi
47
+ }
48
+ trap cleanup EXIT
49
+ trap 'exit 130' INT
50
+ trap 'exit 143' TERM
51
+ source_root="$work/source"
52
+ mkdir -p "$source_root"
53
+ tar -xf "$execution_snapshot" -C "$source_root"
54
+ deps="$(realpath "$source_checkout/node_modules")"
55
+ cp "$harness_worktree/tools/native-fixture.mjs" "$harness_worktree/tools/seed-native-switch-fixture.mjs" \
56
+ "$harness_worktree/tools/native-paired-guards.mjs" "$harness_worktree/tools/native-runtime-identity.mjs" \
57
+ "$harness_worktree/tools/native-owned-runtime-env.sh" \
58
+ "$harness_worktree/tools/test-native-terminal-performance.py" \
59
+ "$source_root/tools/"
60
+ ln -s "$deps" "$source_root/node_modules"
61
+ mkdir -p "$source_root/plugins/carthagent/node_modules/@opencode"
62
+ ln -s "$deps/@opencode/plugin" "$source_root/plugins/carthagent/node_modules/@opencode/plugin"
63
+ seed=("$source_root/tools/seed-native-switch-fixture.mjs" "$source_root/plugins/carthagent" --serve "--ready-file=$ready")
64
+ expected=active
65
+ if [[ "$mode" == baseline ]]; then seed+=(--no-plugin); expected=absent; fi
66
+ CTG_PERF_MANIFEST="$manifest" NODE_PATH="$deps" node "${seed[@]}" >"$seeder_log" 2>&1 &
67
+ server_pid=$!
68
+ for _ in $(seq 1 900); do
69
+ if [[ -s "$ready" ]]; then break; fi
70
+ if ! kill -0 "$server_pid" 2>/dev/null; then
71
+ printf '%s readiness fixture seeder exited before ready (private log retained only until cleanup)\n' "$mode" >&2
72
+ exit 1
73
+ fi
74
+ sleep 0.1
75
+ done
76
+ if [[ ! -s "$ready" ]]; then
77
+ printf '%s readiness fixture timed out before ready\n' "$mode" >&2
78
+ exit 1
79
+ fi
80
+ fixture="$(cat "$ready")"
81
+ CTG_PERF_EXPECT_PLUGIN="$expected" CTG_PERF_SOURCE_COMMIT="$source_commit" \
82
+ CTG_PERF_SOURCE_SNAPSHOT_SHA256="$snapshot_sha" NODE_PATH="$(realpath "$source_root/node_modules")" \
83
+ CTG_OPENCODE_BIN="$binary" python3 "$source_root/tools/test-native-terminal-performance.py" \
84
+ --binary "$binary" --switch-fixture "$fixture" --samples 1 --readiness-only
85
+ printf 'Readiness passed for %s; no timed samples were run.\n' "$mode"
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ /** Run controlled-terminal semantics against the installed package's default owned runtime. */
3
+ import assert from 'node:assert/strict';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { resolveOpenCode, runtimeIdentity } from '../lib/opencode/runtime.mjs';
8
+ import { loadNativeTestRuntime } from './native-runtime-identity.mjs';
9
+ import { withoutRuntimeGithubToken } from './runtime-token-env.mjs';
10
+ import { assertTerminalSemanticsProof, resolveInstalledRuntime } from './installed-runtime-contract.mjs';
11
+
12
+ assert.equal(process.platform, 'linux', 'installed terminal-semantics acceptance is currently a Linux controlled-PTY claim');
13
+ for (const key of Object.keys(process.env)) {
14
+ assert.ok(!/^CTG_(?:OPENCODE_BIN|OWNED_OPENCODE_RUNTIME|TEST_RUNTIME_)/i.test(key),
15
+ `${key} must not override installed default runtime acceptance`);
16
+ }
17
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const manifestPath = join(root, 'lib/opencode/runtime-release.json');
19
+ const manifest = JSON.parse(await (await import('node:fs/promises')).readFile(manifestPath, 'utf8'));
20
+ const { binary, identity } = await resolveInstalledRuntime(resolveOpenCode, runtimeIdentity, manifestPath);
21
+ assert.equal(identity.kind, 'ctg-owned', 'terminal semantics acceptance requires default installed CTG-owned runtime');
22
+ const verified = await loadNativeTestRuntime({
23
+ binary, ownedRoot: dirname(dirname(dirname(binary))), expectedManifest: manifest,
24
+ });
25
+ assert.equal(verified.binary, binary);
26
+ assert.equal(verified.binarySha256, identity.binarySha256);
27
+
28
+ const python = process.env.PYTHON ?? (process.platform === 'win32' ? 'python' : 'python3');
29
+ const env = withoutRuntimeGithubToken(process.env);
30
+ const result = spawnSync(python, [join(root, 'tools/native-terminal-semantics.py'), '--binary', binary], {
31
+ cwd: root, encoding: 'utf8', timeout: 180_000, env,
32
+ });
33
+ if (result.error) throw result.error;
34
+ if (result.stdout) process.stdout.write(result.stdout);
35
+ if (result.stderr) process.stderr.write(result.stderr);
36
+ assert.equal(result.status, 0, `controlled terminal semantics failed with status ${result.status}`);
37
+ const proof = JSON.parse(result.stdout);
38
+ assertTerminalSemanticsProof(proof, { binary: resolve(binary), identity, manifest });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /** Run approved visual-clear PTY proof with the installed package default owned runtime. */
3
+ import assert from 'node:assert/strict';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { resolveOpenCode, runtimeIdentity } from '../lib/opencode/runtime.mjs';
8
+ import { loadNativeTestRuntime } from './native-runtime-identity.mjs';
9
+ import { withoutRuntimeGithubToken } from './runtime-token-env.mjs';
10
+ import { resolveInstalledRuntime } from './installed-runtime-contract.mjs';
11
+
12
+ assert.equal(process.platform, 'linux', 'installed visual-clear acceptance is a Linux POSIX PTY claim');
13
+ for (const key of Object.keys(process.env)) {
14
+ assert.ok(!/^CTG_(?:OPENCODE_BIN|OWNED_OPENCODE_RUNTIME|TEST_RUNTIME_)/i.test(key),
15
+ `${key} must not override installed default runtime acceptance`);
16
+ }
17
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const manifestPath = join(root, 'lib/opencode/runtime-release.json');
19
+ const manifest = JSON.parse(await (await import('node:fs/promises')).readFile(manifestPath, 'utf8'));
20
+ const { binary, identity } = await resolveInstalledRuntime(resolveOpenCode, runtimeIdentity, manifestPath);
21
+ assert.equal(identity.kind, 'ctg-owned', 'installed visual-clear acceptance requires default CTG-owned runtime selection');
22
+ const ownedRoot = dirname(dirname(dirname(binary)));
23
+ const verified = await loadNativeTestRuntime({ binary, ownedRoot, expectedManifest: manifest });
24
+ assert.equal(verified.binarySha256, identity.binarySha256, 'independent verifier must match default-resolved runtime bytes');
25
+
26
+ const python = process.env.PYTHON ?? 'python3';
27
+ const artifact = join(root, `.ctg-visual-clear-${process.pid}.json`);
28
+ try {
29
+ const result = spawnSync(python, [join(root, 'tools/test-owned-visual-clear.py'),
30
+ '--binary', binary, '--owned-root', ownedRoot, '--release-manifest', manifestPath,
31
+ '--artifact', artifact], {
32
+ cwd: root, encoding: 'utf8', timeout: 240_000,
33
+ // The clear runner uses its own strict allowlists and does not need the
34
+ // acquisition token; keep it away from Python, fixture, host, and PTY.
35
+ env: withoutRuntimeGithubToken(process.env),
36
+ });
37
+ if (result.error) throw result.error;
38
+ if (result.stdout) process.stdout.write(result.stdout);
39
+ if (result.stderr) process.stderr.write(result.stderr);
40
+ assert.equal(result.status, 0, `visual-clear PTY proof failed with status ${result.status}`);
41
+ const proof = JSON.parse(await (await import('node:fs/promises')).readFile(artifact, 'utf8'));
42
+ assert.equal(proof.status, 'PASS');
43
+ assert.equal(proof.binarySha256, identity.binarySha256, 'visual-clear proof must identify the installed default-resolved executable bytes');
44
+ assert.equal(proof.runtimeIdentity?.runtimeVersion, manifest.runtimeVersion);
45
+ assert.equal(proof.scope, 'Linux POSIX PTY; explicit trusted-preflighted owned staging artifact; offline loopback provider');
46
+ } finally {
47
+ await (await import('node:fs/promises')).rm(artifact, { force: true });
48
+ }
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ /** Behavioral session/restart, auto-compaction, clear, and shell-origin proof on the owned host. */
3
+ import assert from 'node:assert/strict';
4
+ import { createServer } from 'node:http';
5
+ import { mkdtemp, mkdir, readFile, rm } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { dirname, join } from 'node:path';
8
+ import { spawn } from 'node:child_process';
9
+ import { once } from 'node:events';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { OpenCode } from '@opencode/client';
12
+ import { preflightNativeTestRuntimeArtifact } from './native-runtime-identity.mjs';
13
+ import { resolveOpenCode, runtimeIdentity, provisionRuntime, runtimeEnvironment } from '../lib/opencode/runtime.mjs';
14
+
15
+ if (process.platform !== 'linux') throw new Error('This native shell provenance probe currently targets Linux; it is not a cross-platform acceptance claim.');
16
+ const installedDefault = process.argv.includes('--installed-default');
17
+ if (process.env.CTG_OPENCODE_BIN) throw new Error('Do not set CTG_OPENCODE_BIN for owned acceptance.');
18
+ if (process.env.CTG_TEST_RUNTIME_SHA256) throw new Error('Do not override the trusted release-manifest executable hash.');
19
+ if (installedDefault && (process.env.CTG_OWNED_OPENCODE_RUNTIME || process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST)) {
20
+ throw new Error('Installed acceptance must use the packaged manifest and default runtime resolver without test overrides.');
21
+ }
22
+ const ownedRoot = installedDefault ? undefined : process.env.CTG_OWNED_OPENCODE_RUNTIME;
23
+ const trustedManifestPath = installedDefault
24
+ ? fileURLToPath(new URL('../lib/opencode/runtime-release.json', import.meta.url))
25
+ : process.env.CTG_TEST_RUNTIME_RELEASE_MANIFEST;
26
+ if (!installedDefault && !ownedRoot) throw new Error('Set CTG_OWNED_OPENCODE_RUNTIME to the owned runtime artifact root under test.');
27
+ if (!trustedManifestPath) throw new Error('A trusted release manifest is required for owned acceptance.');
28
+ const preflight = ownedRoot ? await preflightNativeTestRuntimeArtifact({ ownedRoot, expectedManifestPath: trustedManifestPath }) : null;
29
+ const runtimeOptions = { ...(ownedRoot ? { ownedRuntime: ownedRoot } : {}), runtimeReleaseManifestPath: trustedManifestPath };
30
+ const binary = await resolveOpenCode(runtimeOptions);
31
+ const identity = await runtimeIdentity(binary, runtimeOptions);
32
+ assert.equal(identity.kind, 'ctg-owned');
33
+ if (preflight) {
34
+ assert.equal(identity.binaryPath, preflight.identity.binary, 'product resolver selected the preflighted artifact');
35
+ assert.equal(identity.binarySha256, preflight.identity.binarySha256, 'product identity matches trusted pre-execution byte verification');
36
+ assert.equal(identity.version, preflight.identity.runtimeVersion);
37
+ } else {
38
+ assert.equal(identity.kind, 'ctg-owned', 'installed acceptance requires the production default resolver to select CTG-owned runtime');
39
+ const { loadNativeTestRuntime } = await import('./native-runtime-identity.mjs');
40
+ const expectedManifest = JSON.parse(await readFile(trustedManifestPath, 'utf8'));
41
+ const verified = await loadNativeTestRuntime({ binary, ownedRoot: dirname(dirname(dirname(binary))), expectedManifest });
42
+ assert.equal(verified.binary, binary);
43
+ assert.equal(verified.binarySha256, identity.binarySha256);
44
+ }
45
+
46
+ const temp = await mkdtemp(join(tmpdir(), 'ctg-owned-controls-'));
47
+ const cwd = join(temp, 'project');
48
+ const root = join(temp, 'runtime');
49
+ const captureFile = join(temp, 'shell-hooks.jsonl');
50
+ const requests = [];
51
+ let rejectOverflow = true;
52
+ const provider = createServer(async (req, res) => {
53
+ let raw = '';
54
+ for await (const chunk of req) raw += chunk;
55
+ const body = JSON.parse(raw || '{}');
56
+ requests.push(body);
57
+ const serialized = JSON.stringify(body.messages ?? []);
58
+ if (serialized.includes('You MUST use this format for your response')) return stream(res, '## Objective\n- preserve the fixture context');
59
+ if (serialized.includes('OWNED_OVERFLOW') && rejectOverflow) {
60
+ rejectOverflow = false;
61
+ res.writeHead(400, { 'content-type': 'application/json' });
62
+ res.end(JSON.stringify({ error: { message: 'Your input exceeds the context window', code: 'context_length_exceeded', type: 'invalid_request_error' } }));
63
+ return;
64
+ }
65
+ const user = [...(body.messages ?? [])].reverse().find(message => message.role === 'user');
66
+ if (serialized.includes('owned-identical-shell') && !body.messages.some(message => message.role === 'tool')) {
67
+ const calls = (body.tools ?? []).filter(tool => tool.function?.name === 'shell');
68
+ assert.ok(calls.length, 'owned runtime must advertise native shell in the two provenance sessions');
69
+ const callID = `identical-${requests.length}`;
70
+ const userText = String([...body.messages].reverse().find(message => message.role === 'user')?.content ?? '');
71
+ const marker = userText.includes('OWNED_SHELL_SESSION_A') ? 'A' : userText.includes('OWNED_SHELL_SESSION_B') ? 'B' : null;
72
+ assert.ok(marker, `shell tool call has no known originating prompt marker: ${userText}`);
73
+ providerToolCalls.set(marker, callID);
74
+ const chunk = (delta, finish_reason = null) => `data: ${JSON.stringify({ id: `same-command-${requests.length}`, object: 'chat.completion.chunk', created: 1, model: 'fixture', choices: [{ index: 0, delta, finish_reason }] })}\n\n`;
75
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
76
+ res.end(`${chunk({ role: 'assistant', tool_calls: [{ index: 0, id: callID, type: 'function', function: { name: 'shell', arguments: JSON.stringify({ command: 'printf owned-identical-shell' }) } }] })}${chunk({}, 'tool_calls')}data: [DONE]\n\n`);
77
+ return;
78
+ }
79
+ stream(res, `reply:${String(user?.content ?? '').slice(0, 60)}`);
80
+ });
81
+
82
+ let child;
83
+ let client;
84
+ let providerListening = false;
85
+ const providerToolCalls = new Map();
86
+ const password = `owned-${process.pid}-${Date.now()}`;
87
+ const request = () => ({ signal: AbortSignal.timeout(30_000) });
88
+ const start = async () => {
89
+ const env = runtimeEnvironment({ root });
90
+ env.OPENCODE_SERVER_PASSWORD = password;
91
+ env.CTG_OWNED_SHELL_CAPTURE = captureFile;
92
+ child = spawn(binary, ['serve', '--hostname', '127.0.0.1', '--port', '0'], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
93
+ let output = '';
94
+ const baseUrl = await new Promise((resolve, reject) => {
95
+ const timer = setTimeout(() => reject(Error(`owned server startup timeout: ${output}`)), 30_000);
96
+ const collect = chunk => {
97
+ output = (output + chunk.toString()).slice(-8000);
98
+ const url = output.match(/http:\/\/127\.0\.0\.1:\d+/)?.[0];
99
+ if (url) { clearTimeout(timer); resolve(url); }
100
+ };
101
+ child.stdout.on('data', collect);
102
+ child.stderr.on('data', collect);
103
+ child.once('error', error => { clearTimeout(timer); reject(error); });
104
+ child.once('exit', code => { clearTimeout(timer); reject(Error(`owned server exited ${code}: ${output}`)); });
105
+ });
106
+ client = OpenCode.make({ baseUrl, headers: { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` } });
107
+ const info = await client.server.info(request());
108
+ assert.equal(info.version, identity.version);
109
+ assert.equal(info.capabilities?.shellOrigin, true);
110
+ return client;
111
+ };
112
+ const stop = async () => {
113
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
114
+ let exited = once(child, 'exit');
115
+ child.kill('SIGTERM');
116
+ let timer;
117
+ const graceful = await Promise.race([exited.then(() => true), new Promise(resolve => { timer = setTimeout(() => resolve(false), 5000); })]);
118
+ clearTimeout(timer);
119
+ if (graceful) return;
120
+ child.kill('SIGKILL');
121
+ let forceTimer;
122
+ try {
123
+ await Promise.race([exited, new Promise((_, reject) => { forceTimer = setTimeout(() => reject(new Error('Owned host did not exit after SIGKILL')), 5000); })]);
124
+ } finally { clearTimeout(forceTimer); }
125
+ };
126
+
127
+ try {
128
+ await mkdir(cwd, { recursive: true });
129
+ await new Promise((resolve, reject) => {
130
+ provider.once('error', reject);
131
+ provider.listen(0, '127.0.0.1', resolve);
132
+ });
133
+ providerListening = true;
134
+ const providerUrl = `http://127.0.0.1:${provider.address().port}/v1`;
135
+ const profile = {
136
+ plugins: [{ package: join(import.meta.dirname, 'owned-shell-observer'), options: { captureFile } }],
137
+ update: 'disable',
138
+ model: 'ctg-fixture/fixture',
139
+ default_agent: 'build',
140
+ providers: { 'ctg-fixture': {
141
+ package: '@opencode/ai/providers/openai-compatible',
142
+ settings: { baseURL: providerUrl, apiKey: 'offline-fixture' },
143
+ models: { fixture: { limit: { context: 32_000, output: 1024 }, cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 } } },
144
+ } },
145
+ };
146
+ await provisionRuntime({ root, profile });
147
+ client = await start();
148
+ const location = { directory: cwd };
149
+ const model = { providerID: 'ctg-fixture', id: 'fixture' };
150
+ const first = await client.session.create({ location, model, title: 'Owned restart policy A' }, request());
151
+ const second = await client.session.create({ location, model, title: 'Owned restart policy B' }, request());
152
+ await client.session.update({ sessionID: first.id, metadata: { 'ctg.autoCompaction': false } }, request());
153
+ await client.session.update({ sessionID: second.id, metadata: { 'ctg.autoCompaction': true } }, request());
154
+ await client.session.prompt({ sessionID: first.id, text: 'OWNED_AUTOCOMPACT_OFF initial' }, request());
155
+ await client.session.prompt({ sessionID: second.id, text: 'OWNED_AUTOCOMPACT_ON initial' }, request());
156
+ await Promise.all([client.session.wait({ sessionID: first.id }, request()), client.session.wait({ sessionID: second.id }, request())]);
157
+ const beforeRestartA = await client.message.list({ sessionID: first.id }, request());
158
+ const beforeRestartB = await client.message.list({ sessionID: second.id }, request());
159
+ assert.ok(beforeRestartA.data.some(message => JSON.stringify(message).includes('OWNED_AUTOCOMPACT_OFF')));
160
+ assert.ok(beforeRestartB.data.some(message => JSON.stringify(message).includes('OWNED_AUTOCOMPACT_ON')));
161
+
162
+ await stop();
163
+ const restarted = await start();
164
+ assert.equal((await restarted.session.get({ sessionID: first.id }, request())).metadata['ctg.autoCompaction'], false);
165
+ assert.equal((await restarted.session.get({ sessionID: second.id }, request())).metadata['ctg.autoCompaction'], true);
166
+
167
+ requests.length = 0;
168
+ rejectOverflow = true;
169
+ await restarted.session.prompt({ sessionID: first.id, text: 'OWNED_OVERFLOW sessionA' }, request());
170
+ await restarted.session.wait({ sessionID: first.id }, request());
171
+ assert.equal(requests.length, 1, 'auto-compaction-off overflow must produce no summary or retry request');
172
+ assert.equal((await restarted.session.get({ sessionID: first.id }, request())).outcome, 'failed');
173
+
174
+ requests.length = 0;
175
+ rejectOverflow = true;
176
+ await restarted.session.prompt({ sessionID: second.id, text: 'OWNED_OVERFLOW sessionB' }, request());
177
+ await restarted.session.wait({ sessionID: second.id }, request());
178
+ const summaryIndex = requests.findIndex(body => JSON.stringify(body.messages).includes('You MUST use this format for your response'));
179
+ const initialOverflowIndex = requests.findIndex(body => JSON.stringify(body.messages).includes('OWNED_OVERFLOW sessionB'));
180
+ const retryIndex = requests.findIndex((body, index) => index > summaryIndex && JSON.stringify(body.messages).includes('OWNED_OVERFLOW sessionB'));
181
+ assert.ok(initialOverflowIndex >= 0 && summaryIndex > initialOverflowIndex && retryIndex > summaryIndex,
182
+ `expected overflow → summary → retry provider requests, got ${JSON.stringify(requests.map(body => body.messages))}`);
183
+ assert.equal((await restarted.session.get({ sessionID: second.id }, request())).outcome, 'succeeded');
184
+
185
+ const same1 = await restarted.session.create({ location, model, title: 'Concurrent identical shell A', metadata: { 'ctg.autoCompaction': false } }, request());
186
+ const same2 = await restarted.session.create({ location, model, title: 'Concurrent identical shell B', metadata: { 'ctg.autoCompaction': false } }, request());
187
+ const prompts = [
188
+ { session: same1, marker: 'A' },
189
+ { session: same2, marker: 'B' },
190
+ ];
191
+ const pluginList = await restarted.plugin.list({ location }, request());
192
+ assert.ok(pluginList.data.some(plugin => plugin.id === 'ctg.owned-shell-observer' && plugin.state.status === 'active'), JSON.stringify(pluginList.data));
193
+ await Promise.all([
194
+ restarted.session.prompt({ sessionID: same1.id, text: 'OWNED_SHELL_SESSION_A Run shell command: node -e "process.stdout.write(\'owned-identical-shell\')"' }, request()),
195
+ restarted.session.prompt({ sessionID: same2.id, text: 'OWNED_SHELL_SESSION_B Run shell command: node -e "process.stdout.write(\'owned-identical-shell\')"' }, request()),
196
+ ]);
197
+ await Promise.all([restarted.session.wait({ sessionID: same1.id }, request()), restarted.session.wait({ sessionID: same2.id }, request())]);
198
+ for (const { session } of prompts) assert.equal((await restarted.session.get({ sessionID: session.id }, request())).outcome, 'succeeded');
199
+ let capturedText;
200
+ try { capturedText = await readFile(captureFile, 'utf8'); }
201
+ catch (error) { throw new Error(`shell observer did not record a hook event; provider requests=${JSON.stringify(requests.map(body => body.messages?.slice(-2)))}; session tools=${JSON.stringify(await Promise.all([same1, same2].map(session => restarted.message.list({ sessionID: session.id }, request()))))}`, { cause: error }); }
202
+ const shellEvents = capturedText.trim().split('\n').filter(Boolean).map(JSON.parse);
203
+ const attributed = shellEvents.filter(event => event.command.includes('owned-identical-shell'));
204
+ assert.equal(attributed.length, 2, JSON.stringify(shellEvents));
205
+ const observations = new Map(attributed.map(event => [event.sessionID, event]));
206
+ for (const { session, marker } of prompts) {
207
+ const observed = observations.get(session.id);
208
+ assert.ok(observed, `missing shell hook event for ${session.id}: ${JSON.stringify(attributed)}`);
209
+ assert.equal(observed.origin, 'tool');
210
+ assert.equal(observed.callID, providerToolCalls.get(marker), `hook call ID matches the native provider call ID for marker ${marker}`);
211
+ const history = await restarted.message.list({ sessionID: session.id }, request());
212
+ const toolParts = history.data.flatMap(message => message.parts ?? message.content ?? []).filter(part => part.type === 'tool' && part.id === providerToolCalls.get(marker));
213
+ assert.equal(toolParts.length, 1, `durable tool transcript correlates to provider call for session ${session.id}: ${JSON.stringify(history.data)}`);
214
+ assert.equal(toolParts[0].state.status, 'completed', JSON.stringify(toolParts[0]));
215
+ assert.equal(toolParts[0].state.input.command, observed.command);
216
+ assert.ok(toolParts[0].state.content.some(part => part.type === 'text' && part.text.includes('owned-identical-shell')));
217
+ }
218
+
219
+ console.log(JSON.stringify({
220
+ host: { compatibilityVersion: preflight?.identity.compatibilityVersion ?? identity.compatibilityVersion, runtimeVersion: identity.version, binarySha256: identity.binarySha256 },
221
+ runtimeSelection: installedDefault ? 'installed package default resolver; trusted manifest verified' : 'explicit, trusted-preflighted CTG_OWNED_OPENCODE_RUNTIME staging artifact',
222
+ sameDatabaseServiceRestart: 'passed',
223
+ autoCompaction: { off: 'overflow failed with one provider attempt and no compaction', on: 'compaction request and successful retry observed' },
224
+ concurrentIdenticalShell: 'real runtime events correlated to distinct sessionID/origin/callID',
225
+ loopbackProviderRequests: requests.length,
226
+ }));
227
+ } finally {
228
+ await stop();
229
+ provider.closeAllConnections();
230
+ if (providerListening) await new Promise(resolve => provider.close(resolve));
231
+ await rm(temp, { recursive: true, force: true });
232
+ }
233
+
234
+ function stream(res, text) {
235
+ const chunk = (delta, finish_reason = null, usage) => `data: ${JSON.stringify({ id: `owned-${requests.length}`, object: 'chat.completion.chunk', created: 1, model: 'fixture', choices: [{ index: 0, delta, finish_reason }], ...(usage ? { usage } : {}) })}\n\n`;
236
+ res.writeHead(200, { 'content-type': 'text/event-stream' });
237
+ res.end(`${chunk({ role: 'assistant', content: text })}${chunk({}, 'stop', { prompt_tokens: 30, completion_tokens: 10, total_tokens: 40 })}data: [DONE]\n\n`);
238
+ }
@@ -0,0 +1,35 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { resolveOwnedOpenCode } from '../lib/opencode/runtime.mjs';
5
+
6
+ const directory = process.env.CTG_OWNED_OPENCODE_RUNTIME;
7
+ if (!directory) throw new Error('Set CTG_OWNED_OPENCODE_RUNTIME to an extracted, hash-verified CTG runtime artifact.');
8
+
9
+ const binary = await resolveOwnedOpenCode(directory);
10
+ const root = await mkdtemp(path.join(tmpdir(), 'ctg-owned-opencode-smoke-'));
11
+ const child = await import('node:child_process');
12
+ const { runtimeEnvironment } = await import('../lib/opencode/runtime.mjs');
13
+ const env = runtimeEnvironment({ root });
14
+ try {
15
+ const version = await new Promise((resolve, reject) => {
16
+ const process = child.spawn(binary, ['--version'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
17
+ let output = '';
18
+ process.stdout.setEncoding('utf8').on('data', chunk => { output += chunk; });
19
+ process.once('error', reject);
20
+ process.once('close', code => code === 0 ? resolve(output.trim()) : reject(new Error(`owned CLI exited ${code}`)));
21
+ });
22
+ if (version !== 'opencode v0.0.0-ctg.2.0.32') throw new Error(`Unexpected owned CLI identity: ${version}`);
23
+ const files = await import('node:fs/promises');
24
+ for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'XDG_CACHE_HOME', 'OPENCODE_DB']) {
25
+ const value = env[key];
26
+ if (!value || !path.resolve(value).startsWith(`${path.resolve(root)}${path.sep}`)) throw new Error(`${key} escaped disposable runtime root: ${value}`);
27
+ }
28
+ await files.mkdir(env.OPENCODE_CONFIG_DIR, { recursive: true, mode: 0o700 });
29
+ await writeFile(path.join(env.OPENCODE_CONFIG_DIR, 'fixture-marker'), 'isolated');
30
+ console.log(JSON.stringify({ binary, version, profileRoot: root, result: 'PASS' }, null, 2));
31
+ } finally {
32
+ const persisted = await readFile(path.join(root, 'config', 'opencode', 'fixture-marker'), 'utf8').catch(() => undefined);
33
+ if (persisted !== 'isolated') throw new Error('Temporary profile fixture did not persist inside the isolated root.');
34
+ await rm(root, { recursive: true, force: true });
35
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Unit tests for the visual-clear probe's subprocess environment boundary."""
3
+ import importlib.util
4
+ from pathlib import Path
5
+ import unittest
6
+
7
+ MODULE = Path(__file__).with_name('owned-visual-clear-env.py')
8
+ SPEC = importlib.util.spec_from_file_location('owned_visual_clear_env', MODULE)
9
+ ENV = importlib.util.module_from_spec(SPEC)
10
+ SPEC.loader.exec_module(ENV)
11
+ MODEL_PATH = Path(__file__).with_name('owned-visual-clear-model.py')
12
+ MODEL_SPEC = importlib.util.spec_from_file_location('owned_visual_clear_model', MODEL_PATH)
13
+ MODEL = importlib.util.module_from_spec(MODEL_SPEC)
14
+ MODEL_SPEC.loader.exec_module(MODEL)
15
+
16
+
17
+ class ChildEnvironmentTests(unittest.TestCase):
18
+ def test_latest_message_uses_newest_first_api_order_including_idle_events(self):
19
+ latest = {'id': 'msg_idle', 'type': 'idle'}
20
+ older = {'id': 'msg_user', 'type': 'user', 'text': 'prompt'}
21
+ self.assertEqual(MODEL.latest_message({'data': [latest, older]}), latest)
22
+ with self.assertRaisesRegex(ValueError, 'no messages'):
23
+ MODEL.latest_message({'data': []})
24
+
25
+ def test_canonical_conversation_matches_newest_first_data_and_detects_mutation(self):
26
+ oldest = {
27
+ 'id': 'msg_old', 'type': 'user', 'text': 'first exact prompt',
28
+ 'time': {'created': 1},
29
+ }
30
+ newest = {
31
+ 'id': 'msg_new', 'type': 'assistant', 'text': 'second exact reply',
32
+ 'time': {'created': 2},
33
+ }
34
+ after_followup = [oldest, newest]
35
+ # The API's newest-first ordering must compare equal to the same durable
36
+ # transcript captured in a different order, retaining exact message data.
37
+ newest_first_api = [newest, oldest]
38
+ self.assertEqual(MODEL.canonical_conversation(newest_first_api),
39
+ MODEL.canonical_conversation(after_followup))
40
+ self.assertEqual(MODEL.canonical_conversation(newest_first_api),
41
+ MODEL.canonical_conversation([oldest, newest]))
42
+
43
+ mutated_api = [newest, {**oldest, 'text': 'changed prompt'}]
44
+ self.assertNotEqual(MODEL.canonical_conversation(mutated_api),
45
+ MODEL.canonical_conversation(after_followup))
46
+ changed_id_api = [newest, {**oldest, 'id': 'msg_replaced'}]
47
+ self.assertNotEqual(MODEL.canonical_conversation(changed_id_api),
48
+ MODEL.canonical_conversation(after_followup))
49
+
50
+ def test_fixture_environment_only_inherits_explicit_platform_basics(self):
51
+ ambient = {
52
+ 'PATH': '/trusted/test/path',
53
+ 'LANG': 'C.UTF-8',
54
+ 'LC_ALL': 'C',
55
+ 'HOME': '/real/user/home',
56
+ 'AWS_SECRET_ACCESS_KEY': 'FAKE_AWS_SECRET_FOR_TEST',
57
+ 'GITHUB_TOKEN': 'FAKE_GITHUB_TOKEN_FOR_TEST',
58
+ 'CTG_RUNTIME_GITHUB_TOKEN': 'FAKE_RUNTIME_TOKEN_FOR_TEST',
59
+ 'NODE_OPTIONS': '--require=/credential-stealing.js',
60
+ 'OPENCODE_CONFIG': '/real/user/profile.json',
61
+ }
62
+ result = ENV.fixture_environment(
63
+ ambient, '/tmp/clear-test-home', '/tmp/owned-root', '/tmp/release.json')
64
+ self.assertEqual(result['PATH'], ambient['PATH'])
65
+ self.assertEqual(result['LANG'], ambient['LANG'])
66
+ self.assertEqual(result['LC_ALL'], ambient['LC_ALL'])
67
+ self.assertEqual(result['HOME'], '/tmp/clear-test-home')
68
+ self.assertEqual(result['CTG_OWNED_OPENCODE_RUNTIME'], '/tmp/owned-root')
69
+ self.assertEqual(result['CTG_TEST_RUNTIME_RELEASE_MANIFEST'], '/tmp/release.json')
70
+ self.assertFalse({'AWS_SECRET_ACCESS_KEY', 'GITHUB_TOKEN',
71
+ 'CTG_RUNTIME_GITHUB_TOKEN', 'NODE_OPTIONS',
72
+ 'OPENCODE_CONFIG'} & result.keys())
73
+
74
+ def test_tui_allowlist_rejects_unknown_and_case_variant_reserved_credentials(self):
75
+ clean = ENV.fixture_environment(
76
+ {'PATH': '/usr/bin', 'LANG': 'C.UTF-8'},
77
+ '/tmp/clear-test-home', '/tmp/owned-root', '/tmp/release.json')
78
+ clean = {key: value for key, value in clean.items() if key in ENV.TUI_ENV_KEYS}
79
+ clean.update({'TERM': 'xterm-256color', 'OPENCODE_PASSWORD': 'FAKE_FIXTURE_PASSWORD'})
80
+ ENV.assert_clean_child_environment(clean)
81
+
82
+ with self.assertRaisesRegex(AssertionError, 'non-allowlisted'):
83
+ ENV.assert_clean_child_environment({**clean, 'AWS_SECRET_ACCESS_KEY': 'FAKE'})
84
+ with self.assertRaisesRegex(AssertionError, 'reserved runtime download'):
85
+ ENV.assert_clean_child_environment({**clean, 'ctg_runtime_github_token': 'FAKE'})
86
+
87
+ def test_node_helper_environment_does_not_forward_parent_credentials(self):
88
+ fake_parent = {
89
+ 'PATH': '/usr/bin',
90
+ 'LANG': 'C.UTF-8',
91
+ 'AWS_SECRET_ACCESS_KEY': 'FAKE_AWS_SECRET_FOR_TEST',
92
+ 'SSH_AUTH_SOCK': '/fake/agent.sock',
93
+ 'NPM_TOKEN': 'FAKE_NPM_TOKEN_FOR_TEST',
94
+ 'GH_TOKEN': 'FAKE_GH_TOKEN_FOR_TEST',
95
+ }
96
+ child = ENV.fixture_environment(
97
+ fake_parent, '/tmp/isolated-home', '/tmp/owned', '/tmp/manifest.json')
98
+ self.assertEqual(set(child) - set(ENV.SAFE_PARENT_ENV) - {
99
+ 'HOME', 'USERPROFILE', 'CTG_OWNED_OPENCODE_RUNTIME',
100
+ 'CTG_TEST_RUNTIME_RELEASE_MANIFEST', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME',
101
+ 'XDG_STATE_HOME', 'XDG_CACHE_HOME'}, set())
102
+
103
+ def test_preflight_receives_no_ambient_secrets_or_runtime_download_token(self):
104
+ ambient = {
105
+ 'PATH': '/usr/bin', 'LANG': 'C.UTF-8',
106
+ 'CTG_RUNTIME_GITHUB_TOKEN': 'FAKE_RUNTIME_GITHUB_TOKEN',
107
+ 'GITHUB_TOKEN': 'FAKE_GITHUB_TOKEN',
108
+ 'AWS_ACCESS_KEY_ID': 'FAKE_AWS_ACCESS_KEY',
109
+ 'NODE_OPTIONS': '--require=fake-preload.js',
110
+ }
111
+ child = ENV.fixture_environment(
112
+ ambient, '/tmp/isolated-home', '/tmp/owned', '/tmp/manifest.json')
113
+ ENV.assert_clean_child_environment(child, ENV.FIXTURE_ENV_KEYS)
114
+ self.assertFalse(any(name.casefold() == 'ctg_runtime_github_token' for name in child))
115
+ self.assertFalse({'GITHUB_TOKEN', 'AWS_ACCESS_KEY_ID', 'NODE_OPTIONS'} & child.keys())
116
+
117
+
118
+
119
+ if __name__ == '__main__':
120
+ unittest.main()