@bongos/core 1.19.1072 → 1.19.1074
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 +320 -465
- package/.claude/skills/feedback/SKILL.md +2 -2
- 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 +6 -2
- 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 +2 -1
- package/docs/api/openapi.json +171 -1661
- 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 +9 -9
- 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/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/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +0 -5
- package/modules/lifecycle/db.js +0 -4
- 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/routes/tasks.js +5 -7
- package/modules/memory/module.json +1 -0
- 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 +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +2 -2
- 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/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/audit-authorship.sh +1 -13
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/cli-lib.js +8 -8
- 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 +1 -1
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +4 -4
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.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/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/regen-instance-docs.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/ship-flow.js +6 -6
- package/scripts/gds/ship-land.js +3 -4
- package/scripts/gds/ship-regen.js +10 -41
- package/scripts/gds/ship.js +4 -24
- 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/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/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 +6 -22
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes.js +5 -0
- 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 +15 -7
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_client.mjs +0 -15
- package/tests/auth_page_gate.mjs +10 -10
- 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/consumer_layout_boot.mjs +2 -0
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/deploy_divergence_line.mjs +11 -11
- 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/gate_approvals.mjs +0 -1
- package/tests/go_live.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +20 -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_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/host_topology_skips.mjs +1 -2
- 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/module-scope-map.mjs +9 -9
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +15 -15
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +13 -4
- 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/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/profile_route.mjs +1 -6
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +10 -18
- package/tests/publish_manifest.mjs +1 -2
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/repo_map.mjs +3 -4
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/session_rename_fallback.mjs +0 -21
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +1 -1
- 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
|
@@ -1,623 +0,0 @@
|
|
|
1
|
-
// src/bongos/box-access.js — rank-gated SOURCE access for the per-builder dev box
|
|
2
|
-
// (ADR 0031 §6 + open-question §9.2, task #600). Builds on #598's box lifecycle.
|
|
3
|
-
//
|
|
4
|
-
// THE DECISION #598 RECORDED, #600 ENFORCES
|
|
5
|
-
// -----------------------------------------
|
|
6
|
-
// #598 stamps a box with a `scope` ('starter' | 'full') and the `provisioned_rank`
|
|
7
|
-
// at provision time, but injects NO GitHub credential — the cloud-init "logs
|
|
8
|
-
// guidance rather than cloning, and #600 wires the scoped credential." This module
|
|
9
|
-
// is that enforcement: it decides, on the builder's LIVE rank, whether a box may
|
|
10
|
-
// reach source at all and WHICH surface it may materialize, and it drives the
|
|
11
|
-
// offboarding clawback (revoke rank → de-provision box → source gone).
|
|
12
|
-
//
|
|
13
|
-
// THE MODEL (ADR 0016 extended from "writes are gated" to "source is gated")
|
|
14
|
-
// --------------------------------------------------------------------------
|
|
15
|
-
// A box holds NO permanent baked credential. At boot — and on a refresh cron —
|
|
16
|
-
// the box calls GET /api/bongos/box/source-access authenticated as the builder. The
|
|
17
|
-
// GDS reads the builder's LIVE rank+status (their machine cannot forge it) and
|
|
18
|
-
// either DENIES (403 → the box scrubs its credential + clone) or returns a
|
|
19
|
-
// scope-appropriate clone spec plus a short-lived, operator-supplied credential
|
|
20
|
-
// (src/bongos/box-credential.js; the secret lives in the prod ENV, never the repo —
|
|
21
|
-
// ADR 0022). Because the check is live and per-fetch, revocation is INSTANT at the
|
|
22
|
-
// credential layer: demote/deactivate a builder and their next refresh 403s, even
|
|
23
|
-
// before the droplet teardown (the async cleanup) runs.
|
|
24
|
-
//
|
|
25
|
-
// THE HONEST LIMIT (recorded in ADR 0031 §6, preserved here)
|
|
26
|
-
// ----------------------------------------------------------
|
|
27
|
-
// Git cannot sub-path-scope a single private repo: any read credential for the
|
|
28
|
-
// repo can, in principle, read all of it, and sparse-checkout is client-side. So
|
|
29
|
-
// the 'starter' surface is DEFENSE-IN-DEPTH + AUDIT + REVOCATION, NOT insider-proof
|
|
30
|
-
// DRM. A 'starter' box is issued the starter credential and told to sparse-checkout
|
|
31
|
-
// only the safe surface; an authorized builder who fights the tooling could still
|
|
32
|
-
// over-reach. The real day-one protection is (a) full-repo credentials are gated to
|
|
33
|
-
// graduation and (b) every grant/denial is audited. A hard boundary would require a
|
|
34
|
-
// physically separate starter repo — supported via the provider's STARTER_REPO_URL
|
|
35
|
-
// override, left as an operator choice (see box-credential.js).
|
|
36
|
-
|
|
37
|
-
const {
|
|
38
|
-
DEFAULT_PROVISION_FLOOR,
|
|
39
|
-
rankMeetsBoxFloor,
|
|
40
|
-
boxScopeForRank,
|
|
41
|
-
getBoxByBuilderId,
|
|
42
|
-
recordEvent,
|
|
43
|
-
} = require('./boxes');
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// PURE: the scoped surface catalog
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
// The repo surface a 'starter' (Xenos/Thetes) box may sparse-checkout — the
|
|
50
|
-
// "first tasks on a scoped surface" of ADR 0031 §9.2: the art pipeline,
|
|
51
|
-
// docs/design records, the public status surface, the curiosity inbox, and
|
|
52
|
-
// .devcontainer so the box can build its environment — PLUS the builder WORKFLOW
|
|
53
|
-
// TOOLING (#818-followup): the /builder-* skills (.claude), the GDS CLI (scripts),
|
|
54
|
-
// and the GDS lib those scripts require (src/bongos + cone-mode pulls the src/*.js
|
|
55
|
-
// root files like src/db.js). Without that tooling a newcomer's box can't run
|
|
56
|
-
// /builder-start|claim|ship — the whole point of being on the box.
|
|
57
|
-
//
|
|
58
|
-
// SANDBOX-FOR-EVERYONE (ADR 0052, task #1001 — Lars, 2026-06-13): the PRODUCT/game
|
|
59
|
-
// code is now in the starter scope too — `src/world`, `src/rooms`, and the
|
|
60
|
-
// `public/` game client — so EVERY box (not just Metic+ full-clone boxes) can run
|
|
61
|
-
// the live sandbox (sandbox-<login>.example.com, ADR 0044). The box serves
|
|
62
|
-
// the game from a GAME-ONLY entry (src/preview-server.js) that never mounts
|
|
63
|
-
// /api/bongos, so the privacy boundary that matters for a PUBLIC sandbox URL is the
|
|
64
|
-
// RUNTIME (what the box process exposes), NOT source exclusion. That is consistent
|
|
65
|
-
// with the rest of this list: newcomers already hold the build-tooling source
|
|
66
|
-
// (scripts, .claude, src/bongos — no secrets), so adding the game source changes no
|
|
67
|
-
// secret posture. The locked decision (#1001) leaves the broader source-tightening
|
|
68
|
-
// (idea #213) deferred and out of scope.
|
|
69
|
-
//
|
|
70
|
-
// STILL EXCLUDES `migrations/` — a degraded-DB preview needs no schema, and the
|
|
71
|
-
// migrations are the build-pipeline internal that idea #213 would tighten. No DB
|
|
72
|
-
// runs on the box anyway (ADR 0044 §1), so the game preview never reads it.
|
|
73
|
-
//
|
|
74
|
-
// Paths are git cone-mode sparse-checkout entries. ONLY directories — cone mode
|
|
75
|
-
// rejects a file path ("'<file>' is not a directory") which aborts the whole
|
|
76
|
-
// `git sparse-checkout set`. Top-level files (CLAUDE.md, README.md, package.json,
|
|
77
|
-
// server.js, generative-ideas.md, …) are included AUTOMATICALLY by cone mode, so
|
|
78
|
-
// they need not (and must not) be listed here. Cone mode also includes the direct
|
|
79
|
-
// files of every ancestor directory — so listing src/bongos (or src/world, src/rooms)
|
|
80
|
-
// pulls src/*.js root files like src/db.js + src/seats.js, which the game rooms
|
|
81
|
-
// require.
|
|
82
|
-
const STARTER_SPARSE_PATHS = [
|
|
83
|
-
// status surface moved to modules/status-ui/ in BV1.R84 (was public-status/).
|
|
84
|
-
'.devcontainer', 'art', 'docs', 'modules/status-ui',
|
|
85
|
-
'.claude', 'scripts', 'src/bongos',
|
|
86
|
-
// The product/game runtime so the box can serve the live sandbox (ADR 0052).
|
|
87
|
-
// The game-only entry (src/preview-server.js) registers WorldRoom + QueueRoom
|
|
88
|
-
// from src/rooms and renders src/world + the public/ client; none of it mounts
|
|
89
|
-
// /api/bongos. `public/` is the game client (NOT modules/hall-ui/public/, the hall
|
|
90
|
-
// surface carved in BV1.R85, which stays out of a starter box).
|
|
91
|
-
'src/world', 'src/rooms', 'public',
|
|
92
|
-
// infra/ carries the box's own bring-up scripts (box-heartbeat.sh,
|
|
93
|
-
// box-report-terminal.sh, the terminal + game-preview units) that
|
|
94
|
-
// box-source-fetch installs post-clone. Without it the heartbeat cron never
|
|
95
|
-
// installs, so claude_active is never reported, first_connected_at is never
|
|
96
|
-
// stamped, and the "Connect to thy dev box" onboarding step never ticks. No
|
|
97
|
-
// secrets live in infra/ (ADR 0022).
|
|
98
|
-
'infra',
|
|
99
|
-
// tests/ — so a starter box can RUN the suite for the code it holds (BV1.R99,
|
|
100
|
-
// goal 1000052; same rationale as BASE_SPARSE_PATHS). Onboarding is "bare
|
|
101
|
-
// machine → first ship", and a newcomer proving their first change needs the
|
|
102
|
-
// top-level tests/ (tests/*.mjs + helpers.mjs), which sits beside src, not in it.
|
|
103
|
-
'tests',
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
// States in which a box is (or is becoming) a running droplet that can actually
|
|
107
|
-
// hold a clone. The source credential is issued ONLY to a builder whose box is in
|
|
108
|
-
// one of these — a stolen token used OFF a live box, or a builder with no / parked /
|
|
109
|
-
// destroyed box, gets no credential. Defense-in-depth on top of the rank gate, and
|
|
110
|
-
// it aligns the model: "the BOX holds the credential", so there must be a live box.
|
|
111
|
-
const SOURCE_HOLDING_STATES = ['active', 'provisioning', 'waking'];
|
|
112
|
-
function boxCanHoldSource(state) {
|
|
113
|
-
return SOURCE_HOLDING_STATES.includes(state);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// What a given scope is allowed to materialize. Fail-closed: any unknown/missing
|
|
117
|
-
// scope collapses to 'starter' (least privilege), never to 'full'.
|
|
118
|
-
function cloneSpecForScope(scope) {
|
|
119
|
-
if (scope === 'full') {
|
|
120
|
-
return { scope: 'full', mode: 'full', sparsePaths: null };
|
|
121
|
-
}
|
|
122
|
-
return { scope: 'starter', mode: 'sparse', sparsePaths: [...STARTER_SPARSE_PATHS] };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
// PURE: the TASK-scoped surface (ADR 0148 / goal 1000051) — layered on BASE
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
|
|
129
|
-
// BASE_SPARSE_PATHS — the "pre-existing Bongos instance" every box carries no
|
|
130
|
-
// matter what the builder claimed: enough for Claude to hold methodology context
|
|
131
|
-
// and run the /builder-* CLI, but NONE of the product/game surface. It is the
|
|
132
|
-
// NON-PRODUCT subset of STARTER_SPARSE_PATHS — .devcontainer, .claude, scripts,
|
|
133
|
-
// src/bongos, docs, infra. (Top-level files like CLAUDE.md/server.js + the src/*.js
|
|
134
|
-
// root files come AUTOMATICALLY via cone-mode ancestor inclusion, because src/bongos
|
|
135
|
-
// is listed — see the STARTER note above.) A task's own module code (including the
|
|
136
|
-
// product/game when a game task is claimed) layers on top via sparsePathsForScopeKeys.
|
|
137
|
-
// This is the ADR 0148 move: scope the box to the code the claimed task needs, not
|
|
138
|
-
// the whole repo.
|
|
139
|
-
//
|
|
140
|
-
// BEHAVIOR vs ADR 0052 (sandbox-for-everyone): BASE deliberately drops the game
|
|
141
|
-
// (src/world, src/rooms, public) + art + modules/status-ui — a task-scoped box only
|
|
142
|
-
// carries the live sandbox when the claimed work is scoped to the `game` module. The
|
|
143
|
-
// pre-ADR-0148 "every box runs the sandbox" survives on the RANK fallback path
|
|
144
|
-
// (decideSourceAccess with NO scopeKeys), so a vanilla instance without the lifecycle
|
|
145
|
-
// port is unchanged.
|
|
146
|
-
const BASE_SPARSE_PATHS = [
|
|
147
|
-
'.devcontainer', '.claude', 'scripts', 'src/bongos', 'docs', 'infra',
|
|
148
|
-
// tests/ — so a pulled module's build/test commands actually RUN on the box
|
|
149
|
-
// (BV1.R99, goal 1000052). Module tests live in the TOP-LEVEL tests/ dir
|
|
150
|
-
// (tests/box_*.mjs + the tests/helpers.mjs harness), NOT inside modules/<key>/,
|
|
151
|
-
// so a box holding the module source but not tests/ cannot verify a change.
|
|
152
|
-
// A cone-mode directory; it carries no product runtime, only test code.
|
|
153
|
-
'tests',
|
|
154
|
-
// clients/ — the generated bongos-client (ADR 0118) that cli-lib.js hard-imports
|
|
155
|
-
// since task 1996 made it the CLI's one HTTP client. Without it EVERY /builder-*
|
|
156
|
-
// command on the box dies at import, and the builder cannot self-heal: the
|
|
157
|
-
// workspace is a token-less blob:none clone (sparse-checkout add can't fetch)
|
|
158
|
-
// and box-source-fetch re-applies this server-computed list every tick
|
|
159
|
-
// (task 1002745). Generated ESM only, no product runtime.
|
|
160
|
-
'clients',
|
|
161
|
-
// config/ — task 1003227. Without it a box CANNOT RUN ITS OWN QUALITY GATE:
|
|
162
|
-
// `scripts/gds/fitness.js` compares against config/fitness-baselines.json, and
|
|
163
|
-
// with the file absent the ratchet used to report PASS (it read the baselines
|
|
164
|
-
// through a try/catch that returned {}, so every metric had no floor to breach).
|
|
165
|
-
// A planted 5,000-line + 94-cycle regression sailed straight through. So a boxed
|
|
166
|
-
// builder shipped believing they were clean and learned otherwise from red CI,
|
|
167
|
-
// with nothing on the box to reproduce it. The fail-closed half is fixed in
|
|
168
|
-
// fitness-ratchets.js; this is the half that lets the gate actually run.
|
|
169
|
-
//
|
|
170
|
-
// Precedent, and why the "a module glob will pull it" argument does not hold:
|
|
171
|
-
// ADR 0174 put config/government.json in the `government` key's globs and the
|
|
172
|
-
// parent-directory emission below exists precisely because `config` was not in
|
|
173
|
-
// BASE — task 1003018 was RELEASED because its box could never see the file that
|
|
174
|
-
// was its entire deliverable. BASE is the right home, not a per-module accident.
|
|
175
|
-
//
|
|
176
|
-
// Secret posture unchanged: a cone-mode directory of JSON config. The NEUTRAL
|
|
177
|
-
// packs are already public, and an instance pack (config/branding.json,
|
|
178
|
-
// config/government.json) is host content the box's own builder authors — no
|
|
179
|
-
// credentials live here (ADR 0022); those are in /etc and the box env.
|
|
180
|
-
'config',
|
|
181
|
-
// modules/lifecycle — task 1003085. BASE's whole charter is "enough to run the
|
|
182
|
-
// /builder-* CLI", and the CLI does not run without this module:
|
|
183
|
-
// scripts/gds/context-pack.js reads the task enums from
|
|
184
|
-
// modules/lifecycle/task-classifier, and context-pack is on the /builder-start
|
|
185
|
-
// AND /builder-claim paths. On a task-scoped box with NO active claim nothing
|
|
186
|
-
// put lifecycle in scope, so both commands died at import with MODULE_NOT_FOUND
|
|
187
|
-
// — and neither could be used to fix it, because a CLAIM is what fetches the
|
|
188
|
-
// module. The only way out was POST /claims by hand.
|
|
189
|
-
//
|
|
190
|
-
// This is the structural half; context-pack.js also degrades now if the module
|
|
191
|
-
// is absent for any OTHER reason. Both, deliberately: the degrade keeps the CLI
|
|
192
|
-
// alive against a future module gap, and this line is what makes the pack
|
|
193
|
-
// actually COMPLETE on every box rather than quietly reduced.
|
|
194
|
-
//
|
|
195
|
-
// Same posture as the entries above — core-domain work-tracking code, which is
|
|
196
|
-
// methodology and not product runtime, so it belongs in BASE by the same reading
|
|
197
|
-
// that put scripts/ and src/bongos/ here. No secrets; a cone-mode directory.
|
|
198
|
-
'modules/lifecycle',
|
|
199
|
-
];
|
|
200
|
-
|
|
201
|
-
// sparsePathsForScopeKeys — PURE. Given the module KEYS a builder's active claims
|
|
202
|
-
// put in scope (from the lifecycle port `moduleScopeForActiveClaims`, R92) and the
|
|
203
|
-
// doorway `moduleScopeMap` (PASSED IN by the route — box-access imports no core
|
|
204
|
-
// internal), compute the git cone-mode sparse-checkout set: BASE ∪ the DIRECTORY
|
|
205
|
-
// globs of every scoped module.
|
|
206
|
-
//
|
|
207
|
-
// Cone-mode constraint (load-bearing, ADR 0148): `git sparse-checkout set` accepts
|
|
208
|
-
// ONLY directories — a file path aborts the whole set. A module's scope_modules →
|
|
209
|
-
// globs come in two shapes: directory globs (trailing '/', e.g. 'modules/dev-box/')
|
|
210
|
-
// and file-list globs (exact paths, e.g. the kernel key's 'src/bongos/db.js'). We
|
|
211
|
-
// KEEP the directory globs (normalized to no trailing slash, matching the BASE
|
|
212
|
-
// style). A FILE glob cannot be emitted as-is, so it contributes its PARENT
|
|
213
|
-
// DIRECTORY instead — unless BASE or another emitted dir already covers it.
|
|
214
|
-
//
|
|
215
|
-
// It used to be dropped outright, on the reasoning that "every file glob in the
|
|
216
|
-
// map sits under a BASE directory (src/bongos, scripts) or is a top-level file
|
|
217
|
-
// cone mode pulls automatically". That was true when written and **silently
|
|
218
|
-
// stopped being true**: ADR 0174 gave the `government` key the file globs
|
|
219
|
-
// `config/government.json` + `config/government.neutral.json`, and `config` is
|
|
220
|
-
// not in BASE. The result was a goal walled to `government` whose box could never
|
|
221
|
-
// see two files inside its own scope wall — which released task 1003018 (R05 of
|
|
222
|
-
// goal 1000069), whose entire deliverable was one of them. Rather than restore an
|
|
223
|
-
// invariant that expires again the next time a module claims a config file, the
|
|
224
|
-
// parent is emitted and the class is covered by construction.
|
|
225
|
-
//
|
|
226
|
-
// TRADE-OFF, stated rather than implied: the parent directory is a SUPERSET of
|
|
227
|
-
// the glob — asking for `config/government.json` materializes all of `config/`.
|
|
228
|
-
// The box's view is already a superset of the wall (BASE hands out `scripts`,
|
|
229
|
-
// `docs` and `tests` wholesale regardless of scope), so this is consistent with
|
|
230
|
-
// the existing posture, not a new concession. The WALL is unchanged: what a
|
|
231
|
-
// builder may author is still decided by goal-scope-check against the globs, not
|
|
232
|
-
// by what happens to be on disk.
|
|
233
|
-
//
|
|
234
|
-
// Top-level file globs (no '/') contribute nothing — cone mode includes root
|
|
235
|
-
// files automatically. Deterministic: BASE in declared order, then the module
|
|
236
|
-
// dirs sorted + deduped.
|
|
237
|
-
function sparsePathsForScopeKeys(scopeKeys, moduleScopeMap) {
|
|
238
|
-
const base = [...BASE_SPARSE_PATHS];
|
|
239
|
-
const seen = new Set(base);
|
|
240
|
-
const extra = [];
|
|
241
|
-
// Cone mode materializes a directory and everything under it, so a path is
|
|
242
|
-
// already covered when IT or any ancestor has been emitted. This is what keeps
|
|
243
|
-
// `scripts/gds/box.js` from adding `scripts/gds` — BASE's `scripts` covers it.
|
|
244
|
-
const covered = (dir) => {
|
|
245
|
-
let p = dir;
|
|
246
|
-
for (;;) {
|
|
247
|
-
if (seen.has(p)) return true;
|
|
248
|
-
const cut = p.lastIndexOf('/');
|
|
249
|
-
if (cut < 0) return false;
|
|
250
|
-
p = p.slice(0, cut);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
const add = (dir) => {
|
|
254
|
-
if (!dir || covered(dir)) return;
|
|
255
|
-
seen.add(dir);
|
|
256
|
-
extra.push(dir);
|
|
257
|
-
};
|
|
258
|
-
for (const key of Array.isArray(scopeKeys) ? scopeKeys : []) {
|
|
259
|
-
const entry = moduleScopeMap && typeof moduleScopeMap.scopeForModule === 'function'
|
|
260
|
-
? moduleScopeMap.scopeForModule(key)
|
|
261
|
-
: null;
|
|
262
|
-
if (!entry || !Array.isArray(entry.globs)) continue;
|
|
263
|
-
for (const glob of entry.globs) {
|
|
264
|
-
if (typeof glob !== 'string' || !glob) continue;
|
|
265
|
-
if (glob.endsWith('/')) { add(glob.replace(/\/+$/, '')); continue; }
|
|
266
|
-
const cut = glob.lastIndexOf('/');
|
|
267
|
-
if (cut < 0) continue; // a root file — cone mode already includes it
|
|
268
|
-
add(glob.slice(0, cut));
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
extra.sort();
|
|
272
|
-
return [...base, ...extra];
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// PURE: builder-requested WIDENING (task 1003087 / idea 1000793)
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
|
-
|
|
279
|
-
// A builder sometimes needs a directory their task scope did not pull — to run
|
|
280
|
-
// the full suite, to read a caller, to regenerate a TREE-DEPENDENT artifact. The
|
|
281
|
-
// obvious move, `git sparse-checkout add <path>`, does not survive: the */10
|
|
282
|
-
// source-fetch cron re-applies the SERVER-computed set and deletes the addition,
|
|
283
|
-
// including WHILE A COMMAND IS RUNNING (a live suite died mid-run with
|
|
284
|
-
// MODULE_NOT_FOUND that read exactly like a code bug; a box-committed
|
|
285
|
-
// gen-repo-map regen off a narrow tree reds CI's freshness gate — learning
|
|
286
|
-
// 1000192, which stranded PR #254).
|
|
287
|
-
//
|
|
288
|
-
// So the widening is recorded SERVER-side and unioned in HERE, which is what
|
|
289
|
-
// makes it tick-surviving: the cron asks this endpoint for the set on every tick,
|
|
290
|
-
// so it gets the union too. Nothing in the instance-owned fetch script changes —
|
|
291
|
-
// which matters, because the neutral core does not ship it (ADR 0150).
|
|
292
|
-
|
|
293
|
-
// Bounds. Small on purpose: this is an escape hatch for a handful of dirs, not a
|
|
294
|
-
// second scope system. Over the cap the extras are dropped, not an error — a
|
|
295
|
-
// truncated widen still fetches, and failing the source pull over it would be a
|
|
296
|
-
// far worse trade than materializing less.
|
|
297
|
-
const MAX_WIDEN_PATHS = 24;
|
|
298
|
-
const MAX_WIDEN_PATH_LEN = 200;
|
|
299
|
-
|
|
300
|
-
// withinAny(p, roots) — is p one of roots, or underneath one? Cone mode
|
|
301
|
-
// materializes a directory and everything below it, so "already covered" and
|
|
302
|
-
// "inside the allowed surface" are the same ancestor walk.
|
|
303
|
-
function withinAny(p, roots) {
|
|
304
|
-
let q = p;
|
|
305
|
-
for (;;) {
|
|
306
|
-
if (roots.includes(q)) return true;
|
|
307
|
-
const cut = q.lastIndexOf('/');
|
|
308
|
-
if (cut < 0) return false;
|
|
309
|
-
q = q.slice(0, cut);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// normalizeWidenPath — repo-relative POSIX directory, or null when it is not one.
|
|
314
|
-
//
|
|
315
|
-
// This is a path that reaches `git sparse-checkout set` on a box, so it is
|
|
316
|
-
// validated as INPUT rather than trusted: absolute paths, Windows drive letters,
|
|
317
|
-
// '..' traversal and empty segments are all refused rather than normalized away,
|
|
318
|
-
// because "cleaned up into something that still works" is how a traversal becomes
|
|
319
|
-
// a shrug. Backslashes are folded to '/' first so a Windows-typed path is judged
|
|
320
|
-
// on what it means, not on its separator.
|
|
321
|
-
function normalizeWidenPath(raw) {
|
|
322
|
-
if (typeof raw !== 'string') return null;
|
|
323
|
-
// BACKSLASH by char code, not as a literal: this file is edited through
|
|
324
|
-
// tooling that has silently collapsed a doubled backslash before, and a
|
|
325
|
-
// separator fold that quietly stops folding is invisible until a Windows
|
|
326
|
-
// path slips through as one long segment.
|
|
327
|
-
const BACKSLASH = String.fromCharCode(92);
|
|
328
|
-
let p = raw.trim().split(BACKSLASH).join('/');
|
|
329
|
-
while (p.startsWith('./')) p = p.slice(2);
|
|
330
|
-
p = p.replace(/\/+/g, '/').replace(/\/+$/, '');
|
|
331
|
-
if (!p || p === '.' || p.length > MAX_WIDEN_PATH_LEN) return null;
|
|
332
|
-
if (p.startsWith('/') || /^[A-Za-z]:/.test(p)) return null;
|
|
333
|
-
const segs = p.split('/');
|
|
334
|
-
if (!segs.every((s) => s && s !== '.' && s !== '..' && /^[A-Za-z0-9._-]+$/.test(s))) return null;
|
|
335
|
-
return p;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// admissibleWidenPaths — the widen requests this builder's RANK actually permits,
|
|
339
|
-
// normalized, deduped, capped and sorted.
|
|
340
|
-
//
|
|
341
|
-
// THIS IS THE SECURITY BOUNDARY OF THE FEATURE, so it is stated plainly: widening
|
|
342
|
-
// changes what is MATERIALIZED on a box, and rank decides how much of the repo a
|
|
343
|
-
// box may hold at all (ADR 0031 section 9.2 — 'full' for Metic+, the 'starter'
|
|
344
|
-
// surface below that). Honoring an arbitrary widen would hand a Xenos the whole
|
|
345
|
-
// tree through a convenience flag, which is a rank escalation dressed as
|
|
346
|
-
// ergonomics. So:
|
|
347
|
-
// - a builder whose RANK scope is 'full' may widen anywhere (they are already
|
|
348
|
-
// entitled to the entire repo; the widen only decides what gets pulled);
|
|
349
|
-
// - anyone else may widen only INSIDE the starter surface. That is not a null
|
|
350
|
-
// grant: a task-scoped box is BASE union its modules, which is NARROWER than
|
|
351
|
-
// starter, so this is exactly the room to climb back to what their rank
|
|
352
|
-
// already allowed — and no further.
|
|
353
|
-
// The authoring wall is untouched either way. What a builder may CHANGE is still
|
|
354
|
-
// goal-scope-check against the module globs, never what happens to be on disk
|
|
355
|
-
// (ADR 0148).
|
|
356
|
-
function admissibleWidenPaths(widenPaths, rank) {
|
|
357
|
-
const unrestricted = boxScopeForRank(rank) === 'full';
|
|
358
|
-
const out = [];
|
|
359
|
-
const seen = new Set();
|
|
360
|
-
for (const raw of Array.isArray(widenPaths) ? widenPaths : []) {
|
|
361
|
-
const p = normalizeWidenPath(raw);
|
|
362
|
-
if (!p || seen.has(p)) continue;
|
|
363
|
-
if (!unrestricted && !withinAny(p, STARTER_SPARSE_PATHS)) continue;
|
|
364
|
-
seen.add(p);
|
|
365
|
-
out.push(p);
|
|
366
|
-
if (out.length >= MAX_WIDEN_PATHS) break;
|
|
367
|
-
}
|
|
368
|
-
return out.sort();
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// withWidenPaths — union admitted widen dirs into a computed sparse set, skipping
|
|
372
|
-
// any the set already covers (cone mode pulls a dir and everything under it, so
|
|
373
|
-
// adding 'scripts/gds' when 'scripts' is present is noise in the git command and
|
|
374
|
-
// noise in the box's scope readout). A null set is the 'full' clone — there is
|
|
375
|
-
// nothing to widen into, so it passes through untouched.
|
|
376
|
-
function withWidenPaths(sparsePaths, admitted) {
|
|
377
|
-
if (!Array.isArray(sparsePaths) || !admitted || admitted.length === 0) return sparsePaths;
|
|
378
|
-
const out = [...sparsePaths];
|
|
379
|
-
for (const dir of admitted) {
|
|
380
|
-
if (withinAny(dir, out)) continue;
|
|
381
|
-
out.push(dir);
|
|
382
|
-
}
|
|
383
|
-
return out;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// PURE: the per-fetch access decision (consumed by GET /box/source-access)
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
|
|
390
|
-
// Decide what source access a builder gets RIGHT NOW. The route reads the live
|
|
391
|
-
// rank+status from the DB and passes them in; this stays pure + unit-tested so the
|
|
392
|
-
// route and any future caller enforce the SAME rule.
|
|
393
|
-
//
|
|
394
|
-
// Rank still governs ALLOW/DENY + the provision floor (an inactive/below-floor
|
|
395
|
-
// builder is still 403, unchanged). What rank NO LONGER decides — once a claim
|
|
396
|
-
// drives it — is the module BREADTH (ADR 0148 / goal 1000051). The optional
|
|
397
|
-
// `scopeKeys` (from the lifecycle port `moduleScopeForActiveClaims`, R92, resolved
|
|
398
|
-
// + passed by the route in R94) selects the surface, three ways:
|
|
399
|
-
// - scopeKeys is a NON-EMPTY array → TASK scope: BASE ∪ the claims' module dirs
|
|
400
|
-
// (via sparsePathsForScopeKeys + the passed-in `moduleScopeMap`).
|
|
401
|
-
// - scopeKeys is an EMPTY array → depends on whether the builder is actually
|
|
402
|
-
// working (see `hasActiveClaims` below).
|
|
403
|
-
// - scopeKeys is ABSENT (undefined) → the pre-ADR-0148 RANK fallback (full for
|
|
404
|
-
// Metic+, else the starter sparse surface). This is the path a vanilla instance
|
|
405
|
-
// with no lifecycle port takes, and it preserves the old behavior EXACTLY — so
|
|
406
|
-
// shipping R93 before R94 wires the route changes nothing in production.
|
|
407
|
-
//
|
|
408
|
-
// TASK 1002729 — the empty-array case is NOT one case, it is two, and collapsing
|
|
409
|
-
// them was a live defect. moduleScopeForActiveClaims returns [] both when the
|
|
410
|
-
// builder holds no claim AND when they hold claims whose goals declare no
|
|
411
|
-
// scope_modules (the goal wall is optional; only 13 of 46 goals on the live
|
|
412
|
-
// instance declared one). Treating the second as "no scope" handed a working
|
|
413
|
-
// builder a box with NO modules/ directory at all — less code than the rank
|
|
414
|
-
// fallback would have given a newcomer. On the live fleet that was 247 of 248
|
|
415
|
-
// source grants, including an Archon holding 7 active claims.
|
|
416
|
-
// - [] + hasActiveClaims true → RANK fallback. The builder is working; an
|
|
417
|
-
// undeclared goal wall is a planning gap, and it must not cripple their box.
|
|
418
|
-
// - [] + hasActiveClaims false → BASE-only, as before (a genuinely unscoped box
|
|
419
|
-
// — the pre-existing Bongos instance, nothing extra). Least privilege holds.
|
|
420
|
-
// `hasActiveClaims` is optional: omitted (undefined) preserves the old BASE-only
|
|
421
|
-
// reading, so a caller that hasn't been taught the signal is unchanged.
|
|
422
|
-
//
|
|
423
|
-
// Returns one of:
|
|
424
|
-
// { allowed:false, reason:'BUILDER_INACTIVE' }
|
|
425
|
-
// { allowed:false, reason:'INSUFFICIENT_RANK', required, actual }
|
|
426
|
-
// { allowed:true, scope, spec }
|
|
427
|
-
function decideSourceAccess({ rank, status, floor = DEFAULT_PROVISION_FLOOR, scopeKeys, hasActiveClaims, moduleScopeMap, widenPaths } = {}) {
|
|
428
|
-
if (status && status !== 'active') {
|
|
429
|
-
return { allowed: false, reason: 'BUILDER_INACTIVE' };
|
|
430
|
-
}
|
|
431
|
-
if (!rankMeetsBoxFloor(rank, floor)) {
|
|
432
|
-
return {
|
|
433
|
-
allowed: false,
|
|
434
|
-
reason: 'INSUFFICIENT_RANK',
|
|
435
|
-
required: floor,
|
|
436
|
-
actual: rank ?? null,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
// The builder's own widening, narrowed to what their rank already reaches
|
|
440
|
-
// (task 1003087). Computed once here so every sparse branch below unions the
|
|
441
|
-
// SAME set — a widen that applied to one scope and not another would be a
|
|
442
|
-
// surprise that only surfaces when a claim changes.
|
|
443
|
-
const widen = admissibleWidenPaths(widenPaths, rank);
|
|
444
|
-
|
|
445
|
-
// Claim-driven task scope (present → the box asks for exactly its work's code).
|
|
446
|
-
if (Array.isArray(scopeKeys)) {
|
|
447
|
-
if (scopeKeys.length > 0) {
|
|
448
|
-
const sparsePaths = withWidenPaths(sparsePathsForScopeKeys(scopeKeys, moduleScopeMap), widen);
|
|
449
|
-
return { allowed: true, scope: 'task', spec: { scope: 'task', mode: 'sparse', sparsePaths } };
|
|
450
|
-
}
|
|
451
|
-
// Empty. A builder mid-claim whose goals declare no wall gets the rank scope,
|
|
452
|
-
// NOT a stripped box — the planning gap is not theirs to pay for.
|
|
453
|
-
if (!hasActiveClaims) {
|
|
454
|
-
const sparsePaths = withWidenPaths(sparsePathsForScopeKeys([], moduleScopeMap), widen);
|
|
455
|
-
return { allowed: true, scope: 'base', spec: { scope: 'base', mode: 'sparse', sparsePaths } };
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
// No claim signal (no lifecycle port on this instance), or claims with an
|
|
459
|
-
// undeclared goal wall: the pre-ADR-0148 rank fallback.
|
|
460
|
-
const scope = boxScopeForRank(rank);
|
|
461
|
-
const spec = cloneSpecForScope(scope);
|
|
462
|
-
// 'full' carries sparsePaths null (the whole repo) — withWidenPaths passes it
|
|
463
|
-
// through, because there is no sparse set to widen INTO.
|
|
464
|
-
return { allowed: true, scope, spec: { ...spec, sparsePaths: withWidenPaths(spec.sparsePaths, widen) } };
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// scopeMetadata — PURE. The credential-FREE projection of a source-access
|
|
468
|
-
// decision, for the read-only GET /box/scope companion (BV1.R100 / goal 1000051):
|
|
469
|
-
// the laptop app (and R104's proof) needs to KNOW what a box holds — which module
|
|
470
|
-
// dirs, which sparse set — WITHOUT being issued the pull credential. It is computed
|
|
471
|
-
// from the SAME decideSourceAccess result the real fetch uses, so the answer can
|
|
472
|
-
// never drift from the actual checkout.
|
|
473
|
-
//
|
|
474
|
-
// `modules` reports the resolved module KEYS a task-scope pulls (the raw scopeKeys
|
|
475
|
-
// from moduleScopeForActiveClaims, deduped + sorted for a stable display) — or null
|
|
476
|
-
// when the decision came from the RANK fallback (scopeKeys absent), where breadth is
|
|
477
|
-
// rank-driven, not module-driven, so there is no per-module list to name.
|
|
478
|
-
function scopeMetadata({ decision, scopeKeys } = {}) {
|
|
479
|
-
if (!decision || !decision.allowed) return null;
|
|
480
|
-
const modules = Array.isArray(scopeKeys)
|
|
481
|
-
? [...new Set(scopeKeys.filter((k) => typeof k === 'string'))].sort()
|
|
482
|
-
: null;
|
|
483
|
-
return {
|
|
484
|
-
scope: decision.scope,
|
|
485
|
-
mode: decision.spec ? decision.spec.mode : null,
|
|
486
|
-
modules,
|
|
487
|
-
sparse_paths: decision.spec ? decision.spec.sparsePaths : null,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// ---------------------------------------------------------------------------
|
|
492
|
-
// PURE: the clawback / reconcile decision (consumed by `box.js reconcile`)
|
|
493
|
-
// ---------------------------------------------------------------------------
|
|
494
|
-
|
|
495
|
-
// Given a box's CURRENT stored scope+state and the builder's LIVE rank+status,
|
|
496
|
-
// what must the control plane do TO THE BOX ITSELF? (The credential layer is
|
|
497
|
-
// already gated live on every fetch; this is the slower droplet-level cleanup.)
|
|
498
|
-
//
|
|
499
|
-
// - no box (state none/destroyed) → 'none' (nothing to do)
|
|
500
|
-
// - builder inactive, or rank below the floor → 'deprovision'
|
|
501
|
-
// (the clawback: revoke rank → de-provision box → source access gone)
|
|
502
|
-
// - stored 'full' but live rank no longer Metic+ → 'deprovision'
|
|
503
|
-
// (a full box already holds the crown jewels on disk — it cannot be safely
|
|
504
|
-
// "downgraded" in place; destroy it, a fresh starter box can be provisioned)
|
|
505
|
-
// - stored 'starter' but live rank is now Metic+ → 'rescope' to 'full'
|
|
506
|
-
// (graduation: the next source-access fetch returns the full spec; the box
|
|
507
|
-
// re-clones on its refresh)
|
|
508
|
-
// - otherwise → 'none' (in sync)
|
|
509
|
-
function reconcileSourceAccess({ boxScope, boxState, rank, status, floor = DEFAULT_PROVISION_FLOOR } = {}) {
|
|
510
|
-
const hasBox = boxState && boxState !== 'none' && boxState !== 'destroyed';
|
|
511
|
-
if (!hasBox) return { action: 'none', reason: 'no-box' };
|
|
512
|
-
|
|
513
|
-
if (status && status !== 'active') {
|
|
514
|
-
return { action: 'deprovision', reason: 'builder-inactive' };
|
|
515
|
-
}
|
|
516
|
-
if (!rankMeetsBoxFloor(rank, floor)) {
|
|
517
|
-
return { action: 'deprovision', reason: 'below-floor' };
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const liveScope = boxScopeForRank(rank);
|
|
521
|
-
if (boxScope === 'full' && liveScope !== 'full') {
|
|
522
|
-
return { action: 'deprovision', reason: 'scope-downgrade-needs-fresh-box' };
|
|
523
|
-
}
|
|
524
|
-
if (boxScope === 'starter' && liveScope === 'full') {
|
|
525
|
-
return { action: 'rescope', reason: 'graduation', newScope: 'full' };
|
|
526
|
-
}
|
|
527
|
-
return { action: 'none', reason: 'in-sync' };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// ---------------------------------------------------------------------------
|
|
531
|
-
// DB helpers (async; take the pg pool or a client)
|
|
532
|
-
// ---------------------------------------------------------------------------
|
|
533
|
-
|
|
534
|
-
// Roster of every box that currently has compute/snapshot state, joined to the
|
|
535
|
-
// builder's LIVE rank+status — the input to `box.js reconcile`. Excludes 'none'
|
|
536
|
-
// and 'destroyed' (nothing to reconcile). Distinct from boxes.listBoxes() because
|
|
537
|
-
// reconcile MUST see `status` (a deactivated builder keeps rank='xenos' but
|
|
538
|
-
// status='inactive', and the clawback turns on status, not just rank).
|
|
539
|
-
async function listBoxesForReconcile(db) {
|
|
540
|
-
const { rows } = await db.query(
|
|
541
|
-
`SELECT bb.*, b.github_login, b.display_name, b.rank, b.status
|
|
542
|
-
FROM builder_boxes bb
|
|
543
|
-
JOIN builders b ON b.id = bb.builder_id
|
|
544
|
-
WHERE bb.state NOT IN ('none', 'destroyed')
|
|
545
|
-
ORDER BY bb.updated_at DESC
|
|
546
|
-
LIMIT 1000`
|
|
547
|
-
);
|
|
548
|
-
return rows;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Append a source-access audit row to box_events (event vocabulary extends the
|
|
552
|
-
// #598 set with: source_grant | source_deny | source_reconcile | source_rescope).
|
|
553
|
-
// Thin wrapper over boxes.recordEvent so callers share ONE audit path.
|
|
554
|
-
async function recordSourceEvent(db, { box, builderId, event, detail, actor }) {
|
|
555
|
-
await recordEvent(db, {
|
|
556
|
-
boxId: box.id,
|
|
557
|
-
builderId: builderId ?? box.builder_id ?? null,
|
|
558
|
-
event,
|
|
559
|
-
detail: detail ?? null,
|
|
560
|
-
actor: actor ?? null,
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Best-effort post-commit hook called by setBuilderRank / deactivateBuilder. Reads
|
|
565
|
-
// the builder's box (if any) + the just-written live rank/status, computes the
|
|
566
|
-
// reconcile action, and — when one is needed — writes a 'source_reconcile' audit
|
|
567
|
-
// row so an operator (and `box.js reconcile`) has an immediate, queryable signal
|
|
568
|
-
// that this rank/status change requires a box action. NEVER THROWS: it runs on its
|
|
569
|
-
// own connection AFTER the rank-change transaction has committed, so a failure here
|
|
570
|
-
// can never roll back the rank change (mirrors the fire-and-forget Discord broadcast).
|
|
571
|
-
//
|
|
572
|
-
// It does NOT itself touch DigitalOcean — the web process must never shell out to
|
|
573
|
-
// the DO API (routes/box.js design); the actual deprovision/rescope is the control-
|
|
574
|
-
// plane CLI's job. This is the signal + the audit trail, not the teardown.
|
|
575
|
-
async function flagBoxReconcileForBuilder(db, builderId, { rank, status, floor = DEFAULT_PROVISION_FLOOR, actor = 'rank-change' } = {}) {
|
|
576
|
-
try {
|
|
577
|
-
const box = await getBoxByBuilderId(db, builderId);
|
|
578
|
-
if (!box) return { action: 'none', reason: 'no-box' };
|
|
579
|
-
const decision = reconcileSourceAccess({
|
|
580
|
-
boxScope: box.scope,
|
|
581
|
-
boxState: box.state,
|
|
582
|
-
rank,
|
|
583
|
-
status,
|
|
584
|
-
floor,
|
|
585
|
-
});
|
|
586
|
-
if (decision.action === 'none') return decision;
|
|
587
|
-
await recordSourceEvent(db, {
|
|
588
|
-
box,
|
|
589
|
-
builderId,
|
|
590
|
-
event: 'source_reconcile',
|
|
591
|
-
detail: `${decision.action}: ${decision.reason} (live rank=${rank ?? 'unknown'}, status=${status ?? 'unknown'})`,
|
|
592
|
-
actor,
|
|
593
|
-
});
|
|
594
|
-
return decision;
|
|
595
|
-
} catch (err) {
|
|
596
|
-
// Swallow — the rank change already committed; this is best-effort audit.
|
|
597
|
-
// eslint-disable-next-line no-console
|
|
598
|
-
console.error('[gds] flagBoxReconcileForBuilder (non-fatal)', err && err.message ? err.message : err);
|
|
599
|
-
return { action: 'error', reason: err && err.message ? err.message : String(err) };
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
module.exports = {
|
|
604
|
-
// pure — surface catalog
|
|
605
|
-
STARTER_SPARSE_PATHS,
|
|
606
|
-
BASE_SPARSE_PATHS, // ADR 0148 / goal 1000051 — the non-product base a task-scoped box layers on
|
|
607
|
-
SOURCE_HOLDING_STATES,
|
|
608
|
-
boxCanHoldSource,
|
|
609
|
-
cloneSpecForScope,
|
|
610
|
-
sparsePathsForScopeKeys, // ADR 0148 — BASE ∪ the claims' module dirs (cone-mode)
|
|
611
|
-
MAX_WIDEN_PATHS, // task 1003087 — the builder-widening cap
|
|
612
|
-
normalizeWidenPath, // task 1003087 — repo-relative dir, or null
|
|
613
|
-
admissibleWidenPaths, // task 1003087 — the widen requests this RANK permits
|
|
614
|
-
withWidenPaths, // task 1003087 — union a widen set into a sparse set
|
|
615
|
-
scopeMetadata, // BV1.R100 — credential-free projection for GET /box/scope
|
|
616
|
-
// pure — decisions
|
|
617
|
-
decideSourceAccess,
|
|
618
|
-
reconcileSourceAccess,
|
|
619
|
-
// db
|
|
620
|
-
listBoxesForReconcile,
|
|
621
|
-
recordSourceEvent,
|
|
622
|
-
flagBoxReconcileForBuilder,
|
|
623
|
-
};
|