@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
package/tests/backup_cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Run: node tests/backup_cli.mjs
|
|
11
11
|
|
|
12
12
|
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
13
14
|
|
|
14
15
|
let passed = 0;
|
|
15
16
|
let failed = 0;
|
|
@@ -94,6 +95,59 @@ t('trigger response: 409 when already running', () => {
|
|
|
94
95
|
assert.equal(response.error, 'backup_already_running');
|
|
95
96
|
});
|
|
96
97
|
|
|
98
|
+
// ---- staleness detection (task 1002479) -------------------------------------
|
|
99
|
+
// These import the REAL implementations rather than re-implementing them, so a
|
|
100
|
+
// regression in the shipped code fails here instead of passing against a copy.
|
|
101
|
+
const backupRoute = createRequire(import.meta.url)('../src/bongos/routes/backup.js');
|
|
102
|
+
const { parseDumpTimestamp, dumpAgeHours, isStaleDump } = backupRoute;
|
|
103
|
+
|
|
104
|
+
const NOW = Date.parse('2026-07-28T23:00:00Z');
|
|
105
|
+
|
|
106
|
+
t('parseDumpTimestamp: the dump stamp is NOT plain ISO — colons must be reinserted', () => {
|
|
107
|
+
// Regression guard: Date.parse on the raw stamp returns NaN. If someone
|
|
108
|
+
// "simplifies" this to Date.parse(stamp), staleness silently becomes unknown
|
|
109
|
+
// → every dump reads stale (or, with a sloppier fallback, every dump reads fresh).
|
|
110
|
+
assert.ok(Number.isNaN(Date.parse('2026-07-28T230421Z')), 'raw stamp should not parse as ISO');
|
|
111
|
+
assert.equal(parseDumpTimestamp('2026-07-28T230421Z'), Date.parse('2026-07-28T23:04:21Z'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
t('parseDumpTimestamp: rejects garbage rather than guessing', () => {
|
|
115
|
+
for (const bad of ['', null, undefined, 'nope', '2026-07-28', '2026-07-28T2304Z']) {
|
|
116
|
+
assert.equal(parseDumpTimestamp(bad), null, `should reject ${JSON.stringify(bad)}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
t('dumpAgeHours: computes hours, floors at 0 for a future stamp', () => {
|
|
121
|
+
assert.equal(dumpAgeHours('2026-07-28T220000Z', NOW), 1);
|
|
122
|
+
assert.equal(dumpAgeHours('2026-07-27T230000Z', NOW), 24);
|
|
123
|
+
assert.equal(dumpAgeHours('2026-07-29T230000Z', NOW), 0, 'future stamp must not go negative');
|
|
124
|
+
assert.equal(dumpAgeHours('garbage', NOW), null);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
t('isStaleDump: fresh dump is not stale', () => {
|
|
128
|
+
assert.equal(isStaleDump('2026-07-28T230421Z', NOW), false);
|
|
129
|
+
assert.equal(isStaleDump('2026-07-27T140000Z', NOW), false, '33h is inside the 36h threshold');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
t('isStaleDump: the real 13-day cloudbongos outage is detected', () => {
|
|
133
|
+
// The actual last good dump before the 203/EXEC failure was 2026-07-15.
|
|
134
|
+
assert.equal(isStaleDump('2026-07-15T040302Z', NOW), true);
|
|
135
|
+
assert.ok(dumpAgeHours('2026-07-15T040302Z', NOW) > 300, 'should be ~13 days of hours');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
t('isStaleDump: an undateable stamp fails LOUD (stale), never silently healthy', () => {
|
|
139
|
+
assert.equal(isStaleDump('garbage', NOW), true);
|
|
140
|
+
assert.equal(isStaleDump('', NOW), true);
|
|
141
|
+
assert.equal(isStaleDump(undefined, NOW), true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
t('isStaleDump: threshold boundary is exclusive', () => {
|
|
145
|
+
const at36h = '2026-07-27T110000Z'; // exactly 36h before NOW
|
|
146
|
+
assert.equal(dumpAgeHours(at36h, NOW), 36);
|
|
147
|
+
assert.equal(isStaleDump(at36h, NOW, 36), false, 'exactly at threshold is not yet stale');
|
|
148
|
+
assert.equal(isStaleDump('2026-07-27T105900Z', NOW, 36), true, 'one minute past is stale');
|
|
149
|
+
});
|
|
150
|
+
|
|
97
151
|
// ---- summary ----------------------------------------------------------------
|
|
98
152
|
console.log('');
|
|
99
153
|
if (failed === 0) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// helpers are exercised. Picked up by scripts/gds/run-unit-tests.js (the `unit` gate).
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
5
5
|
import { test } from 'node:test';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
6
7
|
import lib from '../scripts/gds/box-connect-lib.js';
|
|
7
8
|
|
|
8
9
|
const HOST = 'box-alice.dev.amazonprimea.com';
|
|
@@ -63,6 +64,62 @@ test('buildKnownHostsUpsert drops the stale line for the host, keeps others, app
|
|
|
63
64
|
assert.ok(next.includes(`${HOST} ssh-rsa AAAARSANEW`), 'appends every reported key');
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
// task 1002509 — the upsert used to compare field 1 to the hostname with `!==`, so
|
|
68
|
+
// the two forms OpenSSH actually writes survived the "replace" and stale lines
|
|
69
|
+
// ACCUMULATED: the client kept verifying against the first, now-wrong entry
|
|
70
|
+
// ("Host denied (verification failed)") on a box that had just re-reported keys.
|
|
71
|
+
test('buildKnownHostsUpsert drops a host,ip entry for this box (the accumulation bug)', () => {
|
|
72
|
+
const existing = `${HOST},64.225.49.206 ssh-ed25519 STALE\n`;
|
|
73
|
+
const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
|
|
74
|
+
assert.ok(!next.includes('STALE'), 'a `host,ip` field-1 names this host and must be replaced');
|
|
75
|
+
assert.ok(next.includes(`${HOST} ssh-ed25519 AAAANEW`));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('buildKnownHostsUpsert drops a HASHED entry for this box, keeps another host, keeps comments', () => {
|
|
79
|
+
const salt = crypto.randomBytes(20);
|
|
80
|
+
const hashFor = (h) => `|1|${salt.toString('base64')}|${crypto.createHmac('sha1', salt).update(h).digest('base64')}`;
|
|
81
|
+
const existing = [
|
|
82
|
+
'# hand-written note',
|
|
83
|
+
`${hashFor(HOST)} ssh-ed25519 STALEHASHED`,
|
|
84
|
+
`${hashFor('other.example.com')} ssh-ed25519 KEEPHASHED`,
|
|
85
|
+
].join('\n') + '\n';
|
|
86
|
+
const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW');
|
|
87
|
+
assert.ok(!next.includes('STALEHASHED'), 'HashKnownHosts entries must be matched, not stepped over');
|
|
88
|
+
assert.ok(next.includes('KEEPHASHED'), 'another host hashed with the same salt stays');
|
|
89
|
+
assert.ok(next.includes('# hand-written note'), 'comments survive');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('buildKnownHostsUpsert drops an IP-keyed line when the box IP is supplied', () => {
|
|
93
|
+
const existing = '203.0.113.7 ssh-ed25519 STALEIP\nunrelated.example.com ssh-ed25519 KEEP\n';
|
|
94
|
+
const next = lib.buildKnownHostsUpsert(existing, HOST, 'ssh-ed25519 AAAANEW', '203.0.113.7');
|
|
95
|
+
assert.ok(!next.includes('STALEIP'), 'DNS is repointed on every wake — an old IP line is stale too');
|
|
96
|
+
assert.ok(next.includes('KEEP'));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('buildKnownHostsUpsert is idempotent — re-running never grows the file', () => {
|
|
100
|
+
const first = lib.buildKnownHostsUpsert('', HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
|
|
101
|
+
const second = lib.buildKnownHostsUpsert(first, HOST, 'ssh-ed25519 AAAANEW\nssh-rsa AAAARSA');
|
|
102
|
+
assert.equal(second, first);
|
|
103
|
+
assert.equal(second.split('\n').filter(Boolean).length, 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// task 1002509 — the old inline logic FAILED OPEN: no fingerprint (ssh-keygen absent
|
|
107
|
+
// from a GUI process's leaner PATH) or an unreadable key list meant "skip", so a
|
|
108
|
+
// builder whose key was never on the server got a bare "Permission denied". A
|
|
109
|
+
// redundant POST is free (the server upserts by fingerprint), uncertainty is not.
|
|
110
|
+
test('decideKeyRegistration registers on uncertainty and skips only on proof', () => {
|
|
111
|
+
const FP = 'SHA256:abc';
|
|
112
|
+
assert.deepEqual(lib.decideKeyRegistration({ created: true }), { register: true, why: 'created' });
|
|
113
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: FP }] }).register, false);
|
|
114
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [{ fingerprint: 'SHA256:other' }] }).register, true);
|
|
115
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listedKeys: [] }).register, true);
|
|
116
|
+
// the two fail-open holes this closes:
|
|
117
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [] }).register, true);
|
|
118
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: null, listedKeys: [{ fingerprint: FP }] }).register, true);
|
|
119
|
+
assert.equal(lib.decideKeyRegistration({ fingerprint: FP, listFailed: true }).register, true);
|
|
120
|
+
assert.equal(lib.decideKeyRegistration({}).register, true, 'no information at all → register');
|
|
121
|
+
});
|
|
122
|
+
|
|
66
123
|
test('buildRemoteShellCommand forces claude only on first run and always lands in /workspace', () => {
|
|
67
124
|
const first = lib.buildRemoteShellCommand({ firstRun: true });
|
|
68
125
|
const later = lib.buildRemoteShellCommand({ firstRun: false });
|
|
@@ -74,6 +131,26 @@ test('buildRemoteShellCommand forces claude only on first run and always lands i
|
|
|
74
131
|
assert.match(later, /exec bash -l/);
|
|
75
132
|
});
|
|
76
133
|
|
|
134
|
+
test('buildRemoteShellCommand re-scopes on connect ONLY with sync:true (BV1.R101)', () => {
|
|
135
|
+
const noSync = lib.buildRemoteShellCommand({ firstRun: false, sync: false });
|
|
136
|
+
const withSync = lib.buildRemoteShellCommand({ firstRun: false, sync: true });
|
|
137
|
+
assert.ok(!/box-sync/.test(noSync), 'default (sync:false) never re-scopes');
|
|
138
|
+
assert.match(withSync, /node scripts\/gds\/box-sync\.js \|\| true/);
|
|
139
|
+
// order: cd → re-scope → shell, so the builder lands in the scoped folder.
|
|
140
|
+
assert.ok(withSync.indexOf('cd /workspace') < withSync.indexOf('box-sync'), 'cd before sync');
|
|
141
|
+
assert.ok(withSync.indexOf('box-sync') < withSync.indexOf('exec bash -l'), 'sync before the shell');
|
|
142
|
+
// sync + firstRun: re-scope THEN the one-time claude /login handoff.
|
|
143
|
+
const both = lib.buildRemoteShellCommand({ firstRun: true, sync: true });
|
|
144
|
+
assert.ok(both.indexOf('box-sync') < both.indexOf('claude || true'), 'sync before the first-run login');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('buildRemoteResyncCommand is a cd + re-scope one-shot (bongos code path) — no shell (BV1.R101)', () => {
|
|
148
|
+
const cmd = lib.buildRemoteResyncCommand();
|
|
149
|
+
assert.match(cmd, /cd \/workspace/);
|
|
150
|
+
assert.match(cmd, /node scripts\/gds\/box-sync\.js \|\| true/);
|
|
151
|
+
assert.ok(!/exec bash/.test(cmd), 'one-shot — never spawns a login shell');
|
|
152
|
+
});
|
|
153
|
+
|
|
77
154
|
test('remoteAuthority builds a VS Code Remote-SSH authority', () => {
|
|
78
155
|
assert.equal(lib.remoteAuthority(HOST), `ssh-remote+${HOST}`);
|
|
79
156
|
});
|
package/tests/box_access.mjs
CHANGED
|
@@ -76,7 +76,7 @@ t('cloneSpecForScope: starter sparsePaths is a COPY (mutating it cannot poison t
|
|
|
76
76
|
console.log('\nSTARTER_SPARSE_PATHS — the scoped surface (ADR 0052 §9.2):');
|
|
77
77
|
|
|
78
78
|
t('starter surface includes the build tooling + game runtime', () => {
|
|
79
|
-
for (const p of ['.claude', 'scripts', 'src/bongos', 'src/world', 'src/rooms', 'public', '.devcontainer', 'infra']) {
|
|
79
|
+
for (const p of ['.claude', 'scripts', 'src/bongos', 'src/world', 'src/rooms', 'public', '.devcontainer', 'infra', 'tests']) {
|
|
80
80
|
assert.ok(access.STARTER_SPARSE_PATHS.includes(p), `starter scope must include ${p}`);
|
|
81
81
|
}
|
|
82
82
|
});
|
|
@@ -182,5 +182,134 @@ t('reconcileSourceAccess: inactivity outranks an otherwise in-sync full Metic bo
|
|
|
182
182
|
assert.deepEqual(d, { action: 'deprovision', reason: 'builder-inactive' });
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
// ===========================================================================
|
|
186
|
+
// TASK-SCOPED source access (ADR 0148 / goal 1000051, R93). BASE_SPARSE_PATHS,
|
|
187
|
+
// the pure sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) cone-filter, and
|
|
188
|
+
// the decideSourceAccess three-way branch. Uses the REAL module-scope-map (a pure
|
|
189
|
+
// core module) as the map the route (R94) will pass in.
|
|
190
|
+
// ===========================================================================
|
|
191
|
+
const scopeMap = require('../src/bongos/module-scope-map.js');
|
|
192
|
+
const BASE = access.BASE_SPARSE_PATHS;
|
|
193
|
+
|
|
194
|
+
console.log('\nBASE_SPARSE_PATHS — the non-product base a task-scoped box layers on:');
|
|
195
|
+
|
|
196
|
+
t('BASE carries the methodology + tooling, NOT the product/game', () => {
|
|
197
|
+
for (const p of ['.devcontainer', '.claude', 'scripts', 'src/bongos', 'docs', 'infra', 'tests']) {
|
|
198
|
+
assert.ok(BASE.includes(p), `BASE must include ${p}`);
|
|
199
|
+
}
|
|
200
|
+
for (const p of ['src/world', 'src/rooms', 'public', 'art', 'modules/status-ui']) {
|
|
201
|
+
assert.ok(!BASE.includes(p), `BASE must NOT include the product surface ${p}`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
console.log('\nsparsePathsForScopeKeys — BASE ∪ the claims’ module DIRS (cone-mode):');
|
|
206
|
+
|
|
207
|
+
t('no keys (empty array) → BASE only, in declared order', () => {
|
|
208
|
+
assert.deepEqual(access.sparsePathsForScopeKeys([], scopeMap), [...BASE]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
t('one key → BASE + that module’s directory (status-ui → modules/status-ui)', () => {
|
|
212
|
+
const out = access.sparsePathsForScopeKeys(['status-ui'], scopeMap);
|
|
213
|
+
assert.deepEqual(out, [...BASE, 'modules/status-ui']);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
t('cone-filter: dev-box keeps its DIRS, DROPS its file globs (scripts/gds/box.js)', () => {
|
|
217
|
+
const out = access.sparsePathsForScopeKeys(['dev-box'], scopeMap);
|
|
218
|
+
assert.ok(out.includes('modules/dev-box'), 'the module dir is kept');
|
|
219
|
+
assert.ok(out.includes('devbox-app'), 'the app dir is kept');
|
|
220
|
+
assert.ok(!out.some((p) => p.endsWith('.js') || p.endsWith('.sh')), 'no file globs survive the cone filter');
|
|
221
|
+
assert.ok(!out.includes('scripts/gds/box.js'), 'the box CLI file is dropped (BASE scripts covers it)');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
t('cone-filter: the kernel key contributes only its real DIR globs (config, src/module-loader)', () => {
|
|
225
|
+
const out = access.sparsePathsForScopeKeys(['kernel'], scopeMap);
|
|
226
|
+
// kernel is mostly individual src/bongos/*.js FILES (dropped — BASE src/bongos
|
|
227
|
+
// covers them via cone ancestor-inclusion) + a few genuine DIR globs.
|
|
228
|
+
assert.ok(out.includes('config'), 'config/ is a real dir the task needs, not in BASE');
|
|
229
|
+
assert.ok(out.includes('src/module-loader'), 'src/module-loader/ is a real dir not in BASE');
|
|
230
|
+
assert.ok(!out.includes('src/bongos/db.js'), 'the kernel FILE globs are dropped');
|
|
231
|
+
assert.ok(!out.includes('server.js'), 'top-level files never enter the sparse set (cone auto-includes them)');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
t('N keys → union, deduped + sorted; unknown keys contribute nothing', () => {
|
|
235
|
+
const out = access.sparsePathsForScopeKeys(['status-ui', 'hall-ui'], scopeMap);
|
|
236
|
+
assert.deepEqual(out, [...BASE, 'modules/hall-ui', 'modules/status-ui'], 'union of both dirs, sorted');
|
|
237
|
+
assert.deepEqual(access.sparsePathsForScopeKeys(['status-ui', 'status-ui'], scopeMap), [...BASE, 'modules/status-ui'], 'dedup');
|
|
238
|
+
assert.deepEqual(access.sparsePathsForScopeKeys(['bogus'], scopeMap), [...BASE], 'unknown key → no extra paths');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
t('missing/!scopeForModule moduleScopeMap → BASE only (guards the missing map)', () => {
|
|
242
|
+
assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], null), [...BASE]);
|
|
243
|
+
assert.deepEqual(access.sparsePathsForScopeKeys(['dev-box'], {}), [...BASE]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
console.log('\ndecideSourceAccess — the ADR 0148 three-way (rank still gates ALLOW/DENY):');
|
|
247
|
+
|
|
248
|
+
t('scopeKeys present + non-empty → TASK scope, BASE ∪ the module dirs (rank no longer caps breadth)', () => {
|
|
249
|
+
const d = access.decideSourceAccess({ rank: 'xenos', status: 'active', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
|
|
250
|
+
assert.equal(d.allowed, true);
|
|
251
|
+
assert.equal(d.scope, 'task');
|
|
252
|
+
assert.equal(d.spec.mode, 'sparse');
|
|
253
|
+
assert.deepEqual(d.spec.sparsePaths, [...BASE, 'modules/status-ui'], 'even a Xenos with a claim gets the claim’s module scope');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
t('scopeKeys present + EMPTY → BASE-only (a live box with no active claim)', () => {
|
|
257
|
+
const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
|
|
258
|
+
assert.equal(d.allowed, true);
|
|
259
|
+
assert.equal(d.scope, 'base');
|
|
260
|
+
assert.deepEqual(d.spec.sparsePaths, [...BASE], 'idle Metic+ now gets BASE-only, not the whole repo (ADR 0148 flagged change)');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
t('scopeKeys ABSENT → the pre-ADR-0148 RANK fallback is preserved EXACTLY (R93-before-R94 safety)', () => {
|
|
264
|
+
assert.equal(access.decideSourceAccess({ rank: 'metic', status: 'active' }).scope, 'full', 'no claim signal → rank fallback (full)');
|
|
265
|
+
assert.equal(access.decideSourceAccess({ rank: 'xenos', status: 'active' }).scope, 'starter', 'no claim signal → rank fallback (starter)');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
t('the ALLOW/DENY gates run BEFORE scope selection', () => {
|
|
269
|
+
const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
|
|
270
|
+
assert.equal(inactive.allowed, false);
|
|
271
|
+
assert.equal(inactive.reason, 'BUILDER_INACTIVE');
|
|
272
|
+
const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['status-ui'], moduleScopeMap: scopeMap });
|
|
273
|
+
assert.equal(belowFloor.allowed, false);
|
|
274
|
+
assert.equal(belowFloor.reason, 'INSUFFICIENT_RANK');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
console.log('\nscopeMetadata — the credential-FREE projection for GET /box/scope (BV1.R100):');
|
|
278
|
+
|
|
279
|
+
t('task scope → modules deduped+sorted, sparse_paths carried, NO credential fields', () => {
|
|
280
|
+
const scopeKeys = ['dev-box', 'onboarding', 'dev-box']; // dup on purpose
|
|
281
|
+
const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys, moduleScopeMap: scopeMap });
|
|
282
|
+
const meta = access.scopeMetadata({ decision, scopeKeys });
|
|
283
|
+
assert.equal(meta.scope, 'task');
|
|
284
|
+
assert.equal(meta.mode, 'sparse');
|
|
285
|
+
assert.deepEqual(meta.modules, ['dev-box', 'onboarding'], 'deduped + sorted');
|
|
286
|
+
assert.ok(Array.isArray(meta.sparse_paths) && meta.sparse_paths.includes('modules/dev-box'));
|
|
287
|
+
assert.ok(!('credential' in meta) && !('repo_url' in meta), 'never carries credential fields');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
t('empty claims → base scope, modules is [] (not null) — a live box with no active claim', () => {
|
|
291
|
+
const decision = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: [], moduleScopeMap: scopeMap });
|
|
292
|
+
const meta = access.scopeMetadata({ decision, scopeKeys: [] });
|
|
293
|
+
assert.equal(meta.scope, 'base');
|
|
294
|
+
assert.deepEqual(meta.modules, []);
|
|
295
|
+
assert.deepEqual(meta.sparse_paths, [...BASE]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
t('rank fallback (scopeKeys absent) → modules is null (breadth is rank-driven, not per-module)', () => {
|
|
299
|
+
const decision = access.decideSourceAccess({ rank: 'metic', status: 'active' });
|
|
300
|
+
const meta = access.scopeMetadata({ decision, scopeKeys: undefined });
|
|
301
|
+
assert.equal(meta.scope, 'full');
|
|
302
|
+
assert.equal(meta.mode, 'full');
|
|
303
|
+
assert.equal(meta.modules, null);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
t('a DENIED decision projects to null (the route returns 403, never a scope body)', () => {
|
|
307
|
+
const denied = access.decideSourceAccess({ rank: 'xenos', status: 'inactive', scopeKeys: [], moduleScopeMap: scopeMap });
|
|
308
|
+
assert.equal(denied.allowed, false);
|
|
309
|
+
assert.equal(access.scopeMetadata({ decision: denied, scopeKeys: [] }), null);
|
|
310
|
+
assert.equal(access.scopeMetadata({}), null, 'no decision → null');
|
|
311
|
+
assert.equal(access.scopeMetadata(), null, 'no args → null');
|
|
312
|
+
});
|
|
313
|
+
|
|
185
314
|
console.log(`\nbox_access.mjs: ${passed} passed, ${failed} failed`);
|
|
186
315
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// tests/box_credential.mjs
|
|
2
|
+
//
|
|
3
|
+
// R102 (goal 1000051, ADR 0148 C4) — VERIFY the server-side-token posture holds
|
|
4
|
+
// once source scope is CLAIM-based (R92–R94). The security property task-scoping
|
|
5
|
+
// must not weaken: the GitHub credential is server-issued per fetch from the prod
|
|
6
|
+
// ENV (never the repo — ADR 0022), the box never receives a fabricated token, and
|
|
7
|
+
// the claim-scoped ACCESS DECISION carries NO secret — the token is minted
|
|
8
|
+
// SEPARATELY, so changing what the box materializes (sparse paths) never touches
|
|
9
|
+
// the token model.
|
|
10
|
+
//
|
|
11
|
+
// Pure + hermetic: makeEnvCredentialProvider takes an injected env; decideSourceAccess
|
|
12
|
+
// is pure. NO env, NO network, NO box. The DEFERRED hardening (short-lived per-claim
|
|
13
|
+
// credential + hard "can't over-pull" isolation) is documented in ADR 0148 C4 /
|
|
14
|
+
// Consequences — this test pins the posture that ships TODAY.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/box_credential.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const cred = require('../modules/dev-box/box-credential.js');
|
|
23
|
+
const access = require('../modules/dev-box/box-access.js');
|
|
24
|
+
const scopeMap = require('../src/bongos/module-scope-map.js');
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
function t(name, fn) {
|
|
29
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
30
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const REPO = 'https://github.com/acme/portable-core.git';
|
|
34
|
+
const STARTER_REPO = 'https://github.com/acme/starter-surface.git';
|
|
35
|
+
|
|
36
|
+
console.log('box-credential — the token is SERVER-ENV sourced, never fabricated (ADR 0022):');
|
|
37
|
+
|
|
38
|
+
t('no env configured → configured:false + token:null (the box gets guidance, not a token)', () => {
|
|
39
|
+
const p = cred.makeEnvCredentialProvider({});
|
|
40
|
+
for (const scope of ['full', 'starter']) {
|
|
41
|
+
const c = p.issue(scope, { nowMs: 0 });
|
|
42
|
+
assert.equal(c.configured, false, `${scope}: unconfigured`);
|
|
43
|
+
assert.equal(c.token, null, `${scope}: NO token is ever fabricated`);
|
|
44
|
+
assert.equal(c.repoUrl, null, `${scope}: no repo without env`);
|
|
45
|
+
assert.match(c.guidance || '', /env/i, `${scope}: guidance points at the prod env`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
t('repo set but token unset → still configured:false (no partial/empty-token leak)', () => {
|
|
50
|
+
const c = cred.makeEnvCredentialProvider({ BOX_SOURCE_REPO_URL: REPO }).issue('full', { nowMs: 0 });
|
|
51
|
+
assert.equal(c.configured, false);
|
|
52
|
+
assert.equal(c.token, null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
t('token comes STRAIGHT from the env var — and lands ONLY in .token, no other field', () => {
|
|
56
|
+
const p = cred.makeEnvCredentialProvider({
|
|
57
|
+
BOX_SOURCE_REPO_URL: REPO,
|
|
58
|
+
BOX_SOURCE_FULL_TOKEN: 'FULL-SECRET-abc',
|
|
59
|
+
BOX_SOURCE_STARTER_TOKEN: 'STARTER-SECRET-xyz',
|
|
60
|
+
});
|
|
61
|
+
const full = p.issue('full', { nowMs: 0 });
|
|
62
|
+
assert.equal(full.token, 'FULL-SECRET-abc', 'full scope → the FULL env token, verbatim');
|
|
63
|
+
const starter = p.issue('starter', { nowMs: 0 });
|
|
64
|
+
assert.equal(starter.token, 'STARTER-SECRET-xyz', 'starter scope → the STARTER env token, verbatim');
|
|
65
|
+
// The secret must appear in NO field other than `token` (scope/repoUrl/guidance/
|
|
66
|
+
// expiresAt/ttl are safe to log + audit — see the source_grant audit).
|
|
67
|
+
for (const c of [full, starter]) {
|
|
68
|
+
for (const [k, v] of Object.entries(c)) {
|
|
69
|
+
if (k === 'token') continue;
|
|
70
|
+
assert.ok(!String(v).includes(c.token), `secret must not leak into .${k}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
console.log('\nbox-credential — the credential SCOPE is rank-based; scope can never over-reach the repo:');
|
|
76
|
+
|
|
77
|
+
t('a separate starter repo is a HARD boundary: only "full" reaches the full repo', () => {
|
|
78
|
+
const p = cred.makeEnvCredentialProvider({
|
|
79
|
+
BOX_SOURCE_REPO_URL: REPO,
|
|
80
|
+
BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
|
|
81
|
+
BOX_SOURCE_FULL_TOKEN: 'F',
|
|
82
|
+
BOX_SOURCE_STARTER_TOKEN: 'S',
|
|
83
|
+
});
|
|
84
|
+
assert.equal(p.issue('full', { nowMs: 0 }).repoUrl, REPO, 'full → the full repo');
|
|
85
|
+
const starter = p.issue('starter', { nowMs: 0 });
|
|
86
|
+
assert.equal(starter.repoUrl, STARTER_REPO, 'starter → the SEPARATE starter repo, cannot reach the full one');
|
|
87
|
+
assert.equal(starter.separateStarterRepo, true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
t('fail-closed: an UNEXPECTED scope label ("task"/"base") never escalates to the full repo', () => {
|
|
91
|
+
// R94 passes only rank scopes ('starter'|'full') to issue(); this pins the safety
|
|
92
|
+
// margin if a task/base label ever reached it — it takes the NON-full (starter)
|
|
93
|
+
// branch, so on a separate-starter-repo instance it can't reach the crown jewels.
|
|
94
|
+
const p = cred.makeEnvCredentialProvider({
|
|
95
|
+
BOX_SOURCE_REPO_URL: REPO,
|
|
96
|
+
BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
|
|
97
|
+
BOX_SOURCE_FULL_TOKEN: 'F',
|
|
98
|
+
BOX_SOURCE_STARTER_TOKEN: 'S',
|
|
99
|
+
});
|
|
100
|
+
for (const label of ['task', 'base', 'wizard', undefined]) {
|
|
101
|
+
const c = p.issue(label, { nowMs: 0 });
|
|
102
|
+
assert.equal(c.repoUrl, STARTER_REPO, `scope='${label}' must NOT reach the full repo`);
|
|
103
|
+
assert.notEqual(c.token, 'F', `scope='${label}' must NOT get the full-only token`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log('\ntask-scoping decoupling — the claim-scoped ACCESS DECISION carries NO secret:');
|
|
108
|
+
|
|
109
|
+
t('decideSourceAccess returns scope/mode/sparsePaths only — the token is minted separately', () => {
|
|
110
|
+
const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
|
|
111
|
+
assert.equal(d.allowed, true);
|
|
112
|
+
assert.deepEqual(Object.keys(d.spec).sort(), ['mode', 'scope', 'sparsePaths'], 'the spec is scope/mode/sparsePaths — NO credential field');
|
|
113
|
+
const blob = JSON.stringify(d);
|
|
114
|
+
assert.ok(!/token|credential|secret|ghp_|github_pat/i.test(blob), 'the decision blob holds no secret-shaped field');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
t('revocation scrubs at the credential layer: an inactive/below-floor builder is denied BEFORE any credential', () => {
|
|
118
|
+
const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
|
|
119
|
+
assert.equal(inactive.allowed, false, 'inactive → denied (the route never reaches issue())');
|
|
120
|
+
const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
|
|
121
|
+
assert.equal(belowFloor.allowed, false, 'below floor → denied (no credential path)');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
console.log(`\nbox_credential.mjs: ${passed} passed, ${failed} failed`);
|
|
125
|
+
process.exit(failed === 0 ? 0 : 1);
|
package/tests/box_host_keys.mjs
CHANGED
|
@@ -99,4 +99,28 @@ const RSA = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/FqVQUVRAmcDHRBeEZZwuW0xgsAP
|
|
|
99
99
|
assert.equal(r.error, 'expect_type_and_key_only');
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// 11) hostKeysAreStale (task 1002509) — a rebuild mints fresh host keys, and the row
|
|
103
|
+
// keeps the OLD ones until the new box re-reports. Serving those is the "Host denied
|
|
104
|
+
// (verification failed)" lockout, so a report that predates the current droplet's
|
|
105
|
+
// activation must be refused and the client must keyscan instead.
|
|
106
|
+
{
|
|
107
|
+
const { hostKeysAreStale } = require('../modules/dev-box/boxes.js');
|
|
108
|
+
assert.equal(
|
|
109
|
+
hostKeysAreStale({ hostKeysAt: '2026-07-01T08:00:00Z', activeSince: '2026-07-01T09:00:00Z' }),
|
|
110
|
+
true,
|
|
111
|
+
'a report older than the activation describes a box that no longer exists'
|
|
112
|
+
);
|
|
113
|
+
assert.equal(
|
|
114
|
+
hostKeysAreStale({ hostKeysAt: '2026-07-01T10:00:00Z', activeSince: '2026-07-01T09:00:00Z' }),
|
|
115
|
+
false,
|
|
116
|
+
'a report from after this box came up is the live identity'
|
|
117
|
+
);
|
|
118
|
+
assert.equal(hostKeysAreStale({ hostKeysAt: null, activeSince: '2026-07-01T09:00:00Z' }), true);
|
|
119
|
+
assert.equal(hostKeysAreStale({ hostKeysAt: '2026-07-01T10:00:00Z', activeSince: null }), true);
|
|
120
|
+
assert.equal(hostKeysAreStale({}), true, 'no evidence of freshness → not fresh');
|
|
121
|
+
assert.equal(hostKeysAreStale({ hostKeysAt: 'garbage', activeSince: '2026-07-01T09:00:00Z' }), true);
|
|
122
|
+
const t = new Date('2026-07-01T09:00:00Z');
|
|
123
|
+
assert.equal(hostKeysAreStale({ hostKeysAt: t, activeSince: t }), false, 'Date objects, equal instant → fresh');
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
console.log('box_host_keys: all assertions passed');
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// tests/box_infra_resolve.mjs
|
|
2
|
+
//
|
|
3
|
+
// R98 (goal 1000051/1000052, ADR 0150) — box.js locates the box cloud-init from
|
|
4
|
+
// the INSTANCE root (not the vendored @bongos/core package) and FAILS LOUD when it
|
|
5
|
+
// cannot, so a box can never be provisioned with empty user-data (= a bare box that
|
|
6
|
+
// boots with no clone, no toolchain, no terminal).
|
|
7
|
+
//
|
|
8
|
+
// Pure + hermetic: boxInfraCandidates takes injected {env, repoRoot}; readCloudInit
|
|
9
|
+
// takes an explicit infraDir. Temp dirs stand in for a real infra/ tree. NO DB, NO
|
|
10
|
+
// network, NO droplet — requiring box.js does not run main() (require.main guard)
|
|
11
|
+
// and loadDeps() (the DB layer) is only reached inside main().
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/box_infra_resolve.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { makeRunner } from './helpers.mjs';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const box = require('../scripts/gds/box.js');
|
|
24
|
+
|
|
25
|
+
const { test, summary } = makeRunner();
|
|
26
|
+
|
|
27
|
+
// The 9 scripts readCloudInit gz+b64-inlines into the cloud-init — missing ANY of
|
|
28
|
+
// them half-wires the box. Kept in sync with box.js's fileGzB64 calls.
|
|
29
|
+
const INLINED = [
|
|
30
|
+
'box-source-fetch.sh', 'box-authorized-keys-fetch.sh', 'box-terminal.service',
|
|
31
|
+
'box-terminal-credsync.service', 'box-terminal-credential.path', 'cloudflared.service',
|
|
32
|
+
'cloudflared-run.sh', 'close-box.sh', 'otb-box-launch.sh',
|
|
33
|
+
];
|
|
34
|
+
const TEMPLATE = box.BOX_INFRA_TEMPLATE; // 'builder-box-cloud-init.yaml'
|
|
35
|
+
|
|
36
|
+
// A minimal template exercising a few of the substitution tokens (readCloudInit
|
|
37
|
+
// still calls fileGzB64 for ALL 9 inlined files regardless, so the happy path must
|
|
38
|
+
// provide every one).
|
|
39
|
+
const TEMPLATE_BODY = [
|
|
40
|
+
'#cloud-config',
|
|
41
|
+
'write_files:',
|
|
42
|
+
' - path: /opt/box-source-fetch.sh',
|
|
43
|
+
' encoding: gz+b64',
|
|
44
|
+
' content: __BOX_SOURCE_FETCH_B64__',
|
|
45
|
+
' - path: /opt/cloudflared-run.sh',
|
|
46
|
+
' encoding: gz+b64',
|
|
47
|
+
' content: __CLOUDFLARED_RUN_B64__',
|
|
48
|
+
'runcmd:',
|
|
49
|
+
' - git clone __REPO_URL__ /workspace',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n');
|
|
52
|
+
|
|
53
|
+
const tmps = [];
|
|
54
|
+
function tmpdir() {
|
|
55
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'boxinfra-'));
|
|
56
|
+
tmps.push(d);
|
|
57
|
+
return d;
|
|
58
|
+
}
|
|
59
|
+
// Build an infra/ dir; choose whether the template + which inlined files exist.
|
|
60
|
+
function makeInfra({ template = true, inlined = INLINED } = {}) {
|
|
61
|
+
const root = tmpdir();
|
|
62
|
+
const infra = path.join(root, 'infra');
|
|
63
|
+
fs.mkdirSync(infra, { recursive: true });
|
|
64
|
+
if (template) fs.writeFileSync(path.join(infra, TEMPLATE), TEMPLATE_BODY);
|
|
65
|
+
for (const f of inlined) fs.writeFileSync(path.join(infra, f), `# ${f}\n`);
|
|
66
|
+
return { root, infra };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ===========================================================================
|
|
70
|
+
// boxInfraCandidates — ordering (pure, injected; ADR 0150 prong A)
|
|
71
|
+
// ===========================================================================
|
|
72
|
+
await test('candidate order: override → explicit instance root → repoRoot → node_modules walk-up', () => {
|
|
73
|
+
const env = { CLOUDBONGOS_BOX_INFRA_DIR: '/ovr', CLOUDBONGOS_INSTANCE_ROOT: '/inst' };
|
|
74
|
+
const c = box.boxInfraCandidates({ env, repoRoot: '/srv/app/node_modules/@bongos/core' });
|
|
75
|
+
assert.equal(c[0], path.resolve('/ovr'), 'explicit override is first');
|
|
76
|
+
assert.equal(c[1], path.join('/inst', 'infra'), 'then the explicit instance root');
|
|
77
|
+
assert.equal(c[2], '/srv/app/node_modules/@bongos/core/infra', 'then REPO_ROOT/infra');
|
|
78
|
+
// the load-bearing case: a vendored core walks UP out of node_modules to its host
|
|
79
|
+
// instance root, so infra/ is found with no pointer at all.
|
|
80
|
+
assert.ok(c.includes('/srv/app/infra'), 'walk-up recovers the host instance root infra/');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await test('repoRoot/infra precedes its ancestors in the walk-up (robust to any cwd pointer)', () => {
|
|
84
|
+
const c = box.boxInfraCandidates({ env: {}, repoRoot: '/x/y/z' });
|
|
85
|
+
const i0 = c.indexOf('/x/y/z/infra');
|
|
86
|
+
const i1 = c.indexOf('/x/y/infra');
|
|
87
|
+
const i2 = c.indexOf('/x/infra');
|
|
88
|
+
assert.ok(i0 >= 0 && i1 > i0 && i2 > i1, 'repoRoot/infra, then /x/y/infra, then /x/infra — in order');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ===========================================================================
|
|
92
|
+
// resolveBoxInfraDir — picks the FIRST candidate that actually holds the template
|
|
93
|
+
// ===========================================================================
|
|
94
|
+
await test('resolveBoxInfraDir picks the override dir when it holds the template', () => {
|
|
95
|
+
const { infra } = makeInfra();
|
|
96
|
+
const got = box.resolveBoxInfraDir({ env: { CLOUDBONGOS_BOX_INFRA_DIR: infra }, repoRoot: '/nope/does/not/exist' });
|
|
97
|
+
assert.equal(got, infra);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await test('resolveBoxInfraDir falls THROUGH a template-less override to the instance-root infra/', () => {
|
|
101
|
+
const emptyOverride = path.join(tmpdir(), 'infra');
|
|
102
|
+
fs.mkdirSync(emptyOverride, { recursive: true }); // exists, but no template inside
|
|
103
|
+
const { infra } = makeInfra(); // a real infra/ to fall through to
|
|
104
|
+
const instRoot = path.dirname(infra); // <root>, so <root>/infra === the real one
|
|
105
|
+
const got = box.resolveBoxInfraDir({
|
|
106
|
+
env: { CLOUDBONGOS_BOX_INFRA_DIR: emptyOverride, CLOUDBONGOS_INSTANCE_ROOT: instRoot },
|
|
107
|
+
repoRoot: '/nope',
|
|
108
|
+
});
|
|
109
|
+
assert.equal(got, infra, 'skips the template-less override, takes the instance-root infra/');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await test('resolveBoxInfraDir → null when NO candidate holds the template', () => {
|
|
113
|
+
const bare = tmpdir(); // a dir tree with no infra/ anywhere
|
|
114
|
+
const got = box.resolveBoxInfraDir({ env: {}, repoRoot: path.join(bare, 'node_modules', '@bongos', 'core') });
|
|
115
|
+
assert.equal(got, null);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ===========================================================================
|
|
119
|
+
// readCloudInit — FAIL LOUD (ADR 0150 prong B): never return null / empty
|
|
120
|
+
// ===========================================================================
|
|
121
|
+
await test('readCloudInit THROWS when infra cannot be located (never returns null)', () => {
|
|
122
|
+
assert.throws(
|
|
123
|
+
() => box.readCloudInit({ infraDir: null }),
|
|
124
|
+
/not found|boot bare|ADR 0150/i,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await test('readCloudInit THROWS when infra is INCOMPLETE (an inlined script is missing)', () => {
|
|
129
|
+
const { infra } = makeInfra({ inlined: INLINED.filter((f) => f !== 'cloudflared-run.sh') });
|
|
130
|
+
assert.throws(
|
|
131
|
+
() => box.readCloudInit({ infraDir: infra }),
|
|
132
|
+
/incomplete|cloudflared-run\.sh|ADR 0150/i,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ===========================================================================
|
|
137
|
+
// readCloudInit — happy path: renders non-empty + every placeholder substituted
|
|
138
|
+
// ===========================================================================
|
|
139
|
+
await test('readCloudInit renders a non-empty cloud-init with no leftover __PLACEHOLDER__ tokens', () => {
|
|
140
|
+
const { infra } = makeInfra();
|
|
141
|
+
const out = box.readCloudInit({
|
|
142
|
+
infraDir: infra, authorizedKeysBlock: 'ssh-ed25519 AAAA', boxEnv: 'X=1', gdsSession: '{"t":1}',
|
|
143
|
+
});
|
|
144
|
+
assert.ok(out && out.length > 0, 'renders a non-empty cloud-init');
|
|
145
|
+
assert.ok(!/__[A-Z0-9_]+__/.test(out), 'no unsubstituted __PLACEHOLDER__ tokens remain');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
for (const d of tmps) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
149
|
+
summary();
|
|
@@ -45,11 +45,15 @@ const t = async (name, fn) => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
// ---- allowBoxScope middleware ------------------------------------------------
|
|
48
|
-
|
|
48
|
+
// BV1.R99: the opt-in is no longer a bare boolean flag — it DECLARES the box
|
|
49
|
+
// scope's own permission allow-list, and requireBuilder checks the declaration
|
|
50
|
+
// against the registry. The reachability contract below is unchanged.
|
|
51
|
+
await t('allowBoxScope declares the box scope\'s allow-list and calls next()', async () => {
|
|
49
52
|
const req = {};
|
|
50
53
|
let nexted = false;
|
|
51
54
|
auth.allowBoxScope(req, fakeRes(), () => { nexted = true; });
|
|
52
|
-
assert.
|
|
55
|
+
assert.deepEqual([...req.sessionScopeNeeds], [...auth.sessionScopeFor('box').permissions]);
|
|
56
|
+
assert.ok(req.sessionScopeNeeds.length > 0, 'an empty declaration would deny every box route');
|
|
53
57
|
assert.equal(nexted, true);
|
|
54
58
|
});
|
|
55
59
|
|