@bongos/core 1.19.1072 → 1.19.1074
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +320 -465
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -60
- package/clients/bongos-client/index.cjs +4 -60
- package/clients/bongos-client/index.d.ts +5 -90
- package/clients/bongos-client/index.mjs +4 -60
- package/config/branding.neutral.json +1 -5
- package/config/modules.neutral.json +2 -3
- package/config/scheduled-routines.json +0 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
- package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
- package/docs/adr/README.md +2 -1
- package/docs/api/openapi.json +171 -1661
- package/docs/api-reference.md +9 -41
- package/docs/architecture.md +16 -1
- package/docs/branding-contract.md +0 -3
- package/docs/copy-inventory.md +454 -479
- package/docs/copy-registry.json +746 -978
- package/docs/file-map.md +5 -34
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +13 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/diagrams/assertions.json +2 -22
- package/docs/onboarding/primer.md +9 -28
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +1032 -1077
- package/docs/recipes/render-pilot.md +29 -10
- package/migrations/core_257_module_store_registry.sql +87 -0
- package/migrations/core_258_drop_dev_box_tables.sql +55 -0
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/builder-settings/render-prefs.js +4 -5
- package/modules/copy-desk/docx.js +78 -5
- package/modules/copy-desk/module.json +1 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -9
- package/modules/copy-desk/tests/copy_docx.mjs +107 -1
- package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/economy/routes/credits.js +1 -2
- package/modules/government/catalog.js +13 -17
- package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
- package/modules/government/module.json +1 -0
- package/modules/government/protected-surfaces.json +3 -11
- package/modules/government/resolver.js +3 -46
- package/modules/government/routes/government.js +0 -2
- package/modules/government/session-scopes.js +43 -83
- package/modules/government/session-scopes.json +8 -18
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/board-lib.js +2 -2
- package/modules/hall-ui/public/builders.js +29 -40
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/gate.states.json +1 -1
- package/modules/hall-ui/public/hall-render.js +43 -79
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.js +1 -0
- package/modules/hall-ui/public/oversight.css +5 -5
- package/modules/hall-ui/public/palette.js +0 -11
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-sessions.js +1 -1
- package/modules/hall-ui/public/settings.css +0 -1
- package/modules/hall-ui/public/settings.html +4 -33
- package/modules/hall-ui/public/settings.js +3 -33
- package/modules/hall-ui/public/settings.states.json +2 -2
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +10 -13
- package/modules/hall-ui/public/task.html +4 -1
- package/modules/hall-ui/public/task.js +36 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
- package/modules/hall-ui/public/tweak-editor.css +16 -0
- package/modules/hall-ui/public/tweak-editor.html +26 -3
- package/modules/hall-ui/public/tweak-editor.js +32 -1
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -0
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +0 -5
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/kickoff-checklist.js +0 -4
- package/modules/lifecycle/lifecycle.js +0 -8
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/tasks.js +5 -7
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/onboarding/onboarding-state.js +17 -36
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/module.json +1 -0
- package/modules/platform-identity/platform-identity.js +13 -5
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/starter-bundles.js +5 -25
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +101 -32
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +4 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +84 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/cli-lib.js +8 -8
- package/scripts/gds/diagram-facts.js +12 -23
- package/scripts/gds/do-api.js +13 -80
- package/scripts/gds/doc-cli-guard.js +2 -0
- package/scripts/gds/doctor.js +1 -1
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +4 -4
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/lint-infra-exec.js +1 -1
- package/scripts/gds/local-preview-lib.js +8 -39
- package/scripts/gds/local-preview.js +2 -15
- package/scripts/gds/migration-namespace.js +3 -3
- package/scripts/gds/module-artifact.js +212 -0
- package/scripts/gds/module.js +95 -6
- package/scripts/gds/package-core.js +5 -116
- package/scripts/gds/provision-config.js +2 -2
- package/scripts/gds/provision-net.js +2 -3
- package/scripts/gds/provision.js +9 -9
- package/scripts/gds/publish-manifest.js +1 -2
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/ship-flow.js +6 -6
- package/scripts/gds/ship-land.js +3 -4
- package/scripts/gds/ship-regen.js +10 -41
- package/scripts/gds/ship.js +4 -24
- package/scripts/gds/start.js +0 -15
- package/scripts/hall-preview/README.md +5 -5
- package/scripts/hall-preview/server.js +6 -6
- package/scripts/render-diagrams.sh +1 -1
- package/src/bongos/api-errors.js +1 -1
- package/src/bongos/auth-github.js +2 -3
- package/src/bongos/auth.js +51 -126
- package/src/bongos/db-kernel.js +2 -3
- package/src/bongos/db.js +5 -28
- package/src/bongos/module-scope-map.js +14 -29
- package/src/bongos/module-store.js +141 -0
- package/src/bongos/route-rank-check.js +3 -16
- package/src/bongos/routes/auth.js +0 -116
- package/src/bongos/routes/builders.js +5 -15
- package/src/bongos/routes/instance.js +8 -14
- package/src/bongos/routes/me.js +6 -22
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes.js +5 -0
- package/src/module-api.js +13 -6
- package/src/module-loader/catalog.js +3 -0
- package/src/module-loader/loader.js +4 -4
- package/src/module-loader/manifest-schema.js +15 -7
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_client.mjs +0 -15
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/canonical_profile_url.mjs +1 -3
- package/tests/claim_action.mjs +4 -1
- package/tests/claim_from_task_and_home.mjs +127 -0
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/do_api.mjs +106 -0
- package/tests/feedback_latest.mjs +122 -0
- package/tests/fitness.mjs +44 -42
- package/tests/gate_approvals.mjs +0 -1
- package/tests/go_live.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +20 -19
- package/tests/government_ownership_scope.mjs +33 -35
- package/tests/government_parity.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +56 -37
- package/tests/government_require_permission.mjs +3 -3
- package/tests/government_seed.mjs +37 -4
- package/tests/government_session_scope.mjs +92 -366
- package/tests/hall_error_envelope.mjs +4 -0
- package/tests/hall_landing_boot.mjs +4 -1
- package/tests/hall_palette.mjs +1 -16
- package/tests/hall_record_world.mjs +6 -0
- package/tests/hall_tweak_editor.mjs +52 -1
- package/tests/host_topology_skips.mjs +1 -2
- package/tests/html_comment_nesting.mjs +137 -0
- package/tests/infra_exec_paths.mjs +9 -10
- package/tests/init.mjs +4 -5
- package/tests/instance_manifest.mjs +18 -25
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +0 -221
- package/tests/local_preview.mjs +0 -12
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +15 -15
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +13 -4
- package/tests/module_store_publish.mjs +307 -0
- package/tests/module_store_publish_route.mjs +121 -0
- package/tests/module_store_registry_migration.mjs +70 -0
- package/tests/modules.mjs +56 -26
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/profile_route.mjs +1 -6
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +10 -18
- package/tests/publish_manifest.mjs +1 -2
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/repo_map.mjs +3 -4
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/session_rename_fallback.mjs +0 -21
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/upgrade.mjs +4 -4
- package/tests/watch_sealed_floor.mjs +0 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +21 -10
- package/tests/wizard_preselect_why.mjs +18 -18
- package/docs/onboarding/browser-terminal-guide.md +0 -73
- package/docs/recipes/managed-settings-remote-control.md +0 -416
- package/modules/dev-box/CLAUDE.md +0 -15
- package/modules/dev-box/box-access.js +0 -623
- package/modules/dev-box/box-credential.js +0 -94
- package/modules/dev-box/box-onboard.js +0 -486
- package/modules/dev-box/boxes.js +0 -1038
- package/modules/dev-box/db.js +0 -39
- package/modules/dev-box/module.json +0 -19
- package/modules/dev-box/routes/box.js +0 -1223
- package/scripts/gds/box-auth-check.js +0 -160
- package/scripts/gds/box-infra.js +0 -321
- package/scripts/gds/box-sync.js +0 -216
- package/scripts/gds/box.js +0 -1332
- package/scripts/gds/cf-tunnel.js +0 -558
- package/scripts/gds/smoke-box.sh +0 -150
- package/scripts/gds/tree-preflight.js +0 -241
- package/src/bongos/app-pair.js +0 -172
- package/tests/app_pair.mjs +0 -168
- package/tests/box_access.mjs +0 -578
- package/tests/box_auth_check.mjs +0 -114
- package/tests/box_code_staleness.mjs +0 -129
- package/tests/box_connect_e2e.mjs +0 -196
- package/tests/box_cost.mjs +0 -70
- package/tests/box_credential.mjs +0 -125
- package/tests/box_dns_repoint.mjs +0 -153
- package/tests/box_env.mjs +0 -64
- package/tests/box_host_keys.mjs +0 -126
- package/tests/box_infra_resolve.mjs +0 -246
- package/tests/box_onboard.mjs +0 -317
- package/tests/box_scope_session.mjs +0 -119
- package/tests/box_sweep_docs.mjs +0 -119
- package/tests/box_sync.mjs +0 -55
- package/tests/box_sync_scope_report.mjs +0 -126
- package/tests/box_task_scope.mjs +0 -169
- package/tests/box_task_scope_pause.mjs +0 -66
- package/tests/box_terminal_ssrf.mjs +0 -96
- package/tests/boxes.mjs +0 -1585
- package/tests/cf_ruleset_preflight.mjs +0 -142
- package/tests/cf_tunnel.mjs +0 -496
- package/tests/docs_scrubber_damage.mjs +0 -111
- package/tests/module_scope_active_claims.mjs +0 -94
- package/tests/ship_api_client_pathspec.mjs +0 -100
- package/tests/tree_preflight.mjs +0 -243
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// scripts/gds/artifact-format.js — the ONE packaging format Bongos ships code in:
|
|
2
|
+
// a deterministic ustar+gzip tarball plus a compression-independent tree pin
|
|
3
|
+
// (per-file sha256 + tree_sha256). Split out of package-core.js (task 1004271) so the
|
|
4
|
+
// module store (ADR 0338 D1: "no second packaging format") builds and VERIFIES module
|
|
5
|
+
// versions with the exact functions the core release uses — and so the always-on
|
|
6
|
+
// server can read a tarball without loading package-core.js's release machinery.
|
|
7
|
+
// Pure: no I/O beyond zlib/crypto, no git, no network.
|
|
8
|
+
|
|
9
|
+
const zlib = require('node:zlib');
|
|
10
|
+
const crypto = require('node:crypto');
|
|
11
|
+
|
|
12
|
+
function sha256hex(buf) {
|
|
13
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The PIN. A canonical, compression-independent digest of a packaged tree: one
|
|
17
|
+
// "path\0mode\0filesha256\n" line per file, byte-sorted by path. A consumer
|
|
18
|
+
// recomputes this from the unpacked tree to prove an install matches the pin.
|
|
19
|
+
// `files` = [{ path, mode, sha256 }]. Modes are normalized so the pin is stable
|
|
20
|
+
// regardless of the checkout's umask.
|
|
21
|
+
function canonicalTreeHash(files) {
|
|
22
|
+
const lines = (files || [])
|
|
23
|
+
.map((f) => `${f.path}\u0000${normalizeMode(f.mode)}\u0000${f.sha256}`)
|
|
24
|
+
.sort();
|
|
25
|
+
return sha256hex(Buffer.from(lines.join('\n') + '\n', 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Collapse ANY mode form — git's 100644/100755, our canonical 0000644/0000755, or a
|
|
29
|
+
// raw filesystem octal string like "755" — to two canonical tar modes, keyed only on
|
|
30
|
+
// the executable bit. IDEMPOTENT (normalizeMode(normalizeMode(x)) === normalizeMode(x))
|
|
31
|
+
// AND exec-aware, so the tree pin reproduces byte-identically whether a consumer
|
|
32
|
+
// recomputes it from the manifest's files[] (already-canonical modes) or from the
|
|
33
|
+
// unpacked tree's stat() modes. Expects a STRING (git mode / canonical mode / octal).
|
|
34
|
+
function normalizeMode(mode) {
|
|
35
|
+
const s = String(mode);
|
|
36
|
+
if (s === '100755' || s === '0000755' || s === '755') return '0000755';
|
|
37
|
+
const n = parseInt(s, 8);
|
|
38
|
+
if (Number.isFinite(n) && (n & 0o111)) return '0000755';
|
|
39
|
+
return '0000644';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---- a minimal, correct, DETERMINISTIC ustar+gzip writer (node-only, #686) ----
|
|
43
|
+
// Deterministic (fixed mtime/uid/gid, normalized mode, sorted entries) so the same
|
|
44
|
+
// source yields byte-identical bytes — a bonus over the tree_sha256 pin. Long paths
|
|
45
|
+
// use the ustar name/prefix split, falling back to a GNU 'L' longname entry (read
|
|
46
|
+
// by GNU tar, bsdtar/macOS, and node-tar) for the rare path that won't split.
|
|
47
|
+
|
|
48
|
+
function octal(n, width) {
|
|
49
|
+
// width-1 octal digits, zero-padded, then a NUL — the classic numeric field.
|
|
50
|
+
return Buffer.from(n.toString(8).padStart(width - 1, '0').slice(-(width - 1)) + '\0', 'ascii');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function tarChecksum(block) {
|
|
54
|
+
let sum = 0;
|
|
55
|
+
for (let i = 0; i < 512; i++) sum += (i >= 148 && i < 156) ? 0x20 : block[i];
|
|
56
|
+
return sum;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build one 512-byte ustar header. `name`/`prefix` must already fit (<=100 / <=155).
|
|
60
|
+
function ustarHeader({ name, prefix = '', mode, size, typeflag }) {
|
|
61
|
+
const h = Buffer.alloc(512, 0);
|
|
62
|
+
h.write(name, 0, 100, 'utf8');
|
|
63
|
+
octal(mode, 8).copy(h, 100);
|
|
64
|
+
octal(0, 8).copy(h, 108); // uid
|
|
65
|
+
octal(0, 8).copy(h, 116); // gid
|
|
66
|
+
octal(size, 12).copy(h, 124);
|
|
67
|
+
octal(0, 12).copy(h, 136); // mtime = 0 (determinism)
|
|
68
|
+
h.write(typeflag, 156, 1, 'ascii');
|
|
69
|
+
h.write('ustar\0', 257, 6, 'ascii');
|
|
70
|
+
h.write('00', 263, 2, 'ascii');
|
|
71
|
+
if (prefix) h.write(prefix, 345, 155, 'utf8');
|
|
72
|
+
// checksum field: 6 octal digits + NUL + space, computed with the field = spaces.
|
|
73
|
+
const cksum = tarChecksum(h);
|
|
74
|
+
h.write(cksum.toString(8).padStart(6, '0').slice(-6) + '\0 ', 148, 8, 'ascii');
|
|
75
|
+
return h;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Split a >100-byte path into { name<=100, prefix<=155 } at a '/' boundary, or
|
|
79
|
+
// return null if no split fits (→ caller uses the GNU longname fallback).
|
|
80
|
+
function splitUstarName(fullPath) {
|
|
81
|
+
const bytes = Buffer.byteLength(fullPath, 'utf8');
|
|
82
|
+
if (bytes <= 100) return { name: fullPath, prefix: '' };
|
|
83
|
+
// Find the LAST '/' such that the tail (after it) is <=100 and the head <=155.
|
|
84
|
+
for (let i = fullPath.length - 1; i > 0; i--) {
|
|
85
|
+
if (fullPath[i] !== '/') continue;
|
|
86
|
+
const prefix = fullPath.slice(0, i);
|
|
87
|
+
const name = fullPath.slice(i + 1);
|
|
88
|
+
if (Buffer.byteLength(name, 'utf8') <= 100 && Buffer.byteLength(prefix, 'utf8') <= 155) {
|
|
89
|
+
return { name, prefix };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function padTo512(buf) {
|
|
96
|
+
const rem = buf.length % 512;
|
|
97
|
+
return rem === 0 ? buf : Buffer.concat([buf, Buffer.alloc(512 - rem, 0)]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// GNU longname pseudo-entry: a header (typeflag 'L', name '././@LongLink') whose
|
|
101
|
+
// BODY is the full path (incl. trailing NUL), immediately followed by the real
|
|
102
|
+
// file entry. Read by GNU tar, bsdtar/macOS, and node-tar.
|
|
103
|
+
function gnuLongName(fullPath) {
|
|
104
|
+
const payload = Buffer.from(fullPath + '\0', 'utf8');
|
|
105
|
+
const hdr = ustarHeader({ name: '././@LongLink', mode: 0o644, size: payload.length, typeflag: 'L' });
|
|
106
|
+
return Buffer.concat([hdr, padTo512(payload)]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// entries: [{ name, mode, buf }] — `name` includes the TAR_ROOT prefix. Returns a
|
|
110
|
+
// gzip'd tar Buffer. Pure + deterministic given sorted entries.
|
|
111
|
+
function buildTar(entries) {
|
|
112
|
+
const sorted = [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
113
|
+
const chunks = [];
|
|
114
|
+
for (const e of sorted) {
|
|
115
|
+
const mode = e.mode & 0o7777;
|
|
116
|
+
const split = splitUstarName(e.name);
|
|
117
|
+
if (split) {
|
|
118
|
+
chunks.push(ustarHeader({ name: split.name, prefix: split.prefix, mode, size: e.buf.length, typeflag: '0' }));
|
|
119
|
+
} else {
|
|
120
|
+
chunks.push(gnuLongName(e.name));
|
|
121
|
+
chunks.push(ustarHeader({ name: e.name.slice(0, 100), mode, size: e.buf.length, typeflag: '0' }));
|
|
122
|
+
}
|
|
123
|
+
chunks.push(padTo512(e.buf));
|
|
124
|
+
}
|
|
125
|
+
// two zero blocks terminate the archive
|
|
126
|
+
chunks.push(Buffer.alloc(1024, 0));
|
|
127
|
+
const tar = Buffer.concat(chunks);
|
|
128
|
+
return zlib.gzipSync(tar, { level: 9 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// ---- the matching READER (task 1004271) -------------------------------------
|
|
134
|
+
// Reads back exactly what buildTar writes — plain ustar file entries (name/prefix
|
|
135
|
+
// split) and GNU 'L' longname entries — into [{ name, mode, buf }]. It is the store's
|
|
136
|
+
// side of the format, so it is strict rather than permissive: any other entry type
|
|
137
|
+
// (a directory, a symlink, a pax header), a size or count over the caps, a truncated
|
|
138
|
+
// archive or a bad header checksum is a THROWN error, never a skipped entry. A server
|
|
139
|
+
// that verifies uploads must not quietly accept a shape it did not check.
|
|
140
|
+
//
|
|
141
|
+
// Two entry points over one parser: readTar (sync — the CLI and tests) and
|
|
142
|
+
// readTarAsync, which inflates on libuv's thread pool so a server verifying an upload
|
|
143
|
+
// does not hold the event loop for the decompression. The parse itself is a single
|
|
144
|
+
// linear pass over the inflated bytes.
|
|
145
|
+
const DEFAULT_READ_LIMITS = { maxUnpackedBytes: 64 * 1024 * 1024, maxFiles: 5000 };
|
|
146
|
+
const gunzipAsync = require('node:util').promisify(zlib.gunzip);
|
|
147
|
+
|
|
148
|
+
const unreadable = (maxUnpackedBytes, e) =>
|
|
149
|
+
new Error(`not a readable gzip tarball within ${maxUnpackedBytes} bytes (${e.code || e.message})`);
|
|
150
|
+
|
|
151
|
+
function readTar(tgz, { maxUnpackedBytes = DEFAULT_READ_LIMITS.maxUnpackedBytes, maxFiles = DEFAULT_READ_LIMITS.maxFiles } = {}) {
|
|
152
|
+
let tar;
|
|
153
|
+
try { tar = zlib.gunzipSync(tgz, { maxOutputLength: maxUnpackedBytes }); }
|
|
154
|
+
catch (e) { throw unreadable(maxUnpackedBytes, e); }
|
|
155
|
+
return parseTar(tar, { maxFiles });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readTarAsync(tgz, { maxUnpackedBytes = DEFAULT_READ_LIMITS.maxUnpackedBytes, maxFiles = DEFAULT_READ_LIMITS.maxFiles } = {}) {
|
|
159
|
+
let tar;
|
|
160
|
+
try { tar = await gunzipAsync(tgz, { maxOutputLength: maxUnpackedBytes }); }
|
|
161
|
+
catch (e) { throw unreadable(maxUnpackedBytes, e); }
|
|
162
|
+
return parseTar(tar, { maxFiles });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseTar(tar, { maxFiles }) {
|
|
166
|
+
const cstr = (b, from, len) => {
|
|
167
|
+
const s = b.subarray(from, from + len);
|
|
168
|
+
const z = s.indexOf(0);
|
|
169
|
+
return (z < 0 ? s : s.subarray(0, z)).toString('utf8');
|
|
170
|
+
};
|
|
171
|
+
const out = [];
|
|
172
|
+
let off = 0;
|
|
173
|
+
let longName = null;
|
|
174
|
+
while (true) {
|
|
175
|
+
if (off + 512 > tar.length) throw new Error('tarball is truncated (no end-of-archive marker)');
|
|
176
|
+
const h = tar.subarray(off, off + 512);
|
|
177
|
+
if (h.every((x) => x === 0)) break;
|
|
178
|
+
const stored = parseInt(cstr(h, 148, 8).trim(), 8);
|
|
179
|
+
if (stored !== tarChecksum(h)) throw new Error(`tar header checksum mismatch at byte ${off}`);
|
|
180
|
+
const size = parseInt(cstr(h, 124, 12).trim() || '0', 8);
|
|
181
|
+
if (!Number.isFinite(size) || size < 0) throw new Error(`bad entry size at byte ${off}`);
|
|
182
|
+
const type = String.fromCharCode(h[156] || 48);
|
|
183
|
+
const bodyStart = off + 512;
|
|
184
|
+
const bodyEnd = bodyStart + size;
|
|
185
|
+
if (bodyEnd > tar.length) throw new Error('tarball is truncated mid-entry');
|
|
186
|
+
const body = tar.subarray(bodyStart, bodyEnd);
|
|
187
|
+
off = bodyStart + size + ((512 - (size % 512)) % 512);
|
|
188
|
+
if (type === 'L') { longName = cstr(body, 0, body.length); continue; }
|
|
189
|
+
if (type !== '0' && type !== '\u0000') throw new Error(`unsupported tar entry type "${type}" — only regular files are allowed`);
|
|
190
|
+
const prefix = cstr(h, 345, 155);
|
|
191
|
+
const name = longName || (prefix ? `${prefix}/${cstr(h, 0, 100)}` : cstr(h, 0, 100));
|
|
192
|
+
longName = null;
|
|
193
|
+
out.push({ name, mode: parseInt(cstr(h, 100, 8).trim() || '0', 8), buf: Buffer.from(body) });
|
|
194
|
+
if (out.length > maxFiles) throw new Error(`tarball has more than ${maxFiles} files`);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
sha256hex, canonicalTreeHash, normalizeMode,
|
|
201
|
+
octal, tarChecksum, ustarHeader, splitUstarName, padTo512, gnuLongName, buildTar,
|
|
202
|
+
readTar, readTarAsync,
|
|
203
|
+
};
|
|
@@ -151,13 +151,6 @@ ALLOWLIST=(
|
|
|
151
151
|
# hardcoded in code.
|
|
152
152
|
'CODEOWNERS'
|
|
153
153
|
|
|
154
|
-
# Dev Box app distribution (#904, ADR 0044): the PUBLIC releases repo lives
|
|
155
|
-
# under the same GitHub account that owns the main repo — repo-hosting
|
|
156
|
-
# identity (same nature as provision-staging.sh above), not a builder
|
|
157
|
-
# identity. The test pins the committed config's repo string.
|
|
158
|
-
'config/devbox-app.json'
|
|
159
|
-
'tests/devbox_downloads.mjs'
|
|
160
|
-
|
|
161
154
|
# This audit script's own self-reference (it documents what it's looking for).
|
|
162
155
|
'scripts/gds/audit-authorship.sh'
|
|
163
156
|
|
|
@@ -173,21 +166,18 @@ ALLOWLIST=(
|
|
|
173
166
|
|
|
174
167
|
# Test DATA that pins a real contract (task 1002640) — identity in an
|
|
175
168
|
# assertion IS the contract, not a mis-attribution: owner approval RECORDS
|
|
176
|
-
# the Archon's login (gate_approvals),
|
|
177
|
-
# term-<login> hostnames (box_terminal_ssrf), packaging/provisioning tests
|
|
169
|
+
# the Archon's login (gate_approvals), packaging/provisioning tests
|
|
178
170
|
# exercise the real repo owner + the mirror-redaction floor. Listed per file
|
|
179
171
|
# (never a blanket tests/ exclusion) so a NEW test file is still scanned.
|
|
180
172
|
'tests/provision.mjs'
|
|
181
173
|
'tests/package_core.mjs'
|
|
182
174
|
'tests/init.mjs'
|
|
183
|
-
'tests/box_terminal_ssrf.mjs'
|
|
184
175
|
'tests/gate_approvals.mjs'
|
|
185
176
|
'tests/publish_reconciler.mjs'
|
|
186
177
|
'tests/dev_lib.mjs'
|
|
187
178
|
'tests/ship_card.mjs'
|
|
188
179
|
'tests/scrubber_corpus.mjs'
|
|
189
180
|
'tests/module_submissions.mjs'
|
|
190
|
-
'tests/boxes.mjs'
|
|
191
181
|
'tests/provisioning_onboard_mode.mjs'
|
|
192
182
|
'tests/instance_manifest.mjs'
|
|
193
183
|
'tests/bongos_downloads.mjs'
|
|
@@ -199,10 +189,8 @@ ALLOWLIST=(
|
|
|
199
189
|
'tests/currency_label.mjs'
|
|
200
190
|
'tests/cloud_bongos_pack.mjs'
|
|
201
191
|
'tests/bump_version.mjs'
|
|
202
|
-
'tests/box_scope_session.mjs'
|
|
203
192
|
'tests/auth_inactive_builder.mjs'
|
|
204
193
|
'tests/auth_anthropic_identity.mjs'
|
|
205
|
-
'modules/dev-box/app/test/unit.mjs'
|
|
206
194
|
|
|
207
195
|
# The mirror-redaction floor: PRIOR_INSTANCE_FLOOR carries the prior
|
|
208
196
|
# instance's owner literal precisely so packaging can SCRUB it from every
|
package/scripts/gds/claim.js
CHANGED
|
@@ -7,21 +7,6 @@
|
|
|
7
7
|
// On success: prints the claim id, the task title, and the touches[] so the
|
|
8
8
|
// builder knows the parallel-safety boundary they're working within.
|
|
9
9
|
|
|
10
|
-
// TREE PREFLIGHT (task 1003098). A task-scoped dev box carries scripts/ and
|
|
11
|
-
// src/bongos/ but not every modules/ directory, so the requires below can fail —
|
|
12
|
-
// and Node's loader names only the FIRST missing one, which turned recovery into
|
|
13
|
-
// guess-read-fix-repeat. This enumerates every missing requirement in one message
|
|
14
|
-
// and exits before the first such require runs. It must stay ABOVE them.
|
|
15
|
-
//
|
|
16
|
-
// A PRE-REQUEST hard exit, like the usage banner and worktree guard: nothing has
|
|
17
|
-
// been sent yet, so there is no in-flight request for cliExit to drain.
|
|
18
|
-
const { action: _surfaceAction } = require('./surface.js');
|
|
19
|
-
const { checkTree: _checkTree } = require('./tree-preflight.js');
|
|
20
|
-
{
|
|
21
|
-
const _fault = _checkTree(__filename, { command: _surfaceAction("claim") });
|
|
22
|
-
if (_fault) { console.error(_fault); process.exit(1); }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
10
|
const { execSync } = require('node:child_process');
|
|
26
11
|
const fs = require('node:fs');
|
|
27
12
|
const path = require('node:path');
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -139,8 +139,8 @@ function hallBase(base = API_BASE) {
|
|
|
139
139
|
// still be mid-close when the caller's process.exit() runs, tripping a
|
|
140
140
|
// UV_HANDLE_CLOSING assertion that corrupts piped JSON stdout. Connection reuse
|
|
141
141
|
// buys these one-shot processes nothing, so route requests through a dispatcher
|
|
142
|
-
// with keep-alive effectively disabled
|
|
143
|
-
//
|
|
142
|
+
// with keep-alive effectively disabled. Degrades to the default dispatcher if
|
|
143
|
+
// undici can't be required.
|
|
144
144
|
//
|
|
145
145
|
// task 1002734 — THIS ALONE IS NOT ENOUGH, and the measurements say so: with the
|
|
146
146
|
// dispatcher wired in and undici resolving fine, `node scripts/gds/api.js GET
|
|
@@ -1069,12 +1069,11 @@ function detectWorktreeName() {
|
|
|
1069
1069
|
// summary — that emitCard consumes directly.
|
|
1070
1070
|
//
|
|
1071
1071
|
// WHY THIS READS THE PAYLOAD AND NEVER require()s render-prefs.js. The ladder
|
|
1072
|
-
// lives in modules/builder-settings/render-prefs.js, and
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
// ships the answers on `render.flags`; this function only picks them up.
|
|
1072
|
+
// lives in modules/builder-settings/render-prefs.js, and a CLI reaches a
|
|
1073
|
+
// module's logic through the server, never by require — the module may be
|
|
1074
|
+
// disabled on the instance, and a require would couple every CLI to one
|
|
1075
|
+
// module's internals. So the server resolves the rung and ships the answers on
|
|
1076
|
+
// `render.flags`; this function only picks them up.
|
|
1078
1077
|
//
|
|
1079
1078
|
// TWO SERVER SHAPES, deliberately. `render.flags` is the ladder (task 1002518);
|
|
1080
1079
|
// `render.widgets_enabled` is the boolean that predates it. A CLI newer than the
|
|
@@ -1174,6 +1173,7 @@ module.exports = {
|
|
|
1174
1173
|
repairMsysMangledPath,
|
|
1175
1174
|
resolveQuiet,
|
|
1176
1175
|
readSessionToken,
|
|
1176
|
+
CLI_USER_AGENT,
|
|
1177
1177
|
apiCall,
|
|
1178
1178
|
cliExit,
|
|
1179
1179
|
isDispatcherIncompatError,
|
|
@@ -174,18 +174,12 @@ const CONCEPTUAL_TASK_IDS = {
|
|
|
174
174
|
246: 'R26 — karma surfaced on profile + sortable on leaderboard',
|
|
175
175
|
};
|
|
176
176
|
|
|
177
|
-
// 04-architecture (the
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// 904 — the OTB Dev Box desktop app (menu-bar/tray on-off switch)
|
|
184
|
-
// 774 — the browser-only box terminal (ttyd over an outbound tunnel)
|
|
185
|
-
// 897 — the per-builder sandbox live game preview on the box
|
|
186
|
-
// 919 — the scoped dev-box session (box token deny-by-default)
|
|
187
|
-
// 1025 — server-mediated branch publish (box uploads bundle, server pushes)
|
|
188
|
-
// 1027 — hardware-verify of the box→prod flow end-to-end (now SHIPPED)
|
|
177
|
+
// 04-architecture (the ship flow) — how code on a builder's machine reaches
|
|
178
|
+
// prod. Same "LIVE iff its backing GDS task is 'shipped'" contract as the memory
|
|
179
|
+
// chain above. Each entry documents the piece it drives in
|
|
180
|
+
// scripts/gds/gen-diagrams.js `architectureMmd`:
|
|
181
|
+
// 1025 — server-mediated branch publish (a checkout with no GitHub push
|
|
182
|
+
// credential uploads a bundle, the server pushes it)
|
|
189
183
|
// 1028 — replaced the standing push PAT with a GitHub App (now SHIPPED)
|
|
190
184
|
//
|
|
191
185
|
// Every id here MUST drive a piece of the diagram. An id no consumer reads is
|
|
@@ -194,15 +188,10 @@ const CONCEPTUAL_TASK_IDS = {
|
|
|
194
188
|
// stop 03 + 04 from regenerating at all (the failure shape task 1002667 fixed
|
|
195
189
|
// for the offset case). Do NOT re-add 859 (CI self-deploy): the deploy leg is
|
|
196
190
|
// the standalone AUTO-PULL of ADR 0108/0129, not a task-gated Actions job, so
|
|
197
|
-
// gen-diagrams.js deliberately excludes it — see its comment at `
|
|
191
|
+
// gen-diagrams.js deliberately excludes it — see its comment at `ship flow`.
|
|
198
192
|
// Removed in task 1002674.
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
774: 'browser-only box terminal (ttyd over tunnel)',
|
|
202
|
-
897: 'per-builder sandbox live preview',
|
|
203
|
-
919: 'scoped dev-box session (box token deny-by-default)',
|
|
204
|
-
1025: 'server-mediated branch publish (box uploads bundle, server pushes)',
|
|
205
|
-
1027: 'hardware-verify the box-to-prod flow end-to-end',
|
|
193
|
+
const SHIP_FLOW_TASK_IDS = {
|
|
194
|
+
1025: 'server-mediated branch publish (a checkout with no push credential uploads a bundle, the server pushes)',
|
|
206
195
|
1028: 'GitHub App hardening (replace the standing push PAT)',
|
|
207
196
|
};
|
|
208
197
|
|
|
@@ -290,10 +279,10 @@ async function conceptualFacts({ repo, migs, api }) {
|
|
|
290
279
|
// process via cli-lib's guidance path (see the loop's degrade-to-null below).
|
|
291
280
|
api = await cliClient({ allowUnauthenticated: true });
|
|
292
281
|
}
|
|
293
|
-
// 03's pieces + 04-architecture's memory chain + its
|
|
282
|
+
// 03's pieces + 04-architecture's memory chain + its ship-flow pieces
|
|
294
283
|
// share one fetch + one taskStatus map; any unreachable id degrades the WHOLE
|
|
295
284
|
// conceptual bundle (so 03 + 04 skip together rather than emit partial data).
|
|
296
|
-
const ALL_TASK_IDS = { ...CONCEPTUAL_TASK_IDS, ...
|
|
285
|
+
const ALL_TASK_IDS = { ...CONCEPTUAL_TASK_IDS, ...SHIP_FLOW_TASK_IDS };
|
|
297
286
|
const ids = Object.keys(ALL_TASK_IDS).map(Number);
|
|
298
287
|
const status = {};
|
|
299
288
|
let lockedOffset = null;
|
|
@@ -360,7 +349,7 @@ module.exports = {
|
|
|
360
349
|
repoSlug,
|
|
361
350
|
conceptualFacts,
|
|
362
351
|
CONCEPTUAL_TASK_IDS,
|
|
363
|
-
|
|
352
|
+
SHIP_FLOW_TASK_IDS,
|
|
364
353
|
IMPORT_ID_OFFSETS,
|
|
365
354
|
gatherFacts,
|
|
366
355
|
};
|
package/scripts/gds/do-api.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// scripts/gds/do-api.js — a thin DigitalOcean API v2 client for the control plane.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
6
|
-
//
|
|
7
|
-
// tier alongside its callers — and a module may not import a sibling module, so two
|
|
8
|
-
// CLIs can only share it from here. Originally #598 (box lifecycle).
|
|
3
|
+
// Control-plane client (ADR 0111 §2, task #1943): the DO client the
|
|
4
|
+
// instance-provisioning runner (scripts/gds/provision.js) and its teardown
|
|
5
|
+
// (provision-teardown.js) use. It is operator-CLI code (never the web tier), so it
|
|
6
|
+
// lives in the control-plane tier alongside its callers. Originally #598.
|
|
9
7
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// box.js orchestration) stays testable without a live account.
|
|
8
|
+
// Everything that touches DO goes through here so the provisioning code stays
|
|
9
|
+
// testable without a live account.
|
|
13
10
|
//
|
|
14
11
|
// Design:
|
|
15
12
|
// - Injectable transport (`fetchImpl`) so tests pass a fake fetch and assert
|
|
@@ -23,10 +20,8 @@
|
|
|
23
20
|
// - Live mode requires a token (DO_API_TOKEN); the constructor throws clearly
|
|
24
21
|
// if asked to run live without one — it never silently no-ops a real call.
|
|
25
22
|
//
|
|
26
|
-
// We implement only the calls
|
|
27
|
-
// createDroplet · getDroplet · deleteDroplet ·
|
|
28
|
-
// snapshotDroplet (action) · getAction (poll) · deleteSnapshot
|
|
29
|
-
// createDropletFromSnapshot
|
|
23
|
+
// We implement only the calls provisioning needs:
|
|
24
|
+
// createDroplet · getDroplet · deleteDroplet · settleDroplet (poll to active)
|
|
30
25
|
|
|
31
26
|
const { JsonHttpApi, makeApiError } = require('./http-api-client');
|
|
32
27
|
|
|
@@ -54,7 +49,7 @@ class DoApi extends JsonHttpApi {
|
|
|
54
49
|
|
|
55
50
|
// ---- droplets --------------------------------------------------------------
|
|
56
51
|
|
|
57
|
-
// image:
|
|
52
|
+
// image: a base image slug (e.g. 'ubuntu-24-04-x64').
|
|
58
53
|
async createDroplet({ name, region, size, image, userData, tags, sshKeys }) {
|
|
59
54
|
const body = {
|
|
60
55
|
name, region, size, image,
|
|
@@ -79,53 +74,9 @@ class DoApi extends JsonHttpApi {
|
|
|
79
74
|
return this._req('DELETE', `/droplets/${dropletId}`, null, { dryRunResult: { deleted: true } });
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// the
|
|
85
|
-
// box row whose droplet vanished). per_page=200 covers the whole fleet in one
|
|
86
|
-
// page at our scale (<100 boxes); a future larger fleet would follow
|
|
87
|
-
// links.pages.next. Returns { droplets: [...] }; dry-run returns an empty list
|
|
88
|
-
// (the reconcile builds its OWN live read client so planning still sees drift).
|
|
89
|
-
async listDroplets({ tag } = {}) {
|
|
90
|
-
const qs = tag ? `?tag_name=${encodeURIComponent(tag)}&per_page=200` : '?per_page=200';
|
|
91
|
-
return this._req('GET', `/droplets${qs}`, null, { dryRunResult: { droplets: [] } });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ---- snapshots (via droplet actions) --------------------------------------
|
|
95
|
-
|
|
96
|
-
// Snapshot a running droplet. DO recommends powering off first for a clean
|
|
97
|
-
// snapshot; the orchestrator powers off then calls this. Returns an ACTION
|
|
98
|
-
// whose completion must be polled (getAction) before the droplet is destroyed.
|
|
99
|
-
async snapshotDroplet(dropletId, snapshotName) {
|
|
100
|
-
return this._req('POST', `/droplets/${dropletId}/actions`, { type: 'snapshot', name: snapshotName }, {
|
|
101
|
-
dryRunResult: { action: { id: `dryrun-action-${dropletId}`, status: 'completed', type: 'snapshot' } },
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async powerOffDroplet(dropletId) {
|
|
106
|
-
return this._req('POST', `/droplets/${dropletId}/actions`, { type: 'power_off' }, {
|
|
107
|
-
dryRunResult: { action: { id: `dryrun-poweroff-${dropletId}`, status: 'completed' } },
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async getAction(actionId) {
|
|
112
|
-
return this._req('GET', `/actions/${actionId}`, null, {
|
|
113
|
-
dryRunResult: { action: { id: actionId, status: 'completed' } },
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async deleteSnapshot(snapshotId) {
|
|
118
|
-
return this._req('DELETE', `/snapshots/${snapshotId}`, null, { dryRunResult: { deleted: true } });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async createDropletFromSnapshot({ name, region, size, snapshotId, userData, tags, sshKeys }) {
|
|
122
|
-
return this.createDroplet({ name, region, size, image: snapshotId, userData, tags, sshKeys });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Shared poll primitive behind waitForAction + settleDroplet (the box
|
|
126
|
-
// subsystem's two DO poll loops, unified per the #1092 cleanup). Calls step(i)
|
|
127
|
-
// each round; step returns { done:true, value } to stop, or a falsy value to keep
|
|
128
|
-
// polling. Returns the stopping value, or { timedOut:true } after `tries` rounds.
|
|
77
|
+
// Poll primitive behind settleDroplet (#1092). Calls step(i) each round; step
|
|
78
|
+
// returns { done:true, value } to stop, or a falsy value to keep polling.
|
|
79
|
+
// Returns the stopping value, or { timedOut:true } after `tries` rounds.
|
|
129
80
|
// `sleepFn` lets tests drive the loop with no real clock.
|
|
130
81
|
async _poll({ tries = 60, sleepMs = 5000, sleepFn } = {}, step) {
|
|
131
82
|
const sleep = sleepFn || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
@@ -137,26 +88,8 @@ class DoApi extends JsonHttpApi {
|
|
|
137
88
|
return { timedOut: true };
|
|
138
89
|
}
|
|
139
90
|
|
|
140
|
-
// Poll an action to completion. Returns true if 'completed', throws on 'errored'.
|
|
141
|
-
// sleepFn lets tests drive the loop deterministically (no real sleeping).
|
|
142
|
-
async waitForAction(actionId, { tries = 60, sleepMs = 5000, sleepFn } = {}) {
|
|
143
|
-
if (this.dryRun) return true;
|
|
144
|
-
const out = await this._poll({ tries, sleepMs, sleepFn }, async () => {
|
|
145
|
-
const { action } = await this.getAction(actionId);
|
|
146
|
-
if (action.status === 'completed') return { done: true, value: true };
|
|
147
|
-
if (action.status === 'errored') {
|
|
148
|
-
throw new DoApiError(`DO action ${actionId} errored`, { body: action });
|
|
149
|
-
}
|
|
150
|
-
return null;
|
|
151
|
-
});
|
|
152
|
-
if (out && out.timedOut) throw new DoApiError(`DO action ${actionId} did not complete in time`);
|
|
153
|
-
return out;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
91
|
// Wait until a freshly-created droplet is 'active' and return its public IPv4.
|
|
157
|
-
//
|
|
158
|
-
// share one primitive; scripts/gds/box.js calls it as a method. dry-run returns
|
|
159
|
-
// no IP (it never created a real droplet).
|
|
92
|
+
// dry-run returns no IP (it never created a real droplet).
|
|
160
93
|
async settleDroplet(dropletId, { tries = 60, sleepMs = 5000, sleepFn } = {}) {
|
|
161
94
|
if (this.dryRun) return { ip: null };
|
|
162
95
|
const out = await this._poll({ tries, sleepMs, sleepFn }, async () => {
|
|
@@ -41,6 +41,8 @@ const DOC_CLI_ALLOW = new Map([
|
|
|
41
41
|
'art-stage.js ships with the art-pipeline HOST module, which publish-manifest.js excludes from core — absent from a core-only checkout by design; the skill says so inline.'],
|
|
42
42
|
['.claude/skills/otb-tile-generate/SKILL.md::scripts/gds/art-stage.js',
|
|
43
43
|
'same as paint/SKILL.md — the art staging CLI travels with the excluded art-pipeline module, not the portable core.'],
|
|
44
|
+
['docs/session-logs/2026-09-10-task-1003507-the-box-that-could-not-be-reaped.md::scripts/gds/box.js',
|
|
45
|
+
'a dated session log — the historical record, never rewritten — of work on a CLI the dev-box removal deleted (task 1003893); it describes what was run then, not what to run now.'],
|
|
44
46
|
]);
|
|
45
47
|
|
|
46
48
|
// `node scripts/gds/foo.js` … up to the end of the line/segment, so the flags that
|
package/scripts/gds/doctor.js
CHANGED
|
@@ -167,7 +167,7 @@ function checkToolchain() {
|
|
|
167
167
|
// Check 3 — Git plumbing
|
|
168
168
|
// --------------------------------------------------------------------------
|
|
169
169
|
// Pure verdict for the hooks gate, extracted so the truth table is testable
|
|
170
|
-
// (tests/doctor_hooks.mjs)
|
|
170
|
+
// (tests/doctor_hooks.mjs).
|
|
171
171
|
//
|
|
172
172
|
// `.husky/` is INSTANCE topology, not shipped in the portable core (README
|
|
173
173
|
// "Secrets scanning"). So its absence is not a builder error: it means ADR 0043
|