@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
|
@@ -99,7 +99,7 @@ export function createClient(opts = {}) {
|
|
|
99
99
|
getAnalyticsBuilderId: (args) => request("GET", "/analytics/builder/{id}", { hasBody: false }, args),
|
|
100
100
|
},
|
|
101
101
|
"auditLog": {
|
|
102
|
-
// GET /audit-log — rank: archon — GET /audit-log
|
|
102
|
+
// GET /audit-log — rank: metic+archon — GET /audit-log
|
|
103
103
|
getAuditLog: (args) => request("GET", "/audit-log", { hasBody: false }, args),
|
|
104
104
|
},
|
|
105
105
|
"auth": {
|
|
@@ -141,7 +141,7 @@ export function createClient(opts = {}) {
|
|
|
141
141
|
getAuthWebStart: (args) => request("GET", "/auth/web/start", { hasBody: false }, args),
|
|
142
142
|
},
|
|
143
143
|
"autonomy": {
|
|
144
|
-
// GET /autonomy/precheck — rank: archon — GET /autonomy/precheck
|
|
144
|
+
// GET /autonomy/precheck — rank: metic+archon — GET /autonomy/precheck
|
|
145
145
|
getAutonomyPrecheck: (args) => request("GET", "/autonomy/precheck", { hasBody: false }, args),
|
|
146
146
|
},
|
|
147
147
|
"backup": {
|
|
@@ -201,13 +201,13 @@ export function createClient(opts = {}) {
|
|
|
201
201
|
postBoxVersion: (args) => request("POST", "/box/version", { hasBody: true }, args),
|
|
202
202
|
},
|
|
203
203
|
"boxes": {
|
|
204
|
-
// GET /boxes — rank: archon — GET /boxes
|
|
204
|
+
// GET /boxes — rank: metic+archon — GET /boxes
|
|
205
205
|
getBoxes: (args) => request("GET", "/boxes", { hasBody: false }, args),
|
|
206
|
-
// PATCH /boxes/{builderId}/block — rank: archon — PATCH /boxes/:builderId/block
|
|
206
|
+
// PATCH /boxes/{builderId}/block — rank: metic+archon — PATCH /boxes/:builderId/block
|
|
207
207
|
patchBoxesBuilderIdBlock: (args) => request("PATCH", "/boxes/{builderId}/block", { hasBody: true }, args),
|
|
208
|
-
// POST /boxes/{builderId}/close — rank: archon — POST /boxes/:builderId/close
|
|
208
|
+
// POST /boxes/{builderId}/close — rank: metic+archon — POST /boxes/:builderId/close
|
|
209
209
|
postBoxesBuilderIdClose: (args) => request("POST", "/boxes/{builderId}/close", { hasBody: true }, args),
|
|
210
|
-
// GET /boxes/cost-ledger — rank: archon — GET /boxes/cost-ledger
|
|
210
|
+
// GET /boxes/cost-ledger — rank: metic+archon — GET /boxes/cost-ledger
|
|
211
211
|
getBoxesCostLedger: (args) => request("GET", "/boxes/cost-ledger", { hasBody: false }, args),
|
|
212
212
|
},
|
|
213
213
|
"bugAttachments": {
|
|
@@ -215,9 +215,9 @@ export function createClient(opts = {}) {
|
|
|
215
215
|
getBugAttachmentsName: (args) => request("GET", "/bug-attachments/{name}", { hasBody: false }, args),
|
|
216
216
|
},
|
|
217
217
|
"builders": {
|
|
218
|
-
// GET /builders/{id}/active-claims — rank: archon — GET /builders/:id/active-claims
|
|
218
|
+
// GET /builders/{id}/active-claims — rank: metic+archon — GET /builders/:id/active-claims
|
|
219
219
|
getBuildersIdActiveClaims: (args) => request("GET", "/builders/{id}/active-claims", { hasBody: false }, args),
|
|
220
|
-
// PATCH /builders/{id}/budget — rank: archon — PATCH /builders/:id/budget
|
|
220
|
+
// PATCH /builders/{id}/budget — rank: metic+archon — PATCH /builders/:id/budget
|
|
221
221
|
patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
|
|
222
222
|
// GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
|
|
223
223
|
getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
|
|
@@ -231,13 +231,13 @@ export function createClient(opts = {}) {
|
|
|
231
231
|
getBuildersDirectory: (args) => request("GET", "/builders/directory", { hasBody: false }, args),
|
|
232
232
|
// GET /builders/me/pending-rank-change — rank: any-builder — GET /builders/me/pending-rank-change
|
|
233
233
|
getBuildersMePendingRankChange: (args) => request("GET", "/builders/me/pending-rank-change", { hasBody: false }, args),
|
|
234
|
-
// GET /builders/roster — rank: archon — GET /builders/roster
|
|
234
|
+
// GET /builders/roster — rank: metic+archon — GET /builders/roster
|
|
235
235
|
getBuildersRoster: (args) => request("GET", "/builders/roster", { hasBody: false }, args),
|
|
236
236
|
},
|
|
237
237
|
"claims": {
|
|
238
238
|
// POST /claims — rank: any-builder — POST /claims
|
|
239
239
|
postClaims: (args) => request("POST", "/claims", { hasBody: true }, args),
|
|
240
|
-
// POST /claims/{id}/release-on-behalf — rank: archon — POST /claims/:id/release-on-behalf
|
|
240
|
+
// POST /claims/{id}/release-on-behalf — rank: metic+archon — POST /claims/:id/release-on-behalf
|
|
241
241
|
postClaimsIdReleaseOnBehalf: (args) => request("POST", "/claims/{id}/release-on-behalf", { hasBody: true }, args),
|
|
242
242
|
// POST /claims/{id}/resolve — rank: any-builder — POST /claims/:id/resolve
|
|
243
243
|
postClaimsIdResolve: (args) => request("POST", "/claims/{id}/resolve", { hasBody: true }, args),
|
|
@@ -259,21 +259,21 @@ export function createClient(opts = {}) {
|
|
|
259
259
|
postDependencies: (args) => request("POST", "/dependencies", { hasBody: true }, args),
|
|
260
260
|
},
|
|
261
261
|
"discord": {
|
|
262
|
-
// GET /discord/channels/plan — rank: archon — GET /discord/channels/plan
|
|
262
|
+
// GET /discord/channels/plan — rank: metic+archon — GET /discord/channels/plan
|
|
263
263
|
getDiscordChannelsPlan: (args) => request("GET", "/discord/channels/plan", { hasBody: false }, args),
|
|
264
|
-
// POST /discord/channels/reconcile — rank: archon — POST /discord/channels/reconcile
|
|
264
|
+
// POST /discord/channels/reconcile — rank: metic+archon — POST /discord/channels/reconcile
|
|
265
265
|
postDiscordChannelsReconcile: (args) => request("POST", "/discord/channels/reconcile", { hasBody: true }, args),
|
|
266
266
|
},
|
|
267
267
|
"doneWhen": {
|
|
268
268
|
// POST /done-when/{criterionId}/satisfy — rank: metic+archon — POST /done-when/:criterionId/satisfy
|
|
269
269
|
postDoneWhenCriterionIdSatisfy: (args) => request("POST", "/done-when/{criterionId}/satisfy", { hasBody: true }, args),
|
|
270
|
-
// POST /done-when/{criterionId}/unsatisfy — rank: archon — POST /done-when/:criterionId/unsatisfy
|
|
270
|
+
// POST /done-when/{criterionId}/unsatisfy — rank: metic+archon — POST /done-when/:criterionId/unsatisfy
|
|
271
271
|
postDoneWhenCriterionIdUnsatisfy: (args) => request("POST", "/done-when/{criterionId}/unsatisfy", { hasBody: true }, args),
|
|
272
272
|
// GET /done-when/pending-review — rank: metic+archon — GET /done-when/pending-review
|
|
273
273
|
getDoneWhenPendingReview: (args) => request("GET", "/done-when/pending-review", { hasBody: false }, args),
|
|
274
274
|
},
|
|
275
275
|
"gateApprovals": {
|
|
276
|
-
// GET /gate-approvals — rank: archon — GET /gate-approvals
|
|
276
|
+
// GET /gate-approvals — rank: metic+archon — GET /gate-approvals
|
|
277
277
|
getGateApprovals: (args) => request("GET", "/gate-approvals", { hasBody: false }, args),
|
|
278
278
|
// POST /gate-approvals/{pr}/approve — rank: archon — POST /gate-approvals/:pr/approve
|
|
279
279
|
postGateApprovalsPrApprove: (args) => request("POST", "/gate-approvals/{pr}/approve", { hasBody: true }, args),
|
|
@@ -331,11 +331,29 @@ export function createClient(opts = {}) {
|
|
|
331
331
|
patchGoalsIdVisibility: (args) => request("PATCH", "/goals/{id}/visibility", { hasBody: true }, args),
|
|
332
332
|
// GET /goals/inbox — rank: any-builder — GET /goals/inbox
|
|
333
333
|
getGoalsInbox: (args) => request("GET", "/goals/inbox", { hasBody: false }, args),
|
|
334
|
-
// POST /goals/reorder — rank: archon — POST /goals/reorder
|
|
334
|
+
// POST /goals/reorder — rank: metic+archon — POST /goals/reorder
|
|
335
335
|
postGoalsReorder: (args) => request("POST", "/goals/reorder", { hasBody: true }, args),
|
|
336
336
|
},
|
|
337
|
+
"governance": {
|
|
338
|
+
// GET /governance/assignments — rank: archon — GET /governance/assignments
|
|
339
|
+
getGovernanceAssignments: (args) => request("GET", "/governance/assignments", { hasBody: false }, args),
|
|
340
|
+
// POST /governance/assignments — rank: archon — POST /governance/assignments
|
|
341
|
+
postGovernanceAssignments: (args) => request("POST", "/governance/assignments", { hasBody: true }, args),
|
|
342
|
+
// DELETE /governance/assignments/{builderId}/{roleKey} — rank: archon — DELETE /governance/assignments/:builderId/:roleKey
|
|
343
|
+
deleteGovernanceAssignmentsBuilderIdRoleKey: (args) => request("DELETE", "/governance/assignments/{builderId}/{roleKey}", { hasBody: false }, args),
|
|
344
|
+
// GET /governance/permissions — rank: archon — GET /governance/permissions
|
|
345
|
+
getGovernancePermissions: (args) => request("GET", "/governance/permissions", { hasBody: false }, args),
|
|
346
|
+
// GET /governance/roles — rank: archon — GET /governance/roles
|
|
347
|
+
getGovernanceRoles: (args) => request("GET", "/governance/roles", { hasBody: false }, args),
|
|
348
|
+
// POST /governance/roles — rank: archon — POST /governance/roles
|
|
349
|
+
postGovernanceRoles: (args) => request("POST", "/governance/roles", { hasBody: true }, args),
|
|
350
|
+
// DELETE /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — DELETE /governance/roles/:roleKey/permissions/:permissionKey
|
|
351
|
+
deleteGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("DELETE", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: false }, args),
|
|
352
|
+
// PUT /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — PUT /governance/roles/:roleKey/permissions/:permissionKey
|
|
353
|
+
putGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("PUT", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: true }, args),
|
|
354
|
+
},
|
|
337
355
|
"grades": {
|
|
338
|
-
// GET /grades/by-builder — rank: archon — GET /grades/by-builder
|
|
356
|
+
// GET /grades/by-builder — rank: metic+archon — GET /grades/by-builder
|
|
339
357
|
getGradesByBuilder: (args) => request("GET", "/grades/by-builder", { hasBody: false }, args),
|
|
340
358
|
},
|
|
341
359
|
"healthz": {
|
|
@@ -351,7 +369,7 @@ export function createClient(opts = {}) {
|
|
|
351
369
|
getInboxId: (args) => request("GET", "/inbox/{id}", { hasBody: false }, args),
|
|
352
370
|
// PATCH /inbox/{id} — rank: metic+archon — PATCH /inbox/:id
|
|
353
371
|
patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
|
|
354
|
-
// POST /inbox/{id}/ratify — rank: archon — POST /inbox/:id/ratify
|
|
372
|
+
// POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
|
|
355
373
|
postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
|
|
356
374
|
},
|
|
357
375
|
"instance": {
|
|
@@ -429,9 +447,9 @@ export function createClient(opts = {}) {
|
|
|
429
447
|
getMemoryBfgBuildersBuilderIdFiles: (args) => request("GET", "/memory/bfg/builders/{builderId}/files", { hasBody: false }, args),
|
|
430
448
|
// POST /memory/builders/{builderId}/bfg-write — rank: bfg-principal — POST /memory/builders/:builderId/bfg-write
|
|
431
449
|
postMemoryBuildersBuilderIdBfgWrite: (args) => request("POST", "/memory/builders/{builderId}/bfg-write", { hasBody: true }, args),
|
|
432
|
-
// GET /memory/builders/{builderId}/file — rank: archon — GET /memory/builders/:builderId/file
|
|
450
|
+
// GET /memory/builders/{builderId}/file — rank: metic+archon — GET /memory/builders/:builderId/file
|
|
433
451
|
getMemoryBuildersBuilderIdFile: (args) => request("GET", "/memory/builders/{builderId}/file", { hasBody: false }, args),
|
|
434
|
-
// GET /memory/builders/{builderId}/files — rank: archon — GET /memory/builders/:builderId/files
|
|
452
|
+
// GET /memory/builders/{builderId}/files — rank: metic+archon — GET /memory/builders/:builderId/files
|
|
435
453
|
getMemoryBuildersBuilderIdFiles: (args) => request("GET", "/memory/builders/{builderId}/files", { hasBody: false }, args),
|
|
436
454
|
// DELETE /memory/file — rank: any-builder — DELETE /memory/file
|
|
437
455
|
deleteMemoryFile: (args) => request("DELETE", "/memory/file", { hasBody: true }, args),
|
|
@@ -467,27 +485,27 @@ export function createClient(opts = {}) {
|
|
|
467
485
|
postMyProjectsJoin: (args) => request("POST", "/my-projects/join", { hasBody: true }, args),
|
|
468
486
|
},
|
|
469
487
|
"overrideRequests": {
|
|
470
|
-
// GET /override-requests — rank: archon — GET /override-requests
|
|
488
|
+
// GET /override-requests — rank: metic+archon — GET /override-requests
|
|
471
489
|
getOverrideRequests: (args) => request("GET", "/override-requests", { hasBody: false }, args),
|
|
472
|
-
// POST /override-requests/{id}/decide — rank: archon — POST /override-requests/:id/decide
|
|
490
|
+
// POST /override-requests/{id}/decide — rank: metic+archon — POST /override-requests/:id/decide
|
|
473
491
|
postOverrideRequestsIdDecide: (args) => request("POST", "/override-requests/{id}/decide", { hasBody: true }, args),
|
|
474
492
|
},
|
|
475
493
|
"projects": {
|
|
476
|
-
// GET /projects — rank: archon — GET /projects
|
|
494
|
+
// GET /projects — rank: metic+archon — GET /projects
|
|
477
495
|
getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
|
|
478
|
-
// POST /projects — rank: archon — POST /projects
|
|
496
|
+
// POST /projects — rank: metic+archon — POST /projects
|
|
479
497
|
postProjects: (args) => request("POST", "/projects", { hasBody: true }, args),
|
|
480
|
-
// PATCH /projects/{id}/featured — rank: archon — PATCH /projects/:id/featured
|
|
498
|
+
// PATCH /projects/{id}/featured — rank: metic+archon — PATCH /projects/:id/featured
|
|
481
499
|
patchProjectsIdFeatured: (args) => request("PATCH", "/projects/{id}/featured", { hasBody: true }, args),
|
|
482
500
|
// GET /projects/featured — rank: public — GET /projects/featured
|
|
483
501
|
getProjectsFeatured: (args) => request("GET", "/projects/featured", { hasBody: false }, args),
|
|
484
|
-
// POST /projects/invite — rank: archon — POST /projects/invite
|
|
502
|
+
// POST /projects/invite — rank: metic+archon — POST /projects/invite
|
|
485
503
|
postProjectsInvite: (args) => request("POST", "/projects/invite", { hasBody: true }, args),
|
|
486
504
|
},
|
|
487
505
|
"provisioning": {
|
|
488
|
-
// GET /provisioning/fleet — rank: archon — GET /provisioning/fleet
|
|
506
|
+
// GET /provisioning/fleet — rank: metic+archon — GET /provisioning/fleet
|
|
489
507
|
getProvisioningFleet: (args) => request("GET", "/provisioning/fleet", { hasBody: false }, args),
|
|
490
|
-
// GET /provisioning/fleet/cost-ledger — rank: archon — GET /provisioning/fleet/cost-ledger
|
|
508
|
+
// GET /provisioning/fleet/cost-ledger — rank: metic+archon — GET /provisioning/fleet/cost-ledger
|
|
491
509
|
getProvisioningFleetCostLedger: (args) => request("GET", "/provisioning/fleet/cost-ledger", { hasBody: false }, args),
|
|
492
510
|
// GET /provisioning/github-app/callback — rank: public — GET /provisioning/github-app/callback
|
|
493
511
|
getProvisioningGithubAppCallback: (args) => request("GET", "/provisioning/github-app/callback", { hasBody: false }, args),
|
|
@@ -501,7 +519,7 @@ export function createClient(opts = {}) {
|
|
|
501
519
|
postProvisioningInstances: (args) => request("POST", "/provisioning/instances", { hasBody: true }, args),
|
|
502
520
|
// GET /provisioning/instances/{id} — rank: any-builder — GET /provisioning/instances/:id
|
|
503
521
|
getProvisioningInstancesId: (args) => request("GET", "/provisioning/instances/{id}", { hasBody: false }, args),
|
|
504
|
-
// POST /provisioning/instances/{id}/force-teardown — rank: archon — POST /provisioning/instances/:id/force-teardown
|
|
522
|
+
// POST /provisioning/instances/{id}/force-teardown — rank: metic+archon — POST /provisioning/instances/:id/force-teardown
|
|
505
523
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
506
524
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
507
525
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
@@ -551,7 +569,7 @@ export function createClient(opts = {}) {
|
|
|
551
569
|
getPublicVersions: (args) => request("GET", "/public/versions", { hasBody: false }, args),
|
|
552
570
|
},
|
|
553
571
|
"scouting": {
|
|
554
|
-
// GET /scouting — rank: archon — GET /scouting
|
|
572
|
+
// GET /scouting — rank: metic+archon — GET /scouting
|
|
555
573
|
getScouting: (args) => request("GET", "/scouting", { hasBody: false }, args),
|
|
556
574
|
},
|
|
557
575
|
"search": {
|
|
@@ -563,24 +581,28 @@ export function createClient(opts = {}) {
|
|
|
563
581
|
"security": {
|
|
564
582
|
// GET /security/adr — rank: metic+archon — GET /security/adr
|
|
565
583
|
getSecurityAdr: (args) => request("GET", "/security/adr", { hasBody: false }, args),
|
|
566
|
-
// GET /security/docs — rank: archon — GET /security/docs
|
|
584
|
+
// GET /security/docs — rank: metic+archon — GET /security/docs
|
|
567
585
|
getSecurityDocs: (args) => request("GET", "/security/docs", { hasBody: false }, args),
|
|
568
|
-
// POST /security/docs — rank: archon — POST /security/docs
|
|
586
|
+
// POST /security/docs — rank: metic+archon — POST /security/docs
|
|
569
587
|
postSecurityDocs: (args) => request("POST", "/security/docs", { hasBody: true }, args),
|
|
570
|
-
// GET /security/docs/{slug} — rank: archon — GET /security/docs/:slug
|
|
588
|
+
// GET /security/docs/{slug} — rank: metic+archon — GET /security/docs/:slug
|
|
571
589
|
getSecurityDocsSlug: (args) => request("GET", "/security/docs/{slug}", { hasBody: false }, args),
|
|
572
|
-
// GET /security/reports — rank: archon — GET /security/reports
|
|
590
|
+
// GET /security/reports — rank: metic+archon — GET /security/reports
|
|
573
591
|
getSecurityReports: (args) => request("GET", "/security/reports", { hasBody: false }, args),
|
|
574
592
|
// POST /security/reports — rank: metic+archon — POST /security/reports
|
|
575
593
|
postSecurityReports: (args) => request("POST", "/security/reports", { hasBody: true }, args),
|
|
576
|
-
// POST /security/reports/{id}/confirm — rank: archon — POST /security/reports/:id/confirm
|
|
594
|
+
// POST /security/reports/{id}/confirm — rank: metic+archon — POST /security/reports/:id/confirm
|
|
577
595
|
postSecurityReportsIdConfirm: (args) => request("POST", "/security/reports/{id}/confirm", { hasBody: true }, args),
|
|
578
|
-
// POST /security/reports/{id}/dispute — rank: archon — POST /security/reports/:id/dispute
|
|
596
|
+
// POST /security/reports/{id}/dispute — rank: metic+archon — POST /security/reports/:id/dispute
|
|
579
597
|
postSecurityReportsIdDispute: (args) => request("POST", "/security/reports/{id}/dispute", { hasBody: true }, args),
|
|
580
|
-
// POST /security/reports/{id}/fix — rank: archon — POST /security/reports/:id/fix
|
|
598
|
+
// POST /security/reports/{id}/fix — rank: metic+archon — POST /security/reports/:id/fix
|
|
581
599
|
postSecurityReportsIdFix: (args) => request("POST", "/security/reports/{id}/fix", { hasBody: true }, args),
|
|
582
|
-
// POST /security/reports/{id}/link-fix — rank: archon — POST /security/reports/:id/link-fix
|
|
600
|
+
// POST /security/reports/{id}/link-fix — rank: metic+archon — POST /security/reports/:id/link-fix
|
|
583
601
|
postSecurityReportsIdLinkFix: (args) => request("POST", "/security/reports/{id}/link-fix", { hasBody: true }, args),
|
|
602
|
+
// GET /security/scans — rank: any-builder — GET /security/scans
|
|
603
|
+
getSecurityScans: (args) => request("GET", "/security/scans", { hasBody: false }, args),
|
|
604
|
+
// POST /security/scans — rank: metic+archon — POST /security/scans
|
|
605
|
+
postSecurityScans: (args) => request("POST", "/security/scans", { hasBody: true }, args),
|
|
584
606
|
},
|
|
585
607
|
"session": {
|
|
586
608
|
// POST /session/optimize — rank: any-builder — POST /session/optimize
|
|
@@ -593,7 +615,7 @@ export function createClient(opts = {}) {
|
|
|
593
615
|
getSessionsId: (args) => request("GET", "/sessions/{id}", { hasBody: false }, args),
|
|
594
616
|
// GET /sessions/mine — rank: any-builder — GET /sessions/mine
|
|
595
617
|
getSessionsMine: (args) => request("GET", "/sessions/mine", { hasBody: false }, args),
|
|
596
|
-
// GET /sessions/search — rank: archon — GET /sessions/search
|
|
618
|
+
// GET /sessions/search — rank: metic+archon — GET /sessions/search
|
|
597
619
|
getSessionsSearch: (args) => request("GET", "/sessions/search", { hasBody: false }, args),
|
|
598
620
|
},
|
|
599
621
|
"sso": {
|
|
@@ -661,17 +683,17 @@ export function createClient(opts = {}) {
|
|
|
661
683
|
getTasksClaimable: (args) => request("GET", "/tasks/claimable", { hasBody: false }, args),
|
|
662
684
|
// GET /tasks/newcomer-floor — rank: any-builder — GET /tasks/newcomer-floor
|
|
663
685
|
getTasksNewcomerFloor: (args) => request("GET", "/tasks/newcomer-floor", { hasBody: false }, args),
|
|
664
|
-
// POST /tasks/newcomer-restock — rank: archon — POST /tasks/newcomer-restock
|
|
686
|
+
// POST /tasks/newcomer-restock — rank: metic+archon — POST /tasks/newcomer-restock
|
|
665
687
|
postTasksNewcomerRestock: (args) => request("POST", "/tasks/newcomer-restock", { hasBody: true }, args),
|
|
666
688
|
// GET /tasks/peer-votes/feed — rank: any-builder — GET /tasks/peer-votes/feed
|
|
667
689
|
getTasksPeerVotesFeed: (args) => request("GET", "/tasks/peer-votes/feed", { hasBody: false }, args),
|
|
668
|
-
// POST /tasks/peer-votes/tally — rank: archon — POST /tasks/peer-votes/tally
|
|
690
|
+
// POST /tasks/peer-votes/tally — rank: metic+archon — POST /tasks/peer-votes/tally
|
|
669
691
|
postTasksPeerVotesTally: (args) => request("POST", "/tasks/peer-votes/tally", { hasBody: true }, args),
|
|
670
692
|
},
|
|
671
693
|
"versions": {
|
|
672
694
|
// GET /versions — rank: public — GET /versions
|
|
673
695
|
getVersions: (args) => request("GET", "/versions", { hasBody: false }, args),
|
|
674
|
-
// POST /versions — rank: archon — POST /versions
|
|
696
|
+
// POST /versions — rank: metic+archon — POST /versions
|
|
675
697
|
postVersions: (args) => request("POST", "/versions", { hasBody: true }, args),
|
|
676
698
|
// GET /versions/{id}/done-when — rank: public — GET /versions/:id/done-when
|
|
677
699
|
getVersionsIdDoneWhen: (args) => request("GET", "/versions/{id}/done-when", { hasBody: false }, args),
|
|
@@ -59,7 +59,7 @@ The defense doesn't depend on the ADR's secrecy. Every protection mentioned in t
|
|
|
59
59
|
- New builders see the public stub (this file) when reading the ADR index. The stub tells them how to read the full ADR once promoted to Metic+.
|
|
60
60
|
- Any change to the *real* defenses (new gated route, new redaction rule, new rate-limit knob) updates the full ADR on the droplet. Significant structural changes (new threat-model attacker, new defense category) also update this public stub.
|
|
61
61
|
- The `GET /api/gds/security/adr` endpoint exists *now* (V3.R68 / [#287](https://example.com/builders#/task/287)) but returns the not-provisioned placeholder until an Archon writes the file. That's intentional — the endpoint is the contract, the file is the content.
|
|
62
|
-
- A Xenos hitting the endpoint sees `403
|
|
62
|
+
- A Xenos hitting the endpoint sees a `403` whether or not the file is provisioned — the gate runs before the file-existence check, so existence of the document isn't leaked downward. The code is `permission_forbidden` with `{ required, held }`: BV1.R104 migrated this route (and the `/security/docs*` store below it) from `requireRank` to `requirePermission`, which admits exactly the same builders via the atom's catalog floor (ADR 0151). It read `rank_forbidden` before that.
|
|
63
63
|
|
|
64
64
|
## Cross-references
|
|
65
65
|
|
|
@@ -38,6 +38,8 @@ The default claim flow (`.claude/skills/builder-claim/SKILL.md`) ensures the tas
|
|
|
38
38
|
- **Predicate divergence from the task spec, on purpose.** The spec cited `WHERE status='active'`, but `claims` has **no `status` column** — "active" is the table's `released_at IS NULL` convention (the row's lifecycle status lives on `tasks`). The index uses the real column.
|
|
39
39
|
- **Migration safety.** A unique index *fails to create* if existing rows already violate it, which would block deploy. The migration first heals any current violation (keep the earliest active bound claim per session, close the rest — the same shape as migration 174), so the `CREATE` always succeeds; it is idempotent. The **app-layer guard is authoritative**; the index is belt-and-braces and must not break deploy.
|
|
40
40
|
|
|
41
|
+
> **Amended 2026-08-06 (task 1002570 / `core_204`).** The index as shipped was *stricter* than the worktree-scoped guard above: keyed on `creator_session_id` alone, it refused at the DB layer the second-worktree parallel claim the guard deliberately allows — breaking the many-sessions-per-builder norm this ADR set out to preserve. `migrations/<redacted>.sql` re-keys it to `(creator_session_id, COALESCE(worktree_name, ''))` under the name `uniq_active_claim_per_session_worktree`, same partial predicate, so the backstop now enforces exactly the guard's boundary. The `COALESCE` fold (rather than PG15's `NULLS NOT DISTINCT`) is load-bearing: a plain composite index treats NULLs as distinct, so two worktree-less claims off one shared clone would both insert and re-open the commingling hole. A `23505` from either index generation surfaces as `session_already_has_bound_claim` (task 1002589), never the false `ALREADY_CLAIMED`.
|
|
42
|
+
|
|
41
43
|
## Consequences
|
|
42
44
|
|
|
43
45
|
- A session can hold at most one active claim — commingled ships are structurally prevented, not just discouraged.
|
|
@@ -35,7 +35,7 @@ Stand up a **cloudbongos-native box plane** alongside (not replacing) the legacy
|
|
|
35
35
|
|
|
36
36
|
## Follow-ups (open)
|
|
37
37
|
|
|
38
|
-
1. **Box first-boot convergence — ROOT-CAUSED +
|
|
38
|
+
1. **Box first-boot convergence — ROOT-CAUSED + CLOSED (proper fix landed; [ADR 0150](<redacted>.md), task 1002386).** Symptom: provisioned boxes reached `active` but `cloudflared`/terminal never came up (`term-…` = CF 530) and SSH keys weren't applied. Root cause: **`box.js` provisioned droplets with EMPTY user-data** (`/var/lib/cloud/instance/user-data.txt` = 0 bytes; cloud-init logged "No user-data found"). `box.js` renders the box cloud-init from `<REPO_ROOT>/infra/` (the 26 KB `builder-box-cloud-init.yaml` + ~12 gz+b64-inlined scripts), and `fileGzB64` **silently returns `''` on a missing file**, so a missing `infra/` yields empty user-data with no error. The R89 split moved `scripts/gds/` into the `@bongos/core` package but **kept `infra/` in the instance repo** — yet `box.js` still resolves `infra/` relative to *its own* location (the vendored core), where `infra/` **isn't shipped at all** (0 files). So every box on any vendored/installed instance booted with only DigitalOcean's default user-data. **Workaround applied (temporary):** populated the vendored core's `infra/` from the instance's `infra/` (`/home/lars/cloudbongos/node_modules/@bongos/core/infra/`); re-provision then rendered a 62 KB user-data and the box converged — `cloudflared` active, `term-example-owner.cloudbongos.com` → 200 (authed), builder SSH key baked + working. **Proper fix — LANDED (spike task 1002385 → [ADR 0150](<redacted>.md); build task 1002386, "R98").** `box.js` gained `boxInfraCandidates()`/`resolveBoxInfraDir()` (an ordered instance-root-first search, including the `node_modules` walk-up) plus `BOX_INFRA_TEMPLATE`; `readCloudInit` now routes every `infra/` read through the resolved dir and **throws** on a missing/incomplete `infra/` instead of silently emitting empty user-data, and `cmdProvision` refuses to provision on empty user-data. Pinned by [`tests/box_infra_resolve.mjs`](../../tests/box_infra_resolve.mjs). The temporary `node_modules` workaround described above is therefore no longer load-bearing. *(Superseded task 1002243 was closed as part of task 1002557.)*
|
|
39
39
|
2. **Cloudflare token scope.** Provision warned the ADR 0053 cookie-strip Transform Rule was **not** applied — `CLOUDFLARE_API_TOKEN` lacks "Transform Rules" edit scope on the cloudbongos.com zone. Add the scope (or apply the rule once in the dashboard) **before** other builders get boxes on `*.cloudbongos.com`, so platform cookies (`cookieDomain=<redacted> aren't sent to box subdomains.
|
|
40
40
|
3. **Cosmetic `otb-` prefix.** Tunnel names (`otb-term-<login>`) and the droplet name (`otb-box-<login>`) are still hardcoded in `box-onboard.js`/`box.js`. Harmless (fleet separation is by tag, and DNS is per-zone), but a branding leak worth parameterizing.
|
|
41
41
|
4. **Phase 4 — retire the legacy example plane** (owner decision) — **part (a) DONE in task 1002248 (2026-07-15).** Pre-checked read-only that the legacy fleet was empty (0 `otb-builder-box` droplets on DigitalOcean; cohost intent queue `ran=0`), then `systemctl disable --now` on the four `cohost-box-*` timers (intent-runner / idle-suspend / drift-reconcile / dormant-reclaim) on the control-plane host — now `disabled`/`inactive`; the native `cloudbongos-box-*` timers were left `enabled`/`active`. **Part (b) — disabling the dev-box module on example.com itself — NOT reachable from the control plane, flagged to owner:** example's web app + instance checkout are not co-located on the cloudbongos droplet (no local checkout, no ssh alias) and the local example GDS token is expired, so a durable module-disable needs owner access to example's own config/host. Harmless meanwhile: with the runners stopped there is no cost/security impact — worst case is a box request on the legacy instance that now hangs unfulfilled.
|
|
@@ -99,7 +99,7 @@ The seed is what makes the cut-over behavior-identical. Four rank-roles are seed
|
|
|
99
99
|
| **`xenos`** | own-work + public read: `task.claim.newcomer`, `task.ship`, `task.act.own`, `cost.log`, `learning.create`, `blocker.file`, `idea.file`, `box.manage.own`, `memory.manage.own`, `credential.manage.own`, `page.view.public` |
|
|
100
100
|
| **`thetes`** | everything `xenos` has **+** `task.claim.any` (the whole general queue, still honoring each task's `requires_rank` floor), `page.view.builder` |
|
|
101
101
|
| **`metic`** | everything `thetes` has **+** `me.wandering.set` (higher clamp) **+** the review/triage/authoring set: `idea.triage`, `blocker.review`, `task.{create,edit,promote,demote,vote}`, `task.dependency.manage`, `task.criterion.manage`, `criterion.satisfy`, `criterion.review`, `planning.run`, `priority.run`, `goal.create`, `goal.archive`, `goal.scope.widen`, `goal.manage.own`, `module.enable`, `module.submit`, `security.report.file`, `llm_cache.use`, `backup.manage`, `source.clone.local`, `box.source.full` |
|
|
102
|
-
| **`archon`** | everything `metic` has **+** the **admin** operational set (`builder.status.set`, `builder.budget.set`, `task.abandon`, `criterion.unsatisfy`, `auth.session.revoke`, `access_request.review`, `security.report.adjudicate`, `provisioning.fleet.manage`, `box.fleet.manage`, `autonomy.precheck`, `discord.channels.manage`, `project.curate`, `override_request.decide`, `claim.release.any`, `idea.ratify`, `task.newcomer_restock`, `task.peer_votes.tally`, `goal.reorder`, `builder.roster.read`, `session.search`, `memory.read.any`, `audit_log.read`, `page.view.governance`) **+** the **`system`** set (`builder.rank.set`, `version.close`, `task.confirm.grade_bypass`, `dependency.cross_tier.manage`, `goal.scope.manage`, `governance.manage`, `gate_approval.decide`, `deploy.push_main`, `path.protected.modify`, `infra.ssh`). The `principal.*` permissions are **not** in any rank-role — they are held only by the machine-principal identities (`system_role='bfg'`/`'discord_bot'`), never by a human, not even an Archon (that is what gating on exact identity means). |
|
|
102
|
+
| **`archon`** | everything `metic` has **+** the **admin** operational set (`builder.status.set`, `builder.budget.set`, `task.abandon`, `criterion.unsatisfy`, `version.create`, `auth.session.revoke`, `access_request.review`, `security.report.adjudicate`, `provisioning.fleet.manage`, `box.fleet.manage`, `autonomy.precheck`, `discord.channels.manage`, `project.curate`, `override_request.decide`, `claim.release.any`, `idea.ratify`, `task.newcomer_restock`, `task.peer_votes.tally`, `goal.reorder`, `builder.roster.read`, `session.search`, `memory.read.any`, `audit_log.read`, `page.view.governance`) **+** the **`system`** set (`builder.rank.set`, `version.close`, `task.confirm.grade_bypass`, `dependency.cross_tier.manage`, `goal.scope.manage`, `governance.manage`, `gate_approval.decide`, `deploy.push_main`, `path.protected.modify`, `infra.ssh`). The `principal.*` permissions are **not** in any rank-role — they are held only by the machine-principal identities (`system_role='bfg'`/`'discord_bot'`), never by a human, not even an Archon (that is what gating on exact identity means). |
|
|
103
103
|
|
|
104
104
|
**Why cumulative preserves the threshold.** Today `requireRank('metic')` admits metic **and** archon (threshold = at-or-above). Because the seeded `archon` role ⊇ `metic` role, the resolver's union preserves exactly that: both roles grant `idea.triage`, so `requirePermission('idea.triage')` admits both — identical to the old threshold. A builder in the compat seed holds exactly **one** rank-role, so their effective set = that role's set = exactly their rank's threshold powers today. The parity test (R103) is the mechanical proof.
|
|
105
105
|
|
|
@@ -146,6 +146,7 @@ The `system` set is deliberately **tight** — only gates whose delegation would
|
|
|
146
146
|
| `builder.budget.set` | `PATCH /builders/:id/budget` |
|
|
147
147
|
| `task.abandon` | `POST /tasks/:id/abandon` — terminal scope removal |
|
|
148
148
|
| `criterion.unsatisfy` | `POST /done-when/:id/unsatisfy` |
|
|
149
|
+
| `version.create` | `POST /versions` — create a version + its done-when criteria. Added BV1.R105 (task 1001847): the route had no atom, so it was the one module write the migration could not finish. **Operational, not `system`,** even though `version.close` is `system` — the flag marks trust-boundary power, and creating a version is *authoring* (it confers no authority). Its siblings are `goal.create` / `task.create`. Close is the disposition that finalises a version's scope-truth; create opens it. |
|
|
149
150
|
| `auth.session.revoke` | `POST /auth/revoke`, `/auth/revoke-all` |
|
|
150
151
|
| `access_request.review` | `GET/PATCH /access-requests/:id` — admit a builder |
|
|
151
152
|
| `security.report.adjudicate` | `POST /security/reports/:id/{confirm,dispute,fix,link-fix}`, `/security/docs` CRUD |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# 0155 — Adopting a private repo: widen the onboarding OAuth scope to `repo`
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted — built in task [#1002561](https://example.com/builders#/task/1002561).
|
|
4
|
+
- **Date:** 2026-08-04
|
|
5
|
+
- **Deciders:** the owner chose the credential approach after being shown the trade-off; Claude diagnosed + built.
|
|
6
|
+
- **Amends:** [ADR 0143](<redacted>.md) §1 (its Decision 1 — scope choice). 0143 otherwise stands unchanged: opt-in per sign-in, lazy consent, encrypted short-TTL token store, personal repos only, 503-degrade. Per 0143's own append-only rule, this is a new ADR, not an edit.
|
|
7
|
+
- **Related:** [ADR 0121](<redacted>.md) (greenfield vs brownfield onboarding).
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Standing up a project on an **existing private repo** failed. Live evidence — `provisioning_instances` id=14, `example-owner/hermeslines-marketing`, `status='error'`:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
git -C /srv/cloudbongos/hermeslines-marketing … push https://x-access-token@github.com/… HEAD:main
|
|
15
|
+
remote: Write access to repository not granted.
|
|
16
|
+
fatal: … The requested URL returned error: 403
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`scopeForWebFlow()` requested `read:user public_repo`. **`public_repo` grants write to *public* repos only**, so the provisioner's scaffold push to a private repo 403s.
|
|
20
|
+
|
|
21
|
+
Three things confirm this was scope and not credentials:
|
|
22
|
+
|
|
23
|
+
1. The live `github_oauth_tokens` row was `scope='public_repo,read:user'` and **still valid** during the failure window. A dead token returns `401 Bad credentials`; `403 Write access to repository not granted` is GitHub's canonical *scope-insufficient* response.
|
|
24
|
+
2. Every **greenfield** project ever created worked, because `createUserRepo` hardcodes `private:false` — the platform only ever pushed to repos it had just made public. Adopt is the first flow where a private repo can be the target, so this had never been exercised.
|
|
25
|
+
3. The picker already renders a **"Private" badge** (`public-landing/create.html`) — UI written for a case `public_repo` made unreachable, since `/user/repos` omits private repos under that scope.
|
|
26
|
+
|
|
27
|
+
ADR 0143 §1 anticipated exactly this and named the exit: *"**Accepted limitation:** private repos can't be listed/adopted or created; revisit to `repo` only if private support is needed"*, with *"switching to `repo` (private support) is a one-line scope change"* under Consequences. This ADR takes that path.
|
|
28
|
+
|
|
29
|
+
Options weighed with the owner:
|
|
30
|
+
|
|
31
|
+
- **(A) Never let the server touch the user's repo.** For adopt, the user runs `bongos init --adopt` and pushes themselves — which the wizard already instructs — and provisioning only stands up server/DB/address. Keeps the narrow scope. Cost: a private repo still needs a read-only deploy key for the instance to *run*, and the "we set it up for you" promise weakens.
|
|
32
|
+
- **(B) Widen the scope to `repo` and keep pushing on the user's behalf.** One consent screen, no extra steps, adopt works for public and private alike. Cost: the persisted token can write to **every repo the user owns**.
|
|
33
|
+
- **(C) Public repos only.** Detect a private repo at the form and refuse early. Cheapest, but declines the feature.
|
|
34
|
+
|
|
35
|
+
## Decision
|
|
36
|
+
|
|
37
|
+
**Take (B) — `scopeForWebFlow({repos:true})` now returns `read:user repo`** — per the owner's explicit call, having been shown that this reverses 0143's least-privilege posture and enlarges what a leaked token can reach. Within that:
|
|
38
|
+
|
|
39
|
+
1. **`repo`, and only on the wizard path.** 0143 §2 is unchanged and load-bearing: the broadened scope is requested *only* via `?repos=1` from `/create`. Hall web sign-in and the CLI Device Flow stay `read:user`. `scopeForWebFlow` remains the single place the string is decided, and the unit test now asserts both that the wizard gets `repo` and that the default path carries **no** repo-acting scope.
|
|
40
|
+
2. **A stale-scope token must re-prompt, not degrade.** Widening the *request* does not upgrade tokens already stored. A surviving `public_repo` row still lists and creates *public* repos, so without a check the wizard would 200 while **silently omitting the user's private repos** — and the real failure would resurface minutes later as the same opaque push 403. New `grantedScopeCoversRepos()` parses GitHub's comma-separated granted scope and gates `resolveRepoToken`, which now returns the existing `repo_scope_required` (403) so the wizard's well-exercised reconnect path handles it. Parsing (not substring-matching) matters: `public_repo` *contains* `"repo"`, and a naive match would accept precisely the token this guards against.
|
|
41
|
+
3. **A repo *we* create stays public.** `createUserRepo` keeps `private:false`. Under `repo` it *could* create a private repo; it doesn't, because a Cloud Bongos instance is open-source / capture-resistant by design. This is now a **product** choice rather than a credential limit, it is deliberately not a parameter, and a test pins it so the widen can't silently change what we create. A user wanting a private instance adopts a private repo they made themselves.
|
|
42
|
+
4. **Consent is stated in our own words first.** GitHub's screen for `repo` reads as full control of all repositories. The wizard's connect copy now says plainly that this is the only scope that can write to a private repo, and what we do with it (read the repo you pick; push the setup commit), so the user does not first learn the breadth from GitHub's wording.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
- **Security posture — the real cost.** The stored token is now a materially bigger honeypot: write access to every repo the user owns, public and private. 0143's containment still applies unchanged and is what makes this tolerable — encrypted at rest via secret-box, **1h TTL**, lazily swept, requested only on the wizard path, and never stored at all when secret-box is unconfigured. The blast radius is nonetheless larger than before, and this is the ADR to revisit first if a token ever leaks.
|
|
47
|
+
- **A GitHub App remains the principled fix.** 0143 §Context option (B) — a central App with fine-grained `Contents:write` on *only* the selected repo — gives adopt-private without the account-wide grant. It was rejected then for the install step, and this decision makes the case for it stronger, not weaker. A future ADR adopting it would supersede this one.
|
|
48
|
+
- **Returning users reconnect once.** Anyone holding a live pre-0155 token gets one `repo_scope_required` prompt. The 1h TTL means the affected window is small.
|
|
49
|
+
- **The picker gains private repos** — `/user/repos` returns them under `repo`, lighting up the dormant "Private" badge.
|
|
50
|
+
- **This alone does not make adopt work.** The second, independent defect is server-side: provisioning has no notion of adopt and unconditionally `git init`s a fresh tree and pushes to `main`, which an existing repo rejects as unrelated history. Task [#1002562](https://example.com/builders#/task/1002562) fixes that; [#1002563](https://example.com/builders#/task/1002563) verifies both live. **Because the wider scope makes a mistaken `--force` in that path a genuine data-loss route on a user's repo, the adopt push must never force, and 1002562 carries a test asserting it.**
|
|
51
|
+
- **Reversal.** Narrowing back to `public_repo` is a one-line change plus dropping the scope guard, and re-accepts 0143's limitation.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 0156 — A builder's own display name wins, always
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-04
|
|
5
|
+
- **Deciders:** Lars (owner / Archon), Claude
|
|
6
|
+
|
|
7
|
+
**Track:** `internal` (GDS / builder experience). Task [1002568](https://cloudbongos.com/builders#/task/1002568), under the **Builder experience** goal [1000025](https://cloudbongos.com/builders#/goal/1000025).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
A builder sets their display name in the hall's Settings page. It saves — the toast confirms it, the sub-header updates, the row really is written — and then it silently reverts to their GitHub account name the next time they sign in.
|
|
14
|
+
|
|
15
|
+
The write path was never the problem. `PATCH /me/display-name` validates via `modules/builder-settings/display-name.js`, writes through `modules/builder-settings/db.js`, and every read surface (hall header, leaderboard, work feed) already prefers `display_name` over `github_login`.
|
|
16
|
+
|
|
17
|
+
The problem is that `upsertBuilder` (`src/bongos/db-kernel.js`) is named for account creation but runs on **every authentication**. Its conflict branch re-synced the whole identity row from GitHub:
|
|
18
|
+
|
|
19
|
+
```sql
|
|
20
|
+
ON CONFLICT (github_id) DO UPDATE
|
|
21
|
+
SET github_login = EXCLUDED.github_login,
|
|
22
|
+
display_name = EXCLUDED.display_name, -- ← the clobber
|
|
23
|
+
avatar_url = EXCLUDED.avatar_url
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`EXCLUDED.display_name` is whatever the caller just read off GitHub. All four login paths feed it: `pollDeviceFlow` (CLI setup/re-auth), `completeWebFlow` (browser hall sign-in), `completeIdpFlow` and `completeDeviceExchange` (federated hub). So the builder's own name survived exactly until their next sign-in — and because CLI re-auth counts, that is often the same working day.
|
|
27
|
+
|
|
28
|
+
That re-sync is not a bug on its own. It is why a builder who changes their name on GitHub sees the change reflected here without doing anything. Keeping that is a real requirement; the row is seeded from GitHub in the first place.
|
|
29
|
+
|
|
30
|
+
## Decision
|
|
31
|
+
|
|
32
|
+
**Once a builder sets their own display name, GitHub never overwrites it. Builders who never renamed keep syncing from GitHub.**
|
|
33
|
+
|
|
34
|
+
The row records *who last set the name*. A new core column, `builders.display_name_source text NOT NULL DEFAULT 'github'` (migration `<redacted>`, CHECK-constrained to `'github' | 'user'`), carries that one bit:
|
|
35
|
+
|
|
36
|
+
- `setBuilderDisplayName` (the only path a builder can rename themselves through) stamps `display_name_source = 'user'` alongside the name.
|
|
37
|
+
- `upsertBuilder`'s conflict branch guards the assignment:
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
display_name = CASE WHEN builders.display_name_source = 'user'
|
|
41
|
+
THEN builders.display_name
|
|
42
|
+
ELSE EXCLUDED.display_name END
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`github_login` and `avatar_url` keep re-syncing unconditionally — neither is user-editable, and `github_login` is identity, not a label.
|
|
46
|
+
|
|
47
|
+
`'github'` is the correct default for every pre-existing row: before this change there was no durable user-set name, so whatever is stored *is* the GitHub-derived value. A builder who had been fighting the revert re-saves once and it sticks.
|
|
48
|
+
|
|
49
|
+
### The rejected alternative: just delete the line
|
|
50
|
+
|
|
51
|
+
The one-line fix is to drop `display_name = EXCLUDED.display_name` from the conflict branch entirely. No migration, no new column, no new state.
|
|
52
|
+
|
|
53
|
+
It was rejected because it fixes one builder's problem by taking a feature away from everyone else. `display_name` would then be frozen at whatever GitHub reported the day the account was created. A builder who renames themselves on GitHub — or who had no GitHub display name at signup and so was seeded with their bare login — would be stuck with a stale label forever, with the hall Settings editor as the only way out. The revert bug affects builders who deliberately renamed; the frozen-name bug would affect everyone who never touched Settings, which is most of them. Trading a visible bug for a silent one is a bad trade.
|
|
54
|
+
|
|
55
|
+
The chosen fix costs one boolean-shaped column and keeps both behaviours correct.
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
- **New core column.** `builders.display_name_source`. Core, not `modules/builder-settings/migrations/`: the column sits on the core `builders` table and the guard that reads it lives in core `db-kernel.js` on the auth path. A module migration may only touch its own namespaced objects ([ADR 0083](<redacted>.md) §Decision #5), and core schema must not depend on whether a module is enabled. Same placement as the other `builders`-table preference columns (037, 075, 119, 164).
|
|
60
|
+
- **Not surfaced in the API.** `GET /me/display-name` and `docs/api/openapi.json` are unchanged. The flag is internal persistence state; no UI copy branches on whether a name is GitHub-derived, so exposing it would be surface with no consumer.
|
|
61
|
+
- **The federated hub needs no matching guard.** `modules/platform-identity/platform-identity.js`'s `upsertAccount` does the same unconditional re-sync on `platform_identity_accounts`, and its `display_name` reaches an instance through `completeIdpFlow`. It does not need changing: the `CASE` above runs regardless of what `EXCLUDED` carries, so an instance's `builders.display_name` is protected no matter which login path supplied the value. And `platform_identity_accounts` is identity-only with no self-service editor — there is no user-set value there to protect.
|
|
62
|
+
- **Regression-tested where it broke.** `tests/display_name.mjs` previously covered only the pure validator, which is exactly why this shipped: the half that was correct was the half under test. It now also reads the real SQL both halves emit and asserts the conflict branch's behaviour — a renamed builder keeps their name against a different incoming GitHub name, and a never-renamed builder still picks up a GitHub rename.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# 0157 — Archon is rank and identity only; the operational block delegates to Metic
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-05
|
|
5
|
+
- **Deciders:** Lars (Archon), Claude
|
|
6
|
+
|
|
7
|
+
**Builds on / amends:** [ADR 0016](<redacted>.md) (server-enforced trust boundary — **unchanged**, this ADR moves floors inside it, never the enforcement point), [ADR 0151](<redacted>.md) (permissions-as-atom — this ADR is only possible because the floor is declared once, in `catalog.js`), [ADR 0090](<redacted>.md) / [ADR 0152](<redacted>.md) (the two earlier one-power delegations this generalises), [ADR 0154](<redacted>.md) (**amends** — its "unsatisfy stays Archon" carve-out is superseded), [ADR 0086](<redacted>.md) (protected-scope goals — **unchanged**, `goal.scope.manage` stays Archon).
|
|
8
|
+
|
|
9
|
+
**Track:** `internal` (GDS / methodology). Task [1002584](https://cloudbongos.com/builders#/task/1002584).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
The Archon rank had accumulated 50 route gates. Almost none of them were about *authority*; they were about **administration** — reading the roster, listing dev boxes, viewing the audit log, adjudicating a vulnerability report, tearing down an idle droplet, reordering a queue, ratifying an idea. Each arrived Archon-gated for the same defensible reason: [ADR 0016](<redacted>.md) sets the default posture for any new privileged route to `requireRank('archon')` until a lower rank is explicitly granted — fail closed. Nothing ever came back to lower them, so "fail closed" quietly became "the owner is the only operator."
|
|
16
|
+
|
|
17
|
+
Two earlier ADRs had already delegated one power each on the same reasoning — [ADR 0090](<redacted>.md) (a rank trusted to *triage* ideas is trusted to *seed* the work they become) and [ADR 0152](<redacted>.md) (a rank trusted to author work is trusted to retire it). This ADR generalises that argument instead of relitigating it one route at a time.
|
|
18
|
+
|
|
19
|
+
Lars's directive (2026-08-05): **"Move all of these to metic, and make archon just responsible for rank and identity."**
|
|
20
|
+
|
|
21
|
+
### Why this is now a small change
|
|
22
|
+
|
|
23
|
+
The literal reading of that directive — edit 41 `requireRank('archon')` call sites — was the wrong shape, and briefly the plan. BV1.R104/R105 (jaxri, tasks [1001846](https://cloudbongos.com/builders#/task/1001846)/[1001847](https://cloudbongos.com/builders#/task/1001847)) had migrated the route gates to `requirePermission(...)` and were sitting at `confirmed` — graded, verified, unmerged — for five days. They landed mid-session.
|
|
24
|
+
|
|
25
|
+
Once every gate reads `requirePermission('<atom>')`, the rank floor is declared in exactly one place: `floor:` in [`modules/governance/catalog.js`](../../modules/governance/catalog.js). The delegation collapses from 41 call-site edits to **20 one-word floor changes in one table**. That is the ADR 0151 payoff arriving in practice, and it is worth recording as the concrete return on that refactor: the same owner decision was a 41-file diff on Monday and a 1-file diff on Tuesday.
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
|
|
29
|
+
### 1. Archon retains rank, identity, and four escalation keys — 13 atoms
|
|
30
|
+
|
|
31
|
+
| Atom | Why it stays |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `builder.rank.set` | **rank** — the single writer of `builders.rank`. The whole point. |
|
|
34
|
+
| `builder.status.set` | **identity** — offboard / (de)activate: who exists. |
|
|
35
|
+
| `access_request.review` | **identity** — who is admitted at all. |
|
|
36
|
+
| `auth.session.revoke` | **identity** — whose sessions are valid. |
|
|
37
|
+
| `task.confirm.grade_bypass` | escalation key (below) |
|
|
38
|
+
| `gate_approval.decide` | escalation key (below) |
|
|
39
|
+
| `goal.scope.manage` | escalation key (below) |
|
|
40
|
+
| `governance.manage` | escalation key (below) |
|
|
41
|
+
| `deploy.push_main`, `path.protected.modify`, `infra.ssh` | substrate-enforced (pre-push hook, `main-audit.js`), outside the HTTP boundary — [ADR 0043](<redacted>.md). Not reachable to delegate here. |
|
|
42
|
+
| `version.close` | see §3 |
|
|
43
|
+
|
|
44
|
+
### 2. The four escalation keys, and why a literal reading of the directive was refused
|
|
45
|
+
|
|
46
|
+
`builder.rank.set` staying Archon is **not sufficient** to keep "only the Archon sets rank" true. Four other atoms form a chain that ends in self-promotion, and every link was Archon-gated, which is why the chain was closed:
|
|
47
|
+
|
|
48
|
+
1. Create a goal scoped to a **protected** module — the permission core itself (`goal.scope.manage`).
|
|
49
|
+
2. Claim a task in it. **Already allowed**: `PROTECTED_GLOBS` floors at *metic*, by [ADR 0043](<redacted>.md)'s design.
|
|
50
|
+
3. Write code deleting the rank check, then confirm past the failed grade (`task.confirm.grade_bypass`).
|
|
51
|
+
4. Approve your own red CI deploy gate (`gate_approval.decide`) → it lands on `main`.
|
|
52
|
+
|
|
53
|
+
Plus `governance.manage`, whose routes R110/R111 have not built yet: it grants roles and permissions directly, so holding it means granting yourself anything — the meta-escalation.
|
|
54
|
+
|
|
55
|
+
Moving all 20 as literally asked would have opened that chain, making the delegation **self-reversible** — a Metic could reach Archon, at which point "Archon owns rank" is decoration. The concern was put to the owner with the chain spelled out; the owner chose to keep all four. Recording this because the reasoning is not visible from the floor values alone: *four of these look operational and are not.*
|
|
56
|
+
|
|
57
|
+
**This is the load-bearing invariant of this ADR:** the delegated set must contain no path to raising your own rank. A future delegation must re-run that check rather than assume it.
|
|
58
|
+
|
|
59
|
+
### 3. Two atoms held back beyond the four, each for a stated reason
|
|
60
|
+
|
|
61
|
+
- **`version.close`** — [`docs/canonical-permissions.md`](../canonical-permissions.md) has long paired closing a version with setting rank as the two powers that never delegate. Closing a version is the disposition that finalises shipped scope-truth. Kept, and flagged to the owner as a judgment call rather than a derivation. Its sibling `version.create` **did** move: authoring opens a container, disposition finalises one — the asymmetry is deliberate and is now recorded at both atoms.
|
|
62
|
+
- **`dependency.cross_tier.manage`** — `system: true`, and `tests/governance_catalog.mjs` enforces that no `system` permission is granted below archon. Moving it would mean clearing the `system` flag, which widens what any **custom role** can be granted — a second-order authority change nobody asked for. The flag is the wall; it was not quietly lowered to satisfy a floor change.
|
|
63
|
+
|
|
64
|
+
### 4. `gate_approval.read` is minted, splitting the Gate page's read from its write
|
|
65
|
+
|
|
66
|
+
`page.view.governance` moving to metic opens `/watch`, `/harbor`, `/gate` to the working rank. But `GET /gate-approvals` — the Gate page's only data endpoint — was still `requireRank('archon')`, one of three read routes R104/R105 deliberately skipped because their atom was declared over the module's *writes* only.
|
|
67
|
+
|
|
68
|
+
Left alone, a Metic would open `/gate` and get a page rendering nothing but a 403. So: `gate_approval.read` (metic) for the list, `gate_approval.decide` (archon, `system`) for approving a held PR. The same "the atom must name the read" step resolves the other two skipped gates — `GET /projects` (`project.curate`) and `GET /security/reports` (`security.report.adjudicate`) — whose write atoms had already moved to metic, which would otherwise have left the **read stricter than the write**: a Metic able to confirm a vulnerability report but not list one.
|
|
69
|
+
|
|
70
|
+
### 5. Pins move from ranks to atoms
|
|
71
|
+
|
|
72
|
+
`EXPECTED_RANKS` / `MODULE_EXPECTED_RANKS` in [`route-rank-check.js`](../../src/bongos/route-rank-check.js) pinned these routes to the literal rank `'archon'`. Rank pins break on every floor change; `pinMismatch` compares **atom identity** for `perm:` pins. The affected pins are now `perm:<atom>`, so the guarded invariant is *"this route gates on this permission"* — which is the property actually worth protecting — and a future owner decision to move a floor needs no pin edit.
|
|
73
|
+
|
|
74
|
+
## Consequences
|
|
75
|
+
|
|
76
|
+
**What a Metic gains.** The Watch / Harbor / Gate pages and the session log; the dev-box and provisioning fleets (including force-teardown and per-builder blocks); security-report adjudication; the audit log; the builder roster and per-builder grades; cross-builder memory reads; project curation and scouting; budget setting; idea ratification; override-request decisions; release-on-behalf; criterion un-satisfy; goal reorder; newcomer restock; peer-vote tally; version creation.
|
|
77
|
+
|
|
78
|
+
**The owner's remaining day-to-day surface** is rank changes, admissions, offboarding, session revocation, grade-bypass confirms, CI gate approvals, protected-scope goals, and version close. That is the intended shape: the owner sets *who may act and how far*; the working rank *acts*.
|
|
79
|
+
|
|
80
|
+
**Two consequences worth a second look** (both flagged to the owner, neither blocking):
|
|
81
|
+
|
|
82
|
+
1. **The exploit cookbook widened.** `security.report.adjudicate` guards `/security/docs` — the engagement-report store holding attacker-usable exploit detail — so that is now Metic-readable. Every other delegated atom widens who may *act*; this one widens who may *read*. If unwanted, the fix mirrors §4: split a `security.docs.read` at archon and let adjudication delegate. Recorded in `tests/security_docs.mjs` at the assertion that used to guard it.
|
|
83
|
+
2. **The backward ratchet delegated.** [ADR 0154](<redacted>.md) made "unsatisfy stays Archon" the explicit safety argument for delegating criterion *create* — the forward ratchet moved, the backward one did not. `criterion.unsatisfy` is neither rank/identity nor an escalation key, so under this directive it moved, superseding that carve-out. What still holds is the part that never depended on rank: satisfy and unsatisfy remain **separate atoms**, so confirming a criterion never confers the power to un-confirm one.
|
|
84
|
+
|
|
85
|
+
**A floor change needs a migration.** `governance_002`'s compat seed is run-once, so an edited `floor:` never reaches an already-migrated database. [`<redacted>.sql`](../../modules/governance/migrations/<redacted>.sql) grants the 20 to `metic` (+ `gate_approval.read` to both, since it is new). Lowering a floor is additive, so `tests/governance_seed.mjs`'s union model handles it and revokes nothing.
|
|
86
|
+
|
|
87
|
+
**`requireArchonPage` is now a misnomer** — it resolves `page.view.governance`, floor metic. Renaming it touches serve-internal, auth, and several test files; left deliberately to keep this diff small in authz code, and filed separately. The hall nav group was relabelled **Archon → Governance**, because a header reading "Archon" over items a Metic can open misdescribes the ladder to exactly the builders still learning it.
|
|
88
|
+
|
|
89
|
+
> **Resolved (2026-08-05, [#1002588](https://cloudbongos.com/builders#/task/1002588)).** That follow-up landed: the gate is now `requireGovernancePage`. It is a pure rename — same `page.view.governance` permission, same `deniedTo`, same audience. References to `requireArchonPage` in this ADR and in ADRs [0036](<redacted>.md) / [0058](<redacted>.md) / [0151](<redacted>.md) are left as written, since they describe what was true at the time; `src/bongos/auth.js` carries a comment naming the old spelling so a grep from any of them lands on the trail.
|
|
90
|
+
|
|
91
|
+
**What did not change.** [ADR 0016](<redacted>.md) end to end: authority is still resolved server-side, per request, uncached, from `builders.rank` via the seeded rank-roles. A builder editing their checkout still cannot grant themselves anything. This ADR moves floors *inside* that boundary; it does not touch the boundary.
|
|
92
|
+
|
|
93
|
+
**The ADR 0016 default posture still stands** for *new* routes: start at `requireRank('archon')` / an archon-floored atom and delegate deliberately. This ADR is that deliberate step taken in bulk for the accumulated backlog — not a licence to skip it.
|