@bongos/core 1.18.18 → 1.19.2
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 +515 -195
- package/.claude/skills/builder-box/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +5 -5
- package/.claude/skills/scan-before-install/SKILL.md +165 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +63 -41
- package/clients/bongos-client/index.cjs +63 -41
- package/clients/bongos-client/index.d.ts +78 -44
- package/clients/bongos-client/index.mjs +63 -41
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
- package/docs/adr/0156-user-set-display-name-wins.md +62 -0
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
- package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
- package/docs/adr/README.md +8 -0
- package/docs/api/openapi.json +1209 -233
- package/docs/api-reference.md +61 -46
- package/docs/architecture.md +1 -1
- package/docs/canonical-permissions.md +61 -16
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +4 -1
- package/docs/onboarding/diagrams/assertions.json +3 -3
- package/docs/recipes/core-release-pipeline.md +179 -0
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/parallel-goal-waves.md +1 -3
- package/docs/recipes/private-npm-distribution.md +2 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/migrations/core_197_display_name_source.sql +56 -0
- package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
- package/modules/autonomy/routes/autonomy.js +1 -1
- package/modules/builder-settings/db.js +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
- package/modules/dev-box/routes/box.js +11 -9
- package/modules/discord/auth.js +3 -1
- package/modules/discord/routes/discord.js +8 -2
- package/modules/discord/ship-broadcast.js +48 -1
- package/modules/governance/catalog.js +70 -20
- package/modules/governance/db.js +102 -0
- package/modules/governance/migrations/governance_006_version_create.sql +38 -0
- package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
- package/modules/governance/routes/governance.js +235 -6
- package/modules/grading/grader-workers/index.js +18 -9
- package/modules/hall-ui/public/gate.js +6 -2
- package/modules/hall-ui/public/goals-page.js +6 -2
- package/modules/hall-ui/public/governance.css +100 -0
- package/modules/hall-ui/public/governance.html +77 -0
- package/modules/hall-ui/public/governance.js +353 -0
- package/modules/hall-ui/public/harbor.js +14 -8
- package/modules/hall-ui/public/instances-panel.js +3 -1
- package/modules/hall-ui/public/new-project-wizard.js +6 -0
- package/modules/hall-ui/public/people.html +51 -0
- package/modules/hall-ui/public/people.js +100 -0
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +9 -3
- package/modules/hall-ui/public/sessions.js +4 -2
- package/modules/hall-ui/public/shell.js +28 -7
- package/modules/hall-ui/public/task-detail.js +6 -3
- package/modules/hall-ui/public/watch.js +17 -9
- package/modules/ideas/blockers.js +44 -7
- package/modules/ideas/routes/blockers.js +12 -7
- package/modules/ideas/routes/inbox.js +2 -2
- package/modules/lifecycle/db.js +84 -17
- package/modules/lifecycle/github-push.js +37 -10
- package/modules/lifecycle/publish-reconciler.js +128 -31
- package/modules/lifecycle/routes/claims.js +10 -4
- package/modules/lifecycle/routes/dependencies.js +2 -2
- package/modules/lifecycle/routes/done-when.js +4 -4
- package/modules/lifecycle/routes/gate-approvals.js +9 -3
- package/modules/lifecycle/routes/goals.js +5 -5
- package/modules/lifecycle/routes/tasks.js +16 -16
- package/modules/lifecycle/routes/versions.js +13 -2
- package/modules/lifecycle/ship-card.js +38 -7
- package/modules/memory/routes/memory.js +2 -2
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/routes/projects.js +10 -6
- package/modules/platform-identity/routes/scouting.js +2 -2
- package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
- package/modules/provisioning/provisioning.js +24 -6
- package/modules/provisioning/routes/provisioning.js +24 -6
- package/modules/security/artifact-scan/aggregate.js +133 -0
- package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
- package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
- package/modules/security/artifact-scan/detectors/credentials.js +112 -0
- package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
- package/modules/security/artifact-scan/detectors/hooks.js +116 -0
- package/modules/security/artifact-scan/detectors/injection.js +118 -0
- package/modules/security/artifact-scan/detectors/mcp.js +102 -0
- package/modules/security/artifact-scan/detectors/network.js +67 -0
- package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
- package/modules/security/artifact-scan/detectors/permissions.js +91 -0
- package/modules/security/artifact-scan/detectors/shared.js +219 -0
- package/modules/security/artifact-scan/fence-local.js +99 -0
- package/modules/security/artifact-scan/fetch.js +330 -0
- package/modules/security/artifact-scan/floor.js +319 -0
- package/modules/security/artifact-scan/limits.js +66 -0
- package/modules/security/artifact-scan/manifest.js +203 -0
- package/modules/security/artifact-scan/mirror.js +158 -0
- package/modules/security/artifact-scan/quarantine.js +149 -0
- package/modules/security/artifact-scan/rebuild-spec.js +120 -0
- package/modules/security/artifact-scan/report.js +155 -0
- package/modules/security/artifact-scan/sanitize.js +317 -0
- package/modules/security/migrations/security_001_tool_scans.sql +145 -0
- package/modules/security/module.json +2 -2
- package/modules/security/routes/reports.js +11 -7
- package/modules/security/routes/scans.js +115 -0
- package/modules/security/scan-store.js +214 -0
- package/modules/security/tests/aggregate.mjs +283 -0
- package/modules/security/tests/artifact_scan.mjs +591 -0
- package/modules/security/tests/floor.mjs +549 -0
- package/modules/security/tests/mirror.mjs +350 -0
- package/modules/security/tests/sanitize_rebuild.mjs +499 -0
- package/modules/security/tests/scan_store.mjs +250 -0
- package/modules/security/tests/tool_scans_migration.mjs +116 -0
- package/modules/sessions/routes/sessions.js +6 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +13 -3
- package/scripts/gds/abandon.js +1 -1
- package/scripts/gds/audit-authorship.sh +6 -0
- package/scripts/gds/box.js +1 -1
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/cli-lib.js +16 -4
- package/scripts/gds/gating.js +28 -13
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/goal.js +234 -0
- package/scripts/gds/init.js +70 -15
- package/scripts/gds/main-audit.js +80 -11
- package/scripts/gds/mirror-redact.js +6 -0
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/provision.js +230 -94
- package/scripts/gds/run-unit-tests.js +9 -1
- package/scripts/gds/sandbox-stage.js +66 -9
- package/scripts/gds/search-ingest-md.js +1 -0
- package/scripts/gds/sequence.js +67 -8
- package/scripts/gds/ship.js +183 -50
- package/scripts/hall-preview/server.js +1 -1
- package/scripts/migrate.sh +99 -14
- package/src/bongos/auth.js +129 -27
- package/src/bongos/db-kernel.js +66 -5
- package/src/bongos/db.js +6 -7
- package/src/bongos/github-repos.js +19 -7
- package/src/bongos/goal-scope-check.js +1 -1
- package/src/bongos/module-scope-map.js +3 -1
- package/src/bongos/permission-path-check.js +1 -1
- package/src/bongos/route-rank-check.js +73 -30
- package/src/bongos/routes/audit-log.js +4 -2
- package/src/bongos/routes/auth.js +8 -4
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +23 -11
- package/src/bongos/routes/github.js +15 -4
- package/src/bongos/routes/llm-cache.js +5 -2
- package/src/bongos/routes/modules.js +9 -5
- package/src/bongos/routes/security.js +21 -12
- package/src/bongos/serve-internal.js +22 -7
- package/src/module-api.js +86 -1
- package/src/platform-server.js +14 -0
- package/tests/api_client.mjs +18 -2
- package/tests/api_path_404.mjs +3 -0
- package/tests/art_stage.mjs +2 -1
- package/tests/atlas.mjs +32 -4
- package/tests/auth_admission_gate.mjs +17 -5
- package/tests/auth_error_brand.mjs +41 -21
- package/tests/auth_resolve_failure.mjs +155 -0
- package/tests/blocker_link.mjs +88 -0
- package/tests/bongos_downloads.mjs +2 -1
- package/tests/box_dns_repoint.mjs +2 -0
- package/tests/box_ship_permissions.mjs +2 -0
- package/tests/box_terminal_ssrf.mjs +23 -17
- package/tests/claim_error_surface.mjs +23 -0
- package/tests/claude_materialize.mjs +4 -0
- package/tests/cli_box_scope.mjs +40 -0
- package/tests/cloud_bongos_pack.mjs +34 -4
- package/tests/conductor.mjs +2 -1
- package/tests/criterion_authoring_gate.mjs +28 -5
- package/tests/currency_label.mjs +10 -1
- package/tests/deploy_instance.mjs +2 -0
- package/tests/devbox_downloads.mjs +2 -1
- package/tests/display_name.mjs +95 -6
- package/tests/eviction_policy.mjs +2 -1
- package/tests/file_map.mjs +10 -0
- package/tests/first_admin_bootstrap.mjs +10 -2
- package/tests/first_run_engine_check.mjs +2 -1
- package/tests/fitness.mjs +26 -6
- package/tests/gate_approvals.mjs +15 -2
- package/tests/github_push_land_proof.mjs +50 -4
- package/tests/github_repos_helpers.mjs +30 -5
- package/tests/github_token_store.mjs +23 -0
- package/tests/goal_abuse_matrix.mjs +13 -3
- package/tests/goal_cli.mjs +89 -0
- package/tests/governance_abuse_matrix.mjs +386 -0
- package/tests/governance_fold.mjs +156 -0
- package/tests/governance_fold_bench.mjs +50 -0
- package/tests/governance_live_proof.mjs +201 -0
- package/tests/governance_page_gates.mjs +21 -9
- package/tests/governance_parity.mjs +11 -2
- package/tests/governance_require_permission.mjs +125 -1
- package/tests/governance_routes.mjs +334 -0
- package/tests/grader.mjs +7 -3
- package/tests/hall_nav.mjs +14 -4
- package/tests/helpers.mjs +73 -0
- package/tests/hook_core_resolve.mjs +2 -1
- package/tests/host_topology_skips.mjs +97 -0
- package/tests/infra_exec_paths.mjs +14 -4
- package/tests/init.mjs +128 -0
- package/tests/known_failing_ratchet.mjs +89 -0
- package/tests/lifecycle-card.mjs +2 -1
- package/tests/main_audit.mjs +72 -1
- package/tests/memory_isolation.mjs +56 -9
- package/tests/migrate_root_resolution.mjs +124 -1
- package/tests/mirror_redact.mjs +19 -0
- package/tests/module_api.mjs +3 -1
- package/tests/module_loader.mjs +4 -0
- package/tests/modules.mjs +35 -7
- package/tests/modules_route.mjs +29 -5
- package/tests/onboarding_observability.mjs +49 -11
- package/tests/onboarding_route_signals.mjs +1 -0
- package/tests/one_active_claim_per_session.mjs +80 -0
- package/tests/pre_compact.mjs +2 -1
- package/tests/preview_server.mjs +2 -1
- package/tests/profile_route.mjs +1 -0
- package/tests/provision.mjs +170 -0
- package/tests/provisioning_onboard_mode.mjs +180 -0
- package/tests/publish_manifest.mjs +16 -3
- package/tests/publish_reconciler.mjs +181 -10
- package/tests/rank_tier_single_source.mjs +205 -0
- package/tests/recall_hints.mjs +2 -1
- package/tests/repo_map.mjs +4 -0
- package/tests/route_permission_check.mjs +14 -5
- package/tests/rubric_fixtures.mjs +3 -0
- package/tests/run_routine_two_root.mjs +2 -0
- package/tests/sandbox_stage.mjs +69 -7
- package/tests/search_ingest.mjs +7 -0
- package/tests/security_docs.mjs +28 -6
- package/tests/sequence.mjs +64 -0
- package/tests/session-cards.mjs +2 -1
- package/tests/session_start_freshness.mjs +2 -1
- package/tests/ship_broadcast_server.mjs +63 -1
- package/tests/ship_card.mjs +66 -0
- package/tests/ship_ci_deploy.mjs +66 -1
- package/tests/ship_smoke_gate.mjs +76 -0
- package/tests/versions_route.mjs +15 -0
- package/tests/workflow_exec_paths.mjs +16 -4
- package/tests/worktree_guard.mjs +2 -1
package/scripts/gds/provision.js
CHANGED
|
@@ -399,10 +399,55 @@ function installedCoreTarball({ instanceRoot, fsImpl = fs } = {}) {
|
|
|
399
399
|
return null;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
402
|
+
// The onboard mode a provisioning row carries (migration provisioning_004, task 1002562).
|
|
403
|
+
// 'adopt' means target_ref is a repo the owner ALREADY has, so the scaffold must LAYER
|
|
404
|
+
// onto its history. Anything else — including a row written before the column existed, or
|
|
405
|
+
// an unrecognized value — resolves to 'greenfield', deliberately the SAFE direction:
|
|
406
|
+
// greenfield pushes a fresh tree and (carrying no --force) is REJECTED by a repo with
|
|
407
|
+
// history, whereas wrongly choosing 'adopt' would start rewriting a repo nobody declared.
|
|
408
|
+
function onboardMode(inst) {
|
|
409
|
+
return String((inst && inst.onboard_mode) || '') === 'adopt' ? 'adopt' : 'greenfield';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Has the scaffold already completed in `root`? The marker is MODE-DEPENDENT, and reusing
|
|
413
|
+
// the greenfield one for adopt is a real bug: greenfield's marker is "package.json exists"
|
|
414
|
+
// (a fresh empty repo has none until we scaffold it), but an adopted repo is an existing
|
|
415
|
+
// project that almost certainly HAS a package.json already — so the greenfield check would
|
|
416
|
+
// report "already scaffolded" on the very first run and the instance would never become
|
|
417
|
+
// bootable. For adopt the honest marker is the thing the layering itself contributes: a
|
|
418
|
+
// @bongos/core dependency (both scope aliases the loader accepts, matching
|
|
419
|
+
// installedCoreTarball). fs injectable for tests; read-only, so dry-run safe.
|
|
420
|
+
function alreadyScaffolded(root, mode, { fsImpl = fs } = {}) {
|
|
421
|
+
const pkgPath = path.join(root, 'package.json');
|
|
422
|
+
if (!fsImpl.existsSync(pkgPath)) return false;
|
|
423
|
+
if (mode !== 'adopt') return true;
|
|
424
|
+
let pkg;
|
|
425
|
+
try { pkg = JSON.parse(fsImpl.readFileSync(pkgPath, 'utf8')); }
|
|
426
|
+
catch { return false; } // unparseable → let the adopt layer report it, don't skip
|
|
427
|
+
const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
|
|
428
|
+
return !!(deps['@bongos/core'] || deps['@cloudbongos/core']);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Materialize + push the instance's own repo. TWO paths, chosen by onboard_mode:
|
|
432
|
+
//
|
|
433
|
+
// greenfield — the target_ref repo is EMPTY (github-repos.createUserRepo makes it with
|
|
434
|
+
// auto_init:false), so there is nothing to clone: `git init` locally,
|
|
435
|
+
// materialize, and push the first commit at main.
|
|
436
|
+
// adopt — the target_ref repo is the owner's OWN existing project. CLONE it (so its
|
|
437
|
+
// history and files come along), LAYER the instance config additively via
|
|
438
|
+
// `bongos init --adopt --accept` (which never overwrites a pre-existing
|
|
439
|
+
// file), and push the layer commit as an ordinary fast-forward on top.
|
|
440
|
+
//
|
|
441
|
+
// Both end at a repo carrying config/ + .claude/ + a file:-pinned @bongos/core + lockfile,
|
|
442
|
+
// which is what makes it `npm ci --omit=dev`-installable by the rest of provisioning.
|
|
443
|
+
//
|
|
444
|
+
// NEITHER PATH EVER USES --force. That is a safety property, not an oversight: since ADR
|
|
445
|
+
// 0155 the credential here can write to every repo the owner owns, so a force-push would
|
|
446
|
+
// be a route to destroying a user's work. A rejected push is the correct, recoverable
|
|
447
|
+
// outcome. Asserted by tests.
|
|
448
|
+
//
|
|
449
|
+
// Idempotent (see alreadyScaffolded) and dry-run safe (every mutation goes through the
|
|
450
|
+
// injected exec/writeFile). ADR 0108 / 0121 / 0125.
|
|
406
451
|
async function scaffoldStandaloneRepo(inst, deps) {
|
|
407
452
|
// The scaffold BUILDS + pushes the instance's repo (git clone, package-core,
|
|
408
453
|
// `bongos init`, git push) — control-plane work that needs the packaging tools +
|
|
@@ -414,12 +459,12 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
414
459
|
const writeFile = deps.controlWriteFile || deps.writeFile;
|
|
415
460
|
const root = standaloneRoot(inst);
|
|
416
461
|
const repo = parseTargetRef(inst.target_ref);
|
|
462
|
+
const mode = onboardMode(inst);
|
|
417
463
|
if (!repo) {
|
|
418
464
|
throw new Error(`standalone instance ${inst.slug}: target_ref "${inst.target_ref || '(unset)'}" is not an owner/repo — set it (e.g. <owner>/${inst.slug}) so the scaffold knows which repo to clone + push.`);
|
|
419
465
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
log(` [scaffold] ${root} already scaffolded (package.json present) — skipping`);
|
|
466
|
+
if (alreadyScaffolded(root, mode)) {
|
|
467
|
+
log(` [scaffold] ${root} already scaffolded (mode=${mode}) — skipping`);
|
|
423
468
|
return { already: true };
|
|
424
469
|
}
|
|
425
470
|
const coreV = coreVersionSafe();
|
|
@@ -427,7 +472,9 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
427
472
|
throw new Error('cannot resolve the core version (src/module-api CORE_VERSION) — run the scaffold from the control-plane checkout.');
|
|
428
473
|
}
|
|
429
474
|
const remote = instanceRepoRemote(repo);
|
|
430
|
-
log(
|
|
475
|
+
log(mode === 'adopt'
|
|
476
|
+
? ` [scaffold] ${repo.owner}/${repo.name} → ${root} (ADOPT: clone → layer additively → pin core ${coreV} → commit → fast-forward push)`
|
|
477
|
+
: ` [scaffold] ${repo.owner}/${repo.name} → ${root} (clone → materialize → pin core ${coreV} → commit → push)`);
|
|
431
478
|
|
|
432
479
|
// Owner login = the founding firstAdmin. Best-effort; the spec falls back to the repo owner.
|
|
433
480
|
let ownerLogin = null;
|
|
@@ -438,79 +485,21 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
438
485
|
} catch { /* fall back to the repo owner */ }
|
|
439
486
|
}
|
|
440
487
|
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// Core-install path (ADR 0134, task 2090). DEFAULT = vendor a tarball built from THIS
|
|
458
|
-
// control-plane checkout (offline, proven — task 2053). OPT-IN = pull the PRIVATE
|
|
459
|
-
// @bongos/core from the npm registry (PROVISION_CORE_FROM_REGISTRY=1). The registry
|
|
460
|
-
// path needs NPM_TOKEN at `npm ci` time; if the flag is set but no token is present we
|
|
461
|
-
// FAIL SOFT to the vendored path so a tokenless standup still works (never a broken
|
|
462
|
-
// instance). The env is injectable for tests; the runner falls back to process.env.
|
|
463
|
-
const env = deps.env || process.env;
|
|
464
|
-
const wantRegistry = registryCoreRequested(env);
|
|
465
|
-
const useRegistry = wantRegistry && hasNpmToken(env);
|
|
466
|
-
if (wantRegistry && !useRegistry) {
|
|
467
|
-
log(' [scaffold] PROVISION_CORE_FROM_REGISTRY set but no NPM_TOKEN in env — falling back to the vendored-tarball core (ADR 0134 fail-soft).');
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// materialize config/ + .claude/ + a pinned package.json + lockfile via `bongos init`.
|
|
471
|
-
// --no-seed: the instance's own DB is seeded by migrate, not here. Two mutually
|
|
472
|
-
// exclusive core-resolution shapes:
|
|
473
|
-
// registry (--registry-core): caret pin + env-fed .npmrc; `npm ci` pulls from the
|
|
474
|
-
// registry. init generates the lockfile (NPM_TOKEN is present — we gated on it).
|
|
475
|
-
// vendored (--vendor-core, default): a tarball built from this checkout, file:-pinned.
|
|
476
|
-
const spec = instanceInitSpec(inst, { ownerLogin });
|
|
477
|
-
const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
|
|
478
|
-
writeFile(specPath, JSON.stringify(spec, null, 2));
|
|
479
|
-
if (useRegistry) {
|
|
480
|
-
log(` [scaffold] core install: registry — PRIVATE @bongos/core@${coreV} via env-fed NPM_TOKEN (ADR 0134)`);
|
|
481
|
-
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed --force --registry-core`);
|
|
482
|
-
} else {
|
|
483
|
-
// Vendored tarball. PREFER a pre-built pinned tarball this control plane already has
|
|
484
|
-
// (a consumer instance's own `file:` core pin) — rebuilding via package-core.js aborts
|
|
485
|
-
// with an EMPTY core set when this runner IS a consumer instance (REPO_ROOT is inside
|
|
486
|
-
// gitignored node_modules), which left new repos empty (task 1002275). Only rebuild
|
|
487
|
-
// from source when there is no such pin (a genuine core source checkout).
|
|
488
|
-
let tgz = installedCoreTarball({ instanceRoot: deps.instanceRoot });
|
|
489
|
-
if (tgz) {
|
|
490
|
-
log(` [scaffold] core install: reusing the pinned core tarball this control plane runs (${path.basename(tgz)}) — no source rebuild (task 1002275)`);
|
|
491
|
-
} else {
|
|
492
|
-
log(` [scaffold] core install: building a fresh vendored tarball from source (package-core.js)`);
|
|
493
|
-
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
494
|
-
tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
|
|
495
|
-
}
|
|
496
|
-
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed --force --vendor-core ${tgz}`);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// 4. commit + push the scaffold to the instance's own main (keep node_modules out).
|
|
500
|
-
writeFile(path.join(root, '.gitignore'), INSTANCE_GITIGNORE);
|
|
501
|
-
exec(`git -C ${root} config user.name "Cloud Bongos"`);
|
|
502
|
-
exec(`git -C ${root} config user.email "bot@cloudbongos.com"`);
|
|
503
|
-
exec(`git -C ${root} add -A`);
|
|
504
|
-
exec(`git -C ${root} commit -m "chore: scaffold ${inst.slug} on a pinned @bongos/core ${coreV} (provision.js, ADR 0108)"`, { allowFail: true });
|
|
505
|
-
// Push. A GitHub DEPLOY KEY is per-repo, so the box's shared key can't write to a NEW
|
|
506
|
-
// project repo — the second empty-repo cause (task 1002317). PREFER the instance owner's
|
|
507
|
-
// OAuth token (it CREATED the repo via github-repos.createUserRepo, so `public_repo`
|
|
508
|
-
// already grants write). The token rides a chmod-600 git credential-store file — never
|
|
509
|
-
// the logged command or makeExec's env (both are surfaced) — and is removed right after
|
|
510
|
-
// (finally, so a failed push can't leave it on disk). No token (org-owned repo, or
|
|
511
|
-
// secret-box off) → fall back to the SSH `origin` push (unchanged; e.g. an instance whose
|
|
512
|
-
// matching deploy key was cloned into origin). `deps.getGithubToken` is injectable for
|
|
513
|
-
// tests; production resolves it lazily against the core db slice with the runner's pool.
|
|
488
|
+
// Resolve the instance owner's OAuth token BEFORE any git network op.
|
|
489
|
+
//
|
|
490
|
+
// Greenfield only needed it at PUSH time, so the lookup used to live down beside the
|
|
491
|
+
// push. Adopt needs it EARLIER: its first act is to clone the owner's own repo, and a
|
|
492
|
+
// PRIVATE one is unreadable by the control plane's shared deploy key (a deploy key is
|
|
493
|
+
// per-repo). Since ADR 0155 this token carries the `repo` scope, so it can both read and
|
|
494
|
+
// write the owner's private repos — which is what makes adopt possible at all.
|
|
495
|
+
//
|
|
496
|
+
// A GitHub DEPLOY KEY is per-repo, so the box's shared key also can't WRITE to a new
|
|
497
|
+
// project repo — the second empty-repo cause (task 1002317) — hence preferring this
|
|
498
|
+
// token on both paths. It rides a chmod-600 git credential-store file, never the logged
|
|
499
|
+
// command or makeExec's env (makeExec surfaces both), and is removed in a `finally` so a
|
|
500
|
+
// failed git op can't leave it on disk. No token (org-owned repo, or secret-box off) →
|
|
501
|
+
// fall back to the SSH `origin` remote. `deps.getGithubToken` is injectable for tests;
|
|
502
|
+
// production resolves it lazily against the core db slice with the runner's pool.
|
|
514
503
|
let ownerToken = null;
|
|
515
504
|
if (inst.owner_builder_id) {
|
|
516
505
|
try {
|
|
@@ -520,21 +509,166 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
520
509
|
ownerToken = r && r.token ? r.token : null;
|
|
521
510
|
} catch (e) { log(` [scaffold] owner OAuth token lookup failed (${e && e.message}) — using the SSH remote`); }
|
|
522
511
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
512
|
+
// The credential-store file + the credential-LESS URL git resolves against it. The URL
|
|
513
|
+
// carries only the `x-access-token` username (no secret), so it is safe in a logged argv.
|
|
514
|
+
const credFile = ownerToken ? path.join(os.tmpdir(), `bongos-gitcred-${inst.slug}-${process.pid}`) : null;
|
|
515
|
+
const httpsUrl = `https://x-access-token@github.com/${repo.owner}/${repo.name}.git`;
|
|
516
|
+
const credOpt = credFile ? `-c credential.helper='store --file=${credFile}' ` : '';
|
|
517
|
+
if (credFile) writeFile(credFile, `https://x-access-token:${ownerToken}@github.com\n`, { mode: 0o600 });
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
// 1. Materialize the working tree.
|
|
521
|
+
//
|
|
522
|
+
// ADOPT — CLONE the owner's existing repo, so its history + files come along and the
|
|
523
|
+
// layer commit lands as an ordinary descendant of its current tip. This is the whole
|
|
524
|
+
// point: `git init`ing a fresh tree here produces an UNRELATED history, which the
|
|
525
|
+
// remote rejects as a non-fast-forward (and, were it ever forced, would erase the
|
|
526
|
+
// owner's work). Cloning over the token's HTTPS URL also covers the private case the
|
|
527
|
+
// shared deploy key cannot read.
|
|
528
|
+
//
|
|
529
|
+
// GREENFIELD — a freshly-created instance repo is EMPTY (github-repos.createUserRepo,
|
|
530
|
+
// auto_init:false), so there is NOTHING to clone; and the shared PROVISION_GIT_SSH_ALIAS
|
|
531
|
+
// deploy key can't even READ a consumer's brand-new repo under their own account
|
|
532
|
+
// (`git clone` fails "Repository not found" — the clone-side twin of the task-1002317
|
|
533
|
+
// push bug). So `git init` locally and set `origin` to the SSH remote for the no-token
|
|
534
|
+
// fallback push. Unchanged behavior.
|
|
535
|
+
//
|
|
536
|
+
// Either way a re-run after a partial attempt reuses the existing dir.
|
|
537
|
+
const hasGit = fs.existsSync(path.join(root, '.git'));
|
|
538
|
+
if (mode === 'adopt') {
|
|
539
|
+
if (!hasGit) {
|
|
540
|
+
exec(`mkdir -p ${root}`);
|
|
541
|
+
exec(`git ${credOpt}clone ${credFile ? httpsUrl : remote} ${root}`);
|
|
542
|
+
// Normalize `origin` to the SSH remote, matching what the greenfield path leaves
|
|
543
|
+
// behind. Two reasons: the clone URL would otherwise persist as origin, and the
|
|
544
|
+
// ongoing deploy (standalonePullCmd → `git pull --ff-only origin main`, and the box
|
|
545
|
+
// timer's `git reset --hard origin/main`) reads origin — it must be a form that can
|
|
546
|
+
// authenticate durably, which the one-shot token URL cannot (its credential file is
|
|
547
|
+
// deleted below). Nothing durable ever holds the token either way; the pushes here
|
|
548
|
+
// always pass an explicit URL.
|
|
549
|
+
if (credFile) exec(`git -C ${root} remote set-url origin ${remote}`);
|
|
550
|
+
} else {
|
|
551
|
+
exec(`git -C ${root} ${credOpt}fetch ${credFile ? httpsUrl : 'origin'} main`);
|
|
552
|
+
}
|
|
553
|
+
// The rest of provisioning is main-only (standalonePullCmd is `git pull --ff-only
|
|
554
|
+
// origin main`, and the box's deploy timer does `git reset --hard origin/main`), so a
|
|
555
|
+
// repo whose default branch is not `main` would be layered and pushed somewhere the
|
|
556
|
+
// deploy never reads. Fail loudly with the fix rather than silently half-working.
|
|
557
|
+
const head = exec(`git -C ${root} rev-parse --abbrev-ref HEAD`, { allowFail: true });
|
|
558
|
+
const branch = String((head && head.stdout) || '').trim();
|
|
559
|
+
if (branch && branch !== 'main' && branch !== 'HEAD') {
|
|
560
|
+
throw new Error(`standalone instance ${inst.slug}: adopting ${repo.owner}/${repo.name} but its default branch is "${branch}", not "main". The instance deploy only ever reads origin/main, so the layer would never be served. Rename the default branch to main (or point target_ref at a repo whose default is main) and re-run.`);
|
|
561
|
+
}
|
|
562
|
+
exec(`git -C ${root} checkout main`, { allowFail: true });
|
|
563
|
+
} else {
|
|
564
|
+
if (!hasGit) {
|
|
565
|
+
exec(`mkdir -p ${root}`);
|
|
566
|
+
exec(`git -C ${root} init -q`);
|
|
567
|
+
exec(`git -C ${root} remote add origin ${remote}`);
|
|
568
|
+
}
|
|
569
|
+
exec(`git -C ${root} checkout -B main`);
|
|
532
570
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
571
|
+
|
|
572
|
+
// Core-install path (ADR 0134, task 2090). DEFAULT = vendor a tarball built from THIS
|
|
573
|
+
// control-plane checkout (offline, proven — task 2053). OPT-IN = pull the PRIVATE
|
|
574
|
+
// @bongos/core from the npm registry (PROVISION_CORE_FROM_REGISTRY=1). The registry
|
|
575
|
+
// path needs NPM_TOKEN at `npm ci` time; if the flag is set but no token is present we
|
|
576
|
+
// FAIL SOFT to the vendored path so a tokenless standup still works (never a broken
|
|
577
|
+
// instance). The env is injectable for tests; the runner falls back to process.env.
|
|
578
|
+
const env = deps.env || process.env;
|
|
579
|
+
const wantRegistry = registryCoreRequested(env);
|
|
580
|
+
const useRegistry = wantRegistry && hasNpmToken(env);
|
|
581
|
+
if (wantRegistry && !useRegistry) {
|
|
582
|
+
log(' [scaffold] PROVISION_CORE_FROM_REGISTRY set but no NPM_TOKEN in env — falling back to the vendored-tarball core (ADR 0134 fail-soft).');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// materialize config/ + .claude/ + a pinned package.json + lockfile via `bongos init`.
|
|
586
|
+
// --no-seed: the instance's own DB is seeded by migrate, not here (and on the adopt path
|
|
587
|
+
// the REAL-state seeding + backlog import happen when the owner runs `bongos init --adopt`
|
|
588
|
+
// themselves, against the live instance — the step the wizard walks them through).
|
|
589
|
+
//
|
|
590
|
+
// MODE FLAGS. Greenfield passes --force (overwrite is safe on an empty repo). Adopt passes
|
|
591
|
+
// --adopt --accept instead and NEVER --force: --adopt takes init's layering path, which
|
|
592
|
+
// writes each config only where absent and leaves pre-existing files untouched, and
|
|
593
|
+
// --accept clears the interactive pre-flight gate for this non-interactive runner (a
|
|
594
|
+
// BLOCKING pre-flight finding still hard-stops — it cannot be waved through). init refuses
|
|
595
|
+
// --adopt together with --force, so the two flag sets are mutually exclusive by design.
|
|
596
|
+
//
|
|
597
|
+
// Two mutually exclusive core-resolution shapes, orthogonal to the mode:
|
|
598
|
+
// registry (--registry-core): caret pin + env-fed .npmrc; `npm ci` pulls from the
|
|
599
|
+
// registry. init generates the lockfile (NPM_TOKEN is present — we gated on it).
|
|
600
|
+
// vendored (--vendor-core, default): a tarball built from this checkout, file:-pinned.
|
|
601
|
+
const modeFlags = mode === 'adopt' ? '--adopt --accept' : '--force';
|
|
602
|
+
const spec = instanceInitSpec(inst, { ownerLogin });
|
|
603
|
+
const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
|
|
604
|
+
writeFile(specPath, JSON.stringify(spec, null, 2));
|
|
605
|
+
if (useRegistry) {
|
|
606
|
+
log(` [scaffold] core install: registry — PRIVATE @bongos/core@${coreV} via env-fed NPM_TOKEN (ADR 0134)`);
|
|
607
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --registry-core`);
|
|
608
|
+
} else {
|
|
609
|
+
// Vendored tarball. PREFER a pre-built pinned tarball this control plane already has
|
|
610
|
+
// (a consumer instance's own `file:` core pin) — rebuilding via package-core.js aborts
|
|
611
|
+
// with an EMPTY core set when this runner IS a consumer instance (REPO_ROOT is inside
|
|
612
|
+
// gitignored node_modules), which left new repos empty (task 1002275). Only rebuild
|
|
613
|
+
// from source when there is no such pin (a genuine core source checkout).
|
|
614
|
+
let tgz = installedCoreTarball({ instanceRoot: deps.instanceRoot });
|
|
615
|
+
if (tgz) {
|
|
616
|
+
log(` [scaffold] core install: reusing the pinned core tarball this control plane runs (${path.basename(tgz)}) — no source rebuild (task 1002275)`);
|
|
617
|
+
} else {
|
|
618
|
+
log(` [scaffold] core install: building a fresh vendored tarball from source (package-core.js)`);
|
|
619
|
+
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
620
|
+
tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
|
|
621
|
+
}
|
|
622
|
+
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 4. commit + push to the instance's own main (keep node_modules out).
|
|
626
|
+
//
|
|
627
|
+
// .gitignore: greenfield OWNS the file. Adopt must not clobber the owner's existing
|
|
628
|
+
// ignores, so it only writes one where none exists — and where one does, it APPENDS the
|
|
629
|
+
// entries we actually require (node_modules/, the vendored-core exceptions, the generated
|
|
630
|
+
// nav docs) if they are missing, since `npm ci` + `git reset --hard` on the box depend on
|
|
631
|
+
// them. Duplicate lines in a .gitignore are harmless, so the append is idempotent-safe.
|
|
632
|
+
const giPath = path.join(root, '.gitignore');
|
|
633
|
+
if (mode === 'adopt' && fs.existsSync(giPath)) {
|
|
634
|
+
const existing = fs.readFileSync(giPath, 'utf8');
|
|
635
|
+
const missing = INSTANCE_GITIGNORE.split('\n')
|
|
636
|
+
.filter((l) => l.trim() && !l.startsWith('#'))
|
|
637
|
+
.filter((l) => !existing.split('\n').some((e) => e.trim() === l.trim()));
|
|
638
|
+
if (missing.length) {
|
|
639
|
+
writeFile(giPath, `${existing.replace(/\n*$/, '\n')}\n# Added by Cloud Bongos (adopt) — required by the instance deploy.\n${missing.join('\n')}\n`);
|
|
640
|
+
log(` [scaffold] appended ${missing.length} required .gitignore entr(ies) — existing rules left intact`);
|
|
641
|
+
} else {
|
|
642
|
+
log(' [scaffold] .gitignore already covers the required entries — left untouched');
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
writeFile(giPath, INSTANCE_GITIGNORE);
|
|
646
|
+
}
|
|
647
|
+
exec(`git -C ${root} config user.name "Cloud Bongos"`);
|
|
648
|
+
exec(`git -C ${root} config user.email "bot@cloudbongos.com"`);
|
|
649
|
+
exec(`git -C ${root} add -A`);
|
|
650
|
+
const commitMsg = mode === 'adopt'
|
|
651
|
+
? `chore: adopt Cloud Bongos onto ${inst.slug} on a pinned @bongos/core ${coreV} (provision.js, ADR 0121)`
|
|
652
|
+
: `chore: scaffold ${inst.slug} on a pinned @bongos/core ${coreV} (provision.js, ADR 0108)`;
|
|
653
|
+
exec(`git -C ${root} commit -m "${commitMsg}"`, { allowFail: true });
|
|
654
|
+
|
|
655
|
+
// Push. NO --force on either path, ever (see the function header): the ADR 0155 `repo`
|
|
656
|
+
// credential can write to every repo the owner owns, so a force here would be a route to
|
|
657
|
+
// destroying their work. Adopt's commit is a descendant of the tip it cloned, so an
|
|
658
|
+
// ordinary push fast-forwards; if the remote moved underneath us the rejection is the
|
|
659
|
+
// correct outcome and a re-run picks up the new tip.
|
|
660
|
+
if (credFile) {
|
|
661
|
+
exec(`git -C ${root} ${credOpt}push ${httpsUrl} HEAD:main`);
|
|
662
|
+
log(` [scaffold] ${mode === 'adopt' ? 'pushed the adopt layer' : 'pushed'} to ${repo.owner}/${repo.name}@main via the owner's GitHub token (task 1002317)`);
|
|
663
|
+
} else {
|
|
664
|
+
exec(`git -C ${root} push -u origin main`);
|
|
665
|
+
log(` [scaffold] pushed ${mode === 'adopt' ? 'the adopt layer' : 'initial scaffold'} to ${repo.owner}/${repo.name}@main (SSH origin)`);
|
|
666
|
+
}
|
|
667
|
+
return { ok: true, scaffolded: true, mode };
|
|
668
|
+
} finally {
|
|
669
|
+
// Always remove the credential file — a failed clone/push must not leave it on disk.
|
|
670
|
+
if (credFile) exec(`rm -f ${credFile}`, { allowFail: true });
|
|
536
671
|
}
|
|
537
|
-
return { ok: true, scaffolded: true };
|
|
538
672
|
}
|
|
539
673
|
|
|
540
674
|
// The per-instance systemd unit (the infra/cloudbongos.service shape): one Node process
|
|
@@ -1767,6 +1901,8 @@ module.exports = {
|
|
|
1767
1901
|
standaloneRoot, standalonePullCmd, standaloneInstallCmd, standaloneMigrateCmd,
|
|
1768
1902
|
// standalone scaffold (ADR 0108 / 0125, task 2055; consumer-core fix task 1002275)
|
|
1769
1903
|
parseTargetRef, instanceRepoRemote, instanceInitSpec, installedCoreTarball, scaffoldStandaloneRepo, coreVersionSafe,
|
|
1904
|
+
// adopt vs greenfield onboarding (ADR 0121, task 1002562)
|
|
1905
|
+
onboardMode, alreadyScaffolded,
|
|
1770
1906
|
zoneForDomain, dnsUpsertEnv, classifyZoneScope, checkDnsTokenScope,
|
|
1771
1907
|
caddyBlock, caddySnippetPath, healthzCmd, dedicatedUserData, nextFreePort,
|
|
1772
1908
|
// executors + orchestration (injectable deps for tests)
|
|
@@ -74,7 +74,7 @@ const INTEGRATION = new Set([
|
|
|
74
74
|
// .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
|
|
75
75
|
'consumer_layout_boot',
|
|
76
76
|
'claim_gate_rebase', 'clawback_confirmed_backlog', 'discord_autojoin', 'discord_oauth',
|
|
77
|
-
'linkify_refs', 'memory_isolation',
|
|
77
|
+
'governance_fold_bench', 'governance_live_proof', 'linkify_refs', 'memory_isolation',
|
|
78
78
|
'memory_pull', 'needs_rebase', 'parallel-load', 'reward_mode_cost_plus_only',
|
|
79
79
|
'session_records', 'setup_smoke',
|
|
80
80
|
'status-probe', 'subdomain_auth',
|
|
@@ -96,6 +96,14 @@ const KNOWN_FAILING = new Set([
|
|
|
96
96
|
'autotile_variants',
|
|
97
97
|
'autotile_xverify',
|
|
98
98
|
'anti_grid',
|
|
99
|
+
// #1002617: red in every clean environment (Linux CI and a Windows checkout
|
|
100
|
+
// alike), surfaced by the unit lane's first-ever runs. Parked so the `unit`
|
|
101
|
+
// check can be required (a gate that's already red gates nothing); per-test
|
|
102
|
+
// diagnoses live in that task. Remove each here as it is fixed.
|
|
103
|
+
// PAIRING: tests/known_failing_ratchet.mjs keeps EXECUTING each parked test
|
|
104
|
+
// and fails the suite the moment one starts passing — so this set cannot rot.
|
|
105
|
+
'platform_boot',
|
|
106
|
+
'served_paths_shipped',
|
|
99
107
|
]);
|
|
100
108
|
|
|
101
109
|
// Discover modules/<key>/tests/*.mjs. Pure-ish (reads disk; `isEnabled` is
|
|
@@ -199,6 +199,28 @@ function restartPreview(deps = {}) {
|
|
|
199
199
|
run(cmd, argv, { stdio: 'inherit' });
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// task 1002607: the discriminator probe URL — same origin as the health URL, at
|
|
203
|
+
// the API healthz path only the FULL server mounts. Pure + exported for tests.
|
|
204
|
+
function apiHealthzProbeUrl(healthUrl) {
|
|
205
|
+
try { return new URL('/api/gds/healthz', healthUrl).toString(); } catch { return null; }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// task 1002607: is the thing answering the health URL actually the MAIN server?
|
|
209
|
+
// The game-only preview entry (ADR 0052) mounts no /api/gds, so a 200 from the
|
|
210
|
+
// API healthz proves the answerer is the full server — whose default bind is the
|
|
211
|
+
// SAME 127.0.0.1:3000 the box health check probes — not the preview. A probe
|
|
212
|
+
// error/404 means "not the main server", which is the expected preview answer.
|
|
213
|
+
async function answeredByMainServer(healthUrl, fetchImpl) {
|
|
214
|
+
const probe = apiHealthzProbeUrl(healthUrl);
|
|
215
|
+
if (!probe) return false;
|
|
216
|
+
try {
|
|
217
|
+
const res = await fetchImpl(probe, { signal: AbortSignal.timeout(5_000) });
|
|
218
|
+
return res.status === 200;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
202
224
|
async function waitHealthy(deps = {}) {
|
|
203
225
|
const fetchImpl = deps.fetchImpl || fetch;
|
|
204
226
|
const healthUrl = deps.healthUrl || HEALTH_URL;
|
|
@@ -207,7 +229,19 @@ async function waitHealthy(deps = {}) {
|
|
|
207
229
|
while (Date.now() < deadline) {
|
|
208
230
|
try {
|
|
209
231
|
const res = await fetchImpl(healthUrl, { signal: AbortSignal.timeout(5_000) });
|
|
210
|
-
|
|
232
|
+
// task 1002607: was `< 500`, which called a 404 "healthy" — any wrong server
|
|
233
|
+
// with nothing at '/' passed. And a good status alone doesn't prove the
|
|
234
|
+
// PREVIEW answered: the main server binds the same port, so a dead preview
|
|
235
|
+
// "passed" whenever the full server was up. Cross-examine the answerer.
|
|
236
|
+
if (res.status < 400) {
|
|
237
|
+
if (await answeredByMainServer(healthUrl, fetchImpl)) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: `${healthUrl} is answered by the MAIN server (/api/gds/healthz responded 200), not the game preview — the preview did not start (its port is taken by the full server)`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return { ok: true };
|
|
244
|
+
}
|
|
211
245
|
lastErr = `HTTP ${res.status}`;
|
|
212
246
|
} catch (e) {
|
|
213
247
|
lastErr = e.message;
|
|
@@ -536,9 +570,7 @@ async function runShipGate({ taskId, files, skipStage, tty, approved }, deps = {
|
|
|
536
570
|
if (!staged.ok) {
|
|
537
571
|
// #1068: a HARD failure (couldn't sync the worktree to the served checkout,
|
|
538
572
|
// or the preview verifiably served stale code) means a review here would be
|
|
539
|
-
// a LIE — abort with the claim intact so the builder fixes it.
|
|
540
|
-
// failure (the preview simply didn't come up) must not brick shipping — the
|
|
541
|
-
// gate is a review aid, not a wall: warn loudly and continue.
|
|
573
|
+
// a LIE — abort with the claim intact so the builder fixes it.
|
|
542
574
|
if (staged.hard) {
|
|
543
575
|
return {
|
|
544
576
|
ok: false,
|
|
@@ -547,8 +579,16 @@ async function runShipGate({ taskId, files, skipStage, tty, approved }, deps = {
|
|
|
547
579
|
reason: `sandbox staging could not be trusted (${staged.reason}) — fix it, then re-run /builder-ship (or --skip-stage to bypass the review). Your claim is still active`,
|
|
548
580
|
};
|
|
549
581
|
}
|
|
550
|
-
|
|
551
|
-
return
|
|
582
|
+
// task 1002607 (§4.4 station 9): a SOFT failure (the preview didn't come up)
|
|
583
|
+
// used to WARN one stderr line and return ok — the review gate silently
|
|
584
|
+
// turned itself off and the ship went on as if the review had happened.
|
|
585
|
+
// Fail closed like the smoke gate (task 1002605): abort with the claim
|
|
586
|
+
// intact; --skip-stage is the explicit, visible bypass. No review:true —
|
|
587
|
+
// this is a failure to present, not a calm review pause.
|
|
588
|
+
return {
|
|
589
|
+
ok: false,
|
|
590
|
+
reason: `sandbox staging failed (${staged.reason}) — the sandbox review step DID NOT run. Fix the preview and re-run /builder-ship, or run /builder-ship --skip-stage to explicitly ship WITHOUT the sandbox review. Your claim is still active`,
|
|
591
|
+
};
|
|
552
592
|
}
|
|
553
593
|
|
|
554
594
|
if (d.action === 'stage_abort') {
|
|
@@ -578,10 +618,20 @@ async function runShipGate({ taskId, files, skipStage, tty, approved }, deps = {
|
|
|
578
618
|
return { ok: true, sandboxReview: approvalRecord('tty', staged.url, deps) };
|
|
579
619
|
}
|
|
580
620
|
|
|
621
|
+
// task 1002607: the old message ("this clone has no game preview entry") implied
|
|
622
|
+
// the BUILDER's clone was at fault, when the commonest cause is that the checkout
|
|
623
|
+
// simply carries no game — src/preview-server.js is an instance's product file
|
|
624
|
+
// (the ADR 0052 game-only server); the core repo does not ship one. Name what is
|
|
625
|
+
// actually missing and both real causes. Exported so the wording is pinned.
|
|
626
|
+
const NO_SANDBOX_MSG =
|
|
627
|
+
'No sandbox here: this machine is not a dev box, and this checkout has no game preview entry (src/preview-server.js — the ADR 0052 game-only server).';
|
|
628
|
+
const NO_SANDBOX_DETAIL =
|
|
629
|
+
'That entry file comes from an instance\'s product layer. This repo does not ship one, and a partial/sparse clone of an instance that has one also lacks it — nothing here can be staged or previewed.';
|
|
630
|
+
|
|
581
631
|
async function cliMain() {
|
|
582
632
|
const cmd = process.argv[2] || 'stage';
|
|
583
633
|
const ctx = resolvePreviewContext();
|
|
584
|
-
const NO_SANDBOX =
|
|
634
|
+
const NO_SANDBOX = NO_SANDBOX_MSG;
|
|
585
635
|
|
|
586
636
|
if (cmd === 'url') {
|
|
587
637
|
console.log(ctx && ctx.url ? ctx.url : '(no sandbox here — not a dev box, and no local game preview in this clone)');
|
|
@@ -606,9 +656,12 @@ async function cliMain() {
|
|
|
606
656
|
process.exit(2);
|
|
607
657
|
}
|
|
608
658
|
if (!ctx) {
|
|
659
|
+
// task 1002607: an explicit `stage` invocation that can stage nothing is a
|
|
660
|
+
// FAILED command, not a quiet success — exit 1 was exit 0, which read as
|
|
661
|
+
// "staged fine" to any caller that only checks the code.
|
|
609
662
|
console.log(NO_SANDBOX);
|
|
610
|
-
console.log(
|
|
611
|
-
|
|
663
|
+
console.log(NO_SANDBOX_DETAIL);
|
|
664
|
+
process.exit(1);
|
|
612
665
|
}
|
|
613
666
|
const res = await stage({});
|
|
614
667
|
if (!res.ok) {
|
|
@@ -643,6 +696,10 @@ module.exports = {
|
|
|
643
696
|
syncWorktreeToServed,
|
|
644
697
|
pickVerifyTarget,
|
|
645
698
|
verifyServedFresh,
|
|
699
|
+
// task 1002607: the health truth table + the honest no-sandbox wording, pinned.
|
|
700
|
+
waitHealthy,
|
|
701
|
+
apiHealthzProbeUrl,
|
|
702
|
+
NO_SANDBOX_MSG,
|
|
646
703
|
};
|
|
647
704
|
|
|
648
705
|
if (require.main === module) {
|
|
@@ -48,6 +48,7 @@ const SENSITIVE_DOC_GLOBS = [
|
|
|
48
48
|
'docs/canonical-permissions.md', // the rank-ladder / authority reference
|
|
49
49
|
'docs/security/', // threat models + red-team reports
|
|
50
50
|
'docs/audits/', // security + architecture audit findings
|
|
51
|
+
'docs/governance/', // the gate ENUMERATION + choke-point map — task 1002598
|
|
51
52
|
'docs/adr/0016-trust-boundary-server-enforced-permissions.md',
|
|
52
53
|
'docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md',
|
|
53
54
|
];
|