@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
package/src/bongos/auth.js
CHANGED
|
@@ -56,18 +56,47 @@ const { branding, userAgent } = require('../branding');
|
|
|
56
56
|
|
|
57
57
|
const SCOPE = 'read:user';
|
|
58
58
|
|
|
59
|
+
// The repo-acting scope the onboarding wizard opts into. `repo`, NOT `public_repo`.
|
|
60
|
+
//
|
|
61
|
+
// ADR 0143 §1 originally chose `public_repo` precisely to avoid the all-or-nothing
|
|
62
|
+
// `repo`, and named its own reversal path: "revisit to `repo` only if private
|
|
63
|
+
// support is needed". It is needed. `public_repo` grants write to PUBLIC repos
|
|
64
|
+
// only, so ADOPTING an existing PRIVATE repo dies at the provisioner's scaffold
|
|
65
|
+
// push with GitHub 403 "Write access to repository not granted" — observed live on
|
|
66
|
+
// lars589/hermeslines-marketing. ADR 0155 takes the reversal at the owner's
|
|
67
|
+
// explicit call, accepting the cost it names: a token we persist can now write to
|
|
68
|
+
// EVERY repo the user owns, not just their public ones.
|
|
69
|
+
const REPO_SCOPE = 'repo';
|
|
70
|
+
|
|
59
71
|
// Web-flow OAuth scope. Identity (`read:user`) is all the builders'-hall sign-in
|
|
60
72
|
// and the CLI Device Flow ever need. The onboarding wizard
|
|
61
|
-
// (public-landing/create.html), however, lists + creates
|
|
62
|
-
// GitHub repos (task 1002099) — which
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// token we then persist (ADR 0143). Only the wizard passes { repos:true }; every
|
|
67
|
-
// other sign-in stays identity-only. Pure + exported so the exact scope string
|
|
68
|
-
// (a security-critical value) is unit-testable.
|
|
73
|
+
// (public-landing/create.html), however, lists + creates + is pushed to by the
|
|
74
|
+
// provisioner on the signed-in user's own GitHub repos (task 1002099) — which
|
|
75
|
+
// `read:user` cannot do — so it OPTS IN to REPO_SCOPE. Only the wizard passes
|
|
76
|
+
// { repos:true }; every other sign-in stays identity-only. Pure + exported so the
|
|
77
|
+
// exact scope string (a security-critical value) is unit-testable.
|
|
69
78
|
function scopeForWebFlow({ repos = false } = {}) {
|
|
70
|
-
return repos ? `${SCOPE}
|
|
79
|
+
return repos ? `${SCOPE} ${REPO_SCOPE}` : SCOPE;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Does a scope string GitHub ACTUALLY GRANTED still satisfy the repo surfaces?
|
|
83
|
+
//
|
|
84
|
+
// WHY THIS EXISTS. Widening scopeForWebFlow does NOT upgrade tokens already in
|
|
85
|
+
// `github_oauth_tokens` — a row minted before ADR 0155 carries `public_repo`, which
|
|
86
|
+
// is enough to list and create PUBLIC repos. So every wizard call would still 200
|
|
87
|
+
// while silently omitting the user's PRIVATE repos from the picker, and the failure
|
|
88
|
+
// would resurface far downstream at the provisioner's push. Callers check this and
|
|
89
|
+
// re-prompt (repo_scope_required) instead of degrading quietly.
|
|
90
|
+
//
|
|
91
|
+
// GitHub returns granted scopes comma-separated in its own order (e.g.
|
|
92
|
+
// "public_repo,read:user"), so parse rather than substring-match — `public_repo`
|
|
93
|
+
// CONTAINS "repo" and a naive match would accept exactly the stale token this
|
|
94
|
+
// guards against. `repo` implies `public_repo`, so `repo` alone is sufficient.
|
|
95
|
+
function grantedScopeCoversRepos(granted) {
|
|
96
|
+
return String(granted || '')
|
|
97
|
+
.split(/[,\s]+/)
|
|
98
|
+
.map((s) => s.trim())
|
|
99
|
+
.includes(REPO_SCOPE);
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
// In-memory map of device_code → { interval, expires_at, last_polled_at, github_creds }
|
|
@@ -1263,7 +1292,23 @@ function sessionScope(...keys) {
|
|
|
1263
1292
|
const allowBoxScope = sessionScope(...((sessionScopeFor('box') || { permissions: [] }).permissions));
|
|
1264
1293
|
|
|
1265
1294
|
async function requireBuilder(req, res, next) {
|
|
1266
|
-
|
|
1295
|
+
let session;
|
|
1296
|
+
try {
|
|
1297
|
+
session = await resolveSession(req);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
// REQUIRED, not defensive: Express 4 discards a middleware's returned promise
|
|
1300
|
+
// (Layer.handle_request wraps only synchronous throws), so an uncaught rejection
|
|
1301
|
+
// here reaches no handler — the request hangs AND the process dies. Every async
|
|
1302
|
+
// middleware mounted directly on a router must own its rejection.
|
|
1303
|
+
//
|
|
1304
|
+
// next(err) rather than res.fail(): the envelope handler already renders and logs
|
|
1305
|
+
// it, and inventing a code here would need an api-errors registration to avoid
|
|
1306
|
+
// classifying as 500. Denying is correct regardless — if the session cannot be
|
|
1307
|
+
// resolved, the caller's identity is unknown, so admitting is never an option.
|
|
1308
|
+
// runPageGate below holds the same guard; the two auth paths must not diverge.
|
|
1309
|
+
next(err);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1267
1312
|
if (!session) {
|
|
1268
1313
|
res.fail('unauthenticated', 401);
|
|
1269
1314
|
return;
|
|
@@ -1335,6 +1380,11 @@ async function requireBuilder(req, res, next) {
|
|
|
1335
1380
|
// reach the write path (ADR 0026 §6C). Read fresh per request like rank.
|
|
1336
1381
|
system_role: session.system_role ?? null,
|
|
1337
1382
|
};
|
|
1383
|
+
// The session-load fold (task 1002556): raw grant keys the session query
|
|
1384
|
+
// carried, read as fresh as rank above. requirePermission feeds them to the
|
|
1385
|
+
// pure catalog-drop instead of a second round-trip; null (fold off, or a test
|
|
1386
|
+
// stub without the column) means the gate falls back to the DB resolve.
|
|
1387
|
+
req.rawPermissionKeys = Array.isArray(session.raw_permission_keys) ? session.raw_permission_keys : null;
|
|
1338
1388
|
next();
|
|
1339
1389
|
}
|
|
1340
1390
|
|
|
@@ -1373,9 +1423,12 @@ function signInWithReturn(req) {
|
|
|
1373
1423
|
// R107 replaced each gate's hand-rolled rank test with the catalog key sitting at
|
|
1374
1424
|
// that gate's EXISTING rank floor, so the audience is unchanged over the four live
|
|
1375
1425
|
// ranks (catalog.RANK_ORDER):
|
|
1376
|
-
// requireBuilderPage
|
|
1377
|
-
// requireNonXenosPage
|
|
1378
|
-
//
|
|
1426
|
+
// requireBuilderPage was "any session" floor xenos → page.view.public
|
|
1427
|
+
// requireNonXenosPage was rank !== 'xenos' floor thetes → page.view.builder
|
|
1428
|
+
// requireGovernancePage was rank === 'archon' floor archon → page.view.governance
|
|
1429
|
+
// That third gate was spelled `requireArchonPage` until ADR 0157 lowered
|
|
1430
|
+
// page.view.governance's floor archon→metic and the name stopped being true; the
|
|
1431
|
+
// floor now lives in modules/governance/catalog.js, not in the function's name.
|
|
1379
1432
|
// The one accepted divergence is the one governance_002 already documents
|
|
1380
1433
|
// (tests/governance_parity.mjs Part D): a builder on a reserved/divine rank holds
|
|
1381
1434
|
// no seeded role, so they now fail a page gate a bare rank test would have
|
|
@@ -1384,14 +1437,16 @@ function signInWithReturn(req) {
|
|
|
1384
1437
|
// `deniedTo` is NEVER the sign-in flow. A visitor on that branch already HAS a
|
|
1385
1438
|
// session, so bouncing them into sign-in returns them to the same gate — an
|
|
1386
1439
|
// infinite loop. Every destination below is an ungated hall page:
|
|
1387
|
-
// - /builders the hall home. A signed-in
|
|
1388
|
-
//
|
|
1440
|
+
// - /builders the hall home. A signed-in builder who lacks the page's
|
|
1441
|
+
// permission IS still a citizen, so send them home.
|
|
1389
1442
|
// - /builders/not-ready the friendly notice for the orientation reading rooms;
|
|
1390
1443
|
// it shows the rank ladder and a way back to the hall.
|
|
1391
1444
|
const PAGE_GATES = Object.freeze({
|
|
1392
1445
|
requireBuilderPage: Object.freeze({ permission: 'page.view.public', deniedTo: '/builders' }),
|
|
1393
1446
|
requireNonXenosPage: Object.freeze({ permission: 'page.view.builder', deniedTo: '/builders/not-ready' }),
|
|
1394
|
-
|
|
1447
|
+
requireGovernancePage: Object.freeze({ permission: 'page.view.governance', deniedTo: '/builders' }),
|
|
1448
|
+
requireGovernanceManagePage: Object.freeze({ permission: 'governance.manage', deniedTo: '/builders' }),
|
|
1449
|
+
requireProjectCuratePage: Object.freeze({ permission: 'project.curate', deniedTo: '/builders' }),
|
|
1395
1450
|
});
|
|
1396
1451
|
|
|
1397
1452
|
// A builder's effective permission set, or NULL when authority cannot be
|
|
@@ -1399,14 +1454,31 @@ const PAGE_GATES = Object.freeze({
|
|
|
1399
1454
|
// threw). Every caller must read null as a DENIAL: admitting when the authority
|
|
1400
1455
|
// system is missing would make disabling a module a privilege escalation — the
|
|
1401
1456
|
// inversion ADR 0016 exists to prevent. Resolved per request, never memoised.
|
|
1402
|
-
|
|
1457
|
+
//
|
|
1458
|
+
// The session-load FOLD (task 1002556): when the caller's session row already
|
|
1459
|
+
// carried the raw grant keys (db-kernel's lookupSession aggregates them in the
|
|
1460
|
+
// same round-trip when governance is enabled), the catalog-drop runs pure via the
|
|
1461
|
+
// port — same instant, same request, one round-trip instead of two. This is a
|
|
1462
|
+
// query fold, not the cross-request memo ADR 0016 forbids. Anything unexpected —
|
|
1463
|
+
// no raw keys on the row, an older port without the pure half, a throw — falls
|
|
1464
|
+
// through to the DB resolve, so the fold can only ever be an optimization.
|
|
1465
|
+
async function resolveHeldPermissions(builderId, rawKeys) {
|
|
1403
1466
|
if (builderId === null || builderId === undefined || builderId === '') return null;
|
|
1404
1467
|
const governance = seams.resolveOptional('governance');
|
|
1405
1468
|
if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
|
|
1469
|
+
if (Array.isArray(rawKeys) && typeof governance.effectivePermissions === 'function') {
|
|
1470
|
+
try {
|
|
1471
|
+
return new Set(governance.effectivePermissions(rawKeys));
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
console.error('[gds] permission fold failed:', err && err.message);
|
|
1474
|
+
// fall through to the DB resolve
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1406
1477
|
try {
|
|
1407
1478
|
return new Set((await governance.resolveBuilderPermissions(builderId)) || []);
|
|
1408
1479
|
} catch (err) {
|
|
1409
|
-
|
|
1480
|
+
// Message only — a resolver error can carry query text or row data.
|
|
1481
|
+
console.error('[gds] permission resolve failed:', err && err.message);
|
|
1410
1482
|
return null;
|
|
1411
1483
|
}
|
|
1412
1484
|
}
|
|
@@ -1428,7 +1500,7 @@ async function runPageGate(name, req, res, next) {
|
|
|
1428
1500
|
res.redirect(302, signInWithReturn(req));
|
|
1429
1501
|
return;
|
|
1430
1502
|
}
|
|
1431
|
-
const held = await resolveHeldPermissions(session.builder_id);
|
|
1503
|
+
const held = await resolveHeldPermissions(session.builder_id, session.raw_permission_keys);
|
|
1432
1504
|
if (held === null || !held.has(permission)) {
|
|
1433
1505
|
res.redirect(302, deniedTo);
|
|
1434
1506
|
return;
|
|
@@ -1445,12 +1517,32 @@ async function requireBuilderPage(req, res, next) {
|
|
|
1445
1517
|
}
|
|
1446
1518
|
|
|
1447
1519
|
// The /watch monitoring surface, the /harbor dev-box page and the /gate page
|
|
1448
|
-
// (#726, idea 257, task 1179) —
|
|
1449
|
-
//
|
|
1450
|
-
//
|
|
1451
|
-
//
|
|
1452
|
-
|
|
1453
|
-
|
|
1520
|
+
// (#726, idea 257, task 1179) — Metic and up, per `page.view.governance`'s catalog
|
|
1521
|
+
// floor. Named `requireArchonPage` until ADR 0157 lowered that floor archon→metic
|
|
1522
|
+
// (older ADRs still use the old name; this line is the trail). Defense-in-depth:
|
|
1523
|
+
// every data endpoint these pages call is independently gated server-side, so no
|
|
1524
|
+
// cross-builder data leaks even if this gate were bypassed; it keeps a builder
|
|
1525
|
+
// without the permission from ever seeing the page shell.
|
|
1526
|
+
async function requireGovernancePage(req, res, next) {
|
|
1527
|
+
return runPageGate('requireGovernancePage', req, res, next);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// The /governance roles-and-permissions tab (BV1.R113) — gated on the
|
|
1531
|
+
// `governance.manage` ATOM itself (system, archon floor), the same key every
|
|
1532
|
+
// /governance/* data route checks, so the page layer can never drift wider than
|
|
1533
|
+
// the API it fronts. Defense-in-depth as above: the data endpoints are all
|
|
1534
|
+
// independently gated; this keeps the tab shell from rendering for anyone else.
|
|
1535
|
+
async function requireGovernanceManagePage(req, res, next) {
|
|
1536
|
+
return runPageGate('requireGovernanceManagePage', req, res, next);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// The /people builder-directory page (task 1002577) — gated on the
|
|
1540
|
+
// `project.curate` ATOM, the same key its one data route (GET /scouting,
|
|
1541
|
+
// modules/platform-identity) checks, so the page layer can never drift wider
|
|
1542
|
+
// than the API it fronts. Spec R14 (goal 1000045) widens the atom's audience
|
|
1543
|
+
// later; this gate follows it automatically.
|
|
1544
|
+
async function requireProjectCuratePage(req, res, next) {
|
|
1545
|
+
return runPageGate('requireProjectCuratePage', req, res, next);
|
|
1454
1546
|
}
|
|
1455
1547
|
|
|
1456
1548
|
// The orientation reading rooms (the citizen's primer, the system-maps/diagrams)
|
|
@@ -1590,6 +1682,11 @@ function requireRank(...allowed) {
|
|
|
1590
1682
|
// UNCACHED — resolved on the request that needs it, exactly as requireRank reads
|
|
1591
1683
|
// the live rank. No memo here and none in resolver.js (ADR 0016).
|
|
1592
1684
|
//
|
|
1685
|
+
// The round-trip this costs (where requireRank compared in memory) IS the
|
|
1686
|
+
// guarantee: a revoked role stops working on the very next request. A hot route
|
|
1687
|
+
// should shed the gate, not the freshness — see task 1002556 for folding the
|
|
1688
|
+
// resolve into requireBuilder's existing session query.
|
|
1689
|
+
//
|
|
1593
1690
|
// The 403 body mirrors requireRank's: `required` is the literal permission list
|
|
1594
1691
|
// the gate names, and `held` is the subset of THOSE the builder actually holds —
|
|
1595
1692
|
// or null when the set could not be resolved at all (the requireRank `actual ??
|
|
@@ -1606,7 +1703,9 @@ function requirePermission(...perms) {
|
|
|
1606
1703
|
if (required.length === 0) return deny(null);
|
|
1607
1704
|
// Shared with the page gates (R107): null = authority could not be established
|
|
1608
1705
|
// (no port / malformed port / a throwing resolver) and is always a denial.
|
|
1609
|
-
|
|
1706
|
+
// req.rawPermissionKeys rides the fold (task 1002556) when the session query
|
|
1707
|
+
// carried the grant keys; absent, the DB resolve runs exactly as before.
|
|
1708
|
+
const held = await resolveHeldPermissions(builderId, req.rawPermissionKeys);
|
|
1610
1709
|
if (held === null) return deny(null);
|
|
1611
1710
|
if (required.every((key) => held.has(key))) return next();
|
|
1612
1711
|
return deny(required.filter((key) => held.has(key)));
|
|
@@ -1663,6 +1762,7 @@ module.exports = {
|
|
|
1663
1762
|
pollDeviceFlow,
|
|
1664
1763
|
buildAuthorizeUrl,
|
|
1665
1764
|
scopeForWebFlow,
|
|
1765
|
+
grantedScopeCoversRepos,
|
|
1666
1766
|
buildIdpAuthorizeUrl,
|
|
1667
1767
|
idpAuthorizeUrl,
|
|
1668
1768
|
completeWebFlow,
|
|
@@ -1683,8 +1783,10 @@ module.exports = {
|
|
|
1683
1783
|
SESSION_SCOPES,
|
|
1684
1784
|
requireBuilder,
|
|
1685
1785
|
requireBuilderPage,
|
|
1686
|
-
|
|
1786
|
+
requireGovernancePage,
|
|
1787
|
+
requireGovernanceManagePage,
|
|
1687
1788
|
requireNonXenosPage,
|
|
1789
|
+
requireProjectCuratePage,
|
|
1688
1790
|
requireRank,
|
|
1689
1791
|
rankMeetsThreshold,
|
|
1690
1792
|
requirePermission,
|
package/src/bongos/db-kernel.js
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
// (grading) lift their query clusters into modules/<key>/db.js the same way,
|
|
13
13
|
// reaching these kernel helpers + `pool` through the doorway.
|
|
14
14
|
//
|
|
15
|
-
// Boundary: this file depends
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// Boundary: this file depends only on ./pool (the singleton Postgres pool) and
|
|
16
|
+
// ../module-seams (the kernel switchboard — kernel→kernel, added for the
|
|
17
|
+
// session-grants fold, task 1002556). It must NEVER require ./db (that would
|
|
18
|
+
// invert the base→monolith direction and risk a load cycle); db.js requires
|
|
19
|
+
// THIS file, not the reverse.
|
|
18
20
|
//
|
|
19
21
|
// db.js re-exports every name below, so its 23 importers + its own internal
|
|
20
22
|
// uses are byte-identical — nothing downstream knows the slice moved.
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
'use strict';
|
|
23
25
|
|
|
24
26
|
const { pool } = require('./pool');
|
|
27
|
+
const seams = require('../module-seams');
|
|
25
28
|
|
|
26
29
|
// withTx(fn, {pool}) — run fn(client) inside BEGIN/COMMIT, ROLLBACK+rethrow on
|
|
27
30
|
// error, always release. The pool option preserves the deps.pool test seam.
|
|
@@ -115,6 +118,17 @@ async function insertAuditLog({
|
|
|
115
118
|
// module can compare ranks without reaching core internals.
|
|
116
119
|
const LIVE_RANK_LADDER = ['xenos', 'thetes', 'metic', 'archon'];
|
|
117
120
|
|
|
121
|
+
// RANK_TIER — the ladder as a 1-based rank → tier map, DERIVED so it cannot
|
|
122
|
+
// drift (BV1.R108). THE canonical tier map: importable code reads this via the
|
|
123
|
+
// doorway (api.RANK_TIER). The few deliberately-local copies — dependency-free
|
|
124
|
+
// substrate (permission-path-check), standalone CLIs, hall browser statics, and
|
|
125
|
+
// src/bongos/db.js whose literal gate-review.js + diagram-facts.js parse as
|
|
126
|
+
// SOURCE TEXT — are each pinned to this map by tests/rank_tier_single_source.mjs,
|
|
127
|
+
// which also fails on any NEW parallel rank-tier map or ladder literal.
|
|
128
|
+
const RANK_TIER = Object.freeze(
|
|
129
|
+
Object.fromEntries(LIVE_RANK_LADDER.map((rank, i) => [rank, i + 1])),
|
|
130
|
+
);
|
|
131
|
+
|
|
118
132
|
// RANKS — the FULL historical rank vocabulary, ordered high→low (lower index =
|
|
119
133
|
// higher authority). ORDER IS LOAD-BEARING: setBuilderRank + auth's requireRank
|
|
120
134
|
// threshold both use RANKS.indexOf to compare authority. A kernel constant
|
|
@@ -132,13 +146,23 @@ const RANKS = [
|
|
|
132
146
|
// `pool`; db.js re-exports every name so its other importers are unchanged.
|
|
133
147
|
|
|
134
148
|
// upsertBuilder — create or update the builder row for a GitHub identity.
|
|
149
|
+
//
|
|
150
|
+
// This runs on EVERY sign-in, not just at account creation, so the conflict
|
|
151
|
+
// branch is a re-sync from GitHub — which used to clobber a builder's
|
|
152
|
+
// self-chosen display name on their next authentication (task 1002568). The
|
|
153
|
+
// display_name CASE below is the "your name wins, always" guard: GitHub keeps
|
|
154
|
+
// seeding and re-syncing the name for anyone who never renamed
|
|
155
|
+
// (display_name_source='github'), and never overwrites one the builder set
|
|
156
|
+
// themselves ('user', stamped by setBuilderDisplayName). See ADR 0156.
|
|
135
157
|
async function upsertBuilder({ githubId, githubLogin, displayName, avatarUrl }) {
|
|
136
158
|
const { rows } = await pool.query(
|
|
137
159
|
`INSERT INTO builders (github_id, github_login, display_name, avatar_url)
|
|
138
160
|
VALUES ($1, $2, $3, $4)
|
|
139
161
|
ON CONFLICT (github_id) DO UPDATE
|
|
140
162
|
SET github_login = EXCLUDED.github_login,
|
|
141
|
-
display_name =
|
|
163
|
+
display_name = CASE WHEN builders.display_name_source = 'user'
|
|
164
|
+
THEN builders.display_name
|
|
165
|
+
ELSE EXCLUDED.display_name END,
|
|
142
166
|
avatar_url = EXCLUDED.avatar_url
|
|
143
167
|
RETURNING id, github_id, github_login, display_name, avatar_url, total_credits, created_at, rank, preferred_disciplines`,
|
|
144
168
|
[String(githubId), githubLogin, displayName, avatarUrl ?? null]
|
|
@@ -234,12 +258,47 @@ async function createSession({ builderId, token, source, userAgent, expiresAt })
|
|
|
234
258
|
// builder identity, and refresh-on-use (bump last_used_at + roll expiry forward,
|
|
235
259
|
// fire-and-forget). Returns null for missing/revoked/expired. Read fresh per
|
|
236
260
|
// request, never cached (ADR 0016).
|
|
261
|
+
//
|
|
262
|
+
// The governance FOLD (task 1002556): when the governance module is enabled, the
|
|
263
|
+
// same round-trip also aggregates the builder's raw permission grant keys, so
|
|
264
|
+
// requirePermission/runPageGate need no second query — one snapshot per request,
|
|
265
|
+
// still uncached (this is a query fold, not the cross-request memo ADR 0016
|
|
266
|
+
// forbids). Enablement is boot-static config, so the SQL shape is chosen once at
|
|
267
|
+
// load; on a governance-less instance the base query runs (the tables may not
|
|
268
|
+
// exist there) and permission gates keep failing closed via the missing port.
|
|
269
|
+
// The kernel stays MODULE-AGNOSTIC (ADR 0083 §Decision #4 — no module key may be
|
|
270
|
+
// hardcoded here): the fragment arrives through the capability-named seam
|
|
271
|
+
// 'session-grants-sql', which the authority module registers at boot alongside
|
|
272
|
+
// its resolver port. The fragment is that module's own SQL over its own tables —
|
|
273
|
+
// a trusted boot-time constant, never user input — and it must yield a single
|
|
274
|
+
// `raw_permission_keys` column against the alias `s` (builder_sessions). No
|
|
275
|
+
// provider (module disabled, or a boot path that never mounts routes) → the base
|
|
276
|
+
// query runs and permission gates keep failing closed via their missing port.
|
|
277
|
+
// Latched only once FOUND: registration order vs the first lookup is a boot
|
|
278
|
+
// race, and resolveOptional on a miss is a cheap map lookup.
|
|
279
|
+
//
|
|
280
|
+
// Shape guard: the fragment must be exactly one comma-led parenthesized scalar
|
|
281
|
+
// subquery aliased raw_permission_keys, with no statement separator. A module
|
|
282
|
+
// registering seams is in-process trusted code (it could reach the pool
|
|
283
|
+
// directly), so this guards MISREGISTRATION — a malformed or mis-aimed
|
|
284
|
+
// fragment degrades to the base query instead of corrupting the session SQL.
|
|
285
|
+
const FRAGMENT_SHAPE = /^,\s*\(SELECT[^;]*\)\s*AS raw_permission_keys\s*$/;
|
|
286
|
+
let SESSION_PERMS_SELECT = null;
|
|
287
|
+
function sessionPermsSelect() {
|
|
288
|
+
if (SESSION_PERMS_SELECT !== null) return SESSION_PERMS_SELECT;
|
|
289
|
+
const provider = seams.resolveOptional('session-grants-sql');
|
|
290
|
+
if (!provider || typeof provider.selectFragment !== 'string' || !FRAGMENT_SHAPE.test(provider.selectFragment)) {
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
SESSION_PERMS_SELECT = provider.selectFragment;
|
|
294
|
+
return SESSION_PERMS_SELECT;
|
|
295
|
+
}
|
|
237
296
|
async function lookupSession(token) {
|
|
238
297
|
if (!token) return null;
|
|
239
298
|
const { rows } = await pool.query(
|
|
240
299
|
`SELECT s.id AS session_id, s.builder_id, s.expires_at, s.revoked_at, s.source,
|
|
241
300
|
b.github_id, b.github_login, b.display_name, b.avatar_url, b.total_credits,
|
|
242
|
-
b.rank, b.status, b.system_role, b.anthropic_user_id
|
|
301
|
+
b.rank, b.status, b.system_role, b.anthropic_user_id${sessionPermsSelect()}
|
|
243
302
|
FROM builder_sessions s
|
|
244
303
|
JOIN builders b ON b.id = s.builder_id
|
|
245
304
|
WHERE s.token = $1`,
|
|
@@ -263,10 +322,12 @@ async function lookupSession(token) {
|
|
|
263
322
|
|
|
264
323
|
module.exports = {
|
|
265
324
|
withTx,
|
|
325
|
+
sessionPermsSelect,
|
|
266
326
|
getBuilderById,
|
|
267
327
|
listActiveBuilders,
|
|
268
328
|
insertAuditLog,
|
|
269
329
|
LIVE_RANK_LADDER,
|
|
330
|
+
RANK_TIER,
|
|
270
331
|
RANKS,
|
|
271
332
|
SESSION_TTL_MS,
|
|
272
333
|
upsertBuilder,
|
package/src/bongos/db.js
CHANGED
|
@@ -298,13 +298,12 @@ async function deleteGithubToken({ builderId, p = pool }) {
|
|
|
298
298
|
// shipped." Pure decision helper is exported so tests/rank_gate.mjs can exercise
|
|
299
299
|
// the threshold without a DB.
|
|
300
300
|
|
|
301
|
-
// REQUIRES_RANK_TIER — the
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
// (
|
|
307
|
-
// db.REQUIRES_RANK_TIER export depend on it.
|
|
301
|
+
// REQUIRES_RANK_TIER — the rank "brain" literal. The CANONICAL map is now
|
|
302
|
+
// db-kernel.RANK_TIER, derived from LIVE_RANK_LADDER (BV1.R108); this stays an
|
|
303
|
+
// inline object literal ONLY because gate-review.js (the root-of-trust region
|
|
304
|
+
// watch) and diagram-facts.js parse it as SOURCE TEXT, which a derived value
|
|
305
|
+
// would break. tests/rank_tier_single_source.mjs pins it byte-identical to the
|
|
306
|
+
// kernel map (and permission-path-check.js RANK_TIER to both — the #863 class).
|
|
308
307
|
const REQUIRES_RANK_TIER = { xenos: 1, thetes: 2, metic: 3, archon: 4 };
|
|
309
308
|
|
|
310
309
|
function singleRungPromotionError(oldRank, newRank) {
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Raw `fetch` against GitHub's REST API (the house style — see auth.js
|
|
5
5
|
// fetchGithubUser; no octokit dependency). The user-to-server token these take is
|
|
6
|
-
// the `
|
|
7
|
-
// github_oauth_tokens
|
|
8
|
-
//
|
|
9
|
-
// never
|
|
6
|
+
// the `repo`-scoped token the web callback captured and stored (db.js
|
|
7
|
+
// github_oauth_tokens; `repo` since ADR 0155 — `public_repo` could not write a
|
|
8
|
+
// private repo, so private repos were unadoptable); callers pass the decrypted
|
|
9
|
+
// token in, this module never touches storage. `fetchImpl` is injectable so tests
|
|
10
|
+
// can drive it with a mock and never hit the network.
|
|
10
11
|
|
|
11
12
|
const { userAgent } = require('../branding');
|
|
12
13
|
|
|
@@ -39,6 +40,10 @@ function ghHeaders(token, extra = {}) {
|
|
|
39
40
|
// `login/repo` and the org-OAuth-policy complexity is out of scope for v1),
|
|
40
41
|
// most-recently-updated first, optionally filtered by a case-insensitive substring
|
|
41
42
|
// `q` on the full name. Returns [{ full_name, name, owner, private }].
|
|
43
|
+
//
|
|
44
|
+
// No `visibility` filter: under `repo` (ADR 0155) GitHub returns the user's PRIVATE
|
|
45
|
+
// repos here too, which is the point — they are adoptable now. That is what lights
|
|
46
|
+
// up the picker's long-dormant "Private" badge (public-landing/create.html).
|
|
42
47
|
async function listUserRepos(token, { q = '', fetchImpl = fetch } = {}) {
|
|
43
48
|
const out = [];
|
|
44
49
|
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
@@ -68,9 +73,16 @@ async function listUserRepos(token, { q = '', fetchImpl = fetch } = {}) {
|
|
|
68
73
|
|
|
69
74
|
// Create an EMPTY repo in the signed-in user's personal account. auto_init:false =
|
|
70
75
|
// no README/first commit (exactly the "leave it completely empty" the manual step
|
|
71
|
-
// demanded)
|
|
72
|
-
//
|
|
73
|
-
//
|
|
76
|
+
// demanded).
|
|
77
|
+
//
|
|
78
|
+
// private:false is now a PRODUCT choice, not a credential limit: since ADR 0155 the
|
|
79
|
+
// token holds `repo` and COULD create a private repo, but a Cloud Bongos instance is
|
|
80
|
+
// open-source / capture-resistant by design, so a repo WE create defaults to public.
|
|
81
|
+
// A user who wants a private instance adopts a private repo they made themselves.
|
|
82
|
+
// Deliberately not a parameter — nothing should be able to flip this implicitly.
|
|
83
|
+
//
|
|
84
|
+
// Returns { full_name, html_url }. A 422 (name already exists / invalid) becomes a
|
|
85
|
+
// typed `name_taken` error the route maps to a friendly 409 rather than an opaque 500.
|
|
74
86
|
async function createUserRepo(token, { name, fetchImpl = fetch } = {}) {
|
|
75
87
|
const res = await fetchImpl(`${GH_API}/user/repos`, {
|
|
76
88
|
method: 'POST',
|
|
@@ -110,7 +110,7 @@ function check(goalScopeModules, touches, builderRank) {
|
|
|
110
110
|
if (!prot.ok) {
|
|
111
111
|
const hits = prot.protected_hits.map((h) => h.file);
|
|
112
112
|
return deny('protected_path',
|
|
113
|
-
`Builder rank '${builderRank}' is below Metic and may not author a task touching protected path(s): ${hits.join(', ')} (ADR 0016, SEC #863).
|
|
113
|
+
`Builder rank '${builderRank}' is below Metic and may not author a task touching protected path(s): ${hits.join(', ')} (ADR 0016, SEC #863). A Metic+ builder must own this work.`,
|
|
114
114
|
{ protected_hits: prot.protected_hits });
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -215,7 +215,9 @@ const MODULE_GLOBS = {
|
|
|
215
215
|
// provisioning / bring-up work (BV1.R91, goal 1000051 C2), not just modules/dev-box/.
|
|
216
216
|
// The box BRING-UP scripts (infra/box-*.sh — box-source-fetch, heartbeat, terminal
|
|
217
217
|
// units) are INSTANCE-VENDORED (ADR 0144; not tracked in core), so they are not
|
|
218
|
-
// listed here yet — they come under this key when de-vendored (
|
|
218
|
+
// listed here yet — they come under this key when de-vendored. (ADR 0150 /
|
|
219
|
+
// task 1002386 made box.js RESOLVE infra/ from the instance root and fail
|
|
220
|
+
// loudly when it is missing; de-vendoring infra/ into core is still open.)
|
|
219
221
|
'dev-box': [
|
|
220
222
|
'modules/dev-box/', 'devbox-app/',
|
|
221
223
|
'scripts/gds/box.js', 'scripts/gds/box-auth-check.js', 'scripts/gds/box-connect-lib.js',
|
|
@@ -223,7 +223,7 @@ function checkPermissionPaths({ changedFiles, builderRank } = {}) {
|
|
|
223
223
|
`${h.file} is a permission-sensitive surface (matches \`${h.glob}\`). `
|
|
224
224
|
+ `Builder rank '${builderRank}' is below ${floorLabel} and may not modify the rank / permission / `
|
|
225
225
|
+ `ship-grade / deploy / migration machinery (ADR 0016, SEC #863). `
|
|
226
|
-
+ `If this change is genuinely needed,
|
|
226
|
+
+ `If this change is genuinely needed, a ${floorLabel}+ builder must own it.`,
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
return {
|