@bongos/core 1.18.19 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.bongos-core.json +514 -194
  2. package/.claude/skills/builder-box/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +1 -1
  4. package/.claude/skills/goal-create/SKILL.md +1 -1
  5. package/.claude/skills/idea-triage/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +5 -5
  7. package/.claude/skills/scan-before-install/SKILL.md +165 -0
  8. package/clients/bongos-client/README.md +1 -1
  9. package/clients/bongos-client/bongos-client.global.js +63 -41
  10. package/clients/bongos-client/index.cjs +63 -41
  11. package/clients/bongos-client/index.d.ts +78 -44
  12. package/clients/bongos-client/index.mjs +63 -41
  13. package/docs/adr/0020-security-model-public-stub.md +1 -1
  14. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
  15. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  16. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
  17. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
  18. package/docs/adr/0156-user-set-display-name-wins.md +62 -0
  19. package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
  20. package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
  21. package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
  22. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
  23. package/docs/adr/README.md +8 -0
  24. package/docs/api/openapi.json +1209 -233
  25. package/docs/api-reference.md +61 -46
  26. package/docs/architecture.md +1 -1
  27. package/docs/canonical-permissions.md +61 -16
  28. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  29. package/docs/file-map.md +4 -1
  30. package/docs/onboarding/diagrams/assertions.json +3 -3
  31. package/docs/recipes/core-release-pipeline.md +179 -0
  32. package/docs/recipes/packaging-the-core.md +2 -0
  33. package/docs/recipes/parallel-goal-waves.md +1 -3
  34. package/docs/recipes/private-npm-distribution.md +2 -0
  35. package/docs/recipes/upgrading-the-core.md +2 -0
  36. package/migrations/core_197_display_name_source.sql +56 -0
  37. package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
  38. package/modules/autonomy/routes/autonomy.js +1 -1
  39. package/modules/builder-settings/db.js +7 -1
  40. package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
  41. package/modules/dev-box/routes/box.js +11 -9
  42. package/modules/discord/auth.js +3 -1
  43. package/modules/discord/routes/discord.js +8 -2
  44. package/modules/discord/ship-broadcast.js +48 -1
  45. package/modules/governance/catalog.js +70 -20
  46. package/modules/governance/db.js +102 -0
  47. package/modules/governance/migrations/governance_006_version_create.sql +38 -0
  48. package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
  49. package/modules/governance/routes/governance.js +235 -6
  50. package/modules/grading/grader-workers/index.js +18 -9
  51. package/modules/hall-ui/public/gate.js +6 -2
  52. package/modules/hall-ui/public/goals-page.js +6 -2
  53. package/modules/hall-ui/public/governance.css +100 -0
  54. package/modules/hall-ui/public/governance.html +77 -0
  55. package/modules/hall-ui/public/governance.js +353 -0
  56. package/modules/hall-ui/public/harbor.js +14 -8
  57. package/modules/hall-ui/public/instances-panel.js +3 -1
  58. package/modules/hall-ui/public/new-project-wizard.js +6 -0
  59. package/modules/hall-ui/public/people.html +51 -0
  60. package/modules/hall-ui/public/people.js +100 -0
  61. package/modules/hall-ui/public/profile.html +2 -1
  62. package/modules/hall-ui/public/profile.js +9 -3
  63. package/modules/hall-ui/public/sessions.js +4 -2
  64. package/modules/hall-ui/public/shell.js +28 -7
  65. package/modules/hall-ui/public/task-detail.js +6 -3
  66. package/modules/hall-ui/public/watch.js +17 -9
  67. package/modules/ideas/blockers.js +44 -7
  68. package/modules/ideas/routes/blockers.js +12 -7
  69. package/modules/ideas/routes/inbox.js +2 -2
  70. package/modules/lifecycle/db.js +84 -17
  71. package/modules/lifecycle/github-push.js +37 -10
  72. package/modules/lifecycle/publish-reconciler.js +128 -31
  73. package/modules/lifecycle/routes/claims.js +10 -4
  74. package/modules/lifecycle/routes/dependencies.js +2 -2
  75. package/modules/lifecycle/routes/done-when.js +4 -4
  76. package/modules/lifecycle/routes/gate-approvals.js +9 -3
  77. package/modules/lifecycle/routes/goals.js +5 -5
  78. package/modules/lifecycle/routes/tasks.js +16 -16
  79. package/modules/lifecycle/routes/versions.js +13 -2
  80. package/modules/lifecycle/ship-card.js +38 -7
  81. package/modules/memory/routes/memory.js +2 -2
  82. package/modules/onboarding/routes/access-requests.js +3 -3
  83. package/modules/platform-identity/routes/projects.js +10 -6
  84. package/modules/platform-identity/routes/scouting.js +2 -2
  85. package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
  86. package/modules/provisioning/provisioning.js +24 -6
  87. package/modules/provisioning/routes/provisioning.js +24 -6
  88. package/modules/security/artifact-scan/aggregate.js +133 -0
  89. package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
  90. package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
  91. package/modules/security/artifact-scan/detectors/credentials.js +112 -0
  92. package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
  93. package/modules/security/artifact-scan/detectors/hooks.js +116 -0
  94. package/modules/security/artifact-scan/detectors/injection.js +118 -0
  95. package/modules/security/artifact-scan/detectors/mcp.js +102 -0
  96. package/modules/security/artifact-scan/detectors/network.js +67 -0
  97. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
  98. package/modules/security/artifact-scan/detectors/permissions.js +91 -0
  99. package/modules/security/artifact-scan/detectors/shared.js +219 -0
  100. package/modules/security/artifact-scan/fence-local.js +99 -0
  101. package/modules/security/artifact-scan/fetch.js +330 -0
  102. package/modules/security/artifact-scan/floor.js +319 -0
  103. package/modules/security/artifact-scan/limits.js +66 -0
  104. package/modules/security/artifact-scan/manifest.js +203 -0
  105. package/modules/security/artifact-scan/mirror.js +158 -0
  106. package/modules/security/artifact-scan/quarantine.js +149 -0
  107. package/modules/security/artifact-scan/rebuild-spec.js +120 -0
  108. package/modules/security/artifact-scan/report.js +155 -0
  109. package/modules/security/artifact-scan/sanitize.js +317 -0
  110. package/modules/security/migrations/security_001_tool_scans.sql +145 -0
  111. package/modules/security/module.json +2 -2
  112. package/modules/security/routes/reports.js +11 -7
  113. package/modules/security/routes/scans.js +115 -0
  114. package/modules/security/scan-store.js +214 -0
  115. package/modules/security/tests/aggregate.mjs +283 -0
  116. package/modules/security/tests/artifact_scan.mjs +591 -0
  117. package/modules/security/tests/floor.mjs +549 -0
  118. package/modules/security/tests/mirror.mjs +350 -0
  119. package/modules/security/tests/sanitize_rebuild.mjs +499 -0
  120. package/modules/security/tests/scan_store.mjs +250 -0
  121. package/modules/security/tests/tool_scans_migration.mjs +116 -0
  122. package/modules/sessions/routes/sessions.js +6 -5
  123. package/package-lock.json +2 -2
  124. package/package.json +1 -1
  125. package/public-landing/create.html +13 -3
  126. package/scripts/gds/abandon.js +1 -1
  127. package/scripts/gds/audit-authorship.sh +6 -0
  128. package/scripts/gds/box.js +1 -1
  129. package/scripts/gds/claim.js +15 -0
  130. package/scripts/gds/cli-lib.js +16 -4
  131. package/scripts/gds/gating.js +28 -13
  132. package/scripts/gds/gen-api-docs.js +1 -1
  133. package/scripts/gds/goal.js +234 -0
  134. package/scripts/gds/init.js +70 -15
  135. package/scripts/gds/main-audit.js +80 -11
  136. package/scripts/gds/mirror-redact.js +6 -0
  137. package/scripts/gds/newcomer-floor.js +2 -2
  138. package/scripts/gds/provision.js +230 -94
  139. package/scripts/gds/run-unit-tests.js +9 -1
  140. package/scripts/gds/sandbox-stage.js +66 -9
  141. package/scripts/gds/search-ingest-md.js +1 -0
  142. package/scripts/gds/sequence.js +67 -8
  143. package/scripts/gds/ship.js +183 -50
  144. package/scripts/hall-preview/server.js +1 -1
  145. package/scripts/migrate.sh +5 -5
  146. package/src/bongos/auth.js +129 -27
  147. package/src/bongos/db-kernel.js +66 -5
  148. package/src/bongos/db.js +6 -7
  149. package/src/bongos/github-repos.js +19 -7
  150. package/src/bongos/goal-scope-check.js +1 -1
  151. package/src/bongos/module-scope-map.js +3 -1
  152. package/src/bongos/permission-path-check.js +1 -1
  153. package/src/bongos/route-rank-check.js +73 -30
  154. package/src/bongos/routes/audit-log.js +4 -2
  155. package/src/bongos/routes/auth.js +8 -4
  156. package/src/bongos/routes/backup.js +7 -4
  157. package/src/bongos/routes/builders.js +23 -11
  158. package/src/bongos/routes/github.js +15 -4
  159. package/src/bongos/routes/llm-cache.js +5 -2
  160. package/src/bongos/routes/modules.js +9 -5
  161. package/src/bongos/routes/security.js +21 -12
  162. package/src/bongos/serve-internal.js +22 -7
  163. package/src/module-api.js +86 -13
  164. package/src/platform-server.js +14 -0
  165. package/tests/api_client.mjs +18 -2
  166. package/tests/api_path_404.mjs +3 -0
  167. package/tests/art_stage.mjs +2 -1
  168. package/tests/atlas.mjs +32 -4
  169. package/tests/auth_admission_gate.mjs +17 -5
  170. package/tests/auth_error_brand.mjs +41 -21
  171. package/tests/auth_resolve_failure.mjs +155 -0
  172. package/tests/blocker_link.mjs +88 -0
  173. package/tests/bongos_downloads.mjs +2 -1
  174. package/tests/box_dns_repoint.mjs +2 -0
  175. package/tests/box_ship_permissions.mjs +2 -0
  176. package/tests/box_terminal_ssrf.mjs +23 -17
  177. package/tests/claim_error_surface.mjs +23 -0
  178. package/tests/claude_materialize.mjs +4 -0
  179. package/tests/cli_box_scope.mjs +40 -0
  180. package/tests/cloud_bongos_pack.mjs +34 -4
  181. package/tests/conductor.mjs +2 -1
  182. package/tests/criterion_authoring_gate.mjs +28 -5
  183. package/tests/currency_label.mjs +10 -1
  184. package/tests/deploy_instance.mjs +2 -0
  185. package/tests/devbox_downloads.mjs +2 -1
  186. package/tests/display_name.mjs +95 -6
  187. package/tests/eviction_policy.mjs +2 -1
  188. package/tests/file_map.mjs +10 -0
  189. package/tests/first_admin_bootstrap.mjs +10 -2
  190. package/tests/first_run_engine_check.mjs +2 -1
  191. package/tests/fitness.mjs +26 -6
  192. package/tests/gate_approvals.mjs +15 -2
  193. package/tests/github_push_land_proof.mjs +50 -4
  194. package/tests/github_repos_helpers.mjs +30 -5
  195. package/tests/github_token_store.mjs +23 -0
  196. package/tests/goal_abuse_matrix.mjs +13 -3
  197. package/tests/goal_cli.mjs +89 -0
  198. package/tests/governance_abuse_matrix.mjs +386 -0
  199. package/tests/governance_fold.mjs +156 -0
  200. package/tests/governance_fold_bench.mjs +50 -0
  201. package/tests/governance_live_proof.mjs +201 -0
  202. package/tests/governance_page_gates.mjs +21 -9
  203. package/tests/governance_parity.mjs +11 -2
  204. package/tests/governance_require_permission.mjs +125 -1
  205. package/tests/governance_routes.mjs +334 -0
  206. package/tests/grader.mjs +7 -3
  207. package/tests/hall_nav.mjs +14 -4
  208. package/tests/helpers.mjs +73 -0
  209. package/tests/hook_core_resolve.mjs +2 -1
  210. package/tests/host_topology_skips.mjs +97 -0
  211. package/tests/infra_exec_paths.mjs +14 -4
  212. package/tests/init.mjs +128 -0
  213. package/tests/known_failing_ratchet.mjs +89 -0
  214. package/tests/lifecycle-card.mjs +2 -1
  215. package/tests/main_audit.mjs +72 -1
  216. package/tests/memory_isolation.mjs +56 -9
  217. package/tests/migrate_root_resolution.mjs +1 -1
  218. package/tests/mirror_redact.mjs +19 -0
  219. package/tests/module_api.mjs +3 -1
  220. package/tests/module_loader.mjs +4 -0
  221. package/tests/modules.mjs +35 -7
  222. package/tests/modules_route.mjs +29 -5
  223. package/tests/onboarding_observability.mjs +49 -11
  224. package/tests/onboarding_route_signals.mjs +1 -0
  225. package/tests/one_active_claim_per_session.mjs +80 -0
  226. package/tests/pre_compact.mjs +2 -1
  227. package/tests/preview_server.mjs +2 -1
  228. package/tests/profile_route.mjs +1 -0
  229. package/tests/provision.mjs +170 -0
  230. package/tests/provisioning_onboard_mode.mjs +180 -0
  231. package/tests/publish_manifest.mjs +16 -3
  232. package/tests/publish_reconciler.mjs +181 -10
  233. package/tests/rank_tier_single_source.mjs +205 -0
  234. package/tests/recall_hints.mjs +2 -1
  235. package/tests/repo_map.mjs +4 -0
  236. package/tests/route_permission_check.mjs +14 -5
  237. package/tests/rubric_fixtures.mjs +3 -0
  238. package/tests/run_routine_two_root.mjs +2 -0
  239. package/tests/sandbox_stage.mjs +69 -7
  240. package/tests/search_ingest.mjs +7 -0
  241. package/tests/security_docs.mjs +28 -6
  242. package/tests/sequence.mjs +64 -0
  243. package/tests/session-cards.mjs +2 -1
  244. package/tests/session_start_freshness.mjs +2 -1
  245. package/tests/ship_broadcast_server.mjs +63 -1
  246. package/tests/ship_card.mjs +66 -0
  247. package/tests/ship_ci_deploy.mjs +66 -1
  248. package/tests/ship_smoke_gate.mjs +76 -0
  249. package/tests/versions_route.mjs +15 -0
  250. package/tests/workflow_exec_paths.mjs +16 -4
  251. package/tests/worktree_guard.mjs +2 -1
@@ -0,0 +1,250 @@
1
+ // modules/security/tests/scan_store.mjs
2
+ //
3
+ // Task 1002547 (goal 1000055) — the scan-record store's PURE validation seam.
4
+ // The security contract under test:
5
+ //
6
+ // * STRUCTURE ONLY, NEVER CONTENT — a findings_summary is a list of one-line,
7
+ // length-capped, known-keys-only claims; anything shaped like quoted target
8
+ // content (multi-line, fence-marked, unknown keys, oversized) is rejected,
9
+ // and every string is scrubbed of dangerous codepoints before it can land;
10
+ // * an ABORTED scan is recordable, and the verdict/completion coupling is
11
+ // enforced before the DB ever sees the row (a dead scan must never read as
12
+ // a passing one);
13
+ // * target_host is DERIVED from the validated URL, never trusted from the
14
+ // caller.
15
+ //
16
+ // DB-free — only the pure normalize/validate seam is exercised, so this runs
17
+ // in `node scripts/gds/run-unit-tests.js`.
18
+ // Run directly: node modules/security/tests/scan_store.mjs
19
+
20
+ import { strict as assert } from 'node:assert';
21
+ import { createRequire } from 'node:module';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const store = require('../scan-store.js');
28
+ const { normalizeScanPayload, SUMMARY_LIMITS } = store;
29
+ const { FENCE_MARKER } = require('../artifact-scan/fence-local.js');
30
+
31
+ let passed = 0;
32
+ let failed = 0;
33
+ function test(name, fn) {
34
+ try { fn(); passed++; console.log(` ok ${name}`); }
35
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
36
+ }
37
+
38
+ function goodPayload(over = {}) {
39
+ return {
40
+ target_url: 'https://github.com/example/repo',
41
+ artifact_kind: 'repo',
42
+ resolved_sha: 'a'.repeat(40),
43
+ content_hash: 'b'.repeat(64),
44
+ file_count: 12,
45
+ total_bytes: 34567,
46
+ tier: 'default',
47
+ verdict: 'caution',
48
+ completion: 'complete',
49
+ findings_critical: 0,
50
+ findings_high: 1,
51
+ findings_medium: 2,
52
+ findings_low: 0,
53
+ findings_summary: [
54
+ { code: 'net_sink', severity: 'high', file: 'src/post.js', message: 'same-file read-then-send co-occurrence' },
55
+ ],
56
+ engine_version: 'scan-engine/0.1.0',
57
+ action_taken: 'none',
58
+ };
59
+ }
60
+
61
+ // ---------- the happy path ----------
62
+
63
+ test('a complete scan payload normalizes, with target_host derived from the URL', () => {
64
+ const r = normalizeScanPayload(goodPayload());
65
+ assert.equal(r.ok, true, JSON.stringify(r));
66
+ assert.equal(r.value.target_host, 'github.com');
67
+ assert.equal(r.value.verdict, 'caution');
68
+ });
69
+
70
+ test('a caller-supplied target_host is ignored — the host comes from the validated URL only', () => {
71
+ const r = normalizeScanPayload({ ...goodPayload(), target_host: 'trusted-looking.example' });
72
+ assert.equal(r.ok, true, JSON.stringify(r));
73
+ assert.equal(r.value.target_host, 'github.com');
74
+ });
75
+
76
+ // ---------- an aborted scan is recordable; the coupling is enforced ----------
77
+
78
+ test('an aborted scan with no verdict is recordable', () => {
79
+ const p = goodPayload();
80
+ delete p.verdict;
81
+ p.completion = 'aborted';
82
+ p.abort_code = 'clone_timeout';
83
+ const r = normalizeScanPayload(p);
84
+ assert.equal(r.ok, true, JSON.stringify(r));
85
+ assert.equal(r.value.verdict, null);
86
+ assert.equal(r.value.abort_code, 'clone_timeout');
87
+ });
88
+
89
+ test('a complete scan WITHOUT a verdict is rejected — silence must never read as passing', () => {
90
+ const p = goodPayload();
91
+ delete p.verdict;
92
+ const r = normalizeScanPayload(p);
93
+ assert.equal(r.ok, false);
94
+ assert.equal(r.code, 'verdict_completion_mismatch');
95
+ });
96
+
97
+ test('an aborted scan WITH a verdict is rejected — a dead scan cannot carry a conclusion', () => {
98
+ const p = goodPayload();
99
+ p.completion = 'aborted';
100
+ const r = normalizeScanPayload(p);
101
+ assert.equal(r.ok, false);
102
+ assert.equal(r.code, 'verdict_completion_mismatch');
103
+ });
104
+
105
+ test('an incomplete scan follows the same coupling: no verdict OK, with verdict rejected', () => {
106
+ const base = goodPayload();
107
+ delete base.verdict;
108
+ const ok = normalizeScanPayload({ ...base, completion: 'incomplete' });
109
+ assert.equal(ok.ok, true, JSON.stringify(ok));
110
+ assert.equal(ok.value.verdict, null);
111
+ const bad = normalizeScanPayload({ ...goodPayload(), completion: 'incomplete' });
112
+ assert.equal(bad.ok, false);
113
+ assert.equal(bad.code, 'verdict_completion_mismatch');
114
+ });
115
+
116
+ // ---------- identity fields ----------
117
+
118
+ test('a URL carrying embedded credentials is REJECTED — a token must never land in an all-ranks row', () => {
119
+ const r = normalizeScanPayload({ ...goodPayload(), target_url: 'https://ghp_secret123@github.com/org/private' });
120
+ assert.equal(r.ok, false);
121
+ assert.equal(r.code, 'bad_target_url');
122
+ });
123
+
124
+ test('the stored target_url is the PARSED serialization, never the raw caller string', () => {
125
+ const r = normalizeScanPayload({ ...goodPayload(), target_url: 'https://github.com/Example/Repo' });
126
+ assert.equal(r.ok, true, JSON.stringify(r));
127
+ assert.equal(r.value.target_url, new URL('https://github.com/Example/Repo').href);
128
+ });
129
+
130
+ test('a non-https target URL is rejected', () => {
131
+ const r = normalizeScanPayload({ ...goodPayload(), target_url: 'http://github.com/x/y' });
132
+ assert.equal(r.ok, false);
133
+ assert.equal(r.code, 'bad_target_url');
134
+ });
135
+
136
+ test('a malformed content_hash is rejected', () => {
137
+ const r = normalizeScanPayload({ ...goodPayload(), content_hash: 'not-hex' });
138
+ assert.equal(r.ok, false);
139
+ assert.equal(r.code, 'bad_content_hash');
140
+ });
141
+
142
+ test('a malformed resolved_sha is rejected', () => {
143
+ const r = normalizeScanPayload({ ...goodPayload(), resolved_sha: 'main' });
144
+ assert.equal(r.ok, false);
145
+ assert.equal(r.code, 'bad_resolved_sha');
146
+ });
147
+
148
+ test('bad enum values are rejected: tier, verdict, kind, action', () => {
149
+ for (const [field, value] of [
150
+ ['tier', 'ultra'], ['verdict', 'fine'], ['artifact_kind', 'zip'], ['action_taken', 'installed'],
151
+ ]) {
152
+ const r = normalizeScanPayload({ ...goodPayload(), [field]: value });
153
+ assert.equal(r.ok, false, `${field}=${value} must be rejected`);
154
+ }
155
+ });
156
+
157
+ // ---------- findings_summary: structure only, never content ----------
158
+
159
+ test('a multi-line summary message is rejected — one-LINE claims only', () => {
160
+ const p = goodPayload();
161
+ p.findings_summary = [{ code: 'x', severity: 'low', file: 'a.js', message: 'line one\nline two of quoted payload' }];
162
+ const r = normalizeScanPayload(p);
163
+ assert.equal(r.ok, false);
164
+ assert.equal(r.code, 'summary_not_one_line');
165
+ });
166
+
167
+ test('an unknown summary item key is rejected — no side channel for quoted content', () => {
168
+ const p = goodPayload();
169
+ p.findings_summary = [{ code: 'x', severity: 'low', file: 'a.js', message: 'ok', excerpt: 'quoted target bytes' }];
170
+ const r = normalizeScanPayload(p);
171
+ assert.equal(r.ok, false);
172
+ assert.equal(r.code, 'summary_unknown_key');
173
+ });
174
+
175
+ test('a summary string carrying the mirror fence marker is rejected as quoted content', () => {
176
+ const p = goodPayload();
177
+ p.findings_summary = [{ code: 'x', severity: 'low', file: 'a.js', message: `saw ${FENCE_MARKER} here` }];
178
+ const r = normalizeScanPayload(p);
179
+ assert.equal(r.ok, false);
180
+ assert.equal(r.code, 'summary_quoted_content');
181
+ });
182
+
183
+ test('dangerous codepoints are scrubbed out of summary strings before they can land', () => {
184
+ const p = goodPayload();
185
+ p.findings_summary = [{ code: 'x', severity: 'low', file: 'a.js', message: `igno${String.fromCodePoint(0xfeff)}re this` }];
186
+ const r = normalizeScanPayload(p);
187
+ assert.equal(r.ok, true, JSON.stringify(r));
188
+ assert.equal(r.value.findings_summary[0].message, 'ignore this');
189
+ });
190
+
191
+ test('too many summary items are rejected', () => {
192
+ const p = goodPayload();
193
+ p.findings_summary = Array.from({ length: SUMMARY_LIMITS.maxItems + 1 }, (_, i) => ({
194
+ code: 'x', severity: 'low', file: `f${i}.js`, message: 'm',
195
+ }));
196
+ const r = normalizeScanPayload(p);
197
+ assert.equal(r.ok, false);
198
+ assert.equal(r.code, 'summary_too_many_items');
199
+ });
200
+
201
+ test('an over-long summary message is rejected', () => {
202
+ const p = goodPayload();
203
+ p.findings_summary = [{ code: 'x', severity: 'low', file: 'a.js', message: 'm'.repeat(SUMMARY_LIMITS.maxMessageChars + 1) }];
204
+ const r = normalizeScanPayload(p);
205
+ assert.equal(r.ok, false);
206
+ assert.equal(r.code, 'summary_string_too_long');
207
+ });
208
+
209
+ test('a summary item severity outside the four levels is rejected', () => {
210
+ const p = goodPayload();
211
+ p.findings_summary = [{ code: 'x', severity: 'catastrophic', file: 'a.js', message: 'm' }];
212
+ const r = normalizeScanPayload(p);
213
+ assert.equal(r.ok, false);
214
+ assert.equal(r.code, 'summary_bad_severity');
215
+ });
216
+
217
+ test('negative findings counts are rejected', () => {
218
+ const r = normalizeScanPayload({ ...goodPayload(), findings_high: -1 });
219
+ assert.equal(r.ok, false);
220
+ assert.equal(r.code, 'bad_counts');
221
+ });
222
+
223
+ // ---------- the route file: rank gates stated where fitness can see them ----------
224
+
225
+ const here = path.dirname(fileURLToPath(import.meta.url));
226
+ const routeSrc = fs.readFileSync(path.join(here, '..', 'routes', 'scans.js'), 'utf8');
227
+
228
+ test("the POST is gated api.requireRank('metic', 'archon') — a verdict-shaped write is a trust-shaped write", () => {
229
+ assert.match(routeSrc, /router\.post\(\s*'\/security\/scans'[\s\S]{0,400}?api\.requireRank\('metic',\s*'archon'\)/);
230
+ });
231
+
232
+ test('the GET is requireBuilder at every rank — the newest builder needs the warning most', () => {
233
+ assert.match(routeSrc, /router\.get\(\s*'\/security\/scans'[\s\S]{0,200}?api\.requireBuilder/);
234
+ const getBlock = routeSrc.slice(routeSrc.indexOf("router.get("));
235
+ assert.ok(!/requireRank/.test(getBlock.slice(0, 300)), 'the read side must not be rank-gated');
236
+ });
237
+
238
+ test('a Xenos POST gets a DEFINED degraded path, not a bare 403', () => {
239
+ assert.match(routeSrc, /degraded/i);
240
+ assert.match(routeSrc, /report file/i);
241
+ });
242
+
243
+ test('module.json mounts the scans route', () => {
244
+ const manifest = JSON.parse(fs.readFileSync(path.join(here, '..', 'module.json'), 'utf8'));
245
+ assert.ok(manifest.contributes.routes.includes('scans'));
246
+ assert.ok(manifest.contributes.routes.includes('reports'), 'reports must stay mounted');
247
+ });
248
+
249
+ console.log(`\nscan_store: ${passed} passed, ${failed} failed`);
250
+ if (failed > 0) process.exit(1);
@@ -0,0 +1,116 @@
1
+ // modules/security/tests/tool_scans_migration.mjs
2
+ //
3
+ // Structural guards on security_001_tool_scans.sql. These are not a substitute
4
+ // for applying it — they are the invariants that must survive any later edit,
5
+ // checked in the DB-free gate where a live Postgres is not available.
6
+ //
7
+ // The load-bearing one is the verdict/completion coupling: an aborted scan must
8
+ // be unable to carry a verdict, and a complete one must be unable to omit it.
9
+ // Without that constraint a scan that died could be read as one that passed,
10
+ // which is the single most dangerous confusion this whole feature can produce.
11
+ //
12
+ // Run directly: node modules/security/tests/tool_scans_migration.mjs
13
+
14
+ import { strict as assert } from 'node:assert';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const here = path.dirname(fileURLToPath(import.meta.url));
20
+ const SQL_PATH = path.join(here, '..', 'migrations', 'security_001_tool_scans.sql');
21
+ const sql = fs.readFileSync(SQL_PATH, 'utf8');
22
+ // Comments carry prose that would otherwise trip the keyword scans below.
23
+ const code = sql.replace(/--[^\n]*/g, '');
24
+
25
+ let passed = 0;
26
+ let failed = 0;
27
+ function test(name, fn) {
28
+ try { fn(); passed++; console.log(` ok ${name}`); }
29
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
30
+ }
31
+
32
+ test('every created object is security_-namespaced (ADR 0083 §Decision #5)', () => {
33
+ const created = [...code.matchAll(/CREATE\s+(?:TABLE|INDEX|VIEW|TYPE|SEQUENCE)\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-z_][a-z0-9_]*)/gi)]
34
+ .map((m) => m[1]);
35
+ assert.ok(created.length >= 4, `expected the table + its indexes, saw ${created.length}`);
36
+ for (const name of created) {
37
+ assert.ok(name.startsWith('security_'), `${name} is outside the security_ namespace`);
38
+ }
39
+ });
40
+
41
+ test('additive only — no destructive DDL', () => {
42
+ for (const re of [/\bDROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT|TYPE)\b/i, /\bTRUNCATE\b/i, /\bALTER\s+TABLE\s+\w+\s+DROP\b/i]) {
43
+ assert.ok(!re.test(code), `destructive DDL found: ${re}`);
44
+ }
45
+ });
46
+
47
+ test('idempotent — table and every index use IF NOT EXISTS', () => {
48
+ assert.match(code, /CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+security_tool_scans/i);
49
+ const indexes = [...code.matchAll(/CREATE\s+INDEX(\s+IF\s+NOT\s+EXISTS)?/gi)];
50
+ assert.ok(indexes.length >= 3, `expected at least 3 indexes, saw ${indexes.length}`);
51
+ for (const m of indexes) {
52
+ assert.ok(m[1], 'an index is missing IF NOT EXISTS — the migration would fail on re-run');
53
+ }
54
+ });
55
+
56
+ test('runs in a single transaction', () => {
57
+ assert.match(code, /^\s*BEGIN;/m);
58
+ assert.match(code, /^\s*COMMIT;/m);
59
+ });
60
+
61
+ test('carries every column the scan record needs', () => {
62
+ for (const col of [
63
+ 'target_url', 'target_host', 'artifact_kind', 'resolved_sha', 'content_hash',
64
+ 'file_count', 'total_bytes', 'tier', 'verdict', 'completion', 'abort_code',
65
+ 'findings_critical', 'findings_high', 'findings_medium', 'findings_low',
66
+ 'findings_summary', 'action_taken', 'engine_version', 'scanned_by_builder_id',
67
+ 'created_at', 'updated_at',
68
+ ]) {
69
+ assert.ok(new RegExp(`\\b${col}\\b`).test(code), `missing column: ${col}`);
70
+ }
71
+ });
72
+
73
+ test('an aborted scan can never carry a verdict, and a complete one can never omit it', () => {
74
+ // The invariant that stops "the scan died" from ever reading as "it passed".
75
+ assert.match(code, /CHECK\s*\(\s*\(completion\s*=\s*'complete'\)\s*=\s*\(verdict\s+IS\s+NOT\s+NULL\)\s*\)/i);
76
+ });
77
+
78
+ test('completion records an aborted scan rather than losing it', () => {
79
+ assert.match(code, /completion\s+IN\s*\('complete',\s*'aborted',\s*'incomplete'\)/i);
80
+ });
81
+
82
+ test('verdict is constrained to the three honest values', () => {
83
+ assert.match(code, /verdict\s+IN\s*\('clean',\s*'caution',\s*'dangerous'\)/i);
84
+ });
85
+
86
+ test('the low tier is a recorded value, so a verdict is always read in tier context', () => {
87
+ assert.match(code, /tier\s+IN\s*\('low',\s*'default',\s*'high',\s*'xhigh',\s*'max'\)/i);
88
+ });
89
+
90
+ test('findings_summary is size-capped at the DB layer', () => {
91
+ // Defense in depth with the route: a hostile artifact must not push unbounded
92
+ // text into a field every future reader of this table renders.
93
+ assert.match(code, /length\(findings_summary::text\)\s*<=\s*\d+/i);
94
+ });
95
+
96
+ test('content_hash and scanner are indexed — the two lookups the table exists for', () => {
97
+ assert.match(code, /CREATE\s+INDEX\s+IF\s+NOT\s+EXISTS\s+security_tool_scans_content_hash_idx[\s\S]*?\(content_hash\)/i);
98
+ assert.match(code, /CREATE\s+INDEX\s+IF\s+NOT\s+EXISTS\s+security_tool_scans_scanner_idx[\s\S]*?\(scanned_by_builder_id/i);
99
+ });
100
+
101
+ test('a deleted builder does not delete the scan record', () => {
102
+ // Attribution is nullable; the security finding outlives the account.
103
+ assert.match(code, /scanned_by_builder_id\s+bigint\s+REFERENCES\s+builders\(id\)\s+ON\s+DELETE\s+SET\s+NULL/i);
104
+ });
105
+
106
+ test('findings_summary must be a JSON array, not a smuggled scalar or object', () => {
107
+ assert.match(code, /jsonb_typeof\(findings_summary\)\s*=\s*'array'/i);
108
+ });
109
+
110
+ test('updated_at is live — rows mutate post-insert (action_taken), so a touch trigger exists', () => {
111
+ assert.match(code, /CREATE\s+OR\s+REPLACE\s+FUNCTION\s+security_tool_scans_touch_updated_at\(\)/i);
112
+ assert.match(code, /CREATE\s+TRIGGER\s+security_tool_scans_updated_at\s+BEFORE\s+UPDATE\s+ON\s+security_tool_scans/i);
113
+ });
114
+
115
+ console.log(`\ntool_scans_migration: ${passed} passed, ${failed} failed`);
116
+ if (failed > 0) process.exit(1);
@@ -65,10 +65,11 @@ const MAX_DETAIL_TURNS = 5000; // a hard ceiling; ship.js bounds well under this
65
65
  // also a follow-up — see SR-17 notes.
66
66
  const MAX_DETAIL_ELEMENT_BYTES = 4 * 1024; // 4 KB per detail[] element
67
67
  const MAX_DETAIL_TOTAL_BYTES = 2 * 1024 * 1024; // 2 MB total detail[] payload
68
- // The three LIVE ranks (ADR 0018) — deliberately NOT db.RANKS, which is the full
69
- // 16-entry historical enum. Only these three are ever assigned to a builder, so
70
- // the search filter validates against them (a query for any other rank is a 400).
71
- const VALID_RANKS = new Set(['xenos', 'thetes', 'metic', 'archon']);
68
+ // The LIVE ranks — deliberately NOT db.RANKS, which is the full 16-entry
69
+ // historical enum. Only the live ladder is ever assigned to a builder, so the
70
+ // search filter validates against it (a query for any other rank is a 400).
71
+ // Derived from the doorway ladder (BV1.R108) rather than hand-copied.
72
+ const VALID_RANKS = new Set(api.LIVE_RANK_LADDER);
72
73
 
73
74
  // Per-builder read limiter for the session corpus (mirrors the memory router's
74
75
  // forensic-read limiter). Reads aren't covered by the global write limiter.
@@ -270,7 +271,7 @@ module.exports = function buildSessionsRouter() {
270
271
  // Express matches the literal paths first (the numeric :id never swallows them).
271
272
 
272
273
  // rank: archon — cross-builder corpus search (BFG-principal posture until 6C.1).
273
- router.get('/sessions/search', auth.requireBuilder, auth.requireRank('archon'), readRateLimit, async (req, res) => {
274
+ router.get('/sessions/search', auth.requireBuilder, auth.requirePermission('session.search'), readRateLimit, async (req, res) => {
274
275
  const q = req.query || {};
275
276
  const rank = q.rank ? String(q.rank) : null;
276
277
  if (rank && !VALID_RANKS.has(rank)) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.18.19",
3
+ "version": "1.19.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.18.19",
9
+ "version": "1.19.2",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.18.19",
3
+ "version": "1.19.2",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -700,9 +700,13 @@ input[type=text].bad{border-color:var(--err);}
700
700
  $('ghFresh').hidden = !(ok && !adopt);
701
701
  $('manualHome').hidden = !isManualHome();
702
702
  if (need) {
703
+ // Informed consent (ADR 0155): this grants `repo`, which GitHub's own screen
704
+ // words as full control of ALL your repositories — so say plainly why we ask
705
+ // and what we do with it, rather than letting GitHub's wording be the first
706
+ // the user hears of it. Private repos are the reason the scope is this wide.
703
707
  $('ghConnectMsg').textContent = adopt
704
- ? 'You’re signed in — creating your project needs one more GitHub permission. Allow repo access to pick the repository to adopt.'
705
- : 'You’re signed in — creating your project needs one more GitHub permission. Allow repo access and we’ll create the empty repo for you.';
708
+ ? 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — that is the only scope it offers that can write to a private repo). We use it to read the repo you pick and push the setup commit to it. Nothing else is touched.'
709
+ : 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — the narrower option cannot write to private repos). We use it to create your new empty repo and push the setup commit to it. Nothing else is touched.';
706
710
  }
707
711
  if (ok && adopt) initAdoptPicker();
708
712
  if (ok && !adopt) syncFresh();
@@ -934,7 +938,13 @@ input[type=text].bad{border-color:var(--err);}
934
938
  slug: state.slug,
935
939
  hosting_shape: 'standalone',
936
940
  domain: state.domain || undefined,
937
- target_ref: state.owner + '/' + state.repo
941
+ target_ref: state.owner + '/' + state.repo,
942
+ /* LOAD-BEARING for the control-plane runner, not just for the checklist we
943
+ render (task 1002562): 'adopt' makes it clone + layer onto the repo's
944
+ existing history instead of pushing a fresh tree, which a repo that already
945
+ has commits rejects as unrelated history. state.mode already tracked
946
+ greenfield|adopt for the UI — it has to reach the server too. */
947
+ onboard_mode: state.mode === 'adopt' ? 'adopt' : 'greenfield'
938
948
  })
939
949
  });
940
950
  }).then(function (res) {
@@ -41,7 +41,7 @@ const { cliClient, requireSession, arg } = require('./cli-lib');
41
41
  } else if (r.data?.error === 'task_not_found') {
42
42
  console.error(`Task #${taskId} not found.`);
43
43
  } else if (r.status === 403) {
44
- console.error('Abandoning a task is Archon-only.');
44
+ console.error('Abandoning a task needs Metic+ rank (ADR 0152).');
45
45
  } else {
46
46
  console.error(`abandon failed (${r.status}):`, r.data);
47
47
  }
@@ -126,6 +126,12 @@ ALLOWLIST=(
126
126
  # That's the GitHub org/user that owns the repo; not a builder identity.
127
127
  'scripts/deploy/provision-staging.sh'
128
128
 
129
+ # CODEOWNERS is GitHub's owner-review list (task 1002611 / ADR 0159): the
130
+ # format requires the literal @username on every entry. Repo-hosting
131
+ # identity, same nature as provision-staging.sh — not a builder identity
132
+ # hardcoded in code.
133
+ 'CODEOWNERS'
134
+
129
135
  # Dev Box app distribution (#904, ADR 0044): the PUBLIC releases repo lives
130
136
  # under the same GitHub account that owns the main repo — repo-hosting
131
137
  # identity (same nature as provision-staging.sh above), not a builder
@@ -1051,7 +1051,7 @@ async function cmdSweepDormant(pool, boxes, { apply }) {
1051
1051
  try {
1052
1052
  await deprovisionOne(pool, boxes, doApi, box, { actor: 'sweep:dormant' });
1053
1053
  reclaimed++;
1054
- const ping = postReclaimPing(`@${box.github_login} — your dev box was idle for ${days} days and has been reclaimed to save cost. Ask an Archon to re-provision when you're back; your memory is server-side and unaffected.`);
1054
+ const ping = postReclaimPing(`@${box.github_login} — your dev box was idle for ${days} days and has been reclaimed to save cost. Queue a fresh one anytime with POST /api/gds/box/ensure (no operator needed); your memory is server-side and unaffected.`);
1055
1055
  if (!ping.ok) pingFailed++;
1056
1056
  console.log(` ✓ reclaimed @${box.github_login} (parked ${days}d)${ping.ok ? '' : ' [chat ping failed]'}`);
1057
1057
  } catch (e) { errors++; await markErrorUnlessDestroyed(pool, boxes, box.builder_id, e.message); console.log(` ✖ @${box.github_login}: ${e.message}`); }
@@ -210,6 +210,21 @@ function formatClaimFailure(r, taskId) {
210
210
  lines.push(` ${d.what_to_do || 'Ship or release the current claim first.'}`);
211
211
  return { lines, exit: 1 };
212
212
  }
213
+ // task 1002589: the per-session DB backstop index tripped. Distinct from
214
+ // session_already_has_active_claim (the route guard's own refusal): since
215
+ // core_204 (task 1002570) the index keys per (session, worktree) —
216
+ // matching the guard — so this mainly surfaces the same-tree race the guard
217
+ // couldn't see, or fires on a DB still carrying the migration-180
218
+ // session-only index. Either way the target task itself is NOT taken — never
219
+ // render this as "claimed by another session".
220
+ if (code === 'session_already_has_bound_claim') {
221
+ const n = d.active_task_id;
222
+ lines.push(n
223
+ ? `Task #${taskId} is still free — but this session already holds a bound claim on #${n}${d.active_task_title ? `: ${d.active_task_title}` : ''}.`
224
+ : `Task #${taskId} is still free — but this session already holds a bound claim on another task.`);
225
+ lines.push(` ${d.what_to_do || `Run /builder-ship ${n || '<task>'} (or /builder-release ${n || '<task>'}) first, claim from a separate worktree, or claim with {"bind_session":false} (an unbound claim always coexists).`}`);
226
+ return { lines, exit: 1 };
227
+ }
213
228
  if (code === 'ALREADY_CLAIMED') {
214
229
  lines.push(`Task #${taskId} was just claimed by another session. Try a different task.`);
215
230
  return { lines, exit: 1 };
@@ -250,10 +250,22 @@ function printUnauthGuidanceAndExit() {
250
250
 
251
251
  // task 1050 (C): is this response the dev-box bootstrap-scoped 403? A box's baked
252
252
  // session (source='box', #919/ADR 0053) may only call the 4 on-box bootstrap
253
- // endpoints; everything else (claim/ship/me/…) returns 403 { error: 'box_scope' }.
254
- // Pure + exported so the detector is unit-tested.
253
+ // endpoints; everything else (claim/ship/me/…) returns a 403 box_scope.
254
+ //
255
+ // task 1002162: accept BOTH body shapes. The live server emits the ADR 0116 (R01)
256
+ // envelope — `{ error: { code, message, details? } }` from api-errors.js
257
+ // `envelope()` — so `res.data.error` is an OBJECT and the old `=== 'box_scope'`
258
+ // string test could never match. Both call sites below were therefore dead code
259
+ // (box-scope was only ever caught by box-auth-check.js instead). Older/flat paths
260
+ // still return `{ error: 'box_scope' }`, so normalize the same way
261
+ // claim.js:formatClaimFailure does rather than swapping one shape for the other.
262
+ //
263
+ // Pure + exported so the detector is unit-tested against the real envelope.
255
264
  function isBoxScope(res) {
256
- return !!res && res.status === 403 && !!res.data && res.data.error === 'box_scope';
265
+ if (!res || res.status !== 403 || !res.data) return false;
266
+ const errRaw = res.data.error;
267
+ const code = errRaw !== null && typeof errRaw === 'object' ? errRaw.code : errRaw;
268
+ return code === 'box_scope';
257
269
  }
258
270
 
259
271
  let _boxScopeExitInFlight = false;
@@ -652,7 +664,7 @@ function printCostWarning(meBody) {
652
664
  const cost = meBody && meBody.cost;
653
665
  if (!cost || typeof cost !== 'object') return;
654
666
  if (cost.level === 'over') {
655
- console.log(`⚠ BUDGET EXCEEDED — $${cost.spend_mtd_usd} of $${cost.budget_mtd_usd} this month (${cost.pct_used}%). Cost-incurring actions (ship, art pipeline) are blocked until an Archon raises your budget.`);
667
+ console.log(`⚠ BUDGET EXCEEDED — $${cost.spend_mtd_usd} of $${cost.budget_mtd_usd} this month (${cost.pct_used}%). Cost-incurring actions (ship, art pipeline) are blocked until a Metic or Archon raises your budget.`);
656
668
  } else if (cost.level === 'warn') {
657
669
  console.log(`⚠ Budget at ${cost.pct_used}% — $${cost.spend_mtd_usd} of $${cost.budget_mtd_usd} this month. You'll be blocked from cost-incurring actions at 100%.`);
658
670
  }
@@ -2,18 +2,25 @@
2
2
  // scripts/gds/gating.js — owner kill-switch for main-branch merge gating.
3
3
  //
4
4
  // What "gating" means here — TWO independent levers, both toggled together:
5
- // 1. GitHub branch protection on `main`: required status checks (unit,
6
- // trufflehog, gate-review) + required code-owner review (CODEOWNERS).
5
+ // 1. GitHub branch protection on `main`: required status checks + required
6
+ // code-owner review (CODEOWNERS). The required contexts are ONLY checks
7
+ // that exist and can pass today — currently `unit` (the suite + fitness
8
+ // lane, task 1002610). The planned `secret-scan` and `gate-review`
9
+ // contexts (ADR 0058 phase 3; CORE_EXPECTED_CHECKS in adopt-preflight.js)
10
+ // join DEFAULT_PROTECTION when their workflows ship — requiring a context
11
+ // nothing reports blocks every merge indefinitely (D-12, ADR 0159).
7
12
  // 2. The application-layer trust gates that run on every PR — the gate-review
8
- // and grade-gate CI workflows. These post their own status checks and the
9
- // ship / auto-merge pipeline refuses to land a PR while gate-review is red
13
+ // and grade-gate CI workflows (ADR 0058 phases 1-3; NOT YET IN THE TREE —
14
+ // workflowState() reports them 'absent' and the toggles below no-op until
15
+ // they ship). Once live they post their own status checks and the ship /
16
+ // auto-merge pipeline refuses to land a PR while gate-review is red
10
17
  // (it escalates gate-surface changes — infra/, migrations/, CODEOWNERS,
11
18
  // scripts/gds plumbing — to owner approval). Branch protection alone does
12
19
  // NOT neutralize these, which is why `off` also `gh workflow disable`s them.
13
20
  // With gating ON, a gate-surface PR needs owner approval / --admin to merge.
14
21
  // With gating OFF, ordinary `gh pr merge N` (and every builder / autonomous
15
- // session) merges with no checks and no approval. `unit` + `trufflehog` keep
16
- // running either way — real safety nets that force no approval.
22
+ // session) merges with no checks and no approval. `unit` keeps running either
23
+ // way — a real safety net that forces no approval.
17
24
  //
18
25
  // It is REVERSIBLE by construction: `off` snapshots the live protection to a
19
26
  // replay-ready file, then deletes protection; `on` PUTs the snapshot back,
@@ -47,19 +54,27 @@ const SNAPSHOT = require('../../src/instance-config').configPath('gating-snapsho
47
54
  // approval / --admin on gate-surface changes. Branch protection alone does NOT
48
55
  // neutralize these — they post their own status checks and the auto-merge /
49
56
  // ship pipeline refuses to land a PR while gate-review is red. So the kill-switch
50
- // also disables/enables these workflows. `unit` + `trufflehog` (secret scan) are
51
- // deliberately left running — they are real safety nets and force no approval.
57
+ // also disables/enables these workflows. PLANNED, not yet in the tree (ADR 0058
58
+ // phases 1-3): while absent, workflowState() reports 'absent' and both toggles
59
+ // no-op — the names stay here so the switch covers them the moment they ship.
60
+ // `unit` is deliberately left running — a real safety net that forces no approval.
52
61
  const GATE_WORKFLOWS = ['gate-review.yml', 'grade-gate.yml'];
53
62
 
54
- // Baked-in known-good config (the live state captured 2026-06-19). Used only as
55
- // a restore fallback when no snapshot exists, so `on` can never be stuck.
63
+ // Baked-in known-good config. Used only as a restore fallback when no snapshot
64
+ // exists, so `on` can never be stuck. Requires ONLY contexts that exist and can
65
+ // pass today (ADR 0159 — D-12: a required context nothing reports blocks every
66
+ // merge indefinitely, and a required context that is red on every PR does the
67
+ // same). When the secret-scan / gate-review workflows ship (ADR 0058), add
68
+ // their contexts back here — those names are the contract (adopt-preflight.js
69
+ // CORE_EXPECTED_CHECKS, init.js), tool-agnostic on purpose.
56
70
  const DEFAULT_PROTECTION = {
57
71
  required_status_checks: {
58
72
  strict: false,
59
73
  checks: [
60
74
  { context: 'unit', app_id: 15368 },
61
- { context: 'trufflehog', app_id: 15368 },
62
- { context: 'gate-review', app_id: 15368 },
75
+ // Stated intent, not yet requirable — uncomment as each workflow ships:
76
+ // { context: 'secret-scan', app_id: 15368 }, // secret scanning (ADR 0058)
77
+ // { context: 'gate-review', app_id: 15368 }, // gate-surface review (ADR 0058 phase 3)
63
78
  ],
64
79
  },
65
80
  enforce_admins: false,
@@ -270,7 +285,7 @@ function cmdOff(repo) {
270
285
 
271
286
  console.log('\n🔴 gating OFF. `main` is UNPROTECTED and the gate-review/grade CI gates no longer');
272
287
  console.log(' run — ordinary `gh pr merge N` (and every builder / autonomous session) merges');
273
- console.log(' with no checks and no approval. (unit + secret-scan still run.)');
288
+ console.log(' with no checks and no approval. (unit still runs.)');
274
289
  console.log(' ↩ restore everything: node scripts/gds/gating.js on');
275
290
  }
276
291
 
@@ -81,7 +81,7 @@ const RANK_BLURB = {
81
81
  public: 'No authentication — any caller.',
82
82
  'any-builder': 'Any authenticated builder (row-level ownership enforced in-handler).',
83
83
  'metic+archon': 'Metic or Archon rank (review/triage powers).',
84
- archon: 'Archon only (the single owner — ratification, infra, rank changes).',
84
+ archon: 'Archon only (rank and identity management + the escalation keys — the trust boundary).',
85
85
  'bfg-principal': 'The BFG cross-builder memory service principal (not a human rank).',
86
86
  unknown: 'Gate could not be classified from source — treat as privileged.',
87
87
  };