@bongos/core 1.19.1073 → 1.19.1075
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 +404 -549
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/bin/bongos.js +1 -3
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -60
- package/clients/bongos-client/index.cjs +4 -60
- package/clients/bongos-client/index.d.ts +5 -90
- package/clients/bongos-client/index.mjs +4 -60
- package/config/branding.neutral.json +1 -5
- package/config/modules.neutral.json +2 -3
- package/config/scheduled-routines.json +0 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
- package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +175 -1665
- package/docs/api-reference.md +9 -41
- package/docs/architecture.md +16 -1
- package/docs/branding-contract.md +0 -3
- package/docs/copy-inventory.md +454 -479
- package/docs/copy-registry.json +746 -978
- package/docs/file-map.md +5 -34
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +13 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/diagrams/assertions.json +2 -22
- package/docs/onboarding/primer.md +9 -28
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +1032 -1077
- package/docs/recipes/render-pilot.md +29 -10
- package/migrations/core_257_module_store_registry.sql +87 -0
- package/migrations/core_258_drop_dev_box_tables.sql +55 -0
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/builder-settings/render-prefs.js +4 -5
- package/modules/copy-desk/docx.js +78 -5
- package/modules/copy-desk/module.json +1 -0
- package/modules/copy-desk/routes/copy-desk.js +10 -10
- package/modules/copy-desk/tests/copy_docx.mjs +107 -1
- package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
- package/modules/discord/module.json +1 -0
- package/modules/discord/ship-broadcast.js +3 -3
- package/modules/economy/credits.js +2 -2
- package/modules/economy/module.json +1 -0
- package/modules/economy/routes/credits.js +1 -2
- package/modules/government/catalog.js +13 -17
- package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
- package/modules/government/module.json +1 -0
- package/modules/government/protected-surfaces.json +3 -11
- package/modules/government/resolver.js +3 -46
- package/modules/government/routes/government.js +0 -2
- package/modules/government/session-scopes.js +43 -83
- package/modules/government/session-scopes.json +8 -18
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/board-lib.js +2 -2
- package/modules/hall-ui/public/builders.js +29 -40
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/gate.states.json +1 -1
- package/modules/hall-ui/public/hall-render.js +43 -79
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.js +1 -0
- package/modules/hall-ui/public/oversight.css +5 -5
- package/modules/hall-ui/public/palette.js +0 -11
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-sessions.js +1 -1
- package/modules/hall-ui/public/settings.css +0 -1
- package/modules/hall-ui/public/settings.html +4 -33
- package/modules/hall-ui/public/settings.js +3 -33
- package/modules/hall-ui/public/settings.states.json +2 -2
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +10 -13
- package/modules/hall-ui/public/task.html +4 -1
- package/modules/hall-ui/public/task.js +36 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
- package/modules/hall-ui/public/tweak-editor.css +16 -0
- package/modules/hall-ui/public/tweak-editor.html +26 -3
- package/modules/hall-ui/public/tweak-editor.js +32 -1
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -0
- package/modules/ideas/module.json +1 -0
- package/modules/ideas/projection.js +1 -1
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +4 -9
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/github-push.js +12 -14
- package/modules/lifecycle/kickoff-checklist.js +0 -4
- package/modules/lifecycle/lifecycle.js +0 -8
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/publish-reconciler.js +10 -10
- package/modules/lifecycle/routes/tasks.js +13 -15
- package/modules/lifecycle/ship-card.js +1 -1
- package/modules/lifecycle/task-visuals.js +2 -2
- package/modules/memory/module.json +1 -0
- package/modules/memory/routes/memory.js +2 -2
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/onboarding/onboarding-state.js +17 -36
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/module.json +1 -0
- package/modules/platform-identity/platform-identity.js +13 -5
- package/modules/platform-identity/routes/sso.js +5 -4
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/capacity.js +1 -1
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +6 -5
- package/modules/provisioning/routes/provisioning.js +4 -4
- package/modules/provisioning/starter-bundles.js +5 -25
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +101 -32
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +4 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/security/module.json +1 -0
- package/modules/sessions/db.js +1 -1
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +84 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/artifact-staleness.js +2 -2
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/backfill-task-rewards.js +5 -5
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/claude-materialize.js +2 -2
- package/scripts/gds/cli-lib.js +15 -15
- package/scripts/gds/codemod-rename-src-gds.js +1 -1
- package/scripts/gds/context-pack.js +5 -7
- package/scripts/gds/diagram-facts.js +12 -23
- package/scripts/gds/do-api.js +13 -80
- package/scripts/gds/doc-cli-guard.js +2 -0
- package/scripts/gds/doctor.js +4 -4
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-packaging.js +4 -4
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +5 -5
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-api-client.js +4 -4
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/gen-repo-map.js +13 -14
- package/scripts/gds/gen-session-index.js +8 -8
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/leak-scan-allowlist.js +1 -1
- package/scripts/gds/lint-infra-exec.js +1 -1
- package/scripts/gds/local-preview-lib.js +8 -39
- package/scripts/gds/local-preview.js +2 -15
- package/scripts/gds/migration-namespace.js +3 -3
- package/scripts/gds/module-artifact.js +212 -0
- package/scripts/gds/module.js +95 -6
- package/scripts/gds/package-core.js +5 -116
- package/scripts/gds/plain-cards.js +1 -1
- package/scripts/gds/provision-config.js +2 -2
- package/scripts/gds/provision-net.js +2 -3
- package/scripts/gds/provision.js +9 -9
- package/scripts/gds/publish-manifest.js +1 -2
- package/scripts/gds/push-path-brief.js +10 -10
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/rename-history-check.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/session-digest-build.js +2 -2
- package/scripts/gds/ship-deploy-target.js +13 -13
- package/scripts/gds/ship-flow.js +7 -7
- package/scripts/gds/ship-land.js +9 -10
- package/scripts/gds/ship-merge.js +4 -4
- package/scripts/gds/ship-preflight-steps.js +4 -4
- package/scripts/gds/ship-regen.js +15 -49
- package/scripts/gds/ship.js +13 -33
- package/scripts/gds/skill-preflight.js +9 -8
- package/scripts/gds/start.js +0 -15
- package/scripts/hall-preview/README.md +5 -5
- package/scripts/hall-preview/server.js +6 -6
- package/scripts/render-diagrams.sh +1 -1
- package/src/bongos/api-errors.js +1 -1
- package/src/bongos/api-prefix.js +4 -4
- package/src/bongos/auth-github.js +2 -3
- package/src/bongos/auth.js +51 -126
- package/src/bongos/db-kernel.js +2 -3
- package/src/bongos/db.js +5 -28
- package/src/bongos/module-scope-map.js +14 -29
- package/src/bongos/module-store.js +141 -0
- package/src/bongos/platform-visibility-gate.js +1 -1
- package/src/bongos/route-rank-check.js +3 -16
- package/src/bongos/routes/auth.js +0 -116
- package/src/bongos/routes/builders.js +5 -15
- package/src/bongos/routes/instance.js +8 -14
- package/src/bongos/routes/me.js +7 -23
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/bongos/routes.js +7 -2
- package/src/bongos/serve-internal.js +5 -6
- package/src/bongos-downloads.js +1 -6
- package/src/branding.js +2 -2
- package/src/build-info.js +4 -6
- package/src/instance-config.js +2 -2
- package/src/module-api.js +13 -6
- package/src/module-loader/catalog.js +3 -0
- package/src/module-loader/loader.js +4 -4
- package/src/module-loader/manifest-schema.js +16 -8
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +1 -1
- package/tests/api_alias_exceptions.mjs +1 -1
- package/tests/api_client.mjs +1 -16
- package/tests/api_path_404.mjs +1 -1
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/backfill_task_rewards.mjs +1 -1
- package/tests/canonical_profile_url.mjs +1 -3
- package/tests/claim_action.mjs +4 -1
- package/tests/claim_from_task_and_home.mjs +127 -0
- package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
- package/tests/cli_sessions.mjs +1 -1
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/context_pack.mjs +13 -13
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/copy_inventory.mjs +7 -7
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/credit_grant.mjs +2 -2
- package/tests/criterion_suggest.mjs +1 -1
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/design_tokens_sync.mjs +3 -2
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/do_api.mjs +106 -0
- package/tests/feedback_latest.mjs +122 -0
- package/tests/fitness.mjs +44 -42
- package/tests/fitness_ratchets.mjs +2 -2
- package/tests/gate_approvals.mjs +0 -1
- package/tests/github_push_land_proof.mjs +1 -1
- package/tests/go_live.mjs +3 -3
- package/tests/goal_advisory.mjs +3 -3
- package/tests/goal_suggest.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +21 -19
- package/tests/government_ownership_scope.mjs +33 -35
- package/tests/government_parity.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +56 -37
- package/tests/government_require_permission.mjs +3 -3
- package/tests/government_seed.mjs +37 -4
- package/tests/government_session_scope.mjs +92 -366
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_error_envelope.mjs +4 -0
- package/tests/hall_landing_boot.mjs +4 -1
- package/tests/hall_palette.mjs +1 -16
- package/tests/hall_record_world.mjs +6 -0
- package/tests/hall_tweak_editor.mjs +52 -1
- package/tests/helpers.mjs +1 -1
- package/tests/host_topology_skips.mjs +3 -4
- package/tests/html_comment_nesting.mjs +137 -0
- package/tests/infra_exec_paths.mjs +9 -10
- package/tests/init.mjs +4 -5
- package/tests/instance_manifest.mjs +18 -25
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +0 -221
- package/tests/local_preview.mjs +0 -12
- package/tests/migration_namespace.mjs +6 -6
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_api.mjs +2 -2
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +20 -20
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +26 -17
- package/tests/module_route_rank.mjs +2 -2
- package/tests/module_store_publish.mjs +307 -0
- package/tests/module_store_publish_route.mjs +121 -0
- package/tests/module_store_registry_migration.mjs +70 -0
- package/tests/modules.mjs +56 -26
- package/tests/modules_route_wiring.mjs +1 -1
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/paste_token_session_store.mjs +1 -1
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/platform_visibility_gate.mjs +1 -1
- package/tests/profile_route.mjs +1 -6
- package/tests/project_modules_ui.mjs +17 -17
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +77 -76
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_capacity.mjs +1 -1
- package/tests/provisioning_recommendations.mjs +2 -1
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +11 -19
- package/tests/publish_manifest.mjs +2 -3
- package/tests/publish_reconciler.mjs +8 -8
- package/tests/push_path_brief.mjs +10 -9
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/regrade_eligibility.mjs +1 -1
- package/tests/rename_history_restraint.mjs +1 -1
- package/tests/repo_map.mjs +12 -13
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/seam_wiring_guard.mjs +11 -11
- package/tests/search_isolation.mjs +3 -3
- package/tests/session_records.mjs +1 -1
- package/tests/session_rename_fallback.mjs +1 -22
- package/tests/session_start_freshness.mjs +3 -3
- package/tests/session_token_hash_db.mjs +1 -1
- package/tests/ship_card.mjs +1 -1
- package/tests/ship_ci_deploy.mjs +17 -17
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/ship_premerge.mjs +1 -1
- package/tests/ship_resume.mjs +1 -1
- package/tests/skill_preflight.mjs +2 -2
- package/tests/skip_is_not_pass.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +2 -2
- package/tests/upgrade.mjs +4 -4
- package/tests/watch_sealed_floor.mjs +0 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +21 -10
- package/tests/wizard_preselect_why.mjs +18 -18
- package/docs/onboarding/browser-terminal-guide.md +0 -73
- package/docs/recipes/managed-settings-remote-control.md +0 -416
- package/modules/dev-box/CLAUDE.md +0 -15
- package/modules/dev-box/box-access.js +0 -623
- package/modules/dev-box/box-credential.js +0 -94
- package/modules/dev-box/box-onboard.js +0 -486
- package/modules/dev-box/boxes.js +0 -1038
- package/modules/dev-box/db.js +0 -39
- package/modules/dev-box/module.json +0 -19
- package/modules/dev-box/routes/box.js +0 -1223
- package/scripts/gds/box-auth-check.js +0 -160
- package/scripts/gds/box-infra.js +0 -321
- package/scripts/gds/box-sync.js +0 -216
- package/scripts/gds/box.js +0 -1332
- package/scripts/gds/cf-tunnel.js +0 -558
- package/scripts/gds/smoke-box.sh +0 -150
- package/scripts/gds/tree-preflight.js +0 -241
- package/src/bongos/app-pair.js +0 -172
- package/tests/app_pair.mjs +0 -168
- package/tests/box_access.mjs +0 -578
- package/tests/box_auth_check.mjs +0 -114
- package/tests/box_code_staleness.mjs +0 -129
- package/tests/box_connect_e2e.mjs +0 -196
- package/tests/box_cost.mjs +0 -70
- package/tests/box_credential.mjs +0 -125
- package/tests/box_dns_repoint.mjs +0 -153
- package/tests/box_env.mjs +0 -64
- package/tests/box_host_keys.mjs +0 -126
- package/tests/box_infra_resolve.mjs +0 -246
- package/tests/box_onboard.mjs +0 -317
- package/tests/box_scope_session.mjs +0 -119
- package/tests/box_sweep_docs.mjs +0 -119
- package/tests/box_sync.mjs +0 -55
- package/tests/box_sync_scope_report.mjs +0 -126
- package/tests/box_task_scope.mjs +0 -169
- package/tests/box_task_scope_pause.mjs +0 -66
- package/tests/box_terminal_ssrf.mjs +0 -96
- package/tests/boxes.mjs +0 -1585
- package/tests/cf_ruleset_preflight.mjs +0 -142
- package/tests/cf_tunnel.mjs +0 -496
- package/tests/docs_scrubber_damage.mjs +0 -111
- package/tests/module_scope_active_claims.mjs +0 -94
- package/tests/ship_api_client_pathspec.mjs +0 -100
- package/tests/tree_preflight.mjs +0 -243
package/src/bongos/api-errors.js
CHANGED
|
@@ -61,7 +61,7 @@ const OVERRIDES = Object.freeze({
|
|
|
61
61
|
unauthenticated: 'unauthenticated',
|
|
62
62
|
cookie_required: 'unauthenticated',
|
|
63
63
|
unknown_or_expired: 'unauthenticated',
|
|
64
|
-
|
|
64
|
+
session_scope: 'forbidden',
|
|
65
65
|
|
|
66
66
|
// --- forbidden: 403 (rank/ownership, not "missing" or "bad input") ---
|
|
67
67
|
forbidden: 'forbidden',
|
package/src/bongos/api-prefix.js
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
//
|
|
10
10
|
// The URL rename (Part B / B1, task 1919) flipped the canonical here: the API is
|
|
11
11
|
// now served at '/api/bongos', and '/api/gds' stays mounted FOREVER as a silent
|
|
12
|
-
// alias (LEGACY_API_PREFIXES). "Forever" is deliberate — shipped
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// alias (LEGACY_API_PREFIXES). "Forever" is deliberate — shipped older CLI
|
|
13
|
+
// releases and the separately-hosted status-dashboard mirror all call '/api/gds'
|
|
14
|
+
// and can't be force-updated. serve-internal.js mounts the router + discovery
|
|
15
|
+
// index at API_PREFIX and every LEGACY_API_PREFIXES entry.
|
|
16
16
|
const API_PREFIX = '/api/bongos';
|
|
17
17
|
|
|
18
18
|
// Prefixes the server ALSO answers on, for callers pinned to the previous path.
|
|
@@ -253,9 +253,8 @@ async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken
|
|
|
253
253
|
// Approval gate (2026-05-30): the web hall sign-in no longer admits anyone
|
|
254
254
|
// with a GitHub account. A first-time visitor must have been approved by an
|
|
255
255
|
// Archon (their access_request marked 'invited') before a builder row is
|
|
256
|
-
// created. This Archon approval is the ONLY gate
|
|
257
|
-
//
|
|
258
|
-
// (2026-06-07). Established builders (a row already exists for this github_id)
|
|
256
|
+
// created. This Archon approval is the ONLY gate sign-in applies — there is
|
|
257
|
+
// no GitHub repo-access / collaborator check in it (2026-06-07). Established builders (a row already exists for this github_id)
|
|
259
258
|
// always pass — this only blocks NEW first-time entries.
|
|
260
259
|
//
|
|
261
260
|
// SR-19 / task #1000 (2026-06-13): the CLI device flow (pollDeviceFlow,
|
package/src/bongos/auth.js
CHANGED
|
@@ -128,47 +128,35 @@ async function resolveSession(req) {
|
|
|
128
128
|
return resolved;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// ── SESSION
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// (
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// against the permission catalog at load by modules/government/session-scopes.js.
|
|
158
|
-
// Core reads it as DATA with fs — never `require` (ADR 0083's one-way rule: a
|
|
159
|
-
// module's code and enable/disable state must never be something the kernel
|
|
160
|
-
// depends on). The sibling trust primitive permission-path-check.js reads the
|
|
161
|
-
// protected-surface registry exactly this way (R101). Reading it statically rather
|
|
162
|
-
// than through the `governance` kernel port is the deliberate call: a request must
|
|
163
|
-
// not need a module to be mounted to stay deny-by-default, and the alternative
|
|
164
|
-
// fails badly in both directions — denying on an absent port would take every
|
|
165
|
-
// cli/web session down, admitting would hand a box token full authority.
|
|
131
|
+
// ── SESSION SOURCES — which kinds of token may authenticate at all (BV1.R99) ──
|
|
132
|
+
//
|
|
133
|
+
// Every builder_sessions row carries a `source` saying where it was minted. The
|
|
134
|
+
// session-scope registry (modules/government/session-scopes.json) DECLARES the
|
|
135
|
+
// sources this instance accepts; a session whose source it does not declare is
|
|
136
|
+
// DENIED — by requireBuilder on every route and by runPageGate on every page —
|
|
137
|
+
// rather than defaulting to its builder's full authority. A null/absent source is
|
|
138
|
+
// a DECLARED row (sessions minted before migration 090 added the column), not an
|
|
139
|
+
// implicit default.
|
|
140
|
+
//
|
|
141
|
+
// Every declared source is UNSCOPED: it carries its builder's full rank authority.
|
|
142
|
+
// The registry used to be able to declare a NARROWING scope too — a
|
|
143
|
+
// deny-by-default allow-list for a token living outside the ADR 0016 trust
|
|
144
|
+
// boundary — and its only user was a machine-baked token (ADR 0053) that goal
|
|
145
|
+
// 1000120 retired. With no other narrowing scope the allow-list machinery went
|
|
146
|
+
// with it (task 1003894). The parser therefore REFUSES a row that declares an
|
|
147
|
+
// allow-list: without the enforcement behind it, such a row would read as a
|
|
148
|
+
// restriction while granting full authority. A session row still carrying a
|
|
149
|
+
// retired source simply fails the "declared?" test and is denied.
|
|
150
|
+
//
|
|
151
|
+
// GOVERNANCE OWNS THE LIST, THE KERNEL STAYS THE ENFORCER. Core reads the registry
|
|
152
|
+
// as DATA with fs — never `require` (ADR 0083's one-way rule: a module's code and
|
|
153
|
+
// enable/disable state must never be something the kernel depends on), exactly as
|
|
154
|
+
// permission-path-check.js reads the protected-surface registry (R101). A request
|
|
155
|
+
// must not need a module mounted to stay fail-closed: denying on an absent port
|
|
156
|
+
// would take every cli/web session down.
|
|
166
157
|
//
|
|
167
158
|
// FAIL-CLOSED both ways: an unreadable or malformed registry THROWS (it never
|
|
168
|
-
// degrades to "every
|
|
169
|
-
// registry does not declare is DENIED rather than defaulting to full authority. A
|
|
170
|
-
// null/absent source is a DECLARED row (rows minted before migration 090 added the
|
|
171
|
-
// column, which predate the 'box' source existing), not an implicit default.
|
|
159
|
+
// degrades to "every source is accepted"), and an undeclared source is denied.
|
|
172
160
|
const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules', 'government', 'session-scopes.json');
|
|
173
161
|
|
|
174
162
|
// Pure: validate + normalize the parsed registry. Split from the file read so the
|
|
@@ -177,31 +165,21 @@ const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules',
|
|
|
177
165
|
function parseSessionScopes(raw, label = 'session-scope registry') {
|
|
178
166
|
const rows = Array.isArray(raw && raw.scopes) ? raw.scopes : null;
|
|
179
167
|
if (!rows || rows.length === 0) {
|
|
180
|
-
throw new Error(`${label} declares no session
|
|
168
|
+
throw new Error(`${label} declares no session sources. Refusing to run with no session-source model (ADR 0151).`);
|
|
181
169
|
}
|
|
182
170
|
const bySource = new Map();
|
|
183
171
|
for (const row of rows) {
|
|
184
172
|
const source = row && row.source !== undefined ? row.source : null;
|
|
185
173
|
if (source !== null && (typeof source !== 'string' || !source)) {
|
|
186
|
-
throw new Error(`${label}: every
|
|
174
|
+
throw new Error(`${label}: every row needs a non-empty session source string, or null for the legacy sourceless row`);
|
|
187
175
|
}
|
|
188
176
|
if (bySource.has(source)) throw new Error(`${label}: duplicate session source "${source}"`);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// unscoped XOR an allow-list. Both would be ambiguous (does it narrow?);
|
|
192
|
-
// neither would be a scope that can never admit anything — an authoring slip
|
|
193
|
-
// either way, and both must fail loud rather than pick a default.
|
|
194
|
-
if (unscoped === (permissions.length > 0)) {
|
|
195
|
-
throw new Error(`${label}: "${source}" must declare EITHER unscoped:true OR a non-empty permissions allow-list, never both/neither`);
|
|
196
|
-
}
|
|
197
|
-
if (permissions.some((key) => typeof key !== 'string' || !key)) {
|
|
198
|
-
throw new Error(`${label}: "${source}" lists a non-string permission key`);
|
|
177
|
+
if (row && (row.permissions !== undefined || row.unscoped !== undefined)) {
|
|
178
|
+
throw new Error(`${label}: "${source}" declares a scope (permissions/unscoped). Narrowing session scopes were removed (task 1003894); a row may only declare a source that carries its builder's full authority.`);
|
|
199
179
|
}
|
|
200
180
|
bySource.set(source, Object.freeze({
|
|
201
181
|
source,
|
|
202
182
|
label: row && typeof row.label === 'string' ? row.label : null,
|
|
203
|
-
unscoped,
|
|
204
|
-
permissions: Object.freeze(permissions),
|
|
205
183
|
}));
|
|
206
184
|
}
|
|
207
185
|
return bySource;
|
|
@@ -212,7 +190,7 @@ function loadSessionScopes(file) {
|
|
|
212
190
|
try {
|
|
213
191
|
raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
214
192
|
} catch (e) {
|
|
215
|
-
throw new Error(`session-scope registry unreadable at ${file}: ${e.message}. Refusing to run with no session-
|
|
193
|
+
throw new Error(`session-scope registry unreadable at ${file}: ${e.message}. Refusing to run with no session-source model (ADR 0151).`);
|
|
216
194
|
}
|
|
217
195
|
return parseSessionScopes(raw, `session-scope registry ${file}`);
|
|
218
196
|
}
|
|
@@ -220,7 +198,7 @@ function loadSessionScopes(file) {
|
|
|
220
198
|
const _sessionScopes = loadSessionScopes(SESSION_SCOPE_REGISTRY_PATH);
|
|
221
199
|
const SESSION_SCOPES = Object.freeze([..._sessionScopes.values()]);
|
|
222
200
|
|
|
223
|
-
// The
|
|
201
|
+
// The declared row for a session's `source`, or null when the registry does not
|
|
224
202
|
// declare it (⇒ deny; never "full").
|
|
225
203
|
function sessionScopeFor(source) {
|
|
226
204
|
const key = source === undefined ? null : source;
|
|
@@ -228,39 +206,6 @@ function sessionScopeFor(source) {
|
|
|
228
206
|
return _sessionScopes.get(key) || null;
|
|
229
207
|
}
|
|
230
208
|
|
|
231
|
-
// Route reachability, deny-by-default. An unscoped session reaches everything its
|
|
232
|
-
// builder's gates admit; a SCOPED session reaches only a route that declared
|
|
233
|
-
// capability its allow-list names — so a route that declares nothing (every route
|
|
234
|
-
// that has not opted in, including any added later) is out of reach.
|
|
235
|
-
function scopeCovers(scope, declared) {
|
|
236
|
-
if (!scope) return false; // unrecognised source
|
|
237
|
-
if (scope.unscoped) return true;
|
|
238
|
-
const needs = Array.isArray(declared) ? declared : [];
|
|
239
|
-
return needs.length > 0 && needs.every((key) => scope.permissions.includes(key));
|
|
240
|
-
}
|
|
241
|
-
function sessionScopeAdmits(source, declared) {
|
|
242
|
-
return scopeCovers(sessionScopeFor(source), declared);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// The route-side DECLARATION: "reaching this route needs these capabilities from
|
|
246
|
-
// the session's scope." Mount it BEFORE requireBuilder. Inert for an unscoped
|
|
247
|
-
// session — requireBuilder compares the declaration only against a narrowing scope.
|
|
248
|
-
function sessionScope(...keys) {
|
|
249
|
-
const needs = Object.freeze([...keys]);
|
|
250
|
-
return function declareSessionScope(req, _res, next) {
|
|
251
|
-
req.sessionScopeNeeds = needs;
|
|
252
|
-
next();
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// The dev-box bootstrap declaration (#919) — the name modules/dev-box mounts
|
|
257
|
-
// through the plugin API (docs/modules-contract.md 1.2.0), kept as the module-
|
|
258
|
-
// facing spelling. It is no longer a bare boolean: it declares the box scope's OWN
|
|
259
|
-
// allow-list, read from the registry, so the declaration cannot drift from the
|
|
260
|
-
// scope it declares. If the registry stopped declaring a box scope it would
|
|
261
|
-
// declare nothing and every box session would be denied — fail closed.
|
|
262
|
-
const allowBoxScope = sessionScope(...((sessionScopeFor('box') || { permissions: [] }).permissions));
|
|
263
|
-
|
|
264
209
|
async function requireBuilder(req, res, next) {
|
|
265
210
|
let session;
|
|
266
211
|
try {
|
|
@@ -295,24 +240,13 @@ async function requireBuilder(req, res, next) {
|
|
|
295
240
|
res.fail('builder_inactive', 403);
|
|
296
241
|
return;
|
|
297
242
|
}
|
|
298
|
-
// SECURITY
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// is DATA (the session-scope registry above), not a hardcoded source test: a
|
|
304
|
-
// narrowing session reaches ONLY a route that declared capability its allow-list
|
|
305
|
-
// names, so EVERY other route (all of /tasks, /me, the rest of /box …) fails
|
|
306
|
-
// closed here, including any route added later; and a source the registry does not
|
|
307
|
-
// declare at all is denied rather than treated as unscoped. A builder who wants to
|
|
308
|
-
// do real work FROM the box runs /builder-reauth, minting a normal cli/web session.
|
|
309
|
-
const scope = sessionScopeFor(session.source);
|
|
310
|
-
if (!scopeCovers(scope, req.sessionScopeNeeds)) {
|
|
311
|
-
res.fail('box_scope', {
|
|
243
|
+
// SECURITY: an UNDECLARED session source is denied (see the registry notes
|
|
244
|
+
// above). This is what keeps a token of a retired source out of every route, and
|
|
245
|
+
// any source added later out until the registry declares it.
|
|
246
|
+
if (!sessionScopeFor(session.source)) {
|
|
247
|
+
res.fail('session_scope', {
|
|
312
248
|
status: 403,
|
|
313
|
-
message:
|
|
314
|
-
? 'This is a box-scoped session; it may only call dev-box bootstrap endpoints.'
|
|
315
|
-
: 'This session carries an unrecognised scope and may not call authenticated endpoints.',
|
|
249
|
+
message: 'This session carries an unrecognised source and may not call authenticated endpoints. Sign in again to mint a normal session.',
|
|
316
250
|
});
|
|
317
251
|
return;
|
|
318
252
|
}
|
|
@@ -382,9 +316,9 @@ function signInWithReturn(req) {
|
|
|
382
316
|
// 1. IDENTITY — is there a live session? (token from the gds_session cookie /
|
|
383
317
|
// legacy pms_session / Bearer, looked up fresh against builder_sessions).
|
|
384
318
|
// No → 302 into the web sign-in flow.
|
|
385
|
-
// 2.
|
|
386
|
-
//
|
|
387
|
-
// requireBuilder denies it every route
|
|
319
|
+
// 2. SOURCE — does the session-scope registry declare this session's source?
|
|
320
|
+
// An undeclared one reaches no page, whatever its builder's rank, exactly as
|
|
321
|
+
// requireBuilder denies it every route (task 1003197).
|
|
388
322
|
// No → 302 into the web sign-in flow: the remedy is a different session.
|
|
389
323
|
// 3. AUTHORITY — does the builder HOLD the page's catalog permission, resolved
|
|
390
324
|
// per request through the `governance` port exactly as requirePermission
|
|
@@ -557,21 +491,15 @@ async function runPageGate(name, req, res, next) {
|
|
|
557
491
|
res.redirect(302, signInWithReturn(req));
|
|
558
492
|
return;
|
|
559
493
|
}
|
|
560
|
-
// SECURITY (task 1003197 (A)):
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
// could fetch a gated page SHELL
|
|
564
|
-
//
|
|
565
|
-
// re-gates through requireBuilder and 403s box_scope, so the exposure was shells
|
|
566
|
-
// only — but a shell is still a surface a token living outside the ADR 0016 trust
|
|
567
|
-
// boundary was never meant to reach. Page routes declare no capability, so
|
|
568
|
-
// scopeCovers refuses EVERY narrowing scope here (and an unrecognised source with
|
|
569
|
-
// it), while an unscoped cli/web session passes untouched.
|
|
494
|
+
// SECURITY (task 1003197 (A)): the same undeclared-source check requireBuilder
|
|
495
|
+
// runs above — the two auth paths must not diverge. resolveSession accepts a
|
|
496
|
+
// Bearer token with NO source filter, so without this a token of an undeclared
|
|
497
|
+
// source could fetch a gated page SHELL whenever its builder's rank held the
|
|
498
|
+
// page's permission.
|
|
570
499
|
//
|
|
571
500
|
// Sign-in, not `deniedTo`: unlike the authority branch below, the remedy is a
|
|
572
|
-
// DIFFERENT SESSION
|
|
573
|
-
|
|
574
|
-
if (!scopeCovers(sessionScopeFor(session.source), req.sessionScopeNeeds)) {
|
|
501
|
+
// DIFFERENT SESSION, not a different page.
|
|
502
|
+
if (!sessionScopeFor(session.source)) {
|
|
575
503
|
res.redirect(302, signInWithReturn(req));
|
|
576
504
|
return;
|
|
577
505
|
}
|
|
@@ -598,14 +526,14 @@ async function runPageGate(name, req, res, next) {
|
|
|
598
526
|
}
|
|
599
527
|
|
|
600
528
|
// The authenticated /builders HTML surface — the Work Board, Roadmap, Goals, the
|
|
601
|
-
// task-detail page, the Repo Atlas
|
|
529
|
+
// task-detail page, the Repo Atlas and Settings. A Xenos IS
|
|
602
530
|
// allowed (page.view.public floors at xenos); an unauthenticated visitor is bounced
|
|
603
531
|
// into sign-in rather than served the page shell.
|
|
604
532
|
async function requireBuilderPage(req, res, next) {
|
|
605
533
|
return runPageGate('requireBuilderPage', req, res, next);
|
|
606
534
|
}
|
|
607
535
|
|
|
608
|
-
// The /watch monitoring surface
|
|
536
|
+
// The /watch monitoring surface and the /gate page
|
|
609
537
|
// (#726, idea 257, task 1179) — Metic and up, per `page.view.government`'s catalog
|
|
610
538
|
// floor. Named `requireArchonPage` until ADR 0157 lowered that floor archon→metic
|
|
611
539
|
// (older ADRs still use the old name; this line is the trail). Defense-in-depth:
|
|
@@ -917,10 +845,7 @@ module.exports = {
|
|
|
917
845
|
extractTokens,
|
|
918
846
|
SESSION_COOKIE_NAMES,
|
|
919
847
|
resolveSession,
|
|
920
|
-
allowBoxScope,
|
|
921
|
-
sessionScope,
|
|
922
848
|
sessionScopeFor,
|
|
923
|
-
sessionScopeAdmits,
|
|
924
849
|
parseSessionScopes,
|
|
925
850
|
SESSION_SCOPES,
|
|
926
851
|
requireBuilder,
|
package/src/bongos/db-kernel.js
CHANGED
|
@@ -45,7 +45,7 @@ async function withTx(fn, { pool: p = pool } = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// getBuilderById — the canonical builder-identity read (id, rank, status, the
|
|
48
|
-
// economy counters
|
|
48
|
+
// economy counters). The kernel's identity primitive; auth and
|
|
49
49
|
// every rank gate ultimately resolve a builder through this shape.
|
|
50
50
|
async function getBuilderById(id) {
|
|
51
51
|
// karma was added in migration 040 (V3.R24 / #244) as a separate
|
|
@@ -55,8 +55,7 @@ async function getBuilderById(id) {
|
|
|
55
55
|
const { rows } = await pool.query(
|
|
56
56
|
`SELECT id, github_id, github_login, display_name, avatar_url, total_credits, karma, created_at, rank,
|
|
57
57
|
status, deactivated_at, reactivated_at, deactivated_reason, preferred_disciplines, system_role,
|
|
58
|
-
anthropic_email, anthropic_user_id, monthly_budget_usd
|
|
59
|
-
box_blocked, box_blocked_reason, box_blocked_at, box_blocked_by
|
|
58
|
+
anthropic_email, anthropic_user_id, monthly_budget_usd
|
|
60
59
|
FROM builders WHERE id = $1`,
|
|
61
60
|
[id]
|
|
62
61
|
);
|
package/src/bongos/db.js
CHANGED
|
@@ -26,10 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
const { pool } = require('./pool');
|
|
28
28
|
const secretBox = require('./secret-box');
|
|
29
|
-
// Module seam (ADR 0083
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// of the module.
|
|
29
|
+
// Module seam (ADR 0083). The builder-exit path EMITS `builder.secrets-scrub-needed`
|
|
30
|
+
// AFTER COMMIT (best-effort) so a module that stores a builder-keyed secret can
|
|
31
|
+
// clear its own store — never a direct require of the module.
|
|
33
32
|
const seams = require('../module-seams');
|
|
34
33
|
|
|
35
34
|
// Kernel db slice (BV1.R69 / ADR 0091 §2): builder IDENTITY (getBuilderById),
|
|
@@ -123,11 +122,6 @@ async function linkAnthropicAccount(builderId, { email, userId }) {
|
|
|
123
122
|
// setBuilderMonthlyBudget() carved into modules/economy/cost.js (BV1.R77 / ADR
|
|
124
123
|
// 0093 §1-2). routes/builders.js resolves it via the `reward` kernel port.
|
|
125
124
|
|
|
126
|
-
// setBuilderBoxBlocked + its box_blocked UPDATE RELOCATED to modules/dev-box/db.js
|
|
127
|
-
// (BV1.R68 / ADR 0091 §3): box-blocked is cleanly dev-box-owned — the module is
|
|
128
|
-
// its only writer AND reader (core never reads box_blocked) — so the query left
|
|
129
|
-
// the core monolith with the doorway re-export when the slice pattern landed.
|
|
130
|
-
|
|
131
125
|
// Approval-gated entry (2026-05-30). The web hall sign-in no longer auto-creates
|
|
132
126
|
// an account for any GitHub user — a first-time visitor must be approved first
|
|
133
127
|
// (an Archon marks their access_request 'invited'). These three small lookups
|
|
@@ -446,14 +440,6 @@ async function setBuilderRank({ builderId, newRank, actorBuilderId }) {
|
|
|
446
440
|
const result = await withTx(async (client) => {
|
|
447
441
|
return _applyRankChangeTx(client, { builderId, newRank, actorBuilderId, enforceSingleRungPromotion: true });
|
|
448
442
|
});
|
|
449
|
-
// Source-access clawback (ADR 0031 §6 / #600). Post-commit, own connection,
|
|
450
|
-
// never throws — records the box reconcile this rank change implies (a demotion
|
|
451
|
-
// below the floor → deprovision; a graduation up → rescope). The CREDENTIAL
|
|
452
|
-
// layer is already cut off live on the next /box/source-access fetch; this
|
|
453
|
-
// just signals + audits the slower droplet-level reconcile.
|
|
454
|
-
await seams.emitAsync('builder.box-reconcile-needed', {
|
|
455
|
-
builderId, rank: newRank, status: result.status, actor: `rank-change:builder#${actorBuilderId}`,
|
|
456
|
-
});
|
|
457
443
|
return {
|
|
458
444
|
builder: result.builder,
|
|
459
445
|
previous_rank: result.previous_rank,
|
|
@@ -613,20 +599,11 @@ async function deactivateBuilder({ builderId, reason, actorBuilderId }) {
|
|
|
613
599
|
await deleteGithubToken({ builderId, p: client });
|
|
614
600
|
return { updated, activeClaims };
|
|
615
601
|
});
|
|
616
|
-
// Source-access clawback (ADR 0031 §6 / #600): a deactivated builder loses
|
|
617
|
-
// their box → loses source. Post-commit, own connection, never throws. The
|
|
618
|
-
// builder is now status='inactive' (rank forced to xenos above), so the
|
|
619
|
-
// reconcile resolves to 'deprovision'. Their next /box/source-access fetch is
|
|
620
|
-
// already 403'd (inactive) — this signals the droplet teardown for `box.js
|
|
621
|
-
// reconcile` / the dormant sweep.
|
|
622
|
-
await seams.emitAsync('builder.box-reconcile-needed', {
|
|
623
|
-
builderId, rank: 'xenos', status: 'inactive', actor: `builder-exit:builder#${actorBuilderId}`,
|
|
624
|
-
});
|
|
625
602
|
// PRIVACY (task 1003338 / ADR 0332): teardown port for a MODULE-owned
|
|
626
603
|
// builder-keyed secret (today: art-pipeline's Gemini BYOK key,
|
|
627
604
|
// gemini_key_enc/last4/set_at — CRUD moved out of core under ADR 0102, so
|
|
628
|
-
// core does not touch those columns directly). Best-effort,
|
|
629
|
-
//
|
|
605
|
+
// core does not touch those columns directly). Best-effort, post-commit:
|
|
606
|
+
// a module that stores such a secret listens
|
|
630
607
|
// for this event and clears its own store; on a vanilla instance (no
|
|
631
608
|
// listener registered) it is a no-op.
|
|
632
609
|
await seams.emitAsync('builder.secrets-scrub-needed', {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// regex, so wildcards would never match. Every PROTECTED_GLOBS entry is likewise
|
|
21
21
|
// an exact path or dir-prefix, so the overlap is exact.
|
|
22
22
|
//
|
|
23
|
-
// FIRST CUT, auto-tightening. We are mid-strangler (ADR 0083):
|
|
23
|
+
// FIRST CUT, auto-tightening. We are mid-strangler (ADR 0083): some domains have
|
|
24
24
|
// moved into modules/<key>/; the rest still live fused in src/bongos/. So a module's
|
|
25
25
|
// globs list WHERE ITS CODE LIVES TODAY plus its modules/<key>/ home, and the
|
|
26
26
|
// `protected` result reflects the current tree (most core domains are protected
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// modules + kernel from docs/design/modular-architecture/02-module-map.md §6.
|
|
31
31
|
//
|
|
32
32
|
// RELATION to src/modules.js: that registry tracks only the OPTIONAL feature
|
|
33
|
-
// modules (game/art-pipeline/discord
|
|
33
|
+
// modules (game/art-pipeline/discord) an instance toggles. This map is a
|
|
34
34
|
// SUPERSET — every module including the always-on core domains — because the goal
|
|
35
35
|
// scope wall must place ALL work, not just the optional features.
|
|
36
36
|
|
|
@@ -316,7 +316,7 @@ const MODULE_GLOBS = {
|
|
|
316
316
|
// (files-are-source — the committed definitions ARE the input agents-sync
|
|
317
317
|
// upserts), listed alongside it on the autonomy precedent, which likewise
|
|
318
318
|
// owns its '.claude/scheduled-tasks/' definition dir. The sync CLI travels
|
|
319
|
-
// with the domain but stays under scripts/gds/ (the
|
|
319
|
+
// with the domain but stays under scripts/gds/ (the provisioning
|
|
320
320
|
// precedent). Without this key the three Agents goals (1000038/39/40) could
|
|
321
321
|
// declare no wall at all (task 1002736).
|
|
322
322
|
agents: [
|
|
@@ -342,7 +342,7 @@ const MODULE_GLOBS = {
|
|
|
342
342
|
'npm-release': ['modules/npm-release/', 'tests/npm_release_*.mjs'],
|
|
343
343
|
// --- instance provisioning: the web-tier REQUEST + STATE surface (module home)
|
|
344
344
|
// plus the control-plane runner CLI — a command travels with its domain (the
|
|
345
|
-
// lifecycle
|
|
345
|
+
// lifecycle precedent). Covers the projects-surface work (goal
|
|
346
346
|
// 1000059); previously NO key resolved these paths, so no goal could wall
|
|
347
347
|
// them (task 1002690). ---
|
|
348
348
|
provisioning: [
|
|
@@ -355,32 +355,17 @@ const MODULE_GLOBS = {
|
|
|
355
355
|
// core/CLI-side under scripts/discord/.
|
|
356
356
|
'scripts/discord/', 'modules/discord/',
|
|
357
357
|
],
|
|
358
|
-
// ---
|
|
359
|
-
//
|
|
360
|
-
// (
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
// listed here yet — they come under this key when de-vendored. (ADR 0150 /
|
|
368
|
-
// task 1002386 made box.js RESOLVE infra/ from the instance root and fail
|
|
369
|
-
// loudly when it is missing; de-vendoring infra/ into core is still open.)
|
|
370
|
-
'dev-box': [
|
|
371
|
-
'modules/dev-box/', 'devbox-app/',
|
|
372
|
-
'scripts/gds/box.js', 'scripts/gds/box-auth-check.js', 'scripts/gds/box-connect-lib.js',
|
|
373
|
-
'scripts/gds/smoke-box.sh',
|
|
374
|
-
'scripts/gds/box-sync.js', // task 1004024: split out of box.js, now protected
|
|
375
|
-
|
|
376
|
-
// ADOPTED (task 1004111): the box BRING-UP surface. box.js (already here)
|
|
377
|
-
// orchestrates these five provisioning CLIs, and infra/ is the shell-script
|
|
378
|
-
// tree it resolves from the instance root — the comment above has described
|
|
379
|
-
// infra/ as dev-box's since task 1002386 without the glob saying so.
|
|
358
|
+
// --- the operator CONTROL PLANE: the provisioning CLIs that hold the DigitalOcean /
|
|
359
|
+
// Cloudflare credentials, and an instance's host topology under infra/. Not a
|
|
360
|
+
// module (nothing under modules/ carries it) — a scope like kernel or devsecops.
|
|
361
|
+
// Its own key rather than provisioning's, deliberately: every path here is a
|
|
362
|
+
// PROTECTED surface, so listing them under provisioning would make that module
|
|
363
|
+
// derive protected:true and put its open goals (1000106) behind an Archon join —
|
|
364
|
+
// the effect tests/government_protected_surfaces.mjs pins against. Given this key
|
|
365
|
+
// by task 1003892 (their previous owner was retired).
|
|
366
|
+
'control-plane': [
|
|
380
367
|
'scripts/gds/provision-config.js',
|
|
381
368
|
'scripts/gds/provision-net.js',
|
|
382
|
-
'scripts/gds/box-infra.js',
|
|
383
|
-
'scripts/gds/cf-tunnel.js',
|
|
384
369
|
'scripts/gds/do-api.js',
|
|
385
370
|
'infra/',
|
|
386
371
|
],
|
|
@@ -709,7 +694,7 @@ function scopeIncludesProtected(keys) {
|
|
|
709
694
|
//
|
|
710
695
|
// WHAT THIS IS AND IS NOT FOR. This narrows ONE consumer: the ship-time module-
|
|
711
696
|
// drift advisory (moduleDriftFiles below), whose question is "you said game, did
|
|
712
|
-
// you touch
|
|
697
|
+
// you touch discord?" — module-to-module confusion. It does NOT touch the goal
|
|
713
698
|
// scope wall, claim collisions (touchesOverlap), or the protected-path wall.
|
|
714
699
|
// Those keep counting methodology's territory as methodology's.
|
|
715
700
|
//
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/bongos/module-store.js — the module store's write side: keep a published
|
|
2
|
+
// module version's tarball and record it in the registry (task 1004271, ADR 0338 D1).
|
|
3
|
+
//
|
|
4
|
+
// The route (POST /store/modules/:key/versions in routes/modules.js) verifies the
|
|
5
|
+
// upload with scripts/gds/module-artifact.js first; this file only persists what has
|
|
6
|
+
// already passed. Two halves, in this order:
|
|
7
|
+
//
|
|
8
|
+
// 1. stageArtifact — write the tarball under the store dir (a temp name first;
|
|
9
|
+
// commitArtifact moves it into place inside step 2's transaction). ADR 0338 lets the
|
|
10
|
+
// store keep tarballs on the control plane's disk while volume is small, so it
|
|
11
|
+
// does: <MODULE_STORE_DIR or var/module-store>/<key>/<key>-<version>.tgz. Outside
|
|
12
|
+
// the git tree, like var/task-visuals, so a deploy's `git reset --hard` never
|
|
13
|
+
// wipes it.
|
|
14
|
+
// 2. publishVersion — one transaction over the registry tables (core_257): create
|
|
15
|
+
// the store_modules row on a key's first publish (the publisher becomes its
|
|
16
|
+
// author), refuse anyone else's key, refuse a delisted key (ADR 0338 D2: delisting
|
|
17
|
+
// stops what comes next), refuse a version that already exists or is not newer
|
|
18
|
+
// than the latest, then INSERT the version row. Rows are only ever INSERTed.
|
|
19
|
+
//
|
|
20
|
+
// If step 2 refuses, the route removes the temp file step 1 wrote, so a refused
|
|
21
|
+
// publish leaves nothing behind.
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const crypto = require('node:crypto');
|
|
26
|
+
const { parseVersion, cmp } = require('../module-loader/semver');
|
|
27
|
+
const { KEY_RE } = require('../module-loader/manifest-schema');
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
function storeDir() {
|
|
31
|
+
return process.env.MODULE_STORE_DIR || path.join(__dirname, '..', '..', 'var', 'module-store');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Where one version's tarball lives. Key and version are re-validated here so this
|
|
35
|
+
// is the path-traversal defense on its own, whatever the caller checked.
|
|
36
|
+
function artifactPath(key, version, { dir = storeDir() } = {}) {
|
|
37
|
+
if (!KEY_RE.test(String(key)) || !parseVersion(String(version))) {
|
|
38
|
+
throw new Error('artifactPath: a bad module key or version');
|
|
39
|
+
}
|
|
40
|
+
return path.join(dir, key, `${key}-${version}.tgz`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Write the tarball to a private temp name beside its final place and return both
|
|
44
|
+
// paths. It becomes the real file only through commitArtifact, which publishVersion
|
|
45
|
+
// runs just before COMMIT — so a crash anywhere leaves at most a temp file (or a
|
|
46
|
+
// final file with no row, which the next publish of that version simply replaces),
|
|
47
|
+
// never a row pointing at nothing and never a stray file that blocks a retry.
|
|
48
|
+
// Async throughout so a multi-MB write never holds the web process's event loop.
|
|
49
|
+
async function stageArtifact(buf, { key, version, dir = storeDir() } = {}) {
|
|
50
|
+
const final = artifactPath(key, version, { dir });
|
|
51
|
+
await fs.promises.mkdir(path.dirname(final), { recursive: true });
|
|
52
|
+
const tmp = `${final}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.partial`;
|
|
53
|
+
await fs.promises.writeFile(tmp, buf, { flag: 'wx', mode: 0o640 });
|
|
54
|
+
return { tmp, final };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function commitArtifact({ tmp, final }) {
|
|
58
|
+
await fs.promises.rename(tmp, final);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function removeArtifact(file) {
|
|
62
|
+
try { await fs.promises.unlink(file); } catch { /* already gone — nothing to undo */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The store keeps artifact_path relative to the store dir, so moving the dir (or
|
|
66
|
+
// moving to object storage later) is a config change, not a data migration.
|
|
67
|
+
function relativeArtifactPath(file, { dir = storeDir() } = {}) {
|
|
68
|
+
return path.relative(dir, file).split(path.sep).join('/');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// One transaction. `db` is a pg Pool (anything with connect()). Returns
|
|
72
|
+
// { ok: true, version: row, created: bool } or { ok: false, code, message, status }.
|
|
73
|
+
async function publishVersion(input, { db, beforeCommit } = {}) {
|
|
74
|
+
const {
|
|
75
|
+
key, title, description, version, coreVersion, manifest,
|
|
76
|
+
treeSha256, tarballSha256, bytes, artifactPath: relPath, builderId,
|
|
77
|
+
} = input;
|
|
78
|
+
const pool = db || require('./pool').pool;
|
|
79
|
+
const client = await pool.connect();
|
|
80
|
+
const refuse = async (status, code, message) => {
|
|
81
|
+
await client.query('ROLLBACK');
|
|
82
|
+
return { ok: false, status, code, message };
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
await client.query('BEGIN');
|
|
86
|
+
const { rows: [mod] } = await client.query(
|
|
87
|
+
'SELECT module_key, author_id, status FROM store_modules WHERE module_key = $1 FOR UPDATE', [key]);
|
|
88
|
+
let created = false;
|
|
89
|
+
if (!mod) {
|
|
90
|
+
await client.query(
|
|
91
|
+
'INSERT INTO store_modules (module_key, title, description, author_id) VALUES ($1, $2, $3, $4)',
|
|
92
|
+
[key, title, description || '', builderId]);
|
|
93
|
+
created = true;
|
|
94
|
+
} else {
|
|
95
|
+
if (String(mod.author_id) !== String(builderId)) {
|
|
96
|
+
return refuse(403, 'not_module_author', `"${key}" belongs to another author — only its author can publish new versions`);
|
|
97
|
+
}
|
|
98
|
+
if (mod.status === 'delisted') {
|
|
99
|
+
return refuse(409, 'module_delisted', `"${key}" is delisted — it takes no new versions (ADR 0338 D2)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Every version of the key, compared in JS: semver order is numeric per part, so
|
|
104
|
+
// a text ORDER BY would rank 1.10.0 below 1.9.0. A module's version count is small.
|
|
105
|
+
const { rows: existing } = await client.query(
|
|
106
|
+
'SELECT version FROM store_module_versions WHERE module_key = $1', [key]);
|
|
107
|
+
const wanted = parseVersion(version);
|
|
108
|
+
if (existing.some((r) => r.version === version)) {
|
|
109
|
+
return refuse(409, 'version_exists', `${key} ${version} is already published — a version never changes; bump module.json's version`);
|
|
110
|
+
}
|
|
111
|
+
const newest = existing.map((r) => parseVersion(r.version)).filter(Boolean).sort(cmp).pop();
|
|
112
|
+
if (newest && cmp(wanted, newest) <= 0) {
|
|
113
|
+
return refuse(409, 'version_not_newer', `${key} ${version} is not newer than the latest published ${newest.join('.')}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { rows: [row] } = await client.query(
|
|
117
|
+
`INSERT INTO store_module_versions
|
|
118
|
+
(module_key, version, core_version, manifest, tree_sha256, tarball_sha256,
|
|
119
|
+
tarball_bytes, artifact_path, published_by)
|
|
120
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
121
|
+
RETURNING id, module_key, version, core_version, tree_sha256, tarball_sha256,
|
|
122
|
+
tarball_bytes, published_by, published_at`,
|
|
123
|
+
[key, version, coreVersion, manifest, treeSha256, tarballSha256, bytes, relPath, builderId]);
|
|
124
|
+
// Put the tarball in place while the row is still uncommitted: if this throws,
|
|
125
|
+
// the catch below rolls the row back, so the two can never disagree.
|
|
126
|
+
if (beforeCommit) await beforeCommit();
|
|
127
|
+
await client.query('COMMIT');
|
|
128
|
+
return { ok: true, created, version: row };
|
|
129
|
+
} catch (e) {
|
|
130
|
+
try { await client.query('ROLLBACK'); } catch { /* the connection may be gone */ }
|
|
131
|
+
// Two publishes of the same version racing past the check: the UNIQUE wins.
|
|
132
|
+
if (e && e.code === '23505') return { ok: false, status: 409, code: 'version_exists', message: `${key} ${version} is already published` };
|
|
133
|
+
throw e;
|
|
134
|
+
} finally {
|
|
135
|
+
client.release();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
141
|
+
};
|