@bongos/core 1.18.19 → 1.19.3
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 +547 -197
- 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/bin/bongos.js +33 -7
- 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/0137-upstream-publish-policy-manual-owner-gated.md +1 -1
- 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/0161-publish-on-merge.md +34 -0
- package/docs/adr/README.md +9 -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 +181 -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 +6 -2
- 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/migrations/core_205_versions_track_default.sql +28 -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 +99 -69
- 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/bump-version.js +76 -0
- 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 +77 -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 +96 -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_cli.mjs +91 -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/bump_version.mjs +71 -0
- 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 +147 -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/patch_gate_ordering.mjs +102 -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,201 @@
|
|
|
1
|
+
// tests/governance_live_proof.mjs — the C6 governance LIVE proof (BV1.R114, ADR 0151).
|
|
2
|
+
//
|
|
3
|
+
// C6's live-demonstration clause: an Archon creates a CUSTOM role granting an
|
|
4
|
+
// OPERATIONAL permission, assigns it to a low-trust builder, and that builder can
|
|
5
|
+
// perform the operational action — WHILE a `system` permission the role also "has"
|
|
6
|
+
// (a grant row forced past the API, straight into the DB) stays enforced by
|
|
7
|
+
// substrate: the deploy/protected-path attempt is still blocked.
|
|
8
|
+
//
|
|
9
|
+
// This is the goal_wall_proof.mjs pattern: the abuse MATRIX
|
|
10
|
+
// (tests/governance_abuse_matrix.mjs, R112) already proves every vector DB-free
|
|
11
|
+
// with stubs; THIS file proves the same story END-TO-END on the real stack —
|
|
12
|
+
// real Postgres rows, real sessions, real HTTP through the real router, the real
|
|
13
|
+
// resolver behind the real `governance` kernel port, no stubs, no monkeypatches.
|
|
14
|
+
// The captured sandbox transcript lives in docs/governance/bongos-v1-c6-live-proof.md.
|
|
15
|
+
//
|
|
16
|
+
// Real-DB integration test (the needs_rebase.mjs class): SELF-SKIPS (exit 0)
|
|
17
|
+
// when DATABASE_URL is unset/unreachable or the governance tables are missing,
|
|
18
|
+
// and is listed in run-unit-tests.js's INTEGRATION set so the DB-free lane
|
|
19
|
+
// never runs it.
|
|
20
|
+
//
|
|
21
|
+
// Run: DATABASE_URL=... node tests/governance_live_proof.mjs
|
|
22
|
+
// GDS_BASE_URL=http://localhost:3000 — optional: drive an already-running
|
|
23
|
+
// sandbox server (sharing that DATABASE_URL) instead of booting the router
|
|
24
|
+
// in-process. Same routes either way; this is how the live sandbox transcript
|
|
25
|
+
// was captured.
|
|
26
|
+
|
|
27
|
+
import { strict as assert } from 'node:assert';
|
|
28
|
+
import crypto from 'node:crypto';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
|
|
31
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
|
|
34
|
+
if (!process.env.DATABASE_URL) {
|
|
35
|
+
console.log('governance_live_proof.mjs SKIPPED — DATABASE_URL not set.');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { Pool } = require('pg');
|
|
40
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
41
|
+
try {
|
|
42
|
+
await pool.query('SELECT 1 FROM governance_roles LIMIT 1');
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.log(`governance_live_proof.mjs SKIPPED — no migrated Postgres (${e.code || e.message}).`);
|
|
45
|
+
await pool.end().catch(() => {});
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── fixtures (high-ID range; cleaned before AND after so reruns are safe) ────
|
|
50
|
+
const ARCHON = 90000701;
|
|
51
|
+
const THETES = 90000702;
|
|
52
|
+
const ROLE_KEY = 'glp-demo-triager';
|
|
53
|
+
const OPERATIONAL_KEY = 'idea.triage'; // system:false, floor metic — above thetes
|
|
54
|
+
const SYSTEM_KEY = 'deploy.push_main'; // system:true — substrate-enforced
|
|
55
|
+
const tok = () => 'glp' + crypto.randomBytes(24).toString('hex');
|
|
56
|
+
const ARCHON_TOKEN = tok();
|
|
57
|
+
const THETES_TOKEN = tok();
|
|
58
|
+
|
|
59
|
+
const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
|
|
60
|
+
|
|
61
|
+
async function cleanup() {
|
|
62
|
+
await q(`DELETE FROM governance_roles WHERE role_key=$1`, [ROLE_KEY]); // grants+assignments cascade
|
|
63
|
+
await q(`DELETE FROM governance_builder_roles WHERE builder_id IN ($1,$2)`, [ARCHON, THETES]);
|
|
64
|
+
await q(`DELETE FROM builder_sessions WHERE builder_id IN ($1,$2)`, [ARCHON, THETES]);
|
|
65
|
+
await q(`DELETE FROM idea_inbox WHERE captured_by IN ($1,$2)`, [ARCHON, THETES]);
|
|
66
|
+
// The audit middleware records the demo's own writes; those rows FK the
|
|
67
|
+
// fixture builders, so they go first.
|
|
68
|
+
await q(`DELETE FROM audit_log WHERE builder_id IN ($1,$2)`, [ARCHON, THETES]);
|
|
69
|
+
await q(`DELETE FROM builders WHERE id IN ($1,$2)`, [ARCHON, THETES]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function seed() {
|
|
73
|
+
await q(
|
|
74
|
+
`INSERT INTO builders (id, github_id, github_login, display_name, rank, status)
|
|
75
|
+
VALUES ($1,'glp-archon','glp-archon','GLP Demo Archon','archon','active'),
|
|
76
|
+
($2,'glp-thetes','glp-thetes','GLP Demo Thetes','thetes','active')`,
|
|
77
|
+
[ARCHON, THETES]
|
|
78
|
+
);
|
|
79
|
+
// The R95b rank-role sync runs at sign-in / rank writes; a directly-seeded
|
|
80
|
+
// builder mirrors its effect here (one seeded rank-role each, fail-closed
|
|
81
|
+
// otherwise — the archon would resolve ZERO permissions and deny itself).
|
|
82
|
+
await q(
|
|
83
|
+
`INSERT INTO governance_builder_roles (builder_id, role_key)
|
|
84
|
+
VALUES ($1,'archon'), ($2,'thetes')`,
|
|
85
|
+
[ARCHON, THETES]
|
|
86
|
+
);
|
|
87
|
+
await q(
|
|
88
|
+
`INSERT INTO builder_sessions (builder_id, token, source)
|
|
89
|
+
VALUES ($1,$2,'web'), ($3,$4,'web')`,
|
|
90
|
+
[ARCHON, ARCHON_TOKEN, THETES, THETES_TOKEN]
|
|
91
|
+
);
|
|
92
|
+
const [idea] = await q(
|
|
93
|
+
`INSERT INTO idea_inbox (title, body_md, captured_by)
|
|
94
|
+
VALUES ('glp live-proof fixture idea','(sandbox demo row)',$1) RETURNING id`,
|
|
95
|
+
[ARCHON]
|
|
96
|
+
);
|
|
97
|
+
return idea.id;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── the server: a live sandbox (GDS_BASE_URL) or the real router in-process ──
|
|
101
|
+
let server = null;
|
|
102
|
+
let base;
|
|
103
|
+
if (process.env.GDS_BASE_URL) {
|
|
104
|
+
base = process.env.GDS_BASE_URL.replace(/\/$/, '');
|
|
105
|
+
} else {
|
|
106
|
+
const express = require('express');
|
|
107
|
+
const { buildGdsRouter } = require('../src/bongos/routes.js');
|
|
108
|
+
const app = express();
|
|
109
|
+
app.use('/api/gds', buildGdsRouter());
|
|
110
|
+
server = await new Promise((resolve) => {
|
|
111
|
+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
112
|
+
});
|
|
113
|
+
base = `http://127.0.0.1:${server.address().port}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const call = async (token, method, path, body) => {
|
|
117
|
+
const res = await fetch(`${base}/api/gds${path}`, {
|
|
118
|
+
method,
|
|
119
|
+
headers: {
|
|
120
|
+
authorization: `Bearer ${token}`,
|
|
121
|
+
...(body ? { 'content-type': 'application/json' } : {}),
|
|
122
|
+
},
|
|
123
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
124
|
+
});
|
|
125
|
+
const json = await res.json().catch(() => ({}));
|
|
126
|
+
const code = json?.error?.code || json?.error || '';
|
|
127
|
+
console.log(` ${method.padEnd(6)} ${path} → ${res.status}${code ? ` ${code}` : ''}`);
|
|
128
|
+
return { status: res.status, body: json };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let pass = 0;
|
|
132
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); console.log(` ok ${msg}`); pass++; };
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await cleanup();
|
|
136
|
+
const ideaId = await seed();
|
|
137
|
+
console.log(`live proof against ${base} (idea fixture #${ideaId})`);
|
|
138
|
+
|
|
139
|
+
// Act 1 — the gate is closed: thetes (floor for idea.triage is metic) is denied.
|
|
140
|
+
let r = await call(THETES_TOKEN, 'PATCH', `/inbox/${ideaId}`, { status: 'discarded' });
|
|
141
|
+
ok(r.status === 403 && r.body.error.code === 'permission_forbidden',
|
|
142
|
+
'BEFORE: the thetes builder cannot triage (403 permission_forbidden)');
|
|
143
|
+
|
|
144
|
+
// Act 2 — an Archon creates a custom role and grants the operational permission.
|
|
145
|
+
r = await call(ARCHON_TOKEN, 'POST', '/governance/roles',
|
|
146
|
+
{ role_key: ROLE_KEY, label: 'GLP demo triage role' });
|
|
147
|
+
ok(r.status === 201, 'the Archon creates a custom role (201)');
|
|
148
|
+
r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${OPERATIONAL_KEY}`);
|
|
149
|
+
ok(r.status === 200 && r.body.ok === true,
|
|
150
|
+
`the Archon grants operational '${OPERATIONAL_KEY}' to the custom role (200)`);
|
|
151
|
+
|
|
152
|
+
// Act 3 — the locked wall: a system permission is refused through the same API.
|
|
153
|
+
r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${SYSTEM_KEY}`);
|
|
154
|
+
ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
|
|
155
|
+
`system '${SYSTEM_KEY}' is NOT grantable, even by the Archon (400 permission_not_grantable)`);
|
|
156
|
+
r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/governance.manage`);
|
|
157
|
+
ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
|
|
158
|
+
'the meta-escalation (governance.manage) is refused the same way');
|
|
159
|
+
|
|
160
|
+
// Act 4 — assign, then the same builder performs the action: no re-login, no
|
|
161
|
+
// restart — the per-request, uncached resolve (ADR 0016) picks the role up.
|
|
162
|
+
r = await call(ARCHON_TOKEN, 'POST', '/governance/assignments',
|
|
163
|
+
{ builder_id: THETES, role_key: ROLE_KEY });
|
|
164
|
+
ok(r.status === 201, 'the Archon assigns the custom role to the thetes builder (201)');
|
|
165
|
+
r = await call(THETES_TOKEN, 'PATCH', `/inbox/${ideaId}`, { status: 'discarded' });
|
|
166
|
+
ok(r.status === 200, 'AFTER: the same builder, same session, can now triage (200) — the custom role works');
|
|
167
|
+
|
|
168
|
+
// Act 5 — the wall behind the wall: force the grant row no API path can create,
|
|
169
|
+
// so the role now "has" a system permission in the DB…
|
|
170
|
+
await q(
|
|
171
|
+
`INSERT INTO governance_role_permissions (role_key, permission_key) VALUES ($1,$2)`,
|
|
172
|
+
[ROLE_KEY, SYSTEM_KEY]
|
|
173
|
+
);
|
|
174
|
+
console.log(` (forced INTO the DB: grant ${ROLE_KEY} ← ${SYSTEM_KEY} — no API path can write this row)`);
|
|
175
|
+
|
|
176
|
+
// …the resolver reports it honestly (it is a defined catalog key; matrix V10
|
|
177
|
+
// proves no route consumes it via requirePermission, so it admits nothing
|
|
178
|
+
// in-app)…
|
|
179
|
+
const resolver = require('../modules/governance/resolver.js');
|
|
180
|
+
const perms = await resolver.resolveBuilderPermissions(THETES);
|
|
181
|
+
ok(perms.includes(OPERATIONAL_KEY), 'the resolved set includes the operational grant');
|
|
182
|
+
console.log(` (resolved set ${perms.includes(SYSTEM_KEY) ? 'includes' : 'omits'} the forced '${SYSTEM_KEY}' — in-app it guards no route either way)`);
|
|
183
|
+
|
|
184
|
+
// …and the SUBSTRATE stays closed: the real checker the pre-push hook, grader
|
|
185
|
+
// pre-pass and main-audit run — structurally grant-blind (node-builtins-only
|
|
186
|
+
// imports; it cannot even reach a DB) — still blocks the sub-metic builder's
|
|
187
|
+
// protected-path diff while the forced grant row sits in the database.
|
|
188
|
+
const { checkPermissionPaths } = require('../src/bongos/permission-path-check.js');
|
|
189
|
+
const sub = checkPermissionPaths({
|
|
190
|
+
changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/governance/catalog.js'],
|
|
191
|
+
builderRank: 'thetes',
|
|
192
|
+
});
|
|
193
|
+
ok(sub.hardFail === true && sub.findings.length === 2,
|
|
194
|
+
'the substrate checker still BLOCKS the protected-path attempt (deploy/migration machinery), forced grant and all');
|
|
195
|
+
|
|
196
|
+
console.log(`\ngovernance_live_proof: ${pass} assertions passed`);
|
|
197
|
+
} finally {
|
|
198
|
+
await cleanup().catch((e) => console.error('cleanup failed', e.message));
|
|
199
|
+
if (server) await new Promise((resolve) => server.close(resolve));
|
|
200
|
+
await pool.end().catch(() => {});
|
|
201
|
+
}
|
|
@@ -50,30 +50,42 @@ const resolver = require('../modules/governance/resolver.js');
|
|
|
50
50
|
const wandering = require('../modules/builder-settings/wandering-prefs.js');
|
|
51
51
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
52
52
|
|
|
53
|
-
const GATES = ['requireBuilderPage', 'requireNonXenosPage', '
|
|
53
|
+
const GATES = ['requireBuilderPage', 'requireNonXenosPage', 'requireGovernancePage'];
|
|
54
54
|
const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
55
55
|
|
|
56
56
|
// ═══ the ORACLE — hand-written, from the rank tests R107 deleted ═════════════
|
|
57
|
-
// requireBuilderPage
|
|
58
|
-
// requireNonXenosPage
|
|
59
|
-
//
|
|
57
|
+
// requireBuilderPage admitted any session at all → every live rank
|
|
58
|
+
// requireNonXenosPage admitted rank !== 'xenos' → thetes and up
|
|
59
|
+
// requireGovernancePage admitted rank === 'archon' → archon only …until ADR 0157
|
|
60
|
+
//
|
|
61
|
+
// ADR 0157 (owner decision, 2026-08-05) lowered `page.view.governance` archon→metic, so
|
|
62
|
+
// the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
|
|
63
|
+
// was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
|
|
64
|
+
// were first written against — and was renamed once the floor moved; the audience here,
|
|
65
|
+
// not the name, is the contract.
|
|
66
|
+
//
|
|
67
|
+
// This is the ONE intentional widening in this table's history. Everything R107 promised
|
|
68
|
+
// (identical audiences across the rank→permission migration) still holds for the other
|
|
69
|
+
// two gates, and the widening is expressed here as DATA so the A1 oracle keeps proving
|
|
70
|
+
// the gate matches its declared floor rather than merely "didn't change".
|
|
60
71
|
const PRE_R107_AUDIENCE = {
|
|
61
72
|
requireBuilderPage: { xenos: true, thetes: true, metic: true, archon: true },
|
|
62
73
|
requireNonXenosPage: { xenos: false, thetes: true, metic: true, archon: true },
|
|
63
|
-
|
|
74
|
+
requireGovernancePage: { xenos: false, thetes: false, metic: true, archon: true },
|
|
64
75
|
};
|
|
65
76
|
// Where a SIGNED-IN builder who fails the gate is sent. Never the sign-in flow —
|
|
66
77
|
// they already have a session, so that would loop.
|
|
67
78
|
const DENIED_TO = {
|
|
68
79
|
requireBuilderPage: '/builders',
|
|
69
80
|
requireNonXenosPage: '/builders/not-ready',
|
|
70
|
-
|
|
81
|
+
requireGovernancePage: '/builders',
|
|
71
82
|
};
|
|
72
83
|
// The equivalent rank floor, for the independent cross-check below.
|
|
73
84
|
const GATE_FLOOR = {
|
|
74
85
|
requireBuilderPage: 'xenos',
|
|
75
86
|
requireNonXenosPage: 'thetes',
|
|
76
|
-
|
|
87
|
+
// ADR 0157: `page.view.governance` floor archon→metic.
|
|
88
|
+
requireGovernancePage: 'metic',
|
|
77
89
|
};
|
|
78
90
|
const SIGN_IN = '/api/gds/auth/web/start';
|
|
79
91
|
|
|
@@ -226,12 +238,12 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
|
|
|
226
238
|
});
|
|
227
239
|
const req = { headers: { cookie: 'gds_session=validtoken' } };
|
|
228
240
|
let admitted = false;
|
|
229
|
-
await auth.
|
|
241
|
+
await auth.requireGovernancePage(req, fakeRes(), () => { admitted = true; });
|
|
230
242
|
assert.equal(admitted, true, 'an Archon should reach the page');
|
|
231
243
|
keys = []; // the role is revoked between requests
|
|
232
244
|
const second = fakeRes();
|
|
233
245
|
let admittedAgain = false;
|
|
234
|
-
await auth.
|
|
246
|
+
await auth.requireGovernancePage(req, second, () => { admittedAgain = true; });
|
|
235
247
|
seams._reset();
|
|
236
248
|
assert.equal(calls, 2, 'the resolver must be consulted on every request');
|
|
237
249
|
assert.equal(admittedAgain, false, 'a revoked role must deny on the very next request');
|
|
@@ -239,8 +239,17 @@ test('B2: the route cross-reference actually covers a substantial slice of the c
|
|
|
239
239
|
`only ${covered.length} permissions cross-reference a gated route (${floorPinned.length} by rank, ${atomPinned.length} by atom; expected >= 25)`);
|
|
240
240
|
const archonCovered = covered.filter((r) => r.floor === 'archon');
|
|
241
241
|
const meticCovered = covered.filter((r) => r.floor === 'metic');
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
// These minimums measure SAMPLE BREADTH — "the cross-reference exercises both sides of
|
|
243
|
+
// the metic/archon boundary" — not how much authority Archon ought to hold. ADR 0157
|
|
244
|
+
// moved 20 atoms archon→metic, so the archon population is now genuinely small by
|
|
245
|
+
// design (rank, identity, and the escalation keys) and the metic population is large.
|
|
246
|
+
// The floors moved with it: archon 10→4, metic 10→25. Lowering the archon minimum is
|
|
247
|
+
// NOT a weakening — the guard it replaces would now fail on a correct codebase, and a
|
|
248
|
+
// guard that must be deleted to ship a legitimate change stops being a guard. The
|
|
249
|
+
// strictness moved to where the population went: metic's minimum more than doubled, so
|
|
250
|
+
// a regression that quietly drained metic-floored routes still reds the build.
|
|
251
|
+
assert.ok(archonCovered.length >= 4, `only ${archonCovered.length} archon-floored routes cross-referenced`);
|
|
252
|
+
assert.ok(meticCovered.length >= 25, `only ${meticCovered.length} metic-floored routes cross-referenced`);
|
|
244
253
|
});
|
|
245
254
|
|
|
246
255
|
// B3 — the oracle that REPLACES B1 as routes migrate (BV1.R109). For a route gated by
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import assert from 'node:assert/strict';
|
|
14
14
|
import { test } from 'node:test';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
16
17
|
import path from 'node:path';
|
|
17
18
|
import { fileURLToPath } from 'node:url';
|
|
18
19
|
|
|
@@ -188,11 +189,76 @@ test('over the four seeded ranks, requirePermission admits exactly who requireRa
|
|
|
188
189
|
// gate the SEED a rank-role grants and it reproduces the rank gate's verdict.
|
|
189
190
|
// Scoped to catalog.RANK_ORDER — the reserved/divine ranks above archon hold no
|
|
190
191
|
// seeded role, so parity is defined only over the four live ranks.
|
|
192
|
+
// BV1.R105 migrated every rank-gated module route (modules/*/routes/) onto the
|
|
193
|
+
// atoms below. Listing each one beside the requireRank(...) call it REPLACED turns
|
|
194
|
+
// the migration's whole claim — "the atom admits exactly who the rank gate did" —
|
|
195
|
+
// into an assertion instead of prose. Grouped by the gate that stood there before.
|
|
196
|
+
// Atoms that replaced a requireRank('archon') gate AND are still archon after
|
|
197
|
+
// ADR 0157 — rank/identity plus the escalation keys that make the delegation
|
|
198
|
+
// irreversible if moved.
|
|
199
|
+
const REPLACED_ARCHON = [
|
|
200
|
+
'dependency.cross_tier.manage', 'gate_approval.decide',
|
|
201
|
+
'task.confirm.grade_bypass', 'access_request.review',
|
|
202
|
+
];
|
|
203
|
+
// ADR 0157 (owner decision, 2026-08-05) — "the Archon rank is responsible for rank and
|
|
204
|
+
// identity only". Each of these replaced a requireRank('archon') gate in R104/R105 and
|
|
205
|
+
// then had its catalog floor lowered archon→metic, so the audience it must now match is
|
|
206
|
+
// requireRank('metic','archon').
|
|
207
|
+
//
|
|
208
|
+
// These are listed EXPLICITLY rather than derived from catalog.byKey(...).floor on
|
|
209
|
+
// purpose: this test is an independent oracle. Deriving the expectation from the same
|
|
210
|
+
// constant the implementation reads would make it assert `x === x` and it would pass
|
|
211
|
+
// through any future floor change — including an accidental one. A hand-written list
|
|
212
|
+
// means a floor moving without an owner decision reds the build, which is the whole
|
|
213
|
+
// point of keeping parity as an assertion instead of prose.
|
|
214
|
+
const DELEGATED_TO_METIC_BY_ADR_0155 = [
|
|
215
|
+
'autonomy.precheck', 'box.fleet.manage', 'discord.channels.manage', 'idea.ratify',
|
|
216
|
+
'claim.release.any', 'criterion.unsatisfy', 'goal.reorder', 'task.newcomer_restock',
|
|
217
|
+
'task.peer_votes.tally', 'override_request.decide', 'memory.read.any',
|
|
218
|
+
'project.curate', 'provisioning.fleet.manage', 'security.report.adjudicate',
|
|
219
|
+
'session.search',
|
|
220
|
+
// Minted by R105 (POST /versions had no atom); ADR 0157 then lowered it to metic.
|
|
221
|
+
// Its sibling `version.close` stayed archon — authoring delegates, disposition does not.
|
|
222
|
+
'version.create',
|
|
223
|
+
// Minted BY ADR 0157, splitting the Gate page's READ off `gate_approval.decide` so a
|
|
224
|
+
// Metic can open /gate while approving a held PR stays Archon.
|
|
225
|
+
'gate_approval.read',
|
|
226
|
+
];
|
|
227
|
+
const REPLACED_METIC_PLUS = [
|
|
228
|
+
'blocker.review', 'idea.triage', 'criterion.create', 'criterion.review',
|
|
229
|
+
'criterion.satisfy', 'goal.create', 'goal.archive', 'goal.scope.widen',
|
|
230
|
+
'task.create', 'task.edit', 'task.promote', 'task.demote', 'task.abandon',
|
|
231
|
+
'task.vote', 'task.dependency.manage', 'task.criterion.manage',
|
|
232
|
+
'security.report.file',
|
|
233
|
+
];
|
|
191
234
|
const cases = [
|
|
192
|
-
['idea.triage', ['metic', 'archon']],
|
|
193
235
|
['governance.manage', ['archon']],
|
|
194
236
|
['task.claim.any', ['thetes', 'metic', 'archon']],
|
|
195
237
|
['cost.log', ['xenos', 'thetes', 'metic', 'archon']],
|
|
238
|
+
// The CORE route gates migrated in BV1.R104, each paired with the requireRank
|
|
239
|
+
// allow-list it replaced. It is the cheapest proof that a swap did not change
|
|
240
|
+
// WHO can reach a route. R105's module atoms are spread in below; these stay
|
|
241
|
+
// listed explicitly because the REPLACED_* arrays cover modules/*/routes only,
|
|
242
|
+
// so dropping them would silently retire the parity proof for the core gates.
|
|
243
|
+
// (security.report.file / security.report.adjudicate are asserted by the R105
|
|
244
|
+
// arrays below and so are not repeated here.)
|
|
245
|
+
['backup.manage', ['metic', 'archon']], // GET /backup/status, POST /backup/trigger
|
|
246
|
+
['llm_cache.use', ['metic', 'archon']], // POST /llm-cache/{lookup,store}
|
|
247
|
+
['module.enable', ['metic', 'archon']], // POST /modules/:key/enable
|
|
248
|
+
['module.submit', ['metic', 'archon']], // POST /modules/:key/submit, GET /modules/submissions
|
|
249
|
+
// ADR 0157 lowered these three archon→metic: spending, reading the roster, and
|
|
250
|
+
// reading the audit log are administration, not authority over who holds authority.
|
|
251
|
+
['builder.budget.set', ['metic', 'archon']], // PATCH /builders/:id/budget
|
|
252
|
+
['builder.roster.read', ['metic', 'archon']], // GET /builders/roster, /grades/by-builder, /builders/:id/active-claims
|
|
253
|
+
['audit_log.read', ['metic', 'archon']], // GET /audit-log
|
|
254
|
+
// …while these three are the identity half of ADR 0157's rank/identity mandate and
|
|
255
|
+
// stay archon: whose sessions are valid, who holds which rank, and who exists at all.
|
|
256
|
+
['auth.session.revoke', ['archon']], // POST /auth/revoke, POST /auth/revoke-all
|
|
257
|
+
['builder.rank.set', ['archon']], // PATCH /builders/:id/rank
|
|
258
|
+
['builder.status.set', ['archon']], // PATCH /builders/:id/status
|
|
259
|
+
...REPLACED_ARCHON.map((key) => [key, ['archon']]),
|
|
260
|
+
...DELEGATED_TO_METIC_BY_ADR_0155.map((key) => [key, ['metic', 'archon']]),
|
|
261
|
+
...REPLACED_METIC_PLUS.map((key) => [key, ['metic', 'archon']]),
|
|
196
262
|
];
|
|
197
263
|
for (const [permission, allowedRanks] of cases) {
|
|
198
264
|
for (const rank of catalog.RANK_ORDER) {
|
|
@@ -221,3 +287,61 @@ test('a principal permission (floor null) is held by NO rank-role', async () =>
|
|
|
221
287
|
assert.equal(res._status, 403);
|
|
222
288
|
}
|
|
223
289
|
});
|
|
290
|
+
|
|
291
|
+
// BV1.R105 completeness — the negative half of the parity proof above.
|
|
292
|
+
//
|
|
293
|
+
// The per-route pins in route-rank-check.js are satisfied by EITHER spelling, on
|
|
294
|
+
// purpose, so the R104-R107 migration could land incrementally. The cost is that
|
|
295
|
+
// nothing there fails when a call site is simply MISSED: a forgotten requireRank
|
|
296
|
+
// keeps its pin green because the effective authority is unchanged. The parity
|
|
297
|
+
// tests above have the same blind spot — they assert the atoms that WERE replaced
|
|
298
|
+
// behave identically, never that none were left behind.
|
|
299
|
+
//
|
|
300
|
+
// So scan the source instead. R105 left four deliberate holdouts, each carrying a
|
|
301
|
+
// "Still requireRank after BV1.R105" note at its site: their permission atom was
|
|
302
|
+
// declared over the module's WRITES, so the read gate stayed rank-based rather than
|
|
303
|
+
// minting a second atom meaning "may look at this". Anything else is a miss.
|
|
304
|
+
//
|
|
305
|
+
// ADR 0157 cleared THREE of those four. It had to: lowering a write atom to metic
|
|
306
|
+
// while its read sibling stayed on requireRank('archon') would have left the read
|
|
307
|
+
// STRICTER than the write — a Metic able to confirm a vulnerability report but not
|
|
308
|
+
// list one, and a Gate page open to Metic whose only data endpoint 403s. So the ADR
|
|
309
|
+
// extended each atom's declared guards to name its read (and split out
|
|
310
|
+
// `gate_approval.read`, since approving must stay Archon while reading must not),
|
|
311
|
+
// which is exactly the "mint an atom that covers the read" step R105 deferred.
|
|
312
|
+
test('every modules/*/routes gate is migrated, bar the one documented holdout', () => {
|
|
313
|
+
const RETAINED = [
|
|
314
|
+
// Still requireRank after BV1.R105 — this route did not EXIST when R105 was
|
|
315
|
+
// written. modules/security/routes/scans.js landed on main from task 1002547
|
|
316
|
+
// while R105 sat at 'completed' awaiting an owner override decision, so it was
|
|
317
|
+
// authored against the requireRank spelling that was still current. Untouched by
|
|
318
|
+
// ADR 0157: its gate is already requireRank('metic','archon'), so it is not an
|
|
319
|
+
// Archon surface and the delegation had nothing to move. Migrating it is still
|
|
320
|
+
// worth doing — it means minting an atom over the write — but it is the
|
|
321
|
+
// continuing R107/R108 migration's job, not this ADR's.
|
|
322
|
+
'security/routes/scans.js',
|
|
323
|
+
].sort();
|
|
324
|
+
|
|
325
|
+
const modulesDir = path.join(ROOT, 'modules');
|
|
326
|
+
const found = [];
|
|
327
|
+
for (const key of fs.readdirSync(modulesDir)) {
|
|
328
|
+
const routesDir = path.join(modulesDir, key, 'routes');
|
|
329
|
+
if (!fs.existsSync(routesDir)) continue;
|
|
330
|
+
for (const file of fs.readdirSync(routesDir)) {
|
|
331
|
+
if (!file.endsWith('.js')) continue;
|
|
332
|
+
const text = fs.readFileSync(path.join(routesDir, file), 'utf8');
|
|
333
|
+
// Line-based so a requireRank named in prose (there are ~20 such mentions)
|
|
334
|
+
// is not mistaken for a live gate. A gate is always a call.
|
|
335
|
+
const live = text.split('\n').some((line) => {
|
|
336
|
+
const t = line.trim();
|
|
337
|
+
if (t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')) return false;
|
|
338
|
+
return /requireRank\s*\(/.test(line);
|
|
339
|
+
});
|
|
340
|
+
if (live) found.push(`${key}/routes/${file}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
assert.deepEqual(found.sort(), RETAINED,
|
|
345
|
+
'a modules/*/routes gate still calls requireRank — migrate it to requirePermission, ' +
|
|
346
|
+
'or add it to RETAINED with a "Still requireRank after BV1.R105" note explaining why');
|
|
347
|
+
});
|