@bongos/core 1.19.1072 → 1.19.1074

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 (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +2 -1
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +84 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
@@ -1,317 +0,0 @@
1
- // tests/box_onboard.mjs
2
- //
3
- // Hermetic unit smoke for the one-command onboarding module
4
- // (modules/dev-box/box-onboard.js, ADR 0031 §1/§6, #685). Covers the PURE core — SSH
5
- // public-key validation + fingerprinting, the injection-rejection that protects
6
- // authorized_keys, the managed-block + managed-settings + connect-guide
7
- // renderers — and the DB query SHAPES via a fake pool. NO database, NO network,
8
- // NO real box. The live-rank GATE for /box/authorized-keys reuses
9
- // box-access.decideSourceAccess, which is pinned in scripts/gds/smoke-box.sh.
10
- //
11
- // Run: node tests/box_onboard.mjs
12
-
13
- import { strict as assert } from 'node:assert';
14
- import { createRequire } from 'node:module';
15
- import http from 'node:http';
16
-
17
- const require = createRequire(import.meta.url);
18
- const o = require('../modules/dev-box/box-onboard.js');
19
-
20
- let passed = 0;
21
- let failed = 0;
22
- function t(name, fn) {
23
- try { fn(); passed++; console.log(` PASS ${name}`); }
24
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
25
- }
26
- async function ta(name, fn) {
27
- try { await fn(); passed++; console.log(` PASS ${name}`); }
28
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
29
- }
30
-
31
- // A throwaway ed25519 public key + the SHA256 fingerprint `ssh-keygen -lf`
32
- // prints for it (verified against ssh-keygen during development). Public keys
33
- // are non-secret; this one's matching private half was discarded.
34
- const FIXTURE_PUB = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOsaRCiI/mJIQy+FFxmIudxnSYYI6spAHH3DFkab4MVU otb@fixture';
35
- const FIXTURE_FP = 'SHA256:LOQ37km6nDckWk1R6EzLqObe4bKdFCe8vY4VFHy+eBY';
36
-
37
- // Minimal fake pg pool: returns queued results in order, records every call.
38
- function fakeDb(results) {
39
- const calls = [];
40
- return {
41
- calls,
42
- async query(sql, params) {
43
- calls.push({ sql, params });
44
- return results.shift() || { rows: [] };
45
- },
46
- };
47
- }
48
-
49
- console.log('validateSshPublicKey — accept:');
50
-
51
- t('valid ed25519 → ok, type, fingerprint matches ssh-keygen, comment dropped from normalized', () => {
52
- const r = o.validateSshPublicKey(FIXTURE_PUB);
53
- assert.equal(r.ok, true);
54
- assert.equal(r.keyType, 'ssh-ed25519');
55
- assert.equal(r.fingerprint, FIXTURE_FP);
56
- // normalized is exactly `<type> <base64>` — the user comment is NOT carried in.
57
- assert.equal(r.normalized, 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOsaRCiI/mJIQy+FFxmIudxnSYYI6spAHH3DFkab4MVU');
58
- assert.equal(r.label, 'otb@fixture');
59
- });
60
-
61
- t('trailing newline (a .pub file) is tolerated', () => {
62
- assert.equal(o.validateSshPublicKey(FIXTURE_PUB + '\n').ok, true);
63
- });
64
-
65
- console.log('validateSshPublicKey — reject (the authorized_keys injection surface):');
66
-
67
- t('embedded newline → multiline_or_control', () => {
68
- assert.equal(o.validateSshPublicKey('ssh-ed25519 AAAA\ncommand="x" ssh-ed25519 BBBB').error, 'multiline_or_control');
69
- });
70
- t('a command="" options PREFIX is refused (line must START with a key type)', () => {
71
- assert.equal(o.validateSshPublicKey('command="rm -rf /" ssh-ed25519 AAAAC3').error, 'unsupported_type');
72
- });
73
- t('unsupported key type → unsupported_type', () => {
74
- assert.equal(o.validateSshPublicKey('ssh-dss AAAAB3').error, 'unsupported_type');
75
- });
76
- t('non-base64 body → bad_base64', () => {
77
- assert.equal(o.validateSshPublicKey('ssh-ed25519 not-base64!!!').error, 'bad_base64');
78
- });
79
- t('valid base64 that is NOT an ssh key → bad_key_blob / type_mismatch', () => {
80
- const r = o.validateSshPublicKey('ssh-ed25519 ' + Buffer.from('hello world').toString('base64'));
81
- assert.equal(r.ok, false);
82
- assert.ok(r.error === 'bad_key_blob' || r.error === 'type_mismatch');
83
- });
84
- t('empty / non-string → rejected', () => {
85
- assert.equal(o.validateSshPublicKey('').error, 'empty');
86
- assert.equal(o.validateSshPublicKey(null).error, 'not_a_string');
87
- assert.equal(o.validateSshPublicKey(42).error, 'not_a_string');
88
- });
89
-
90
- console.log('sanitizeLabel:');
91
-
92
- t('strips quotes/control, collapses, caps length; null on empty', () => {
93
- assert.equal(o.sanitizeLabel('laptop "evil"\nrm'), 'laptop evil rm');
94
- assert.equal(o.sanitizeLabel(' '), null);
95
- assert.equal(o.sanitizeLabel('#$%^&*'), null); // no allowed chars → null
96
- assert.equal(o.sanitizeLabel('@'), '@'); // @ IS an allowed label char
97
- assert.equal(o.sanitizeLabel('x'.repeat(200)).length, 64);
98
- assert.equal(o.sanitizeLabel(undefined), null);
99
- });
100
-
101
- console.log('authorized_keys rendering:');
102
-
103
- t('buildAuthorizedKeysFile: one line per key, trailing newline, empty set → empty string', () => {
104
- assert.equal(o.buildAuthorizedKeysFile([]), '');
105
- const f = o.buildAuthorizedKeysFile([
106
- { public_key: 'ssh-ed25519 AAAA', label: 'laptop' },
107
- { public_key: 'ssh-ed25519 BBBB', label: null },
108
- ]);
109
- assert.equal(f, 'ssh-ed25519 AAAA laptop\nssh-ed25519 BBBB\n');
110
- });
111
-
112
- t('buildAuthorizedKeysLines: a malicious DB label is re-sanitized (defense in depth)', () => {
113
- // Even if a label with a newline somehow reached the DB, the renderer strips
114
- // it — no second authorized_keys line can be smuggled via the comment.
115
- const lines = o.buildAuthorizedKeysLines([{ public_key: 'ssh-ed25519 AAAA', label: 'ok\nssh-ed25519 EVIL' }]);
116
- assert.equal(lines.length, 1);
117
- assert.ok(!lines[0].includes('\n'));
118
- assert.ok(lines[0].startsWith('ssh-ed25519 AAAA '));
119
- });
120
-
121
- t('wrapManagedBlock: markers present; empty keys → empty block (the clawback shape)', () => {
122
- const empty = o.wrapManagedBlock([]);
123
- assert.ok(empty.includes(o.MANAGED_BLOCK_BEGIN) && empty.includes(o.MANAGED_BLOCK_END));
124
- assert.ok(!empty.includes('ssh-'));
125
- const one = o.wrapManagedBlock([{ public_key: 'ssh-ed25519 AAAA', label: 'l' }]);
126
- assert.match(one, /otb-managed[\s\S]*ssh-ed25519 AAAA l[\s\S]*<<< otb-managed/);
127
- });
128
-
129
- console.log('managed-settings:');
130
-
131
- t('buildManagedSettings: shape matches the template; allowlist pins the passed sshZone', () => {
132
- // world + sshZone are supplied by the caller from the branding pack (task 1705);
133
- // the renderer is pure and bakes exactly what it's given.
134
- const s = o.buildManagedSettings({ login: 'alice', hostname: 'box-alice.dev.example.com', world: 'Example', sshZone: 'dev.example.com' });
135
- assert.equal(s.sshConfigs[0].id, 'otb-alice');
136
- assert.equal(s.sshConfigs[0].name, 'Example Dev Box');
137
- assert.equal(s.sshConfigs[0].sshHost, 'root@box-alice.dev.example.com');
138
- assert.equal(s.sshConfigs[0].startDirectory, '~/workspace');
139
- assert.deepEqual(s.sshHostAllowlist, ['*.dev.example.com']);
140
- });
141
-
142
- t('buildManagedSettings: bad login / hostname throw coded errors (no injection into the JSON)', () => {
143
- assert.throws(() => o.buildManagedSettings({ login: 'a/b', hostname: 'x.dev.example.com' }), /BAD_LOGIN|invalid login/);
144
- assert.throws(() => o.buildManagedSettings({ login: 'alice', hostname: 'evil"\nhost' }), /BAD_HOSTNAME|invalid hostname/);
145
- });
146
-
147
- console.log('connect guide:');
148
-
149
- t('renderConnectGuide: bakes the apiBase into the bootstrap URLs', () => {
150
- const g = o.renderConnectGuide({ apiBase: 'https://staging.example.com' });
151
- assert.ok(g.includes('https://staging.example.com/api/gds/box/connect.sh'));
152
- assert.ok(g.includes('/api/gds/box/connect.ps1'));
153
- // the supplied apiBase is baked verbatim (the caller PINS the origin — it is never
154
- // a hardcoded host brand, task 1705)
155
- assert.ok(o.renderConnectGuide({ apiBase: 'https://example.com' }).includes('https://example.com/api/gds/box/connect.sh'));
156
- });
157
-
158
- console.log('DB query shapes (fake pool):');
159
-
160
- await ta('registerSshKey: upserts ON CONFLICT (builder_id, fingerprint)', async () => {
161
- const db = fakeDb([{ rows: [{ id: 7, key_type: 'ssh-ed25519', fingerprint: FIXTURE_FP, label: 'l' }] }]);
162
- const row = await o.registerSshKey(db, 3, { publicKey: 'ssh-ed25519 AAAA', keyType: 'ssh-ed25519', fingerprint: FIXTURE_FP, label: 'l' });
163
- assert.equal(row.id, 7);
164
- assert.match(db.calls[0].sql, /INSERT INTO builder_ssh_keys/);
165
- assert.match(db.calls[0].sql, /ON CONFLICT \(builder_id, fingerprint\)/);
166
- assert.deepEqual(db.calls[0].params, [3, 'ssh-ed25519 AAAA', 'ssh-ed25519', FIXTURE_FP, 'l']);
167
- });
168
-
169
- await ta('deleteSshKey: scoped by builder_id (cannot delete another builder\'s key)', async () => {
170
- const db = fakeDb([{ rows: [{ id: 9 }] }]);
171
- const id = await o.deleteSshKey(db, 3, 9);
172
- assert.equal(id, 9);
173
- assert.match(db.calls[0].sql, /DELETE FROM builder_ssh_keys WHERE id = \$1 AND builder_id = \$2/);
174
- assert.deepEqual(db.calls[0].params, [9, 3]);
175
- });
176
-
177
- await ta('deleteSshKey: returns null when nothing matched', async () => {
178
- const db = fakeDb([{ rows: [] }]);
179
- assert.equal(await o.deleteSshKey(db, 3, 999), null);
180
- });
181
-
182
- await ta('listSshKeys: selects the builder\'s keys', async () => {
183
- const db = fakeDb([{ rows: [{ id: 1 }, { id: 2 }] }]);
184
- const rows = await o.listSshKeys(db, 5);
185
- assert.equal(rows.length, 2);
186
- assert.match(db.calls[0].sql, /FROM builder_ssh_keys\s+WHERE builder_id = \$1/);
187
- assert.deepEqual(db.calls[0].params, [5]);
188
- });
189
-
190
- // ===========================================================================
191
- // Layer 2 — REAL in-process HTTP through the actual box router (no DB).
192
- //
193
- // Same "stub the dependency module" pattern as tests/memory_isolation.mjs:
194
- // monkeypatch auth.requireBuilder BEFORE building the router (middleware is
195
- // captured at build time), stub the DB-touching box-onboard / db / boxes /
196
- // box-access functions (called by property at request time) so no Postgres is
197
- // needed, keep the PURE renderers + decideSourceAccess REAL, then fire real
198
- // requests. This exercises status codes, content-types, body parsing, and the
199
- // live-rank gate end-to-end through the genuine Express handlers.
200
- // ===========================================================================
201
-
202
- console.log('\nHTTP (real router, stubbed DB):');
203
-
204
- const express = require('express');
205
- const auth = require('../src/bongos/auth.js');
206
- const gdsDb = require('../src/bongos/db.js');
207
- const boxes = require('../modules/dev-box/boxes.js');
208
- const boxAccess = require('../modules/dev-box/box-access.js');
209
-
210
- // Mutable per-test fixtures.
211
- let CURRENT_SESSION = { id: 1, github_login: 'alice', rank: 'metic' };
212
- let CURRENT_DB_BUILDER = { rank: 'metic', status: 'active' };
213
-
214
- // Patch the auth middleware BEFORE building the router.
215
- auth.requireBuilder = (req, _res, next) => { req.builder = CURRENT_SESSION; next(); };
216
- auth.requireRank = () => (_req, _res, next) => next();
217
-
218
- // Stub the DB-touching functions (called by property → patching the cached
219
- // module object is enough). Pure renderers + decideSourceAccess stay real.
220
- gdsDb.getBuilderById = async () => CURRENT_DB_BUILDER;
221
- boxes.getBoxByBuilderId = async () => ({ id: 1, builder_id: 1, state: 'active', scope: 'full' });
222
- boxAccess.recordSourceEvent = async () => {};
223
- o.registerSshKey = async (_db, _id, k) => ({ id: 11, key_type: k.keyType, fingerprint: k.fingerprint, label: k.label });
224
- o.listSshKeys = async () => ([{ public_key: 'ssh-ed25519 AAAA', label: 'laptop' }]);
225
- o.touchSshKeysUsed = async () => {};
226
-
227
- // Pin the public origin BEFORE requiring the router (PUBLIC_ORIGIN is a
228
- // module-level const read at require time). This proves the served bootstrap
229
- // bakes the PINNED origin, not a spoofable request header (blocker #2/#5).
230
- process.env.GDS_PUBLIC_ORIGIN = 'https://pinned.example';
231
- const buildBoxRouter = require('../modules/dev-box/routes/box.js');
232
- const app = express();
233
- app.use(express.json());
234
- app.use('/api/gds', buildBoxRouter());
235
-
236
- const server = http.createServer(app);
237
- await new Promise((r) => server.listen(0, r));
238
- const base = `http://127.0.0.1:${server.address().port}`;
239
- const j = (path, opts) => fetch(`${base}${path}`, opts);
240
-
241
- await ta('POST /box/ssh-key: a valid key → 201 ok', async () => {
242
- const r = await j('/api/gds/box/ssh-key', {
243
- method: 'POST', headers: { 'content-type': 'application/json' },
244
- body: JSON.stringify({ public_key: FIXTURE_PUB }),
245
- });
246
- assert.equal(r.status, 201);
247
- const b = await r.json();
248
- assert.equal(b.ok, true);
249
- assert.equal(b.key.fingerprint, FIXTURE_FP);
250
- });
251
-
252
- await ta('POST /box/ssh-key: a bad key → 400 bad_ssh_key (rejected at the door)', async () => {
253
- const r = await j('/api/gds/box/ssh-key', {
254
- method: 'POST', headers: { 'content-type': 'application/json' },
255
- body: JSON.stringify({ public_key: 'command="evil" ssh-ed25519 AAAA' }),
256
- });
257
- assert.equal(r.status, 400);
258
- assert.equal((await r.json()).error, 'bad_ssh_key');
259
- });
260
-
261
- await ta('POST /box/ssh-key: a NEW key past the per-builder cap → 400 too_many_keys', async () => {
262
- const saved = o.listSshKeys;
263
- o.listSshKeys = async () => Array.from({ length: o.MAX_SSH_KEYS_PER_BUILDER },
264
- (_, i) => ({ fingerprint: `SHA256:other${i}`, public_key: 'ssh-ed25519 X', label: null }));
265
- try {
266
- const r = await j('/api/gds/box/ssh-key', {
267
- method: 'POST', headers: { 'content-type': 'application/json' },
268
- body: JSON.stringify({ public_key: FIXTURE_PUB }),
269
- });
270
- assert.equal(r.status, 400);
271
- const b = await r.json();
272
- assert.equal(b.error, 'too_many_keys');
273
- assert.equal(b.limit, o.MAX_SSH_KEYS_PER_BUILDER);
274
- } finally {
275
- o.listSshKeys = saved;
276
- }
277
- });
278
-
279
- await ta('GET /box/authorized-keys: active builder → 200 text/plain with the managed lines', async () => {
280
- CURRENT_DB_BUILDER = { rank: 'metic', status: 'active' };
281
- const r = await j('/api/gds/box/authorized-keys');
282
- assert.equal(r.status, 200);
283
- assert.match(r.headers.get('content-type') || '', /text\/plain/);
284
- assert.equal(await r.text(), 'ssh-ed25519 AAAA laptop\n');
285
- });
286
-
287
- await ta('GET /box/authorized-keys: deactivated builder → 403 BOX_ACCESS_REVOKED (login clawback)', async () => {
288
- CURRENT_DB_BUILDER = { rank: 'metic', status: 'inactive' };
289
- const r = await j('/api/gds/box/authorized-keys');
290
- assert.equal(r.status, 403);
291
- assert.equal((await r.json()).error, 'BOX_ACCESS_REVOKED');
292
- CURRENT_DB_BUILDER = { rank: 'metic', status: 'active' };
293
- });
294
-
295
- await ta('GET /box/managed-settings: → 200 JSON keyed off the session login', async () => {
296
- const r = await j('/api/gds/box/managed-settings');
297
- assert.equal(r.status, 200);
298
- const b = await r.json();
299
- assert.equal(b.sshConfigs[0].id, 'otb-alice');
300
- assert.equal(b.sshConfigs[0].sshHost, 'root@box-alice.dev.example.com');
301
- });
302
-
303
- await ta('GET /box/connect.sh: PUBLIC, no auth → 200; bakes the PINNED origin and IGNORES a spoofed Host (blocker #2/#5)', async () => {
304
- // Send a spoofed X-Forwarded-Host: a pre-fix server would reflect it into the
305
- // script's GDS_API_BASE (token theft / RCE). The fix pins GDS_PUBLIC_ORIGIN.
306
- const r = await j('/api/gds/box/connect.sh', { headers: { 'X-Forwarded-Host': 'evil.attacker.example' } });
307
- assert.equal(r.status, 200);
308
- const body = await r.text();
309
- assert.ok(!body.includes('__GDS_API_BASE__'), 'placeholder must be substituted');
310
- assert.ok(body.includes('https://pinned.example'), 'pinned GDS_PUBLIC_ORIGIN baked in');
311
- assert.ok(!body.includes('evil.attacker.example'), 'a spoofed X-Forwarded-Host must NOT reach the served script');
312
- });
313
-
314
- await new Promise((r) => server.close(r));
315
-
316
- console.log(`\nbox_onboard.mjs: ${passed} passed, ${failed} failed`);
317
- process.exit(failed === 0 ? 0 : 1);
@@ -1,119 +0,0 @@
1
- // tests/box_scope_session.mjs
2
- //
3
- // SECURITY regression guard for #919 (security_reports SR#2): a dev-box session
4
- // (source='box') is DENY-BY-DEFAULT. The box provisioner bakes such a session into
5
- // cloud-init so the box can bootstrap (#818), but the token lives ON the box —
6
- // OUTSIDE the ADR 0016 trust boundary — so it must NOT carry the builder's full
7
- // authority. auth.requireBuilder rejects a 'box' session on every route EXCEPT the
8
- // handful the on-box crons need, which opt in via auth.allowBoxScope (a tiny
9
- // middleware mounted ahead of requireBuilder that sets req.allowBoxScope = true).
10
- //
11
- // The gate is fail-closed: a route WITHOUT allowBoxScope denies a box session, so
12
- // any route added later is denied to boxes automatically. cli/web sessions are
13
- // completely unaffected (the gate only fires when source === 'box').
14
- //
15
- // lookupSession is stubbed on the db module BEFORE auth.js is required, because
16
- // auth.js destructures it at load time. No server/DB/network.
17
- //
18
- // Run: node tests/box_scope_session.mjs
19
-
20
- import { strict as assert } from 'node:assert';
21
- import { createRequire } from 'node:module';
22
-
23
- const require = createRequire(import.meta.url);
24
-
25
- const db = require('../src/bongos/db-kernel.js'); // lookupSession lives in db-kernel now (ADR 0102)
26
- let nextSession = null;
27
- db.lookupSession = async () => nextSession;
28
- const auth = require('../src/bongos/auth.js');
29
-
30
- const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
31
- function fakeRes() {
32
- const res = {
33
- _status: null, _json: null,
34
- status(c) { this._status = c; return this; },
35
- json(b) { this._json = b; return this; },
36
- };
37
- attachFail({}, res, () => {}); // install res.fail(), exactly as the router does (task 1991)
38
- return res;
39
- }
40
-
41
- let pass = 0, fail = 0;
42
- const t = async (name, fn) => {
43
- try { await fn(); pass++; console.log(`PASS ${name}`); }
44
- catch (e) { fail++; console.log(`FAIL ${name}\n ${e.stack || e.message}`); }
45
- };
46
-
47
- // ---- allowBoxScope middleware ------------------------------------------------
48
- // BV1.R99: the opt-in is no longer a bare boolean flag — it DECLARES the box
49
- // scope's own permission allow-list, and requireBuilder checks the declaration
50
- // against the registry. The reachability contract below is unchanged.
51
- await t('allowBoxScope declares the box scope\'s allow-list and calls next()', async () => {
52
- const req = {};
53
- let nexted = false;
54
- auth.allowBoxScope(req, fakeRes(), () => { nexted = true; });
55
- assert.deepEqual([...req.sessionScopeNeeds], [...auth.sessionScopeFor('box').permissions]);
56
- assert.ok(req.sessionScopeNeeds.length > 0, 'an empty declaration would deny every box route');
57
- assert.equal(nexted, true);
58
- });
59
-
60
- // ---- box session DENIED on a route that did NOT opt in -----------------------
61
- await t('box session on a non-box route → 403 box_scope, next() NOT called', async () => {
62
- nextSession = { builder_id: 3, github_login: 'boxbot', rank: 'metic', status: 'active', source: 'box' };
63
- const req = { headers: { authorization: 'Bearer boxtoken' } }; // no req.allowBoxScope
64
- const res = fakeRes();
65
- let nexted = false;
66
- await auth.requireBuilder(req, res, () => { nexted = true; });
67
- assert.equal(nexted, false, 'a box session must not reach a route without allowBoxScope');
68
- assert.equal(res._status, 403);
69
- assert.equal(res._json.error.code, 'box_scope');
70
- });
71
-
72
- // ---- box session ALLOWED when the route opted in -----------------------------
73
- await t('box session + allowBoxScope (middleware ran) → next() called, req.builder set', async () => {
74
- nextSession = { builder_id: 3, github_login: 'boxbot', rank: 'metic', status: 'active', source: 'box' };
75
- // Simulate the real chain: allowBoxScope runs first and flags the request.
76
- const req = { headers: { authorization: 'Bearer boxtoken' } };
77
- auth.allowBoxScope(req, fakeRes(), () => {});
78
- const res = fakeRes();
79
- let nexted = false;
80
- await auth.requireBuilder(req, res, () => { nexted = true; });
81
- assert.equal(nexted, true, 'a box session must pass on an allow-listed route');
82
- assert.equal(res._status, null);
83
- assert.equal(req.builder.rank, 'metic');
84
- });
85
-
86
- // ---- cli / web sessions are unaffected by the box gate -----------------------
87
- await t('cli session on a non-box route → unaffected, next() called', async () => {
88
- nextSession = { builder_id: 1, github_login: 'example-owner', rank: 'archon', status: 'active', source: 'cli' };
89
- const req = { headers: { authorization: 'Bearer clitoken' } }; // no allowBoxScope
90
- const res = fakeRes();
91
- let nexted = false;
92
- await auth.requireBuilder(req, res, () => { nexted = true; });
93
- assert.equal(nexted, true, 'a full cli session is never box-gated');
94
- assert.equal(res._status, null);
95
- });
96
-
97
- await t('web session on a non-box route → unaffected, next() called', async () => {
98
- nextSession = { builder_id: 9, github_login: 'weber', rank: 'metic', status: 'active', source: 'web' };
99
- const req = { headers: { cookie: 'gds_session=webtoken' } };
100
- const res = fakeRes();
101
- let nexted = false;
102
- await auth.requireBuilder(req, res, () => { nexted = true; });
103
- assert.equal(nexted, true);
104
- assert.equal(res._status, null);
105
- });
106
-
107
- // ---- a legacy session with no source field is NOT treated as a box -----------
108
- await t('session with no source field → not box-gated (only source==="box" fires)', async () => {
109
- nextSession = { builder_id: 2, github_login: 'legacy', rank: 'metic', status: 'active' }; // no source
110
- const req = { headers: { authorization: 'Bearer legacytoken' } };
111
- const res = fakeRes();
112
- let nexted = false;
113
- await auth.requireBuilder(req, res, () => { nexted = true; });
114
- assert.equal(nexted, true, 'absent source must never trip the box gate');
115
- assert.equal(res._status, null);
116
- });
117
-
118
- console.log(`\n${pass} passed, ${fail} failed`);
119
- process.exit(fail === 0 ? 0 : 1);
@@ -1,119 +0,0 @@
1
- // tests/box_sweep_docs.mjs — the box sweep docs must not invert against box.js
2
- // a third time (task 1003823).
3
- //
4
- // HISTORY, because it is the whole reason this file exists. The idle sweep has
5
- // changed direction twice:
6
- //
7
- // task 804 — sweep-idle went from park to a direct deprovision, and
8
- // task 1001135 dragged four doc surfaces along with it.
9
- // task 1002726 — the CODE went BACK to park-and-keep-the-snapshot, because
10
- // annihilating an idle box took the builder's uncommitted work
11
- // with it. The four doc surfaces were NOT walked back.
12
- //
13
- // So for months the routine's own SKILL.md told an agent "your single job is to
14
- // DESTROY" while cmdSweepIdle called parkOne. The danger is specific and worth
15
- // naming: an agent that believes it is supposed to destroy, and finds the sweep
16
- // merely parking, is being invited to reach for `box.js deprovision` to finish a
17
- // job it was never given — which is the one path that can silently discard a
18
- // builder's work.
19
- //
20
- // This guard is therefore NOT a spell-check. It asserts the one relationship
21
- // that matters: WHILE cmdSweepIdle parks, the docs may not say it destroys.
22
- // If a future task legitimately moves the idle path back to a destroy, this test
23
- // goes green on its own as soon as the code changes — it is pinned to the code,
24
- // not to a preferred wording.
25
-
26
- import assert from 'node:assert/strict';
27
- import fs from 'node:fs';
28
- import path from 'node:path';
29
- import { test } from 'node:test';
30
- import { fileURLToPath } from 'node:url';
31
-
32
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
- const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
34
-
35
- const BOX_JS = 'scripts/gds/box.js';
36
- const IDLE_SKILL = '.claude/scheduled-tasks/box-idle-suspend/SKILL.md';
37
- const DORMANT_SKILL = '.claude/scheduled-tasks/box-dormant-reclaim/SKILL.md';
38
- // The fourth surface this guard used to read, docs/recipes/builder-box-lifecycle.md,
39
- // was deleted with the other three published dev-box recipes (task 1004013,
40
- // ADR 0305: a recipe lives with what it documents). A file that does not exist
41
- // cannot invert against box.js, so it drops out of the lists below rather than
42
- // weakening them — the two SKILL.md surfaces are still read in full.
43
-
44
- // Comments are stripped before scanning prose: each surface deliberately keeps a
45
- // sentence SAYING which model it used to describe, and a naive substring check
46
- // would read that history as the claim still being made.
47
- const stripHtmlComments = (md) => md.replace(/<!--[\s\S]*?-->/g, '');
48
-
49
- // The body of cmdSweepIdle, so "what the sweep does" is read from the function
50
- // itself rather than from a header comment that can rot the same way the docs did.
51
- function cmdSweepIdleBody() {
52
- const src = read(BOX_JS);
53
- const start = src.indexOf('async function cmdSweepIdle(');
54
- assert.ok(start > 0, `cmdSweepIdle not found in ${BOX_JS} — this guard is reading the wrong file`);
55
- const next = src.indexOf('\nasync function ', start + 1);
56
- return src.slice(start, next > 0 ? next : src.length);
57
- }
58
-
59
- test('the idle sweep parks — that is the fact the docs are measured against', () => {
60
- const body = cmdSweepIdleBody();
61
- assert.match(body, /parkOne\(/, 'cmdSweepIdle no longer calls parkOne');
62
- assert.doesNotMatch(body, /deprovisionOne\(/,
63
- 'cmdSweepIdle calls deprovisionOne — the idle path destroys again. That is allowed, '
64
- + 'but the docs below must be rewritten to match BEFORE this guard is relaxed.');
65
- });
66
-
67
- test('no box-sweep doc claims the IDLE sweep destroys, while the code parks', () => {
68
- // Sentences that assert a destroy as the idle sweep's OUTCOME. Each pattern is
69
- // anchored on the idle path specifically, because "destroy the droplet" is a
70
- // true statement about a park (the droplet does go; the snapshot stays) and
71
- // "destroy the snapshot" is a true statement about sweep-dormant.
72
- const lies = [
73
- /idle[- ]deprovision/i,
74
- /idle box is fully DESTROYED/i,
75
- /destroy the droplet \*\*AND\*\* (?:its )?snapshot/i,
76
- /droplet \+ snapshot destroyed/i,
77
- /idle no longer feeds it/i,
78
- /no longer parks/i,
79
- /there is no snapshot to wake from/i,
80
- /single job is to DESTROY/i,
81
- ];
82
-
83
- for (const file of [IDLE_SKILL, DORMANT_SKILL]) {
84
- const prose = stripHtmlComments(read(file));
85
- for (const lie of lies) {
86
- assert.doesNotMatch(prose, lie,
87
- `${file} still describes the pre-task-1002726 destroy model (${lie}). `
88
- + 'cmdSweepIdle calls parkOne and keeps the snapshot — see task 1003823.');
89
- }
90
- }
91
- });
92
-
93
- test('the idle skill quotes the strings the worker actually prints', () => {
94
- // Step 3 of the skill orders a VERBATIM relay, so a drifted string here makes
95
- // the agent either fabricate a line or stall. These are the three the worker emits.
96
- const skill = read(IDLE_SKILL);
97
- const body = cmdSweepIdleBody();
98
-
99
- for (const s of ['would park', 'snapshot kept, compute stopped', 'done — parked=']) {
100
- assert.ok(body.includes(s), `cmdSweepIdle no longer prints ${JSON.stringify(s)}`);
101
- assert.ok(skill.includes(s),
102
- `${IDLE_SKILL} does not quote ${JSON.stringify(s)}, which the worker prints and `
103
- + 'step 3 tells the agent to relay verbatim.');
104
- }
105
-
106
- assert.ok(!/done — deprovisioned=/.test(skill),
107
- `${IDLE_SKILL} tells the agent to relay "done — deprovisioned=", a line the worker never emits.`);
108
- });
109
-
110
- test('every surface names sweep-dormant as the only automated snapshot delete', () => {
111
- // The safety property box.js implements in prose: full reclaim warns first, so
112
- // no automated path takes uncommitted work silently. A reader who misses this
113
- // is the reader who reaches for deprovision.
114
- for (const file of [IDLE_SKILL, DORMANT_SKILL]) {
115
- assert.match(stripHtmlComments(read(file)), /only automated path that destroys a snapshot|only automated snapshot delete/i,
116
- `${file} never tells the reader that sweep-dormant is the only automated path that `
117
- + 'destroys a snapshot (and that it warns first) — the safety property this subsystem rests on.');
118
- }
119
- });
@@ -1,55 +0,0 @@
1
- // tests/box_sync.mjs
2
- //
3
- // BV1.R96 (goal 1000051) — "sync now": box-sync.js locates the box source-fetch
4
- // script (so re-running it re-scopes the box to the active claim) and degrades
5
- // cleanly off a box. Pure + hermetic: resolveFetchScript threads {env, repoRoot}
6
- // to the shared resolveBoxInfraDir (ADR 0150 / R98); temp dirs stand in for a real
7
- // box infra/ tree. NO DB, NO network, NO box.
8
- //
9
- // Run: node tests/box_sync.mjs
10
-
11
- import { strict as assert } from 'node:assert';
12
- import { createRequire } from 'node:module';
13
- import fs from 'node:fs';
14
- import os from 'node:os';
15
- import path from 'node:path';
16
- import { makeRunner } from './helpers.mjs';
17
-
18
- const require = createRequire(import.meta.url);
19
- const boxSync = require('../scripts/gds/box-sync.js');
20
-
21
- const { test, summary } = makeRunner();
22
-
23
- const tmps = [];
24
- function makeInfra({ template = true, fetch = true } = {}) {
25
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'boxsync-'));
26
- tmps.push(root);
27
- const infra = path.join(root, 'infra');
28
- fs.mkdirSync(infra, { recursive: true });
29
- // resolveBoxInfraDir validates an infra/ by the presence of the cloud-init template.
30
- if (template) fs.writeFileSync(path.join(infra, 'builder-box-cloud-init.yaml'), '#cloud-config\n');
31
- if (fetch) fs.writeFileSync(path.join(infra, boxSync.FETCH_SCRIPT), '#!/usr/bin/env bash\necho fetch\n');
32
- return { root, infra };
33
- }
34
-
35
- await test('resolves the box source-fetch script when it is present on the box', () => {
36
- const { infra } = makeInfra();
37
- const got = boxSync.resolveFetchScript({ env: { CLOUDBONGOS_BOX_INFRA_DIR: infra }, repoRoot: '/nope' });
38
- assert.equal(got, path.join(infra, boxSync.FETCH_SCRIPT));
39
- });
40
-
41
- await test('→ null when the infra/ has no source-fetch script (template present, fetch absent)', () => {
42
- const { infra } = makeInfra({ fetch: false });
43
- const got = boxSync.resolveFetchScript({ env: { CLOUDBONGOS_BOX_INFRA_DIR: infra }, repoRoot: '/nope' });
44
- assert.equal(got, null);
45
- });
46
-
47
- await test('→ null when there is no infra/ at all (i.e. NOT on a box)', () => {
48
- const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'boxsync-bare-'));
49
- tmps.push(bare);
50
- const got = boxSync.resolveFetchScript({ env: {}, repoRoot: path.join(bare, 'node_modules', '@bongos', 'core') });
51
- assert.equal(got, null);
52
- });
53
-
54
- for (const d of tmps) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
55
- summary();