@bongos/core 1.18.3 → 1.18.18
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 +1575 -285
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +4 -2
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +6 -1
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +8 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +51 -58
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -27
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +124 -2
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +112 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +12 -0
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +93 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +132 -7
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +80 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ADR 0147 — Carry ADR 0110's deploy-regen of generated docs to STANDALONE instances
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-07-17
|
|
5
|
+
- **Track:** internal
|
|
6
|
+
- **Builds on:** [ADR 0110](<redacted>.md) (untrack + deploy-regen the whole-file nav docs), [ADR 0108](<redacted>.md) (standalone instance = own repo + pinned `@bongos/core`), [ADR 0062](<redacted>.md) §8 (`gen-session-index.js`), [ADR 0063](<redacted>.md) (`gen-repo-map.js`)
|
|
7
|
+
- **Task:** [#1002314](https://cloudbongos.com/builders#/task/1002314)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
The owner's report: *"All of the docs in cloudbongos.com did not transfer well from primea. Make sure any new project is also set up with live-updating docs."*
|
|
12
|
+
|
|
13
|
+
The generated navigation docs (`repo-map.md`, `session-log-index.md`, `file-map.md`, the onboarding diagrams, the API reference) stay fresh on the **example monorepo** because ADR 0110's regen-on-deploy is wired into `scripts/deploy/deploy-prod.sh` + `infra/box-source-fetch.sh`. **STANDALONE instances (ADR 0108) never got that step.** A standalone instance is its own lean repo with `@bongos/core` vendored in `node_modules`; it deploys via a bespoke pull script and updates the core via `bongos upgrade`. Two concrete defects left its `docs/` frozen and nearly empty (cloudbongos.com had only the 3 API-doc files):
|
|
14
|
+
|
|
15
|
+
1. **`gen-session-index.js` hardcoded the core root** — `REPO_ROOT = path.resolve(__dirname,'..','..')`, the exact ADR 0108 configurable-root bug `gen-repo-map.js`/`gen-file-map.js` warn against. On a standalone instance it read/wrote `node_modules/@bongos/core/docs/session-logs`, never the instance's own sessions. It was left behind in the configurable-root migration.
|
|
16
|
+
2. **No standalone regen-on-deploy path.** The bespoke pull-deploy (`git reset --hard origin/main` → migrate → restart) regenerated *nothing*; `bongos init` + `bongos upgrade` only regenerated the API docs (`regenerateApiArtifacts`). So the whole-file nav docs were never seeded and never refreshed on a standalone box.
|
|
17
|
+
|
|
18
|
+
There is no `scripts/deploy/` in the neutral core — `deploy-prod.sh` is the monorepo's, not something a standalone instance runs.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
Carry ADR 0110's model to the standalone path — the standalone analogue of the `deploy-prod.sh` regen block.
|
|
23
|
+
|
|
24
|
+
1. **Fix the root bug.** `gen-session-index.js` resolves `REPO_ROOT = resolveInstanceRoot()` (mirrors `gen-file-map.js`), so it indexes the instance's own `docs/session-logs/` and writes the instance's `docs/session-log-index.md` + the instance-root `CLAUDE.md` §13 snippet. On the monorepo/at-ship it still resolves via the cwd fallback.
|
|
25
|
+
2. **A shared regen seam.** `upgrade.regenerateNavDocs({instanceDir, coreRoot})` runs the whole-file nav generators against the instance root, best-effort + LOUD (a broken generator prints `::ERROR::` but never fails the deploy/bump — ADR 0110 §2 "accepted staleness"). A thin CLI, `scripts/gds/regen-instance-docs.js`, is the one-liner a standalone deploy calls.
|
|
26
|
+
3. **Wired at every refresh point.** `bongos init` seeds it at standup; `bongos upgrade` refreshes it on a core bump; `provision.js`'s standalone deploy runs it after migrate; a bespoke pull-deploy (e.g. cloudbongos.com's `deploy.sh`) calls the CLI after `git reset --hard` + migrate.
|
|
27
|
+
4. **Gitignored on the instance.** `provision.js`'s `INSTANCE_GITIGNORE` ignores `docs/session-log-index.md` (+ `docs/repo-map.md` for parity), so `git reset --hard` never wipes the regenerated file and no branch can ever diff it — the same reasoning ADR 0110 applies on the monorepo.
|
|
28
|
+
|
|
29
|
+
**Scope — deliberately `session-index` only.** It is the one *gitignored, whole-file, instance-content* nav doc. Explicitly excluded:
|
|
30
|
+
|
|
31
|
+
- **`repo-map.md`** — CORE-scoped: `gen-repo-map` writes to `resolveCoreRoot()` (the platform-code map), not the instance. A pure standalone instance runs the core from `node_modules` and has ~no code of its own to map, so an instance-level repo-map is empty/meaningless; the core ships its own. An instance-scoped map for **brownfield/adopted** repos (which *do* carry their own code) is a separate enhancement (task 1001655).
|
|
32
|
+
- **`file-map.md` + the API docs** — TRACKED/committed (seeded at init, refreshed at ship/upgrade). Regenerating them on the box would perpetually dirty the tree that `deploy.sh` `git reset --hard`es. `file-map` is also HYBRID (hand-written prose + generated sections) so it cannot leave git; **seeding a fresh instance's `file-map.md` at standup is a follow-up** (it needs a scaffolded `file-map.md` skeleton with the section markers + a `file-map.notes.json`).
|
|
33
|
+
- **Diagrams** — the headless-browser + authed-GDS-session exception (same as ADR 0110 / task 1001641). They do not migrate to the box; they are rendered at package/CI time and shipped.
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
|
|
37
|
+
- A freshly-init'd standalone instance seeds `session-log-index.md`, and its deploy + `bongos upgrade` keep it live against the *instance's* sessions — the "docs didn't transfer" gap is closed for every new project, not just cloudbongos.com.
|
|
38
|
+
- **Accepted staleness (inherited from ADR 0110 §2):** the served whole-file doc is fresh only after a deploy/upgrade, not at merge — tolerable for a navigation index. A generator that breaks leaves a stale/absent file with no *blocking* backstop; the `::ERROR::` line is the alarm.
|
|
39
|
+
- **Follow-ups:** (a) seed `file-map.md` at standup (scaffold skeleton + notes); (b) an instance-scoped `repo-map` for brownfield/adopted repos (task 1001655); (c) productize a continuous pull-deploy timer for standalone instances (today only cloudbongos.com has a bespoke one) so content-only commits redeploy without a manual pull.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 0148 — Task-scoped box source access (rank-scope → claim-scope)
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted (spike R90 / task 1002378 — the architecture pass for goal 1000051). Confirms the build shape of R92–R95.
|
|
4
|
+
- **Date:** 2026-07-18
|
|
5
|
+
- **Deciders:** Lars (owner) — "claim a task → the box pulls only that module's code, on a base Bongos instance; the GitHub token stays on the server." Corrected the earlier "bring-your-own-GitHub" framing.
|
|
6
|
+
- **Extends:** ADR 0031 §6 (box source-access, task 600), ADR 0086 (goals + `scope_modules` scope wall), ADR 0093 (the `lifecycle` port), ADR 0052 (sandbox-for-everyone starter scope). Spec: [`docs/specs/<redacted>.md`](../specs/<redacted>.md).
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
A builder's dev box already pulls **scoped** source from the control plane — the GitHub token stays server-side (`box-credential.js`), the box calls `GET /box/source-access` authenticated as the builder, and **git cone-mode sparse-checkout** materializes only the returned folders (`infra/box-source-fetch.sh`, cron `*/10`). Today the scope is a coarse **rank** bucket: `boxScopeForRank(rank)` → `full` (Metic+, whole repo) or `starter` (a fixed `STARTER_SPARSE_PATHS` slice). We want the box to instead carry **the code the builder's claimed task needs**, on top of a base instance — moving the scope from *rank* to *task/module*.
|
|
11
|
+
|
|
12
|
+
The question this spike answers: **how does `box-access` derive the sparse paths from the builder's active claim(s) without breaking the dev-box↔core module boundary**, and what exactly do R92–R95 build.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
**Scope = `BASE ∪ (union of the builder's active claims' goals' `scope_modules`, mapped to globs)`.** The claimed task's **goal** is the unit of "what code this work needs" — its `scope_modules` is the already-declared module footprint (the ADR 0086 scope wall), so we reuse it rather than inventing a new per-task field. Union across active claims (owner decision). The transport (`/box/source-access` → `cloneSpecForScope` → sparse-checkout) is unchanged; only *what scope is computed* changes.
|
|
17
|
+
|
|
18
|
+
**The seam — four small pieces, each already has a home:**
|
|
19
|
+
|
|
20
|
+
1. **The map is already on the doorway.** `src/module-api.js` exposes `moduleScopeMap` (since doorway `1.12.0`, BV1.R86). So `box-access` reads `api.moduleScopeMap.scopeForModule(key).globs` directly — **no new kernel edit**. The only change is dev-box's `module.json` `coreVersion` `^1.2.0` → `^1.12.0` (core is 1.18.x; satisfied).
|
|
21
|
+
|
|
22
|
+
2. **The claims read is a small lifecycle-port add.** The `lifecycle` port already exposes `<redacted>`; add a sibling **`<redacted>(builderId)`** that JOINs `claims → tasks → goals` for the builder's active claims and returns the **set-union of `goals.scope_modules`** (empty array when no active claim). dev-box resolves it via `seams.resolveOptional('lifecycle')` — the ADR 0093 module-consumes-port pattern — and adds `lifecycle` to its `consumes` (optional; a vanilla instance without it degrades gracefully to the rank fallback).
|
|
23
|
+
|
|
24
|
+
3. **`box-access` gains a pure scope→paths function.** `sparsePathsForScopeKeys(scopeKeys, moduleScopeMap)` = `BASE_SPARSE_PATHS ∪ ⋃ directoryGlobs(scopeForModule(key))`. `decideSourceAccess({rank, status, scopeKeys})` gains the optional `scopeKeys`: **present → task-scope spec; absent/empty → base-only; no lifecycle port → the existing rank fallback.** Rank still governs ALLOW/DENY and the provision floor (a below-floor/inactive builder is still 403); rank no longer picks the module *breadth* once a claim drives it.
|
|
25
|
+
|
|
26
|
+
4. **The route resolves + passes.** In `GET /box/source-access` (which already reads `gdsDb.getBuilderById` via the doorway), resolve the lifecycle port, `const scopeKeys = await lifecycle?.<redacted>(req.builder.id)`, and pass it into `decideSourceAccess`. One added read on an existing handler.
|
|
27
|
+
|
|
28
|
+
**`BASE_SPARSE_PATHS`** (the "pre-existing Bongos instance" — always present so Claude has context + can run `/builder-*`): the non-product subset of today's `STARTER_SPARSE_PATHS` — `.devcontainer`, `.claude`, `scripts`, `src/bongos`, `docs`, `infra` (+ top-level files, auto by cone mode). Task-module globs layer on top.
|
|
29
|
+
|
|
30
|
+
### The cone-mode constraint (load-bearing)
|
|
31
|
+
|
|
32
|
+
Git **cone-mode** sparse-checkout accepts only **directory** entries — a file path aborts the whole `sparse-checkout set` (already documented in `box-access.js`). Module `scope_modules` → globs are of two shapes:
|
|
33
|
+
- **Directory globs** (`modules/dev-box/`, `devbox-app/`, `modules/onboarding/`, `modules/hall-ui/`) — cone-friendly, added directly.
|
|
34
|
+
- **File-list globs** (the `kernel` key = individual `src/bongos/*.js` files) — NOT cone-friendly, BUT their parent `src/bongos` is already in `BASE`, so a `kernel`-scoped task needs no extra sparse entry.
|
|
35
|
+
|
|
36
|
+
So R93 filters task globs to directory entries; file-list keys are covered by base. (A future move to non-cone sparse patterns could pull individual files, but it is out of scope — cone mode stays.)
|
|
37
|
+
|
|
38
|
+
## Consequences
|
|
39
|
+
|
|
40
|
+
- **Confirmed R92–R95 shapes** (the spike's deliverable):
|
|
41
|
+
- **R92** — dev-box `module.json` `coreVersion` → `^1.12.0` + `consumes:['lifecycle']`; add `lifecycle` port read `<redacted>(builderId)` (db.js query + `lifecycle.js` delegation + a DB-integration test). *Smaller than planned — the map is already exposed.*
|
|
42
|
+
- **R93** — `box-access.js`: `BASE_SPARSE_PATHS`, pure `sparsePathsForScopeKeys()`, `decideSourceAccess({…, scopeKeys})` with the three-way branch; unit tests (0/1/N keys, cone-filter, fallback).
|
|
43
|
+
- **R94** — `routes/box.js`: resolve lifecycle + pass `scopeKeys`; graceful when the port is absent.
|
|
44
|
+
- **R95** — folded into R93's pure decision: 0 claims → base-only; N claims → base ∪ all N goals' scopes. Kept as its own test-focused task.
|
|
45
|
+
- **Behavior change to flag:** an **idle Metic+** (no active claim) now gets **base-only**, not the whole-repo `full` clone they get today. That is the intent (the box is task-scoped), but it is a regression for a Metic+ who relied on an idle box holding everything. Preserving idle full-access would be a config toggle — **deferred**, not built here.
|
|
46
|
+
- **Security posture unchanged (C4):** the credential is still server-issued per fetch from `box-credential.js`; the box never stores the GitHub token durably. This spike does not touch the token model. The deferred hardening (short-lived per-claim credential + hard "can't over-pull" enforcement) is unaffected and still deferred — task-scope here is what the box *asks for*, not yet an *enforced* ceiling.
|
|
47
|
+
- **C4 verified (R102 / task 1002390):** the server-side-token posture is pinned by [`tests/box_credential.mjs`](../../tests/box_credential.mjs) (7 assertions): the credential is sourced only from the prod env (unset → `configured:false`, no fabricated token — ADR 0022); the secret lands in **no** field but `.token` (so the `source_grant` audit — now `scope=<sparse> cred=<rank> mode=…` — carries no secret); the credential scope stays **rank**-based (a separate `BOX_SOURCE_STARTER_REPO_URL` is a hard boundary that only `full` crosses, and an unexpected `task`/`base` label fail-closes to the non-full repo, never the crown jewels); the **claim-scoped `decideSourceAccess` decision carries no secret** (the token is minted *separately* by `box-credential.js`, so changing the sparse breadth never touches the token model); and an inactive/below-floor builder is denied **before** any credential is issued (instant, credential-layer revocation). The **deferred hardening** — a genuinely short-lived, per-claim-*minted* credential plus a hard "can't over-pull" isolation (physically separate per-task materialization) — remains explicitly OUT of scope for this goal; task-scope is the box's *ask*, and the token model is unchanged.
|
|
48
|
+
- **No new migration, no kernel/doorway change, no protected-path edit** beyond the additive lifecycle-port read — R92–R95 all sit inside the `dev-box` + `lifecycle` surface the goal is already scoped to (plus the R91 scope-map prep for the separate C2 chain).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 0149 — `bongos dev`: a one-command local launcher for a standalone instance
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted (owner decision — "we need a better option to stand up the local host").
|
|
4
|
+
- **Date:** 2026-07-19
|
|
5
|
+
- **Deciders:** Lars (owner) — chose a one-command local launcher over a cloud dev box or a recipe-only fix.
|
|
6
|
+
- **Extends:** [ADR 0108](<redacted>.md) (a standalone instance repo pins the core as a dependency), [ADR 0062 §5](<redacted>.md) (the core owns its own pool + branding). Sibling of `local-preview-lib.js` (task 1056 — the game-only local sandbox) and `onboard`/`provision.js` (the *cloud* standup path).
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
The project-creation flow scaffolds a repo whose `config/branding.json` points at `http://localhost:<port>` (each new project gets its own auto-incremented port — the trial produced test3→`:3005`, test4→`:3006`). The repo is **configured** to run on the owner's own machine, but the platform shipped **no command that actually starts it there**.
|
|
11
|
+
|
|
12
|
+
An owner who cloned a fresh project hit a wall (session export, 2026-07-19): `npm install` works, but then standing the instance up by hand is a six-part assembly — install + start Postgres, `createdb`, run migrations, export `PORT`/`PGDATABASE`/`PGHOST`/OAuth env, launch the server — before `bongos login` can even connect. The guided command that *does* exist, `bongos onboard`, provisions a **cloud** server (needs a public domain + DigitalOcean/Cloudflare tokens); the Docker path (`docs/recipes/self-host.md`) runs a **vanilla** brand on `:3000`, not the repo's brand on its own port. So *local-on-your-Mac* — exactly what a freshly-created project is configured for — was the one unsupported path.
|
|
13
|
+
|
|
14
|
+
Three silent mismatches made the manual path especially error-prone:
|
|
15
|
+
|
|
16
|
+
1. **Port.** `src/platform-server.js:44` reads `PORT` from env and **ignores branding** — so the server comes up on `3000` while branding (and every printed link) says `:3006`.
|
|
17
|
+
2. **DB name.** The server pool falls back to the OS username (`src/bongos/pool.js:47` via `instanceDbName()`, which is `null` in a fresh pack), while `scripts/migrate.sh:52` defaults to `example`. They only agree if `PGDATABASE` is set for **both**.
|
|
18
|
+
3. **Socket.** node-pg defaults to the Linux peer socket `/var/run/postgresql` (`pool.js:46`), which does not exist under Homebrew Postgres on a Mac — you must set `PGHOST`.
|
|
19
|
+
|
|
20
|
+
A fourth failure was operational: the manual server ran **unmanaged in the background**, so a stale server for one project (`:3005`) was still answering when the owner tried the next project — they signed into the wrong instance by accident.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
Ship **`bongos dev`** — one idempotent command that stands up *this* instance locally and hands off to sign-in.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
bongos dev up: Postgres → createdb → migrate → server → sign-in
|
|
28
|
+
bongos dev stop stop the background server this launched
|
|
29
|
+
bongos dev status is it up? what URL? is sign-in configured?
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`bongos dev` (default) runs, in order:
|
|
33
|
+
|
|
34
|
+
1. **Preflight** — `psql`/`createdb`/`pg_isready` present (clear `brew install` guidance if not); `bash` present for migrations.
|
|
35
|
+
2. **Postgres up** — if not running, start it (Homebrew `postgresql@N`, else `pg_ctl` with `PGDATA`), poll until ready.
|
|
36
|
+
3. **`createdb`** the instance's database if absent (migrate.sh does not create it).
|
|
37
|
+
4. **Migrate** — run the core `migrate.sh` with `MEDUSA_GDS_ONLY=1` (a fresh local instance never wants OTB's instance seeds).
|
|
38
|
+
5. **Start the server** DETACHED but **managed** (pid/log keyed per instance), poll `/healthz` + `/api/gds/healthz`.
|
|
39
|
+
6. **Sign in** — if creds resolve and the machine isn't already signed in to this instance, run `bongos login <origin>`; otherwise print the exact command (`--no-login` skips).
|
|
40
|
+
|
|
41
|
+
**It resolves the three mismatches from one source of truth (branding):** `PORT` = the port in `domains.publicOrigin`; `PGDATABASE` = `db.database` → repo dir name → `cloudbongos_dev`, exported for **both** migrate and the server; `PGHOST` defaults to TCP `localhost` (an explicit env override still wins). The pure resolution + process management live in `scripts/gds/dev-lib.js` (fully injectable, unit-tested in `tests/dev_lib.mjs`); the side-effecting orchestration is `scripts/gds/dev.js`.
|
|
42
|
+
|
|
43
|
+
**Managed-not-orphaned** is the fix for the wrong-instance accident: the background server has a pid/log keyed per `(dbName, port)`, so `bongos dev status`/`stop` make it visible and killable and a re-run is a no-op. `--foreground` runs it attached (Ctrl-C lifecycle) for those who prefer it.
|
|
44
|
+
|
|
45
|
+
**The one irreducibly-manual step** — a GitHub OAuth App — is only surfaced when no creds resolve (env `<PREFIX>_GITHUB_CLIENT_ID/SECRET` or `~/.config/<configDir>/gds-github.json`, mirroring `auth.js:loadGithubCreds`). A machine that already ran another instance carries usable creds and skips it entirely.
|
|
46
|
+
|
|
47
|
+
## Alternatives considered
|
|
48
|
+
|
|
49
|
+
- **Cloud dev box** (`bongos box` + `bongos code`) — the platform's blessed remote-dev model, but it needs the project *hosted* first and adds cloud cost; overkill for "try this project on my Mac." Rejected by the owner for the local case (still the right answer for remote/multi-machine work).
|
|
50
|
+
- **Docker** (`self-host.md`) — real, but serves the *vanilla* brand on `:3000`; matching the repo's brand + port needs a `docker-compose.override.yml` and a port change, and Docker Desktop is heavy for a non-technical owner. Kept as the Windows / container path.
|
|
51
|
+
- **Recipe only** (no command) — fastest, but leaves the six-step assembly and the three mismatches in the owner's hands every time. Rejected; the recipe ([`docs/recipes/local-dev.md`](../recipes/local-dev.md)) now just documents `bongos dev`.
|
|
52
|
+
|
|
53
|
+
## Consequences
|
|
54
|
+
|
|
55
|
+
- A freshly-created, localhost-configured project goes from `git clone` to a running, signed-in instance with `npm install` then `bongos dev` — no manual DB/env/port work.
|
|
56
|
+
- Windows is not covered by the native path (migrations need `bash`); those users take the Docker path. Noted in help + the recipe.
|
|
57
|
+
- `bongos dev` runs a repo verb (needs the installed core), so it is **not** bundled into the standalone on-ramp binary — it needs a real checkout + `npm install`, which the flow already has.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 0150 — Box first-boot bring-up on vendored instances: resolve `infra/` from the instance root, fail loud on empty user-data
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted (spike BV1.R97 / task 1002385 — the C2 diagnosis for goal 1000051 / 1000052). Confirms the build shape of R98 (task 1002386) + R99 (task 1002387).
|
|
4
|
+
- **Date:** 2026-07-19
|
|
5
|
+
- **Deciders:** Lars (owner) — "continue with C2" (make cloudbongos boxes stop coming up bare). Claude diagnosed + designed the fix.
|
|
6
|
+
- **Extends:** [ADR 0144](<redacted>.md) (its first follow-up item root-caused this), [ADR 0108](<redacted>.md) (the `resolveCoreRoot()`/`resolveInstanceRoot()` split this fix rides), [ADR 0031](<redacted>.md) (box lifecycle), [ADR 0148](<redacted>.md) (task-scoped source access — which only matters once a box actually boots).
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
"cloudbongos boxes come up bare" — a freshly provisioned box reaches `active` but has **no clone, no toolchain, no browser terminal** (`term-<login>` returns Cloudflare 530). The task-scoped source-access mechanism (ADR 0148, C1) is irrelevant until a box actually boots with the base instance present — so C2 is the practical foundation.
|
|
11
|
+
|
|
12
|
+
There are two provisioning paths in the fleet's history:
|
|
13
|
+
|
|
14
|
+
- **The reference (example) path** — `scripts/gds/box.js` renders a DigitalOcean cloud-init from `infra/builder-box-cloud-init.yaml` (+ ~12 gz+b64-inlined scripts: `box-source-fetch.sh`, the cloudflared/ttyd units, `box-heartbeat.sh`, …). The cloud-init is what clones the source, installs the toolchain, and starts the terminal. A box is "non-bare" **because** its user-data ran.
|
|
15
|
+
- **The cloudbongos path** ([ADR 0144](<redacted>.md)) — native `cloudbongos-box-*` runners against the local DB, but the **same `box.js`** renders the same cloud-init.
|
|
16
|
+
|
|
17
|
+
## Root cause (code-grounded + live-confirmed)
|
|
18
|
+
|
|
19
|
+
The box boots bare because **`box.js` renders EMPTY user-data and no one notices**:
|
|
20
|
+
|
|
21
|
+
1. `box.js` sets `REPO_ROOT = path.resolve(__dirname, '..', '..')` ([`scripts/gds/box.js:47`](../../scripts/gds/box.js)). On a **vendored** instance `box.js` lives at `node_modules/@bongos/core/scripts/gds/box.js`, so `REPO_ROOT` resolves to the **vendored-core package root**.
|
|
22
|
+
2. The R89 packaging split moved `scripts/gds/` **into** `@bongos/core` but **kept `infra/` as instance content** — `infra/` is **not** in the core publish-manifest, so `node_modules/@bongos/core/infra/` **does not exist**.
|
|
23
|
+
3. `readCloudInit()` reads the template at `REPO_ROOT/infra/builder-box-cloud-init.yaml` ([`box.js:258`](../../scripts/gds/box.js)). Missing dir → `readFileSync` **throws** → the surrounding `try { … } catch { return null }` ([`box.js:313`](../../scripts/gds/box.js)) swallows it → **`readCloudInit` returns `null`**.
|
|
24
|
+
4. The caller treats `null` as *zero bytes, fine*: `const userDataBytes = userData ? … : 0` then `if (userDataBytes > MAX_USERDATA_BYTES) fail()` ([`box.js:453-455`](../../scripts/gds/box.js)). `0 > 64KiB` is false, so the **only** size guard passes and provisioning proceeds with empty user-data. DigitalOcean logs "No user-data found"; the box is bare Ubuntu.
|
|
25
|
+
5. Even a **partial** `infra/` half-wires a box: `fileGzB64()` ([`box.js:268-275`](../../scripts/gds/box.js)) warns on a missing inlined file but still **returns `''`** (fail-soft), so the placeholder is substituted with nothing and that component silently never boots.
|
|
26
|
+
|
|
27
|
+
**Live re-confirmation (2026-07-19, on the cloudbongos droplet):**
|
|
28
|
+
|
|
29
|
+
- `infra/` **is present at the instance root** — `/home/lars/cloudbongos/infra/builder-box-cloud-init.yaml` exists.
|
|
30
|
+
- `infra/` **is absent in the vendored core** — `/home/lars/cloudbongos/node_modules/@bongos/core/infra/` is gone. The ADR 0144 stopgap (hand-copying `infra/` into `node_modules`) was **wiped by a core reinstall** since 2026-07-15, exactly as that ADR warned.
|
|
31
|
+
- `bongos.config.json` at the instance root already declares `{"instanceRoot": "."}` — so `resolveInstanceRootExplicit()` deterministically resolves to `/home/lars/cloudbongos`, where the real `infra/` lives.
|
|
32
|
+
- Fleet is idle (1 box, `destroyed`), so the bug is **latent but live**: nothing is broken this instant, but the **next** provision boots bare.
|
|
33
|
+
|
|
34
|
+
So the failure is a **packaging-locality bug**, not a logic bug: `box.js` looks for `infra/` in the one place it isn't (the core package) instead of the one place it is (the instance root).
|
|
35
|
+
|
|
36
|
+
## Decision
|
|
37
|
+
|
|
38
|
+
### R98 — make the box clone the base instance on boot
|
|
39
|
+
|
|
40
|
+
**Prong A — resolve `infra/` from the instance root** (ride [ADR 0108](<redacted>.md), exactly like [`scripts/migrate.sh:35`](../../scripts/migrate.sh) resolves its roots). A new `resolveBoxInfraDir()` in `box.js` returns the first candidate that actually contains `builder-box-cloud-init.yaml`, in order:
|
|
41
|
+
|
|
42
|
+
1. an explicit `<PREFIX>_BOX_INFRA_DIR` env override (escape hatch);
|
|
43
|
+
2. `resolveInstanceRootExplicit()/infra` — the env / `bongos.config.json` pointer (cloudbongos: `{"instanceRoot":"."}` → `/home/lars/cloudbongos/infra`);
|
|
44
|
+
3. `REPO_ROOT/infra` — the dev single-checkout case (and the old node_modules workaround location, for continuity);
|
|
45
|
+
4. a bounded walk **up out of `node_modules`** from the core root, so a vendored `…/node_modules/@bongos/core` finds its host instance root even with no pointer set.
|
|
46
|
+
|
|
47
|
+
All `infra/` reads (`readCloudInit`'s template + every `fileGzB64`) route through this resolved dir. **The neutral core still ships no `infra/`** — this changes only how `box.js` *locates* instance-owned infra, not what the core packages.
|
|
48
|
+
|
|
49
|
+
**Prong B — fail loud, never provision empty.** Two backstops so a bare box can never be created silently again:
|
|
50
|
+
|
|
51
|
+
- `readCloudInit()` **throws** an actionable error when no `infra/` dir with the template is found (stop returning `null`);
|
|
52
|
+
- `cmdProvision` aborts with `fail(...)` if the rendered user-data is empty / 0 bytes, naming the candidate dirs searched and pointing at this ADR — **before** touching DigitalOcean.
|
|
53
|
+
|
|
54
|
+
(Optionally escalate `fileGzB64` to hard-fail for the load-bearing inlined files, or assert no `__…_B64__` placeholders survive the render, so a *partial* `infra/` also surfaces rather than half-wiring.)
|
|
55
|
+
|
|
56
|
+
### R99 — toolchain builds on the box
|
|
57
|
+
|
|
58
|
+
Once `infra/` resolves and `box-source-fetch.sh` runs on first boot, the base instance clones. R99 then ensures the **runtimes** are present so a pulled module is actually buildable/runnable by Claude on the box: verify the cloud-init (or the devcontainer/`box-source-fetch` bring-up) installs Node + the common build tools, and that on a fresh box a pulled module's `npm ci` / build / test commands run. Concrete scope is R99's own; this ADR only fixes *getting the code + bootstrap onto the box*.
|
|
59
|
+
|
|
60
|
+
## Consequences
|
|
61
|
+
|
|
62
|
+
- **Reinstall-proof.** The fix reads the instance's own `infra/` (or an explicit pointer), so a core `npm ci` / `bongos upgrade` can never wipe it — the fragile ADR 0144 workaround-in-`node_modules` is retired.
|
|
63
|
+
- **Portable.** Every vendored instance benefits: `box.js` stops assuming `infra/` ships inside the core package. This is a **core** fix (lands on core `main`), not a cloudbongos-only patch.
|
|
64
|
+
- **Fail-loud tradeoff (intended).** A genuinely infra-less instance can no longer provision a box — it errors clearly instead of creating a useless bare one. A bare box is worse than a legible failure.
|
|
65
|
+
- **Go-live gap.** The fix lands on core `main`; it reaches cloudbongos.com only via the separate release (package-core + `bongos upgrade`). The fleet is idle today, so nothing is broken this instant; the next box provision needs the fix live (or the `node_modules` workaround re-applied as a one-off) — flagged to the owner in the session handoff.
|
|
66
|
+
- **No new migration, no doorway/kernel change.** The fix is contained to `scripts/gds/box.js` + a unit test; it reuses the existing `src/instance-config.js` resolvers.
|
|
67
|
+
- **R99 landed (task 1002387) — toolchain + test-runnability.** Core-side audit: the `.devcontainer` toolchain is complete (Node 22 via features + `build-essential`/`pkg-config`/Python 3.12 in the Dockerfile, with a build-time integrity healthcheck); the root `package.json` declares **no npm workspaces**, so a task-scoped *sparse* checkout can't break `npm ci`; and the task-scoped BASE already carries the top-level `package.json` + lockfile (cone-mode ancestor inclusion), so `npm ci` has what it needs. The one real gap: a module's **tests live in the top-level `tests/` dir** (`tests/box_*.mjs` + `tests/helpers.mjs`), which was in **neither** `BASE_SPARSE_PATHS` nor `STARTER_SPARSE_PATHS` — so a task-scoped or starter box pulled the module source but **not its tests**, and "a pulled module's build/test commands run" failed. Fix: add `tests` to both scope lists (`modules/dev-box/box-access.js`) — a cone-mode dir carrying only test code, no product runtime. Pinned by the updated `tests/box_access.mjs` + `tests/box_task_scope.mjs`. **Deferred to the live proof (R104, task 1002392):** the end-to-end "fresh box → `npm ci` → a pulled module's build/test actually runs" on the cloudbongos plane — the toolchain *install* rides the instance-only cloud-init (now reachable thanks to R98) and provisioning a box is owner-gated (spends money), so the live demonstration belongs to the C5 proof, not this core change.
|
|
68
|
+
- **R98 landed (task 1002386).** `scripts/gds/box.js` gained `boxInfraCandidates()`/`resolveBoxInfraDir()` (the ordered instance-root-first search) + `BOX_INFRA_TEMPLATE`; `readCloudInit` now routes every `infra/` read through the resolved dir and **throws** on a missing/incomplete `infra/` instead of returning `null`; `cmdProvision` resolves the source up front (dry-run + apply, printing `cloud-init source: <dir>`), catches the throw into a clean `fail()` with box state `error`, and refuses to provision on empty user-data. Pinned by [`tests/box_infra_resolve.mjs`](../../tests/box_infra_resolve.mjs) (8 assertions: candidate ordering incl. the node_modules walk-up, first-with-template selection, `null` when none, and the two fail-loud throws). Backward-compatible: a dev single-checkout still resolves `REPO_ROOT/infra`.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# 0151 — Governance: permissions as the atom, ranks as seeded roles
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-07-24
|
|
5
|
+
- **Deciders:** Lars (owner / Archon), Claude
|
|
6
|
+
- **Track:** `internal` (GDS / methodology — a Cloud Bongos platform capability)
|
|
7
|
+
- **Task:** BV1.R90 ([#1001838](https://cloudbongos.com/builders#/task/1001838)) — the foundation of the BONGOS-V1 **Governance** goal ([#1000030](https://cloudbongos.com/builders#/goal/1000030)); settles the model in an ADR before any of R93–R114 build against it.
|
|
8
|
+
- **Builds on:** [ADR 0016](<redacted>.md) (the server-enforced HTTP trust boundary — **preserved untouched**), [ADR 0043](<redacted>.md) (the out-of-band git/ssh substrate + protected-path rank floor), [ADR 0062](<redacted>.md) (core ↔ host split), [ADR 0083](<redacted>.md) (modules), [ADR 0084](<redacted>.md) (auto-derived per-task rank floor), [ADR 0086](<redacted>.md) (goal-scoped hierarchy + cross-tier edges), [ADR 0090](<redacted>.md) (Metic authoring powers), [ADR 0106](<redacted>.md) (ownership authority).
|
|
9
|
+
- **Gates:** R91 (module key) · R93/R94 (tables + catalog code) · R95 (resolver) · R97 (`requirePermission`) · R98–R101 (principals / session-scopes / ownership / registry) · R96/R102 (core-host split) · R104–R109 (gate migration) · R110/R111/R113 (the tab) · R103/R112/R114 (proofs). This ADR is the decision those tasks implement.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
Authority in Cloud Bongos is a **single rank threshold** today. [ADR 0016](<redacted>.md) put authority in one DB column (`builders.rank`), checked server-side per request with no caching; [ADR 0018](<redacted>.md) / [ADR 0034](<redacted>.md) wired the four live ranks into the ordered tier `xenos (1) < thetes (2) < metic (3) < archon (4)`. Every privileged route calls `requireRank(...allowed)`, which admits a builder whose rank sits **at or above** the lowest-authority rank named (a threshold, [#591](https://cloudbongos.com/builders#/task/591)). That design is sound and is **not** being replaced — it is the substrate this ADR builds *on top of*.
|
|
16
|
+
|
|
17
|
+
But a rank is an **all-or-nothing bundle**. There is no way to say "this builder may triage ideas but not author tasks," or to stand up a trusted reviewer who is not a full Metic, without inventing a new rank and threading it through a dozen-plus hand-copied tier maps (§Decision 2 counts 13). The owner wants a **Discord-style roles-and-permissions system**: configurable roles, assignable to builders, composed from granular permissions — where **permissions are the atomic primitive and ranks become seeded roles** — delivered with **zero day-one behavior change** and **without weakening the trust boundary** that is Cloud Bongos's one hard-to-copy moat ([ADR 0087](<redacted>.md): *compete on governance, not tooling*).
|
|
18
|
+
|
|
19
|
+
The 2026-07-03 governance planning session ([goal 1000030](https://cloudbongos.com/builders#/goal/1000030)) locked the shape and seeded the R90–R114 task chain against six done-when criteria (C1–C6). This ADR records the decision so those tasks build against **one** settled model instead of re-litigating it fourteen times, and it enumerates the **full permission catalog** (the appendix) grounded in the 2026-07-24 authority sweep of every server-side gate in the tree.
|
|
20
|
+
|
|
21
|
+
**What a rank threshold cannot express (the axes a role model must cover).** The sweep found four distinct *kinds* of authority already live in the code that a flat rank ladder flattens together:
|
|
22
|
+
|
|
23
|
+
1. **Rank-derived power** — `requireRank('metic')` and friends. The bulk. Maps cleanly to roles.
|
|
24
|
+
2. **Machine principals** — the BFG session-eval uploader and the Discord bot are gated on their **exact `system_role`** (`requireBfgPrincipal`, `requireDiscordBot` in `src/bongos/auth.js`), *not* rank: an Archon token is rejected because it carries `system_role = NULL`. A governance axis roles don't cover.
|
|
25
|
+
3. **Session-scopes** — a dev-box token (`session.source = 'box'`) is **deny-by-default**: it may reach only the routes that opted in via `allowBoxScope`, *regardless of how senior its builder is*. Capability attached to the session, orthogonal to the builder's rank.
|
|
26
|
+
4. **Ownership-scoped authority** — "act on **own** X" and "act on a goal you **own/manage**." Enforced today by in-handler pure gates (`authorizeMemberRoleChange`, [ADR 0106](<redacted>.md)) and by row-level ownership checks, not by `requireRank`.
|
|
27
|
+
|
|
28
|
+
A model that claims to be "the complete authority model" (criterion C3) has to represent all four.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
Six pillars, one per criterion.
|
|
35
|
+
|
|
36
|
+
### 1. Permissions are the atom; ranks become seeded roles (C1)
|
|
37
|
+
|
|
38
|
+
A **permission** is the atomic unit of authority — a stable string key (e.g. `idea.triage`, `task.create`, `builder.rank.set`) that maps to a real capability (a route, a claim rule, a page). Permissions are **defined in code, not UI-invented data**: because each key corresponds to an actual server capability, the catalog is a code constant that the resolver, the gates, and the tab all read from one place (R94). A permission that no route consumes does not exist.
|
|
39
|
+
|
|
40
|
+
A **role** is a named, assignable bundle of permissions (`governance_roles` + `governance_role_permissions`). A **builder** holds zero or more roles (`governance_builder_roles`).
|
|
41
|
+
|
|
42
|
+
The four live ranks become **four seeded roles** — `xenos`, `thetes`, `metic`, `archon` — whose grants reproduce today's rank thresholds **exactly** (the compat seed, below). `builders.rank` stays as the identity/graduation column it is today; the seed keeps each builder's role assignment in lock-step with their rank. Ranks do not disappear — they become the *default* roles a fresh instance ships, and the anchor the auto-graduation ([#692](https://cloudbongos.com/builders#/task/692)) and promotion ladder still move along.
|
|
43
|
+
|
|
44
|
+
A per-request, **uncached** resolver `resolveBuilderPermissions(builderId)` returns a builder's **effective permission set** = the union of their roles' permissions, read fresh on every request (R95). This preserves ADR 0016's single most important property — **instant demotion, no stale-authority window** — because a role change, like a rank change today, takes effect on the builder's very next request. The resolver is exposed through a **kernel port** so core `auth` resolves permissions without importing the `governance` module (respecting the ADR 0083 core-never-imports-a-module boundary; the module registers the port, core calls it).
|
|
45
|
+
|
|
46
|
+
A new middleware `requirePermission(...perms)` gates a route on the caller holding **all** of the named permissions (fail-closed: 403 on any missing). `requireRank` keeps working unchanged throughout the migration — the seeded rank-roles make `requireRank('metic')` and `requirePermission(<the metic-role's keys>)` equivalent — so gates migrate route-by-route with no flag day (R97).
|
|
47
|
+
|
|
48
|
+
### 2. Every DB-resolvable authority gate reads permissions, not rank (C2)
|
|
49
|
+
|
|
50
|
+
Once the primitive exists, the gates migrate off rank:
|
|
51
|
+
|
|
52
|
+
- **Core routes** (`src/bongos/routes/`) and the **~60 module route gates** (`modules/*/routes/*`) move `requireRank(...)` → `requirePermission(<key>)` with the catalog key that names each route's capability, behavior verified identical against the parity intent (R104, R105).
|
|
53
|
+
- **Claim eligibility** — today rank-driven (`xenosClaimAllowed` / the per-task `requires_rank` floor) — is re-expressed as permissions (`task.claim.newcomer` vs `task.claim.any`), with the per-task floor still honored and the abuse cases still blocked (R106).
|
|
54
|
+
- **HTML page gates** (`requireArchonPage` / `requireNonXenosPage` / `requireBuilderPage`) and the **wandering-prefs rank-default/ceiling clamp** resolve via permissions / role metadata (R107).
|
|
55
|
+
- The **duplicated rank-ladder / tier / order maps** collapse behind the resolver, with a fitness guard forbidding a new parallel map (R108). The 2026-07-24 sweep found **13 definition sites across 5 families** — `RANKS` (the full 16-value ladder, ×2 "twinned"), `LIVE_RANK_LADDER` (×2), `REQUIRES_RANK_TIER`/`RANK_TIER` (×3, one a deliberately-local copy in `permission-path-check.js`), `RANK_ORDER` (×4, one **0-indexed** while the tier maps are 1-indexed), and four one-off rank `Set`s — several commented as hand-kept copies. This is the exact drift class that *caused the Builder 25 incident* (a tier map that forgot `thetes` — [ADR 0043](<redacted>.md)); centralizing it is a security win, not just tidiness.
|
|
56
|
+
- The **route-rank-check fitness gate** learns to read `requirePermission`, re-expresses its EXPECTED pins in permission terms, and still hard-fails the build on an ungated or under-gated write route (R109).
|
|
57
|
+
|
|
58
|
+
### 3. The complete authority model — roles + principals + session-scopes + ownership-scoped permissions (C3)
|
|
59
|
+
|
|
60
|
+
The model represents all four axes from §Context as **first-class types**, not as rank special-cases:
|
|
61
|
+
|
|
62
|
+
- **Roles** — the rank-seeded and custom bundles of §1. The common case.
|
|
63
|
+
- **Principals** (R98) — a **machine identity** is a first-class principal type, its capability keyed on `system_role` (the existing `bfg` / `discord_bot` gate), **provisioned out-of-band and never UI-grantable**. The resolver treats a principal's capability distinctly from a builder's roles; the governance tab *shows* principals but cannot *grant* to them. (A human token can never acquire a principal capability — that is the point of gating on exact identity.)
|
|
64
|
+
- **Session-scopes** (R99) — a **session-scope** is a first-class capability the resolver honors: a box-scoped session's effective permissions are its **deny-by-default allow-list**, intersected with (never widening) its builder's role permissions — so a box session is limited even when its builder is an Archon. This models the current ad-hoc `allowBoxScope` flag explicitly.
|
|
65
|
+
- **Ownership-scoped permissions** (R100) — "act on **own** X" is a **parameterized** permission distinct from the global grant: `box.manage.own` ≠ `box.manage.any`, `claim.act.own` ≠ `claim.act.any`, likewise own-memory / own-credential, and the goal owner/manager authority of [ADR 0106](<redacted>.md). A route requires the scoped form; the resolver + an ownership predicate (the in-handler pure-gate pattern) decide it against the target row. Existing own-scoped routes migrate to it.
|
|
66
|
+
|
|
67
|
+
### 4. Policy owns / substrate enforces — the protected-surface registry + the system-permission wall (C6 / R101)
|
|
68
|
+
|
|
69
|
+
This is the pillar that keeps configurability from becoming a foot-gun, and it has two halves.
|
|
70
|
+
|
|
71
|
+
**The `system` flag.** Every permission in the catalog is flagged **`system: true`** (trust-boundary, **non-grantable**) or **`system: false`** (operational, grantable). A `system` permission — deploy, protected-path modification, rank-setting, version disposition, grade-bypass, cross-tier structural edges — **cannot be added to a custom role**; the governance API rejects the grant server-side and the tab renders it **LOCKED** (visible, so the model is legible; not togglable). `system: false` permissions are the ones an Archon may compose into custom roles freely. The rule for the flag: *a permission is `system` iff granting it to a non-Archon would breach the trust boundary or a scope/quality gate* (privilege management, scope disposition, quality-gate bypass, cross-tier structural edges, and everything the out-of-band substrate enforces).
|
|
72
|
+
|
|
73
|
+
**The protected-surface registry.** Today's rank→surface map — `PROTECTED_GLOBS` + the per-tier `EXPECTED_RANKS` — is baked into code (`src/bongos/permission-path-check.js`). Governance takes ownership of it **as data**: which paths are protected, and at what floor, becomes a governance-owned registry. The **out-of-band substrate stays the enforcer** — the pre-push hook, the grader pre-pass, `main-audit.js`, `gate-review.js`, and the goal-scope check *read the registry* instead of a hardcoded list, but enforcement never moves into the app. The Metic floor and the `system`-permission mapping *flow from* the registry. The registry (R101) is also the moment to **close the [ADR 0087](<redacted>.md) §3 path-matching gap** the sweep confirmed: `permission-path-check.js`'s `normalize()` does backslash→slash + strip-`./` only — **no case-folding and no Unicode normalization** — so a case-varied or homoglyph path evades the protected-glob match (Linux-deployed, so latent today, but it is the Cursor-class `.cursorignore`/case-bypass CVE shape). The registry matcher should normalize (case-fold + Unicode NFC) and gate file **creation** as strictly as edits.
|
|
74
|
+
|
|
75
|
+
The invariant this buys, stated as a testable promise (R112, R114): **the tab can re-map which surfaces are protected and at what floor (policy), but it cannot move enforcement into the request path (substrate).** An Archon can create a custom role and grant it every `system: false` permission in the catalog, assign it to a `xenos`, and that builder gains exactly those operational powers — **while a `system` permission the role "has" stays enforced by the substrate**: the deploy / protected-path / SSH attempt is still blocked, because [ADR 0043](<redacted>.md)'s layer-3 audit reads the real git history and the real registry, independent of anything the app grants. Config owns policy; the substrate owns enforcement; a forced grant cannot cross that line. (Note: `main` on this repo still has **no** GitHub branch protection — re-verified 2026-07-24, [ADR 0043](<redacted>.md) §"Branch-protection status" — so `main-audit.js` remains the *authoritative* substrate layer, not a redundant net. The registry does not change that; it only makes the protected list data instead of a constant.)
|
|
76
|
+
|
|
77
|
+
### 5. Capability in core, settings per project (C4)
|
|
78
|
+
|
|
79
|
+
Governance is a **core module** (`modules/governance/`, `default: true`) — a Cloud Bongos instance ships configurable roles the way it ships its status page. The *capability* (tables, resolver, catalog, gate, tab) lives in core; the *settings* (which roles exist, their starting grants) are **per-instance config**, mirroring the branding pattern:
|
|
80
|
+
|
|
81
|
+
`config/governance.json` (instance) resolves over `config/governance.neutral.json` (neutral starter, ships with the core package) with an env override on top — the exact `env → instance → neutral` precedence of `src/branding.js` (`resolveBranding` / `deepMerge`) (R96). The neutral config seeds the four rank-role templates a fresh instance inherits; **live** role assignments live in the DB (`governance_builder_roles`, R93), never in config. A vanilla instance boots with sane default roles, no OTB-specific identity, and the trust boundary intact — proven by a vanilla-boot fitness check (R102).
|
|
82
|
+
|
|
83
|
+
The tables are **module-owned, additive migrations** (`governance_roles`, `governance_role_permissions`, `governance_builder_roles`, all `governance_*`-namespaced per ADR 0083's module-migration rule), plus a `governance` scope-map key covering `modules/governance/**` + `config/governance*.json` (protected, since it overlaps the authz core — R91).
|
|
84
|
+
|
|
85
|
+
### 6. The Archon Governance tab + the proofs (C5, C6)
|
|
86
|
+
|
|
87
|
+
- **API** (R110): routes to list roles/permissions/assignments, create a role, toggle an **operational** permission on a role, and assign a role to a builder — all gated `governance.manage` (an Archon-seeded, itself-`system` permission), with `system` permissions **rejected as non-grantable server-side** and the routes pinned in route-rank-check.
|
|
88
|
+
- **Page** (R111, R113): `modules/hall-ui/public/governance.{html,js}` renders the role list, a per-role permission grid (toggle operational perms; `system` perms render LOCKED), and assign-role-to-builder — Discord-style, theme-aware, behind an Archon nav entry + page gate (defense-in-depth on top of the API gate).
|
|
89
|
+
- **Proofs**: a **parity test** (R103) enumerates every current builder + rank and asserts their resolved permission set grants **exactly** today's powers — no more, no less (red if the seed drifts). An **abuse-matrix test** (R112) asserts every attempt to grant a `system` permission to a custom role / low-trust builder is rejected, and the substrate still blocks the action even if a grant were forced. A **live demo** (R114) closes the goal: an Archon builds a custom role, assigns it, the builder performs the operational action — while a `system` permission stays walled.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## The compatibility seed (the "zero day-one change" guarantee)
|
|
94
|
+
|
|
95
|
+
The seed is what makes the cut-over behavior-identical. Four rank-roles are seeded with **cumulative (nested-superset)** grants, and every existing builder is backfilled the role matching their current rank (R93):
|
|
96
|
+
|
|
97
|
+
| Seeded role | Grants (cumulative — each ⊇ the one above) |
|
|
98
|
+
|---|---|
|
|
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
|
+
| **`thetes`** | everything `xenos` has **+** `task.claim.any` (the whole general queue, still honoring each task's `requires_rank` floor), `page.view.builder` |
|
|
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). |
|
|
103
|
+
|
|
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
|
+
|
|
106
|
+
**One deliberate, fail-closed divergence.** The threshold check currently *also* admits the reserved-but-unwired ranks **above** archon (`olympian`…`demigod`) to `requireRank('archon')` gates. The seed maps **only the four live ranks** to roles; a reserved rank maps to **no role → no permissions → fail-closed**. No current builder holds a divine rank, so parity holds for the actual roster; but the model is stricter than today's *latent* threshold-admit-divine edge. This is intended (fail-closed beats fail-open for an unwired rank) and is called out for R103/R106 so the parity test asserts against the live roster, not the reserved enum.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## The permission catalog (appendix)
|
|
111
|
+
|
|
112
|
+
Enumerated from the **2026-07-24 authority sweep** ([`docs/governance/<redacted>.md`](../governance/<redacted>.md) — 81 guarded routes + the ownership/page/substrate gates), grouped here by **capability** (one key may cover a set of sibling routes; the sweep doc holds the exhaustive 1:1 route→gate list R94 transcribes). `sys` = `system` flag (✔ = non-grantable, LOCKED in the tab). Ownership-scoped keys carry an `own`/`any` split. This catalog + the sweep doc are the single source R94 transcribes into `modules/governance/` code constants; R93 seeds roles against it.
|
|
113
|
+
|
|
114
|
+
### System permissions (`system: true` — non-grantable; substrate- or Archon-enforced)
|
|
115
|
+
|
|
116
|
+
`system` iff granting to a non-Archon would breach the trust boundary or a scope/quality gate — privilege management, scope disposition, quality-gate bypass, cross-tier structural edges, and everything the out-of-band substrate enforces.
|
|
117
|
+
|
|
118
|
+
| Key | Guards (route / surface) | Enforced by | sys |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `builder.rank.set` | `PATCH /builders/:id/rank` — the single rank writer; one-rung, upward | HTTP (`requireRank('archon')`) | ✔ |
|
|
121
|
+
| `version.close` | version close disposition (never delegated below Archon — canonical) | HTTP | ✔ |
|
|
122
|
+
| `task.confirm.grade_bypass` | `POST /tasks/:id/confirm` — ratify past a failed grade (env-relaxable; see note) | HTTP + grader | ✔ |
|
|
123
|
+
| `dependency.cross_tier.manage` | `POST\|DELETE /dependencies` — cross-tier structural edges ([ADR 0086](<redacted>.md)) | HTTP | ✔ |
|
|
124
|
+
| `goal.scope.manage` | cross-tier goal-scope edges + protected-scope admission ([ADR 0086](<redacted>.md)) | HTTP + goal-scope-check | ✔ |
|
|
125
|
+
| `governance.manage` | the governance tab's write routes (roles/perms/assignments) — meta-escalation | HTTP (`requireRank('archon')`) | ✔ |
|
|
126
|
+
| `gate_approval.decide` | `POST /gate-approvals/:pr/approve` — CI deploy-gate override | HTTP + gate-review substrate | ✔ |
|
|
127
|
+
| `deploy.push_main` | `git push origin main` / the deploy | **substrate** (pre-push, main-audit, deploy gate) | ✔ |
|
|
128
|
+
| `path.protected.modify` | modifying a `PROTECTED_GLOBS` path below the Metic floor | **substrate** (3 layers, [ADR 0043](<redacted>.md)) | ✔ |
|
|
129
|
+
| `infra.ssh` | `ssh` to the droplet / operator surface | **substrate** (credential placement, not app) | ✔ |
|
|
130
|
+
| `principal.bfg` | `/memory/bfg/*` — `requireBfgPrincipal` (exact `system_role='bfg'`) | HTTP (identity, not rank) | ✔ |
|
|
131
|
+
| `principal.discord_bot` | Discord bot routes — `requireDiscordBot` (exact `system_role`; dormant) | HTTP (identity, not rank) | ✔ |
|
|
132
|
+
|
|
133
|
+
The `system` set is deliberately **tight** — only gates whose delegation would *compromise the platform* (escalate rank, bypass the grader/deploy gate, touch protected paths/infra, or re-grant permissions). Every other authority — including many currently archon-gated **admin** ops (offboard, abandon, onboarding review, fleet teardown) — is **operational** and therefore *delegable*, which is the whole value of the model: an Archon can stand up a "moderator" or "onboarder" role without handing out deploy. (The exact system/operational line for a couple of scope-disposition ops is R94's to finalize; this ADR sets the rule and the default.)
|
|
134
|
+
|
|
135
|
+
> **Env-relaxable exception (flagged for R94/R106):** `POST /tasks/:id/confirm` is archon-only *unless* `GRADER_BYPASS_ENABLED=1` (read live per request, [ADR 0041](<redacted>.md)), which relaxes the middleware to any-builder — then the handler re-narrows in-code via `gateTaskOwnership` (claim holder or Archon only). The permission model represents this as `task.confirm.grade_bypass` (system) with a documented env override on the *substrate* side; the grant is never widened by config, only by the operator env flag.
|
|
136
|
+
|
|
137
|
+
### Operational permissions (`system: false` — grantable to custom roles)
|
|
138
|
+
|
|
139
|
+
"Floor" = the rank that holds this permission *today* via the compat seed; it is the seed's job, not an intrinsic property — a custom role may be granted any of these regardless of floor.
|
|
140
|
+
|
|
141
|
+
**Admin ops — floor `archon` today (delegable to a custom role; that is the point):**
|
|
142
|
+
|
|
143
|
+
| Key | Guards (route / surface) |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `builder.status.set` | `PATCH /builders/:id/status` — offboard / (de)activate |
|
|
146
|
+
| `builder.budget.set` | `PATCH /builders/:id/budget` |
|
|
147
|
+
| `task.abandon` | `POST /tasks/:id/abandon` — terminal scope removal |
|
|
148
|
+
| `criterion.unsatisfy` | `POST /done-when/:id/unsatisfy` |
|
|
149
|
+
| `auth.session.revoke` | `POST /auth/revoke`, `/auth/revoke-all` |
|
|
150
|
+
| `access_request.review` | `GET/PATCH /access-requests/:id` — admit a builder |
|
|
151
|
+
| `security.report.adjudicate` | `POST /security/reports/:id/{confirm,dispute,fix,link-fix}`, `/security/docs` CRUD |
|
|
152
|
+
| `provisioning.fleet.manage` | `GET /provisioning/fleet[/cost-ledger]`, `POST …/force-teardown` |
|
|
153
|
+
| `box.fleet.manage` | `GET /boxes[/cost-ledger]`, `POST /boxes/:b/close`, `PATCH …/block` |
|
|
154
|
+
| `autonomy.precheck` | `GET /autonomy/precheck` — unsupervised-runner gate |
|
|
155
|
+
| `discord.channels.manage` | `GET /discord/channels/plan`, `POST …/reconcile` |
|
|
156
|
+
| `project.curate` | platform-identity: `POST /projects`, `/projects/invite`, `PATCH /projects/:id/featured`, `GET /scouting` |
|
|
157
|
+
| `override_request.decide` | `GET /override-requests`, `POST …/:id/decide` |
|
|
158
|
+
| `claim.release.any` | `POST /claims/:id/release-on-behalf` |
|
|
159
|
+
| `idea.ratify` | `POST /inbox/:id/ratify` |
|
|
160
|
+
| `task.newcomer_restock` | `POST /tasks/newcomer-restock` |
|
|
161
|
+
| `task.peer_votes.tally` | `POST /tasks/peer-votes/tally` |
|
|
162
|
+
| `goal.reorder` | `POST /goals/reorder` |
|
|
163
|
+
| `builder.roster.read` | `GET /builders/roster`, `/grades/by-builder`, `/builders/:id/active-claims` |
|
|
164
|
+
| `session.search` | `GET /sessions/search` |
|
|
165
|
+
| `memory.read.any` | `GET /memory/builders/:id/file[s]` — cross-builder memory read (Archon) |
|
|
166
|
+
| `audit_log.read` | `GET /audit-log` |
|
|
167
|
+
|
|
168
|
+
**Working rank — floor `metic` today:**
|
|
169
|
+
|
|
170
|
+
| Key | Guards (route / surface) |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `idea.triage` | `PATCH /inbox/:id` |
|
|
173
|
+
| `blocker.review` | `POST /blockers/:id/{resolve,link}` |
|
|
174
|
+
| `task.create` / `task.edit` / `task.promote` / `task.demote` | `POST /tasks`, `PATCH /tasks/:id`, `/promote`, `/demote` ([ADR 0090](<redacted>.md)) |
|
|
175
|
+
| `task.dependency.manage` / `task.criterion.manage` | `/tasks/:id/dependencies` + `/criteria` CRUD (same-tier) |
|
|
176
|
+
| `task.vote` | `POST /tasks/:id/vote` |
|
|
177
|
+
| `criterion.satisfy` / `criterion.review` | `POST /done-when/:id/satisfy`, `GET /done-when/pending-review` |
|
|
178
|
+
| `criterion.create` | `POST /versions/:id/done-when` — floor `metic` but **qualified**: a non-Archon must name a `goal_id`, on a `building` version, in an `open` goal they own or manage ([ADR 0154](<redacted>.md)). A floor alone does not describe this permission. *(Was bundled into the `criterion.review` row above while the route was still Archon-only — a transcription slip against the sweep; see ADR 0154 §5.)* |
|
|
179
|
+
| `planning.run` / `priority.run` | planning- / priority-session surface |
|
|
180
|
+
| `goal.create` | `POST /goals` (protected scope → archon in-handler; [ADR 0086](<redacted>.md)) |
|
|
181
|
+
| `goal.archive` | `POST /goals/:id/{archive,reopen}` |
|
|
182
|
+
| `goal.scope.widen` | `POST /goals/:id/scope` (same-tier; cross-tier = `goal.scope.manage`, system) |
|
|
183
|
+
| `module.enable` / `module.submit` | `POST /modules/:key/{enable,submit}`, `GET /modules/submissions` |
|
|
184
|
+
| `security.report.file` | `POST /security/reports`, `GET /security/adr` |
|
|
185
|
+
| `llm_cache.use` | `POST /llm-cache/{lookup,store}` |
|
|
186
|
+
| `backup.manage` | `GET /backup/status`, `POST /backup/trigger` |
|
|
187
|
+
| `source.clone.local` | Metic+ local clone / collaborator invite ([ADR 0035](<redacted>.md)) |
|
|
188
|
+
| `box.source.full` | `GET /box/source-access` → `full` (vs `starter`); `boxScopeForRank` |
|
|
189
|
+
|
|
190
|
+
**Ownership-scoped (`own` ≠ `any`) + baseline — floor `thetes` / `xenos` / any builder / `own`:**
|
|
191
|
+
|
|
192
|
+
| Key | Guards (route / surface) | Floor |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `goal.manage.own` | `PATCH /goals/:id/members/:b`, `POST /goals/:id/transfer` — owner/manager, in-handler ([ADR 0106](<redacted>.md)) | owner/manager |
|
|
195
|
+
| `task.act.own` | act on **own** active claim (release, cost, notes) | own |
|
|
196
|
+
| `box.manage.own` | own dev-box lifecycle | own |
|
|
197
|
+
| `memory.manage.own` | own builder memory | own |
|
|
198
|
+
| `credential.manage.own` | own API key / credential | own |
|
|
199
|
+
| `me.wandering.set` | `PATCH /me/wandering` (clamped by `maxLevelForRank`) | own (rank-clamped) |
|
|
200
|
+
| `task.claim.any` | claim the general queue (per-task floor still applies) | thetes |
|
|
201
|
+
| `task.claim.newcomer` | claim a `newcomer_friendly` task (`xenosClaimAllowed`; xenos = one active claim) | xenos |
|
|
202
|
+
| `task.ship` · `cost.log` · `learning.create` · `blocker.file` · `idea.file` | own-work write surfaces | any builder |
|
|
203
|
+
| `page.view.public` | public read surfaces | any (incl. xenos) |
|
|
204
|
+
| `page.view.builder` | authenticated hall pages (`requireNonXenosPage` / `requireBuilderPage`) | thetes+ |
|
|
205
|
+
| `page.view.governance` | the Archon governance page (`requireArchonPage`; write = `governance.manage`) | archon |
|
|
206
|
+
|
|
207
|
+
> **Sweep-completeness note.** These keys cover every authority **domain** the 2026-07-24 sweep found ([the sweep doc](../governance/<redacted>.md) holds the exhaustive 1:1 route list — 81 guarded routes). R94 (the code transcription) is where each remaining `requireRank` site is mapped 1:1 to a key and any residual gap is closed; the route-rank-check fitness gate (R109) is the mechanical backstop that no write route ends up keyless. Keys are stable strings; adding a permission is a catalog + migration change, never a UI action. **A permission's `system` flag, not its floor, decides grantability** — floors are seed data and move; the flag is the wall.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Alternatives considered
|
|
212
|
+
|
|
213
|
+
- **Keep ranks; add more of them.** Rejected. Every new rank threads through the 13 hand-copied tier/ladder maps (the exact drift that caused the Builder 25 incident) and still can't express "triage but not author," principals, session-scopes, or ownership. Ranks-as-roles subsumes ranks without losing them.
|
|
214
|
+
- **Permissions replace ranks entirely (drop the rank column).** Rejected for the cut-over. `builders.rank` anchors auto-graduation, the promotion ladder, and the per-task `requires_rank` floor; ripping it out forfeits the zero-day-one-change guarantee. Ranks become *seeded roles* and the column stays as identity/graduation — a strangler, not a rewrite.
|
|
215
|
+
- **Make every permission grantable (no `system` flag).** Rejected — it is the whole risk. A configurable role that could be granted `deploy` or `path.protected.modify` would let an Archon (or a compromised Archon token) hand the crown jewels to a `xenos`. The `system` flag + "substrate enforces" is what makes the tab safe; the abuse-matrix test (R112) exists to prove it.
|
|
216
|
+
- **Enforce the protected-surface floor in the app (move the substrate in-process).** Rejected — it re-opens exactly the gap [ADR 0043](<redacted>.md) closed. Git/SSH sit *outside* the HTTP boundary; the authoritative audit must read real git history + real ranks/registry independent of the request path. Governance owns the *data* (policy); the out-of-band substrate stays the *enforcer*.
|
|
217
|
+
- **Cache the resolved permission set per token/TTL.** Rejected, same reasoning as ADR 0016 rejected caching rank: a cache is a stale-authority window on a demotion. The resolver is per-request and uncached; the cost is one more query on a join we already do, bought for instant revocation.
|
|
218
|
+
- **Store roles/permissions as free-form UI data.** Rejected. Permissions map to real routes, so the catalog is **code** — a permission no route consumes is meaningless. The UI assigns and toggles; it never invents a key.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Consequences
|
|
223
|
+
|
|
224
|
+
- **R91–R114 build against one settled model.** R93/R94 (tables + catalog code) transcribe this ADR's catalog and seed; R95–R97 build the resolver + gate; R98–R101 add the three non-role axes + the registry; R96/R102 do the core-host config split; R104–R109 migrate the gates and collapse the tier maps; R110/R111/R113 build the tab; R103/R112/R114 prove parity, the abuse matrix, and the live demo.
|
|
225
|
+
- **ADR 0016 is preserved, not amended.** Authority still lives server-side, resolved fresh per request against the DB, with instant demotion. The unit of authority narrows from *rank* to *permission*; the enforcement property is unchanged. `requireRank` and `requirePermission` coexist during the migration via the seed.
|
|
226
|
+
- **The trust boundary strengthens.** The 13 duplicated tier/ladder maps — a proven incident source — collapse to one resolver behind a fitness guard; the protected-surface list becomes auditable data with case/Unicode-normalized matching and a mechanical abuse-matrix proof; principals and session-scopes stop being ad-hoc special-cases and become modeled, testable types.
|
|
227
|
+
- **Configurability is bounded by design.** An Archon can compose any `system: false` permission into a custom role, but the `system` set is non-grantable and substrate-enforced, so no configuration can hand out deploy / protected-path / rank-setting power. This is the C6 safety guarantee, proven not asserted.
|
|
228
|
+
- **Zero day-one behavior change.** The compat seed reproduces every current builder's exact powers; the one divergence (unwired divine ranks fail closed instead of latently inheriting Archon) is a fail-closed improvement no live builder touches.
|
|
229
|
+
- **Portable, and the moat.** Governance ships as a `default: true` core module with a neutral config every instance inherits; it is precisely the "governance / audit / org-memory" substrate [ADR 0087](<redacted>.md) named as the hard-to-copy asset. No new recurring cost — tables + code + a config file.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# 0152 — The landing gate is served by the core app, not by the front proxy
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-07-27
|
|
5
|
+
- **Deciders:** Lars (owner / Archon), Claude
|
|
6
|
+
- **Track:** `internal` (platform serving / instance infra)
|
|
7
|
+
- **Task:** <redacted> ([#1002411](https://example.com/builders#/task/1002411)) — the cutover step of the landing-migration chain L01→L07 under the **Project creation flow** goal ([#1000035](https://example.com/builders#/goal/1000035)).
|
|
8
|
+
- **Builds on:** [ADR 0062](<redacted>.md) (core ↔ host split), [ADR 0108](<redacted>.md) (instance composition / pinned vendored core), [ADR 0111](<redacted>.md) (per-instance Caddy snippets), [ADR 0003](<redacted>.md) (origin certs).
|
|
9
|
+
- **Supersedes in practice:** the "give the landing its own Caddy config + its own deploy script" plan that L05 was scoped around. See §Decision.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
The reference instance's landing page — the interactive orb "gate" — was historically **served off disk by the front proxy**. Caddy's apex block set `root * <checkout>/brand/cloud-bongos` and `file_server`'d `/`, `/gate/*` and `/world/*` straight out of a checkout of a *different* product repo (`example-owner/example`), while proxying every functional path to the platform app. The landing therefore had:
|
|
16
|
+
|
|
17
|
+
- **two sources of truth** — the gate bundle lived in the other repo, and a copy of the serving rules lived in that repo's `infra/Caddyfile` and `scripts/deploy/deploy-cloudbongos.sh`;
|
|
18
|
+
- **no deploy path of its own** — updating the landing meant pushing to the other repo and hand-running `git reset --hard origin/main` on the box;
|
|
19
|
+
- **a cross-product coupling** the core could not express: a Cloud Bongos instance's front page was owned by an unrelated product's repository.
|
|
20
|
+
|
|
21
|
+
L01 ([#1002407](https://example.com/builders#/task/1002407)) ported the verified gate bundle into the core as `public-landing/`, and L02 ([#1002408](https://example.com/builders#/task/1002408)) taught `src/platform-server.js` to serve it: `GET /` on the apex host 302-redirects to `/gate/`, `/gate` 301s to `/gate/`, `/gate/` serves the gate index raw, and the existing `express.static(public-landing)` already covers `/gate/*`, `/world/*` and `/assets/*`.
|
|
22
|
+
|
|
23
|
+
Once L01+L02 shipped, **the app could already serve the entire landing on its own** — but the live box was still file-serving the other repo's copy, because nothing had flipped the proxy. L05 was scoped (before L01/L02 existed) as "give cloud-bongos (a) its own Caddy config and (b) its own deploy script, then cut the box over." By the time L05 came up, both halves of that plan had dissolved.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
**The core app owns the landing end to end. The front proxy does TLS, the staging lock, and `reverse_proxy` — nothing else.**
|
|
28
|
+
|
|
29
|
+
Concretely, the apex and vanity-door site blocks drop `root` + `file_server` and proxy the landing paths to the app, exactly like every other path:
|
|
30
|
+
|
|
31
|
+
```caddy
|
|
32
|
+
example.com, www.example.com {
|
|
33
|
+
tls /etc/caddy/origin.crt /etc/caddy/origin.key
|
|
34
|
+
encode gzip zstd
|
|
35
|
+
import security_headers
|
|
36
|
+
|
|
37
|
+
# The landing surface. Identical to the catch-all except for the staging
|
|
38
|
+
# lock — so lifting the lock is a one-block delete, not a re-plumb.
|
|
39
|
+
@gate path / /gate /gate/* /world/*
|
|
40
|
+
handle @gate {
|
|
41
|
+
basic_auth {
|
|
42
|
+
# optional pre-launch lock; omit once the site is public
|
|
43
|
+
}
|
|
44
|
+
# Caddy runs `redir` BEFORE `basic_auth`, so "/" redirects first and the
|
|
45
|
+
# visitor meets the lock at /gate/. The app issues the same 302, so this
|
|
46
|
+
# line stays correct after the lock is removed.
|
|
47
|
+
redir / /gate/ 302
|
|
48
|
+
reverse_proxy 127.0.0.1:3000
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
handle {
|
|
52
|
+
reverse_proxy 127.0.0.1:3000
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Three consequences follow, and they are the actual decision:
|
|
58
|
+
|
|
59
|
+
**1. There is no separate Caddy config to "give" the landing.** The block above is the *generic* instance block — the same pure-proxy shape `provision.js`'s `caddyBlock()` already writes for every provisioned instance ([ADR 0111](<redacted>.md) §4). The landing stopped being a special case in the proxy the moment the app could serve it. Host-specific values (hostnames, cert paths, whether a lock exists) stay on the host, per [ADR 0062](<redacted>.md) §7 — the core ships the *shape*, not an instance's hostnames.
|
|
60
|
+
|
|
61
|
+
**2. There is no separate deploy script to write.** The landing ships **inside the pinned core package**, so it rides the instance's existing deploy: bump the core pin in the instance repo → the box's pull-deploy picks it up → the new landing is live. One artifact, one version number, one deploy path. A landing change is now a normal core change (edit `public-landing/`, ship, release), not an out-of-band push to a foreign repo.
|
|
62
|
+
|
|
63
|
+
**3. The proxy keeps exactly three jobs on the landing surface:** terminate TLS, enforce any pre-launch `basic_auth` lock, and hide anything the bundle ships but should not expose (the gate's own `README.md` — an `@hidden` matcher for `*.md`/`*.json`/`*.py` and the brand-pipeline `proof`/`refs`/`references` paths). These are deployment-environment concerns, correctly the proxy's; everything else is the app's.
|
|
64
|
+
|
|
65
|
+
## Consequences
|
|
66
|
+
|
|
67
|
+
**Good**
|
|
68
|
+
|
|
69
|
+
- **One source of truth.** The landing bundle exists once, in the core, versioned with everything else. The foreign-repo checkout becomes dead weight (retired by L06, [#1002412](https://example.com/builders#/task/1002412)).
|
|
70
|
+
- **Portable.** Any instance that ships a `public-landing/gate/` gets the gate with a stock proxy block. L02 already guards this: the apex `/` → `/gate/` redirect only engages when the instance actually ships a gate, so a gate-less instance keeps its plain placeholder.
|
|
71
|
+
- **Atomic + rollback-able.** The landing and the app that serves it move together on one pin. Rolling the core back rolls the landing back with it.
|
|
72
|
+
- **Smaller proxy config.** No `root`, no `file_server`, no filesystem path in the site block — so a moved or re-cloned checkout can no longer silently change what the front page serves.
|
|
73
|
+
|
|
74
|
+
**Costs / risks**
|
|
75
|
+
|
|
76
|
+
- **The landing is now in the app's request path.** A dead app means a dead front page, where previously Caddy could still serve static files. Accepted: the front page's whole purpose is to reach the app (sign-in, project creation, the live featured-projects feed that fills the orbs), so a dead app already meant a useless landing.
|
|
77
|
+
- **A landing tweak needs a core release.** Slower than pushing to a static checkout. That is the point — the landing is now under the same review and versioning as the rest of the platform.
|
|
78
|
+
- **The `@hidden` guard is load-bearing and hand-maintained.** It is what keeps `gate/README.md` unreachable. If the landing bundle ever gains a file that should not be public, the matcher must keep up. Better long-term fix: don't ship non-public files in `public-landing/` at all.
|
|
79
|
+
|
|
80
|
+
## Alternatives considered
|
|
81
|
+
|
|
82
|
+
- **Keep file-serving, but from the core's `public-landing/` instead of the foreign checkout.** Would have removed the cross-repo coupling with a one-line `root` change. Rejected: it hardcodes a `node_modules/@bongos/core/...` path into the proxy config, which then breaks on any packaging-layout change, and it keeps the landing a special case in a config the core cannot ship.
|
|
83
|
+
- **Write the landing its own Caddy config + deploy script, as originally scoped.** Rejected: it builds a second deploy path to maintain, for an artifact that already ships inside the core package and already has one.
|
|
84
|
+
- **Serve the landing from a CDN / object store.** Rejected: the gate is not fully static — it calls the instance's own API for the featured-projects feed and the sign-in state — and it adds a service and a cost for a page the app can serve for free.
|
|
85
|
+
|
|
86
|
+
## Verification
|
|
87
|
+
|
|
88
|
+
The cutover was verified against the live reference instance:
|
|
89
|
+
|
|
90
|
+
- Every landing path returns an identical status code before and after the flip, across the apex and both vanity doors — including the staging lock (`401` on `/gate/*`), the `@hidden` guard (`404` on `/gate/README.md`), and the proxied functional paths (`/healthz`, `/docs`, `/create`, the featured-projects feed).
|
|
91
|
+
- The app's redirects are **relative** (`Location: /gate/`), so proxying them leaks no internal origin.
|
|
92
|
+
- **Severance test:** with the foreign repo's landing folder moved aside entirely, the site continued to serve the byte-identical gate (same md5, same 34,366 bytes) and every asset, proving the dependency is genuinely cut rather than merely unused.
|