@bongos/core 1.17.0
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 +5727 -0
- package/.claude/skills/blocker-review/SKILL.md +91 -0
- package/.claude/skills/blocker-solve/SKILL.md +96 -0
- package/.claude/skills/builder-backup/SKILL.md +54 -0
- package/.claude/skills/builder-box/SKILL.md +45 -0
- package/.claude/skills/builder-claim/SKILL.md +76 -0
- package/.claude/skills/builder-connect/SKILL.md +59 -0
- package/.claude/skills/builder-cost/SKILL.md +47 -0
- package/.claude/skills/builder-end/SKILL.md +81 -0
- package/.claude/skills/builder-exit/SKILL.md +88 -0
- package/.claude/skills/builder-key/SKILL.md +40 -0
- package/.claude/skills/builder-reauth/SKILL.md +50 -0
- package/.claude/skills/builder-redteam/SKILL.md +109 -0
- package/.claude/skills/builder-release/SKILL.md +41 -0
- package/.claude/skills/builder-setup/SKILL.md +62 -0
- package/.claude/skills/builder-ship/SKILL.md +79 -0
- package/.claude/skills/builder-stage/SKILL.md +51 -0
- package/.claude/skills/builder-start/SKILL.md +46 -0
- package/.claude/skills/close-box/SKILL.md +43 -0
- package/.claude/skills/design-sync/SKILL.md +69 -0
- package/.claude/skills/dev/SKILL.md +49 -0
- package/.claude/skills/feedback/SKILL.md +32 -0
- package/.claude/skills/figma-design-sync/SKILL.md +74 -0
- package/.claude/skills/goal-review/SKILL.md +89 -0
- package/.claude/skills/idea-triage/SKILL.md +144 -0
- package/.claude/skills/ideate/SKILL.md +82 -0
- package/.claude/skills/merge-mode/SKILL.md +169 -0
- package/.claude/skills/new-project/SKILL.md +108 -0
- package/.claude/skills/otb-character-review/SKILL.md +71 -0
- package/.claude/skills/otb-design-review/SKILL.md +47 -0
- package/.claude/skills/otb-feedback-capture/SKILL.md +45 -0
- package/.claude/skills/otb-figma-sync/SKILL.md +54 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +45 -0
- package/.claude/skills/paint/SKILL.md +70 -0
- package/.claude/skills/planning-session/SKILL.md +299 -0
- package/.claude/skills/priority-session/SKILL.md +134 -0
- package/.claude/skills/read-session-export/SKILL.md +58 -0
- package/.claude/skills/recall/SKILL.md +52 -0
- package/.claude/skills/session-handoff/SKILL.md +46 -0
- package/.claude/skills/status/SKILL.md +33 -0
- package/.devcontainer/Dockerfile +69 -0
- package/.devcontainer/README.md +73 -0
- package/.devcontainer/devcontainer-lock.json +19 -0
- package/.devcontainer/devcontainer.json +93 -0
- package/.devcontainer/healthcheck.sh +84 -0
- package/.devcontainer/init-firewall.sh +198 -0
- package/.devcontainer/post-create.sh +153 -0
- package/.dockerignore +36 -0
- package/.env.local.example +32 -0
- package/.gitattributes +83 -0
- package/.gitignore +91 -0
- package/CLAUDE.md +242 -0
- package/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +51 -0
- package/Dockerfile +46 -0
- package/GOVERNANCE.md +44 -0
- package/LICENSE +661 -0
- package/README.md +88 -0
- package/bin/bongos.js +222 -0
- package/clients/bongos-client/README.md +48 -0
- package/clients/bongos-client/bongos-client.global.js +617 -0
- package/clients/bongos-client/examples/hello-world.mjs +62 -0
- package/clients/bongos-client/index.cjs +619 -0
- package/clients/bongos-client/index.d.ts +793 -0
- package/clients/bongos-client/index.mjs +614 -0
- package/clients/bongos-client/package.json +32 -0
- package/config/branding.neutral.json +60 -0
- package/config/design-tokens.neutral.json +51 -0
- package/config/design-tokens.schema.json +96 -0
- package/config/modules.neutral.json +9 -0
- package/docker-compose.yml +52 -0
- package/docker-entrypoint.sh +30 -0
- package/docs/adr/0001-stack-choice-single-vps.md +45 -0
- package/docs/adr/0002-digitalocean-over-hetzner.md +27 -0
- package/docs/adr/0003-cloudflare-origin-cert-over-letsencrypt.md +35 -0
- package/docs/adr/0004-amazonprimea-name-and-trademark-acceptance.md +45 -0
- package/docs/adr/0005-invisible-other-players-in-v1.md +37 -0
- package/docs/adr/0006-singleton-worldroom-plus-queueroom.md +30 -0
- package/docs/adr/0007-persistent-identity-via-localstorage-uuid.md +31 -0
- package/docs/adr/0008-family-based-tile-generation.md +86 -0
- package/docs/adr/0008-google-chat-oauth-user-auth.md +37 -0
- package/docs/adr/0009-autotile-architecture.md +181 -0
- package/docs/adr/0010-pms-architecture.md +70 -0
- package/docs/adr/0011-terrain-id-rendering-model.md +174 -0
- package/docs/adr/0012-ground-object-cell-split.md +208 -0
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +158 -0
- package/docs/adr/0014-families-canonical-over-vocabulary.md +103 -0
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +125 -0
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +132 -0
- package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +74 -0
- package/docs/adr/0018-three-rank-model-goes-live.md +97 -0
- package/docs/adr/0019-red-team-bounty-payout-table.md +52 -0
- package/docs/adr/0020-security-model-public-stub.md +70 -0
- package/docs/adr/0021-per-builder-skill-model-preferences.md +99 -0
- package/docs/adr/0022-secrets-policy.md +124 -0
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +118 -0
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +277 -0
- package/docs/adr/0024-multi-agent-system-architecture.md +125 -0
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +59 -0
- package/docs/adr/0025-structured-criterion-task-link.md +44 -0
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +117 -0
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +170 -0
- package/docs/adr/0028-honest-llm-spend-accounting.md +109 -0
- package/docs/adr/0029-status-page-outage-resilience.md +59 -0
- package/docs/adr/0030-builders-hall-live-updates.md +59 -0
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +222 -0
- package/docs/adr/0032-discord-mirror-of-gds.md +50 -0
- package/docs/adr/0033-discord-bot-service-principal.md +82 -0
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +53 -0
- package/docs/adr/0035-builder-onboarding-three-paths.md +128 -0
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +40 -0
- package/docs/adr/0037-discord-channel-management.md +51 -0
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +111 -0
- package/docs/adr/0039-setup-first-onboarding-ux.md +49 -0
- package/docs/adr/0040-remote-control-default-browser-onramp.md +47 -0
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +40 -0
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +82 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +74 -0
- package/docs/adr/0044-mediterranean-palette-replacement.md +73 -0
- package/docs/adr/0044-per-box-live-game-preview.md +57 -0
- package/docs/adr/0045-devbox-desktop-app.md +133 -0
- package/docs/adr/0046-sandbox-first-review-gate.md +72 -0
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +36 -0
- package/docs/adr/0048-works-in-progress-ia-split.md +32 -0
- package/docs/adr/0049-split-parallel-safety-contract.md +46 -0
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +43 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +57 -0
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +70 -0
- package/docs/adr/0053-scoped-dev-box-session.md +55 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +63 -0
- package/docs/adr/0055-server-mediated-branch-publish.md +47 -0
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +62 -0
- package/docs/adr/0057-container-cost-ledger.md +28 -0
- package/docs/adr/0057-discord-archon-approval-channels.md +64 -0
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +64 -0
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +37 -0
- package/docs/adr/0060-gds-retrieval-layer.md +97 -0
- package/docs/adr/0061-context-layer-decomposition.md +61 -0
- package/docs/adr/0062-medusa-instance-model-decoupling.md +107 -0
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +44 -0
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +49 -0
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +69 -0
- package/docs/adr/0066-generated-file-map-sections.md +51 -0
- package/docs/adr/0067-session-start-fast-forward-pull.md +43 -0
- package/docs/adr/0068-shared-art-key-for-newcomers.md +38 -0
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +35 -0
- package/docs/adr/0070-deterministic-session-card-delivery.md +44 -0
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +59 -0
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +35 -0
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +39 -0
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +64 -0
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +37 -0
- package/docs/adr/0074-memory-map-stays-light.md +32 -0
- package/docs/adr/0075-per-system-cost-attribution.md +37 -0
- package/docs/adr/0076-in-session-deterministic-compaction.md +58 -0
- package/docs/adr/0077-content-addressed-llm-cache.md +32 -0
- package/docs/adr/0078-deterministic-first-cron.md +48 -0
- package/docs/adr/0079-value-gated-autonomous-engagement.md +39 -0
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +46 -0
- package/docs/adr/0081-tool-agnostic-design-layer.md +62 -0
- package/docs/adr/0082-productized-key-storage-provisioning.md +32 -0
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +55 -0
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +143 -0
- package/docs/adr/0084-auto-derived-per-task-rank-floor.md +86 -0
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +39 -0
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +123 -0
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +49 -0
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +65 -0
- package/docs/adr/0088-consistent-character-generative-animation.md +169 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +179 -0
- package/docs/adr/0090-metic-task-authoring.md +73 -0
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +106 -0
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +117 -0
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +72 -0
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +49 -0
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +65 -0
- package/docs/adr/0095-cross-agent-context-management.md +58 -0
- package/docs/adr/0096-require-reward-before-workable.md +209 -0
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +46 -0
- package/docs/adr/0097-retroactive-reward-backfill.md +124 -0
- package/docs/adr/0098-public-mirror-publish-manifest.md +92 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +55 -0
- package/docs/adr/0100-two-repo-instance-model.md +60 -0
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +90 -0
- package/docs/adr/0102-finish-bounding-the-kernel.md +36 -0
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +70 -0
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +28 -0
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +42 -0
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +47 -0
- package/docs/adr/0106-goal-owner-manager-authority.md +84 -0
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +83 -0
- package/docs/adr/0108-instance-composition-configurable-root.md +87 -0
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +44 -0
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +49 -0
- package/docs/adr/0111-instance-hosting-provisioning-module.md +135 -0
- package/docs/adr/0112-private-goals-invitations-join-requests.md +72 -0
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +41 -0
- package/docs/adr/0114-instance-docs-discovery-contract.md +32 -0
- package/docs/adr/0115-scheduled-routines-portability.md +31 -0
- package/docs/adr/0116-api-error-envelope-and-code-taxonomy.md +62 -0
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +50 -0
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +47 -0
- package/docs/adr/0119-api-pagination-contract.md +34 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +108 -0
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +97 -0
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +43 -0
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +36 -0
- package/docs/adr/0124-typed-api-response-bodies.md +37 -0
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +36 -0
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +30 -0
- package/docs/adr/0127-cross-box-fleet-observability.md +64 -0
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +46 -0
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +34 -0
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +61 -0
- package/docs/adr/0131-rank-scoped-skill-visibility.md +47 -0
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +54 -0
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +42 -0
- package/docs/adr/0134-private-first-npm-distribution.md +38 -0
- package/docs/adr/README.md +208 -0
- package/docs/api/openapi.json +13720 -0
- package/docs/api-reference.md +437 -0
- package/docs/architecture.md +244 -0
- package/docs/branding-contract.md +66 -0
- package/docs/branding-fill-prompt.md +25 -0
- package/docs/canonical-permissions.md +241 -0
- package/docs/design/container-exterior-tiles.md +49 -0
- package/docs/design/container-exterior.md +81 -0
- package/docs/design/container-interior.md +77 -0
- package/docs/design/modular-architecture/00-research-report.md +107 -0
- package/docs/design/modular-architecture/01-architecture.md +125 -0
- package/docs/design/modular-architecture/02-module-map.md +116 -0
- package/docs/design/modular-architecture/03-builder-flows.md +104 -0
- package/docs/design/modular-architecture/04-module-lifecycle.md +62 -0
- package/docs/design/modular-architecture/README.md +27 -0
- package/docs/design/vanilla-hall-ui-redesign-scope.md +284 -0
- package/docs/design/world-map-scroll.md +41 -0
- package/docs/design-contract.md +320 -0
- package/docs/file-map.md +418 -0
- package/docs/handoff-template.md +92 -0
- package/docs/modules-contract.md +293 -0
- package/docs/onboarding/browser-terminal-guide.md +73 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +28 -0
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +54 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +37 -0
- package/docs/onboarding/diagrams/04-architecture.mmd +51 -0
- package/docs/onboarding/diagrams/05-versioning.mmd +29 -0
- package/docs/onboarding/diagrams/README.md +94 -0
- package/docs/onboarding/diagrams/assertions.json +170 -0
- package/docs/onboarding/diagrams/puppeteer-config.json +1 -0
- package/docs/onboarding/diagrams/render-config.json +9 -0
- package/docs/onboarding/drachmae-and-karma.md +178 -0
- package/docs/onboarding/primer.md +202 -0
- package/docs/project-context.template.md +67 -0
- package/docs/recipes/README.md +35 -0
- package/docs/recipes/audit-replay.md +100 -0
- package/docs/recipes/bongos-cli-release.md +66 -0
- package/docs/recipes/builder-box-lifecycle.md +152 -0
- package/docs/recipes/claude-design-loop.md +143 -0
- package/docs/recipes/claude-gemini-bongos-doors.md +155 -0
- package/docs/recipes/cloudbongos-staging-gate.md +139 -0
- package/docs/recipes/cloudbongos-standup.md +111 -0
- package/docs/recipes/devbox-app-release.md +93 -0
- package/docs/recipes/devbox-app-windows-test.md +128 -0
- package/docs/recipes/failed-ship-recovery-proof.md +102 -0
- package/docs/recipes/figma-design-loop.md +129 -0
- package/docs/recipes/gds-db-backup.md +255 -0
- package/docs/recipes/instance-deploy.md +37 -0
- package/docs/recipes/managed-settings-remote-control.md +416 -0
- package/docs/recipes/memory-backup.md +71 -0
- package/docs/recipes/memory-sync-conflicts.md +98 -0
- package/docs/recipes/multi-builder-merge.md +78 -0
- package/docs/recipes/ops-gotchas.md +212 -0
- package/docs/recipes/overnight-builder.md +53 -0
- package/docs/recipes/packaging-the-core.md +49 -0
- package/docs/recipes/parallel-load-results-2026-05-30.md +63 -0
- package/docs/recipes/parallel-load-test.md +32 -0
- package/docs/recipes/pixel-art-pipeline.md +411 -0
- package/docs/recipes/private-npm-distribution.md +79 -0
- package/docs/recipes/search-index-ingestion.md +67 -0
- package/docs/recipes/self-host.md +60 -0
- package/docs/recipes/ship-and-regrade.md +72 -0
- package/docs/recipes/staging-data.md +50 -0
- package/docs/recipes/staging-environment.md +215 -0
- package/docs/recipes/upgrading-the-core.md +58 -0
- package/docs/research/cursor-competitive-analysis.md +255 -0
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +227 -0
- package/migrations/003_pms.sql +284 -0
- package/migrations/004_pms_cost_idempotency.sql +13 -0
- package/migrations/005_pms_smoke_filter.sql +44 -0
- package/migrations/006_pms_v2.sql +378 -0
- package/migrations/007_pms_v2_hierarchy_trigger.sql +59 -0
- package/migrations/008_pms_v2_migration_allocator.sql +27 -0
- package/migrations/009_pms_v2_blockers_table.sql +68 -0
- package/migrations/010_uptime.sql +29 -0
- package/migrations/011_backfill_v2_v3_done_when.sql +20 -0
- package/migrations/012_pms_v2_task_lifecycle_states.sql +82 -0
- package/migrations/013_claim_locking.sql +22 -0
- package/migrations/014_pms_task_discipline.sql +86 -0
- package/migrations/015_pms_estimation_calibration.sql +140 -0
- package/migrations/016_pms_task_grades.sql +60 -0
- package/migrations/017_version_progress_exclude_abandoned.sql +49 -0
- package/migrations/019_rename_pms_v3_to_gds_v3.sql +65 -0
- package/migrations/020_task_dependencies.sql +99 -0
- package/migrations/021_v3_renumber_and_deps.sql +224 -0
- package/migrations/022_v3_reconcile_dep_gated_status.sql +41 -0
- package/migrations/023_pms_rank.sql +30 -0
- package/migrations/025_builder_exit_status.sql +15 -0
- package/migrations/027_builders_preferred_disciplines.sql +46 -0
- package/migrations/028_tasks_parallel_safe.sql +71 -0
- package/migrations/029_rank_three_live.sql +32 -0
- package/migrations/030_vulnerability_reports.sql +61 -0
- package/migrations/031_reward_simplification.sql +57 -0
- package/migrations/032_repo_health_snapshots.sql +56 -0
- package/migrations/033_audit_log.sql +64 -0
- package/migrations/034_lifecycle_breakdown_leaf_only.sql +62 -0
- package/migrations/035_auth_token_hygiene.sql +66 -0
- package/migrations/037_builder_skill_model_prefs.sql +44 -0
- package/migrations/038_task_override_requests.sql +51 -0
- package/migrations/039_newcomer_friendly_column.sql +124 -0
- package/migrations/040_builder_karma.sql +58 -0
- package/migrations/041_kind_multipliers_and_idea_bonus.sql +42 -0
- package/migrations/042_criterion_proposal_workflow.sql +78 -0
- package/migrations/043_builder_onboarding_observability.sql +66 -0
- package/migrations/044_tasks_requires_rank.sql +97 -0
- package/migrations/045_builder_onboarding_state.sql +147 -0
- package/migrations/046_cost_log_builder_attribution.sql +42 -0
- package/migrations/054_builder_memory.sql +108 -0
- package/migrations/055_task_criteria.sql +90 -0
- package/migrations/058_peer_votes.sql +41 -0
- package/migrations/059_onboarding_pick_disciplines.sql +59 -0
- package/migrations/060_access_requests.sql +46 -0
- package/migrations/061_backfill_override_ship_attribution.sql +29 -0
- package/migrations/062_session_records.sql +34 -0
- package/migrations/063_session_records_tool_errors_idx.sql +11 -0
- package/migrations/064_grade_attempts.sql +18 -0
- package/migrations/065_bfg_principal.sql +58 -0
- package/migrations/066_live_notify_triggers.sql +69 -0
- package/migrations/067_builder_boxes.sql +117 -0
- package/migrations/067_discord_bot_foundation.sql +69 -0
- package/migrations/068_thetes_requires_rank.sql +27 -0
- package/migrations/069_builder_ssh_keys.sql +66 -0
- package/migrations/070_box_intents.sql +101 -0
- package/migrations/071_box_first_provision_approval.sql +46 -0
- package/migrations/072_onboarding_ship_three_remove_primer.sql +80 -0
- package/migrations/073_box_terminal_access.sql +46 -0
- package/migrations/075_builder_sound_prefs.sql +28 -0
- package/migrations/078_box_claude_active.sql +22 -0
- package/migrations/079_box_first_connected_at.sql +20 -0
- package/migrations/081_tasks_needs_migration.sql +34 -0
- package/migrations/084_tasks_security_sensitive.sql +24 -0
- package/migrations/085_security_engagement_docs.sql +34 -0
- package/migrations/086_rename_disciplines.sql +125 -0
- package/migrations/088_builders_anthropic_link.sql +28 -0
- package/migrations/089_builders_monthly_budget.sql +19 -0
- package/migrations/090_box_scoped_sessions.sql +38 -0
- package/migrations/091_session_token_reward.sql +44 -0
- package/migrations/093_tasks_sandbox_review.sql +22 -0
- package/migrations/102_remove_box_first_provision_approval.sql +58 -0
- package/migrations/103_search_chunks.sql +90 -0
- package/migrations/104_builder_box_blocked.sql +34 -0
- package/migrations/105_box_host_keys.sql +35 -0
- package/migrations/111_medusa_customer0_framing.sql +35 -0
- package/migrations/112_drop_track_check.sql +25 -0
- package/migrations/113_merge_lease.sql +16 -0
- package/migrations/114_cloudbongos_rename_live_strings.sql +32 -0
- package/migrations/118_currency_label_decoupling.sql +23 -0
- package/migrations/119_builder_behavior_prefs.sql +31 -0
- package/migrations/121_box_code_version.sql +37 -0
- package/migrations/122_builder_gemini_key.sql +30 -0
- package/migrations/123_cost_log_tiers.sql +33 -0
- package/migrations/124_cost_baseline.sql +45 -0
- package/migrations/125_llm_cache.sql +54 -0
- package/migrations/126_autonomy_runs.sql +33 -0
- package/migrations/143_cost_calibration.sql +63 -0
- package/migrations/145_backfill_task_requires_rank.sql +84 -0
- package/migrations/160_goal_tier.sql +88 -0
- package/migrations/163_polymorphic_dependencies.sql +187 -0
- package/migrations/164_event_sound_prefs.sql +20 -0
- package/migrations/165_task_module_key.sql +15 -0
- package/migrations/171_claims_published_head_sha.sql +24 -0
- package/migrations/174_close_orphaned_claims_on_terminal_tasks.sql +30 -0
- package/migrations/175_task_grades_diff_hash.sql +9 -0
- package/migrations/176_promotion_respects_open_blockers.sql +58 -0
- package/migrations/176_ui_discipline.sql +40 -0
- package/migrations/177_require_reward_before_workable.sql +76 -0
- package/migrations/178_reward_gate_auto_assign_revert_autopromote.sql +61 -0
- package/migrations/179_search_chunk_usefulness.sql +33 -0
- package/migrations/180_one_active_claim_per_session.sql +54 -0
- package/migrations/181_cost_log_skill_name.sql +33 -0
- package/migrations/184_claim_release_requested.sql +25 -0
- package/migrations/185_module_upstreaming_goal_wiring.sql +59 -0
- package/migrations/187_goal_sort_order.sql +30 -0
- package/migrations/187_tasks_kind_check_add_verify.sql +21 -0
- package/migrations/README.md +88 -0
- package/migrations/core_188_goal_visibility_and_membership_requests.sql +71 -0
- package/migrations/core_189_core_upgrades_ledger.sql +37 -0
- package/migrations/core_189_credit_log_task_id_index.sql +19 -0
- package/migrations/core_190_rebuild_claimable_task_views.sql +71 -0
- package/migrations/instance/018_pms_v3_planning_seed.sql +131 -0
- package/migrations/instance/080_gds_v4_planning_seed.sql +75 -0
- package/migrations/instance/110_medusa_planning_seed.sql +70 -0
- package/migrations/instance/120_bongos_v1_planning_seed.sql +69 -0
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +26 -0
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +29 -0
- package/migrations/instance/172_v2_world_planning_seed.sql +163 -0
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +33 -0
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +132 -0
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +116 -0
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +69 -0
- package/modules/autonomy/CLAUDE.md +49 -0
- package/modules/autonomy/db.js +50 -0
- package/modules/autonomy/module.json +13 -0
- package/modules/autonomy/pollers/routine-timers.js +146 -0
- package/modules/autonomy/routes/autonomy.js +37 -0
- package/modules/builder-settings/CLAUDE.md +58 -0
- package/modules/builder-settings/builder-needs.js +115 -0
- package/modules/builder-settings/db.js +168 -0
- package/modules/builder-settings/display-name.js +84 -0
- package/modules/builder-settings/event-sound-prefs.js +169 -0
- package/modules/builder-settings/interaction-prefs.js +201 -0
- package/modules/builder-settings/model-alloc-prefs.js +147 -0
- package/modules/builder-settings/module.json +10 -0
- package/modules/builder-settings/render-prefs.js +103 -0
- package/modules/builder-settings/routes/settings.js +417 -0
- package/modules/builder-settings/settings.js +52 -0
- package/modules/builder-settings/skill-prefs.js +196 -0
- package/modules/builder-settings/sound-prefs.js +178 -0
- package/modules/builder-settings/wandering-prefs.js +282 -0
- package/modules/dev-box/CLAUDE.md +38 -0
- package/modules/dev-box/box-access.js +275 -0
- package/modules/dev-box/box-credential.js +94 -0
- package/modules/dev-box/box-onboard.js +486 -0
- package/modules/dev-box/boxes.js +842 -0
- package/modules/dev-box/db.js +39 -0
- package/modules/dev-box/module.json +14 -0
- package/modules/dev-box/routes/box.js +977 -0
- package/modules/discord/CLAUDE.md +50 -0
- package/modules/discord/auth.js +54 -0
- package/modules/discord/bug-attachments.js +210 -0
- package/modules/discord/db.js +96 -0
- package/modules/discord/discord-approvals.js +145 -0
- package/modules/discord/discord-audit.js +29 -0
- package/modules/discord/discord-bot.js +545 -0
- package/modules/discord/discord-bugs.js +257 -0
- package/modules/discord/discord-channels.js +412 -0
- package/modules/discord/discord-inbound.js +147 -0
- package/modules/discord/discord-message-buffer.js +54 -0
- package/modules/discord/discord-reaction.js +186 -0
- package/modules/discord/discord-role-sync.js +318 -0
- package/modules/discord/module.json +14 -0
- package/modules/discord/pollers/discord-bot.js +19 -0
- package/modules/discord/routes/bug-attachments.js +44 -0
- package/modules/discord/routes/discord.js +102 -0
- package/modules/discord/ship-broadcast.js +78 -0
- package/modules/discord/webhook.js +74 -0
- package/modules/economy/CLAUDE.md +48 -0
- package/modules/economy/achievements.js +594 -0
- package/modules/economy/cost-classify.js +331 -0
- package/modules/economy/cost-summary-shape.js +90 -0
- package/modules/economy/cost.js +438 -0
- package/modules/economy/credits.js +430 -0
- package/modules/economy/migrations/economy_001_fix_gds_shipper_description.sql +91 -0
- package/modules/economy/module.json +10 -0
- package/modules/economy/reward.js +61 -0
- package/modules/economy/routes/cost.js +97 -0
- package/modules/economy/routes/leaderboard.js +26 -0
- package/modules/economy/routes/reward.js +42 -0
- package/modules/economy/streak.js +285 -0
- package/modules/grading/CLAUDE.md +42 -0
- package/modules/grading/db.js +39 -0
- package/modules/grading/grader-rubric.json +151 -0
- package/modules/grading/grader-workers/efficiency.js +213 -0
- package/modules/grading/grader-workers/extract-json.js +80 -0
- package/modules/grading/grader-workers/fence.js +100 -0
- package/modules/grading/grader-workers/hacker.js +239 -0
- package/modules/grading/grader-workers/index.js +183 -0
- package/modules/grading/grader-workers/narc.js +207 -0
- package/modules/grading/grader-workers/quality.js +198 -0
- package/modules/grading/grader-workers/rubric.js +20 -0
- package/modules/grading/grader-workers/worker-prompt.js +89 -0
- package/modules/grading/grader-workers/worker-verdict.js +50 -0
- package/modules/grading/grader.js +1790 -0
- package/modules/grading/module.json +12 -0
- package/modules/grading/routes/grade.js +51 -0
- package/modules/hall-ui/CLAUDE.md +61 -0
- package/modules/hall-ui/module.json +14 -0
- package/modules/hall-ui/public/atlas.html +180 -0
- package/modules/hall-ui/public/atlas.js +336 -0
- package/modules/hall-ui/public/box-panel.js +423 -0
- package/modules/hall-ui/public/brand-apply.js +117 -0
- package/modules/hall-ui/public/builders.js +3028 -0
- package/modules/hall-ui/public/chime.wav +0 -0
- package/modules/hall-ui/public/copy-on-click.js +174 -0
- package/modules/hall-ui/public/cursors/arrow.png +0 -0
- package/modules/hall-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier-out.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier.png +0 -0
- package/modules/hall-ui/public/diagram-viewer.css +125 -0
- package/modules/hall-ui/public/diagram-viewer.js +330 -0
- package/modules/hall-ui/public/diagrams.html +113 -0
- package/modules/hall-ui/public/dom-utils.js +212 -0
- package/modules/hall-ui/public/drachmae.html +68 -0
- package/modules/hall-ui/public/drachmae.js +136 -0
- package/modules/hall-ui/public/gate.html +143 -0
- package/modules/hall-ui/public/gate.js +228 -0
- package/modules/hall-ui/public/goals-page.js +1297 -0
- package/modules/hall-ui/public/goals.html +376 -0
- package/modules/hall-ui/public/goals.js +119 -0
- package/modules/hall-ui/public/hall-widgets.js +189 -0
- package/modules/hall-ui/public/harbor.html +167 -0
- package/modules/hall-ui/public/harbor.js +335 -0
- package/modules/hall-ui/public/index.html +116 -0
- package/modules/hall-ui/public/instances-panel.js +172 -0
- package/modules/hall-ui/public/md-reader.js +234 -0
- package/modules/hall-ui/public/modules.html +119 -0
- package/modules/hall-ui/public/modules.js +154 -0
- package/modules/hall-ui/public/new-project-wizard.js +186 -0
- package/modules/hall-ui/public/not-ready.html +48 -0
- package/modules/hall-ui/public/pair.html +119 -0
- package/modules/hall-ui/public/pair.js +132 -0
- package/modules/hall-ui/public/primer.css +404 -0
- package/modules/hall-ui/public/primer.html +85 -0
- package/modules/hall-ui/public/primer.js +641 -0
- package/modules/hall-ui/public/profile.html +85 -0
- package/modules/hall-ui/public/profile.js +236 -0
- package/modules/hall-ui/public/ranks.html +69 -0
- package/modules/hall-ui/public/ranks.js +206 -0
- package/modules/hall-ui/public/roadmap.html +209 -0
- package/modules/hall-ui/public/roadmap.js +375 -0
- package/modules/hall-ui/public/sessions.html +92 -0
- package/modules/hall-ui/public/sessions.js +222 -0
- package/modules/hall-ui/public/settings.html +640 -0
- package/modules/hall-ui/public/settings.js +1219 -0
- package/modules/hall-ui/public/shell.js +495 -0
- package/modules/hall-ui/public/style.css +1748 -0
- package/modules/hall-ui/public/task-detail.js +253 -0
- package/modules/hall-ui/public/task.html +83 -0
- package/modules/hall-ui/public/task.js +326 -0
- package/modules/hall-ui/public/watch.html +333 -0
- package/modules/hall-ui/public/watch.js +859 -0
- package/modules/hall-ui/public/work.html +469 -0
- package/modules/hall-ui/public/work.js +1292 -0
- package/modules/ideas/CLAUDE.md +37 -0
- package/modules/ideas/blockers.js +100 -0
- package/modules/ideas/inbox.js +459 -0
- package/modules/ideas/module.json +10 -0
- package/modules/ideas/routes/blockers.js +120 -0
- package/modules/ideas/routes/inbox.js +230 -0
- package/modules/lifecycle/CLAUDE.md +63 -0
- package/modules/lifecycle/conflict-resolve.js +436 -0
- package/modules/lifecycle/db.js +4970 -0
- package/modules/lifecycle/done-when.js +355 -0
- package/modules/lifecycle/github-push.js +969 -0
- package/modules/lifecycle/goal-conflicts.js +133 -0
- package/modules/lifecycle/hierarchy.js +104 -0
- package/modules/lifecycle/lifecycle.js +58 -0
- package/modules/lifecycle/merge-lock.js +261 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +28 -0
- package/modules/lifecycle/migrations/lifecycle_002_backfill_orphan_task_goals.sql +36 -0
- package/modules/lifecycle/module.json +13 -0
- package/modules/lifecycle/pollers/publish-reconciler.js +20 -0
- package/modules/lifecycle/publish-reconciler.js +418 -0
- package/modules/lifecycle/routes/analytics.js +63 -0
- package/modules/lifecycle/routes/claims.js +506 -0
- package/modules/lifecycle/routes/dependencies.js +82 -0
- package/modules/lifecycle/routes/done-when.js +136 -0
- package/modules/lifecycle/routes/gate-approvals.js +197 -0
- package/modules/lifecycle/routes/goals.js +1419 -0
- package/modules/lifecycle/routes/lifecycle.js +43 -0
- package/modules/lifecycle/routes/tasks.js +1703 -0
- package/modules/lifecycle/routes/versions.js +128 -0
- package/modules/lifecycle/ship-card.js +505 -0
- package/modules/lifecycle/ship-preflight.js +225 -0
- package/modules/lifecycle/task-classifier.js +512 -0
- package/modules/lifecycle/version-close.js +106 -0
- package/modules/memory/CLAUDE.md +40 -0
- package/modules/memory/learnings.js +275 -0
- package/modules/memory/memory.js +572 -0
- package/modules/memory/module.json +12 -0
- package/modules/memory/routes/learnings.js +90 -0
- package/modules/memory/routes/memory.js +916 -0
- package/modules/memory/routes/search.js +188 -0
- package/modules/memory/search-chunker.js +252 -0
- package/modules/memory/search.js +320 -0
- package/modules/onboarding/CLAUDE.md +55 -0
- package/modules/onboarding/approval-broadcast.js +59 -0
- package/modules/onboarding/db.js +54 -0
- package/modules/onboarding/module.json +10 -0
- package/modules/onboarding/newcomer-restock.js +212 -0
- package/modules/onboarding/onboarding-state.js +532 -0
- package/modules/onboarding/onboarding.js +508 -0
- package/modules/onboarding/port.js +63 -0
- package/modules/onboarding/routes/access-requests.js +208 -0
- package/modules/onboarding/routes/onboarding.js +54 -0
- package/modules/security/CLAUDE.md +44 -0
- package/modules/security/module.json +10 -0
- package/modules/security/routes/reports.js +251 -0
- package/modules/security/secret-scrub.js +201 -0
- package/modules/security/security.js +294 -0
- package/modules/sessions/CLAUDE.md +39 -0
- package/modules/sessions/bfg.js +357 -0
- package/modules/sessions/db.js +326 -0
- package/modules/sessions/module.json +10 -0
- package/modules/sessions/pulse.js +149 -0
- package/modules/sessions/routes/sessions.js +331 -0
- package/modules/status-ui/CLAUDE.md +47 -0
- package/modules/status-ui/module.json +14 -0
- package/modules/status-ui/public/README.md +29 -0
- package/modules/status-ui/public/cursors/arrow.png +0 -0
- package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/status-ui/public/index.html +363 -0
- package/modules/status-ui/public/snapshots/.gitkeep +0 -0
- package/modules/status-ui/public/status.js +1375 -0
- package/modules/status-ui/public/style.css +1342 -0
- package/package-lock.json +975 -0
- package/package.json +28 -0
- package/public-docs/index.html +35 -0
- package/public-docs/redoc.standalone.js +1838 -0
- package/public-docs/redoc.standalone.js.LICENSE.txt +102 -0
- package/public-landing/buddha.png +0 -0
- package/public-landing/create.html +868 -0
- package/public-landing/index.html +168 -0
- package/scripts/bongos-mcp.js +28 -0
- package/scripts/cron/style_guide_audit.sh +134 -0
- package/scripts/discord/README.md +52 -0
- package/scripts/discord/lib.js +80 -0
- package/scripts/discord/post.js +51 -0
- package/scripts/gds/CLAUDE.md +144 -0
- package/scripts/gds/abandon.js +58 -0
- package/scripts/gds/adopt-detect.js +123 -0
- package/scripts/gds/adopt-import.js +111 -0
- package/scripts/gds/adopt-preflight.js +121 -0
- package/scripts/gds/api.js +78 -0
- package/scripts/gds/architect-audit.js +288 -0
- package/scripts/gds/art-stage.js +152 -0
- package/scripts/gds/audit-authorship.sh +243 -0
- package/scripts/gds/audit-deps.js +121 -0
- package/scripts/gds/audit-exports.js +128 -0
- package/scripts/gds/audit-replay.sh +161 -0
- package/scripts/gds/audit-ships.js +251 -0
- package/scripts/gds/autonomy-gate.js +135 -0
- package/scripts/gds/backfill-cost.js +165 -0
- package/scripts/gds/backfill-sessions.js +351 -0
- package/scripts/gds/backfill-task-rewards.js +408 -0
- package/scripts/gds/backup.js +94 -0
- package/scripts/gds/bfg-principal-token.js +65 -0
- package/scripts/gds/box-auth-check.js +160 -0
- package/scripts/gds/box-connect-lib.js +453 -0
- package/scripts/gds/box.js +1258 -0
- package/scripts/gds/build-bongos.js +94 -0
- package/scripts/gds/capture.js +123 -0
- package/scripts/gds/cf-tunnel.js +419 -0
- package/scripts/gds/ci-grade.js +82 -0
- package/scripts/gds/claim-recall.js +112 -0
- package/scripts/gds/claim.js +333 -0
- package/scripts/gds/classify-discipline.js +153 -0
- package/scripts/gds/claude-materialize.js +176 -0
- package/scripts/gds/cleanup-orphans.js +74 -0
- package/scripts/gds/cli-lib.js +518 -0
- package/scripts/gds/code.js +101 -0
- package/scripts/gds/codemod-rename-src-gds.js +145 -0
- package/scripts/gds/conductor-dispatch.js +426 -0
- package/scripts/gds/context-pack.js +228 -0
- package/scripts/gds/coreB-carve-proof.sh +716 -0
- package/scripts/gds/cost.js +47 -0
- package/scripts/gds/db-backup-nightly.sh +160 -0
- package/scripts/gds/db-backup-offsite.sh +152 -0
- package/scripts/gds/demote.js +40 -0
- package/scripts/gds/design-sync.js +62 -0
- package/scripts/gds/diagnose-achievements.js +118 -0
- package/scripts/gds/diagram-facts.js +319 -0
- package/scripts/gds/discipline-modes.json +25 -0
- package/scripts/gds/discord-channels.js +66 -0
- package/scripts/gds/do-api.js +176 -0
- package/scripts/gds/docs-entropy.js +403 -0
- package/scripts/gds/doctor.js +389 -0
- package/scripts/gds/feedback-latest.js +94 -0
- package/scripts/gds/fetch-art-key.js +106 -0
- package/scripts/gds/fetch-sound-prefs.js +88 -0
- package/scripts/gds/figma-design-sync.js +61 -0
- package/scripts/gds/fitness.js +1446 -0
- package/scripts/gds/gate-review.js +322 -0
- package/scripts/gds/gating.js +325 -0
- package/scripts/gds/gds-db-backup.service +24 -0
- package/scripts/gds/gds-db-backup.timer +20 -0
- package/scripts/gds/gds-db-offsite.service +29 -0
- package/scripts/gds/gds-db-offsite.timer +18 -0
- package/scripts/gds/gen-api-client.js +508 -0
- package/scripts/gds/gen-api-docs.js +1037 -0
- package/scripts/gds/gen-atlas.js +366 -0
- package/scripts/gds/gen-diagrams.js +608 -0
- package/scripts/gds/gen-file-map.js +196 -0
- package/scripts/gds/gen-repo-map.js +519 -0
- package/scripts/gds/gen-session-index.js +210 -0
- package/scripts/gds/git-merge-regen.js +147 -0
- package/scripts/gds/grade-smoke.js +205 -0
- package/scripts/gds/hooks/pre-push.js +169 -0
- package/scripts/gds/http-api-client.js +88 -0
- package/scripts/gds/init.js +903 -0
- package/scripts/gds/install-git-hooks.js +114 -0
- package/scripts/gds/key-card.js +213 -0
- package/scripts/gds/key-status.js +126 -0
- package/scripts/gds/land-watch.js +168 -0
- package/scripts/gds/learning-capture.js +145 -0
- package/scripts/gds/lib.sh +44 -0
- package/scripts/gds/linkify-refs.js +473 -0
- package/scripts/gds/local-preview-lib.js +159 -0
- package/scripts/gds/local-preview.js +95 -0
- package/scripts/gds/login.js +225 -0
- package/scripts/gds/main-audit.js +279 -0
- package/scripts/gds/me.js +88 -0
- package/scripts/gds/memory-map.js +174 -0
- package/scripts/gds/merge-lock.js +71 -0
- package/scripts/gds/migration-namespace.js +200 -0
- package/scripts/gds/mirror-lineage-check.js +319 -0
- package/scripts/gds/mirror-redact.js +305 -0
- package/scripts/gds/module.js +482 -0
- package/scripts/gds/newcomer-floor.config.json +13 -0
- package/scripts/gds/newcomer-floor.js +415 -0
- package/scripts/gds/newcomer-floor.templates.json +86 -0
- package/scripts/gds/newcomer-floor.test.js +235 -0
- package/scripts/gds/next-number.js +163 -0
- package/scripts/gds/oauth-secret.js +197 -0
- package/scripts/gds/onboard.js +217 -0
- package/scripts/gds/onboarding-analyze.js +226 -0
- package/scripts/gds/onboarding-config.js +178 -0
- package/scripts/gds/onboarding-ship.js +92 -0
- package/scripts/gds/package-core.js +684 -0
- package/scripts/gds/paste-token.js +131 -0
- package/scripts/gds/portability-smoke-art.sh +85 -0
- package/scripts/gds/portability-smoke-memory.sh +147 -0
- package/scripts/gds/provision.js +1324 -0
- package/scripts/gds/publish-manifest.js +307 -0
- package/scripts/gds/pulse.js +309 -0
- package/scripts/gds/push-main.js +45 -0
- package/scripts/gds/read-session-export.js +359 -0
- package/scripts/gds/reauth.js +149 -0
- package/scripts/gds/recall.js +135 -0
- package/scripts/gds/release.js +71 -0
- package/scripts/gds/render-ideas.js +173 -0
- package/scripts/gds/repo-metrics.js +363 -0
- package/scripts/gds/resolve-skill-model.js +57 -0
- package/scripts/gds/run-routine.js +197 -0
- package/scripts/gds/run-unit-tests.js +178 -0
- package/scripts/gds/sandbox-stage.js +653 -0
- package/scripts/gds/search-ingest-db.js +209 -0
- package/scripts/gds/search-ingest-md.js +225 -0
- package/scripts/gds/security-baseline-check.sh +180 -0
- package/scripts/gds/seed-adopt-tasks.js +75 -0
- package/scripts/gds/seed-api-contract-goal-tasks.js +141 -0
- package/scripts/gds/seed-bongos-app-tasks.js +294 -0
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +81 -0
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +100 -0
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +307 -0
- package/scripts/gds/seed-bongos-module-system-tasks.js +346 -0
- package/scripts/gds/seed-bongos-tasks.js +214 -0
- package/scripts/gds/seed-gds-v4-tasks.js +624 -0
- package/scripts/gds/seed-governance-tasks.js +340 -0
- package/scripts/gds/seed-medusa-tasks.js +440 -0
- package/scripts/gds/seed-module-upstreaming.js +219 -0
- package/scripts/gds/seed-productization-tasks.js +235 -0
- package/scripts/gds/seed-provisioning-tasks.js +83 -0
- package/scripts/gds/seed-staging.sh +104 -0
- package/scripts/gds/seed-v2-world-tasks.js +283 -0
- package/scripts/gds/seed-v3-tasks.js +1981 -0
- package/scripts/gds/session-cost.js +393 -0
- package/scripts/gds/session-digest.js +179 -0
- package/scripts/gds/session-eval.js +576 -0
- package/scripts/gds/session.js +404 -0
- package/scripts/gds/setup.js +504 -0
- package/scripts/gds/shell.js +100 -0
- package/scripts/gds/ship.js +3618 -0
- package/scripts/gds/smoke-batch-claim.sh +213 -0
- package/scripts/gds/smoke-batch-release.sh +296 -0
- package/scripts/gds/smoke-box.sh +150 -0
- package/scripts/gds/smoke-claim-consistency.sh +212 -0
- package/scripts/gds/smoke-dependencies.sh +84 -0
- package/scripts/gds/smoke-gds.js +125 -0
- package/scripts/gds/smoke-write.sh +168 -0
- package/scripts/gds/start.js +782 -0
- package/scripts/gds/status.js +365 -0
- package/scripts/gds/triage.js +366 -0
- package/scripts/gds/upgrade.js +421 -0
- package/scripts/gds/validate-design.js +254 -0
- package/scripts/gds/version-close.js +549 -0
- package/scripts/migrate.sh +183 -0
- package/scripts/migrate_cost_ledger.sh +178 -0
- package/scripts/public-mirror-export.js +291 -0
- package/scripts/render-diagrams.sh +88 -0
- package/scripts/security/dep-audit.js +276 -0
- package/scripts/setup-builder.js +274 -0
- package/scripts/setup-builder.sh +28 -0
- package/scripts/snapshots/backfill-history.mjs +292 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +66 -0
- package/scripts/snapshots/render-pre-art-eras.mjs +329 -0
- package/scripts/snapshots/render-world.mjs +322 -0
- package/scripts/status-mirror-sync.js +192 -0
- package/scripts/status-probe.mjs +185 -0
- package/server.js +212 -0
- package/src/asset-version.js +71 -0
- package/src/bongos/CLAUDE.md +64 -0
- package/src/bongos/api-errors.js +281 -0
- package/src/bongos/api-path-404.js +67 -0
- package/src/bongos/api-prefix.js +38 -0
- package/src/bongos/app-pair.js +172 -0
- package/src/bongos/auth.js +979 -0
- package/src/bongos/cascade-dispatch.js +123 -0
- package/src/bongos/db-kernel.js +252 -0
- package/src/bongos/db.js +911 -0
- package/src/bongos/goal-scope-check.js +120 -0
- package/src/bongos/hierarchy-config.js +189 -0
- package/src/bongos/live-channel.js +145 -0
- package/src/bongos/llm-cache.js +260 -0
- package/src/bongos/llm-pricing.js +220 -0
- package/src/bongos/metrics.js +286 -0
- package/src/bongos/middleware/audit.js +274 -0
- package/src/bongos/middleware/error-envelope.js +93 -0
- package/src/bongos/middleware/rate-limit.js +195 -0
- package/src/bongos/module-scope-map.js +271 -0
- package/src/bongos/optimizer.js +166 -0
- package/src/bongos/path-match.js +47 -0
- package/src/bongos/permission-path-check.js +248 -0
- package/src/bongos/pool.js +55 -0
- package/src/bongos/repo-info.js +91 -0
- package/src/bongos/route-rank-check.js +420 -0
- package/src/bongos/routes/CLAUDE.md +43 -0
- package/src/bongos/routes/_helpers.js +369 -0
- package/src/bongos/routes/audit-log.js +59 -0
- package/src/bongos/routes/auth.js +755 -0
- package/src/bongos/routes/backup.js +104 -0
- package/src/bongos/routes/builders.js +526 -0
- package/src/bongos/routes/healthz.js +16 -0
- package/src/bongos/routes/instance.js +129 -0
- package/src/bongos/routes/live.js +125 -0
- package/src/bongos/routes/llm-cache.js +119 -0
- package/src/bongos/routes/me.js +380 -0
- package/src/bongos/routes/modules.js +131 -0
- package/src/bongos/routes/optimizer.js +54 -0
- package/src/bongos/routes/public.js +476 -0
- package/src/bongos/routes/security.js +207 -0
- package/src/bongos/routes.js +166 -0
- package/src/bongos/secret-box.js +177 -0
- package/src/bongos/sensitive-surfaces.js +30 -0
- package/src/bongos/serve-internal.js +889 -0
- package/src/bongos/uptime-poller.js +90 -0
- package/src/bongos-downloads.js +70 -0
- package/src/branding.js +182 -0
- package/src/build-info.js +77 -0
- package/src/devbox-downloads.js +67 -0
- package/src/instance-config.js +200 -0
- package/src/module-api.js +366 -0
- package/src/module-loader/loader.js +268 -0
- package/src/module-loader/manifest-schema.js +140 -0
- package/src/module-loader/provenance.js +28 -0
- package/src/module-loader/semver.js +100 -0
- package/src/module-seams.js +291 -0
- package/src/modules.js +254 -0
- package/src/platform-server.js +135 -0
- package/src/ui/adapters/claude-design/index.js +333 -0
- package/src/ui/adapters/figma/index.js +328 -0
- package/tests/abandon_task.mjs +138 -0
- package/tests/achievements_reconcile.mjs +167 -0
- package/tests/adopt_detect.mjs +141 -0
- package/tests/adopt_import.mjs +132 -0
- package/tests/adopt_preflight.mjs +105 -0
- package/tests/anti_grid.mjs +89 -0
- package/tests/api_client.mjs +231 -0
- package/tests/api_docs.mjs +283 -0
- package/tests/api_error_envelope_buildcheck.mjs +56 -0
- package/tests/api_error_envelope_middleware.mjs +99 -0
- package/tests/api_error_registry.mjs +108 -0
- package/tests/api_pagination_contract.mjs +51 -0
- package/tests/api_path_404.mjs +96 -0
- package/tests/app_pair.mjs +168 -0
- package/tests/architect.mjs +140 -0
- package/tests/art_key.mjs +121 -0
- package/tests/art_stage.mjs +121 -0
- package/tests/asset_version.mjs +94 -0
- package/tests/atlas.mjs +69 -0
- package/tests/audit_deps.mjs +74 -0
- package/tests/audit_middleware.mjs +385 -0
- package/tests/auth_admission_gate.mjs +164 -0
- package/tests/auth_anthropic_identity.mjs +95 -0
- package/tests/auth_duplicate_cookie.mjs +56 -0
- package/tests/auth_error_brand.mjs +77 -0
- package/tests/auth_flow_slice1.mjs +223 -0
- package/tests/auth_inactive_builder.mjs +94 -0
- package/tests/auth_page_gate.mjs +92 -0
- package/tests/auth_session_ttl.mjs +155 -0
- package/tests/autonomous_fit_protected_paths.mjs +115 -0
- package/tests/autonomy_gate.mjs +85 -0
- package/tests/autotile_variants.mjs +256 -0
- package/tests/autotile_xverify.mjs +133 -0
- package/tests/backfill_sessions.mjs +101 -0
- package/tests/backfill_task_rewards.mjs +152 -0
- package/tests/backup_cli.mjs +104 -0
- package/tests/bfg.mjs +327 -0
- package/tests/bfg_write.mjs +423 -0
- package/tests/bongos_box_connect.mjs +89 -0
- package/tests/bongos_dispatch.mjs +74 -0
- package/tests/bongos_downloads.mjs +118 -0
- package/tests/bongos_login.mjs +34 -0
- package/tests/box_access.mjs +186 -0
- package/tests/box_auth_check.mjs +114 -0
- package/tests/box_code_staleness.mjs +116 -0
- package/tests/box_connect_e2e.mjs +196 -0
- package/tests/box_cost.mjs +70 -0
- package/tests/box_dns_repoint.mjs +151 -0
- package/tests/box_host_keys.mjs +102 -0
- package/tests/box_onboard.mjs +317 -0
- package/tests/box_scope_session.mjs +115 -0
- package/tests/box_ship_permissions.mjs +71 -0
- package/tests/box_terminal_ssrf.mjs +90 -0
- package/tests/boxes.mjs +1255 -0
- package/tests/branding.mjs +88 -0
- package/tests/bug_attachments.mjs +189 -0
- package/tests/build_info.mjs +88 -0
- package/tests/builder_needs.mjs +99 -0
- package/tests/cap_newcomer_tasks.mjs +114 -0
- package/tests/cascade-dispatch.mjs +95 -0
- package/tests/cf_tunnel.mjs +459 -0
- package/tests/claim_recall.mjs +150 -0
- package/tests/claims_bind_session.mjs +47 -0
- package/tests/claude_materialize.mjs +148 -0
- package/tests/cli_box_scope.mjs +52 -0
- package/tests/cli_lib_formatters.mjs +87 -0
- package/tests/cli_token_reissue.mjs +147 -0
- package/tests/client_bundle_route.mjs +47 -0
- package/tests/cloud_bongos_pack.mjs +85 -0
- package/tests/conductor.mjs +496 -0
- package/tests/confirm_attribution.mjs +156 -0
- package/tests/conflict_resolve.mjs +462 -0
- package/tests/context_pack.mjs +240 -0
- package/tests/cost.mjs +94 -0
- package/tests/cost_audit.mjs +122 -0
- package/tests/cost_budget_status.mjs +78 -0
- package/tests/cost_reconcile.mjs +102 -0
- package/tests/cost_summary_shape.mjs +94 -0
- package/tests/credit_grant.mjs +376 -0
- package/tests/criterion_progress.mjs +220 -0
- package/tests/criterion_proposal.mjs +109 -0
- package/tests/criterion_ref_parse.mjs +53 -0
- package/tests/currency_label.mjs +119 -0
- package/tests/day_cycle.mjs +197 -0
- package/tests/dependencies_migration.mjs +77 -0
- package/tests/dependencies_polymorphic.mjs +212 -0
- package/tests/deploy_instance.mjs +78 -0
- package/tests/devbox_downloads.mjs +122 -0
- package/tests/diff_derived_signals.mjs +115 -0
- package/tests/discord_approvals.mjs +174 -0
- package/tests/discord_autojoin.mjs +158 -0
- package/tests/discord_bot.mjs +94 -0
- package/tests/discord_bugs.mjs +297 -0
- package/tests/discord_channels.mjs +337 -0
- package/tests/discord_foundation.mjs +76 -0
- package/tests/discord_inbound.mjs +190 -0
- package/tests/discord_message_buffer.mjs +87 -0
- package/tests/discord_oauth.mjs +55 -0
- package/tests/discord_reaction.mjs +170 -0
- package/tests/discord_role_sync.mjs +151 -0
- package/tests/display_name.mjs +123 -0
- package/tests/dns_preflight.mjs +74 -0
- package/tests/event_sound_prefs.mjs +194 -0
- package/tests/eviction_policy.mjs +279 -0
- package/tests/figma_adapter.mjs +199 -0
- package/tests/file_map.mjs +91 -0
- package/tests/first_admin_bootstrap.mjs +40 -0
- package/tests/fitness.mjs +435 -0
- package/tests/fitness_two_root.mjs +104 -0
- package/tests/fixtures/session-506-digest.json +34 -0
- package/tests/gate_approvals.mjs +451 -0
- package/tests/gate_review.mjs +152 -0
- package/tests/git_merge_regen.mjs +151 -0
- package/tests/github_push_ancestry.mjs +119 -0
- package/tests/github_push_land_proof.mjs +127 -0
- package/tests/github_push_merge_driver.mjs +175 -0
- package/tests/goal_abuse_matrix.mjs +187 -0
- package/tests/goal_achievement.mjs +146 -0
- package/tests/goal_conflicts.mjs +305 -0
- package/tests/goal_dependencies_read.mjs +73 -0
- package/tests/goal_member_roles.mjs +420 -0
- package/tests/goal_membership_requests.mjs +332 -0
- package/tests/goal_routes.mjs +203 -0
- package/tests/goal_scope_check.mjs +167 -0
- package/tests/goal_tier.mjs +312 -0
- package/tests/goal_wall_proof.mjs +112 -0
- package/tests/grade_attempts.mjs +73 -0
- package/tests/grade_attribution.mjs +117 -0
- package/tests/grade_diff_hash.mjs +191 -0
- package/tests/grade_server_authoritative.mjs +305 -0
- package/tests/grade_unavailable_display.mjs +66 -0
- package/tests/grader.mjs +1960 -0
- package/tests/grader_bypass.mjs +57 -0
- package/tests/grader_panel.mjs +252 -0
- package/tests/grader_prompt_fencing.mjs +192 -0
- package/tests/helpers.mjs +52 -0
- package/tests/hierarchy-config.mjs +81 -0
- package/tests/init.mjs +463 -0
- package/tests/instance_config.mjs +76 -0
- package/tests/instance_manifest.mjs +110 -0
- package/tests/interaction_prefs.mjs +72 -0
- package/tests/key-card.mjs +110 -0
- package/tests/key-status.mjs +45 -0
- package/tests/land_watch.mjs +115 -0
- package/tests/lifecycle-card.mjs +145 -0
- package/tests/lifecycle_breakdown_view.mjs +236 -0
- package/tests/linkify_refs.mjs +264 -0
- package/tests/live_channel.mjs +128 -0
- package/tests/live_sse.mjs +122 -0
- package/tests/llm-cache-route.mjs +110 -0
- package/tests/llm-cache.mjs +140 -0
- package/tests/llm-pricing.mjs +152 -0
- package/tests/local_preview.mjs +136 -0
- package/tests/main_audit.mjs +137 -0
- package/tests/memory_isolation.mjs +431 -0
- package/tests/memory_map.mjs +107 -0
- package/tests/memory_pull.mjs +429 -0
- package/tests/merge_lease.mjs +100 -0
- package/tests/merge_lock.mjs +312 -0
- package/tests/metrics.mjs +147 -0
- package/tests/migration_allocation.mjs +146 -0
- package/tests/migration_namespace.mjs +185 -0
- package/tests/migrations_fresh_replay_guard.mjs +38 -0
- package/tests/mirror_lineage_fulltree.mjs +45 -0
- package/tests/mirror_lineage_proof.mjs +72 -0
- package/tests/mirror_redact.mjs +193 -0
- package/tests/model_alloc_prefs.mjs +63 -0
- package/tests/module-scope-map.mjs +88 -0
- package/tests/module_api.mjs +110 -0
- package/tests/module_cli.mjs +292 -0
- package/tests/module_contributions.mjs +148 -0
- package/tests/module_loader.mjs +286 -0
- package/tests/module_manifest.mjs +194 -0
- package/tests/module_owned_tests.mjs +105 -0
- package/tests/module_provenance.mjs +34 -0
- package/tests/module_route_rank.mjs +147 -0
- package/tests/module_seams.mjs +68 -0
- package/tests/modules.mjs +156 -0
- package/tests/modules_route.mjs +38 -0
- package/tests/newcomer_floor.mjs +17 -0
- package/tests/newcomer_restock.mjs +80 -0
- package/tests/next_number.mjs +154 -0
- package/tests/oauth_secret.mjs +60 -0
- package/tests/onboard.mjs +70 -0
- package/tests/onboarding_gate.mjs +267 -0
- package/tests/onboarding_observability.mjs +344 -0
- package/tests/onboarding_route_signals.mjs +175 -0
- package/tests/onboarding_state.mjs +346 -0
- package/tests/one_active_claim_per_session.mjs +111 -0
- package/tests/orphaned_claim_ship.mjs +135 -0
- package/tests/override_attribution.mjs +142 -0
- package/tests/override_request_ownership.mjs +235 -0
- package/tests/package_core.mjs +435 -0
- package/tests/parallel-load.mjs +402 -0
- package/tests/path_match.mjs +62 -0
- package/tests/permission_path.mjs +151 -0
- package/tests/platform_boot.mjs +167 -0
- package/tests/pre_compact.mjs +60 -0
- package/tests/preview_server.mjs +127 -0
- package/tests/profile_route.mjs +197 -0
- package/tests/provision.mjs +805 -0
- package/tests/provisioning_github_app_routes.mjs +98 -0
- package/tests/public_mirror_export.mjs +124 -0
- package/tests/public_task_resolver.mjs +112 -0
- package/tests/publish_branch_route.mjs +422 -0
- package/tests/publish_manifest.mjs +184 -0
- package/tests/publish_reconciler.mjs +728 -0
- package/tests/rank_gate.mjs +463 -0
- package/tests/rate_limit_public.mjs +217 -0
- package/tests/read_session_export.mjs +89 -0
- package/tests/recall_cli.mjs +82 -0
- package/tests/recall_hints.mjs +119 -0
- package/tests/recent_shipped_window.mjs +80 -0
- package/tests/regrade_eligibility.mjs +202 -0
- package/tests/render_prefs.mjs +122 -0
- package/tests/repo_map.mjs +92 -0
- package/tests/reward_gate.mjs +284 -0
- package/tests/route_rank_check.mjs +243 -0
- package/tests/route_validation.mjs +490 -0
- package/tests/routine_timers.mjs +72 -0
- package/tests/rubric_fixtures.mjs +140 -0
- package/tests/run_routine.mjs +69 -0
- package/tests/sandbox_stage.mjs +582 -0
- package/tests/scrubber_corpus.mjs +257 -0
- package/tests/search_chunker.mjs +209 -0
- package/tests/search_ingest.mjs +96 -0
- package/tests/search_ingest_db.mjs +88 -0
- package/tests/search_isolation.mjs +204 -0
- package/tests/search_route.mjs +95 -0
- package/tests/secret_box.mjs +189 -0
- package/tests/security_docs.mjs +186 -0
- package/tests/security_route.mjs +108 -0
- package/tests/served_paths_shipped.mjs +118 -0
- package/tests/session-cards.mjs +142 -0
- package/tests/session_506_regression.mjs +69 -0
- package/tests/session_caps.mjs +238 -0
- package/tests/session_cost.mjs +133 -0
- package/tests/session_digest.mjs +73 -0
- package/tests/session_eval.mjs +386 -0
- package/tests/session_index.mjs +105 -0
- package/tests/session_records.mjs +369 -0
- package/tests/session_search.mjs +78 -0
- package/tests/session_start_freshness.mjs +123 -0
- package/tests/setup_smoke.mjs +154 -0
- package/tests/ship_autolinkify.mjs +60 -0
- package/tests/ship_automerge_injection.mjs +174 -0
- package/tests/ship_broadcast_routing.mjs +125 -0
- package/tests/ship_broadcast_server.mjs +175 -0
- package/tests/ship_card.mjs +280 -0
- package/tests/ship_ci_deploy.mjs +374 -0
- package/tests/ship_preflight.mjs +368 -0
- package/tests/ship_premerge.mjs +75 -0
- package/tests/ship_resume.mjs +129 -0
- package/tests/ship_upload_skip.mjs +89 -0
- package/tests/ship_zero_commit.mjs +60 -0
- package/tests/single_host_hall.mjs +98 -0
- package/tests/skill_prefs.mjs +150 -0
- package/tests/smoke_preflight.mjs +66 -0
- package/tests/snapshot_picker.mjs +233 -0
- package/tests/sound_prefs.mjs +198 -0
- package/tests/sprite_facing.mjs +35 -0
- package/tests/status-probe.mjs +89 -0
- package/tests/status_backfill.mjs +94 -0
- package/tests/stop_refcheck.mjs +127 -0
- package/tests/streak.mjs +175 -0
- package/tests/subdomain_auth.mjs +58 -0
- package/tests/task_classifier_vocab.mjs +41 -0
- package/tests/task_create_idempotency.mjs +96 -0
- package/tests/task_goal_link.mjs +93 -0
- package/tests/task_ownership_gate.mjs +125 -0
- package/tests/terrain_picker.mjs +298 -0
- package/tests/touches_overlap.mjs +75 -0
- package/tests/triage_development.mjs +140 -0
- package/tests/turn_in_place.mjs +280 -0
- package/tests/upgrade.mjs +357 -0
- package/tests/verify_task_guard.mjs +64 -0
- package/tests/version_close.mjs +76 -0
- package/tests/versions_route.mjs +93 -0
- package/tests/walkability.mjs +188 -0
- package/tests/wandering_prefs.mjs +176 -0
- package/tests/worktree_disambiguation.mjs +105 -0
- package/tests/worktree_guard.mjs +90 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
// tests/provision.mjs
|
|
2
|
+
//
|
|
3
|
+
// Hermetic unit smoke for the instance-provisioning control-plane runner
|
|
4
|
+
// (scripts/gds/provision.js, ADR 0111 §2/§3, task #1944). Covers the PURE runbook
|
|
5
|
+
// step-command generators, port allocation, and the dry-run COMPOSITION of
|
|
6
|
+
// provisionInstance / teardownInstance / cmdRunIntents — all with NO database, NO
|
|
7
|
+
// DigitalOcean, NO shell, NO network (exec + writeFile + db are recording fakes).
|
|
8
|
+
// The LIVE leg (real createdb/migrate/systemctl/caddy) is exercised on the control
|
|
9
|
+
// plane in --apply; the composition + ordering it relies on are pinned here.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/provision.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const P = require('../scripts/gds/provision.js');
|
|
21
|
+
|
|
22
|
+
// The env-var prefix a provisioned instance resolves = the envPrefix of the pack
|
|
23
|
+
// webEnvBody points GDS_BRANDING_FILE at (config/branding.neutral.json). Read it
|
|
24
|
+
// here so these assertions stay in lockstep with the code (both derive it), not a
|
|
25
|
+
// pinned literal that would rot if the neutral default ever changed.
|
|
26
|
+
const NEUTRAL_PREFIX =
|
|
27
|
+
JSON.parse(readFileSync(new URL('../config/branding.neutral.json', import.meta.url), 'utf8')).envPrefix || 'CLOUDBONGOS';
|
|
28
|
+
const reEsc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
|
|
30
|
+
let passed = 0, failed = 0;
|
|
31
|
+
function t(name, fn) {
|
|
32
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
33
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
34
|
+
}
|
|
35
|
+
async function ta(name, fn) {
|
|
36
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
37
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const coTenant = { id: 1, slug: 'mercury', hosting_shape: 'co-tenant', status: 'requested', owner_builder_id: 7, port: 3003, db_name: 'mercury', domain: null };
|
|
41
|
+
|
|
42
|
+
console.log('pure step generators:');
|
|
43
|
+
|
|
44
|
+
t('dbCreateCmd / migrateCmd use the slug db + GDS-only migrate', () => {
|
|
45
|
+
assert.equal(P.dbCreateCmd(coTenant), 'createdb mercury');
|
|
46
|
+
assert.match(P.migrateCmd(coTenant), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 \.\/scripts\/migrate\.sh/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
t('serviceUnit binds the instance port + DB + per-instance env file, runs platform-server', () => {
|
|
50
|
+
const u = P.serviceUnit(coTenant, { etcBase: '/etc' });
|
|
51
|
+
assert.match(u, /Environment=PORT=3003/);
|
|
52
|
+
assert.match(u, /Environment=PGDATABASE=mercury/);
|
|
53
|
+
assert.match(u, /EnvironmentFile=\/etc\/mercury\/web\.env/);
|
|
54
|
+
assert.match(u, /ExecStart=\/usr\/bin\/node src\/platform-server\.js/);
|
|
55
|
+
assert.match(u, /WantedBy=multi-user\.target/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
t('serviceUnit(standalone) runs the CORE PACKAGE server from the instance\'s OWN repo dir (ADR 0108)', () => {
|
|
59
|
+
const inst = { ...coTenant, hosting_shape: 'standalone' };
|
|
60
|
+
const u = P.serviceUnit(inst, { etcBase: '/etc' });
|
|
61
|
+
const root = P.standaloneRoot(inst);
|
|
62
|
+
assert.match(u, new RegExp('WorkingDirectory=' + reEsc(root)), 'runs from the instance\'s own repo dir');
|
|
63
|
+
assert.match(u, /ExecStart=\/usr\/bin\/node node_modules\/@cloudbongos\/core\/src\/platform-server\.js/, 'runs the pinned core package server');
|
|
64
|
+
assert.match(u, /Environment=PORT=3003/);
|
|
65
|
+
assert.match(u, /EnvironmentFile=\/etc\/mercury\/web\.env/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t('standalone step generators: root under standaloneBase + pull / npm-ci / instance-root migrate', () => {
|
|
69
|
+
const inst = { ...coTenant, hosting_shape: 'standalone' };
|
|
70
|
+
assert.match(P.standaloneRoot(inst), /\/mercury$/);
|
|
71
|
+
assert.equal(P.standalonePullCmd(), 'git pull --ff-only origin main');
|
|
72
|
+
assert.equal(P.standaloneInstallCmd(), 'npm ci --omit=dev');
|
|
73
|
+
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@cloudbongos\/core\/scripts\/migrate\.sh/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + brand/port/db', () => {
|
|
77
|
+
const b = P.webEnvBody(coTenant); // coTenant.domain === null
|
|
78
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=`));
|
|
79
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_SECRET=`));
|
|
80
|
+
assert.match(b, /GDS_BRANDING_FILE=config\/branding\.neutral\.json/);
|
|
81
|
+
assert.match(b, /PORT=3003/);
|
|
82
|
+
assert.match(b, /PGDATABASE=mercury/);
|
|
83
|
+
// The prior slug prefix was a latent bug: it never matched the neutral pack's
|
|
84
|
+
// envPrefix, so resolveEnv could not read the OAuth creds. It must be gone.
|
|
85
|
+
assert.doesNotMatch(b, /MERCURY_GITHUB_CLIENT_ID=/);
|
|
86
|
+
// No domain ⇒ NO origin overrides (writing them would pin neutral/localhost wrongly).
|
|
87
|
+
assert.doesNotMatch(b, /_BUILDERS_ORIGIN=/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
t('webEnvBody: a domain writes the single-host origin overrides so the hall loads (task 1972)', () => {
|
|
91
|
+
const b = P.webEnvBody({ ...coTenant, domain: 'mercury.example.com' });
|
|
92
|
+
const O = reEsc('https://mercury.example.com');
|
|
93
|
+
// publicOrigin + buildersOrigin (the hall) + statusOrigin + oauthOrigin all → the one host,
|
|
94
|
+
// under the prefix src/branding.js's ENV_OVERRIDES actually reads.
|
|
95
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_PUBLIC_ORIGIN=${O}`));
|
|
96
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_BUILDERS_ORIGIN=${O}`));
|
|
97
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_STATUS_ORIGIN=${O}`));
|
|
98
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_OAUTH_ORIGIN=${O}`));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
t('caddyBlock: a domain → on-demand-TLS reverse_proxy to the local port; no domain → a comment', () => {
|
|
102
|
+
const withDomain = P.caddyBlock({ ...coTenant, domain: 'mercury.example.com' });
|
|
103
|
+
assert.match(withDomain, /mercury\.example\.com \{/);
|
|
104
|
+
assert.match(withDomain, /reverse_proxy 127\.0\.0\.1:3003/);
|
|
105
|
+
// P4: on-demand ACME TLS (no manual Origin-Cert leg), gated by on_demand_tls ask.
|
|
106
|
+
assert.match(withDomain, /tls \{/);
|
|
107
|
+
assert.match(withDomain, /on_demand/);
|
|
108
|
+
const noDomain = P.caddyBlock(coTenant);
|
|
109
|
+
assert.match(noDomain, /^#/);
|
|
110
|
+
assert.doesNotMatch(noDomain, /reverse_proxy/);
|
|
111
|
+
assert.doesNotMatch(noDomain, /on_demand/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log('\nDNS UPSERT helpers (P4):');
|
|
115
|
+
|
|
116
|
+
t('zoneForDomain: registrable (last two labels), apex, trailing dot, empty', () => {
|
|
117
|
+
assert.equal(P.zoneForDomain('mercury.example.com'), 'example.com');
|
|
118
|
+
assert.equal(P.zoneForDomain('example.com'), 'example.com');
|
|
119
|
+
assert.equal(P.zoneForDomain('a.b.c.example.com'), 'example.com');
|
|
120
|
+
assert.equal(P.zoneForDomain('EXAMPLE.COM.'), 'example.com');
|
|
121
|
+
assert.equal(P.zoneForDomain(''), '');
|
|
122
|
+
assert.equal(P.zoneForDomain(null), '');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
t('dnsUpsertEnv: hostname + target IP + derived zone; an explicit zone wins', () => {
|
|
126
|
+
const e = P.dnsUpsertEnv({ domain: 'mercury.example.com' }, { publicIp: '203.0.113.5' });
|
|
127
|
+
assert.equal(e.BOX_HOSTNAME, 'mercury.example.com');
|
|
128
|
+
assert.equal(e.BOX_IP, '203.0.113.5');
|
|
129
|
+
assert.equal(e.CLOUDFLARE_ZONE, 'example.com');
|
|
130
|
+
// A multi-part public suffix (foo.co.uk) can't be derived — the operator overrides.
|
|
131
|
+
const e2 = P.dnsUpsertEnv({ domain: 'x.foo.co.uk' }, { publicIp: '1.2.3.4', zone: 'foo.co.uk' });
|
|
132
|
+
assert.equal(e2.CLOUDFLARE_ZONE, 'foo.co.uk');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
t('dnsUpsertEnv: BOX_PROXIED defaults off (co-tenant on-demand ACME); proxied:true opts in (standalone behind the wildcard origin cert, task 2077)', () => {
|
|
136
|
+
assert.equal(P.dnsUpsertEnv({ domain: 'mercury.example.com' }, { publicIp: '203.0.113.5' }).BOX_PROXIED, '0');
|
|
137
|
+
assert.equal(P.dnsUpsertEnv({ domain: 'mercury.example.com' }, { publicIp: '203.0.113.5', proxied: false }).BOX_PROXIED, '0');
|
|
138
|
+
assert.equal(P.dnsUpsertEnv({ domain: 'demo.cloudbongos.com' }, { publicIp: '161.35.132.201', proxied: true }).BOX_PROXIED, '1');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
t('healthzCmd hits the instance port /healthz', () => {
|
|
142
|
+
assert.match(P.healthzCmd(coTenant), /127\.0\.0\.1:3003\/healthz/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
t('dedicatedUserData bakes createdb + migrate into cloud-init', () => {
|
|
146
|
+
const ud = P.dedicatedUserData(coTenant);
|
|
147
|
+
assert.match(ud, /^#cloud-config/);
|
|
148
|
+
assert.match(ud, /createdb mercury/);
|
|
149
|
+
assert.match(ud, /MEDUSA_GDS_ONLY=1/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
console.log('\nport allocation:');
|
|
153
|
+
|
|
154
|
+
t('nextFreePort skips reserved + taken, returns the lowest free ≥ base', () => {
|
|
155
|
+
assert.equal(P.nextFreePort([], { portBase: 3003, reserved: [3000, 3001, 3002] }), 3003);
|
|
156
|
+
assert.equal(P.nextFreePort([3003, 3004], { portBase: 3003, reserved: [3000, 3001, 3002] }), 3005);
|
|
157
|
+
assert.equal(P.nextFreePort([3005], { portBase: 3003, reserved: [3003] }), 3004);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
t('boundLocalPorts parses ss output — skips header, handles wildcard + IPv6, degrades safely', () => {
|
|
161
|
+
const exec = () => ({ ok: true, stdout:
|
|
162
|
+
'State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\n' +
|
|
163
|
+
'LISTEN 0 4096 127.0.0.1:3003 0.0.0.0:*\n' +
|
|
164
|
+
'LISTEN 0 511 127.0.0.1:3002 0.0.0.0:*\n' +
|
|
165
|
+
'LISTEN 0 128 0.0.0.0:22 0.0.0.0:*\n' +
|
|
166
|
+
'LISTEN 0 128 [::1]:3000 [::]:*\n' });
|
|
167
|
+
assert.deepEqual(P.boundLocalPorts(exec).sort((a, b) => a - b), [22, 3000, 3002, 3003]);
|
|
168
|
+
assert.deepEqual(P.boundLocalPorts(null), []); // no exec → safe empty
|
|
169
|
+
assert.deepEqual(P.boundLocalPorts(() => { throw new Error('ss missing'); }), []); // ss unavailable → safe empty
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await ta('allocatePort excludes OS-bound ports (regression: Grafana squatting :3003 on the co-located control plane, task 2070)', async () => {
|
|
173
|
+
const db = { query: async () => ({ rows: [{ port: 3004 }] }) }; // an existing instance already on 3004
|
|
174
|
+
const grafanaOn3003 = () => ({ ok: true, stdout: 'LISTEN 0 4096 127.0.0.1:3003 0.0.0.0:*\n' });
|
|
175
|
+
// reserved 3000-3002 + instance 3004 + OS-bound 3003 → lowest free ≥ base is 3005
|
|
176
|
+
assert.equal(await P.allocatePort(db, null, { exec: grafanaOn3003 }), 3005);
|
|
177
|
+
// WITHOUT the OS view the allocator would hand out 3003 → the exact EADDRINUSE collision this fixes
|
|
178
|
+
assert.equal(await P.allocatePort(db, null), 3003);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
console.log('\nco-tenant composition (dry-run, recording fakes):');
|
|
182
|
+
|
|
183
|
+
// A recording harness: exec + writeFile capture what the runbook WOULD do; db is a
|
|
184
|
+
// fake whose only query (allocatePort) returns no rows; provisioning is a no-op
|
|
185
|
+
// (apply:false skips the status writes anyway).
|
|
186
|
+
function recorder({ apply = false } = {}) {
|
|
187
|
+
const cmds = [], writes = [], statuses = [];
|
|
188
|
+
const deps = {
|
|
189
|
+
apply, log: () => {},
|
|
190
|
+
db: { query: async () => ({ rows: [] }) },
|
|
191
|
+
provisioning: {
|
|
192
|
+
setInstanceStatus: async (_db, _id, s) => { statuses.push(s); },
|
|
193
|
+
recordEvent: async () => {},
|
|
194
|
+
getInstanceBySlug: async () => null,
|
|
195
|
+
claimNextIntent: async () => null,
|
|
196
|
+
resolveIntent: async () => {},
|
|
197
|
+
enqueueIntent: async () => ({ created: true }),
|
|
198
|
+
getInstanceById: async () => null,
|
|
199
|
+
},
|
|
200
|
+
exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: '' }; },
|
|
201
|
+
writeFile: (p) => { writes.push(p); return { ok: true }; },
|
|
202
|
+
doApi: null,
|
|
203
|
+
// Stub the DNS token-scope preflight (task 1980) so the composition stays hermetic
|
|
204
|
+
// (no real CF call). Default: reachable; a test overrides it to exercise the block.
|
|
205
|
+
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
206
|
+
};
|
|
207
|
+
return { deps, cmds, writes, statuses };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await ta('provisionInstance(co-tenant) composes db → migrate → service → healthz in order', async () => {
|
|
211
|
+
const { deps, cmds, writes } = recorder();
|
|
212
|
+
const r = await P.provisionInstance(coTenant, deps);
|
|
213
|
+
assert.equal(r.ok, true);
|
|
214
|
+
// the runbook command sequence
|
|
215
|
+
const joined = cmds.join('\n');
|
|
216
|
+
assert.match(joined, /createdb mercury/);
|
|
217
|
+
assert.match(joined, /MEDUSA_GDS_ONLY=1/);
|
|
218
|
+
assert.match(joined, /systemctl daemon-reload/);
|
|
219
|
+
assert.match(joined, /systemctl enable --now mercury/);
|
|
220
|
+
assert.match(joined, /healthz/);
|
|
221
|
+
// ordering: createdb before migrate before enable before healthz
|
|
222
|
+
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
223
|
+
assert.ok(idx(/createdb/) < idx(/migrate\.sh/), 'createdb before migrate');
|
|
224
|
+
assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
|
|
225
|
+
assert.ok(idx(/enable --now/) < idx(/healthz/), 'service before healthz');
|
|
226
|
+
// writes: the per-instance env file + the systemd unit
|
|
227
|
+
assert.ok(writes.some((w) => /\/mercury\/web\.env$/.test(w)), 'wrote web.env');
|
|
228
|
+
assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote systemd unit');
|
|
229
|
+
assert.ok(writes.some((w) => /\/mercury\.caddy$/.test(w)), 'wrote per-instance Caddy snippet');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + npm ci) + migrates from the instance root', async () => {
|
|
233
|
+
const { deps, cmds, writes } = recorder();
|
|
234
|
+
const r = await P.provisionInstance({ ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury' }, deps);
|
|
235
|
+
assert.equal(r.ok, true);
|
|
236
|
+
const joined = cmds.join('\n');
|
|
237
|
+
// scaffold leg (task 2055): clone → package-core → init --vendor-core → push
|
|
238
|
+
assert.match(joined, /git clone git@github-deploy:lars589\/mercury\.git/, 'clones the instance repo');
|
|
239
|
+
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball');
|
|
240
|
+
assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'materializes + file:-pins the core');
|
|
241
|
+
assert.match(joined, /git -C .* push -u origin main/, 'pushes the scaffold');
|
|
242
|
+
// then the code sync + migrate
|
|
243
|
+
assert.match(joined, /git pull --ff-only origin main/, 'pulls the instance repo');
|
|
244
|
+
assert.match(joined, /npm ci --omit=dev/, 'installs the pinned core');
|
|
245
|
+
assert.match(joined, /createdb mercury/);
|
|
246
|
+
assert.match(joined, /node_modules\/@cloudbongos\/core\/scripts\/migrate\.sh/, 'migrates from the INSTANCE root\'s core');
|
|
247
|
+
assert.ok(!cmds.some((c) => /\.\/scripts\/migrate\.sh/.test(c)), 'standalone does NOT use the control-plane checkout migrate');
|
|
248
|
+
// ordering: clone → package-core → init → push → pull → npm ci → migrate → service
|
|
249
|
+
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
250
|
+
assert.ok(idx(/git clone/) < idx(/package-core/), 'clone before tarball build');
|
|
251
|
+
assert.ok(idx(/package-core/) < idx(/init\.js/), 'tarball before init');
|
|
252
|
+
assert.ok(idx(/init\.js/) < idx(/push -u origin main/), 'init before push');
|
|
253
|
+
assert.ok(idx(/push -u origin main/) < idx(/git pull/), 'scaffold push before the deploy pull');
|
|
254
|
+
assert.ok(idx(/git pull/) < idx(/npm ci/), 'pull before npm ci');
|
|
255
|
+
assert.ok(idx(/npm ci/) < idx(/migrate\.sh/), 'npm ci before migrate');
|
|
256
|
+
assert.ok(idx(/migrate\.sh/) < idx(/enable --now/), 'migrate before service enable');
|
|
257
|
+
assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote the (standalone) systemd unit');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ── standalone scaffold helpers + guards (task 2055) ──────────────────────────
|
|
261
|
+
t('parseTargetRef: owner/repo, SSH + HTTPS URLs, .git suffix; garbage → null', () => {
|
|
262
|
+
assert.deepEqual(P.parseTargetRef('lars589/demo'), { owner: 'lars589', name: 'demo' });
|
|
263
|
+
assert.deepEqual(P.parseTargetRef('git@github-deploy:lars589/demo.git'), { owner: 'lars589', name: 'demo' });
|
|
264
|
+
assert.deepEqual(P.parseTargetRef('https://github.com/lars589/demo.git'), { owner: 'lars589', name: 'demo' });
|
|
265
|
+
assert.equal(P.parseTargetRef('garbage'), null);
|
|
266
|
+
assert.equal(P.parseTargetRef(''), null);
|
|
267
|
+
assert.equal(P.parseTargetRef(null), null);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
t('instanceRepoRemote: reuses the SSH deploy alias (no secret in the URL); alias overridable', () => {
|
|
271
|
+
assert.equal(P.instanceRepoRemote({ owner: 'lars589', name: 'demo' }), 'git@github-deploy:lars589/demo.git');
|
|
272
|
+
assert.equal(P.instanceRepoRemote({ owner: 'o', name: 'r' }, { sshAlias: 'gh-alt' }), 'git@gh-alt:o/r.git');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
t('instanceInitSpec: builds a VALID greenfield init spec from the row + owner login', () => {
|
|
276
|
+
const spec = P.instanceInitSpec(
|
|
277
|
+
{ slug: 'demo', target_ref: 'lars589/demo', domain: 'demo.cloudbongos.com', port: 3003 },
|
|
278
|
+
{ ownerLogin: 'lars589' },
|
|
279
|
+
);
|
|
280
|
+
assert.equal(spec.identity.productName, 'demo');
|
|
281
|
+
assert.deepEqual(spec.repo, { owner: 'lars589', name: 'demo' });
|
|
282
|
+
assert.equal(spec.firstAdmin, 'lars589');
|
|
283
|
+
assert.equal(spec.domains.publicOrigin, 'https://demo.cloudbongos.com');
|
|
284
|
+
// it must satisfy init.validateSpec (greenfield) — the scaffold feeds it to `bongos init`
|
|
285
|
+
const init = require('../scripts/gds/init.js');
|
|
286
|
+
assert.deepEqual(init.validateSpec(init.normalizeSpec(spec)), [], 'spec passes greenfield validation');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
t('instanceInitSpec: falls back to the repo owner as firstAdmin when no owner login', () => {
|
|
290
|
+
const spec = P.instanceInitSpec({ slug: 'demo', target_ref: 'acme/demo' }, {});
|
|
291
|
+
assert.equal(spec.firstAdmin, 'acme');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await ta('scaffoldStandaloneRepo: refuses when target_ref is not owner/repo', async () => {
|
|
295
|
+
const { deps } = recorder({ apply: true });
|
|
296
|
+
await assert.rejects(
|
|
297
|
+
() => P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'standalone', target_ref: null }, deps),
|
|
298
|
+
/target_ref .* is not an owner\/repo/,
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries package.json is skipped', async () => {
|
|
303
|
+
const tmp = mkdtempSync(join(tmpdir(), 'scaffold-'));
|
|
304
|
+
const origBase = P.CONFIG.standaloneBase;
|
|
305
|
+
P.CONFIG.standaloneBase = tmp; // standaloneRoot() reads CONFIG.standaloneBase at call time
|
|
306
|
+
try {
|
|
307
|
+
mkdirSync(join(tmp, 'demo'), { recursive: true });
|
|
308
|
+
writeFileSync(join(tmp, 'demo', 'package.json'), '{}\n'); // sign of a completed scaffold
|
|
309
|
+
const { deps, cmds } = recorder({ apply: true });
|
|
310
|
+
const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' }, deps);
|
|
311
|
+
assert.equal(res.already, true);
|
|
312
|
+
assert.equal(cmds.length, 0, 'no clone/build/push when already scaffolded');
|
|
313
|
+
} finally {
|
|
314
|
+
P.CONFIG.standaloneBase = origBase;
|
|
315
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await ta('provisionInstance is idempotent: an already-active instance is a no-op', async () => {
|
|
320
|
+
const { deps, cmds } = recorder();
|
|
321
|
+
const r = await P.provisionInstance({ ...coTenant, status: 'active' }, deps);
|
|
322
|
+
assert.equal(r.already, true);
|
|
323
|
+
assert.equal(cmds.length, 0, 'no commands run for an active instance');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await ta('provisionInstance(dedicated) does NOT run local createdb/migrate (they ride cloud-init)', async () => {
|
|
327
|
+
const { deps, cmds } = recorder();
|
|
328
|
+
await P.provisionInstance({ ...coTenant, hosting_shape: 'dedicated', tier: 's-2vcpu-4gb' }, deps);
|
|
329
|
+
assert.ok(!cmds.some((c) => /createdb/.test(c)), 'dedicated does not run createdb locally');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
console.log('\nDNS UPSERT composition (P4, CONFIG override):');
|
|
333
|
+
|
|
334
|
+
// A recorder that also captures each exec's env (so we can assert the DNS hook env).
|
|
335
|
+
function dnsRecorder({ apply = false } = {}) {
|
|
336
|
+
const cmds = [], envs = [];
|
|
337
|
+
const deps = {
|
|
338
|
+
apply, log: () => {},
|
|
339
|
+
db: { query: async () => ({ rows: [] }) },
|
|
340
|
+
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
341
|
+
exec: (cmd, opts) => { cmds.push(cmd); envs.push(opts && opts.env); return { ok: true, stdout: '' }; },
|
|
342
|
+
writeFile: () => ({ ok: true }), doApi: null,
|
|
343
|
+
// Preflight (task 1980) stubbed reachable so --apply DNS tests reach the step-5
|
|
344
|
+
// behavior they assert (the preflight itself is covered by its own test below).
|
|
345
|
+
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
346
|
+
};
|
|
347
|
+
return { deps, cmds, envs };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await ta('a co-tenant WITH a domain UPSERTs DNS: the hook is invoked BY PATH with the control-plane env (proxied=false)', async () => {
|
|
351
|
+
const savedIp = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
|
|
352
|
+
P.CONFIG.publicIp = '203.0.113.5';
|
|
353
|
+
P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
354
|
+
try {
|
|
355
|
+
const { deps, cmds, envs } = dnsRecorder();
|
|
356
|
+
await P.provisionInstance({ ...coTenant, domain: 'mercury.example.com' }, deps);
|
|
357
|
+
const i = cmds.indexOf('/opt/infra/box-dns-cloudflare.sh');
|
|
358
|
+
assert.ok(i >= 0, 'dns hook invoked by path (not string-interpolated)');
|
|
359
|
+
assert.equal(envs[i].BOX_HOSTNAME, 'mercury.example.com');
|
|
360
|
+
assert.equal(envs[i].BOX_IP, '203.0.113.5');
|
|
361
|
+
assert.equal(envs[i].CLOUDFLARE_ZONE, 'example.com');
|
|
362
|
+
assert.equal(envs[i].BOX_PROXIED, '0', 'co-tenant stays UNPROXIED — its on-demand ACME challenge must reach the origin (task 2077 only proxies standalone)');
|
|
363
|
+
} finally { P.CONFIG.publicIp = savedIp; P.CONFIG.dnsHook = savedHook; }
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await ta('--apply THROWS if a domain has no target IP (task-1180: never report success on a silent DNS skip)', async () => {
|
|
367
|
+
const savedIp = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
|
|
368
|
+
P.CONFIG.publicIp = null;
|
|
369
|
+
P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
370
|
+
try {
|
|
371
|
+
const { deps } = dnsRecorder({ apply: true });
|
|
372
|
+
await assert.rejects(
|
|
373
|
+
() => P.provisionInstance({ ...coTenant, domain: 'mercury.example.com' }, deps),
|
|
374
|
+
/no target IP|PROVISION_PUBLIC_IP/,
|
|
375
|
+
);
|
|
376
|
+
} finally { P.CONFIG.publicIp = savedIp; P.CONFIG.dnsHook = savedHook; }
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await ta('no domain → DNS is skipped even with a hook + IP configured', async () => {
|
|
380
|
+
const savedIp = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
|
|
381
|
+
P.CONFIG.publicIp = '203.0.113.5';
|
|
382
|
+
P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
383
|
+
try {
|
|
384
|
+
const { deps, cmds } = dnsRecorder();
|
|
385
|
+
await P.provisionInstance({ ...coTenant, domain: null }, deps);
|
|
386
|
+
assert.ok(!cmds.includes('/opt/infra/box-dns-cloudflare.sh'), 'no DNS hook without a domain');
|
|
387
|
+
} finally { P.CONFIG.publicIp = savedIp; P.CONFIG.dnsHook = savedHook; }
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await ta('provisionInstance(--apply) FAILS FAST when the DNS token can\'t reach the zone (task 1980 preflight)', async () => {
|
|
391
|
+
const savedHook = P.CONFIG.dnsHook; P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
392
|
+
try {
|
|
393
|
+
const { deps, cmds } = dnsRecorder({ apply: true });
|
|
394
|
+
deps.checkDnsTokenScope = async () => ({ reachable: false, zone: 'newproject.com', reason: 'not in scope', guidance: 'widen the token' });
|
|
395
|
+
await assert.rejects(
|
|
396
|
+
() => P.provisionInstance({ ...coTenant, domain: 'app.newproject.com' }, deps),
|
|
397
|
+
/DNS preflight failed.*widen the token/,
|
|
398
|
+
'a non-reachable token must fail the provision up front',
|
|
399
|
+
);
|
|
400
|
+
assert.ok(!cmds.some((c) => /createdb/.test(c)), 'nothing is provisioned when the preflight fails');
|
|
401
|
+
} finally { P.CONFIG.dnsHook = savedHook; }
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
console.log('\nintent drain loop:');
|
|
405
|
+
|
|
406
|
+
await ta('cmdRunIntents (dry-run) lists the pending queue, runs nothing', async () => {
|
|
407
|
+
const deps = {
|
|
408
|
+
apply: false, log: () => {},
|
|
409
|
+
db: { query: async () => ({ rows: [{ id: 1, action: 'provision', slug: 'mercury', attempts: 0 }] }) },
|
|
410
|
+
provisioning: {}, exec: () => {}, writeFile: () => {},
|
|
411
|
+
};
|
|
412
|
+
const r = await P.cmdRunIntents(deps);
|
|
413
|
+
assert.equal(r.ran, 0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
await ta('cmdRunIntents (--apply) claims → provisions → resolves the intent done', async () => {
|
|
417
|
+
let claims = 0; const resolved = [];
|
|
418
|
+
const deps = {
|
|
419
|
+
apply: true, log: () => {},
|
|
420
|
+
db: { query: async () => ({ rows: [] }) },
|
|
421
|
+
provisioning: {
|
|
422
|
+
claimNextIntent: async () => (claims++ === 0 ? { id: 5, instance_id: 1, action: 'provision', attempts: 1 } : null),
|
|
423
|
+
getInstanceById: async () => ({ ...coTenant }),
|
|
424
|
+
resolveIntent: async (_db, id, state) => { resolved.push({ id, state }); },
|
|
425
|
+
setInstanceStatus: async () => {},
|
|
426
|
+
recordEvent: async () => {},
|
|
427
|
+
// The same tick also drains the GitHub-App-Manifest conversion queue (task 2080);
|
|
428
|
+
// nothing awaiting conversion here → a clean no-op.
|
|
429
|
+
claimNextOAuthManifest: async () => null,
|
|
430
|
+
resolveOAuthManifest: async () => {},
|
|
431
|
+
},
|
|
432
|
+
exec: () => ({ ok: true }), writeFile: () => ({ ok: true }), doApi: null,
|
|
433
|
+
};
|
|
434
|
+
const r = await P.cmdRunIntents(deps);
|
|
435
|
+
assert.equal(r.ran, 1);
|
|
436
|
+
assert.equal(r.ok, 1);
|
|
437
|
+
assert.deepEqual(resolved, [{ id: 5, state: 'done' }]);
|
|
438
|
+
assert.deepEqual(r.manifests, { converted: 0, errors: 0 }, 'the manifest drain ran with nothing to do');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
console.log('\nGitHub App Manifest conversion + placement (task 2080, ADR 0133):');
|
|
442
|
+
|
|
443
|
+
t('webEnvBody fills the OAuth cred lines when creds are given; blank placeholders otherwise', () => {
|
|
444
|
+
const inst = { ...coTenant, domain: 'mercury.example.com' };
|
|
445
|
+
const blank = P.webEnvBody(inst);
|
|
446
|
+
assert.match(blank, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=\\n`), 'blank id line by default');
|
|
447
|
+
const filled = P.webEnvBody(inst, { clientId: 'Iv1.abc123def456ghi7', clientSecret: 's'.repeat(40) });
|
|
448
|
+
assert.match(filled, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=Iv1\\.abc123def456ghi7`));
|
|
449
|
+
assert.match(filled, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_SECRET=s{40}`));
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await ta('convertManifestCode POSTs the unauthenticated conversion endpoint + maps the response', async () => {
|
|
453
|
+
let calledUrl = '', calledInit = null;
|
|
454
|
+
const fetchImpl = async (url, init) => {
|
|
455
|
+
calledUrl = url; calledInit = init;
|
|
456
|
+
return { ok: true, text: async () => JSON.stringify({ id: 42, slug: 'mercury-signin', client_id: 'Iv1.abcdef0123456789', client_secret: 'x'.repeat(40) }) };
|
|
457
|
+
};
|
|
458
|
+
const creds = await P.convertManifestCode('THECODE', { fetchImpl });
|
|
459
|
+
assert.equal(calledUrl, 'https://api.github.com/app-manifests/THECODE/conversions');
|
|
460
|
+
assert.equal(calledInit.method, 'POST');
|
|
461
|
+
assert.ok(calledInit.headers['User-Agent'], 'stamps a User-Agent (GitHub requires one)');
|
|
462
|
+
assert.ok(!('Authorization' in calledInit.headers), 'unauthenticated — the code IS the auth');
|
|
463
|
+
assert.deepEqual(creds, { clientId: 'Iv1.abcdef0123456789', clientSecret: 'x'.repeat(40), appId: 42, appSlug: 'mercury-signin' });
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await ta('convertManifestCode throws on a non-2xx or a creds-less response', async () => {
|
|
467
|
+
const bad = async () => ({ ok: false, text: async () => JSON.stringify({ message: 'code expired' }) });
|
|
468
|
+
await assert.rejects(P.convertManifestCode('X', { fetchImpl: bad }), /code expired/);
|
|
469
|
+
const empty = async () => ({ ok: true, text: async () => JSON.stringify({ id: 1 }) });
|
|
470
|
+
await assert.rejects(P.convertManifestCode('X', { fetchImpl: empty }), /no client_id/);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
await ta('placeManifestCreds (--apply): converts, rewrites web.env with the creds, restarts, verifies /healthz', async () => {
|
|
474
|
+
const inst = { ...coTenant, hosting_shape: 'co-tenant', domain: 'mercury.example.com' };
|
|
475
|
+
const writes = []; const cmds = [];
|
|
476
|
+
const deps = {
|
|
477
|
+
apply: true, privileged: false, log: () => {},
|
|
478
|
+
convertManifestCode: async () => ({ clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 9, appSlug: 'm-signin' }),
|
|
479
|
+
exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: '{"ok":true,"auth_configured":true}' }; },
|
|
480
|
+
writeFile: (p, body) => { writes.push({ p, body }); return { ok: true }; },
|
|
481
|
+
};
|
|
482
|
+
const r = await P.placeManifestCreds(inst, { github_code: 'CODE', attempts: 1 }, deps);
|
|
483
|
+
assert.equal(r.ok, true);
|
|
484
|
+
assert.equal(r.appSlug, 'm-signin');
|
|
485
|
+
// web.env rewritten WITH the creds
|
|
486
|
+
const env = writes.find((w) => /web\.env$/.test(w.p));
|
|
487
|
+
assert.ok(env, 'web.env was written');
|
|
488
|
+
assert.match(env.body, /_GITHUB_CLIENT_SECRET=z{40}/);
|
|
489
|
+
// the unit was restarted, and /healthz was probed
|
|
490
|
+
assert.ok(cmds.some((c) => /systemctl restart mercury/.test(c)), 'restarts the unit');
|
|
491
|
+
assert.ok(cmds.some((c) => /\/healthz/.test(c)), 'probes healthz');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
await ta('placeManifestCreds rejects a dedicated droplet (no post-hoc exec path) and creds-less rows', async () => {
|
|
495
|
+
const deps = { apply: true, log: () => {}, exec: () => ({ ok: true }), writeFile: () => ({}) };
|
|
496
|
+
const ded = await P.placeManifestCreds({ ...coTenant, hosting_shape: 'dedicated', domain: 'd.example.com' }, { github_code: 'C' }, deps);
|
|
497
|
+
assert.equal(ded.ok, false); assert.match(ded.error, /dedicated/);
|
|
498
|
+
const noCode = await P.placeManifestCreds({ ...coTenant, domain: 'm.example.com' }, {}, deps);
|
|
499
|
+
assert.equal(noCode.ok, false); assert.match(noCode.error, /no github_code/);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
await ta('placeManifestCreds fails when /healthz never reports auth_configured (never claims success)', async () => {
|
|
503
|
+
const inst = { ...coTenant, domain: 'mercury.example.com' };
|
|
504
|
+
const deps = {
|
|
505
|
+
apply: true, privileged: false, log: () => {},
|
|
506
|
+
convertManifestCode: async () => ({ clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 1, appSlug: 's' }),
|
|
507
|
+
exec: () => ({ ok: true, stdout: '{"ok":true,"auth_configured":false}' }),
|
|
508
|
+
writeFile: () => ({ ok: true }),
|
|
509
|
+
};
|
|
510
|
+
const r = await P.placeManifestCreds(inst, { github_code: 'C', attempts: 1 }, deps);
|
|
511
|
+
assert.equal(r.ok, false);
|
|
512
|
+
assert.match(r.error, /auth_configured=false/);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await ta('drainOAuthManifests (--apply): claims a code_received row, places creds, resolves done', async () => {
|
|
516
|
+
let claims = 0; const resolved = []; const events = [];
|
|
517
|
+
const deps = {
|
|
518
|
+
apply: true, privileged: false, log: () => {},
|
|
519
|
+
db: {}, exec: () => ({ ok: true, stdout: '{"auth_configured":true}' }), writeFile: () => ({ ok: true }),
|
|
520
|
+
convertManifestCode: async () => ({ clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 3, appSlug: 'app-3' }),
|
|
521
|
+
provisioning: {
|
|
522
|
+
claimNextOAuthManifest: async () => (claims++ === 0 ? { id: 11, instance_id: 1, github_code: 'CODE', attempts: 1 } : null),
|
|
523
|
+
getInstanceById: async () => ({ ...coTenant, hosting_shape: 'co-tenant', domain: 'mercury.example.com' }),
|
|
524
|
+
resolveOAuthManifest: async (_db, id, status, patch) => { resolved.push({ id, status, patch }); },
|
|
525
|
+
recordEvent: async (_db, e) => { events.push(e.event); },
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
const r = await P.drainOAuthManifests(deps);
|
|
529
|
+
assert.deepEqual(r, { converted: 1, errors: 0 });
|
|
530
|
+
assert.equal(resolved.length, 1);
|
|
531
|
+
assert.equal(resolved[0].status, 'done');
|
|
532
|
+
assert.equal(resolved[0].patch.app_slug, 'app-3');
|
|
533
|
+
assert.ok(events.includes('oauth-manifest-done'));
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
console.log('\nprivileged mode (PROVISION_SUDO — task 2066):');
|
|
537
|
+
|
|
538
|
+
t('dbCreateCmd: privileged runs createdb as the postgres role; default is bare (peer auth)', () => {
|
|
539
|
+
assert.equal(P.dbCreateCmd(coTenant), 'createdb mercury');
|
|
540
|
+
assert.equal(P.dbCreateCmd(coTenant, { privileged: false }), 'createdb mercury');
|
|
541
|
+
assert.equal(P.dbCreateCmd(coTenant, { privileged: true }), 'sudo -u postgres createdb mercury');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
t('migrateCmd: privileged runs migrate as postgres via env+bash (survives sudo env reset); default unchanged', () => {
|
|
545
|
+
assert.equal(P.migrateCmd(coTenant), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 ./scripts/migrate.sh');
|
|
546
|
+
const priv = P.migrateCmd(coTenant, { privileged: true });
|
|
547
|
+
assert.equal(priv, 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash ./scripts/migrate.sh');
|
|
548
|
+
// A bare `sudo -u postgres VAR=val ./script` would have sudo treat VAR=val as a
|
|
549
|
+
// command — the env MUST ride `env …` and the script run via `bash …`.
|
|
550
|
+
assert.doesNotMatch(priv, /sudo -u postgres PGDATABASE=/);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
t('standaloneMigrateCmd: privileged runs the instance-root migrate as postgres; default unchanged', () => {
|
|
554
|
+
const inst = { ...coTenant, hosting_shape: 'standalone' };
|
|
555
|
+
assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@cloudbongos/core/scripts/migrate.sh');
|
|
556
|
+
assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@cloudbongos/core/scripts/migrate.sh');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
t('makeWriteFile(privileged): a { sudo:true } write stages a temp file + `sudo install -D -m <mode>` (never writes /etc directly)', () => {
|
|
560
|
+
const cmds = [];
|
|
561
|
+
const exec = (cmd) => { cmds.push(cmd); return { ok: true }; };
|
|
562
|
+
const wf = P.makeWriteFile({ apply: true, privileged: true, exec, log: () => {} });
|
|
563
|
+
const dest = '/etc/mercury/web.env'; // NEVER actually written (install is the fake exec)
|
|
564
|
+
const r = wf(dest, 'PORT=3003\n', { mode: 0o600, sudo: true });
|
|
565
|
+
assert.equal(r.ok, true);
|
|
566
|
+
assert.equal(cmds.length, 1, 'the write is a single sudo install');
|
|
567
|
+
// temp file (lars-writable) → atomic root placement with -D (creates /etc/mercury/) + mode.
|
|
568
|
+
assert.match(cmds[0], /^sudo install -D -m 600 .+ \/etc\/mercury\/web\.env$/);
|
|
569
|
+
const tmp = cmds[0].split(' ')[5]; // sudo install -D -m 600 <tmp> <dest>
|
|
570
|
+
assert.ok(tmp.startsWith(tmpdir()), 'temp file staged under the OS tmpdir, not /etc');
|
|
571
|
+
assert.ok(!existsSync(tmp), 'temp file cleaned up after the install');
|
|
572
|
+
assert.ok(!existsSync(dest), 'the /etc destination is placed by install, never fs.writeFileSync');
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
t('makeWriteFile(privileged): a NON-sudo write (scaffold temp / .gitignore) still writes directly, no exec', () => {
|
|
576
|
+
const dir = mkdtempSync(join(tmpdir(), 'wf-'));
|
|
577
|
+
const exec = () => { throw new Error('exec must not be called for a direct write'); };
|
|
578
|
+
const wf = P.makeWriteFile({ apply: true, privileged: true, exec, log: () => {} });
|
|
579
|
+
const p = join(dir, 'sub', 'file.json'); // parent dir does not exist yet
|
|
580
|
+
wf(p, '{}\n'); // no { sudo:true } → direct fs write (mkdir -p)
|
|
581
|
+
assert.equal(readFileSync(p, 'utf8'), '{}\n');
|
|
582
|
+
rmSync(dir, { recursive: true, force: true });
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
t('makeWriteFile(non-privileged): a { sudo:true } write falls back to a direct fs write (knob off → no sudo)', () => {
|
|
586
|
+
const dir = mkdtempSync(join(tmpdir(), 'wf-'));
|
|
587
|
+
const exec = () => { throw new Error('no exec — non-privileged writes go direct'); };
|
|
588
|
+
const wf = P.makeWriteFile({ apply: true, privileged: false, exec, log: () => {} });
|
|
589
|
+
const p = join(dir, 'web.env');
|
|
590
|
+
wf(p, 'PORT=1\n', { mode: 0o600, sudo: true });
|
|
591
|
+
assert.equal(readFileSync(p, 'utf8'), 'PORT=1\n');
|
|
592
|
+
rmSync(dir, { recursive: true, force: true });
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// A recorder for the privileged path: captures each exec string + each writeFile's
|
|
596
|
+
// (path, opts), so we can assert the sudo shapes AND that the /etc writes opt into
|
|
597
|
+
// escalation. Mirrors recorder() but with privileged:true (and opts-capturing writes).
|
|
598
|
+
function privRecorder({ apply = false } = {}) {
|
|
599
|
+
const cmds = [], writes = [];
|
|
600
|
+
const deps = {
|
|
601
|
+
apply, privileged: true, log: () => {},
|
|
602
|
+
db: { query: async () => ({ rows: [] }) },
|
|
603
|
+
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
604
|
+
exec: (cmd) => { cmds.push(cmd); return { ok: true, stdout: '' }; },
|
|
605
|
+
writeFile: (p, _body, opts) => { writes.push({ p, opts: opts || {} }); return { ok: true }; },
|
|
606
|
+
doApi: null,
|
|
607
|
+
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
608
|
+
};
|
|
609
|
+
return { deps, cmds, writes };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
await ta('provisionInstance(co-tenant, privileged): createdb/migrate run as postgres, systemctl is sudo-prefixed, /etc writes request escalation', async () => {
|
|
613
|
+
const { deps, cmds, writes } = privRecorder();
|
|
614
|
+
await P.provisionInstance(coTenant, deps);
|
|
615
|
+
const joined = cmds.join('\n');
|
|
616
|
+
assert.match(joined, /sudo -u postgres createdb mercury/, 'createdb as postgres');
|
|
617
|
+
assert.match(joined, /sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash \.\/scripts\/migrate\.sh/, 'migrate as postgres');
|
|
618
|
+
assert.match(joined, /sudo systemctl daemon-reload/, 'daemon-reload sudo-prefixed');
|
|
619
|
+
assert.match(joined, /sudo systemctl enable --now mercury/, 'enable sudo-prefixed');
|
|
620
|
+
assert.match(joined, /sudo systemctl reload caddy/, 'reload caddy sudo-prefixed');
|
|
621
|
+
// the three /etc writes must opt into escalation (sudo:true) so makeWriteFile installs them.
|
|
622
|
+
for (const suffix of ['/mercury/web.env', '/mercury.service', '/mercury.caddy']) {
|
|
623
|
+
const w = writes.find((x) => x.p.endsWith(suffix));
|
|
624
|
+
assert.ok(w, `wrote ${suffix}`);
|
|
625
|
+
assert.equal(w.opts.sudo, true, `${suffix} requests sudo escalation`);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
await ta('provisionInstance(co-tenant, default): NO sudo anywhere (bare commands — co-tenant/root/test unaffected)', async () => {
|
|
630
|
+
const { deps, cmds } = recorder(); // recorder() sets no privileged flag → falsy
|
|
631
|
+
await P.provisionInstance(coTenant, deps);
|
|
632
|
+
assert.ok(!cmds.some((c) => /(^|\s)sudo(\s|$)/.test(c)), 'no sudo in the non-privileged composition');
|
|
633
|
+
assert.ok(cmds.includes('createdb mercury'), 'bare createdb (peer auth)');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
await ta('teardownInstance(privileged): systemctl + rm-under-/etc are sudo-prefixed', async () => {
|
|
637
|
+
const { deps, cmds } = privRecorder();
|
|
638
|
+
await P.teardownInstance(coTenant, deps);
|
|
639
|
+
const joined = cmds.join('\n');
|
|
640
|
+
assert.match(joined, /sudo systemctl disable --now mercury/);
|
|
641
|
+
assert.match(joined, /sudo rm -f \/etc\/systemd\/system\/mercury\.service/);
|
|
642
|
+
assert.match(joined, /sudo systemctl daemon-reload/);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
await ta('teardownInstance(default): NO sudo (bare, unchanged)', async () => {
|
|
646
|
+
const { deps, cmds } = recorder();
|
|
647
|
+
await P.teardownInstance(coTenant, deps);
|
|
648
|
+
assert.ok(!cmds.some((c) => /(^|\s)sudo(\s|$)/.test(c)), 'no sudo when the knob is off');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// ── remote-exec over SSH (task 2065, ADR 0130) ─────────────────────────────
|
|
652
|
+
console.log('\nremote orchestration over SSH (task 2065):');
|
|
653
|
+
|
|
654
|
+
t('makeExec(remoteHost): a step is ssh-wrapped and cd\'s to the remote checkout (dry-run)', () => {
|
|
655
|
+
const logs = [];
|
|
656
|
+
const run = P.makeExec({ apply: false, log: (m) => logs.push(m), remoteHost: 'lars@10.0.0.9', remoteCwd: '/home/lars/amazonprimea', sshOpts: '-o BatchMode=yes' });
|
|
657
|
+
run('createdb mercury');
|
|
658
|
+
const line = logs.join('\n');
|
|
659
|
+
assert.match(line, /\[ssh lars@10\.0\.0\.9\]/, 'shows the ssh target');
|
|
660
|
+
assert.match(line, /cd \/home\/lars\/amazonprimea && createdb mercury/, 'cd to the remote checkout, then the command');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
t('makeExec(remoteHost): explicit cwd overrides the default; env is composed INTO the remote script (crosses the ssh hop)', () => {
|
|
664
|
+
const logs = [];
|
|
665
|
+
const run = P.makeExec({ apply: false, log: (m) => logs.push(m), remoteHost: 'host', remoteCwd: '/default' });
|
|
666
|
+
run('./scripts/migrate.sh', { cwd: '/srv/x', env: { PGDATABASE: 'x' } });
|
|
667
|
+
const line = logs.join('\n');
|
|
668
|
+
assert.match(line, /cd \/srv\/x && PGDATABASE=x \.\/scripts\/migrate\.sh/, 'explicit cwd + env prefix inside the remote script');
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
t('makeExec(no remoteHost): a step runs locally, never ssh-wrapped (default, unchanged)', () => {
|
|
672
|
+
const logs = [];
|
|
673
|
+
const run = P.makeExec({ apply: false, log: (m) => logs.push(m) });
|
|
674
|
+
run('createdb mercury');
|
|
675
|
+
const line = logs.join('\n');
|
|
676
|
+
assert.doesNotMatch(line, /\[ssh/, 'no ssh wrapping when remoteHost is unset');
|
|
677
|
+
assert.match(line, /would run: createdb mercury/);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
t('makeWriteFile(remoteHost): a sudo write base64-pipes the body to `sudo tee` on the box via exec (no raw secret, no local fs write)', () => {
|
|
681
|
+
const cmds = [];
|
|
682
|
+
const exec = (cmd) => { cmds.push(cmd); return { ok: true }; };
|
|
683
|
+
const wf = P.makeWriteFile({ apply: true, privileged: true, exec, remoteHost: 'host', log: () => {} });
|
|
684
|
+
wf('/etc/mercury/web.env', 'SECRET=hunter2\n', { mode: 0o600, sudo: true });
|
|
685
|
+
assert.equal(cmds.length, 1, 'exactly one remote command (no local fs write)');
|
|
686
|
+
const c = cmds[0];
|
|
687
|
+
assert.match(c, /sudo mkdir -p \/etc\/mercury/, 'creates the parent dir as root');
|
|
688
|
+
assert.match(c, /base64 -d \| sudo tee \/etc\/mercury\/web\.env/, 'decodes + tees into place as root');
|
|
689
|
+
assert.match(c, /sudo chmod 600 \/etc\/mercury\/web\.env/, 'applies the mode as root');
|
|
690
|
+
assert.doesNotMatch(c, /hunter2/, 'the raw secret is base64-encoded, never inlined');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
await ta('provisionInstance(co-tenant, remote): box steps use the box `exec`, but the DNS hook uses `controlExec` — the token step never crosses to the box', async () => {
|
|
694
|
+
const savedIp = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
|
|
695
|
+
P.CONFIG.publicIp = '203.0.113.5'; // the CO-HOSTING box's IP (task 2065: point PROVISION_PUBLIC_IP at where the co-tenant runs)
|
|
696
|
+
P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
697
|
+
try {
|
|
698
|
+
const box = [], control = [];
|
|
699
|
+
const deps = {
|
|
700
|
+
apply: false, log: () => {},
|
|
701
|
+
db: { query: async () => ({ rows: [] }) },
|
|
702
|
+
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
703
|
+
exec: (cmd) => { box.push(cmd); return { ok: true, stdout: '' }; },
|
|
704
|
+
controlExec: (cmd) => { control.push(cmd); return { ok: true, stdout: '' }; },
|
|
705
|
+
writeFile: () => ({ ok: true }),
|
|
706
|
+
controlWriteFile: () => ({ ok: true }),
|
|
707
|
+
doApi: null,
|
|
708
|
+
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
709
|
+
};
|
|
710
|
+
await P.provisionInstance({ ...coTenant, domain: 'mercury.example.com' }, deps);
|
|
711
|
+
// box-mutating steps → the box executor (which, in prod, ssh's to the co-hosting box)
|
|
712
|
+
assert.ok(box.some((c) => /createdb/.test(c)), 'createdb via the box exec');
|
|
713
|
+
assert.ok(box.some((c) => /migrate\.sh/.test(c)), 'migrate via the box exec');
|
|
714
|
+
assert.ok(box.some((c) => /systemctl enable --now/.test(c)), 'systemctl via the box exec');
|
|
715
|
+
assert.ok(box.some((c) => /healthz/.test(c)), 'healthz via the box exec');
|
|
716
|
+
// the DNS hook (needs the Cloudflare token) → the control executor, NEVER the box
|
|
717
|
+
assert.ok(control.includes('/opt/infra/box-dns-cloudflare.sh'), 'DNS hook ran on the control executor');
|
|
718
|
+
assert.ok(!box.includes('/opt/infra/box-dns-cloudflare.sh'), 'DNS hook NEVER on the box exec — the token stays on the control plane');
|
|
719
|
+
} finally { P.CONFIG.publicIp = savedIp; P.CONFIG.dnsHook = savedHook; }
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
await ta('provisionInstance(standalone, remote): EVERY box step runs control-plane-local — nothing crosses to the co-hosting box; the A record points at the control plane (task 2074)', async () => {
|
|
723
|
+
const savedPub = P.CONFIG.publicIp, savedCp = P.CONFIG.controlPlaneIp, savedHook = P.CONFIG.dnsHook, savedRemote = P.CONFIG.remoteHost;
|
|
724
|
+
P.CONFIG.remoteHost = 'cohost'; // remote-exec mode: co-tenant steps WOULD go to the co-hosting box
|
|
725
|
+
P.CONFIG.publicIp = '104.236.254.243'; // the co-hosting box's IP (where co-tenants run)
|
|
726
|
+
P.CONFIG.controlPlaneIp = '161.35.132.201'; // the control plane's OWN IP (where a standalone runs)
|
|
727
|
+
P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
728
|
+
try {
|
|
729
|
+
const box = [], control = [], boxWrites = [], controlWrites = [], dnsEnvs = [];
|
|
730
|
+
const deps = {
|
|
731
|
+
apply: false, log: () => {},
|
|
732
|
+
db: { query: async () => ({ rows: [] }) },
|
|
733
|
+
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
734
|
+
exec: (cmd) => { box.push(cmd); return { ok: true, stdout: '' }; }, // the REMOTE (co-hosting box) executor
|
|
735
|
+
controlExec: (cmd, opts) => { control.push(cmd); if (opts && opts.env) dnsEnvs.push({ cmd, env: opts.env }); return { ok: true, stdout: '' }; },
|
|
736
|
+
writeFile: (p) => { boxWrites.push(p); return { ok: true }; }, // the REMOTE writer
|
|
737
|
+
controlWriteFile: (p) => { controlWrites.push(p); return { ok: true }; }, // the control-plane writer
|
|
738
|
+
doApi: null,
|
|
739
|
+
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
740
|
+
};
|
|
741
|
+
await P.provisionInstance({ ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury', domain: 'mercury.cloudbongos.com' }, deps);
|
|
742
|
+
// The bug (task 2074): these landed on the REMOTE box, which never had the scaffolded
|
|
743
|
+
// dir. They must ALL be on the control-plane executor now.
|
|
744
|
+
for (const re of [/git pull --ff-only/, /npm ci/, /createdb mercury/, /migrate\.sh/, /systemctl enable --now/, /systemctl reload caddy/, /healthz/]) {
|
|
745
|
+
assert.ok(control.some((c) => re.test(c)), `${re} runs on the control-plane executor`);
|
|
746
|
+
assert.ok(!box.some((c) => re.test(c)), `${re} does NOT cross to the co-hosting box`);
|
|
747
|
+
}
|
|
748
|
+
// The scaffold leg was already control-local; confirm it stayed there.
|
|
749
|
+
assert.ok(control.some((c) => /git clone /.test(c)) && control.some((c) => /push -u origin main/.test(c)), 'scaffold clone+push stay control-local');
|
|
750
|
+
// NOTHING crosses to the remote box for a standalone — the whole point of the fix.
|
|
751
|
+
assert.equal(box.length, 0, 'zero remote (co-hosting box) commands for a standalone');
|
|
752
|
+
assert.equal(boxWrites.length, 0, 'zero remote writes for a standalone');
|
|
753
|
+
// writes (web.env, systemd unit, caddy snippet) all go to the control-plane writer.
|
|
754
|
+
assert.ok(controlWrites.some((w) => /\/mercury\/web\.env$/.test(w)), 'web.env written control-local');
|
|
755
|
+
assert.ok(controlWrites.some((w) => /\/mercury\.service$/.test(w)), 'systemd unit written control-local');
|
|
756
|
+
assert.ok(controlWrites.some((w) => /\/mercury\.caddy$/.test(w)), 'caddy snippet written control-local');
|
|
757
|
+
// DNS: the A record points at the CONTROL PLANE's IP (not the co-hosting box's publicIp).
|
|
758
|
+
const dns = dnsEnvs.find((e) => e.cmd === '/opt/infra/box-dns-cloudflare.sh');
|
|
759
|
+
assert.ok(dns, 'DNS hook ran on the control executor');
|
|
760
|
+
assert.equal(dns.env.BOX_IP, '161.35.132.201', 'A record → the control-plane IP, NOT the co-hosting box');
|
|
761
|
+
assert.equal(dns.env.BOX_PROXIED, '1', 'standalone A record is PROXIED (behind the wildcard origin cert; task 2077) — publicly-trusted cert + hidden IP');
|
|
762
|
+
} finally { P.CONFIG.publicIp = savedPub; P.CONFIG.controlPlaneIp = savedCp; P.CONFIG.dnsHook = savedHook; P.CONFIG.remoteHost = savedRemote; }
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
await ta('provisionInstance(standalone, remote, --apply): FAILS LOUD when the control-plane IP is unset (never silently points the domain at the co-hosting box)', async () => {
|
|
766
|
+
const savedPub = P.CONFIG.publicIp, savedCp = P.CONFIG.controlPlaneIp, savedHook = P.CONFIG.dnsHook, savedRemote = P.CONFIG.remoteHost;
|
|
767
|
+
P.CONFIG.remoteHost = 'cohost'; // split-box mode
|
|
768
|
+
P.CONFIG.publicIp = '104.236.254.243'; // co-hosting box — must NOT be used for a standalone A record
|
|
769
|
+
P.CONFIG.controlPlaneIp = null; // operator forgot to set it
|
|
770
|
+
P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
771
|
+
try {
|
|
772
|
+
const deps = {
|
|
773
|
+
apply: true, log: () => {},
|
|
774
|
+
db: { query: async () => ({ rows: [] }) },
|
|
775
|
+
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
776
|
+
exec: () => ({ ok: true, stdout: '' }), controlExec: () => ({ ok: true, stdout: '' }),
|
|
777
|
+
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }), doApi: null,
|
|
778
|
+
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
779
|
+
};
|
|
780
|
+
await assert.rejects(
|
|
781
|
+
() => P.provisionInstance({ ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury', domain: 'mercury.cloudbongos.com' }, deps),
|
|
782
|
+
/PROVISION_CONTROL_PLANE_IP/,
|
|
783
|
+
'must demand the control-plane IP, not fall back to the co-hosting box',
|
|
784
|
+
);
|
|
785
|
+
} finally { P.CONFIG.publicIp = savedPub; P.CONFIG.controlPlaneIp = savedCp; P.CONFIG.dnsHook = savedHook; P.CONFIG.remoteHost = savedRemote; }
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
await ta('teardownInstance(standalone, remote): tears down on the CONTROL PLANE — the box it ran on — not the co-hosting box (task 2074)', async () => {
|
|
789
|
+
const box = [], control = [];
|
|
790
|
+
const deps = {
|
|
791
|
+
apply: false, log: () => {},
|
|
792
|
+
db: { query: async () => ({ rows: [] }) },
|
|
793
|
+
provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
|
|
794
|
+
exec: (cmd) => { box.push(cmd); return { ok: true }; },
|
|
795
|
+
controlExec: (cmd) => { control.push(cmd); return { ok: true }; },
|
|
796
|
+
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }), doApi: null,
|
|
797
|
+
};
|
|
798
|
+
await P.teardownInstance({ ...coTenant, hosting_shape: 'standalone' }, deps);
|
|
799
|
+
assert.ok(control.some((c) => /systemctl disable --now mercury/.test(c)), 'disable runs on the control plane');
|
|
800
|
+
assert.ok(control.some((c) => /rm -f .*mercury\.service/.test(c)), 'unit removed on the control plane');
|
|
801
|
+
assert.equal(box.length, 0, 'nothing crosses to the co-hosting box for a standalone teardown');
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
console.log(`\nprovision: ${passed} passed, ${failed} failed`);
|
|
805
|
+
process.exit(failed ? 1 : 0);
|