@bongos/core 1.19.846 → 1.19.848
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 +27 -22
- package/Dockerfile +6 -1
- package/docs/adr/0304-routine-reads-run-without-a-prompt-and-anything-a-flag-can-turn-into-a-write-is-exact-form.md +84 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/self-host.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claude-materialize.js +31 -0
- package/scripts/gds/package-core.js +96 -16
- package/scripts/gds/provision-repo.js +40 -23
- package/scripts/gds/provision.js +3 -1
- package/src/module-api.js +1 -1
- package/tests/box_ship_permissions.mjs +130 -1
- package/tests/claude_materialize.mjs +1 -0
- package/tests/package_core.mjs +65 -0
- package/tests/provision.mjs +72 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.848",
|
|
6
|
+
"core_contract": "1.19.848",
|
|
7
|
+
"source_commit": "cec883a4362510c614bbf91576983ff03d6df2ce",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-17T19:
|
|
9
|
+
"built_at": "2026-09-17T19:52:14.256Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 496,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
14
|
"functional_verbatim": 2273,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2795,
|
|
20
|
+
"tree_sha256": "31f2347b41b6fb4c9974eb45d6b3b99d10ae8827fec75f36b3f239159ecbe87c",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
{
|
|
323
323
|
"path": "Dockerfile",
|
|
324
324
|
"mode": "0000644",
|
|
325
|
-
"sha256": "
|
|
325
|
+
"sha256": "e0876b6353244e13917d10510d9f60f61dd85983dee9b19a5da762c574cbae4b"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "GOVERNANCE.md",
|
|
@@ -2009,10 +2009,15 @@
|
|
|
2009
2009
|
"mode": "0000644",
|
|
2010
2010
|
"sha256": "62ffb9c1aeeaaafd5c51767f1ffff1c67adb3563e8d55ceb81ef77511184faa4"
|
|
2011
2011
|
},
|
|
2012
|
+
{
|
|
2013
|
+
"path": "docs/adr/0304-routine-reads-run-without-a-prompt-and-anything-a-flag-can-turn-into-a-write-is-exact-form.md",
|
|
2014
|
+
"mode": "0000644",
|
|
2015
|
+
"sha256": "104b5675fe52c1725dbbc78ab6fde9fd676b4020bf0e3f05411733c95d4342e0"
|
|
2016
|
+
},
|
|
2012
2017
|
{
|
|
2013
2018
|
"path": "docs/adr/README.md",
|
|
2014
2019
|
"mode": "0000644",
|
|
2015
|
-
"sha256": "
|
|
2020
|
+
"sha256": "61dd72c5f0d4157062fb93585252448e5493afd25cb014fb747bf0e20037a2ab"
|
|
2016
2021
|
},
|
|
2017
2022
|
{
|
|
2018
2023
|
"path": "docs/api-reference.md",
|
|
@@ -2577,7 +2582,7 @@
|
|
|
2577
2582
|
{
|
|
2578
2583
|
"path": "docs/module-api-changelog.md",
|
|
2579
2584
|
"mode": "0000644",
|
|
2580
|
-
"sha256": "
|
|
2585
|
+
"sha256": "ed51b2e95fbd3cfd4d5aed5a54aea53a9fc147ee0c7075dd4ecf9eda265ec50e"
|
|
2581
2586
|
},
|
|
2582
2587
|
{
|
|
2583
2588
|
"path": "docs/modules-contract.md",
|
|
@@ -2807,7 +2812,7 @@
|
|
|
2807
2812
|
{
|
|
2808
2813
|
"path": "docs/recipes/self-host.md",
|
|
2809
2814
|
"mode": "0000644",
|
|
2810
|
-
"sha256": "
|
|
2815
|
+
"sha256": "135f3942a01e50af87c4645f8920b25e0b779aac45cb5101b39e62cacfa59168"
|
|
2811
2816
|
},
|
|
2812
2817
|
{
|
|
2813
2818
|
"path": "docs/recipes/server-bring-up.md",
|
|
@@ -7902,12 +7907,12 @@
|
|
|
7902
7907
|
{
|
|
7903
7908
|
"path": "package-lock.json",
|
|
7904
7909
|
"mode": "0000644",
|
|
7905
|
-
"sha256": "
|
|
7910
|
+
"sha256": "c4a58e2d12411921c911607c5345671c595e34d7d7c8c51747d7ea5978cf21d4"
|
|
7906
7911
|
},
|
|
7907
7912
|
{
|
|
7908
7913
|
"path": "package.json",
|
|
7909
7914
|
"mode": "0000644",
|
|
7910
|
-
"sha256": "
|
|
7915
|
+
"sha256": "a3b15f291e7851a3578a38113485b76f54b695dca7bf97859954a26707a24c2a"
|
|
7911
7916
|
},
|
|
7912
7917
|
{
|
|
7913
7918
|
"path": "public-docs/index.html",
|
|
@@ -8217,7 +8222,7 @@
|
|
|
8217
8222
|
{
|
|
8218
8223
|
"path": "scripts/gds/claude-materialize.js",
|
|
8219
8224
|
"mode": "0000644",
|
|
8220
|
-
"sha256": "
|
|
8225
|
+
"sha256": "de8dcb51a250334621b104e99f7bc5f857f4422718c3c3ed8519cd5d0ebc8739"
|
|
8221
8226
|
},
|
|
8222
8227
|
{
|
|
8223
8228
|
"path": "scripts/gds/claude-md-template.js",
|
|
@@ -8762,7 +8767,7 @@
|
|
|
8762
8767
|
{
|
|
8763
8768
|
"path": "scripts/gds/package-core.js",
|
|
8764
8769
|
"mode": "0000644",
|
|
8765
|
-
"sha256": "
|
|
8770
|
+
"sha256": "4d439440fe0fe498382c7ee6e1684637a5bcad46ae67878c0d2c98d5de6cbbdd"
|
|
8766
8771
|
},
|
|
8767
8772
|
{
|
|
8768
8773
|
"path": "scripts/gds/paste-token.js",
|
|
@@ -8832,7 +8837,7 @@
|
|
|
8832
8837
|
{
|
|
8833
8838
|
"path": "scripts/gds/provision-repo.js",
|
|
8834
8839
|
"mode": "0000644",
|
|
8835
|
-
"sha256": "
|
|
8840
|
+
"sha256": "b04300749c4f119912ecfb2e6df4a6703f004130570e47a4e5bfaffacd7e2118"
|
|
8836
8841
|
},
|
|
8837
8842
|
{
|
|
8838
8843
|
"path": "scripts/gds/provision-teardown.js",
|
|
@@ -8847,7 +8852,7 @@
|
|
|
8847
8852
|
{
|
|
8848
8853
|
"path": "scripts/gds/provision.js",
|
|
8849
8854
|
"mode": "0000644",
|
|
8850
|
-
"sha256": "
|
|
8855
|
+
"sha256": "cbf44d415e3c5bb803e68af7b70ebfb08e7d0ab2388cc7f17a114512f29662e8"
|
|
8851
8856
|
},
|
|
8852
8857
|
{
|
|
8853
8858
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -9842,7 +9847,7 @@
|
|
|
9842
9847
|
{
|
|
9843
9848
|
"path": "src/module-api.js",
|
|
9844
9849
|
"mode": "0000644",
|
|
9845
|
-
"sha256": "
|
|
9850
|
+
"sha256": "f6dcadffbe81524b07bdc5821ae2cf1f5268eeea497354a09d0e7158673e3b85"
|
|
9846
9851
|
},
|
|
9847
9852
|
{
|
|
9848
9853
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10407,7 +10412,7 @@
|
|
|
10407
10412
|
{
|
|
10408
10413
|
"path": "tests/box_ship_permissions.mjs",
|
|
10409
10414
|
"mode": "0000644",
|
|
10410
|
-
"sha256": "
|
|
10415
|
+
"sha256": "063c94d759fa8a79b8c402145801ae2a4d9863bef591fb5adf57697e15effc39"
|
|
10411
10416
|
},
|
|
10412
10417
|
{
|
|
10413
10418
|
"path": "tests/box_sync.mjs",
|
|
@@ -10562,7 +10567,7 @@
|
|
|
10562
10567
|
{
|
|
10563
10568
|
"path": "tests/claude_materialize.mjs",
|
|
10564
10569
|
"mode": "0000644",
|
|
10565
|
-
"sha256": "
|
|
10570
|
+
"sha256": "42df08d261aaeb7cf94db3eef0e95b06af138d8d317b57f12dc4cb2c23d9f81a"
|
|
10566
10571
|
},
|
|
10567
10572
|
{
|
|
10568
10573
|
"path": "tests/clawback_confirmed_backlog.mjs",
|
|
@@ -12422,7 +12427,7 @@
|
|
|
12422
12427
|
{
|
|
12423
12428
|
"path": "tests/package_core.mjs",
|
|
12424
12429
|
"mode": "0000644",
|
|
12425
|
-
"sha256": "
|
|
12430
|
+
"sha256": "7d64e3c217274af48f24557d49deeb2cb54d7929bd9e939f15b3d3ceb757336c"
|
|
12426
12431
|
},
|
|
12427
12432
|
{
|
|
12428
12433
|
"path": "tests/parallel-load.mjs",
|
|
@@ -12697,7 +12702,7 @@
|
|
|
12697
12702
|
{
|
|
12698
12703
|
"path": "tests/provision.mjs",
|
|
12699
12704
|
"mode": "0000644",
|
|
12700
|
-
"sha256": "
|
|
12705
|
+
"sha256": "d763e2b5a10fbdd9a0e95fae493b524c7a52b437663125e3dbaf753435402e9c"
|
|
12701
12706
|
},
|
|
12702
12707
|
{
|
|
12703
12708
|
"path": "tests/provision_caddy_wiring.mjs",
|
package/Dockerfile
CHANGED
|
@@ -15,8 +15,13 @@ FROM node:22-bookworm-slim
|
|
|
15
15
|
|
|
16
16
|
# postgresql-client gives us `psql` for scripts/migrate.sh; bash runs it; tini
|
|
17
17
|
# is a proper PID 1 so signals/zombies are handled. curl powers HEALTHCHECK.
|
|
18
|
+
# git is the provisioning scaffold's only unmet tool (task 1003995): scaffoldStandaloneRepo
|
|
19
|
+
# materializes a child instance's repo with `git init`/`clone` → commit → push, so without it
|
|
20
|
+
# `provision.js run-intents --apply` cannot run here at all. The image still carries NO .git
|
|
21
|
+
# of its own (.dockerignore excludes it by design) — that half is package-core's
|
|
22
|
+
# --from-worktree rung, not an apt line.
|
|
18
23
|
RUN apt-get update \
|
|
19
|
-
&& apt-get install -y --no-install-recommends postgresql-client bash tini curl ca-certificates \
|
|
24
|
+
&& apt-get install -y --no-install-recommends postgresql-client bash tini curl ca-certificates git \
|
|
20
25
|
&& rm -rf /var/lib/apt/lists/*
|
|
21
26
|
|
|
22
27
|
WORKDIR /app
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ADR 0304 — Routine reads run without a prompt; anything a flag can turn into a write is exact-form
|
|
2
|
+
|
|
3
|
+
**Status:** accepted · **Date:** 2026-09-17 · **Task:** [task 1003992](https://cloudbongos.com/builders#/task/1003992) (goal 1000095 — BONGOS-V2)
|
|
4
|
+
|
|
5
|
+
**Related:** [ADR 0104](0104-trust-gds-api-channel-in-auto-mode.md) (the allow-list this extends — it named read wrappers as the next step) · [ADR 0016](0016-trust-boundary-server-enforced-permissions.md) (an allow-rule grants no authority) · [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) (git-guard is the cooperative layer) · [ADR 0302](0302-the-third-identical-failure-stops-the-session-and-asks.md) (same investigation)
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The shipped `.claude/settings.json` pre-allowed six commands. Much of what a session runs to look
|
|
10
|
+
around — `gh pr view`, `git fetch origin`, `node scripts/gds/status.js` — stopped and asked the
|
|
11
|
+
builder for permission. The 2026-09-17 investigation of a non-technical builder's stalls found
|
|
12
|
+
this is a prompt they cannot evaluate: they are asked to approve a command they do not read, many
|
|
13
|
+
times an hour, and the only safe answer they have is "yes". A prompt that is always answered yes
|
|
14
|
+
protects nothing and teaches the builder to stop reading prompts.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
Pre-allow the **read-only subset**, in the core's `.claude/settings.json` and — rule for rule, in
|
|
19
|
+
verb form — in the starter an instance receives (`STARTER_ALLOW`, task 1003962):
|
|
20
|
+
|
|
21
|
+
- `git status` as a prefix rule.
|
|
22
|
+
- `git branch` and `git fetch` as **exact forms only** (`git branch --list`, `--show-current`,
|
|
23
|
+
`-a`, `-vv`; `git fetch`, `git fetch origin`, `git fetch origin main`).
|
|
24
|
+
- `gh pr view | checks | list` and `gh run view | list` as prefix rules.
|
|
25
|
+
- The gds CLIs audited read-only under every flag they accept: `start`, `status`, `recall`, `me`,
|
|
26
|
+
`doctor`, `preflight`, `skill-preflight`, `ship-check`, `fitness`, `strand-watch`,
|
|
27
|
+
`stale-worktrees`, `read-session-export`. (`recall` POSTs, but to the search endpoint — a query.
|
|
28
|
+
`ship-check` and `fitness` run the repo's own checks, the same trust as the already-allowed
|
|
29
|
+
`ship.js`: a rule naming a script in the tree trusts the tree.) Audited and left out:
|
|
30
|
+
`backlog-review.js`, whose default walk POSTs promote / abandon, and `key-status.js`, whose
|
|
31
|
+
`--recheck` rewrites the local session file.
|
|
32
|
+
|
|
33
|
+
**The rule behind the split.** `Bash(x:*)` is a prefix rule: it allows every flag that can follow
|
|
34
|
+
`x`. So a command gets a prefix rule only when no flag turns it into a write or an exec.
|
|
35
|
+
`git fetch --upload-pack=<cmd>` runs a command, `git fetch origin x:main` moves a local branch, and
|
|
36
|
+
`git branch -D` deletes one — so those two subcommands are listed in the exact forms a session
|
|
37
|
+
actually uses, and anything else under them still asks.
|
|
38
|
+
|
|
39
|
+
**`git diff`, `git log` and `git show` get no rule at all.** The task asked for them, the first
|
|
40
|
+
cut had them as prefix rules, and review caught what that did — both reproduced on this repo:
|
|
41
|
+
`git log -1 --format=<text> --output=<path>` writes chosen content to any path (a
|
|
42
|
+
`.git/hooks/pre-push` is code execution), and `git diff --no-index -- /dev/null <path>` prints any
|
|
43
|
+
file, including the ones the `Read(...)` deny-list protects. Claude Code already runs the
|
|
44
|
+
read-only forms of these commands without a prompt in every permission mode, and checks their
|
|
45
|
+
flags when it does; an allow-rule is matched first and switches that check off. So for these three
|
|
46
|
+
the rule bought nothing and cost the flag check. By this ADR's own test they fail it.
|
|
47
|
+
|
|
48
|
+
**git-guard stays the backstop, and the lists are pinned.** A PreToolUse hook runs whether or not
|
|
49
|
+
a command is pre-allowed, so git-guard still blocks its four shapes. `tests/box_ship_permissions.mjs`
|
|
50
|
+
goes further: no allow-rule may *cover* a command git-guard blocks (blocked samples per guard rule;
|
|
51
|
+
a new guard rule fails the suite until it has one); no allow-rule may cover a write dressed as a
|
|
52
|
+
read (`--output`, `--no-index`, `--upload-pack`, a refspec); and the whole allow-list is pinned —
|
|
53
|
+
every rule is a named gds script or one of the thirteen git/gh strings in the test — so
|
|
54
|
+
widening it is a deliberate, reviewed edit.
|
|
55
|
+
|
|
56
|
+
## Consequences
|
|
57
|
+
|
|
58
|
+
- A session on a laptop or a box in auto mode reads PR and CI state, fetches, and runs the
|
|
59
|
+
read-only gds CLIs without interrupting the builder. `bypassPermissions` boxes are unaffected
|
|
60
|
+
(the rules are a no-op there).
|
|
61
|
+
- An allow-rule matches the command text, not the program: it is a convenience, never a security
|
|
62
|
+
boundary. That is why the dangerous cases are handled by *not writing the rule* rather than by an
|
|
63
|
+
`ask` or `deny` pattern for `--output` — Claude Code's own docs note such argument patterns are
|
|
64
|
+
evaded by quoting (`--out''put`).
|
|
65
|
+
- Rules are `Bash` only — the evidence is a hosted Linux box. `PowerShell(...)` twins are a
|
|
66
|
+
follow-up ([task 1004014](https://cloudbongos.com/builders#/task/1004014)), possible now that
|
|
67
|
+
[task 1003999](https://cloudbongos.com/builders#/task/1003999) has put that tool behind the same
|
|
68
|
+
guards; allowing first and guarding second was the wrong order.
|
|
69
|
+
- An instance that already has a `.claude/settings.json` keeps its own; only a fresh starter carries
|
|
70
|
+
the new rules (the never-clobber rule of task 1003962).
|
|
71
|
+
|
|
72
|
+
## Rejected
|
|
73
|
+
|
|
74
|
+
- **`Bash(git:*)` or `Bash(gh:*)`.** One rule instead of thirteen, and it pre-allows `git push`,
|
|
75
|
+
`git reset --hard`, `gh pr merge` and `gh api -X DELETE`.
|
|
76
|
+
- **Prefix rules for `git diff | log | show`, with `ask` rules for `--output` and `--no-index`.**
|
|
77
|
+
Evaluated in the order deny → ask → allow, so it works on the text a session normally writes — and
|
|
78
|
+
is evaded by quoting. The harness's built-in handling already covers the safe forms, so the pair
|
|
79
|
+
of rules would add a leaky guard to remove a prompt that does not exist.
|
|
80
|
+
- **Prefix rules for `git fetch` / `git branch --list`.** `git branch --list -D x` is covered by the
|
|
81
|
+
prefix and blocked by git-guard — harmless in practice (git refuses the combination) but it makes
|
|
82
|
+
"nothing the guard blocks is allowed" false, and that sentence is worth keeping true.
|
|
83
|
+
- **`bypassPermissions` everywhere** (task 1251's box default). It is a mode, not a list: a builder
|
|
84
|
+
who cycles modes or works from a laptop is back to prompts, and it removes the prompt for writes too.
|
package/docs/adr/README.md
CHANGED
|
@@ -395,3 +395,4 @@ This keeps the decision history honest and traceable.
|
|
|
395
395
|
| 0301 | [**A red PR names its failing tests in a commit status, and says so when it cannot see** ([task 1003988](https://cloudbongos.com/builders#/task/1003988), goal 1000095). One pull request took nine red CI runs in four hours because the builder box could not learn which test failed: without Checks:read GitHub nulls every check-run node, the server dropped the holes silently, and the red PR was reported as a review-or-pending wait. Now the merge state carries checks_unreadable and a red, unreadable rollup reads failing_check_unreadable; the unit workflow posts the failing test names as a unit-report commit STATUS (readable without the permission) from a separate job that holds the only write token and checks out no PR code; the server returns it as failing_tests, reduced to test-list vocabulary because the PR controls the workflow that wrote it.](0301-a-red-pr-names-its-failing-tests-in-a-commit-status.md) | GDS / shipping pipeline |
|
|
396
396
|
| 0302 | [**The third identical failure stops the session and asks** ([task 1003990](https://cloudbongos.com/builders#/task/1003990), goal 1000095). Sessions looped without noticing: one PR re-shipped into the same red check nine times, and 45 of 169 claim attempts were the same refusal retried minutes apart. A PostToolUse hook now counts walls per session — the signature is the `[otb-plain]` card's "What happened" line + the task id + the RED line, so a red check naming different failing tests each run reads as progress — and on the third identical one injects a single instruction: stop retrying, diagnose (path named), or /ask-for-help. Waiting cards ("Nothing to do…") are never counted; git-guard and worktree-guard count their own blocks through the same helper; it never blocks a tool call and echoes nothing a tool printed. Rejected: counting any repeated failing command (red→green is that), blocking the fourth attempt (a new gate, ADR 0162), server-side detection (cannot see guard blocks or speak into the session).](0302-the-third-identical-failure-stops-the-session-and-asks.md) | GDS / sessions / hooks |
|
|
397
397
|
| 0303 | [**Provisioning verifies the base Caddyfile, and never edits it** ([task 1002860](https://cloudbongos.com/builders#/task/1002860), goal 1000106, from idea 1000524). The runner assumed the box's `/etc/caddy/Caddyfile` carried the `on_demand_tls` tls-check ask and `import /etc/caddy/sites/*.caddy`, and reloaded with `allowFail` — a box missing either provisioned "successfully" and the instance was unreachable. It now reads Caddy's RESOLVED config (`caddy adapt`: unprivileged, no key access, indifferent to how the directives are written) before touching the box (the ask) and after writing the snippet (the domain is served ⇒ the import reaches it), refusing with the exact line to add, and the reload is loud. Rejected: editing the base file (`on_demand_tls` must sit inside the global block an append cannot place, and a bad write takes down the control plane's own apex); `caddy validate` (loads certificate keys, needs root).](0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md) | provisioning / infra |
|
|
398
|
+
| 0304 | [**Routine reads run without a prompt; anything a flag can turn into a write is exact-form** ([task 1003992](https://cloudbongos.com/builders#/task/1003992), goal 1000095). The shipped `.claude/settings.json` pre-allowed six commands, so every `gh pr view` or `git fetch` asked a non-technical builder to approve a command they cannot evaluate. The read-only subset is now pre-allowed — `git status`, `gh pr view/checks/list`, `gh run view/list` and the gds CLIs audited read-only under every flag as prefix rules; `git branch` and `git fetch` as exact forms only, because a flag turns either into a write or an exec — and the instance starter mirrors it rule for rule. `git diff/log/show` get NO rule: the harness already runs their read-only forms unprompted with a flag check, and a prefix rule switched that check off (`--output=<path>` writes anywhere, `diff --no-index` reads deny-listed files — both reproduced in review). A test pins the whole allow-list, that no rule covers a command git-guard blocks, and that no rule covers a write dressed as a read. Rejected: `Bash(git:*)`, prefix-plus-`ask` for diff/log/show (argument patterns are evaded by quoting), prefix rules for fetch/branch, `bypassPermissions` everywhere.](0304-routine-reads-run-without-a-prompt-and-anything-a-flag-can-turn-into-a-write-is-exact-form.md) | GDS / sessions / permissions |
|
|
@@ -2166,5 +2166,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2166
2166
|
landed since 1.19.844 with no explicit bump. run 35263096514. (task 1002620)
|
|
2167
2167
|
1.19.846 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2168
2168
|
landed since 1.19.845 with no explicit bump. run 35264882014. (task 1002620)
|
|
2169
|
+
1.19.847 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2170
|
+
landed since 1.19.846 with no explicit bump. run 35266409194. (task 1002620)
|
|
2171
|
+
1.19.848 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2172
|
+
landed since 1.19.847 with no explicit bump. run 35267380720. (task 1002620)
|
|
2169
2173
|
---------------------------------------------------------------------------
|
|
2170
2174
|
```
|
|
@@ -39,7 +39,7 @@ Author `config/branding.json` from the neutral starter — see [the branding con
|
|
|
39
39
|
|
|
40
40
|
**Or scaffold it with `bongos init`** ([task #1204](https://example.com/builders#/task/1204)): a short interview (or `bongos init --from spec.json` for a hands-free / CI run) that writes both config files, **seeds your first version + its done-when goal**, and **files the human-only kickoff steps** (register a GitHub OAuth app, first owner sign-in, repo binding, admission) as real tracked tasks — so a fresh instance has a claimable queue on day one instead of an empty hall. It never invents your branding (it points you at the fill prompt for that). Preview the whole plan with `bongos init --dry-run`.
|
|
41
41
|
|
|
42
|
-
**Production hosting, optional (the `provisioning` module — [ADR 0111](../adr/0111-instance-hosting-provisioning-module.md) §3).** If you enable the `provisioning` module and answer the interview's hosting questions (shape — co-tenant `$0` or a dedicated droplet — bring-your-own domain, credential ref, tier), init turns those answers into a real **provisioning request** (a queued `provision` intent the control-plane runner drains) instead of only filing a manual "production topology" task. **A queued request does nothing until a runner drains it:** on the control plane that is a systemd timer running `node scripts/gds/provision.js run-intents --apply`. This Docker image
|
|
42
|
+
**Production hosting, optional (the `provisioning` module — [ADR 0111](../adr/0111-instance-hosting-provisioning-module.md) §3).** If you enable the `provisioning` module and answer the interview's hosting questions (shape — co-tenant `$0` or a dedicated droplet — bring-your-own domain, credential ref, tier), init turns those answers into a real **provisioning request** (a queued `provision` intent the control-plane runner drains) instead of only filing a manual "production topology" task. **A queued request does nothing until a runner drains it:** on the control plane that is a systemd timer running `node scripts/gds/provision.js run-intents --apply`. This Docker image now carries the tools the scaffold needs — git, and a core it can package out of its own `.git`-less checkout (task 1003995) — but the standup leg still assumes root + systemd, which a container has not got (task 1003997), so on a compose self-host a new project waits indefinitely. The two irreducibly-human legs — the GitHub OAuth app and the first owner sign-in — stay guided/manual. If the module isn't enabled on the target instance, init falls back to the manual production-topology task, so nothing is silently skipped.
|
|
43
43
|
|
|
44
44
|
## Configuration surface
|
|
45
45
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.848",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.848",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.848",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -347,6 +347,37 @@ const STARTER_ALLOW = [
|
|
|
347
347
|
'Bash(bongos reauth:*)',
|
|
348
348
|
'Bash(bongos paste-token:*)',
|
|
349
349
|
'Bash(bongos exec scripts/gds/api.js:*)',
|
|
350
|
+
// The read-only subset (task 1003992, ADR 0304). `git branch` and `git fetch`
|
|
351
|
+
// are exact forms on purpose: a flag turns either into a write or an exec. No
|
|
352
|
+
// `git diff|log|show` rule, also on purpose: the harness already runs their
|
|
353
|
+
// read-only forms unprompted, and a prefix rule would switch off its flag check
|
|
354
|
+
// (`--output=<file>` writes anywhere, `diff --no-index` reads any file).
|
|
355
|
+
// tests/box_ship_permissions.mjs pins this list to the core's, rule for rule.
|
|
356
|
+
'Bash(bongos start:*)',
|
|
357
|
+
'Bash(bongos status:*)',
|
|
358
|
+
'Bash(bongos recall:*)',
|
|
359
|
+
'Bash(bongos exec scripts/gds/me.js:*)',
|
|
360
|
+
'Bash(bongos doctor:*)',
|
|
361
|
+
'Bash(bongos exec scripts/gds/preflight.js:*)',
|
|
362
|
+
'Bash(bongos exec scripts/gds/skill-preflight.js:*)',
|
|
363
|
+
'Bash(bongos exec scripts/gds/ship-check.js:*)',
|
|
364
|
+
'Bash(bongos exec scripts/gds/fitness.js:*)',
|
|
365
|
+
'Bash(bongos exec scripts/gds/strand-watch.js:*)',
|
|
366
|
+
'Bash(bongos exec scripts/gds/stale-worktrees.js:*)',
|
|
367
|
+
'Bash(bongos exec scripts/gds/read-session-export.js:*)',
|
|
368
|
+
'Bash(git status:*)',
|
|
369
|
+
'Bash(git branch --list)',
|
|
370
|
+
'Bash(git branch --show-current)',
|
|
371
|
+
'Bash(git branch -a)',
|
|
372
|
+
'Bash(git branch -vv)',
|
|
373
|
+
'Bash(git fetch)',
|
|
374
|
+
'Bash(git fetch origin)',
|
|
375
|
+
'Bash(git fetch origin main)',
|
|
376
|
+
'Bash(gh pr view:*)',
|
|
377
|
+
'Bash(gh pr checks:*)',
|
|
378
|
+
'Bash(gh pr list:*)',
|
|
379
|
+
'Bash(gh run view:*)',
|
|
380
|
+
'Bash(gh run list:*)',
|
|
350
381
|
];
|
|
351
382
|
|
|
352
383
|
// The starter an instance gets when the core root carries no settings.json — a
|
|
@@ -51,9 +51,22 @@
|
|
|
51
51
|
// The manifest ALSO records tarball.sha256 to verify the exact downloaded bytes.
|
|
52
52
|
//
|
|
53
53
|
// USAGE:
|
|
54
|
-
// node scripts/gds/package-core.js [--ref <git-ref>] [--out-dir dist]
|
|
54
|
+
// node scripts/gds/package-core.js [--ref <git-ref> | --from-worktree] [--out-dir dist]
|
|
55
55
|
// [--version <v>] [--droplet-ip <ip>] [--self-verify [--self-verify-boot]] [--publish [--live]]
|
|
56
56
|
// Defaults: --ref HEAD, --out-dir dist, --version = platform CORE_VERSION.
|
|
57
|
+
// --from-worktree build from the files ON DISK instead of a commit, for a core checkout
|
|
58
|
+
// that carries no git history — which is exactly what the shipped
|
|
59
|
+
// self-host image is (.dockerignore excludes .git by design), and the
|
|
60
|
+
// only way the provisioning scaffold inside it can vendor a core into a
|
|
61
|
+
// child instance (task 1003995). Selection is unchanged (isPublishable);
|
|
62
|
+
// the artifact simply has no commit to anchor to, so source_commit is null
|
|
63
|
+
// and the tree_sha256 pin is the whole lineage. It packages what is ON DISK,
|
|
64
|
+
// which in the image is exactly what `COPY . .` put there — so on a DEV
|
|
65
|
+
// checkout prefer the ref build: measured on this repo the two select an
|
|
66
|
+
// IDENTICAL 2793-file set, but a Windows working tree hands back CRLF where
|
|
67
|
+
// the blob is LF, which changes every text file's hash and would ship a
|
|
68
|
+
// CRLF shebang. That is why rung 4 in provision-repo.js is reached only
|
|
69
|
+
// when there is no history to build from.
|
|
57
70
|
// --self-verify after building, prove the artifact reproduces its pin (unpack the .tgz,
|
|
58
71
|
// recompute tree_sha256 == manifest + verify tarball.sha256). Fatal on mismatch.
|
|
59
72
|
// (task 1002218 / audit H3) — deterministic + network-free.
|
|
@@ -85,7 +98,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
85
98
|
|
|
86
99
|
// Reuse — never re-type. The core definition, the redaction, and the tree parser
|
|
87
100
|
// all already exist and are unit-tested at their homes.
|
|
88
|
-
const { isPublishable } = require(path.join(REPO_ROOT, 'scripts/gds/publish-manifest.js'));
|
|
101
|
+
const { isPublishable, PUBLISH_ALLOWLIST } = require(path.join(REPO_ROOT, 'scripts/gds/publish-manifest.js'));
|
|
89
102
|
const {
|
|
90
103
|
buildRedactionRules, collectSensitiveLiterals, findLeaks,
|
|
91
104
|
} = require(path.join(REPO_ROOT, 'scripts/gds/mirror-redact.js'));
|
|
@@ -544,6 +557,47 @@ function buildTar(entries) {
|
|
|
544
557
|
return zlib.gzipSync(tar, { level: 9 });
|
|
545
558
|
}
|
|
546
559
|
|
|
560
|
+
// ---- the git-LESS source: enumerate the working tree (task 1003995) --------
|
|
561
|
+
//
|
|
562
|
+
// Everything below the enumeration step is already git-free, so the ONE thing that
|
|
563
|
+
// makes package-core unrunnable without history is `git ls-tree`. That matters because
|
|
564
|
+
// the shipped self-host image IS a core checkout with no `.git` (.dockerignore excludes
|
|
565
|
+
// it by design), and the provisioning scaffold inside it has no other way to build the
|
|
566
|
+
// tarball it vendors into a child instance — the fourth rung of the ladder in
|
|
567
|
+
// scripts/gds/provision-repo.js.
|
|
568
|
+
//
|
|
569
|
+
// The walk starts at PUBLISH_ALLOWLIST's own entries rather than the repo root. The
|
|
570
|
+
// allowlist is already the single source of truth for what the core is, so starting
|
|
571
|
+
// there means node_modules/ and dist/ are never descended into at all. Selection still
|
|
572
|
+
// runs through isPublishable() in main(), so the denylist applies exactly as it does on
|
|
573
|
+
// the git path — this function only decides what is OFFERED, never what is kept.
|
|
574
|
+
//
|
|
575
|
+
// Rows match parseLsTree's shape ({ path, type, mode }) minus `sha`: there is no object
|
|
576
|
+
// to name, so main() reads these blobs off disk instead. Mode is derived the way git
|
|
577
|
+
// derives it — the executable bit and nothing else. fs injectable for tests.
|
|
578
|
+
function walkWorktreeEntries(root, { allow = PUBLISH_ALLOWLIST, fsImpl = fs } = {}) {
|
|
579
|
+
const out = [];
|
|
580
|
+
const addFile = (rel) => {
|
|
581
|
+
let st;
|
|
582
|
+
try { st = fsImpl.statSync(path.join(root, rel)); } catch { return; } // absent here → simply not in the core set
|
|
583
|
+
if (st.isFile()) out.push({ path: rel, type: 'blob', mode: (st.mode & 0o111) ? '100755' : '100644' });
|
|
584
|
+
};
|
|
585
|
+
const walk = (rel) => {
|
|
586
|
+
let children;
|
|
587
|
+
try { children = fsImpl.readdirSync(path.join(root, rel), { withFileTypes: true }); } catch { return; }
|
|
588
|
+
for (const child of children) {
|
|
589
|
+
const childRel = `${rel}/${child.name}`;
|
|
590
|
+
if (child.isDirectory()) walk(childRel);
|
|
591
|
+
else if (child.isFile()) addFile(childRel);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
for (const entry of allow) {
|
|
595
|
+
if (entry.endsWith('/')) walk(entry.replace(/\/$/, ''));
|
|
596
|
+
else addFile(entry);
|
|
597
|
+
}
|
|
598
|
+
return out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
599
|
+
}
|
|
600
|
+
|
|
547
601
|
// ---- git plumbing (impure) ------------------------------------------------
|
|
548
602
|
function git(args) {
|
|
549
603
|
return execFileSync('git', ['-C', REPO_ROOT, ...args], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
@@ -773,16 +827,22 @@ function selfVerify({ tgzPath, sidecar, outDir, boot }) {
|
|
|
773
827
|
}
|
|
774
828
|
|
|
775
829
|
function main() {
|
|
776
|
-
|
|
830
|
+
// --from-worktree: build from the files on disk instead of a commit (task 1003995).
|
|
831
|
+
// The lineage anchor of such a build is the tree hash alone — there is no commit to
|
|
832
|
+
// name — so source_commit is null and source_ref says where it came from.
|
|
833
|
+
const fromWorktree = process.argv.includes('--from-worktree');
|
|
834
|
+
const ref = fromWorktree ? '(worktree)' : arg('--ref', 'HEAD');
|
|
777
835
|
const outDir = path.resolve(REPO_ROOT, arg('--out-dir', 'dist'));
|
|
778
836
|
const version = arg('--version', coreVersion());
|
|
779
837
|
const contract = coreVersion();
|
|
780
838
|
const dropletIp = arg('--droplet-ip', process.env.DROPLET_IP || null);
|
|
781
839
|
|
|
782
840
|
// 1. Resolve the source ref → commit SHA (the lineage anchor of this artifact).
|
|
783
|
-
let sha;
|
|
784
|
-
|
|
785
|
-
|
|
841
|
+
let sha = null;
|
|
842
|
+
if (!fromWorktree) {
|
|
843
|
+
try { sha = git(['rev-parse', ref]).trim(); }
|
|
844
|
+
catch (e) { console.error(`refusing: could not resolve --ref "${ref}": ${e.message}`); process.exit(2); }
|
|
845
|
+
}
|
|
786
846
|
|
|
787
847
|
// 2. Enumerate + partition the tree through the manifest (single source of truth).
|
|
788
848
|
// package.json/package-lock.json are SYNTHESIZED (task 1882, ADR 0108 §2) —
|
|
@@ -790,7 +850,12 @@ function main() {
|
|
|
790
850
|
// ships them verbatim), but the core package needs its OWN identity + a pruned
|
|
791
851
|
// dependency set, not the game's. Pulled out of the generic per-file loop below.
|
|
792
852
|
const SYNTHESIZED_PATHS = new Set(['package.json', 'package-lock.json']);
|
|
793
|
-
const entries =
|
|
853
|
+
const entries = fromWorktree
|
|
854
|
+
? walkWorktreeEntries(REPO_ROOT)
|
|
855
|
+
: parseLsTree(git(['ls-tree', '-r', sha])).filter((e) => e.type === 'blob');
|
|
856
|
+
// A blob is named by its git object on the ref path and by its own path on the
|
|
857
|
+
// worktree path; everything downstream just wants the bytes.
|
|
858
|
+
const readBlob = (e) => (fromWorktree ? fs.readFileSync(path.join(REPO_ROOT, e.path)) : gitBuf(['cat-file', 'blob', e.sha]));
|
|
794
859
|
const selected = entries.filter((e) => isPublishable(e.path) && !SYNTHESIZED_PATHS.has(e.path));
|
|
795
860
|
// Drop npm-never-installed files (.gitignore, .npmignore, .npmrc) so the manifest +
|
|
796
861
|
// tree_sha256 pin describe exactly what `npm install` materializes — otherwise the
|
|
@@ -798,7 +863,10 @@ function main() {
|
|
|
798
863
|
// auto-rolls-back (idea 1000543). Done here, so they never reach the tarball or the pin.
|
|
799
864
|
const npmStripped = selected.filter((e) => isNpmStripped(e.path));
|
|
800
865
|
const publishable = selected.filter((e) => !isNpmStripped(e.path));
|
|
801
|
-
|
|
866
|
+
// The worktree walk is already allowlist-scoped and never offers the two synthesized
|
|
867
|
+
// paths, so only the ref path discounts them from what it enumerated.
|
|
868
|
+
const excludedCount = entries.length - selected.length - (fromWorktree ? 0 : SYNTHESIZED_PATHS.size);
|
|
869
|
+
console.error(`[package-core] ${ref}${sha ? ` = ${sha.slice(0, 12)}` : ''} — ${entries.length} files, ${publishable.length} in the core (+2 synthesized), ${excludedCount} excluded.`);
|
|
802
870
|
if (npmStripped.length) {
|
|
803
871
|
console.error(`[package-core] dropped ${npmStripped.length} npm-never-installed file(s) from the pin (npm strips these on install; idea 1000543): ${npmStripped.map((e) => e.path).join(', ')}`);
|
|
804
872
|
}
|
|
@@ -811,14 +879,24 @@ function main() {
|
|
|
811
879
|
// SOURCE files at this ref (never the live working tree, so --ref stays
|
|
812
880
|
// reproducible) — express+pg only, name @bongos/core, version = the
|
|
813
881
|
// platform version being packaged.
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
882
|
+
// On the worktree path there is no ref to stay reproducible against, so the two
|
|
883
|
+
// files come off disk beside everything else in the build.
|
|
884
|
+
const sourceRootFile = (rel) => {
|
|
885
|
+
if (fromWorktree) {
|
|
886
|
+
const abs = path.join(REPO_ROOT, rel);
|
|
887
|
+
return fs.existsSync(abs) ? fs.readFileSync(abs) : null;
|
|
888
|
+
}
|
|
889
|
+
const blob = entries.find((e) => e.path === rel);
|
|
890
|
+
return blob ? gitBuf(['cat-file', 'blob', blob.sha]) : null;
|
|
891
|
+
};
|
|
892
|
+
const pkgJsonRaw = sourceRootFile('package.json');
|
|
893
|
+
const pkgLockRaw = sourceRootFile('package-lock.json');
|
|
894
|
+
if (!pkgJsonRaw || !pkgLockRaw) {
|
|
895
|
+
console.error(`refusing: source package.json/package-lock.json not found at ${fromWorktree ? 'the repo root' : 'this ref'} — cannot synthesize the core package.`);
|
|
818
896
|
process.exit(1);
|
|
819
897
|
}
|
|
820
|
-
const sourcePkgJson = JSON.parse(
|
|
821
|
-
const sourcePkgLock = JSON.parse(
|
|
898
|
+
const sourcePkgJson = JSON.parse(pkgJsonRaw.toString('utf8'));
|
|
899
|
+
const sourcePkgLock = JSON.parse(pkgLockRaw.toString('utf8'));
|
|
822
900
|
const corePkgJson = buildPackageJson({ version, sourceDeps: sourcePkgJson.dependencies, sourceOverrides: sourcePkgJson.overrides });
|
|
823
901
|
const corePkgLock = prunePackageLock(sourcePkgLock, {
|
|
824
902
|
name: corePkgJson.name, version, rootRanges: { express: corePkgJson.dependencies.express, pg: corePkgJson.dependencies.pg },
|
|
@@ -865,7 +943,7 @@ function main() {
|
|
|
865
943
|
let stubCount = 0; // agent docs (CLAUDE.md/AGENTS.md) replaced with the brand-free stub (task 2121)
|
|
866
944
|
for (const e of publishable) {
|
|
867
945
|
let raw;
|
|
868
|
-
try { raw =
|
|
946
|
+
try { raw = readBlob(e); }
|
|
869
947
|
catch (err) { leaks.push({ path: e.path, label: `read-failed:${err.message}` }); continue; }
|
|
870
948
|
const { buf, leaks: fileLeaks, advisories: fileAdvisories, kind } =
|
|
871
949
|
processCoreFile({ pathRel: e.path, raw, rules, sensitiveLiterals, envSecrets, knownDocNames });
|
|
@@ -955,7 +1033,7 @@ function main() {
|
|
|
955
1033
|
|
|
956
1034
|
console.error('');
|
|
957
1035
|
console.error(`[package-core] Cloud Bongos core v${version} packaged (contract ^${contract}).`);
|
|
958
|
-
console.error(` source_commit ${sha}`);
|
|
1036
|
+
console.error(` source_commit ${sha || '(none — built from the working tree)'}`);
|
|
959
1037
|
console.error(` file_count ${filesMeta.length}`);
|
|
960
1038
|
console.error(` tree_sha256 ${treeHash}`);
|
|
961
1039
|
console.error(` tarball ${path.relative(REPO_ROOT, tgzPath)} (${(tgz.length / 1024).toFixed(1)} KiB, sha256 ${sidecar.tarball.sha256.slice(0, 12)}…)`);
|
|
@@ -1059,6 +1137,8 @@ module.exports = {
|
|
|
1059
1137
|
leakScanVerdict,
|
|
1060
1138
|
// task 1002217 (audit H4) — the fixed neutral floor injected into the doc-redaction + fail-closed gate
|
|
1061
1139
|
PRIOR_INSTANCE_FLOOR,
|
|
1140
|
+
// task 1003995 — --from-worktree's enumerator, for a core checkout with no git history
|
|
1141
|
+
walkWorktreeEntries,
|
|
1062
1142
|
};
|
|
1063
1143
|
|
|
1064
1144
|
if (require.main === module) main();
|
|
@@ -399,7 +399,17 @@ function installedCorePackDir({ instanceRoot, fsImpl = fs } = {}) {
|
|
|
399
399
|
return null;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
//
|
|
402
|
+
// Can package-core.js enumerate this checkout from GIT? It builds the core set from
|
|
403
|
+
// `git ls-tree` at a ref, so a checkout with no `.git` aborts at `git rev-parse HEAD` —
|
|
404
|
+
// and that is not a broken install, it is the SHIPPED SELF-HOST IMAGE: .dockerignore
|
|
405
|
+
// excludes .git by design, so /app is a complete core source tree with no history
|
|
406
|
+
// (task 1003995). The presence of `.git` — a directory in a clone, a file in a linked
|
|
407
|
+
// worktree — is the whole question. fs injectable for tests; read-only → dry-run safe.
|
|
408
|
+
function coreCheckoutHasGit({ repoRoot = REPO_ROOT, fsImpl = fs } = {}) {
|
|
409
|
+
return fsImpl.existsSync(path.join(repoRoot, '.git'));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Resolve a vendorable @bongos/core tarball — the four-rung ladder, freshest-correct
|
|
403
413
|
// first, shared by the fresh scaffold and the re-provision pin refresh (task 1003050):
|
|
404
414
|
// 1. a `file:`-pinned control plane reuses its own pinned tarball (task 1002275 —
|
|
405
415
|
// rebuilding via package-core.js aborts with an EMPTY core set when this runner
|
|
@@ -408,31 +418,37 @@ function installedCorePackDir({ instanceRoot, fsImpl = fs } = {}) {
|
|
|
408
418
|
// it has INSTALLED — the child pins the SAME, CURRENT core the hub runs, ending
|
|
409
419
|
// the frozen-old-tarball births (task 1002706, gap 13) without requiring the
|
|
410
420
|
// child's owner to hold a token for the still-private registry package;
|
|
411
|
-
// 3. a genuine core source checkout rebuilds fresh via package-core.js
|
|
421
|
+
// 3. a genuine core source checkout rebuilds fresh via package-core.js;
|
|
422
|
+
// 4. a core checkout with NO git history — the self-host image — packs its working
|
|
423
|
+
// tree via package-core.js --from-worktree (task 1003995). LAST because it is the
|
|
424
|
+
// only rung with no commit to anchor the artifact to: whenever history is there,
|
|
425
|
+
// rung 3's reproducible build is the better answer for the identical file set.
|
|
412
426
|
// Returns { tgz, packDest } — packDest (set only on rung 2) is a temp dir the CALLER
|
|
413
427
|
// must `rm -rf` once the tarball has been vendored.
|
|
414
|
-
function resolveVendorableCoreTarball({ instanceRoot, exec, log, coreV, tag = '[core]' }) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
428
|
+
function resolveVendorableCoreTarball({ instanceRoot, repoRoot = REPO_ROOT, exec, log, coreV, tag = '[core]' }) {
|
|
429
|
+
const pinned = installedCoreTarball({ instanceRoot });
|
|
430
|
+
if (pinned) {
|
|
431
|
+
log(` ${tag} reusing the pinned core tarball this control plane runs (${path.basename(pinned)}) — no source rebuild (task 1002275)`);
|
|
432
|
+
return { tgz: pinned, packDest: null };
|
|
433
|
+
}
|
|
434
|
+
const packSrc = installedCorePackDir({ instanceRoot });
|
|
435
|
+
if (packSrc) {
|
|
436
|
+
// Keyed by VERSION, not slug: no request-derived value enters this command, and
|
|
437
|
+
// two concurrent scaffolds of the same core pack identical bytes — collision-safe.
|
|
438
|
+
const packDest = path.join(os.tmpdir(), `bongos-core-pack-${packSrc.version}`);
|
|
439
|
+
log(` ${tag} packing the registry-installed core this control plane runs (v${packSrc.version}) — no source rebuild (task 1002706)`);
|
|
440
|
+
exec(`mkdir -p ${packDest}`);
|
|
441
|
+
exec(`npm pack ${packSrc.dir} --pack-destination ${packDest}`);
|
|
442
|
+
return { tgz: path.join(packDest, packSrc.tarballName), packDest };
|
|
443
|
+
}
|
|
444
|
+
if (coreCheckoutHasGit({ repoRoot })) {
|
|
445
|
+
log(` ${tag} building a fresh vendored tarball from source (package-core.js)`);
|
|
446
|
+
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
419
447
|
} else {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// Keyed by VERSION, not slug: no request-derived value enters this command, and
|
|
423
|
-
// two concurrent scaffolds of the same core pack identical bytes — collision-safe.
|
|
424
|
-
packDest = path.join(os.tmpdir(), `bongos-core-pack-${packSrc.version}`);
|
|
425
|
-
log(` ${tag} packing the registry-installed core this control plane runs (v${packSrc.version}) — no source rebuild (task 1002706)`);
|
|
426
|
-
exec(`mkdir -p ${packDest}`);
|
|
427
|
-
exec(`npm pack ${packSrc.dir} --pack-destination ${packDest}`);
|
|
428
|
-
tgz = path.join(packDest, packSrc.tarballName);
|
|
429
|
-
} else {
|
|
430
|
-
log(` ${tag} building a fresh vendored tarball from source (package-core.js)`);
|
|
431
|
-
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
432
|
-
tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
|
|
433
|
-
}
|
|
448
|
+
log(` ${tag} this checkout IS the core and carries no git history — packing the working tree (package-core.js --from-worktree, task 1003995)`);
|
|
449
|
+
exec(`node scripts/gds/package-core.js --from-worktree`);
|
|
434
450
|
}
|
|
435
|
-
return { tgz, packDest };
|
|
451
|
+
return { tgz: path.join(repoRoot, 'dist', `bongos-core-${coreV}.tgz`), packDest: null };
|
|
436
452
|
}
|
|
437
453
|
|
|
438
454
|
// The onboard mode a provisioning row carries (migration provisioning_004, task 1002562).
|
|
@@ -633,7 +649,7 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
633
649
|
const spec = instanceInitSpec(inst, { ownerLogin });
|
|
634
650
|
const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
|
|
635
651
|
writeFile(specPath, JSON.stringify(spec, null, 2));
|
|
636
|
-
// Vendored tarball — resolveVendorableCoreTarball's
|
|
652
|
+
// Vendored tarball — resolveVendorableCoreTarball's four-rung ladder (shared
|
|
637
653
|
// with the re-provision pin refresh, task 1003050).
|
|
638
654
|
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
639
655
|
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
|
|
@@ -1007,6 +1023,7 @@ module.exports = {
|
|
|
1007
1023
|
ownerLoginOf,
|
|
1008
1024
|
asPostgres,
|
|
1009
1025
|
buildCorePinRefreshCommit,
|
|
1026
|
+
coreCheckoutHasGit,
|
|
1010
1027
|
dbCreateCmd,
|
|
1011
1028
|
dbName,
|
|
1012
1029
|
dbRoleCmd,
|
package/scripts/gds/provision.js
CHANGED
|
@@ -50,7 +50,7 @@ const cp = require('node:child_process');
|
|
|
50
50
|
const crypto = require('node:crypto');
|
|
51
51
|
|
|
52
52
|
const { CONFIG, MANIFEST_UA, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
|
|
53
|
-
const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, dbCreateCmd, dbName, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd, ensureInstanceUserCmd, generateDbPassword, grantInstanceRepoReadCmd, instanceDbRole, instanceStateDir, instanceUser, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
53
|
+
const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, coreCheckoutHasGit, dbCreateCmd, dbName, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd, ensureInstanceUserCmd, generateDbPassword, grantInstanceRepoReadCmd, instanceDbRole, instanceStateDir, instanceUser, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
54
54
|
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
55
55
|
const { caddyBlock, caddySnippetPath, checkCaddyWiring, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
56
56
|
const { capacityPreflight, statusCapacityLine } = require('./provision-capacity.js');
|
|
@@ -1457,6 +1457,8 @@ module.exports = {
|
|
|
1457
1457
|
parseTargetRef, instanceRepoRemote, instanceInitSpec, installedCoreTarball, installedCorePackDir, scaffoldStandaloneRepo, coreVersionSafe,
|
|
1458
1458
|
// re-provision core-pin refresh (gap-list W3, task 1003050)
|
|
1459
1459
|
resolveVendorableCoreTarball, planCorePinRefresh, safeVersionLabel, buildCorePinRefreshCommit, refreshStandaloneCorePin,
|
|
1460
|
+
// the git-less core checkout — the self-host image's rung (task 1003995)
|
|
1461
|
+
coreCheckoutHasGit,
|
|
1460
1462
|
// adopt vs greenfield onboarding (ADR 0121, task 1002562)
|
|
1461
1463
|
onboardMode, alreadyScaffolded,
|
|
1462
1464
|
// private-repo deploy key (ADR 0176, task 1002735)
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.848'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -12,11 +12,15 @@ import { strict as assert } from 'node:assert';
|
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import {
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { skipUnlessPresent, requireHostModule } from './helpers.mjs';
|
|
16
17
|
|
|
17
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const settingsPath = path.join(__dirname, '..', '.claude', 'settings.json');
|
|
19
20
|
skipUnlessPresent('.claude/settings.json');
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const guard = requireHostModule(require, '../.claude/hooks/git-guard.js');
|
|
23
|
+
const { STARTER_ALLOW, bongosify } = require('../scripts/gds/claude-materialize.js');
|
|
20
24
|
|
|
21
25
|
let passed = 0, failed = 0;
|
|
22
26
|
function test(name, fn) {
|
|
@@ -68,6 +72,131 @@ test('every allow-rule points at a script that actually exists (no inert rules)'
|
|
|
68
72
|
}
|
|
69
73
|
});
|
|
70
74
|
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// task 1003992 — the read-only subset (ADR 0304). A non-technical builder was
|
|
77
|
+
// interrupted for every `gh pr view` and `git fetch`; those are now pre-allowed.
|
|
78
|
+
// An allow-rule skips the prompt, so the list must never reach a command
|
|
79
|
+
// git-guard blocks, and must stay read-only.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
// How Claude Code reads a Bash rule: `Bash(x:*)` covers x and anything starting
|
|
83
|
+
// "x "; `Bash(x)` covers exactly x. A chained command is covered only when every
|
|
84
|
+
// part is, so `git status && git stash` is not covered by a `git status` rule.
|
|
85
|
+
function ruleCovers(rule, part) {
|
|
86
|
+
const m = rule.match(/^Bash\((.*?)(:\*)?\)$/);
|
|
87
|
+
if (!m) return false;
|
|
88
|
+
return part === m[1] || (Boolean(m[2]) && part.startsWith(`${m[1]} `));
|
|
89
|
+
}
|
|
90
|
+
function allowCovers(allow, command) {
|
|
91
|
+
const parts = command.split(/&&|\|\||[;|&\n]/).map((s) => s.trim()).filter(Boolean);
|
|
92
|
+
return parts.every((part) => allow.some((rule) => ruleCovers(rule, part)));
|
|
93
|
+
}
|
|
94
|
+
const isBlocked = (command) => guard.decide({ tool_name: 'Bash', tool_input: { command } }, {}).block;
|
|
95
|
+
|
|
96
|
+
// At least one sample per git-guard rule, keyed by the rule's own name.
|
|
97
|
+
const BLOCKED_SAMPLES = {
|
|
98
|
+
'git stash': ['git stash', 'git stash pop', 'git stash push -m wip', 'git status && git stash'],
|
|
99
|
+
'git worktree remove': ['git worktree remove ../wt', 'git worktree remove --force ../wt'],
|
|
100
|
+
'git push origin main': ['git push origin main', 'git push -u origin HEAD:main', 'git push --force origin task-1:main'],
|
|
101
|
+
'git branch -D': ['git branch -D task-1', 'git branch --list -D task-1', 'git branch -a -D task-1'],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
test('every git-guard rule has blocked samples here (a new rule forces a new sample)', () => {
|
|
105
|
+
for (const [name] of guard.RULES) {
|
|
106
|
+
assert.ok((BLOCKED_SAMPLES[name] || []).length, `no blocked sample for git-guard rule "${name}"`);
|
|
107
|
+
}
|
|
108
|
+
for (const samples of Object.values(BLOCKED_SAMPLES)) {
|
|
109
|
+
for (const cmd of samples) assert.ok(isBlocked(cmd), `sample is not actually blocked by git-guard: ${cmd}`);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('no allow-rule covers a command git-guard blocks', () => {
|
|
114
|
+
const allow = settings.permissions.allow;
|
|
115
|
+
for (const rule of allow) {
|
|
116
|
+
const text = rule.replace(/^Bash\(/, '').replace(/(:\*)?\)$/, '');
|
|
117
|
+
assert.ok(!isBlocked(text), `allow-rule is itself a git-guard-blocked command: ${rule}`);
|
|
118
|
+
}
|
|
119
|
+
for (const cmd of Object.values(BLOCKED_SAMPLES).flat()) {
|
|
120
|
+
assert.ok(!allowCovers(allow, cmd), `an allow-rule covers a git-guard-blocked command: ${cmd}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the check has teeth: a too-broad git rule IS caught', () => {
|
|
125
|
+
const samples = Object.values(BLOCKED_SAMPLES).flat();
|
|
126
|
+
for (const broad of ['Bash(git:*)', 'Bash(git stash:*)', 'Bash(git worktree:*)', 'Bash(git push:*)', 'Bash(git branch:*)', 'Bash(git branch --list:*)']) {
|
|
127
|
+
assert.ok(samples.some((cmd) => allowCovers([broad], cmd)), `${broad} should cover a blocked sample`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// The whole git/gh allow-list, pinned. A flag can turn a read into a write or an
|
|
132
|
+
// exec (`git fetch --upload-pack=<cmd>`, `git fetch origin x:main`, `git branch -D`),
|
|
133
|
+
// so `branch` and `fetch` appear only as the exact commands a session runs.
|
|
134
|
+
//
|
|
135
|
+
// NO `git diff|log|show` rule, on purpose. Claude Code already runs their read-only
|
|
136
|
+
// forms without a prompt in every mode and checks their flags; a prefix rule would
|
|
137
|
+
// switch that check off (see WRITES_DRESSED_AS_READS below — both were reproduced).
|
|
138
|
+
const GIT_GH_RULES = [
|
|
139
|
+
'Bash(git status:*)',
|
|
140
|
+
'Bash(git branch --list)', 'Bash(git branch --show-current)', 'Bash(git branch -a)', 'Bash(git branch -vv)',
|
|
141
|
+
'Bash(git fetch)', 'Bash(git fetch origin)', 'Bash(git fetch origin main)',
|
|
142
|
+
'Bash(gh pr view:*)', 'Bash(gh pr checks:*)', 'Bash(gh pr list:*)', 'Bash(gh run view:*)', 'Bash(gh run list:*)',
|
|
143
|
+
];
|
|
144
|
+
// gds CLIs audited read-only under EVERY flag they take — a prefix rule allows them all.
|
|
145
|
+
// Audited and left OUT: backlog-review.js (POSTs from its default walk) and
|
|
146
|
+
// key-status.js (`--recheck` rewrites the local session file).
|
|
147
|
+
const READ_ONLY_CLIS = ['start', 'status', 'recall', 'me', 'doctor', 'preflight', 'skill-preflight',
|
|
148
|
+
'ship-check', 'fitness', 'strand-watch', 'stale-worktrees', 'read-session-export'];
|
|
149
|
+
const BLESSED_WRITERS = ['ship', 'claim', 'reauth', 'release', 'paste-token', 'api']; // tasks 1051 / 1716
|
|
150
|
+
|
|
151
|
+
const SCRIPT_RULE = /^Bash\(node scripts\/gds\/([a-z-]+)\.js:\*\)$/;
|
|
152
|
+
|
|
153
|
+
test('every allow-rule that is not a gds script is in the pinned git/gh set (adding one is a deliberate edit here)', () => {
|
|
154
|
+
// Catches `Bash(gh:*)`, `Bash(gh *)`, `Bash(git -C x status:*)`, a bare `Bash` — anything unlisted.
|
|
155
|
+
const rest = settings.permissions.allow.filter((rule) => !SCRIPT_RULE.test(rule));
|
|
156
|
+
assert.deepEqual([...rest].sort(), [...GIT_GH_RULES].sort());
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// "Read" commands whose flags write a file, read one the Read deny-list protects,
|
|
160
|
+
// run a command, or move a branch. None may ever run without a prompt.
|
|
161
|
+
const WRITES_DRESSED_AS_READS = [
|
|
162
|
+
"git log -1 --format='#!/bin/sh' --output=.git/hooks/pre-push",
|
|
163
|
+
'git show --output=/tmp/x HEAD',
|
|
164
|
+
'git diff --output=/tmp/x',
|
|
165
|
+
'git diff --no-index -- /dev/null .env',
|
|
166
|
+
'git fetch --upload-pack=./x origin',
|
|
167
|
+
'git fetch origin task-1:main',
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
test('no allow-rule lets a write dressed as a read through (--output, --no-index, --upload-pack, a refspec)', () => {
|
|
171
|
+
for (const cmd of WRITES_DRESSED_AS_READS) {
|
|
172
|
+
assert.ok(!allowCovers(settings.permissions.allow, cmd), `runs without a prompt: ${cmd}`);
|
|
173
|
+
}
|
|
174
|
+
// Teeth: the prefix rules this list deliberately does NOT carry would let them through.
|
|
175
|
+
const tempting = ['Bash(git log:*)', 'Bash(git show:*)', 'Bash(git diff:*)', 'Bash(git fetch:*)'];
|
|
176
|
+
for (const cmd of WRITES_DRESSED_AS_READS) assert.ok(allowCovers(tempting, cmd), `teeth: ${cmd}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('every gds-script allow-rule is a blessed writer or an audited read-only CLI', () => {
|
|
180
|
+
const known = new Set([...BLESSED_WRITERS, ...READ_ONLY_CLIS]);
|
|
181
|
+
for (const rule of settings.permissions.allow) {
|
|
182
|
+
const m = rule.match(SCRIPT_RULE);
|
|
183
|
+
if (m) assert.ok(known.has(m[1]), `${m[1]}.js is pre-allowed but not audited here — a prefix rule allows every flag it takes`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('the read-only subset is pre-allowed', () => {
|
|
188
|
+
const allow = settings.permissions.allow;
|
|
189
|
+
for (const cmd of ['git status', 'git status --short', 'git branch --list', 'git fetch origin',
|
|
190
|
+
'gh pr view 922', 'gh pr checks 922', 'gh pr list', 'gh run view 1 --log-failed', 'gh run list --limit 5',
|
|
191
|
+
...READ_ONLY_CLIS.map((s) => `node scripts/gds/${s}.js`)]) {
|
|
192
|
+
assert.ok(allowCovers(allow, cmd), `should run without a prompt: ${cmd}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('the instance starter mirrors this list in verb form (claude-materialize.js STARTER_ALLOW)', () => {
|
|
197
|
+
assert.deepEqual([...STARTER_ALLOW].sort(), settings.permissions.allow.map((rule) => bongosify(rule)).sort());
|
|
198
|
+
});
|
|
199
|
+
|
|
71
200
|
console.log('');
|
|
72
201
|
console.log(`box_ship_permissions: ${passed} passed, ${failed} failed`);
|
|
73
202
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -729,6 +729,7 @@ t('every allow-rule names a verb the CLI actually registers', () => {
|
|
|
729
729
|
// is silently inert — it looks like a permission and grants nothing, so the
|
|
730
730
|
// builder still meets the classifier and nobody can see why.
|
|
731
731
|
for (const rule of m.STARTER_ALLOW) {
|
|
732
|
+
if (!rule.startsWith('Bash(bongos ')) continue; // the git/gh reads (task 1003992) name no verb
|
|
732
733
|
const inner = rule.replace(/^Bash\(/, '').replace(/:\*\)$/, '');
|
|
733
734
|
const verb = inner.split(' ')[1];
|
|
734
735
|
assert.ok(cliVerbs[verb], `allow-rule "${rule}" names "${verb}", which is not a registered bongos verb`);
|
package/tests/package_core.mjs
CHANGED
|
@@ -42,8 +42,11 @@ const {
|
|
|
42
42
|
scanArtifactForLeaks, PRIOR_INSTANCE_LITERALS,
|
|
43
43
|
// task 1002217 (audit H4) — the fixed neutral floor
|
|
44
44
|
PRIOR_INSTANCE_FLOOR,
|
|
45
|
+
// task 1003995 — --from-worktree's enumerator, for a core checkout with no git history
|
|
46
|
+
walkWorktreeEntries,
|
|
45
47
|
} = m;
|
|
46
48
|
const { buildRedactionRules, collectSensitiveLiterals, redactText, findLeaks } = require('../scripts/gds/mirror-redact.js');
|
|
49
|
+
const { isPublishable } = require('../scripts/gds/publish-manifest.js');
|
|
47
50
|
|
|
48
51
|
// A synthetic instance brand for the classification tests (never the real OTB literals).
|
|
49
52
|
const TEST_BRANDING = {
|
|
@@ -747,5 +750,67 @@ t('idea 1000543 (the fix): the isNpmStripped filter makes the pin reproduce agai
|
|
|
747
750
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
748
751
|
});
|
|
749
752
|
|
|
753
|
+
// ── --from-worktree: the git-less enumeration (task 1003995) ─────────────────────
|
|
754
|
+
// package-core builds the core set from `git ls-tree`, so a core checkout with no .git —
|
|
755
|
+
// the shipped self-host image, where .dockerignore excludes it by design — cannot build
|
|
756
|
+
// the tarball its provisioning scaffold vendors into a child instance. walkWorktreeEntries
|
|
757
|
+
// is the replacement source. It decides only what is OFFERED; isPublishable still decides
|
|
758
|
+
// what is KEPT, so the core set stays defined in exactly one place.
|
|
759
|
+
function worktreeFixture() {
|
|
760
|
+
const root = mkdtempSync(join(tmpdir(), 'wt-core-'));
|
|
761
|
+
const put = (rel, body = 'x') => {
|
|
762
|
+
mkdirSync(join(root, dirname(rel)), { recursive: true });
|
|
763
|
+
writeFileSync(join(root, rel), body);
|
|
764
|
+
};
|
|
765
|
+
put('src/module-api.js');
|
|
766
|
+
put('src/bongos/tasks.js'); // nested, to prove the walk recurses
|
|
767
|
+
put('scripts/gds/package-core.js');
|
|
768
|
+
put('CLAUDE.md'); // an allowlisted FILE entry, not a subtree
|
|
769
|
+
put('node_modules/@bongos/core/index.js'); // huge + never core: must never be descended
|
|
770
|
+
put('dist/bongos-core-1.0.0.tgz'); // this tool's own output
|
|
771
|
+
put('config/branding.json'); // instance identity: outside every allowlist root
|
|
772
|
+
return { root, cleanup: () => rmSync(root, { recursive: true, force: true }) };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
t('walkWorktreeEntries: offers the allowlisted tree and never descends node_modules/ or dist/', () => {
|
|
776
|
+
const { root, cleanup } = worktreeFixture();
|
|
777
|
+
try {
|
|
778
|
+
const paths = walkWorktreeEntries(root).map((e) => e.path);
|
|
779
|
+
assert.deepEqual(
|
|
780
|
+
paths,
|
|
781
|
+
['CLAUDE.md', 'scripts/gds/package-core.js', 'src/bongos/tasks.js', 'src/module-api.js'],
|
|
782
|
+
`unexpected set: ${paths.join(', ')}`,
|
|
783
|
+
);
|
|
784
|
+
assert.ok(!paths.some((p) => p.startsWith('node_modules/')), 'node_modules is never walked');
|
|
785
|
+
assert.ok(!paths.some((p) => p.startsWith('dist/')), 'the packager never re-enumerates its own output');
|
|
786
|
+
assert.ok(!paths.includes('config/branding.json'), 'instance identity is outside every allowlist root');
|
|
787
|
+
} finally { cleanup(); }
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
t('walkWorktreeEntries: rows match the git-tree shape — sorted blobs carrying a mode', () => {
|
|
791
|
+
const { root, cleanup } = worktreeFixture();
|
|
792
|
+
try {
|
|
793
|
+
const entries = walkWorktreeEntries(root);
|
|
794
|
+
assert.deepEqual([...entries].sort((a, b) => (a.path < b.path ? -1 : 1)), entries, 'sorted, so the pin is stable');
|
|
795
|
+
for (const e of entries) {
|
|
796
|
+
assert.equal(e.type, 'blob', 'only files, matching the ls-tree filter main() applies');
|
|
797
|
+
assert.ok(['100644', '100755'].includes(e.mode), `mode must be a git file mode, got ${e.mode}`);
|
|
798
|
+
}
|
|
799
|
+
} finally { cleanup(); }
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
t('walkWorktreeEntries: a DENIED path inside an allowlisted subtree is still offered — the denylist is main()’s job, not the walk’s', () => {
|
|
803
|
+
const { root, cleanup } = worktreeFixture();
|
|
804
|
+
try {
|
|
805
|
+
// A host-feature script lives under the allowlisted scripts/ root and is denied by name.
|
|
806
|
+
// If the walk pre-filtered it, the two enumeration paths would be applying different rules.
|
|
807
|
+
const denied = 'scripts/gds/art-stage.js';
|
|
808
|
+
writeFileSync(join(root, denied), 'x');
|
|
809
|
+
const offered = walkWorktreeEntries(root).map((e) => e.path);
|
|
810
|
+
assert.ok(offered.includes(denied), 'the walk offers it…');
|
|
811
|
+
assert.equal(isPublishable(denied), false, '…and isPublishable is what drops it');
|
|
812
|
+
} finally { cleanup(); }
|
|
813
|
+
});
|
|
814
|
+
|
|
750
815
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
751
816
|
process.exit(failed ? 1 : 0);
|
package/tests/provision.mjs
CHANGED
|
@@ -816,6 +816,78 @@ t('installedCorePackDir: a registry-pinned consumer with an installed core → i
|
|
|
816
816
|
[reg, filePinned, uninstalled, bare2, alt2].forEach((d) => rmSync(d, { recursive: true, force: true }));
|
|
817
817
|
});
|
|
818
818
|
|
|
819
|
+
// ── the git-less core checkout: rung 4 (task 1003995) ────────────────────────────
|
|
820
|
+
// The shipped self-host image is a complete core source tree with NO .git (.dockerignore
|
|
821
|
+
// excludes it by design), so package-core.js's `git ls-tree` enumeration has nothing to
|
|
822
|
+
// read and the scaffold inside the container could never build the tarball it vendors
|
|
823
|
+
// into a child. Rung 4 packs the working tree instead.
|
|
824
|
+
t('coreCheckoutHasGit: a clone (.git dir) and a linked worktree (.git file) both have history; the image does not', () => {
|
|
825
|
+
const clone = mkdtempSync(join(tmpdir(), 'cchg-clone-'));
|
|
826
|
+
mkdirSync(join(clone, '.git'));
|
|
827
|
+
assert.equal(P.coreCheckoutHasGit({ repoRoot: clone }), true, 'a clone carries a .git DIRECTORY');
|
|
828
|
+
|
|
829
|
+
// A linked worktree's .git is a FILE pointing at the real gitdir — every claim in this
|
|
830
|
+
// project is worked in one, so treating it as history-less would take the wrong rung on
|
|
831
|
+
// the machine that runs these tests.
|
|
832
|
+
const wt = mkdtempSync(join(tmpdir(), 'cchg-worktree-'));
|
|
833
|
+
writeFileSync(join(wt, '.git'), 'gitdir: /somewhere/.git/worktrees/task-1\n');
|
|
834
|
+
assert.equal(P.coreCheckoutHasGit({ repoRoot: wt }), true, 'a linked worktree carries a .git FILE');
|
|
835
|
+
|
|
836
|
+
const image = mkdtempSync(join(tmpdir(), 'cchg-image-'));
|
|
837
|
+
assert.equal(P.coreCheckoutHasGit({ repoRoot: image }), false, 'the self-host image has no .git at all');
|
|
838
|
+
|
|
839
|
+
[clone, wt, image].forEach((d) => rmSync(d, { recursive: true, force: true }));
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
t('resolveVendorableCoreTarball: the ladder picks file: pin → registry pack → git source → worktree, in that order', () => {
|
|
843
|
+
// One helper per rung so the ONLY thing varying between cases is which shape the
|
|
844
|
+
// control-plane root presents. Asserting on the recorded commands is what proves the
|
|
845
|
+
// ORDER: a rung that fires early leaves the later rung's command unrun.
|
|
846
|
+
const run = (instanceRoot, repoRoot) => {
|
|
847
|
+
const cmds = [];
|
|
848
|
+
const res = P.resolveVendorableCoreTarball({
|
|
849
|
+
instanceRoot, repoRoot, exec: (c) => { cmds.push(c); }, log: () => {}, coreV: '9.9.9',
|
|
850
|
+
});
|
|
851
|
+
return { cmds, ...res };
|
|
852
|
+
};
|
|
853
|
+
const gitRoot = mkdtempSync(join(tmpdir(), 'rvct-git-'));
|
|
854
|
+
mkdirSync(join(gitRoot, '.git'));
|
|
855
|
+
const bareRoot = mkdtempSync(join(tmpdir(), 'rvct-bare-')); // no .git → the image shape
|
|
856
|
+
|
|
857
|
+
// Rung 1 — a file: pin wins even from a git-less root: nothing is rebuilt at all.
|
|
858
|
+
const filePinned = mkdtempSync(join(tmpdir(), 'rvct-file-'));
|
|
859
|
+
mkdirSync(join(filePinned, 'vendor'));
|
|
860
|
+
writeFileSync(join(filePinned, 'vendor', 'bongos-core-1.2.3.tgz'), 'x');
|
|
861
|
+
writeFileSync(join(filePinned, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': 'file:vendor/bongos-core-1.2.3.tgz' } }));
|
|
862
|
+
const r1 = run(filePinned, bareRoot);
|
|
863
|
+
assert.equal(r1.tgz, join(filePinned, 'vendor', 'bongos-core-1.2.3.tgz'));
|
|
864
|
+
assert.deepEqual(r1.cmds, [], 'rung 1 runs no command — it reuses bytes already on disk');
|
|
865
|
+
|
|
866
|
+
// Rung 2 — a registry-installed core packs, still without touching package-core.js.
|
|
867
|
+
const reg = mkdtempSync(join(tmpdir(), 'rvct-reg-'));
|
|
868
|
+
mkdirSync(join(reg, 'node_modules', '@bongos', 'core'), { recursive: true });
|
|
869
|
+
writeFileSync(join(reg, 'node_modules', '@bongos', 'core', 'package.json'), JSON.stringify({ name: '@bongos/core', version: '1.19.56' }));
|
|
870
|
+
writeFileSync(join(reg, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '1.19.56' } }));
|
|
871
|
+
const r2 = run(reg, bareRoot);
|
|
872
|
+
assert.ok(r2.cmds.some((c) => c.startsWith('npm pack ')), 'rung 2 packs the installed core');
|
|
873
|
+
assert.ok(!r2.cmds.some((c) => /package-core\.js/.test(c)), 'rung 2 never reaches package-core.js');
|
|
874
|
+
assert.ok(r2.packDest, 'rung 2 returns the temp pack dir for the caller to clean up');
|
|
875
|
+
|
|
876
|
+
// Rung 3 — a source checkout WITH history rebuilds reproducibly from the ref.
|
|
877
|
+
const source = mkdtempSync(join(tmpdir(), 'rvct-src-'));
|
|
878
|
+
const r3 = run(source, gitRoot);
|
|
879
|
+
assert.deepEqual(r3.cmds, ['node scripts/gds/package-core.js'], 'history present → the ref build, NOT --from-worktree');
|
|
880
|
+
assert.equal(r3.tgz, join(gitRoot, 'dist', 'bongos-core-9.9.9.tgz'));
|
|
881
|
+
assert.equal(r3.packDest, null);
|
|
882
|
+
|
|
883
|
+
// Rung 4 — the same source checkout with its history stripped: the self-host image.
|
|
884
|
+
const r4 = run(source, bareRoot);
|
|
885
|
+
assert.deepEqual(r4.cmds, ['node scripts/gds/package-core.js --from-worktree'], 'no history → pack the working tree');
|
|
886
|
+
assert.equal(r4.tgz, join(bareRoot, 'dist', 'bongos-core-9.9.9.tgz'), 'the child still pins the running CORE_VERSION');
|
|
887
|
+
|
|
888
|
+
[gitRoot, bareRoot, filePinned, reg, source].forEach((d) => rmSync(d, { recursive: true, force: true }));
|
|
889
|
+
});
|
|
890
|
+
|
|
819
891
|
await ta('scaffoldStandaloneRepo: a REGISTRY-pinned consumer control plane PACKS its installed core — current version, no source rebuild (task 1002706)', async () => {
|
|
820
892
|
const cp = mkdtempSync(join(tmpdir(), 'cpdir-cp-'));
|
|
821
893
|
mkdirSync(join(cp, 'node_modules', '@bongos', 'core'), { recursive: true });
|