@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,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/governance/rank-roles.js — the PURE rank→role rotation decision
|
|
4
|
+
// (ADR 0151, BV1.R95b).
|
|
5
|
+
//
|
|
6
|
+
// A builder holds exactly ONE seeded rank-role. governance_002 seeds four of them
|
|
7
|
+
// (is_system=true, seeded_rank set) and backfills each builder the one matching
|
|
8
|
+
// their rank — but that backfill is run-once, so nothing kept it true afterwards.
|
|
9
|
+
// This is the decision the writer (db.syncRankRole) executes to keep it true:
|
|
10
|
+
// given a builder's CURRENT assignments and a rank, which seeded role to ADD and
|
|
11
|
+
// which to REMOVE.
|
|
12
|
+
//
|
|
13
|
+
// THE REMOVAL IS THE SECURITY HALF. An additive-only sync would leave a demoted
|
|
14
|
+
// Archon still holding the `archon` role, so resolveBuilderPermissions() would keep
|
|
15
|
+
// returning Archon permissions. That is the exact privilege retention ADR 0016's
|
|
16
|
+
// "a demotion takes effect on the builder's very next request" promise forbids: the
|
|
17
|
+
// resolver is correctly per-request and uncached, but it reads an ASSIGNMENT, and a
|
|
18
|
+
// stale assignment breaks the freshness guarantee one layer beneath the resolver.
|
|
19
|
+
//
|
|
20
|
+
// CUSTOM ROLES ARE NEVER TOUCHED. An Archon-assigned role (is_system=false, the
|
|
21
|
+
// R110 tab's output) must survive a rank change — only rows that are BOTH
|
|
22
|
+
// is_system AND carry a seeded_rank rotate. Keying the removal on "every role this
|
|
23
|
+
// builder holds" would silently revoke delegated authority on every promotion.
|
|
24
|
+
//
|
|
25
|
+
// A rank with NO seeded role — one of the twelve reserved ranks migration 023's
|
|
26
|
+
// CHECK still admits — yields target=null: every seeded role is removed and none is
|
|
27
|
+
// added, so the builder resolves to ZERO permissions. That is governance_002's
|
|
28
|
+
// deliberate fail-closed divergence (ADR 0151), preserved rather than re-derived —
|
|
29
|
+
// never invent a role for a rank the seed does not define.
|
|
30
|
+
//
|
|
31
|
+
// Pure: no DB, no I/O, no module state. The writer reads the two row sets and
|
|
32
|
+
// executes this verdict, so the unit tests exercise production logic directly.
|
|
33
|
+
|
|
34
|
+
// A row (an assignment joined to governance_roles, or a role row) is a SEEDED
|
|
35
|
+
// rank-role only when it is both a system role and names the rank it reproduces.
|
|
36
|
+
// Both halves are required: is_system alone would admit a future non-rank system
|
|
37
|
+
// role, and seeded_rank alone would trust a column a custom role could carry.
|
|
38
|
+
function isSeededRankRole(row) {
|
|
39
|
+
return !!row
|
|
40
|
+
&& row.is_system === true
|
|
41
|
+
&& typeof row.seeded_rank === 'string'
|
|
42
|
+
&& row.seeded_rank.length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// planRankRoleSync({ rank, currentRoles, seededRoles }) → { target, add, remove, next }
|
|
46
|
+
//
|
|
47
|
+
// rank the builder's rank AFTER the write being synced.
|
|
48
|
+
// currentRoles the builder's assignments, each { role_key, is_system, seeded_rank }.
|
|
49
|
+
// seededRoles the seeded rank-roles the DB defines, each { role_key, seeded_rank,
|
|
50
|
+
// is_system }. Passed in rather than hardcoded so the four role keys
|
|
51
|
+
// stay DB truth — this file never re-declares the seed.
|
|
52
|
+
//
|
|
53
|
+
// target the seeded role_key the builder should end up with, or null when the
|
|
54
|
+
// rank has no seeded role (fail-closed divergence, above).
|
|
55
|
+
// add [] or [target] — empty when it is already held (idempotence).
|
|
56
|
+
// remove every OTHER seeded rank-role currently held. Custom roles never appear.
|
|
57
|
+
// next the resulting role_key set, sorted — "what the role set becomes".
|
|
58
|
+
function planRankRoleSync({ rank, currentRoles = [], seededRoles = [] } = {}) {
|
|
59
|
+
const match = seededRoles.find((r) => isSeededRankRole(r) && r.seeded_rank === rank);
|
|
60
|
+
const target = match ? match.role_key : null;
|
|
61
|
+
|
|
62
|
+
const held = currentRoles.filter((r) => r && typeof r.role_key === 'string');
|
|
63
|
+
const remove = held
|
|
64
|
+
.filter((r) => isSeededRankRole(r) && r.role_key !== target)
|
|
65
|
+
.map((r) => r.role_key);
|
|
66
|
+
const add = target && !held.some((r) => r.role_key === target) ? [target] : [];
|
|
67
|
+
|
|
68
|
+
const removed = new Set(remove);
|
|
69
|
+
const next = held
|
|
70
|
+
.map((r) => r.role_key)
|
|
71
|
+
.filter((key) => !removed.has(key))
|
|
72
|
+
.concat(add)
|
|
73
|
+
.sort();
|
|
74
|
+
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
target,
|
|
77
|
+
add: Object.freeze(add),
|
|
78
|
+
remove: Object.freeze(remove),
|
|
79
|
+
next: Object.freeze(next),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { planRankRoleSync, isSeededRankRole };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/governance/resolver.js — the permission resolver (ADR 0151, BV1.R95).
|
|
4
|
+
//
|
|
5
|
+
// Turns a builder's ROLES into their effective PERMISSION SET: the union of every
|
|
6
|
+
// permission granted to every role they hold. This is the heart of the
|
|
7
|
+
// permissions-as-atom model — `requirePermission()` (R97) and every migrated gate
|
|
8
|
+
// (R104-R107) resolve authority through here.
|
|
9
|
+
//
|
|
10
|
+
// TWO AXES, two entry points (R98, ADR 0151 §3). Roles are one axis; a machine
|
|
11
|
+
// PRINCIPAL's capability is the other, resolved from its exact `system_role` and
|
|
12
|
+
// never from a role. They are kept apart on purpose — see resolvePrincipalPermissions
|
|
13
|
+
// below, and the principal keys effectivePermissions drops.
|
|
14
|
+
//
|
|
15
|
+
// It preserves ADR 0016 exactly:
|
|
16
|
+
// • server-side — the answer is computed on the server from DB rows, never from
|
|
17
|
+
// a token claim or anything the client can influence;
|
|
18
|
+
// • per request — resolveBuilderPermissions() is called on the request that
|
|
19
|
+
// needs it, so a revoked role takes effect on the builder's very next request;
|
|
20
|
+
// • UNCACHED — there is no memo, no TTL, no process-local map. Caching authority
|
|
21
|
+
// is the one optimisation this file must never take (a cached grant survives
|
|
22
|
+
// its revocation, which is the failure mode the trust boundary exists to
|
|
23
|
+
// prevent). If a future profile shows the join is hot, the answer is a DB
|
|
24
|
+
// index, not a cache.
|
|
25
|
+
//
|
|
26
|
+
// Fail-closed throughout: a builder with no roles resolves to the EMPTY set, and a
|
|
27
|
+
// grant naming a key the code catalog doesn't define is DROPPED (see
|
|
28
|
+
// effectivePermissions below) rather than admitted on trust.
|
|
29
|
+
|
|
30
|
+
const catalog = require('./catalog');
|
|
31
|
+
const db = require('./db');
|
|
32
|
+
const sessionScopes = require('./session-scopes');
|
|
33
|
+
|
|
34
|
+
// Pure core of the ROLE axis, split out so it is unit-testable without a database.
|
|
35
|
+
//
|
|
36
|
+
// Filters raw grant rows against the code catalog: the catalog is the source of
|
|
37
|
+
// truth for what a permission IS (every key maps to a real route — modules/
|
|
38
|
+
// governance/CLAUDE.md), so a DB row naming an unknown key is stale or hand-
|
|
39
|
+
// inserted and grants NOTHING. Returns a frozen, sorted, de-duplicated array.
|
|
40
|
+
//
|
|
41
|
+
// It also DROPS every principal key (R98). A machine identity's capability is the
|
|
42
|
+
// other axis — it comes from `builders.system_role`, never from a role — so a grant
|
|
43
|
+
// row naming `principal.*` confers nothing here no matter how it got written (a
|
|
44
|
+
// hand-inserted row, a future bug in the governance CRUD's system-permission
|
|
45
|
+
// rejection). That makes "a role cannot confer a principal permission" structural
|
|
46
|
+
// rather than merely policed at the write side.
|
|
47
|
+
function effectivePermissions(rawKeys) {
|
|
48
|
+
const known = new Set();
|
|
49
|
+
for (const key of rawKeys || []) {
|
|
50
|
+
if (!catalog.byKey(key)) continue;
|
|
51
|
+
if (catalog.isPrincipalKey(key)) continue;
|
|
52
|
+
known.add(key);
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze([...known].sort());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The PRINCIPAL axis (R98, ADR 0151 §3) — deliberately a SEPARATE entry point, not
|
|
58
|
+
// a branch inside the role resolve. A builder's authority is the union of their
|
|
59
|
+
// roles; a machine principal's authority is its identity, and collapsing the two
|
|
60
|
+
// into one lookup is exactly the mistake that would let a rank reach a principal
|
|
61
|
+
// capability.
|
|
62
|
+
//
|
|
63
|
+
// Pure and DB-free: the only input is `builders.system_role`, which requireBuilder
|
|
64
|
+
// already reads fresh from the DB per request (alongside rank), and which no API
|
|
65
|
+
// route writes — a principal is provisioned out-of-band by migration, under a DB
|
|
66
|
+
// CHECK allowlist. The match is EXACT string equality, so NULL (every human,
|
|
67
|
+
// including an Archon), a differently-cased value, or an unknown role resolves to
|
|
68
|
+
// the empty set. Mirrors requireBfgPrincipal / requireDiscordBot in src/bongos/
|
|
69
|
+
// auth.js, which stay the HTTP enforcement point.
|
|
70
|
+
function resolvePrincipalPermissions(systemRole) {
|
|
71
|
+
const principal = catalog.principalByRole(systemRole);
|
|
72
|
+
return principal ? Object.freeze([principal.permission]) : Object.freeze([]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Does this exact machine identity hold this permission? Fail-closed on a blank or
|
|
76
|
+
// unknown identity, and on a key the identity does not own (one principal never
|
|
77
|
+
// reaches another's capability).
|
|
78
|
+
function principalHasPermission(systemRole, key) {
|
|
79
|
+
return resolvePrincipalPermissions(systemRole).includes(key);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The resolve entry point. Returns the builder's effective permission set as a
|
|
83
|
+
// frozen sorted array of catalog keys. A null/invalid builder id resolves to the
|
|
84
|
+
// empty set (fail-closed) without touching the DB.
|
|
85
|
+
async function resolveBuilderPermissions(builderId) {
|
|
86
|
+
if (builderId === null || builderId === undefined || builderId === '') return Object.freeze([]);
|
|
87
|
+
const raw = await db.getPermissionKeysForBuilder(builderId);
|
|
88
|
+
return effectivePermissions(raw);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Convenience for gate code: does this builder hold EVERY one of `required`?
|
|
92
|
+
// An empty `required` list is a programming error, not an open gate — it returns
|
|
93
|
+
// false so a mis-authored gate denies rather than admits (the same posture
|
|
94
|
+
// requireRank takes on an all-unknown allow-list).
|
|
95
|
+
//
|
|
96
|
+
// It also REFUSES ownership-scoped keys (R100). "Do you hold box.manage.own" has
|
|
97
|
+
// no unconditional answer — the permission is parameterized by a resource this
|
|
98
|
+
// function was never given — and returning true would be precisely the collapse
|
|
99
|
+
// of `.own` into an unconditional grant the axis exists to prevent. The caller
|
|
100
|
+
// wants authorizeOwned(), which cannot be called without naming the owner.
|
|
101
|
+
async function builderHasPermissions(builderId, required) {
|
|
102
|
+
if (!Array.isArray(required) || required.length === 0) return false;
|
|
103
|
+
if (required.some((k) => catalog.isOwnershipScoped(k))) return false;
|
|
104
|
+
const held = new Set(await resolveBuilderPermissions(builderId));
|
|
105
|
+
return required.every((k) => held.has(k));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── the OWNERSHIP axis (R100, ADR 0151 §3) ──────────────────────────────────────
|
|
109
|
+
// The third entry point, and a SEPARATE one for the same reason the principal axis
|
|
110
|
+
// is separate: an ownership-scoped permission is a conjunction of two facts from
|
|
111
|
+
// two different places, and folding it into the plain resolve would silently drop
|
|
112
|
+
// the second one.
|
|
113
|
+
//
|
|
114
|
+
// 1. the ROLE fact — does the builder hold the key? (this file, from the DB)
|
|
115
|
+
// 2. the OWNER fact — is the concrete row theirs? (REQUEST-scoped; only the
|
|
116
|
+
// handler that loaded the row knows it)
|
|
117
|
+
//
|
|
118
|
+
// The resolver structurally cannot know (2): it is handed a builder id, never a
|
|
119
|
+
// box/claim/memory row. So the caller SUPPLIES the owner and the model refuses to
|
|
120
|
+
// answer without it — `ownerId` is not an option, it is the second half of the
|
|
121
|
+
// question. That is what keeps `.own` from degrading into `.any`: there is no code
|
|
122
|
+
// path here that turns an ownership-scoped key into an admit on its own.
|
|
123
|
+
//
|
|
124
|
+
// Pure + DB-free where it can be: the comparison below never touches Postgres, and
|
|
125
|
+
// the role fact reuses resolveBuilderPermissions, so ADR 0016 holds unchanged —
|
|
126
|
+
// per-request, server-side, no stored answer.
|
|
127
|
+
|
|
128
|
+
// The owner comparison, normalized once so ~15 sites stop hand-rolling it. STRICT:
|
|
129
|
+
// only a scalar id (Postgres hands bigints back as strings, JS code carries them as
|
|
130
|
+
// numbers, so 7 and '7' must match) — anything else, including null/undefined, an
|
|
131
|
+
// object, an array, a boolean, NaN or an empty/blank string, has NO identity and
|
|
132
|
+
// therefore matches NOTHING. No trimming and no case-folding: normalizing harder
|
|
133
|
+
// than the data warrants is how a comparison quietly widens.
|
|
134
|
+
function ownerIdentity(value) {
|
|
135
|
+
if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null;
|
|
136
|
+
if (typeof value === 'bigint') return String(value);
|
|
137
|
+
if (typeof value === 'string') return value === '' ? null : value;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function sameOwner(a, b) {
|
|
141
|
+
const left = ownerIdentity(a);
|
|
142
|
+
const right = ownerIdentity(b);
|
|
143
|
+
return left !== null && right !== null && left === right;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Does this builder hold `key` AND own the resource `ownerId` identifies?
|
|
147
|
+
// Fail-closed on every unhappy path — an unknown key, a key that is NOT
|
|
148
|
+
// ownership-scoped (wrong entry point; the plain resolve answers those), a missing
|
|
149
|
+
// or unidentifiable owner, or a builder who simply does not hold the key.
|
|
150
|
+
async function authorizeOwned(builderId, key, context) {
|
|
151
|
+
const permission = catalog.byKey(key);
|
|
152
|
+
if (!permission || permission.scope !== 'own') return false;
|
|
153
|
+
if (!sameOwner(context && context.ownerId, builderId)) return false;
|
|
154
|
+
return (await resolveBuilderPermissions(builderId)).includes(key);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The composition every own-scoped route actually wants: reach this row because it
|
|
158
|
+
// is YOURS, or because you hold the cross-owner permission over its resource
|
|
159
|
+
// family (claim.release.any, memory.read.any, box.fleet.manage). One resolve, both
|
|
160
|
+
// branches. A resource with no cross-owner form by design (credential) simply has
|
|
161
|
+
// no second branch, so the answer stays own-only.
|
|
162
|
+
async function authorizeOwnedOrAny(builderId, ownKey, context) {
|
|
163
|
+
const permission = catalog.byKey(ownKey);
|
|
164
|
+
if (!permission || permission.scope !== 'own') return false;
|
|
165
|
+
const held = await resolveBuilderPermissions(builderId);
|
|
166
|
+
if (catalog.crossOwnerKeysFor(ownKey).some((k) => held.includes(k))) return true;
|
|
167
|
+
return sameOwner(context && context.ownerId, builderId) && held.includes(ownKey);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── the SESSION-SCOPE axis (R99, ADR 0151 §3) ───────────────────────────────────
|
|
171
|
+
// The FOURTH entry point, separate for the same reason the other three are: it
|
|
172
|
+
// answers a different question with a different input. Roles answer "what may this
|
|
173
|
+
// PERSON do"; a principal answers "what may this MACHINE do"; an ownership scope
|
|
174
|
+
// answers "whose row may it be done to"; a session-scope answers "how much of that
|
|
175
|
+
// does THIS CREDENTIAL carry". A dev-box token is minted onto a machine outside the
|
|
176
|
+
// ADR 0016 trust boundary (#919/SR#2, ADR 0053), so it must stay limited even when
|
|
177
|
+
// its builder is an Archon — which no role-shaped answer can express.
|
|
178
|
+
//
|
|
179
|
+
// A SESSION-SCOPE CAN ONLY EVER NARROW, in both directions:
|
|
180
|
+
// • INTERSECTION, never union. The scope may not GRANT a permission the roles do
|
|
181
|
+
// not confer — so the composition below is written as a FILTER of the ROLE
|
|
182
|
+
// answer. There is no branch here that can add a key, which is what makes
|
|
183
|
+
// "a box token cannot escalate" structural rather than merely intended.
|
|
184
|
+
// • DENY-BY-DEFAULT. A key the allow-list does not name is denied; an
|
|
185
|
+
// unrecognised session source resolves to NOTHING rather than to "full".
|
|
186
|
+
// Widening is therefore not expressible without deleting the filter, and R103's
|
|
187
|
+
// parity is untouched: effectivePermissions is the unchanged ROLE answer this
|
|
188
|
+
// composes ON TOP of, never inside.
|
|
189
|
+
//
|
|
190
|
+
// Pure and DB-free (the DB half is resolveBuilderPermissions'), so an unknown key
|
|
191
|
+
// on an allow-list is inert as well as refused at load: it can never appear in the
|
|
192
|
+
// catalog-filtered role answer, so it can never survive the intersection.
|
|
193
|
+
function narrowToSessionScope(rawKeys, source) {
|
|
194
|
+
const scope = sessionScopes.scopeFor(source);
|
|
195
|
+
if (!scope) return Object.freeze([]); // unrecognised source → no authority at all
|
|
196
|
+
const held = effectivePermissions(rawKeys);
|
|
197
|
+
if (scope.unscoped) return held; // this credential narrows nothing (cli/web)
|
|
198
|
+
const allowed = new Set(scope.permissions);
|
|
199
|
+
return Object.freeze(held.filter((key) => allowed.has(key)));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// What can THIS SESSION actually do? The builder's roles ∩ this session's scope,
|
|
203
|
+
// as a frozen sorted array of catalog keys. Fails closed BEFORE touching the DB on
|
|
204
|
+
// a source the registry does not declare.
|
|
205
|
+
async function resolveSessionPermissions(builderId, source) {
|
|
206
|
+
if (!sessionScopes.scopeFor(source)) return Object.freeze([]);
|
|
207
|
+
return narrowToSessionScope(await resolveBuilderPermissions(builderId), source);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
resolveBuilderPermissions,
|
|
212
|
+
builderHasPermissions,
|
|
213
|
+
effectivePermissions,
|
|
214
|
+
resolvePrincipalPermissions,
|
|
215
|
+
principalHasPermission,
|
|
216
|
+
sameOwner,
|
|
217
|
+
authorizeOwned,
|
|
218
|
+
authorizeOwnedOrAny,
|
|
219
|
+
narrowToSessionScope,
|
|
220
|
+
resolveSessionPermissions,
|
|
221
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The governance module's kernel PORT registration (ADR 0151, BV1.R95).
|
|
4
|
+
//
|
|
5
|
+
// governance exposes the permission RESOLVER as the `governance` kernel PORT.
|
|
6
|
+
// CORE files that must NOT import a module resolve it instead — first among them
|
|
7
|
+
// src/bongos/auth.js, whose `requirePermission()` gate (R97) needs a builder's
|
|
8
|
+
// effective permission set on every gated request. The one-way rule (ADR 0083)
|
|
9
|
+
// forbids core requiring modules/governance/ directly; the port is how the
|
|
10
|
+
// capability crosses that line.
|
|
11
|
+
//
|
|
12
|
+
// Why a route factory for a (currently) routeless capability: `mountModuleRoutes`
|
|
13
|
+
// is the one boot hook that runs BOTH in the live server AND whenever a test
|
|
14
|
+
// rebuilds the GDS router, so registering here keeps the port present for the
|
|
15
|
+
// route-building tests too. Registration is hasProvider-guarded so
|
|
16
|
+
// re-instantiation never trips the seam's single-provider duplicate guard.
|
|
17
|
+
// (Mirrors modules/economy/routes/reward.js.) The governance CRUD endpoints land
|
|
18
|
+
// on this same router in R110.
|
|
19
|
+
|
|
20
|
+
const express = require('express');
|
|
21
|
+
const api = require('../../../src/module-api');
|
|
22
|
+
const resolver = require('../resolver');
|
|
23
|
+
const catalog = require('../catalog');
|
|
24
|
+
const db = require('../db');
|
|
25
|
+
const sessionScopes = require('../session-scopes');
|
|
26
|
+
|
|
27
|
+
// The port surface. Deliberately narrow: resolution + the catalog reads a gate or
|
|
28
|
+
// the Archon tab needs. It grows with R98-R101 (principals, session-scopes,
|
|
29
|
+
// ownership scoping, the protected-surface registry) — one object port rather
|
|
30
|
+
// than a port per capability, so core resolves `governance` once.
|
|
31
|
+
//
|
|
32
|
+
// The principal half (R98) is READ-ONLY by construction: resolution keyed on an
|
|
33
|
+
// exact system_role, plus `listPrincipals` for the tab to render. There is no
|
|
34
|
+
// create/grant/assign counterpart and there must never be one — a principal is
|
|
35
|
+
// provisioned out-of-band in `builders.system_role` (migration + DB CHECK
|
|
36
|
+
// allowlist), so the tab can SHOW a principal but has nothing to call to make one.
|
|
37
|
+
//
|
|
38
|
+
// The ownership half (R100) is why an own-scoped route reaches the port from
|
|
39
|
+
// INSIDE its handler rather than from a middleware: the second half of the
|
|
40
|
+
// decision is the concrete row's owner, which only the handler that loaded the row
|
|
41
|
+
// knows. `authorizeOwned`/`authorizeOwnedOrAny` take that owner as an argument, so
|
|
42
|
+
// there is no arity at which an ownership-scoped key resolves to an admit on its
|
|
43
|
+
// own (ADR 0151 §3's in-handler pure-gate pattern, made a first-class call).
|
|
44
|
+
// Callers must fail CLOSED when the port is absent, exactly as requirePermission
|
|
45
|
+
// does — a missing authority system is a denial, never an admission.
|
|
46
|
+
//
|
|
47
|
+
// The session-scope half (R99) is READ-ONLY and deliberately NOT the enforcement
|
|
48
|
+
// path: the kernel gate (auth.requireBuilder) reads the same registry as static
|
|
49
|
+
// data, so a request never depends on this port being registered to stay
|
|
50
|
+
// deny-by-default. What the port adds is the composed answer — roles ∩ this
|
|
51
|
+
// session's scope — for the governance tab and for any gate that wants the
|
|
52
|
+
// session's real authority rather than the builder's.
|
|
53
|
+
const governancePort = {
|
|
54
|
+
resolveBuilderPermissions: resolver.resolveBuilderPermissions,
|
|
55
|
+
builderHasPermissions: resolver.builderHasPermissions,
|
|
56
|
+
getRoleKeysForBuilder: db.getRoleKeysForBuilder,
|
|
57
|
+
isSystem: catalog.isSystem,
|
|
58
|
+
isGrantable: catalog.isGrantable,
|
|
59
|
+
permissionKeys: catalog.permissionKeys,
|
|
60
|
+
resolvePrincipalPermissions: resolver.resolvePrincipalPermissions,
|
|
61
|
+
principalHasPermission: resolver.principalHasPermission,
|
|
62
|
+
isPrincipalKey: catalog.isPrincipalKey,
|
|
63
|
+
listPrincipals: catalog.principals,
|
|
64
|
+
authorizeOwned: resolver.authorizeOwned,
|
|
65
|
+
authorizeOwnedOrAny: resolver.authorizeOwnedOrAny,
|
|
66
|
+
sameOwner: resolver.sameOwner,
|
|
67
|
+
isOwnershipScoped: catalog.isOwnershipScoped,
|
|
68
|
+
crossOwnerKeysFor: catalog.crossOwnerKeysFor,
|
|
69
|
+
resolveSessionPermissions: resolver.resolveSessionPermissions,
|
|
70
|
+
narrowToSessionScope: resolver.narrowToSessionScope,
|
|
71
|
+
sessionScopeFor: sessionScopes.scopeFor,
|
|
72
|
+
listSessionScopes: () => sessionScopes.SESSION_SCOPES,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function registerSeams() {
|
|
76
|
+
if (!api.hasProvider('governance')) {
|
|
77
|
+
api.registerProvider('governance', governancePort);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = function buildGovernanceRouter() {
|
|
82
|
+
registerSeams();
|
|
83
|
+
// No HTTP surface yet — the resolver is a pure kernel port. The empty router is
|
|
84
|
+
// only the loader's contributes.routes mount vehicle; R110 adds the roles /
|
|
85
|
+
// permissions / assignments CRUD here behind `governance.manage`.
|
|
86
|
+
return express.Router();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
module.exports.governancePort = governancePort;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The governance module's rank-role SYNC port (ADR 0151, BV1.R95b).
|
|
4
|
+
//
|
|
5
|
+
// A second, deliberately tiny kernel port beside `governance` (routes/governance.js).
|
|
6
|
+
// `governance` is the READ surface every gate resolves on every request; this is the
|
|
7
|
+
// single WRITE core needs — "this builder's rank is now X, make their seeded
|
|
8
|
+
// rank-role match". They are kept apart on purpose:
|
|
9
|
+
// • the read port is on the hot path of every permission-gated request and is
|
|
10
|
+
// resolved by requirePermission(); a mutation has no business hanging off it;
|
|
11
|
+
// • this one is resolved by exactly three writers (the two _applyRankChangeTx
|
|
12
|
+
// twins + the sign-in path) and is a TRANSACTION PARTICIPANT — it takes the
|
|
13
|
+
// caller's client so the role rotation commits or rolls back with the rank write.
|
|
14
|
+
// The port name is namespaced (`governance.roleSync`) like the `ideas.capture` /
|
|
15
|
+
// `art.getBuilderGeminiKeyRow` / `grade.builderRollingAvgGrade` ports.
|
|
16
|
+
//
|
|
17
|
+
// Registered from a route factory for the same reason routes/governance.js is:
|
|
18
|
+
// mountModuleRoutes is the one boot hook that runs BOTH in the live server and
|
|
19
|
+
// whenever a test rebuilds the router, and registration is hasProvider-guarded so a
|
|
20
|
+
// rebuild never trips the seam's single-provider duplicate guard.
|
|
21
|
+
//
|
|
22
|
+
// The router is EMPTY and must stay empty. Rank-derived assignment is automatic and
|
|
23
|
+
// has no HTTP surface; the MANUAL assignment CRUD is R110's Archon-gated tab on the
|
|
24
|
+
// governance router. An endpoint here would be a way to hand out a rank-role without
|
|
25
|
+
// changing the rank.
|
|
26
|
+
|
|
27
|
+
const express = require('express');
|
|
28
|
+
const api = require('../../../src/module-api');
|
|
29
|
+
const db = require('../db');
|
|
30
|
+
|
|
31
|
+
// DELEGATE, never spread — a test that stubs db.syncRankRole must still bite at call
|
|
32
|
+
// time (the modules/onboarding/port.js precedent; a spread captures the original
|
|
33
|
+
// reference at module-load).
|
|
34
|
+
const rankRoleSyncPort = {
|
|
35
|
+
syncRankRole: (...a) => db.syncRankRole(...a),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function registerSeams() {
|
|
39
|
+
if (!api.hasProvider('governance.roleSync')) {
|
|
40
|
+
api.registerProvider('governance.roleSync', rankRoleSyncPort);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = function buildGovernanceRoleSyncRouter() {
|
|
45
|
+
registerSeams();
|
|
46
|
+
return express.Router();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
module.exports.rankRoleSyncPort = rankRoleSyncPort;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/governance/session-scopes.js — the SESSION-SCOPE axis (ADR 0151 §3, BV1.R99).
|
|
4
|
+
//
|
|
5
|
+
// The FOURTH axis, and the only one attached to the TOKEN rather than to the
|
|
6
|
+
// builder. Roles say what a person may do (R95); a principal's identity says what
|
|
7
|
+
// a machine may do (R98); an ownership scope says whose row it may be done to
|
|
8
|
+
// (R100); a SESSION-SCOPE says how much of that authority this particular
|
|
9
|
+
// credential carries. A dev-box session is minted onto a machine OUTSIDE the ADR
|
|
10
|
+
// 0016 trust boundary (#919/SR#2, ADR 0053), so it must stay limited even when its
|
|
11
|
+
// builder is an Archon.
|
|
12
|
+
//
|
|
13
|
+
// A SESSION-SCOPE CAN ONLY EVER NARROW. That is the whole load-bearing property,
|
|
14
|
+
// and it has two halves that fail in opposite directions:
|
|
15
|
+
// • INTERSECTION, never union — the scope may not GRANT a permission the
|
|
16
|
+
// builder's roles do not already confer, or a box token would escalate;
|
|
17
|
+
// • DENY-BY-DEFAULT — a permission the allow-list does not name is denied, or an
|
|
18
|
+
// unlisted permission leaks through.
|
|
19
|
+
// resolver.narrowToSessionScope() is the one place the two compose, and it is
|
|
20
|
+
// written as a FILTER of the role answer so widening is not expressible there.
|
|
21
|
+
//
|
|
22
|
+
// EXPLICIT DATA, not `if (source === 'box')`. session-scopes.json (next to this
|
|
23
|
+
// file) is the declaration; this module validates it against the permission
|
|
24
|
+
// catalog at load. GOVERNANCE OWNS THE POLICY, THE KERNEL STAYS THE ENFORCER —
|
|
25
|
+
// the same split R101 draws for the protected-surface registry: src/bongos/auth.js
|
|
26
|
+
// reads the same JSON as DATA (fs, never `require` — ADR 0083's one-way rule) and
|
|
27
|
+
// requireBuilder remains the gate every route composes. Nothing here enforces
|
|
28
|
+
// anything; re-mapping a scope here changes what the gate admits, it can never
|
|
29
|
+
// move the gate.
|
|
30
|
+
//
|
|
31
|
+
// FAIL-CLOSED at load: a key the catalog does not define, a `system: true`
|
|
32
|
+
// trust-boundary key, or a machine-principal key on a session-scope allow-list all
|
|
33
|
+
// THROW. Silently dropping the row would quietly shrink (or, for a principal,
|
|
34
|
+
// blur) the surface a token outside the trust boundary may reach — an unreadable
|
|
35
|
+
// policy is a broken install, not an opted-out one.
|
|
36
|
+
|
|
37
|
+
const catalog = require('./catalog');
|
|
38
|
+
const registry = require('./session-scopes.json');
|
|
39
|
+
|
|
40
|
+
// Normalize the declared rows into the same shape the kernel loader produces:
|
|
41
|
+
// `source` null for the legacy sourceless row, `permissions` always an array.
|
|
42
|
+
function normalizeScope(row) {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
source: row && row.source !== undefined ? row.source : null,
|
|
45
|
+
label: row && typeof row.label === 'string' ? row.label : null,
|
|
46
|
+
unscoped: !!(row && row.unscoped === true),
|
|
47
|
+
permissions: Object.freeze(row && Array.isArray(row.permissions) ? [...row.permissions] : []),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The catalog cross-check. Exported so the shipped assertion itself is what the
|
|
52
|
+
// tests exercise, rather than a re-implementation of it (the fixture would
|
|
53
|
+
// otherwise have to rewrite this file on disk to be loaded).
|
|
54
|
+
function validateAgainstCatalog(scopes) {
|
|
55
|
+
for (const scope of scopes) {
|
|
56
|
+
const where = `session-scope registry: scope "${scope.source}"`;
|
|
57
|
+
// unscoped XOR an allow-list. Both would be ambiguous (does it narrow?);
|
|
58
|
+
// neither would be a scope that can never admit anything.
|
|
59
|
+
if (scope.unscoped === (scope.permissions.length > 0)) {
|
|
60
|
+
throw new Error(`${where} must declare EITHER unscoped:true OR a non-empty permissions allow-list, never both/neither`);
|
|
61
|
+
}
|
|
62
|
+
for (const key of scope.permissions) {
|
|
63
|
+
if (!catalog.byKey(key)) {
|
|
64
|
+
throw new Error(`${where} allows "${key}", which the permission catalog does not define`);
|
|
65
|
+
}
|
|
66
|
+
// R98's rule, kept structural: a machine principal's capability comes from
|
|
67
|
+
// its exact system_role, never from a role — so a box-scoped session
|
|
68
|
+
// carrying one must not be expressible on a scope allow-list either.
|
|
69
|
+
if (catalog.isPrincipalKey(key)) {
|
|
70
|
+
throw new Error(`${where} allows the machine-principal permission "${key}" — a session-scope narrows a builder's ROLE authority and never composes with the identity axis`);
|
|
71
|
+
}
|
|
72
|
+
// A session-scope IS the surface a credential outside the ADR 0016 trust
|
|
73
|
+
// boundary may reach. A trust-boundary permission must never appear on one,
|
|
74
|
+
// even though the intersection would make listing it inert today.
|
|
75
|
+
if (catalog.isSystem(key)) {
|
|
76
|
+
throw new Error(`${where} allows the system:true permission "${key}" — a session-scope may only list operational capability (a scoped token lives outside the trust boundary)`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return scopes;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const SESSION_SCOPES = Object.freeze(
|
|
84
|
+
validateAgainstCatalog(Object.freeze((registry.scopes || []).map(normalizeScope))),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const _bySource = new Map(SESSION_SCOPES.map((s) => [s.source, s]));
|
|
88
|
+
|
|
89
|
+
// The scope a session's `source` declares, or null when the registry does not
|
|
90
|
+
// declare it. An UNRECOGNISED source resolves to null and therefore to NO
|
|
91
|
+
// permissions — it never defaults to "full" (a null/absent source is a DECLARED
|
|
92
|
+
// row here, not an implicit default: it predates the box source existing).
|
|
93
|
+
function scopeFor(source) {
|
|
94
|
+
const key = source === undefined ? null : source;
|
|
95
|
+
if (key !== null && typeof key !== 'string') return null;
|
|
96
|
+
return _bySource.get(key) || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Does this session's scope narrow at all? Unknown source → true (fail-closed:
|
|
100
|
+
// treated as narrowing to nothing, which is what scopeFor/narrowToSessionScope do).
|
|
101
|
+
function isScoped(source) {
|
|
102
|
+
const scope = scopeFor(source);
|
|
103
|
+
return !scope || !scope.unscoped;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The deny-by-default allow-list for a source; empty for an unscoped source (it
|
|
107
|
+
// narrows nothing) AND for an unrecognised one (it admits nothing) — callers must
|
|
108
|
+
// consult scopeFor()/isScoped() to tell those apart, exactly as the resolver does.
|
|
109
|
+
const NO_KEYS = Object.freeze([]);
|
|
110
|
+
function allowedKeys(source) {
|
|
111
|
+
const scope = scopeFor(source);
|
|
112
|
+
return scope ? scope.permissions : NO_KEYS;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sources() {
|
|
116
|
+
return SESSION_SCOPES.map((s) => s.source);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
SESSION_SCOPES,
|
|
121
|
+
validateAgainstCatalog,
|
|
122
|
+
normalizeScope,
|
|
123
|
+
scopeFor,
|
|
124
|
+
isScoped,
|
|
125
|
+
allowedKeys,
|
|
126
|
+
sources,
|
|
127
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": [
|
|
3
|
+
"The SESSION-SCOPE registry (BV1.R99, ADR 0151 §3; the model behind ADR 0053).",
|
|
4
|
+
"A session's scope is capability attached to the TOKEN, orthogonal to the builder's roles.",
|
|
5
|
+
"Each row declares EITHER unscoped:true (the session narrows nothing) OR a deny-by-default",
|
|
6
|
+
"permissions allow-list (unlisted => denied). A scope can only ever NARROW: effective",
|
|
7
|
+
"authority is (the builder's role permissions) INTERSECT (this allow-list), never a union.",
|
|
8
|
+
"Every key here is validated against modules/governance/catalog.js at load by",
|
|
9
|
+
"modules/governance/session-scopes.js — unknown, system:true and principal keys are refused.",
|
|
10
|
+
"Read as DATA by src/bongos/auth.js (fs, never require — ADR 0083's one-way rule)."
|
|
11
|
+
],
|
|
12
|
+
"scopes": [
|
|
13
|
+
{
|
|
14
|
+
"source": "cli",
|
|
15
|
+
"label": "CLI session",
|
|
16
|
+
"unscoped": true,
|
|
17
|
+
"note": "Device Flow token on a builder's own machine — inside the trust boundary, carries the builder's full role authority."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"source": "web",
|
|
21
|
+
"label": "web session",
|
|
22
|
+
"unscoped": true,
|
|
23
|
+
"note": "Hall browser cookie — same authority as the CLI session."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"source": null,
|
|
27
|
+
"label": "legacy session",
|
|
28
|
+
"unscoped": true,
|
|
29
|
+
"note": "Rows minted before migration 090 added a source column; they predate the 'box' source existing, so they were never box tokens. Declared explicitly rather than defaulted — an UNRECOGNISED source denies."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"source": "box",
|
|
33
|
+
"label": "box-scoped session",
|
|
34
|
+
"permissions": ["box.manage.own"],
|
|
35
|
+
"note": "Baked into cloud-init so a dev box can bootstrap (#818/SR#2, ADR 0053). The token lives ON the box — OUTSIDE the ADR 0016 trust boundary — so it reaches only its own box record, whatever its builder's rank."
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|