@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
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// scripts/gds/box-auth-check.js — dev-box session-start auth gate (task 1712).
|
|
3
|
-
//
|
|
4
|
-
// THE FIRST THING a dev-box session should confirm: that its GDS CLI session is
|
|
5
|
-
// a REAL, full-authority session — not the box-scoped bootstrap token, and not
|
|
6
|
-
// an expired one. A box's baked session is `source:'box'` (ADR 0053 / #919): it
|
|
7
|
-
// may call the on-box bootstrap endpoints but is 403'd (`box_scope`) on
|
|
8
|
-
// claim / ship / publish. Nothing used to surface that until a ship hit the
|
|
9
|
-
// publish-branch wall (the 2026-07-02 incident: a doc couldn't be pushed and the
|
|
10
|
-
// session burned time on raw `git push`). This check makes the session say, up
|
|
11
|
-
// front, "re-auth first" so no work starts on a token that can't finish it.
|
|
12
|
-
//
|
|
13
|
-
// Self-gating + fail-open, by design:
|
|
14
|
-
// * NOT a dev box (no /etc/otb/box.env) → silent no-op. Laptops handle auth at
|
|
15
|
-
// point-of-use and can't be box-scoped, so this stays out of their way.
|
|
16
|
-
// * a subagent (OTB_SUBAGENT) → silent (grader/worker context stays clean).
|
|
17
|
-
// * box-scoped or missing session → the directive comes from the LOCAL file
|
|
18
|
-
// alone (no network) — instant, and exactly the incident case.
|
|
19
|
-
// * a real token present → ONE bounded, fail-open GET /me to also catch an
|
|
20
|
-
// EXPIRED/revoked token; a quiet '✓ authed' on success; silent when the
|
|
21
|
-
// network is flaky (point-of-use still catches a genuine problem — never
|
|
22
|
-
// cry wolf and never delay session start).
|
|
23
|
-
//
|
|
24
|
-
// Printed to stdout so the SessionStart hook forwards it into session context.
|
|
25
|
-
// Wired FIRST (right after the identity card) in .claude/hooks/session-start.js.
|
|
26
|
-
|
|
27
|
-
const fs = require('node:fs');
|
|
28
|
-
|
|
29
|
-
const BOX_ENV_PATH = '/etc/otb/box.env';
|
|
30
|
-
const ME_TIMEOUT_MS = 3000;
|
|
31
|
-
|
|
32
|
-
// Pure decision — given the auth facts, what should the card do? Exported +
|
|
33
|
-
// unit-tested (tests/box_auth_check.mjs). Never does I/O.
|
|
34
|
-
// onBox: provisioned dev box (/etc/otb/box.env present)
|
|
35
|
-
// hasToken: a local session file with a token exists
|
|
36
|
-
// isBoxScoped: that session is the baked box token (source === 'box')
|
|
37
|
-
// meStatus: HTTP status of a bounded GET /me, or null (not attempted / network failed)
|
|
38
|
-
// → { action: 'skip' | 'setup' | 'reauth' | 'ok', reason }
|
|
39
|
-
//
|
|
40
|
-
// Ordering matters: isBoxScoped is decided from the local file BEFORE any
|
|
41
|
-
// meStatus, so the incident case never needs (or waits on) the network.
|
|
42
|
-
function decideBoxAuth({ onBox, hasToken, isBoxScoped, meStatus }) {
|
|
43
|
-
if (!onBox) return { action: 'skip', reason: 'not-a-box' };
|
|
44
|
-
if (!hasToken) return { action: 'setup', reason: 'no-session' };
|
|
45
|
-
if (isBoxScoped) return { action: 'reauth', reason: 'box-scoped' };
|
|
46
|
-
if (meStatus === 200) return { action: 'ok', reason: 'validated' };
|
|
47
|
-
if (meStatus === 401 || meStatus === 403) return { action: 'reauth', reason: 'expired-or-revoked' };
|
|
48
|
-
// Couldn't definitively verify (no network / fetch unavailable / timeout /
|
|
49
|
-
// 5xx). A locally valid-looking real token is trusted; stay silent rather
|
|
50
|
-
// than warn falsely — the point-of-use guard (cli-lib) still catches a real
|
|
51
|
-
// 401/403 the moment a command runs.
|
|
52
|
-
return { action: 'skip', reason: 'unverified' };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function onBox() {
|
|
56
|
-
try {
|
|
57
|
-
return fs.existsSync(BOX_ENV_PATH);
|
|
58
|
-
} catch (_) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Bounded, fail-open GET /me. Returns { status, login }; status is null and
|
|
64
|
-
// login is null on ANY failure (no global fetch, network error, abort/timeout).
|
|
65
|
-
// Never throws.
|
|
66
|
-
async function probeMe(tokenInfo) {
|
|
67
|
-
if (!tokenInfo || !tokenInfo.token || typeof fetch !== 'function') {
|
|
68
|
-
return { status: null, login: null };
|
|
69
|
-
}
|
|
70
|
-
const ctl = new AbortController();
|
|
71
|
-
const timer = setTimeout(() => ctl.abort(), ME_TIMEOUT_MS);
|
|
72
|
-
try {
|
|
73
|
-
const res = await fetch(`${tokenInfo.apiBase}/api/bongos/me`, {
|
|
74
|
-
method: 'GET',
|
|
75
|
-
headers: { Authorization: `Bearer ${tokenInfo.token}`, 'User-Agent': 'box-auth-check/1.0' },
|
|
76
|
-
signal: ctl.signal,
|
|
77
|
-
});
|
|
78
|
-
let login = null;
|
|
79
|
-
if (res.ok) {
|
|
80
|
-
try {
|
|
81
|
-
const j = await res.json();
|
|
82
|
-
login = (j && j.builder && j.builder.github_login) || null;
|
|
83
|
-
} catch (_) { /* body parse is cosmetic only */ }
|
|
84
|
-
}
|
|
85
|
-
return { status: res.status, login };
|
|
86
|
-
} catch (_) {
|
|
87
|
-
return { status: null, login: null };
|
|
88
|
-
} finally {
|
|
89
|
-
clearTimeout(timer);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function printReauth({ boxScoped }) {
|
|
94
|
-
const why = boxScoped
|
|
95
|
-
? 'This box holds the BOX-SCOPED bootstrap token (source=box, ADR 0053) — it can bootstrap the box but is refused (403) on claim / ship / publish.'
|
|
96
|
-
: 'This session token is REVOKED or otherwise rejected (CLI tokens do not expire on their own — ADR 0294).';
|
|
97
|
-
console.log('[otb] ── dev box: RE-AUTH BEFORE YOU WORK ──');
|
|
98
|
-
console.log(`[otb] ${why}`);
|
|
99
|
-
console.log('[otb] ⚠ FIRST STEP this session: get the builder onto a real CLI session BEFORE claiming or shipping —');
|
|
100
|
-
console.log('[otb] run /builder-reauth (UI-first: opens Settings → "Re-issue CLI token" → paste the one-liner back).');
|
|
101
|
-
console.log('[otb] Work claimed on this token cannot be shipped/published — it will strand at the publish 403 wall.');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function printSetup() {
|
|
105
|
-
console.log('[otb] ── dev box: NO GDS SESSION — SIGN IN BEFORE YOU WORK ──');
|
|
106
|
-
console.log('[otb] ⚠ No CLI session was found on this box. FIRST STEP this session: sign in before claiming or');
|
|
107
|
-
console.log('[otb] shipping any work — run /builder-reauth (UI-first), or /builder-setup for a brand-new machine.');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function main() {
|
|
111
|
-
if (process.env.OTB_SUBAGENT) return;
|
|
112
|
-
if (!onBox()) return;
|
|
113
|
-
|
|
114
|
-
let lib;
|
|
115
|
-
let session = null;
|
|
116
|
-
let tokenInfo = null;
|
|
117
|
-
try {
|
|
118
|
-
lib = require('./cli-lib');
|
|
119
|
-
session = lib.loadSessionSync();
|
|
120
|
-
tokenInfo = lib.readSessionToken();
|
|
121
|
-
} catch (_) {
|
|
122
|
-
// cli-lib unreadable → treat as no-session (the setup path), which is the
|
|
123
|
-
// safe, actionable default rather than silence.
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const hasToken = !!(session && session.token);
|
|
127
|
-
const isBoxScoped = !!(session && session.source === 'box');
|
|
128
|
-
|
|
129
|
-
// Only pay a network round-trip when a real (non-box) token is present — the
|
|
130
|
-
// box-scoped and no-session cases are already decided from the file alone.
|
|
131
|
-
let meStatus = null;
|
|
132
|
-
let login = null;
|
|
133
|
-
if (hasToken && !isBoxScoped) {
|
|
134
|
-
({ status: meStatus, login } = await probeMe(tokenInfo));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const { action } = decideBoxAuth({ onBox: true, hasToken, isBoxScoped, meStatus });
|
|
138
|
-
if (action === 'reauth') {
|
|
139
|
-
printReauth({ boxScoped: isBoxScoped });
|
|
140
|
-
} else if (action === 'setup') {
|
|
141
|
-
printSetup();
|
|
142
|
-
} else if (action === 'ok') {
|
|
143
|
-
console.log(`[otb] ✓ dev box: GDS session is a real CLI session${login ? ` (signed in as ${login})` : ''} — clear to claim + ship.`);
|
|
144
|
-
}
|
|
145
|
-
// action === 'skip' → say nothing (off-box handled above; 'unverified' is silent).
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Direct run (the SessionStart hook spawns `node .../box-auth-check.js`) — run
|
|
149
|
-
// the check and exit fast. A require() of this file (the unit test importing
|
|
150
|
-
// decideBoxAuth) must NOT run main, so guard on require.main. The watchdog is
|
|
151
|
-
// belt-and-suspenders: the SessionStart hook spawns this synchronously with no
|
|
152
|
-
// timeout of its own, so guarantee a bounded exit even if something wedges (the
|
|
153
|
-
// fetch already self-aborts at ME_TIMEOUT_MS).
|
|
154
|
-
if (require.main === module) {
|
|
155
|
-
const watchdog = setTimeout(() => process.exit(0), ME_TIMEOUT_MS + 1500);
|
|
156
|
-
watchdog.unref();
|
|
157
|
-
main().then(() => process.exit(0)).catch(() => process.exit(0));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
module.exports = { decideBoxAuth, onBox, printReauth, printSetup };
|
package/scripts/gds/box-infra.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
// scripts/gds/box-infra.js — the dev-box CONFIG block plus everything that
|
|
2
|
-
// turns it into a bootable droplet: infra/ resolution, naming, the DNS hook,
|
|
3
|
-
// and cloud-init rendering.
|
|
4
|
-
//
|
|
5
|
-
// Carved out of scripts/gds/box.js by task 1003206 (goal 1000079, the R03
|
|
6
|
-
// de-monolith plan) to bring that file under the 1,500-line ratchet. A
|
|
7
|
-
// STRUCTURAL move: the lines below are box.js's originals unchanged, and box.js
|
|
8
|
-
// requires every name straight back, so the operator CLI is untouched.
|
|
9
|
-
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const fs = require('node:fs');
|
|
13
|
-
const path = require('node:path');
|
|
14
|
-
const zlib = require('node:zlib');
|
|
15
|
-
const { execFileSync } = require('node:child_process');
|
|
16
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
17
|
-
const instanceConfig = require(path.join(REPO_ROOT, 'src/instance-config'));
|
|
18
|
-
const BOX_INFRA_TEMPLATE = 'builder-box-cloud-init.yaml';
|
|
19
|
-
|
|
20
|
-
function boxInfraCandidates({ env = process.env, repoRoot = REPO_ROOT } = {}) {
|
|
21
|
-
const out = [];
|
|
22
|
-
const seen = new Set();
|
|
23
|
-
const add = (d) => { if (d && !seen.has(d)) { seen.add(d); out.push(d); } };
|
|
24
|
-
// 1. explicit override: <PREFIX>_BOX_INFRA_DIR (operator escape hatch).
|
|
25
|
-
const override = instanceConfig.resolveEnv('BOX_INFRA_DIR', { env });
|
|
26
|
-
if (override) add(path.resolve(override));
|
|
27
|
-
// 2. the instance root via an EXPLICIT pointer (<PREFIX>_INSTANCE_ROOT env or a
|
|
28
|
-
// bongos.config.json {"instanceRoot":"."}) — where a vendored instance keeps infra/.
|
|
29
|
-
const inst = instanceConfig.resolveInstanceRootExplicit({ env });
|
|
30
|
-
if (inst) add(path.join(inst, 'infra'));
|
|
31
|
-
// 3. REPO_ROOT/infra — the dev single-checkout (and the ADR 0144 node_modules workaround).
|
|
32
|
-
add(path.join(repoRoot, 'infra'));
|
|
33
|
-
// 4. walk up out of node_modules from the core root, so a vendored
|
|
34
|
-
// …/node_modules/@bongos/core still finds its host instance's infra/ with no pointer set.
|
|
35
|
-
let dir = repoRoot;
|
|
36
|
-
for (let i = 0; i < 6; i += 1) {
|
|
37
|
-
const parent = path.dirname(dir);
|
|
38
|
-
if (parent === dir) break;
|
|
39
|
-
dir = parent;
|
|
40
|
-
add(path.join(dir, 'infra'));
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// The resolved infra/ dir (first candidate holding the template), or null when the
|
|
46
|
-
// box cloud-init cannot be located anywhere — the caller MUST fail loud on null
|
|
47
|
-
// rather than provision a box with empty user-data (ADR 0150).
|
|
48
|
-
function resolveBoxInfraDir(opts = {}) {
|
|
49
|
-
for (const c of boxInfraCandidates(opts)) {
|
|
50
|
-
try { if (fs.existsSync(path.join(c, BOX_INFRA_TEMPLATE))) return c; }
|
|
51
|
-
catch { /* unreadable candidate → try the next */ }
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// task 1002757 — the instance's own apex; null when unconfigured (callers refuse).
|
|
57
|
-
const { instanceApex } = require('./instance-apex');
|
|
58
|
-
|
|
59
|
-
const CONFIG = {
|
|
60
|
-
floor: process.env.BOX_PROVISION_MIN_RANK || 'xenos',
|
|
61
|
-
region: process.env.BOX_REGION || 'nyc3',
|
|
62
|
-
size: process.env.BOX_SIZE || 's-2vcpu-4gb',
|
|
63
|
-
baseImage: process.env.BOX_BASE_IMAGE || 'ubuntu-24-04-x64',
|
|
64
|
-
idleMinutes: numEnv('BOX_IDLE_MINUTES', 10),
|
|
65
|
-
// task 1003507 — the longest a box may stay active without evidence that a
|
|
66
|
-
// HUMAN was attached to it. One number bounds both proxies that used to keep a
|
|
67
|
-
// box alive on their own (a running `claude` process; load above the floor), so
|
|
68
|
-
// an abandoned session can no longer pin a droplet forever. 12h is deliberately
|
|
69
|
-
// generous — longer than any interactive session, long enough for a real
|
|
70
|
-
// overnight autonomous run — because being wrong costs one wake, while having
|
|
71
|
-
// no cap at all cost $20.21 on a single box.
|
|
72
|
-
unattendedMaxHours: numEnv('BOX_UNATTENDED_MAX_HOURS', 12),
|
|
73
|
-
dormantDays: numEnv('BOX_DORMANT_DAYS', 14),
|
|
74
|
-
// task 1002727 — how many days BEFORE the reclaim a builder gets warned.
|
|
75
|
-
dormantWarnLeadDays: numEnv('BOX_DORMANT_WARN_LEAD_DAYS', 3),
|
|
76
|
-
sshKeyIds: (process.env.BOX_SSH_KEY_IDS || '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
77
|
-
dnsHook: process.env.BOX_DNS_HOOK || null,
|
|
78
|
-
tag: process.env.BOX_TAG || 'otb-builder-box',
|
|
79
|
-
// task 1002757 settled this file's old carve-out: an operator-plane DNS zone is
|
|
80
|
-
// still a FUNCTIONAL DEFAULT, and a founder literal aimed another operator's
|
|
81
|
-
// fleet at the founder's zone. Derived from the instance apex, null when
|
|
82
|
-
// unconfigured — hostnameFor() then refuses instead of minting a dead FQDN.
|
|
83
|
-
dnsZone: process.env.BOX_DNS_ZONE || (instanceApex() ? `dev.${instanceApex()}` : null),
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Named-tunnel automation for the browser terminal (#771, ADR 0038 Part B).
|
|
87
|
-
// OPT-IN, default OFF — an unset BOX_NAMED_TUNNEL leaves the box on the tokenless
|
|
88
|
-
// quick tunnel (trycloudflare.com), the zero-secret #760 default. When the operator
|
|
89
|
-
// turns it on AND supplies a Cloudflare token with Tunnel + DNS scope (the same
|
|
90
|
-
// CLOUDFLARE_API_TOKEN box-dns uses) + the account id, every provision creates a
|
|
91
|
-
// stable term-<login>.<terminalZone> tunnel and bakes its connector token into the
|
|
92
|
-
// box; deprovision tears it down.
|
|
93
|
-
const CF = {
|
|
94
|
-
enabled: /^(1|true|yes)$/i.test(process.env.BOX_NAMED_TUNNEL || ''),
|
|
95
|
-
apiToken: process.env.CLOUDFLARE_API_TOKEN || null,
|
|
96
|
-
accountId: process.env.CLOUDFLARE_ACCOUNT_ID || null,
|
|
97
|
-
// The Cloudflare ZONE the record lives in (the registrable apex) — the terminal
|
|
98
|
-
// host is a record WITHIN it, matching box-dns-cloudflare.sh. Distinct from
|
|
99
|
-
// terminalZone (the host suffix). Both resolve from the instance (task 1002757).
|
|
100
|
-
zone: process.env.CLOUDFLARE_ZONE || instanceApex(),
|
|
101
|
-
// Host suffix for term-<login>.<terminalZone>. DEFAULTS TO THE APEX, NOT dev.*:
|
|
102
|
-
// the terminal rides a PROXIED tunnel, and free Universal SSL covers <apex> +
|
|
103
|
-
// *.<apex> (ONE label) but not a TWO-label host like term-x.dev.<apex>, which
|
|
104
|
-
// fails TLS at the edge (verified live, #771). An operator holding an Advanced
|
|
105
|
-
// Certificate for *.dev.<apex> can override via BOX_TERMINAL_ZONE. (The SSH
|
|
106
|
-
// A-record path stays on dev.* — proxied=false, so it needs no edge cert.)
|
|
107
|
-
terminalZone: process.env.BOX_TERMINAL_ZONE || instanceApex(),
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// Named-tunnel automation is live only when the operator opted in AND the
|
|
111
|
-
// credentials are present. Missing creds while enabled is a loud config error at
|
|
112
|
-
// provision time (see cmdProvision), not a silent fall-back, so an operator who
|
|
113
|
-
// meant to use named tunnels learns immediately.
|
|
114
|
-
function numEnv(name, dflt) {
|
|
115
|
-
const v = process.env[name];
|
|
116
|
-
if (v == null || v === '') return dflt;
|
|
117
|
-
const n = Number(v);
|
|
118
|
-
return Number.isFinite(n) && n > 0 ? n : dflt;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function hasFlag(args, name) { return args.includes(name); }
|
|
122
|
-
|
|
123
|
-
// Lazy + memoized DB + lib load so `--help` and arg errors don't require Postgres
|
|
124
|
-
// (this CLI runs on a non-droplet machine for dry-run planning of pure logic too):
|
|
125
|
-
// nothing is require()d until the first makeDo/makeCf/IIFE call, and repeat calls
|
|
126
|
-
// reuse the first resolution instead of re-running the six requires.
|
|
127
|
-
let _deps;
|
|
128
|
-
function shortStamp() {
|
|
129
|
-
return new Date().toISOString().replace(/[-:T.Z]/g, '').slice(2, 14);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function dropletName(login) { return `otb-box-${login}`; }
|
|
133
|
-
// Refuses on an unresolved zone (task 1002757) — `box-x.undefined` would mint a
|
|
134
|
-
// DNS record and a box that can never be reached.
|
|
135
|
-
function hostnameFor(login) {
|
|
136
|
-
if (!CONFIG.dnsZone) throw new Error('box: no DNS zone configured — set BOX_DNS_ZONE, or give this instance a domains.publicOrigin in config/branding.json (task 1002757).');
|
|
137
|
-
return `box-${login}.${CONFIG.dnsZone}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Run the optional DNS hook so the per-builder hostname tracks the new IP; with no
|
|
141
|
-
// hook configured, print the reminder — the stable-hostname CUTOVER is #599's, #598
|
|
142
|
-
// only records IP + hostname and offers the hook so an operator can wire DNS without
|
|
143
|
-
// editing this file. → { ok, error?, skipped? }. task 1180: callers MUST act on
|
|
144
|
-
// ok:false (box_events row + flag the box), never continue silently — that swallow
|
|
145
|
-
// left @kingaonline's A record pinned at a dead IP while provision reported success.
|
|
146
|
-
// The hook retries transient Cloudflare blips, so ok:false is a real, exhausted
|
|
147
|
-
// failure. `hook` is injectable so the contract is unit-testable (#1034).
|
|
148
|
-
function runDnsHook(hostname, ip, hook = CONFIG.dnsHook) {
|
|
149
|
-
if (!ip) return { ok: true, skipped: true };
|
|
150
|
-
if (!hook) {
|
|
151
|
-
console.log(` ↳ DNS: point ${hostname} → ${ip} (no BOX_DNS_HOOK set; #599 owns the cutover)`);
|
|
152
|
-
return { ok: true, skipped: true };
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
execFileSync('bash', ['-c', hook], {
|
|
156
|
-
cwd: REPO_ROOT, stdio: 'pipe',
|
|
157
|
-
env: { ...process.env, BOX_HOSTNAME: hostname, BOX_IP: ip },
|
|
158
|
-
});
|
|
159
|
-
console.log(` ↳ DNS hook updated ${hostname} → ${ip}`);
|
|
160
|
-
return { ok: true };
|
|
161
|
-
} catch (e) {
|
|
162
|
-
const msg = (e.stderr && e.stderr.toString().trim()) || e.message;
|
|
163
|
-
console.log(` ⚠ DNS hook failed: ${msg} (point ${hostname} → ${ip} manually)`);
|
|
164
|
-
return { ok: false, error: msg };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Blackhole sentinel for a torn-down box's A record (idea 309). 192.0.2.1 is in
|
|
169
|
-
// TEST-NET-1 (RFC 5737) — reserved for documentation and GUARANTEED never to be a
|
|
170
|
-
// routable host, so no recycled DigitalOcean IP can ever land here. A perfect
|
|
171
|
-
// dead end to park a record at.
|
|
172
|
-
const DNS_BLACKHOLE_IP = '192.0.2.1';
|
|
173
|
-
|
|
174
|
-
// Park the per-builder A record at the blackhole sentinel on deprovision (idea 309,
|
|
175
|
-
// refining task 1180). 1180 DELETED the record so it couldn't dangle at a recycled
|
|
176
|
-
// DO IP, but delete-then-recreate across a reap→rebuild left an NXDOMAIN window that
|
|
177
|
-
// resolvers negative-cache for the zone's SOA neg-TTL (~30 min, not builder-editable)
|
|
178
|
-
// — a client querying then kept getting ENOTFOUND long after the box was back.
|
|
179
|
-
// REPOINTING to a non-routable sentinel keeps the name always-resolving (nothing to
|
|
180
|
-
// negative-cache) while preserving 1180's guarantee that a torn-down box's name can
|
|
181
|
-
// never dial a stranger's recycled IP; the next provision/wake PUTs the live IP
|
|
182
|
-
// in-place at the 60s TTL, no gap. Best-effort like the prior delete. Reuses
|
|
183
|
-
// runDnsHook's UPSERT path; `hook` injectable for tests. → { ok, error?, skipped? }
|
|
184
|
-
function runDnsHookBlackhole(hostname, hook = CONFIG.dnsHook) {
|
|
185
|
-
if (!hook || !hostname) return { ok: true, skipped: true };
|
|
186
|
-
return runDnsHook(hostname, DNS_BLACKHOLE_IP, hook);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Read the cloud-init template and substitute the repo URL from BOX_REPO_URL
|
|
190
|
-
// (kept out of the repo file so no GitHub identity is hardcoded — V3.R12 rule).
|
|
191
|
-
// An unset URL leaves the placeholder; the cloud-init then logs guidance rather
|
|
192
|
-
// than cloning, and #600 wires the scoped credential.
|
|
193
|
-
// Read + render the cloud-init. `authorizedKeysBlock` is the otb-managed block
|
|
194
|
-
// for the builder being provisioned (#685) — base64'd here and baked in so the
|
|
195
|
-
// FIRST SSH login works before the box has a GDS token. An empty/absent block
|
|
196
|
-
// renders an empty managed block (a keyless box; the operator is warned upstream).
|
|
197
|
-
function readCloudInit({ authorizedKeysBlock = '', boxEnv = '', gdsSession = '', infraDir = resolveBoxInfraDir() } = {}) {
|
|
198
|
-
// Fail loud, never provision a BARE box (ADR 0150). No infra/ located → there is
|
|
199
|
-
// no cloud-init to render, so THROW here rather than the old silent `return null`
|
|
200
|
-
// that let cmdProvision ship empty user-data (a box that boots as bare Ubuntu).
|
|
201
|
-
if (!infraDir) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`box cloud-init not found: no infra/${BOX_INFRA_TEMPLATE} in any candidate location `
|
|
204
|
-
+ `(set <PREFIX>_BOX_INFRA_DIR or <PREFIX>_INSTANCE_ROOT, or ship infra/). A box must `
|
|
205
|
-
+ `never be provisioned without user-data — it would boot bare (ADR 0150).`,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
const p = path.join(infraDir, BOX_INFRA_TEMPLATE);
|
|
209
|
-
// gzip+base64 a repo file into the cloud-init write_files (`encoding: gz+b64`).
|
|
210
|
-
// EVERY inlined infra file goes through this: cloud-init gunzips it back to the
|
|
211
|
-
// exact bytes on the box, so it is byte-identical to plain b64 but ~2-3x smaller,
|
|
212
|
-
// which keeps the rendered user-data well under DO's 64 KiB cap (#903; the source
|
|
213
|
-
// -fetch growth in task 1314 had pushed the all-b64-but-source-fetch render to
|
|
214
|
-
// 67 KB — gz+b64-ing the rest brings it back to ~51 KB, task 1321). Only the
|
|
215
|
-
// dynamic runtime values (authorized_keys, box.env, gds-session) stay plain b64
|
|
216
|
-
// below — they are minted per-provision, not read from infra/. A missing inlined
|
|
217
|
-
// file is COLLECTED and fails loud after the render (ADR 0150) — a half-wired box
|
|
218
|
-
// (that component silently never boots) is a bug, not something to warn past.
|
|
219
|
-
//
|
|
220
|
-
// Every name passed to fileGzB64 below is INSTANCE-owned and deliberately absent
|
|
221
|
-
// from the neutral core's own infra/ — see infra/README.md (ADR 0150).
|
|
222
|
-
const missingInfra = [];
|
|
223
|
-
const fileGzB64 = (name) => {
|
|
224
|
-
try {
|
|
225
|
-
return zlib.gzipSync(fs.readFileSync(path.join(infraDir, name))).toString('base64');
|
|
226
|
-
} catch {
|
|
227
|
-
missingInfra.push(name);
|
|
228
|
-
return '';
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
const raw = fs.readFileSync(p, 'utf8');
|
|
232
|
-
// Inject the rank-gated source-fetch (#600) + authorized-keys-fetch (#685)
|
|
233
|
-
// scripts as gz+b64 (a single YAML-safe token — no indentation/escaping
|
|
234
|
-
// pitfalls; cloud-init gunzips encoding: gz+b64 back to the exact file), plus
|
|
235
|
-
// the baked authorized_keys block. ONE canonical copy of each lives in infra/.
|
|
236
|
-
const rendered = raw
|
|
237
|
-
.replace(/__REPO_URL__/g, process.env.BOX_REPO_URL || '')
|
|
238
|
-
.replace(/__BOX_SOURCE_FETCH_B64__/g, fileGzB64('box-source-fetch.sh'))
|
|
239
|
-
.replace(/__BOX_AUTHKEYS_FETCH_B64__/g, fileGzB64('box-authorized-keys-fetch.sh'))
|
|
240
|
-
.replace(/__BUILDER_AUTHORIZED_KEYS_B64__/g, Buffer.from(authorizedKeysBlock || '', 'utf8').toString('base64'))
|
|
241
|
-
// /etc/otb/box.env — empty by default (quick tunnel); the named-tunnel
|
|
242
|
-
// connector token + stable hostname when BOX_NAMED_TUNNEL is on (#771).
|
|
243
|
-
.replace(/__BOX_ENV_B64__/g, Buffer.from(boxEnv || '', 'utf8').toString('base64'))
|
|
244
|
-
// /root/.config/otb/bongos-session.json — the builder's Bongos session, minted at
|
|
245
|
-
// provision so box-source-fetch (#600) can authenticate on FIRST boot and
|
|
246
|
-
// clone the rank-scoped repo. Without it the box never clones (the #818
|
|
247
|
-
// chicken-and-egg: the session normally comes from /builder-setup, which
|
|
248
|
-
// lives in the un-cloned repo).
|
|
249
|
-
.replace(/__GDS_SESSION_B64__/g, Buffer.from(gdsSession || '', 'utf8').toString('base64'))
|
|
250
|
-
// #774: the browser-terminal units, inlined so they start at FIRST BOOT
|
|
251
|
-
// (independent of the repo clone). ONE canonical copy lives in infra/.
|
|
252
|
-
.replace(/__BOX_TERMINAL_SERVICE_B64__/g, fileGzB64('box-terminal.service'))
|
|
253
|
-
// #917: credential-drift watcher pair — restarts ttyd when the credential
|
|
254
|
-
// file lands/changes after the service started (the wake/first-boot race).
|
|
255
|
-
.replace(/__BOX_TERMINAL_CREDSYNC_SERVICE_B64__/g, fileGzB64('box-terminal-credsync.service'))
|
|
256
|
-
.replace(/__BOX_TERMINAL_CREDENTIAL_PATH_B64__/g, fileGzB64('box-terminal-credential.path'))
|
|
257
|
-
.replace(/__CLOUDFLARED_SERVICE_B64__/g, fileGzB64('cloudflared.service'))
|
|
258
|
-
.replace(/__CLOUDFLARED_RUN_B64__/g, fileGzB64('cloudflared-run.sh'))
|
|
259
|
-
// #893: the `close-box` teardown command, present at first boot so a bare
|
|
260
|
-
// browser-only box has it before any repo clone (box-source-fetch refreshes
|
|
261
|
-
// it on existing boxes). ONE canonical copy lives in infra/close-box.sh.
|
|
262
|
-
.replace(/__CLOSE_BOX_B64__/g, fileGzB64('close-box.sh'))
|
|
263
|
-
// task 986: the browser-terminal auto-launcher — runs `claude` on first
|
|
264
|
-
// connect so the builder skips the "type claude" step. box-source-fetch
|
|
265
|
-
// refreshes it on existing boxes. ONE canonical copy in infra/otb-box-launch.sh.
|
|
266
|
-
.replace(/__OTB_BOX_LAUNCH_B64__/g, fileGzB64('otb-box-launch.sh'));
|
|
267
|
-
if (missingInfra.length) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
`box cloud-init is incomplete under ${infraDir}: missing ${missingInfra.join(', ')} `
|
|
270
|
-
+ `— a box would boot half-wired (ADR 0150). Fix the instance infra/ before provisioning.`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
// ADR 0150 prong B named this escalation and never implemented it: "assert no
|
|
274
|
-
// __…_B64__ placeholders survive the render, so a *partial* infra/ also surfaces
|
|
275
|
-
// rather than half-wiring". The missingInfra check above only catches files THIS
|
|
276
|
-
// file knows to ask for. It cannot catch the other direction — an instance
|
|
277
|
-
// template carrying a token box-infra.js does not substitute. That renders a
|
|
278
|
-
// cloud-init with the literal token in it, and cloud-init happily writes the
|
|
279
|
-
// garbage as a file: the box boots half-wired, silently, which is the one
|
|
280
|
-
// outcome ADR 0150 exists to prevent.
|
|
281
|
-
//
|
|
282
|
-
// The stakes are not hypothetical for one of them. __BUILDER_AUTHORIZED_KEYS_B64__
|
|
283
|
-
// is the baked SSH key block; if a template misspells it, the substitution never
|
|
284
|
-
// happens and the box comes up with a garbage authorized_keys — permanently
|
|
285
|
-
// unreachable over SSH, exactly the failure task 1003635 was filed about (it
|
|
286
|
-
// guessed the wrong mechanism, but the outcome it named was reachable this way).
|
|
287
|
-
//
|
|
288
|
-
// Same pattern the happy-path test has always asserted, now enforced in the code
|
|
289
|
-
// rather than only in the test.
|
|
290
|
-
// The pattern is the placeholder CONVENTION, so this assumes a template never
|
|
291
|
-
// carries a literal __UPPER_SNAKE__ it means to keep. Nothing in this tree does,
|
|
292
|
-
// and a false positive fails loud and legibly (it names the token) rather than
|
|
293
|
-
// shipping a bad box — the safe direction to be wrong in. An instance that ever
|
|
294
|
-
// needs a literal one should escape it in the template, not weaken this check.
|
|
295
|
-
const survivors = [...new Set(rendered.match(/__[A-Z0-9_]+__/g) || [])];
|
|
296
|
-
if (survivors.length) {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`box cloud-init under ${infraDir} left ${survivors.length} placeholder(s) unsubstituted: `
|
|
299
|
-
+ `${survivors.join(', ')} — box-infra.js does not know these tokens, so the box would `
|
|
300
|
-
+ `boot half-wired (ADR 0150). Reconcile the instance template with this renderer.`,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
return rendered;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
module.exports = {
|
|
308
|
-
CF,
|
|
309
|
-
CONFIG,
|
|
310
|
-
boxInfraCandidates,
|
|
311
|
-
dropletName,
|
|
312
|
-
hasFlag,
|
|
313
|
-
hostnameFor,
|
|
314
|
-
instanceApex,
|
|
315
|
-
numEnv,
|
|
316
|
-
readCloudInit,
|
|
317
|
-
resolveBoxInfraDir,
|
|
318
|
-
runDnsHook,
|
|
319
|
-
runDnsHookBlackhole,
|
|
320
|
-
shortStamp,
|
|
321
|
-
};
|