@bongos/core 1.18.18 → 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 +515 -195
- 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 +99 -14
- 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 -1
- 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 +124 -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/modules/governance/db.js
CHANGED
|
@@ -116,10 +116,112 @@ async function syncRankRole(client, { builderId, rank } = {}) {
|
|
|
116
116
|
return { target: plan.target, added: [...plan.add], removed: [...plan.remove] };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// --- the R110 CRUD surface (roles / grants / assignments) --------------------
|
|
120
|
+
//
|
|
121
|
+
// The db half of the governance API routes. Deliberately DUMB writes: the
|
|
122
|
+
// authority invariants (system perms non-grantable, seeded rank-roles immutable
|
|
123
|
+
// through the API) are enforced in routes/governance.js against the catalog —
|
|
124
|
+
// the same split governance_001 documents (permission_key is validated app-side,
|
|
125
|
+
// not a FK). Grant/assign are ON CONFLICT DO NOTHING so the tab's toggle is
|
|
126
|
+
// idempotent; each returns whether a row actually changed.
|
|
127
|
+
|
|
128
|
+
async function listRolesWithGrants() {
|
|
129
|
+
const { rows } = await pool.query(
|
|
130
|
+
`SELECT r.role_key, r.label, r.description, r.is_system, r.seeded_rank,
|
|
131
|
+
r.created_by, r.created_at,
|
|
132
|
+
COALESCE(array_agg(rp.permission_key ORDER BY rp.permission_key)
|
|
133
|
+
FILTER (WHERE rp.permission_key IS NOT NULL), '{}') AS permissions
|
|
134
|
+
FROM governance_roles r
|
|
135
|
+
LEFT JOIN governance_role_permissions rp ON rp.role_key = r.role_key
|
|
136
|
+
GROUP BY r.id
|
|
137
|
+
ORDER BY r.is_system DESC, r.role_key`,
|
|
138
|
+
);
|
|
139
|
+
return rows;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function getRoleByKey(roleKey) {
|
|
143
|
+
const { rows } = await pool.query(
|
|
144
|
+
`SELECT role_key, label, description, is_system, seeded_rank, created_by, created_at
|
|
145
|
+
FROM governance_roles
|
|
146
|
+
WHERE role_key = $1`,
|
|
147
|
+
[roleKey],
|
|
148
|
+
);
|
|
149
|
+
return rows[0] ?? null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function createRole({ roleKey, label, description, createdBy }) {
|
|
153
|
+
const { rows } = await pool.query(
|
|
154
|
+
`INSERT INTO governance_roles (role_key, label, description, is_system, seeded_rank, created_by)
|
|
155
|
+
VALUES ($1, $2, $3, false, NULL, $4)
|
|
156
|
+
RETURNING role_key, label, description, is_system, seeded_rank, created_by, created_at`,
|
|
157
|
+
[roleKey, label, description ?? null, createdBy],
|
|
158
|
+
);
|
|
159
|
+
return rows[0];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function grantRolePermission({ roleKey, permissionKey }) {
|
|
163
|
+
const { rowCount } = await pool.query(
|
|
164
|
+
`INSERT INTO governance_role_permissions (role_key, permission_key)
|
|
165
|
+
VALUES ($1, $2)
|
|
166
|
+
ON CONFLICT (role_key, permission_key) DO NOTHING`,
|
|
167
|
+
[roleKey, permissionKey],
|
|
168
|
+
);
|
|
169
|
+
return rowCount > 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function revokeRolePermission({ roleKey, permissionKey }) {
|
|
173
|
+
const { rowCount } = await pool.query(
|
|
174
|
+
`DELETE FROM governance_role_permissions
|
|
175
|
+
WHERE role_key = $1 AND permission_key = $2`,
|
|
176
|
+
[roleKey, permissionKey],
|
|
177
|
+
);
|
|
178
|
+
return rowCount > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function listAssignments() {
|
|
182
|
+
const { rows } = await pool.query(
|
|
183
|
+
`SELECT br.builder_id, b.github_login, b.display_name, b.rank,
|
|
184
|
+
br.role_key, r.label AS role_label, r.is_system, r.seeded_rank,
|
|
185
|
+
br.assigned_by, br.assigned_at
|
|
186
|
+
FROM governance_builder_roles br
|
|
187
|
+
JOIN builders b ON b.id = br.builder_id
|
|
188
|
+
JOIN governance_roles r ON r.role_key = br.role_key
|
|
189
|
+
ORDER BY br.builder_id, br.role_key`,
|
|
190
|
+
);
|
|
191
|
+
return rows;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function assignRole({ builderId, roleKey, assignedBy }) {
|
|
195
|
+
const { rowCount } = await pool.query(
|
|
196
|
+
`INSERT INTO governance_builder_roles (builder_id, role_key, assigned_by)
|
|
197
|
+
VALUES ($1, $2, $3)
|
|
198
|
+
ON CONFLICT (builder_id, role_key) DO NOTHING`,
|
|
199
|
+
[builderId, roleKey, assignedBy],
|
|
200
|
+
);
|
|
201
|
+
return rowCount > 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function unassignRole({ builderId, roleKey }) {
|
|
205
|
+
const { rowCount } = await pool.query(
|
|
206
|
+
`DELETE FROM governance_builder_roles
|
|
207
|
+
WHERE builder_id = $1 AND role_key = $2`,
|
|
208
|
+
[builderId, roleKey],
|
|
209
|
+
);
|
|
210
|
+
return rowCount > 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
119
213
|
module.exports = {
|
|
120
214
|
getPermissionKeysForBuilder,
|
|
121
215
|
getRoleKeysForBuilder,
|
|
122
216
|
getSeededRankRoles,
|
|
123
217
|
getRoleAssignmentsForBuilder,
|
|
124
218
|
syncRankRole,
|
|
219
|
+
listRolesWithGrants,
|
|
220
|
+
getRoleByKey,
|
|
221
|
+
createRole,
|
|
222
|
+
grantRolePermission,
|
|
223
|
+
revokeRolePermission,
|
|
224
|
+
listAssignments,
|
|
225
|
+
assignRole,
|
|
226
|
+
unassignRole,
|
|
125
227
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- governance_006_version_create.sql — grant the new `version.create` atom to the
|
|
2
|
+
-- Archon rank-role (BV1.R105; task 1001847).
|
|
3
|
+
--
|
|
4
|
+
-- `POST /versions` was the ONE rank-gated module write route the R105 migration could
|
|
5
|
+
-- not finish: no catalog atom named version CREATION. `version.close` is a different
|
|
6
|
+
-- capability (the close disposition), so the route stayed on requireRank while the
|
|
7
|
+
-- other 60 moved. catalog.js now carries `version.create`, floor `archon`,
|
|
8
|
+
-- system:false — see the comment beside the row for why it is NOT system:true.
|
|
9
|
+
--
|
|
10
|
+
-- Same shape as governance_004 (a BRAND-NEW key, not a floor move): no role holds it
|
|
11
|
+
-- yet. But floor `archon` is the TOP rung, so the cumulative superset RANK_ROLE_SEED
|
|
12
|
+
-- builds contains it for exactly ONE role — hence a single grant block here, where 004
|
|
13
|
+
-- needed two.
|
|
14
|
+
--
|
|
15
|
+
-- THIS MIGRATION IS LOAD-BEARING, not bookkeeping. The seed (governance_002) is
|
|
16
|
+
-- run-once, so a database that has already migrated would never learn the new key:
|
|
17
|
+
-- the archon rank-role would not hold `version.create`, and the freshly-migrated route
|
|
18
|
+
-- would 403 every Archon — locking the instance out of creating versions. The grant
|
|
19
|
+
-- has to arrive as its own migration for the change to be behaviour-identical in
|
|
20
|
+
-- practice and not just on a fresh install.
|
|
21
|
+
--
|
|
22
|
+
-- Additive only: one new permission key granted to one role; nothing is revoked.
|
|
23
|
+
-- Idempotent (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
|
|
24
|
+
--
|
|
25
|
+
-- One block PER ROLE, in the governance_003/004 shape: the drift guard
|
|
26
|
+
-- (tests/governance_seed.mjs) scans for `SELECT '<role>', unnest(ARRAY[…])` literally,
|
|
27
|
+
-- so a combined `unnest(ARRAY['metic','archon']), 'key'` form parses as zero grants
|
|
28
|
+
-- and reds it.
|
|
29
|
+
|
|
30
|
+
BEGIN;
|
|
31
|
+
|
|
32
|
+
INSERT INTO governance_role_permissions (role_key, permission_key)
|
|
33
|
+
SELECT 'archon', unnest(ARRAY[
|
|
34
|
+
'version.create'
|
|
35
|
+
])
|
|
36
|
+
ON CONFLICT (role_key, permission_key) DO NOTHING;
|
|
37
|
+
|
|
38
|
+
COMMIT;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
-- governance_007_delegate_archon_ops_to_metic.sql — grant the Metic rank-role the
|
|
2
|
+
-- twenty permissions ADR 0157 lowered archon→metic, plus the new `gate_approval.read`
|
|
3
|
+
-- (task 1002584).
|
|
4
|
+
--
|
|
5
|
+
-- Owner decision (2026-08-05): the Archon rank is responsible for RANK and IDENTITY
|
|
6
|
+
-- only. `catalog.js` moved twenty operational floors archon→metic to match, and split
|
|
7
|
+
-- `gate_approval.read` out of `gate_approval.decide` so the Gate page can open to Metic
|
|
8
|
+
-- while approving a held PR stays Archon.
|
|
9
|
+
--
|
|
10
|
+
-- The compat seed (governance_002) is RUN-ONCE, so a floor change cannot be made by
|
|
11
|
+
-- editing it — every already-migrated database would keep the old grant. This is the
|
|
12
|
+
-- follow-on grant migration the 002 header calls for, and it follows
|
|
13
|
+
-- governance_003_task_abandon_metic.sql (the ADR 0152 worked example) exactly.
|
|
14
|
+
--
|
|
15
|
+
-- Additive only: `archon` already holds all twenty (the seeded roles are cumulative
|
|
16
|
+
-- supersets), so lowering each floor adds exactly one grant — metic — and revokes
|
|
17
|
+
-- nothing. `gate_approval.read` is genuinely new, so it is granted to BOTH metic and
|
|
18
|
+
-- archon to keep archon ⊇ metic. Idempotent (ON CONFLICT DO NOTHING); forward-safe
|
|
19
|
+
-- (INSERT only, no dropped or retyped state).
|
|
20
|
+
--
|
|
21
|
+
-- The drift guard (tests/governance_seed.mjs) reads the UNION of the seed and every
|
|
22
|
+
-- later governance_* grant migration, so this file is what keeps the SQL snapshot equal
|
|
23
|
+
-- to catalog.js's floor-derived RANK_ROLE_SEED.
|
|
24
|
+
--
|
|
25
|
+
-- NOT here, deliberately: `version.close` and the four escalation keys
|
|
26
|
+
-- (`task.confirm.grade_bypass`, `gate_approval.decide`, `goal.scope.manage`,
|
|
27
|
+
-- `governance.manage`) stay Archon — see ADR 0157 for why moving any one of them would
|
|
28
|
+
-- make this delegation self-reversible. `dependency.cross_tier.manage` also stays: it is
|
|
29
|
+
-- system:true, and granting a system key to metic violates the catalog's own invariant.
|
|
30
|
+
|
|
31
|
+
BEGIN;
|
|
32
|
+
|
|
33
|
+
INSERT INTO governance_role_permissions (role_key, permission_key)
|
|
34
|
+
SELECT 'metic', unnest(ARRAY[
|
|
35
|
+
-- admin ops (spend, adjudicate, operate) — none decides who holds authority
|
|
36
|
+
'builder.budget.set',
|
|
37
|
+
'builder.roster.read',
|
|
38
|
+
'security.report.adjudicate',
|
|
39
|
+
'provisioning.fleet.manage',
|
|
40
|
+
'box.fleet.manage',
|
|
41
|
+
'autonomy.precheck',
|
|
42
|
+
'discord.channels.manage',
|
|
43
|
+
'project.curate',
|
|
44
|
+
-- work disposition
|
|
45
|
+
'criterion.unsatisfy',
|
|
46
|
+
'version.create',
|
|
47
|
+
'override_request.decide',
|
|
48
|
+
'claim.release.any',
|
|
49
|
+
'idea.ratify',
|
|
50
|
+
'task.newcomer_restock',
|
|
51
|
+
'task.peer_votes.tally',
|
|
52
|
+
'goal.reorder',
|
|
53
|
+
-- reads
|
|
54
|
+
'session.search',
|
|
55
|
+
'memory.read.any',
|
|
56
|
+
'audit_log.read',
|
|
57
|
+
-- surfaces
|
|
58
|
+
'page.view.governance',
|
|
59
|
+
'gate_approval.read'
|
|
60
|
+
])
|
|
61
|
+
ON CONFLICT (role_key, permission_key) DO NOTHING;
|
|
62
|
+
|
|
63
|
+
-- `gate_approval.read` is a NEW catalog key, so archon does not already hold it from
|
|
64
|
+
-- governance_002. Granted here to preserve the cumulative-superset invariant
|
|
65
|
+
-- (tests/governance_catalog.mjs: archon ⊇ metic ⊇ thetes ⊇ xenos).
|
|
66
|
+
INSERT INTO governance_role_permissions (role_key, permission_key)
|
|
67
|
+
SELECT 'archon', unnest(ARRAY[
|
|
68
|
+
'gate_approval.read'
|
|
69
|
+
])
|
|
70
|
+
ON CONFLICT (role_key, permission_key) DO NOTHING;
|
|
71
|
+
|
|
72
|
+
COMMIT;
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// rebuilds the GDS router, so registering here keeps the port present for the
|
|
15
15
|
// route-building tests too. Registration is hasProvider-guarded so
|
|
16
16
|
// re-instantiation never trips the seam's single-provider duplicate guard.
|
|
17
|
-
// (Mirrors modules/economy/routes/reward.js.) The governance CRUD
|
|
18
|
-
// on this same router
|
|
17
|
+
// (Mirrors modules/economy/routes/reward.js.) The R110 governance CRUD
|
|
18
|
+
// endpoints live on this same router, below.
|
|
19
19
|
|
|
20
20
|
const express = require('express');
|
|
21
21
|
const api = require('../../../src/module-api');
|
|
@@ -52,6 +52,11 @@ const sessionScopes = require('../session-scopes');
|
|
|
52
52
|
// session's real authority rather than the builder's.
|
|
53
53
|
const governancePort = {
|
|
54
54
|
resolveBuilderPermissions: resolver.resolveBuilderPermissions,
|
|
55
|
+
// The pure catalog-drop half, for the session-load fold (task 1002556): the
|
|
56
|
+
// kernel feeds it the raw grant keys its own session query carried, so a
|
|
57
|
+
// permission gate needs no second round-trip. Same answer as
|
|
58
|
+
// resolveBuilderPermissions given the same rows — DB-free by construction.
|
|
59
|
+
effectivePermissions: resolver.effectivePermissions,
|
|
55
60
|
builderHasPermissions: resolver.builderHasPermissions,
|
|
56
61
|
getRoleKeysForBuilder: db.getRoleKeysForBuilder,
|
|
57
62
|
isSystem: catalog.isSystem,
|
|
@@ -76,14 +81,238 @@ function registerSeams() {
|
|
|
76
81
|
if (!api.hasProvider('governance')) {
|
|
77
82
|
api.registerProvider('governance', governancePort);
|
|
78
83
|
}
|
|
84
|
+
// The session-load fold (task 1002556): the kernel's lookupSession embeds this
|
|
85
|
+
// fragment so the SAME round-trip carries the builder's raw grant keys, and
|
|
86
|
+
// requirePermission runs the pure effectivePermissions instead of a second
|
|
87
|
+
// query. Registered HERE — capability-named, module-agnostic on the kernel
|
|
88
|
+
// side (ADR 0083 §Decision #4) — because this module owns the governance SQL.
|
|
89
|
+
// Contract: a boot-time CONSTANT (never user input) yielding one
|
|
90
|
+
// `raw_permission_keys` column against the kernel query's alias `s`
|
|
91
|
+
// (builder_sessions). Raw keys only — the catalog-drop stays in the resolver.
|
|
92
|
+
if (!api.hasProvider('session-grants-sql')) {
|
|
93
|
+
api.registerProvider('session-grants-sql', {
|
|
94
|
+
selectFragment: `,
|
|
95
|
+
(SELECT COALESCE(array_agg(DISTINCT rp.permission_key), '{}')
|
|
96
|
+
FROM governance_builder_roles br
|
|
97
|
+
JOIN governance_role_permissions rp ON rp.role_key = br.role_key
|
|
98
|
+
WHERE br.builder_id = s.builder_id) AS raw_permission_keys`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
|
|
103
|
+
// ── the R110 CRUD surface (ADR 0151 §2 — the governance tab's server half) ────
|
|
104
|
+
//
|
|
105
|
+
// Every route — the three reads included — gates on the `governance.manage`
|
|
106
|
+
// atom (system, floor archon): this surface EDITS the role/permission graph
|
|
107
|
+
// itself, the meta-escalation the catalog flags non-grantable, and even listing
|
|
108
|
+
// it exposes the full authority map. Two invariants are enforced HERE, against
|
|
109
|
+
// the catalog, not in the db layer (governance_001's app-side-validation split):
|
|
110
|
+
// • a `system:true` permission is NON-GRANTABLE to a custom role (R94's wall —
|
|
111
|
+
// "The governance API (R110) rejects granting it");
|
|
112
|
+
// • the four seeded rank-roles are IMMUTABLE through this API: their grants
|
|
113
|
+
// derive from catalog floors (a change is a migration — R94) and their
|
|
114
|
+
// assignment derives from builders.rank via the R95b sync. A second writer
|
|
115
|
+
// for either would fork authority truth.
|
|
116
|
+
const { validateOrRespond, LIMITS, parseId } = api;
|
|
117
|
+
const ROLE_KEY_RE = /^[a-z][a-z0-9-]*$/;
|
|
118
|
+
const ROLE_KEY_MAX = 64;
|
|
119
|
+
|
|
81
120
|
module.exports = function buildGovernanceRouter() {
|
|
82
121
|
registerSeams();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
122
|
+
const router = express.Router();
|
|
123
|
+
|
|
124
|
+
// Param guard for :roleKey. Responds bad_role_key and returns null on a
|
|
125
|
+
// malformed slug (seeded keys like 'metic' pass shape and are refused later,
|
|
126
|
+
// by policy, with the honest code).
|
|
127
|
+
function roleKeyParam(req, res) {
|
|
128
|
+
const roleKey = req.params.roleKey;
|
|
129
|
+
if (roleKey.length > ROLE_KEY_MAX || !ROLE_KEY_RE.test(roleKey)) {
|
|
130
|
+
res.fail('bad_role_key', 400, { role_key: roleKey, expected: 'a kebab-case slug (^[a-z][a-z0-9-]*$)' });
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return roleKey;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Load a role that this API may MUTATE: 404 on a missing role, refuse a seeded
|
|
137
|
+
// rank-role with the caller's `immutableCode`. Returns null after responding.
|
|
138
|
+
async function loadMutableRole(res, roleKey, immutableCode, reason) {
|
|
139
|
+
const role = await db.getRoleByKey(roleKey);
|
|
140
|
+
if (!role) {
|
|
141
|
+
res.fail('role_not_found', 404, { role_key: roleKey });
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (role.is_system) {
|
|
145
|
+
res.fail(immutableCode, 403, { role_key: roleKey, reason });
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return role;
|
|
149
|
+
}
|
|
150
|
+
const GRANTS_REASON = 'a seeded rank-role\'s grants derive from catalog floors; changing one is a migration (R94), never an API write';
|
|
151
|
+
const ASSIGN_REASON = 'a seeded rank-role is assigned by rank (R95b sync); change the builder\'s rank instead';
|
|
152
|
+
|
|
153
|
+
// rank: perm governance.manage — the full role list with each role's grants.
|
|
154
|
+
router.get('/governance/roles', api.requireBuilder, api.requirePermission('governance.manage'), async (req, res) => {
|
|
155
|
+
try {
|
|
156
|
+
const roles = await db.listRolesWithGrants();
|
|
157
|
+
res.json({ roles });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error('[gds] GET /governance/roles', err);
|
|
160
|
+
res.fail('roles_list_failed', { status: 500, message: 'internal error' });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// rank: perm governance.manage — the permission catalog + principals, straight
|
|
165
|
+
// from code (the catalog is CODE, not a table); no DB read. `grantable` is the
|
|
166
|
+
// flag the tab renders LOCKED from; principals are read-only by construction.
|
|
167
|
+
router.get('/governance/permissions', api.requireBuilder, api.requirePermission('governance.manage'), (req, res) => {
|
|
168
|
+
const permissions = catalog.PERMISSIONS.map((p) => ({
|
|
169
|
+
key: p.key,
|
|
170
|
+
system: p.system,
|
|
171
|
+
floor: p.floor,
|
|
172
|
+
guards: p.guards,
|
|
173
|
+
scope: p.scope,
|
|
174
|
+
resource: p.resource,
|
|
175
|
+
grantable: catalog.isGrantable(p.key),
|
|
176
|
+
}));
|
|
177
|
+
res.json({ permissions, principals: catalog.principals() });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// rank: perm governance.manage — every builder↔role assignment row.
|
|
181
|
+
router.get('/governance/assignments', api.requireBuilder, api.requirePermission('governance.manage'), async (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const assignments = await db.listAssignments();
|
|
184
|
+
res.json({ assignments });
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error('[gds] GET /governance/assignments', err);
|
|
187
|
+
res.fail('assignments_list_failed', { status: 500, message: 'internal error' });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// rank: perm governance.manage — create a CUSTOM role (is_system false, no
|
|
192
|
+
// seeded_rank — the seeded four exist only via governance_002). Grants are
|
|
193
|
+
// added one at a time via the toggle below, so creation carries none.
|
|
194
|
+
router.post('/governance/roles', api.requireBuilder, api.requirePermission('governance.manage'), async (req, res) => {
|
|
195
|
+
if (validateOrRespond(req, res, {
|
|
196
|
+
role_key: { required: true, type: 'string', minLength: 2, maxLength: ROLE_KEY_MAX },
|
|
197
|
+
label: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
|
|
198
|
+
description: { type: 'string', maxLength: 2000 },
|
|
199
|
+
})) return;
|
|
200
|
+
const roleKey = req.body.role_key;
|
|
201
|
+
if (!ROLE_KEY_RE.test(roleKey)) {
|
|
202
|
+
return res.fail('bad_role_key', 400, { role_key: roleKey, expected: 'a kebab-case slug (^[a-z][a-z0-9-]*$)' });
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const role = await db.createRole({
|
|
206
|
+
roleKey,
|
|
207
|
+
label: req.body.label,
|
|
208
|
+
description: req.body.description ?? null,
|
|
209
|
+
createdBy: req.builder.id,
|
|
210
|
+
});
|
|
211
|
+
res.status(201).json({ role: { ...role, permissions: [] } });
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (err && err.code === '23505') {
|
|
214
|
+
return res.fail('role_exists', 409, { role_key: roleKey });
|
|
215
|
+
}
|
|
216
|
+
console.error('[gds] POST /governance/roles', err);
|
|
217
|
+
res.fail('role_create_failed', { status: 500, message: 'internal error' });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// rank: perm governance.manage — grant one OPERATIONAL permission to a custom
|
|
222
|
+
// role (the toggle's ON half). The order of refusals is the authority order:
|
|
223
|
+
// role exists → role is custom → the key exists → the key is grantable.
|
|
224
|
+
router.put('/governance/roles/:roleKey/permissions/:permissionKey', api.requireBuilder, api.requirePermission('governance.manage'), async (req, res) => {
|
|
225
|
+
const roleKey = roleKeyParam(req, res);
|
|
226
|
+
if (roleKey === null) return;
|
|
227
|
+
const permissionKey = req.params.permissionKey;
|
|
228
|
+
try {
|
|
229
|
+
const role = await loadMutableRole(res, roleKey, 'seeded_role_immutable', GRANTS_REASON);
|
|
230
|
+
if (role === null) return;
|
|
231
|
+
if (!catalog.byKey(permissionKey)) {
|
|
232
|
+
return res.fail('unknown_permission', 400, { permission_key: permissionKey });
|
|
233
|
+
}
|
|
234
|
+
if (!catalog.isGrantable(permissionKey)) {
|
|
235
|
+
return res.fail('permission_not_grantable', 400, {
|
|
236
|
+
permission_key: permissionKey,
|
|
237
|
+
reason: 'system (trust-boundary) permissions are non-grantable to a custom role (ADR 0151)',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const granted = await db.grantRolePermission({ roleKey, permissionKey });
|
|
241
|
+
res.json({ ok: true, role_key: roleKey, permission_key: permissionKey, granted });
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error('[gds] PUT /governance/roles/:roleKey/permissions/:permissionKey', err);
|
|
244
|
+
res.fail('grant_failed', { status: 500, message: 'internal error' });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// rank: perm governance.manage — revoke a grant (the toggle's OFF half).
|
|
249
|
+
// Deliberately NOT validated against the catalog: a grant whose key the
|
|
250
|
+
// catalog since retired is exactly the row an Archon most needs to remove
|
|
251
|
+
// (the resolver already drops it — this is hygiene, not authority).
|
|
252
|
+
router.delete('/governance/roles/:roleKey/permissions/:permissionKey', api.requireBuilder, api.requirePermission('governance.manage'), async (req, res) => {
|
|
253
|
+
const roleKey = roleKeyParam(req, res);
|
|
254
|
+
if (roleKey === null) return;
|
|
255
|
+
const permissionKey = req.params.permissionKey;
|
|
256
|
+
try {
|
|
257
|
+
const role = await loadMutableRole(res, roleKey, 'seeded_role_immutable', GRANTS_REASON);
|
|
258
|
+
if (role === null) return;
|
|
259
|
+
const removed = await db.revokeRolePermission({ roleKey, permissionKey });
|
|
260
|
+
res.json({ ok: true, role_key: roleKey, permission_key: permissionKey, removed });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error('[gds] DELETE /governance/roles/:roleKey/permissions/:permissionKey', err);
|
|
263
|
+
res.fail('revoke_failed', { status: 500, message: 'internal error' });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// rank: perm governance.manage — assign a CUSTOM role to a builder.
|
|
268
|
+
// assigned_by is ALWAYS the session builder, never input (the audit trail's
|
|
269
|
+
// "who did this" must be the authenticated actor).
|
|
270
|
+
router.post('/governance/assignments', api.requireBuilder, api.requirePermission('governance.manage'), async (req, res) => {
|
|
271
|
+
if (validateOrRespond(req, res, {
|
|
272
|
+
builder_id: { required: true, type: 'integer', min: 1 },
|
|
273
|
+
role_key: { required: true, type: 'string', minLength: 2, maxLength: ROLE_KEY_MAX },
|
|
274
|
+
})) return;
|
|
275
|
+
const roleKey = req.body.role_key;
|
|
276
|
+
if (!ROLE_KEY_RE.test(roleKey)) {
|
|
277
|
+
return res.fail('bad_role_key', 400, { role_key: roleKey, expected: 'a kebab-case slug (^[a-z][a-z0-9-]*$)' });
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const role = await loadMutableRole(res, roleKey, 'seeded_role_assignment_forbidden', ASSIGN_REASON);
|
|
281
|
+
if (role === null) return;
|
|
282
|
+
const assigned = await db.assignRole({
|
|
283
|
+
builderId: req.body.builder_id,
|
|
284
|
+
roleKey,
|
|
285
|
+
assignedBy: req.builder.id,
|
|
286
|
+
});
|
|
287
|
+
res.status(assigned ? 201 : 200).json({ ok: true, builder_id: req.body.builder_id, role_key: roleKey, assigned });
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (err && err.code === '23503') {
|
|
290
|
+
return res.fail('builder_not_found', 404, { builder_id: req.body.builder_id });
|
|
291
|
+
}
|
|
292
|
+
console.error('[gds] POST /governance/assignments', err);
|
|
293
|
+
res.fail('assign_failed', { status: 500, message: 'internal error' });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// rank: perm governance.manage — remove a custom-role assignment. Idempotent:
|
|
298
|
+
// `removed` says whether a row went away.
|
|
299
|
+
router.delete('/governance/assignments/:builderId/:roleKey', api.requireBuilder, api.requirePermission('governance.manage'), async (req, res) => {
|
|
300
|
+
const builderId = parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
|
|
301
|
+
if (builderId === null) return;
|
|
302
|
+
const roleKey = roleKeyParam(req, res);
|
|
303
|
+
if (roleKey === null) return;
|
|
304
|
+
try {
|
|
305
|
+
const role = await loadMutableRole(res, roleKey, 'seeded_role_assignment_forbidden', ASSIGN_REASON);
|
|
306
|
+
if (role === null) return;
|
|
307
|
+
const removed = await db.unassignRole({ builderId, roleKey });
|
|
308
|
+
res.json({ ok: true, builder_id: builderId, role_key: roleKey, removed });
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.error('[gds] DELETE /governance/assignments/:builderId/:roleKey', err);
|
|
311
|
+
res.fail('unassign_failed', { status: 500, message: 'internal error' });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return router;
|
|
87
316
|
};
|
|
88
317
|
|
|
89
318
|
module.exports.governancePort = governancePort;
|
|
@@ -47,18 +47,27 @@ const TRANSIENT_WORKER_ERRORS = new Set([
|
|
|
47
47
|
|
|
48
48
|
// #467: per-worker subagent timeout. The global 240s applied to EVERY worker,
|
|
49
49
|
// which timed out the Read-heavy ones (Narc walks ACs + reads files — hence
|
|
50
|
-
// Phase 2.0.5's 5-read workaround; Quality is next-heaviest)
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
50
|
+
// Phase 2.0.5's 5-read workaround; Quality is next-heaviest). Per-worker budgets
|
|
51
|
+
// let the heavy lenses breathe without globally inflating every ship's
|
|
52
|
+
// worst-case wall time. A worker module may override by exporting its own
|
|
53
|
+
// MAX_TIMEOUT_MS; otherwise this map (keyed by KIND) is the single tuning
|
|
54
|
+
// surface. Falls back to runOpts.timeoutMs (then runSubagent's own 240s default)
|
|
55
|
+
// for any unlisted worker.
|
|
56
|
+
//
|
|
57
|
+
// task 1002580 — calibration data (#292): the earlier assumption that Hacker and
|
|
58
|
+
// Efficiency are "light" INVERTS when the diff is itself security- or
|
|
59
|
+
// performance-heavy code (a security module, a hot path). On task 1002544's
|
|
60
|
+
// ~92k-char security-module diff the Hacker worker hit SUBAGENT_TIMEOUT at 240s
|
|
61
|
+
// three runs straight while Narc/Quality completed at 360s — and because a
|
|
62
|
+
// gating-worker timeout HARD-BLOCKS (security is non-optional), a fully-passing
|
|
63
|
+
// 9/10 feature could not land. All four gating/advisory workers now share the
|
|
64
|
+
// 360s heavy-tier budget. Worst-case wall time is unchanged: workers run in
|
|
65
|
+
// parallel, so the panel's wall time was already max()=360s from Narc/Quality.
|
|
57
66
|
const WORKER_MAX_TIMEOUT_MS = {
|
|
58
67
|
narc: 360_000, // Read-heavy: walks every acceptance criterion
|
|
59
68
|
quality: 360_000, // Read-heavy: reads changed files for cleanliness review
|
|
60
|
-
hacker:
|
|
61
|
-
efficiency:
|
|
69
|
+
hacker: 360_000, // Read-heavy on a security diff: exploit-surface scan of every changed file
|
|
70
|
+
efficiency: 360_000, // Read-heavy on a large diff: traces perf across every changed file
|
|
62
71
|
};
|
|
63
72
|
|
|
64
73
|
// Resolve the timeout for a worker: explicit module override > KIND map >
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Like The Watch + The Harbor, this reads /api/bongos/me to show a Sealed notice for
|
|
11
11
|
// non-Archons — DEFENCE IN DEPTH only. GET /gate-approvals + POST
|
|
12
12
|
// /gate-approvals/:pr/approve are ALSO archon-gated server-side (ADR 0016), and
|
|
13
|
-
// the page itself is gated by server.js
|
|
13
|
+
// the page itself is gated by server.js requireGovernancePage. On 401 → web sign-in.
|
|
14
14
|
|
|
15
15
|
(() => {
|
|
16
16
|
'use strict';
|
|
@@ -217,7 +217,11 @@
|
|
|
217
217
|
me = null;
|
|
218
218
|
}
|
|
219
219
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
220
|
-
|
|
220
|
+
// ADR 0157: the Gate page sits at the `page.view.governance` floor (metic). The
|
|
221
|
+
// server's requireGovernancePage is the wall — this only decides whether to render the
|
|
222
|
+
// body or the sealed notice, so it must agree or a Metic sees "sealed" on a page
|
|
223
|
+
// they were just admitted to.
|
|
224
|
+
if (rank !== 'metic' && rank !== 'archon') { showSealed(); return; }
|
|
221
225
|
|
|
222
226
|
document.getElementById('gate-sub').textContent = 'Changes touching protected build surfaces, held for your approval.';
|
|
223
227
|
document.getElementById('gate-body').hidden = false;
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
|
|
225
225
|
// task 1002164 — confirming a criterion is Metic+ (POST /done-when/:id/satisfy,
|
|
226
226
|
// ADR 0086 §6). Client-side this only decides whether to SHOW the control; the
|
|
227
|
-
// server's
|
|
227
|
+
// server's requirePermission('criterion.satisfy') is the real gate (BV1.R105).
|
|
228
228
|
const RANK_ORDER = { archon: 4, metic: 3, thetes: 2, xenos: 1 };
|
|
229
229
|
const canConfirmCriteria = () =>
|
|
230
230
|
(RANK_ORDER[(state.me?.builder?.rank || '').toLowerCase()] ?? 0) >= RANK_ORDER.metic;
|
|
@@ -1091,7 +1091,11 @@
|
|
|
1091
1091
|
} catch (err) {
|
|
1092
1092
|
if (err && err.message === 'redirecting to auth') return;
|
|
1093
1093
|
const code = err && err.data && err.data.error;
|
|
1094
|
-
|
|
1094
|
+
// POST /done-when/:id/satisfy gates on criterion.satisfy since BV1.R105, so the
|
|
1095
|
+
// denial code is now 'permission_forbidden'; 'rank_forbidden' is kept for an
|
|
1096
|
+
// instance still on a pre-R105 core (the doorway is versioned, ADR 0083).
|
|
1097
|
+
const denial = code && (code.code || code);
|
|
1098
|
+
const reason = (denial === 'permission_forbidden' || denial === 'rank_forbidden')
|
|
1095
1099
|
? 'Confirming a criterion needs Metic rank or higher.'
|
|
1096
1100
|
: `Could not confirm that criterion (${(err && err.status) || 'error'}).`;
|
|
1097
1101
|
toast(reason);
|