@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.
- package/.bongos-core.json +514 -194
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/README.md +8 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +179 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +2 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +16 -16
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +70 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +5 -5
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +86 -13
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +128 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +1 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- 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
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
705
|
-
: 'You’re signed in — creating your project needs one more GitHub permission.
|
|
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) {
|
package/scripts/gds/abandon.js
CHANGED
|
@@ -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
|
|
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
|
package/scripts/gds/box.js
CHANGED
|
@@ -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.
|
|
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}`); }
|
package/scripts/gds/claim.js
CHANGED
|
@@ -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 };
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -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
|
|
254
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/scripts/gds/gating.js
CHANGED
|
@@ -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
|
|
6
|
-
//
|
|
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
|
|
9
|
-
//
|
|
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`
|
|
16
|
-
//
|
|
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.
|
|
51
|
-
//
|
|
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
|
|
55
|
-
//
|
|
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
|
-
|
|
62
|
-
{ context: '
|
|
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
|
|
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
|
|
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
|
};
|