create-agentic-workspace 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,8 +17,11 @@ never pre-grants anything — it *declares*, the platform's trust dialog is the
17
17
  ## What it does
18
18
 
19
19
  - Emits the plugin's reviewed three-tier permission map verbatim into the new workspace's
20
- committed `.claude/settings.json`, alongside `extraKnownMarketplaces` and `enabledPlugins`
21
- pinned to an exact marketplace ref (`autoUpdate: false` — no floating grant).
20
+ committed `.claude/settings.json`, alongside `extraKnownMarketplaces` and `enabledPlugins`.
21
+ As of `feat-foundry-installer-unpinning`, the marketplace registration names no ref at all
22
+ (`autoUpdate: false` still stops it from floating on its own — no floating grant); the ARTIFACT
23
+ fetched is what stays pinned, via the untouched `plugins[].source.sha` in the marketplace
24
+ manifest, which `sha` outranks `ref` at install time regardless of which index ref is named.
22
25
  - Absorbs `foundry-bootstrap.sh`'s out-of-session `git` commit-identity isolation (`--gh-account`),
23
26
  proved differentially equal to the shipped script.
24
27
  - Scaffolds a seven-file, schema-valid workspace seed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agentic-workspace",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "The pre-session bootstrap wizard for an Agentic Foundry workspace: declares (never grants) the permission floor, absorbs foundry-bootstrap.sh's out-of-session identity wiring, and scaffolds a seven-file schema-valid workspace. Zero dependencies, no lifecycle scripts, no telemetry.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "marketplace_name": "agentic-foundry",
35
35
  "marketplace_repo": "lukasrepublic/agentic-foundry",
36
36
  "plugin_name": "foundry",
37
- "plugin_version": "1.6.0",
37
+ "plugin_version": "1.7.0",
38
38
  "pins_researched": "2026-08-02"
39
39
  }
40
40
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "plugin_root_glob": "~/.claude/plugins/cache/*/foundry/*",
4
- "generated_for_plugin_version": "1.6.0",
4
+ "generated_for_plugin_version": "1.7.0",
5
5
  "entries": [
6
6
  {
7
7
  "rule": "Bash(~/.claude/plugins/cache/*/foundry/*/scripts/foundry-acceptance-contract-validate.py:*)",
@@ -356,6 +356,10 @@
356
356
  "script": "foundry_id_apply.py",
357
357
  "rationale": "pure decision library: no argparse/main()/__main__; exposes classify_gitops/decide_apply as pure functions (R5)"
358
358
  },
359
+ {
360
+ "script": "foundry_main_catalogue.py",
361
+ "rationale": "the shipped __main__ runs only as a push-scoped CI workflow step (.github/workflows/ci.yml); never instructed by any skill or hook in an agent session"
362
+ },
359
363
  {
360
364
  "script": "foundry_permission_floor.py",
361
365
  "rationale": "pure library: no argparse/main()/__main__; imported by scripts/foundry-doctor.py's permission-floor probe (feat-foundry-doctor-permission-floor-check), never in command position"
@@ -65,14 +65,60 @@ export function classifyPin(settingsObj, pins) {
65
65
  const entry = ((settingsObj && settingsObj.extraKnownMarketplaces) || {})[pins.marketplace_name];
66
66
  if (entry === undefined) return { state: 'absent', ref: null, skew: false };
67
67
  const ref = entry && entry.source && entry.source.ref;
68
- const pinned = typeof ref === 'string' && ref !== '' && !ref.includes('*') && entry.autoUpdate === false;
68
+ // feat-foundry-installer-unpinning (AC-IUP-5). Both installers now register the marketplace
69
+ // TAGLESS by default (AC-IUP-1/AC-IUP-3), so "no ref" must classify as pinned, not unpinned — an
70
+ // ABSENT ref is well-formed (refAbsent), distinct from a malformed EXPLICIT one (empty string, or
71
+ // carrying a wildcard, both still refused below). AC-IUP-4's Clarifications also widen the
72
+ // autoUpdate check from `=== false` to `!== true`: the shell installer has no --autoUpdate flag
73
+ // on `claude plugin marketplace add` and a verified isolated run wrote no such key at all, so an
74
+ // ABSENT autoUpdate must read the same as an explicit `false` — never merely tolerate `true`.
75
+ //
76
+ // SECURITY REVIEW (PR #132): widening "no ref" to `pinned` must NOT also widen it to "any entry".
77
+ // Property access on a primitive yields undefined rather than throwing, so `{}`, `"x"` and `[]`
78
+ // all reach refAbsent -- and NOTHING here compared the entry's own repo to ours, so a tagless
79
+ // entry naming a FOREIGN repository under our key would classify pinned and be granted the 42
80
+ // wildcarded cache allow rules, with the report REASSURING the operator about a manifest that is
81
+ // not ours. Before this atom that input hit the brake and warned. An entry must therefore be a
82
+ // plain object naming OUR github source before "no ref" can mean anything. `autoUpdate` is
83
+ // accepted only as absent-or-false: `!== true` admitted truthy coercions (1, "true") that the
84
+ // platform may honour as auto-update-on, which is wider than AC-IUP-5's text.
85
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
86
+ const src = isPlainObject(entry) && isPlainObject(entry.source) ? entry.source : null;
87
+ // Both operands undefined must NOT compare equal: a pin block that lost marketplace_repo
88
+ // would otherwise make every source carrying no repo key classify as ours.
89
+ const ourRepo = typeof pins.marketplace_repo === 'string' && pins.marketplace_repo !== ''
90
+ ? pins.marketplace_repo : null;
91
+ // GitHub owner/repo is CASE-INSENSITIVE, so `LukasRepublic/agentic-foundry` is the SAME
92
+ // repository. Comparing byte-exact would classify it unpinned and tell the operator "not ours",
93
+ // which is false. Normalize both sides; the comparison stays exact-match otherwise.
94
+ // Gate on the ASCII shape GitHub can actually issue BEFORE folding. toLowerCase() is
95
+ // locale-independent but NOT injective: U+212A KELVIN SIGN folds to ASCII 'k', and this repo's
96
+ // name contains one -- so "lu\u212Aasrepublic/agentic-foundry" would fold equal and read as
97
+ // ours. No such string can name a real GitHub repository, so it buys a false reassurance rather
98
+ // than attacker-controlled code; the shape gate closes the class outright, including whatever
99
+ // confusable the next Unicode revision adds.
100
+ const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
101
+ const sameRepo = typeof src?.repo === 'string' && REPO_RE.test(src.repo) && !!ourRepo
102
+ && src.repo.toLowerCase() === ourRepo.toLowerCase();
103
+ const isOurSource = !!src && sameRepo && src.source === 'github';
104
+ const refAbsent = isOurSource && ref === undefined;
105
+ const refWellFormed = isOurSource
106
+ && (refAbsent || (typeof ref === 'string' && ref !== '' && !ref.includes('*')));
107
+ const autoUpdateOk = entry && (entry.autoUpdate === undefined || entry.autoUpdate === false);
108
+ const pinned = refWellFormed && autoUpdateOk;
69
109
  // A pin can be perfectly well-formed and still name a DIFFERENT plugin than the map these rules
70
110
  // came from. The 42 allow rules are wildcarded across the cache and their per-script rationales
71
111
  // were reviewed against THIS version's scripts; writing them over a workspace pinned at an older
72
112
  // one grants the same paths against different code. Surfaced rather than refused — the operator's
73
- // review of the plan is the control, and it can only work if the skew is on screen.
74
- const skew = pinned && ref !== `v${pins.plugin_version}`;
75
- return { state: pinned ? 'pinned' : 'unpinned', ref: typeof ref === 'string' ? ref : null, skew };
113
+ // review of the plan is the control, and it can only work if the skew is on screen. AC-IUP-8: a
114
+ // tagless (refAbsent) entry has nothing to compare against a version, so it can never be "skewed"
115
+ // — computing `ref !== vX` against a null ref would otherwise fire the warning on every run.
116
+ const skew = pinned && !refAbsent && ref !== `v${pins.plugin_version}`;
117
+ // The foreign/malformed-source case must not render as an ordinary ref-shaped 'unpinned':
118
+ // its remediation ("pin the marketplace") can NEVER clear it, because refWellFormed requires
119
+ // isOurSource. Carry the reason so renderPlan can say the thing the operator can act on.
120
+ const reason = pinned ? null : (!isOurSource ? 'source' : (!autoUpdateOk ? 'autoUpdate' : 'ref'));
121
+ return { state: pinned ? 'pinned' : 'unpinned', ref: typeof ref === 'string' ? ref : null, skew, reason };
76
122
  }
77
123
 
78
124
  /** Compute the delta WITHOUT touching the filesystem. Returns
@@ -215,15 +261,26 @@ export function renderPlan(plan, { applied }) {
215
261
  lines.push(` + marketplace pin added — the bundled allow rules are wildcarded across the plugin cache and are bounded only by it`);
216
262
  } else if (plan.pin.state === 'pinned') {
217
263
  // printed on EVERY pinned run, not only the skewed one: an operator cannot notice a mismatch
218
- // that is never shown, and this is the branch where 42 grants are written without comment
264
+ // that is never shown, and this is the branch where 42 grants are written without comment.
265
+ // AC-IUP-8: a TAGLESS entry (plan.pin.ref === null, the default registration as of
266
+ // feat-foundry-installer-unpinning) must never render as "pinned at null" — say what actually
267
+ // bounds it instead. classifyPin's `skew` is already forced false for this case, so the
268
+ // trailing VERSION SKEW clause never appends here.
269
+ const refDisplay = plan.pin.ref === null
270
+ ? 'the default catalogue (tagless index; the artifact commit stays fixed by the manifest\'s source.sha)'
271
+ : plan.pin.ref;
219
272
  lines.push(
220
- ` · marketplace pinned at ${plan.pin.ref}; these rules come from the floor generated for v${plan.pinsVersion}` +
273
+ ` · marketplace pinned at ${refDisplay}; these rules come from the floor generated for v${plan.pinsVersion}` +
221
274
  (plan.pin.skew ? ' — VERSION SKEW: the same wildcarded paths will resolve to that pin\'s scripts, not this one\'s' : ''),
222
275
  );
223
276
  } else if (plan.withheldAllow) {
224
277
  lines.push(
225
- ` ! marketplace entry present but unpinned (ref=${plan.pin.ref === null ? 'none' : plan.pin.ref})` +
226
- ' — allow-tier rules WITHHELD; ask and deny still applied. Pin the marketplace, then re-run.',
278
+ plan.pin.reason === 'source' || plan.pin.reason === 'autoUpdate'
279
+ ? ` ! the marketplace entry ${plan.pin.reason === 'autoUpdate' ? 'sets autoUpdate to something other than false' : "does not name this marketplace's github source"}` +
280
+ ' — allow-tier rules WITHHELD; ask and deny still applied. Pinning will NOT clear this:' +
281
+ ' the registered source itself is not ours. Inspect the entry before re-running.'
282
+ : ` ! marketplace entry present but unpinned (ref=${plan.pin.ref === null ? 'none' : plan.pin.ref})` +
283
+ ' — allow-tier rules WITHHELD; ask and deny still applied. Pin the marketplace, then re-run.',
227
284
  );
228
285
  }
229
286
  for (const rule of plan.blanket) {
@@ -214,17 +214,52 @@ export function buildSettings(map, pins) {
214
214
  for (const e of map.entries) {
215
215
  byTier[e.tier].push(e.rule);
216
216
  }
217
- // THE PINNED LITERAL (AC-BCL-4(b), contract v1.2 — PR #61 security review Block 1). `ref` is
218
- // load-bearing, not decoration: without it the adopter's FIRST marketplace resolution floats to
219
- // whatever the default branch points at, and because the floor's allow rules are
220
- // version-wildcarded (`cache/*/foundry/*/scripts/...`), the operator's single trust acceptance
221
- // becomes a standing grant over whatever code that resolution delivered. That is the same
222
- // floating-pin defect `autoUpdate: false` guards the LATER fetches against, left open on the
223
- // first one — and strictly weaker than the manual path docs/QUICKSTART.md documents
224
- // (the documented `marketplace add …#<tag>` install line). Single-sourced from
225
- // the `foundry` pin block, so it cannot drift from the marketplace manifest.
217
+ // THE PINNED LITERAL — SUPERSEDED (feat-foundry-installer-unpinning, AC-IUP-3). This block used
218
+ // to read (AC-BCL-4(b), contract v1.2 — PR #61 security review Block 1), verbatim:
219
+ //
220
+ // "THE PINNED LITERAL (AC-BCL-4(b), contract v1.2 — PR #61 security review Block 1). `ref` is
221
+ // load-bearing, not decoration: without it the adopter's FIRST marketplace resolution floats
222
+ // to whatever the default branch points at, and because the floor's allow rules are
223
+ // version-wildcarded (`cache/*/foundry/*/scripts/...`), the operator's single trust acceptance
224
+ // becomes a standing grant over whatever code that resolution delivered. That is the same
225
+ // floating-pin defect `autoUpdate: false` guards the LATER fetches against, left open on the
226
+ // first one — and strictly weaker than the manual path docs/QUICKSTART.md documents (the
227
+ // documented `marketplace add …#<tag>` install line). Single-sourced from the `foundry` pin
228
+ // block, so it cannot drift from the marketplace manifest."
229
+ //
230
+ // That reasoning was CORRECT at the time and is not dismissed — it is superseded, on grounds
231
+ // re-derived from the LIVE repository configuration (not a shipped adopter template, which an
232
+ // earlier draft of the supersession wrongly cited and then withdrew), recorded in full in
233
+ // specs/features/foundry/onboarding/installer-unpinning/feat-foundry-installer-unpinning.md
234
+ // ("This SUPERSEDES a prior security-review Block"). Summary of the three grounds:
235
+ // (i) Ref integrity does not distinguish "pinned" from "tagless" on THIS repository: `main`
236
+ // is Tier A (6 required status contexts, enforce_admins, strict) and `refs/tags/v*` is
237
+ // doubly ruleset-protected (immutable, one ruleset with bypass_actors: []). The Block's
238
+ // threat model — an index that "floats to whatever the default branch points at" — is
239
+ // not a tampering threat here; both refs are equally tamper-resistant.
240
+ // (ii) The index pin controls CADENCE, not integrity, and that cost is paid three other ways:
241
+ // AC-IUP-4 makes BOTH installers register with an explicit-or-effective `autoUpdate:
242
+ // false` (this one keeps writing the literal `false`; the shell installer's Clarified
243
+ // resolution is that an ABSENT key reads identically, per floorReconcile.mjs's `pinned`
244
+ // predicate below); the untouched, content-addressed `plugins[].source.sha` in
245
+ // `.claude-plugin/marketplace.json` fixes exactly what commit is fetched regardless of
246
+ // which ref the index names; and `.github/workflows/btb-gates-base.yml`'s
247
+ // `^\.claude-plugin/` security-path rule gates what may enter the catalogue at all.
248
+ // (iii) The Block's own mechanism never implemented the Block's stated threat: this same
249
+ // floorReconcile.mjs already classified `ref: "main"` (a branch) as PINNED before this
250
+ // atom, and `scripts/foundry-bootstrap.sh --channel edge` already shipped exactly that
251
+ // floating-ref state as a supported, if opt-in, option. And the withholding branch this
252
+ // Block relied on (`floorReconcile.mjs`'s `withheldAllow`) only runs on the RECONCILE
253
+ // path against an ALREADY-EXISTING entry — never on this create path, which the Block's
254
+ // own "the adopter's FIRST marketplace resolution" language describes.
255
+ // AC-IUP-5 is a NECESSARY CONSEQUENCE of removing `ref` here, not a mitigation invented to excuse
256
+ // it: without floorReconcile.mjs's predicate change below, EVERY adopter's reconcile run would
257
+ // silently drop the allow tier. The residual this supersession leaves OPEN, not closed: nothing
258
+ // in this atom bounds what may land on `main` beyond the security-path label — the sibling
259
+ // default-branch catalogue gate (not delivered here) is what closes that gap. Single-sourced
260
+ // from the `foundry` pin block, so `repo` cannot drift from the marketplace manifest.
226
261
  const marketplaceEntry = {
227
- source: { source: 'github', repo: pins.marketplace_repo, ref: `v${pins.plugin_version}` },
262
+ source: { source: 'github', repo: pins.marketplace_repo },
228
263
  autoUpdate: false,
229
264
  };
230
265
  return {