@bongos/core 1.18.0 → 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 +1740 -640
- 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 +5 -3
- 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 +11 -2
- 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/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- 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 +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- 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 +15 -7
- 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/packaging-the-core.md +13 -0
- 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/upgrading-the-core.md +5 -2
- 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/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- 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/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- 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/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- 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/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -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 +252 -62
- 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 -21
- 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/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- 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 +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- 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 +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -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 +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -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 +41 -23
- 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/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -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 +198 -0
- package/tests/idp_federation.mjs +100 -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/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- 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/package_core.mjs +246 -1
- 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 +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- 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 +140 -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 +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// modules/governance/catalog.js — the permission catalog + compatibility seed.
|
|
2
|
+
//
|
|
3
|
+
// The single CODE source of truth for authority atoms (ADR 0151, BV1.R94). Every
|
|
4
|
+
// permission is a stable string key that maps to a REAL server capability (a route,
|
|
5
|
+
// a claim rule, a page) — a permission no route consumes does not exist. The
|
|
6
|
+
// resolver (R95), the requirePermission gate (R97), R93's role seed, and the Archon
|
|
7
|
+
// tab (R110/R111) all read from here; the enumerated catalog it transcribes is
|
|
8
|
+
// docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md, grounded in
|
|
9
|
+
// docs/governance/authority-sweep-2026-07-24.md.
|
|
10
|
+
//
|
|
11
|
+
// Two orthogonal axes per permission:
|
|
12
|
+
// • system — true = trust-boundary, NON-GRANTABLE. The governance API (R110)
|
|
13
|
+
// rejects adding it to a CUSTOM role and the tab renders it LOCKED;
|
|
14
|
+
// it is deploy/protected-path/rank-set/version-close/grade-bypass/
|
|
15
|
+
// cross-tier/principal power, enforced by the out-of-band substrate
|
|
16
|
+
// (ADR 0043) or reserved to Archon. The built-in `archon` rank-role
|
|
17
|
+
// legitimately HOLDS the archon-floored system perms — "non-grantable"
|
|
18
|
+
// constrains custom roles, not the seed.
|
|
19
|
+
// — false = operational, freely composable into a custom role.
|
|
20
|
+
// • floor — the rank that holds this permission TODAY via the compat seed
|
|
21
|
+
// ('xenos' < 'thetes' < 'metic' < 'archon'), or null for a machine
|
|
22
|
+
// PRINCIPAL permission held by NO rank-role (gated on exact
|
|
23
|
+
// system_role, never a human/role — ADR 0151 §3). floor drives the
|
|
24
|
+
// seed derivation below; it is seed DATA, not grantability (the
|
|
25
|
+
// `system` flag is the wall). Ownership-scoped + baseline own-work
|
|
26
|
+
// perms floor at 'xenos' (every authenticated builder holds them).
|
|
27
|
+
//
|
|
28
|
+
// PRINCIPALS — the second authority AXIS (BV1.R98, ADR 0151 §3). A builder's
|
|
29
|
+
// authority is the UNION of their ROLES; a machine principal's authority is its
|
|
30
|
+
// IDENTITY. Those are different axes and the model keeps them apart: a principal
|
|
31
|
+
// permission carries `principal: '<system_role>'` and `floor: null`, so it is in
|
|
32
|
+
// NO rank-role (not even archon's) and no custom role may hold it (`system:true`).
|
|
33
|
+
// The two are a PARTITION, asserted at load: floor:null ⟺ principal set. The only
|
|
34
|
+
// source of a principal permission is resolver.resolvePrincipalPermissions(), keyed
|
|
35
|
+
// on the exact `builders.system_role` value — a column with no API writer at all
|
|
36
|
+
// (provisioned out-of-band by migration 065/067, constrained by the DB CHECK
|
|
37
|
+
// allowlist + a UNIQUE index). Nothing in this module can create, grant, or assign
|
|
38
|
+
// one; the Archon tab reads `PRINCIPALS` to SHOW them and has no counterpart write.
|
|
39
|
+
//
|
|
40
|
+
// OWNERSHIP SCOPE — the third AXIS (BV1.R100, ADR 0151 §3). "May act on OWN X" is a
|
|
41
|
+
// PARAMETERIZED permission, not a global grant: box.manage.own ≠ the cross-owner
|
|
42
|
+
// box authority. Two optional fields make that axis explicit DATA (the way
|
|
43
|
+
// `principal` made the identity axis data) rather than something inferred from a
|
|
44
|
+
// key's spelling:
|
|
45
|
+
// • scope — 'own' = ownership-scoped. Holding it authorizes acting ONLY on a
|
|
46
|
+
// resource the holder owns. The permission alone is NEVER
|
|
47
|
+
// sufficient: a second, REQUEST-scoped fact (does this
|
|
48
|
+
// concrete row belong to the caller?) must also hold, and
|
|
49
|
+
// the resolver cannot know it — only the handler that
|
|
50
|
+
// loaded the row does. resolver.authorizeOwned() is the one
|
|
51
|
+
// entry point that answers an 'own' key, and it demands the
|
|
52
|
+
// owner alongside the key.
|
|
53
|
+
// 'any' = the CROSS-OWNER counterpart over the same resource: it
|
|
54
|
+
// reaches every owner's row, so no predicate applies.
|
|
55
|
+
// null = not ownership-scoped (the resource has no owner axis).
|
|
56
|
+
// • resource — WHAT is owned ('claim', 'box', 'memory', …). It is what LINKS an
|
|
57
|
+
// 'own' key to its 'any' counterpart, which a name cannot: the
|
|
58
|
+
// cross-owner box authority is spelled `box.fleet.manage`, while
|
|
59
|
+
// `task.claim.any` ends in `.any` and means "any task in the pool",
|
|
60
|
+
// not "any owner's resource". Inferring the axis from the suffix
|
|
61
|
+
// would get both of those wrong — hence data.
|
|
62
|
+
// Asserted at load: scope ⟺ resource; a `.own`-suffixed key MUST declare scope
|
|
63
|
+
// 'own' (a namespace can never outrun the data); a scoped key needs a real rank
|
|
64
|
+
// floor; every 'any' needs an 'own' in its family; and no family's cross-owner
|
|
65
|
+
// floor sits BELOW its own-only floor (wider authority is never cheaper). The
|
|
66
|
+
// converse of that last pairing rule is deliberately NOT asserted — some resources
|
|
67
|
+
// have no cross-owner form by design (nobody manages another builder's credential,
|
|
68
|
+
// Archon included), and minting a key no route consumes to satisfy a symmetry
|
|
69
|
+
// check would violate this module's first rule.
|
|
70
|
+
//
|
|
71
|
+
// The RANK_ROLE_SEED is DERIVED from floors (cumulative nested supersets), so the
|
|
72
|
+
// "zero day-one behavior change" parity (R103) is structural, not hand-maintained:
|
|
73
|
+
// role(rank) = { p : p.floor !== null and tier(p.floor) <= tier(rank) }, and the
|
|
74
|
+
// resolver's UNION over a builder's one rank-role reproduces requireRank's
|
|
75
|
+
// at-or-above threshold exactly.
|
|
76
|
+
|
|
77
|
+
'use strict';
|
|
78
|
+
|
|
79
|
+
// Low→high; index is the tier. Mirrors LIVE_RANK_LADDER (db-kernel.js) — the ONE
|
|
80
|
+
// ladder the resolver will collapse the ~13 duplicated tier maps onto (R108).
|
|
81
|
+
const RANK_ORDER = Object.freeze(['xenos', 'thetes', 'metic', 'archon']);
|
|
82
|
+
|
|
83
|
+
// key → capability. `guards` is a human note of the route(s)/surface the key maps to
|
|
84
|
+
// (the exhaustive 1:1 list lives in the authority sweep). Order: system first, then
|
|
85
|
+
// operational by floor (archon → metic → thetes/own/any), matching the ADR appendix.
|
|
86
|
+
const PERMISSIONS = [
|
|
87
|
+
// ── system: true — non-grantable (substrate- or Archon-enforced) ──────────────
|
|
88
|
+
{ key: 'builder.rank.set', system: true, floor: 'archon', guards: 'PATCH /builders/:id/rank — the single rank writer' },
|
|
89
|
+
{ key: 'version.close', system: true, floor: 'archon', guards: 'version close disposition (never delegated below Archon)' },
|
|
90
|
+
{ key: 'task.confirm.grade_bypass', system: true, floor: 'archon', guards: 'POST /tasks/:id/confirm — ratify past a failed grade (env-relaxable, ADR 0041)' },
|
|
91
|
+
{ key: 'dependency.cross_tier.manage', system: true, floor: 'archon', guards: 'POST|DELETE /dependencies — cross-tier structural edges (ADR 0086)' },
|
|
92
|
+
{ key: 'goal.scope.manage', system: true, floor: 'archon', guards: 'cross-tier goal-scope edges + protected-scope admission (ADR 0086)' },
|
|
93
|
+
{ key: 'governance.manage', system: true, floor: 'archon', guards: 'the governance tab write routes (roles/perms/assignments) — meta-escalation' },
|
|
94
|
+
{ key: 'gate_approval.decide', system: true, floor: 'archon', guards: 'POST /gate-approvals/:pr/approve — CI deploy-gate override' },
|
|
95
|
+
{ key: 'deploy.push_main', system: true, floor: 'archon', guards: 'git push origin main / the deploy — substrate-enforced (pre-push, main-audit)' },
|
|
96
|
+
{ key: 'path.protected.modify', system: true, floor: 'archon', guards: 'modifying a PROTECTED_GLOBS path below the Metic floor — substrate (ADR 0043)' },
|
|
97
|
+
{ key: 'infra.ssh', system: true, floor: 'archon', guards: 'ssh to the droplet / operator surface — substrate (credential placement)' },
|
|
98
|
+
{ key: 'principal.bfg', system: true, floor: null, principal: 'bfg', guards: '/memory/bfg/* — requireBfgPrincipal (exact system_role=bfg; NO rank-role holds it)' },
|
|
99
|
+
{ key: 'principal.discord_bot', system: true, floor: null, principal: 'discord_bot', guards: 'Discord bot routes — requireDiscordBot (exact system_role; dormant)' },
|
|
100
|
+
|
|
101
|
+
// ── operational — floor archon (admin ops; grantable to a custom role) ────────
|
|
102
|
+
{ key: 'builder.status.set', system: false, floor: 'archon', guards: 'PATCH /builders/:id/status — offboard / (de)activate' },
|
|
103
|
+
{ key: 'builder.budget.set', system: false, floor: 'archon', guards: 'PATCH /builders/:id/budget' },
|
|
104
|
+
{ key: 'criterion.unsatisfy', system: false, floor: 'archon', guards: 'POST /done-when/:id/unsatisfy' },
|
|
105
|
+
{ key: 'auth.session.revoke', system: false, floor: 'archon', guards: 'POST /auth/revoke, /auth/revoke-all' },
|
|
106
|
+
{ key: 'access_request.review', system: false, floor: 'archon', guards: 'GET/PATCH /access-requests/:id — admit a builder' },
|
|
107
|
+
{ key: 'security.report.adjudicate', system: false, floor: 'archon', guards: 'POST /security/reports/:id/{confirm,dispute,fix,link-fix}, /security/docs CRUD' },
|
|
108
|
+
{ key: 'provisioning.fleet.manage', system: false, floor: 'archon', guards: 'GET /provisioning/fleet[/cost-ledger], POST .../force-teardown' },
|
|
109
|
+
{ key: 'box.fleet.manage', system: false, floor: 'archon', scope: 'any', resource: 'box', guards: 'GET /boxes[/cost-ledger], POST /boxes/:b/close, PATCH .../block' },
|
|
110
|
+
{ key: 'autonomy.precheck', system: false, floor: 'archon', guards: 'GET /autonomy/precheck — unsupervised-runner gate' },
|
|
111
|
+
{ key: 'discord.channels.manage', system: false, floor: 'archon', guards: 'GET /discord/channels/plan, POST .../reconcile' },
|
|
112
|
+
{ key: 'project.curate', system: false, floor: 'archon', guards: 'platform-identity: POST /projects, /projects/invite, PATCH /projects/:id/featured, GET /scouting' },
|
|
113
|
+
{ key: 'override_request.decide', system: false, floor: 'archon', guards: 'GET /override-requests, POST .../:id/decide' },
|
|
114
|
+
{ key: 'claim.release.any', system: false, floor: 'archon', scope: 'any', resource: 'claim', guards: 'POST /claims/:id/release-on-behalf' },
|
|
115
|
+
{ key: 'idea.ratify', system: false, floor: 'archon', guards: 'POST /inbox/:id/ratify' },
|
|
116
|
+
{ key: 'task.newcomer_restock', system: false, floor: 'archon', guards: 'POST /tasks/newcomer-restock' },
|
|
117
|
+
{ key: 'task.peer_votes.tally', system: false, floor: 'archon', guards: 'POST /tasks/peer-votes/tally' },
|
|
118
|
+
{ key: 'goal.reorder', system: false, floor: 'archon', guards: 'POST /goals/reorder' },
|
|
119
|
+
{ key: 'builder.roster.read', system: false, floor: 'archon', guards: 'GET /builders/roster, /grades/by-builder, /builders/:id/active-claims' },
|
|
120
|
+
{ key: 'session.search', system: false, floor: 'archon', guards: 'GET /sessions/search' },
|
|
121
|
+
{ key: 'memory.read.any', system: false, floor: 'archon', scope: 'any', resource: 'memory', guards: 'GET /memory/builders/:id/file[s] — cross-builder memory read' },
|
|
122
|
+
{ key: 'audit_log.read', system: false, floor: 'archon', guards: 'GET /audit-log' },
|
|
123
|
+
|
|
124
|
+
// ── operational — floor metic (the working rank's review/triage/authoring) ────
|
|
125
|
+
{ key: 'idea.triage', system: false, floor: 'metic', guards: 'PATCH /inbox/:id' },
|
|
126
|
+
{ key: 'blocker.review', system: false, floor: 'metic', guards: 'POST /blockers/:id/{resolve,link}' },
|
|
127
|
+
{ key: 'task.create', system: false, floor: 'metic', guards: 'POST /tasks (incl. newcomer_friendly) — ADR 0090' },
|
|
128
|
+
{ key: 'task.edit', system: false, floor: 'metic', guards: 'PATCH /tasks/:id' },
|
|
129
|
+
{ key: 'task.promote', system: false, floor: 'metic', guards: 'POST /tasks/:id/promote' },
|
|
130
|
+
{ key: 'task.demote', system: false, floor: 'metic', guards: 'POST /tasks/:id/demote' },
|
|
131
|
+
{ key: 'task.abandon', system: false, floor: 'metic', guards: 'POST /tasks/:id/abandon — scope removal; undo = POST /tasks/:id/promote (ADR 0152, was archon)' },
|
|
132
|
+
{ key: 'task.vote', system: false, floor: 'metic', guards: 'POST /tasks/:id/vote' },
|
|
133
|
+
{ key: 'task.dependency.manage', system: false, floor: 'metic', guards: 'POST|PUT|DELETE /tasks/:id/dependencies (same-tier)' },
|
|
134
|
+
{ key: 'task.criterion.manage', system: false, floor: 'metic', guards: 'POST|DELETE /tasks/:id/criteria' },
|
|
135
|
+
{ key: 'criterion.satisfy', system: false, floor: 'metic', guards: 'POST /done-when/:id/satisfy' },
|
|
136
|
+
{ key: 'criterion.review', system: false, floor: 'metic', guards: 'GET /done-when/pending-review' },
|
|
137
|
+
// ADR 0154. Split out of criterion.review, where the route sat while it was still
|
|
138
|
+
// Archon-only (a slip against the sweep — ADR 0154 §5). floor 'metic' is real but
|
|
139
|
+
// NOT the whole gate: a non-Archon must also name a goal_id they own or manage, in
|
|
140
|
+
// an open goal on a planning/building version (authorizeCriterionCreate, in-handler).
|
|
141
|
+
{ key: 'criterion.create', system: false, floor: 'metic', guards: 'POST /versions/:id/done-when (goal owner|manager)' },
|
|
142
|
+
{ key: 'planning.run', system: false, floor: 'metic', guards: 'planning-session surface' },
|
|
143
|
+
{ key: 'priority.run', system: false, floor: 'metic', guards: 'priority-session surface' },
|
|
144
|
+
{ key: 'goal.create', system: false, floor: 'metic', guards: 'POST /goals (protected scope → Archon in-handler; ADR 0086)' },
|
|
145
|
+
{ key: 'goal.archive', system: false, floor: 'metic', guards: 'POST /goals/:id/{archive,reopen}' },
|
|
146
|
+
{ key: 'goal.scope.widen', system: false, floor: 'metic', guards: 'POST /goals/:id/scope (same-tier; cross-tier = goal.scope.manage, system)' },
|
|
147
|
+
{ key: 'module.enable', system: false, floor: 'metic', guards: 'POST /modules/:key/enable' },
|
|
148
|
+
{ key: 'module.submit', system: false, floor: 'metic', guards: 'POST /modules/:key/submit, GET /modules/submissions' },
|
|
149
|
+
{ key: 'security.report.file', system: false, floor: 'metic', guards: 'POST /security/reports, GET /security/adr' },
|
|
150
|
+
{ key: 'llm_cache.use', system: false, floor: 'metic', guards: 'POST /llm-cache/{lookup,store}' },
|
|
151
|
+
{ key: 'backup.manage', system: false, floor: 'metic', guards: 'GET /backup/status, POST /backup/trigger' },
|
|
152
|
+
{ key: 'source.clone.local', system: false, floor: 'metic', guards: 'Metic+ local clone / collaborator invite (ADR 0035)' },
|
|
153
|
+
{ key: 'box.source.full', system: false, floor: 'metic', guards: 'GET /box/source-access → full (vs starter); boxScopeForRank' },
|
|
154
|
+
|
|
155
|
+
// ── ownership-scoped + baseline own-work (floor thetes / xenos) ───────────────
|
|
156
|
+
{ key: 'task.claim.any', system: false, floor: 'thetes', guards: 'claim the general queue (per-task requires_rank floor still applies)' },
|
|
157
|
+
{ key: 'page.view.builder', system: false, floor: 'thetes', guards: 'authenticated hall pages (requireNonXenosPage / requireBuilderPage)' },
|
|
158
|
+
{ key: 'goal.manage.own', system: false, floor: 'metic', scope: 'own', resource: 'goal', guards: 'PATCH /goals/:id/members/:b, POST /goals/:id/transfer — owner/manager, in-handler (ADR 0106)' },
|
|
159
|
+
{ key: 'task.claim.newcomer', system: false, floor: 'xenos', guards: 'claim a newcomer_friendly task (xenosClaimAllowed; xenos = one active claim)' },
|
|
160
|
+
{ key: 'task.ship', system: false, floor: 'xenos', guards: '/builder-ship own claim' },
|
|
161
|
+
{ key: 'task.act.own', system: false, floor: 'xenos', scope: 'own', resource: 'claim', guards: 'act on OWN active claim (release, cost, notes) — ownership-scoped' },
|
|
162
|
+
{ key: 'box.manage.own', system: false, floor: 'xenos', scope: 'own', resource: 'box', guards: 'own dev-box lifecycle — ownership-scoped' },
|
|
163
|
+
{ key: 'memory.manage.own', system: false, floor: 'xenos', scope: 'own', resource: 'memory', guards: 'own builder memory — ownership-scoped' },
|
|
164
|
+
// No cross-owner counterpart, deliberately: nobody manages another builder's
|
|
165
|
+
// credential, Archon included. A `credential.manage.any` would be a key no route
|
|
166
|
+
// consumes — the catalog's first rule forbids minting one for symmetry.
|
|
167
|
+
{ key: 'credential.manage.own', system: false, floor: 'xenos', scope: 'own', resource: 'credential', guards: 'own API key / credential — ownership-scoped' },
|
|
168
|
+
// Own-scoped WITHOUT a `.own` suffix — the `me.` namespace is the ownership
|
|
169
|
+
// marker here. Exactly why scope is declared data: the suffix rule only runs the
|
|
170
|
+
// safe direction ('.own' ⇒ scope 'own'), never the converse.
|
|
171
|
+
{ key: 'me.wandering.set', system: false, floor: 'xenos', scope: 'own', resource: 'builder_prefs', guards: 'PATCH /me/wandering (clamped by maxLevelForRank) — ownership-scoped' },
|
|
172
|
+
{ key: 'cost.log', system: false, floor: 'xenos', guards: 'POST /costs' },
|
|
173
|
+
{ key: 'learning.create', system: false, floor: 'xenos', guards: 'POST /learnings' },
|
|
174
|
+
{ key: 'blocker.file', system: false, floor: 'xenos', guards: 'POST /blockers' },
|
|
175
|
+
{ key: 'idea.file', system: false, floor: 'xenos', guards: 'POST /inbox (own idea capture)' },
|
|
176
|
+
{ key: 'page.view.public', system: false, floor: 'xenos', guards: 'public read surfaces (incl. xenos)' },
|
|
177
|
+
{ key: 'page.view.governance', system: false, floor: 'archon', guards: 'the Archon governance page (requireArchonPage; write = governance.manage)' },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
// ── invariants asserted at module load (fail loud on an authoring typo) ─────────
|
|
181
|
+
const PRINCIPAL_KEY_PREFIX = 'principal.';
|
|
182
|
+
const OWN_KEY_SUFFIX = '.own';
|
|
183
|
+
const OWNERSHIP_SCOPES = Object.freeze(['own', 'any']);
|
|
184
|
+
const _byKey = new Map();
|
|
185
|
+
const _byPrincipal = new Map();
|
|
186
|
+
const _byResource = new Map();
|
|
187
|
+
for (const p of PERMISSIONS) {
|
|
188
|
+
if (typeof p.key !== 'string' || !p.key) throw new Error(`governance catalog: permission missing a key`);
|
|
189
|
+
if (typeof p.system !== 'boolean') throw new Error(`governance catalog: ${p.key} missing boolean 'system'`);
|
|
190
|
+
if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`governance catalog: ${p.key} has invalid floor "${p.floor}"`);
|
|
191
|
+
if (_byKey.has(p.key)) throw new Error(`governance catalog: duplicate key "${p.key}"`);
|
|
192
|
+
const principal = p.principal ?? null;
|
|
193
|
+
if (principal !== null && (typeof principal !== 'string' || !principal)) {
|
|
194
|
+
throw new Error(`governance catalog: ${p.key} has a non-string principal identity`);
|
|
195
|
+
}
|
|
196
|
+
// The two axes PARTITION the catalog: a permission is held by a rank-role (a real
|
|
197
|
+
// floor) XOR by a machine identity (floor null + a principal). Neither-nor would be
|
|
198
|
+
// a permission nothing can ever hold; both-and would let a role reach an identity.
|
|
199
|
+
if ((p.floor === null) !== (principal !== null)) {
|
|
200
|
+
throw new Error(`governance catalog: ${p.key} — floor:null and a principal identity must come together (roles and identities are separate axes)`);
|
|
201
|
+
}
|
|
202
|
+
if (p.key.startsWith(PRINCIPAL_KEY_PREFIX) !== (principal !== null)) {
|
|
203
|
+
throw new Error(`governance catalog: ${p.key} — a "${PRINCIPAL_KEY_PREFIX}*" key and a principal identity must come together`);
|
|
204
|
+
}
|
|
205
|
+
if (principal !== null) {
|
|
206
|
+
if (!p.system) throw new Error(`governance catalog: ${p.key} is a principal permission and must be system:true (never grantable to a role)`);
|
|
207
|
+
if (_byPrincipal.has(principal)) throw new Error(`governance catalog: duplicate principal identity "${principal}"`);
|
|
208
|
+
}
|
|
209
|
+
// ── the OWNERSHIP axis (R100) ─────────────────────────────────────────────
|
|
210
|
+
const scope = p.scope ?? null;
|
|
211
|
+
const resource = p.resource ?? null;
|
|
212
|
+
if (scope !== null && !OWNERSHIP_SCOPES.includes(scope)) {
|
|
213
|
+
throw new Error(`governance catalog: ${p.key} has invalid scope "${scope}" (expected 'own', 'any', or absent)`);
|
|
214
|
+
}
|
|
215
|
+
// An ownership SCOPE and the RESOURCE it is scoped over are one fact spelled in
|
|
216
|
+
// two fields; either alone is meaningless ('own' of what? / a resource nothing
|
|
217
|
+
// is scoped over). Same partition posture as floor ⟺ principal above.
|
|
218
|
+
if ((scope === null) !== (resource === null)) {
|
|
219
|
+
throw new Error(`governance catalog: ${p.key} — scope and resource must come together (an ownership scope is always scoped over a named resource)`);
|
|
220
|
+
}
|
|
221
|
+
if (resource !== null && (typeof resource !== 'string' || !resource)) {
|
|
222
|
+
throw new Error(`governance catalog: ${p.key} has a non-string resource family`);
|
|
223
|
+
}
|
|
224
|
+
// The namespace can never outrun the data: a key that CALLS itself own-scoped
|
|
225
|
+
// must BE declared own-scoped, or a gate reading the declared axis would treat a
|
|
226
|
+
// parameterized permission as an unconditional grant. Deliberately one-way — the
|
|
227
|
+
// converse would be false (me.wandering.set is own-scoped without the suffix).
|
|
228
|
+
if (p.key.endsWith(OWN_KEY_SUFFIX) && scope !== 'own') {
|
|
229
|
+
throw new Error(`governance catalog: ${p.key} ends in "${OWN_KEY_SUFFIX}" but does not declare scope:'own' — an ownership-scoped key must say so in data`);
|
|
230
|
+
}
|
|
231
|
+
// An ownership scope NARROWS a role-held permission; a machine identity has no
|
|
232
|
+
// owner axis, so the two never compose.
|
|
233
|
+
if (scope !== null && p.floor === null) {
|
|
234
|
+
throw new Error(`governance catalog: ${p.key} is ownership-scoped but has no rank floor — the ownership axis narrows a ROLE permission, never an identity one`);
|
|
235
|
+
}
|
|
236
|
+
const frozen = Object.freeze({ ...p, principal, scope, resource });
|
|
237
|
+
_byKey.set(p.key, frozen);
|
|
238
|
+
if (principal !== null) _byPrincipal.set(principal, frozen);
|
|
239
|
+
if (resource !== null) {
|
|
240
|
+
if (!_byResource.has(resource)) _byResource.set(resource, { own: [], any: [] });
|
|
241
|
+
_byResource.get(resource)[scope].push(p.key);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
Object.freeze(PERMISSIONS);
|
|
245
|
+
|
|
246
|
+
// ── the ownership axis's cross-row invariants ──────────────────────────────────
|
|
247
|
+
// A 'any' row is BY DEFINITION the cross-owner counterpart of an 'own' one, so a
|
|
248
|
+
// family with no 'own' side is a mis-declaration. The converse is NOT required:
|
|
249
|
+
// some resources have no cross-owner form at all (credential), and minting a key
|
|
250
|
+
// no route consumes purely for symmetry would break this module's first rule.
|
|
251
|
+
const floorTier = (rank) => RANK_ORDER.indexOf(rank);
|
|
252
|
+
for (const [resource, fam] of _byResource) {
|
|
253
|
+
if (fam.any.length > 0 && fam.own.length === 0) {
|
|
254
|
+
throw new Error(`governance catalog: resource "${resource}" declares a cross-owner permission (${fam.any.join(', ')}) with no ownership-scoped counterpart`);
|
|
255
|
+
}
|
|
256
|
+
// Reaching EVERY owner's row must never be cheaper than reaching your own. If it
|
|
257
|
+
// were, a rank could manage the whole fleet but not its own box — the model
|
|
258
|
+
// inverted, and the governance tab would render the wider grant as the safer one.
|
|
259
|
+
for (const anyKey of fam.any) {
|
|
260
|
+
for (const ownKey of fam.own) {
|
|
261
|
+
if (floorTier(_byKey.get(anyKey).floor) < floorTier(_byKey.get(ownKey).floor)) {
|
|
262
|
+
throw new Error(`governance catalog: ${anyKey} (floor ${_byKey.get(anyKey).floor}) sits BELOW ${ownKey} (floor ${_byKey.get(ownKey).floor}) — cross-owner authority must never be cheaper than own-only`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// resource → { own: [keys], any: [keys] }. The pairing data a gate, a test, or the
|
|
269
|
+
// Archon tab reads to relate "act on own X" to "act on any X"; DERIVED from the
|
|
270
|
+
// rows above so it cannot drift from them.
|
|
271
|
+
const RESOURCE_FAMILIES = Object.freeze(Object.fromEntries(
|
|
272
|
+
[..._byResource.entries()].map(([resource, fam]) => [resource, Object.freeze({
|
|
273
|
+
own: Object.freeze([...fam.own]),
|
|
274
|
+
any: Object.freeze([...fam.any]),
|
|
275
|
+
})]),
|
|
276
|
+
));
|
|
277
|
+
|
|
278
|
+
// The principal registry — the read-only projection the Archon tab renders (R110/
|
|
279
|
+
// R111) and the resolver's identity axis reads. Derived from the catalog so it
|
|
280
|
+
// cannot drift, and deliberately WITHOUT a counterpart writer: a principal is
|
|
281
|
+
// provisioned out-of-band in `builders.system_role` (migration-only, DB CHECK
|
|
282
|
+
// allowlist), so `grantable: false` here is a statement of fact, not a policy flag.
|
|
283
|
+
const PRINCIPALS = Object.freeze(
|
|
284
|
+
[..._byPrincipal.values()].map((p) => Object.freeze({
|
|
285
|
+
system_role: p.principal,
|
|
286
|
+
permission: p.key,
|
|
287
|
+
guards: p.guards,
|
|
288
|
+
grantable: false,
|
|
289
|
+
provisioned: 'out-of-band: builders.system_role (no API writes it)',
|
|
290
|
+
})),
|
|
291
|
+
);
|
|
292
|
+
const _principalByRole = new Map(PRINCIPALS.map((p) => [p.system_role, p]));
|
|
293
|
+
|
|
294
|
+
function permissionKeys() {
|
|
295
|
+
return PERMISSIONS.map((p) => p.key);
|
|
296
|
+
}
|
|
297
|
+
function byKey(key) {
|
|
298
|
+
return _byKey.get(key) || null;
|
|
299
|
+
}
|
|
300
|
+
function isSystem(key) {
|
|
301
|
+
const p = _byKey.get(key);
|
|
302
|
+
return p ? p.system : null; // null = unknown key (caller fails closed)
|
|
303
|
+
}
|
|
304
|
+
// Grantable to a CUSTOM role = exists and not system. Unknown key → false (fail-closed).
|
|
305
|
+
function isGrantable(key) {
|
|
306
|
+
const p = _byKey.get(key);
|
|
307
|
+
return !!p && !p.system;
|
|
308
|
+
}
|
|
309
|
+
function systemKeys() {
|
|
310
|
+
return PERMISSIONS.filter((p) => p.system).map((p) => p.key);
|
|
311
|
+
}
|
|
312
|
+
function grantableKeys() {
|
|
313
|
+
return PERMISSIONS.filter((p) => !p.system).map((p) => p.key);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── the principal axis (identity, not roles) ────────────────────────────────────
|
|
317
|
+
// Reads only. Unknown/blank input fails closed to null/false — the same posture the
|
|
318
|
+
// rest of the catalog takes, and the one the identity gates depend on.
|
|
319
|
+
function principals() {
|
|
320
|
+
return PRINCIPALS;
|
|
321
|
+
}
|
|
322
|
+
function principalRoles() {
|
|
323
|
+
return PRINCIPALS.map((p) => p.system_role);
|
|
324
|
+
}
|
|
325
|
+
function principalKeys() {
|
|
326
|
+
return PRINCIPALS.map((p) => p.permission);
|
|
327
|
+
}
|
|
328
|
+
// Is this key held by a machine identity rather than by any role? Unknown → false.
|
|
329
|
+
function isPrincipalKey(key) {
|
|
330
|
+
const p = _byKey.get(key);
|
|
331
|
+
return !!p && p.principal !== null;
|
|
332
|
+
}
|
|
333
|
+
// EXACT match on the system_role string — never a prefix, case-fold, or coercion.
|
|
334
|
+
// That exactness is the security property: it is what makes an Archon token (whose
|
|
335
|
+
// system_role is NULL) unable to resolve a principal permission.
|
|
336
|
+
function principalByRole(systemRole) {
|
|
337
|
+
if (typeof systemRole !== 'string' || !systemRole) return null;
|
|
338
|
+
return _principalByRole.get(systemRole) || null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── the ownership axis (scope, not identity) ───────────────────────────────────
|
|
342
|
+
// Reads only, and fail-closed on an unknown key in the direction that matters:
|
|
343
|
+
// isOwnershipScoped() answers FALSE for a key it does not know, but the resolver
|
|
344
|
+
// never reaches a gate decision off that alone — resolver.authorizeOwned() looks
|
|
345
|
+
// the row up itself and denies an unknown key outright.
|
|
346
|
+
function scopeOf(key) {
|
|
347
|
+
const p = _byKey.get(key);
|
|
348
|
+
return p ? p.scope : null;
|
|
349
|
+
}
|
|
350
|
+
function resourceOf(key) {
|
|
351
|
+
const p = _byKey.get(key);
|
|
352
|
+
return p ? p.resource : null;
|
|
353
|
+
}
|
|
354
|
+
// Does holding this key require an ownership predicate against the concrete row?
|
|
355
|
+
// This is the question a gate must ask BEFORE treating a held permission as an
|
|
356
|
+
// admit — an ownership-scoped key is never sufficient on its own.
|
|
357
|
+
function isOwnershipScoped(key) {
|
|
358
|
+
return scopeOf(key) === 'own';
|
|
359
|
+
}
|
|
360
|
+
// The cross-owner counterpart: reaches every owner's row, so no predicate applies.
|
|
361
|
+
function isCrossOwner(key) {
|
|
362
|
+
return scopeOf(key) === 'any';
|
|
363
|
+
}
|
|
364
|
+
function ownershipScopedKeys() {
|
|
365
|
+
return PERMISSIONS.filter((p) => p.scope === 'own').map((p) => p.key);
|
|
366
|
+
}
|
|
367
|
+
function crossOwnerKeys() {
|
|
368
|
+
return PERMISSIONS.filter((p) => p.scope === 'any').map((p) => p.key);
|
|
369
|
+
}
|
|
370
|
+
// The cross-owner keys over the SAME resource as `key` — what a route offers as
|
|
371
|
+
// the escalation path ("your own claim, or claim.release.any"). Empty when the
|
|
372
|
+
// resource has no cross-owner form by design.
|
|
373
|
+
const NO_KEYS = Object.freeze([]);
|
|
374
|
+
function crossOwnerKeysFor(key) {
|
|
375
|
+
const resource = resourceOf(key);
|
|
376
|
+
const fam = resource === null ? null : RESOURCE_FAMILIES[resource];
|
|
377
|
+
return fam ? fam.any : NO_KEYS;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// The compat seed, DERIVED from floors (cumulative supersets). A rank-role holds
|
|
381
|
+
// every permission whose floor is a real rank at-or-below it; principals (floor
|
|
382
|
+
// null) are held by NO rank-role. This is the "zero day-one change" guarantee —
|
|
383
|
+
// role(rank) reproduces requireRank(rank)'s at-or-above threshold exactly (R103).
|
|
384
|
+
function seedForRank(rank) {
|
|
385
|
+
const tier = RANK_ORDER.indexOf(rank);
|
|
386
|
+
if (tier < 0) return []; // unknown/reserved rank → no role → no permissions (fail-closed; ADR 0151)
|
|
387
|
+
return PERMISSIONS.filter((p) => p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier).map((p) => p.key);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const RANK_ROLE_SEED = Object.freeze(
|
|
391
|
+
Object.fromEntries(RANK_ORDER.map((r) => [r, Object.freeze(seedForRank(r))])),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
module.exports = {
|
|
395
|
+
RANK_ORDER,
|
|
396
|
+
PERMISSIONS,
|
|
397
|
+
permissionKeys,
|
|
398
|
+
byKey,
|
|
399
|
+
isSystem,
|
|
400
|
+
isGrantable,
|
|
401
|
+
systemKeys,
|
|
402
|
+
grantableKeys,
|
|
403
|
+
seedForRank,
|
|
404
|
+
RANK_ROLE_SEED,
|
|
405
|
+
PRINCIPALS,
|
|
406
|
+
principals,
|
|
407
|
+
principalRoles,
|
|
408
|
+
principalKeys,
|
|
409
|
+
isPrincipalKey,
|
|
410
|
+
principalByRole,
|
|
411
|
+
RESOURCE_FAMILIES,
|
|
412
|
+
scopeOf,
|
|
413
|
+
resourceOf,
|
|
414
|
+
isOwnershipScoped,
|
|
415
|
+
isCrossOwner,
|
|
416
|
+
ownershipScopedKeys,
|
|
417
|
+
crossOwnerKeys,
|
|
418
|
+
crossOwnerKeysFor,
|
|
419
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/governance/config.js — the governance config contract (ADR 0151, BV1.R96).
|
|
4
|
+
//
|
|
5
|
+
// Role TEMPLATES and their display labels are per-instance CONFIG; live grants and
|
|
6
|
+
// assignments are DB rows. This file resolves the config half, mirroring
|
|
7
|
+
// src/branding.js precedence exactly:
|
|
8
|
+
//
|
|
9
|
+
// env override -> instance config (config/governance.json) -> neutral
|
|
10
|
+
// starter (config/governance.neutral.json)
|
|
11
|
+
//
|
|
12
|
+
// A fresh instance with NO config resolves to the PLAIN neutral role set — never to
|
|
13
|
+
// a host's labels (fail-to-vanilla, the same posture branding takes). R102 proves
|
|
14
|
+
// that boot.
|
|
15
|
+
//
|
|
16
|
+
// WHAT LIVES HERE vs WHAT LIVES IN THE DB — get this wrong and config becomes a
|
|
17
|
+
// second source of authority truth, which is exactly what ADR 0016 forbids:
|
|
18
|
+
// • config — TEMPLATES/DEFAULTS: a role's display label, its description, and the
|
|
19
|
+
// permission list a starting CUSTOM role template proposes. Shapes only.
|
|
20
|
+
// • DB — LIVE STATE: governance_roles / governance_role_permissions /
|
|
21
|
+
// governance_builder_roles (R93). WHO HOLDS WHAT is a row, resolved server-side
|
|
22
|
+
// per request by resolver.js. Nothing in this file grants anything to anybody.
|
|
23
|
+
//
|
|
24
|
+
// Three fail-closed rules, in the same spirit as resolver.js's drop-unknown-key rule:
|
|
25
|
+
// 1. A template naming a `system: true` catalog key is STRIPPED. An instance's
|
|
26
|
+
// config file is editable by anyone who can write the checkout, so it must never
|
|
27
|
+
// be able to hand out deploy / rank-set / protected-path power. catalog
|
|
28
|
+
// .isGrantable() is that wall — the same one the governance API and the Archon
|
|
29
|
+
// tab use, so this cannot drift from the catalog.
|
|
30
|
+
// 2. A template naming a key the catalog does not define is DROPPED (the catalog is
|
|
31
|
+
// the source of truth for what a permission IS — resolver.js, same rule).
|
|
32
|
+
// 3. A `permissions` list on one of the four SEEDED rank-roles is DROPPED wholesale.
|
|
33
|
+
// Their grants are DERIVED from catalog floors and materialised by
|
|
34
|
+
// governance_002; config may relabel a seeded role, never re-grant it.
|
|
35
|
+
//
|
|
36
|
+
// Stripping rather than throwing matches resolver.js: a typo in a config file must
|
|
37
|
+
// fail CLOSED, not brick the boot.
|
|
38
|
+
//
|
|
39
|
+
// SLICE BOUNDARY: this file is the config CONTRACT, and deliberately has no production
|
|
40
|
+
// caller yet — R96 is the "what shape is a role template, and what can config never
|
|
41
|
+
// do" decision, landed before the surfaces that read it. R110/R111 (the Archon
|
|
42
|
+
// governance tab) is the first intended consumer, and R102 (the vanilla-boot proof)
|
|
43
|
+
// asserts against the neutral starter. Wiring a speculative caller now would guess at
|
|
44
|
+
// their needs; the tests pin the contract until they land.
|
|
45
|
+
|
|
46
|
+
const fs = require('node:fs');
|
|
47
|
+
const path = require('node:path');
|
|
48
|
+
const api = require('../../src/module-api');
|
|
49
|
+
const catalog = require('./catalog');
|
|
50
|
+
|
|
51
|
+
// Both the neutral starter and the instance pack sit at the repo root's config/
|
|
52
|
+
// today (src/instance-config.js resolveCoreRoot() === resolveInstanceRoot() === this
|
|
53
|
+
// checkout), so this is the established module idiom for a repo-root read
|
|
54
|
+
// (modules/lifecycle, modules/onboarding, modules/discord). When ADR 0108's root
|
|
55
|
+
// split lands, the INSTANCE path routes through resolveInstanceRoot() — the module
|
|
56
|
+
// doorway does not re-export the root resolvers yet.
|
|
57
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
58
|
+
const NEUTRAL_PATH = path.join(ROOT, 'config', 'governance.neutral.json');
|
|
59
|
+
const INSTANCE_PATH = path.join(ROOT, 'config', 'governance.json');
|
|
60
|
+
|
|
61
|
+
const SEEDED_RANK_ROLES = new Set(catalog.RANK_ORDER);
|
|
62
|
+
|
|
63
|
+
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
64
|
+
|
|
65
|
+
// Instance roles merge over neutral PER ROLE KEY and per field, so an instance can
|
|
66
|
+
// relabel `metic` without restating the other three. The shape is exactly two levels
|
|
67
|
+
// deep, so this is a targeted merge rather than branding's generic deepMerge.
|
|
68
|
+
function mergeRoles(neutral, instance) {
|
|
69
|
+
const out = {};
|
|
70
|
+
const keys = new Set([...Object.keys(neutral || {}), ...Object.keys(instance || {})]);
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
const n = isObj((neutral || {})[key]) ? neutral[key] : {};
|
|
73
|
+
const i = isObj((instance || {})[key]) ? instance[key] : {};
|
|
74
|
+
out[key] = { ...n, ...i };
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The env layer overrides LABELS ONLY, deliberately: an env var must never be able to
|
|
80
|
+
// widen authority, so there is no env knob that adds a permission. It exists for the
|
|
81
|
+
// co-tenant case branding's AUTH_IDP override exists for — several hosted instances
|
|
82
|
+
// sharing the neutral pack, differentiated only by env, with no config file of their
|
|
83
|
+
// own. Format: "<PREFIX>_GOVERNANCE_ROLE_LABELS=metic=Trusted,archon=Owner". It can
|
|
84
|
+
// only RELABEL a role the merged config already defines — a typo names no role and is
|
|
85
|
+
// inert, rather than quietly growing the role set from an env string.
|
|
86
|
+
function applyLabelOverrides(roles, env) {
|
|
87
|
+
const raw = api.resolveEnv('GOVERNANCE_ROLE_LABELS', { env });
|
|
88
|
+
if (!raw) return roles;
|
|
89
|
+
const out = { ...roles };
|
|
90
|
+
for (const pair of String(raw).split(',')) {
|
|
91
|
+
const eq = pair.indexOf('=');
|
|
92
|
+
if (eq <= 0) continue;
|
|
93
|
+
const key = pair.slice(0, eq).trim();
|
|
94
|
+
const label = pair.slice(eq + 1).trim();
|
|
95
|
+
if (!label || !isObj(out[key])) continue;
|
|
96
|
+
out[key] = { ...out[key], label };
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Filter a template's proposed permissions against the code catalog. isGrantable() is
|
|
102
|
+
// false for BOTH an unknown key and a `system: true` key, so rules 1 and 2 above are
|
|
103
|
+
// one call — and they can never drift from the catalog. Returns a frozen sorted array.
|
|
104
|
+
function grantableOnly(keys) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const k of Array.isArray(keys) ? keys : []) {
|
|
107
|
+
if (typeof k === 'string' && catalog.isGrantable(k)) seen.add(k);
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze([...seen].sort());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sanitizeRole(key, raw) {
|
|
113
|
+
const seeded = SEEDED_RANK_ROLES.has(key);
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
key,
|
|
116
|
+
seeded,
|
|
117
|
+
label: typeof raw.label === 'string' && raw.label ? raw.label : key,
|
|
118
|
+
description: typeof raw.description === 'string' ? raw.description : '',
|
|
119
|
+
// Rule 3: a seeded rank-role's grants come from catalog floors via governance_002.
|
|
120
|
+
permissions: seeded ? Object.freeze([]) : grantableOnly(raw.permissions),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Pure resolver — all inputs injected, so tests drive it with zero I/O (the
|
|
125
|
+
// resolveBranding pattern).
|
|
126
|
+
function resolveGovernanceConfig({ neutral, instance, env = process.env } = {}) {
|
|
127
|
+
const merged = applyLabelOverrides(mergeRoles((neutral || {}).roles, (instance || {}).roles), env);
|
|
128
|
+
const roles = {};
|
|
129
|
+
for (const key of Object.keys(merged).sort()) {
|
|
130
|
+
if (!isObj(merged[key])) continue; // a malformed entry is not a role, and grants nothing
|
|
131
|
+
roles[key] = sanitizeRole(key, merged[key]);
|
|
132
|
+
}
|
|
133
|
+
// The four rank-roles ALWAYS exist (governance_002 seeds them unconditionally), so a
|
|
134
|
+
// config that omits or mangles one falls back to the canonical rank name rather than
|
|
135
|
+
// dropping the role out of the display set.
|
|
136
|
+
for (const key of catalog.RANK_ORDER) {
|
|
137
|
+
if (!roles[key]) roles[key] = sanitizeRole(key, {});
|
|
138
|
+
}
|
|
139
|
+
return Object.freeze({ roles: Object.freeze(roles) });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// READ, never require(): a bare require('../../config/X.json') of a boundary-excluded
|
|
143
|
+
// config crashes a vanilla boot with MODULE_NOT_FOUND (fitness Check 16 / task 1824).
|
|
144
|
+
function readJson(p) {
|
|
145
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
146
|
+
catch (e) { if (e.code === 'ENOENT') return null; throw e; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Disk loader with the documented precedence. BOTH files are optional: the instance
|
|
150
|
+
// pack because a vanilla instance ships only the neutral starter, and the neutral
|
|
151
|
+
// starter because the four rank-roles still resolve to their canonical names without
|
|
152
|
+
// it. Unlike branding — where a missing identity is fatal — no value here is required,
|
|
153
|
+
// so the loader fails to sane defaults rather than throwing.
|
|
154
|
+
//
|
|
155
|
+
// CALL THIS AT BOOT, NOT PER REQUEST. The two reads are synchronous, which is correct
|
|
156
|
+
// for a startup read of a committed file and wrong inside a request handler. If a
|
|
157
|
+
// consumer needs it on a hot path, resolve once at boot and pass the frozen result —
|
|
158
|
+
// do NOT add a cache here (this file sits next to resolver.js, whose no-cache rule is
|
|
159
|
+
// absolute; keeping config cache-free too means nobody has to check which is which).
|
|
160
|
+
function loadGovernanceConfig({ neutralPath = NEUTRAL_PATH, instancePath = INSTANCE_PATH, env = process.env } = {}) {
|
|
161
|
+
return resolveGovernanceConfig({
|
|
162
|
+
neutral: readJson(neutralPath) || {},
|
|
163
|
+
instance: readJson(instancePath) || {},
|
|
164
|
+
env,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = {
|
|
169
|
+
resolveGovernanceConfig,
|
|
170
|
+
loadGovernanceConfig,
|
|
171
|
+
grantableOnly,
|
|
172
|
+
NEUTRAL_PATH,
|
|
173
|
+
INSTANCE_PATH,
|
|
174
|
+
};
|