create-agentic-workspace 0.6.0 → 0.8.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 +5 -2
- package/package.json +2 -2
- package/permission-floor.json +5 -1
- package/src/floorReconcile.mjs +65 -8
- package/src/permissionFloor.mjs +45 -10
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.8.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.
|
|
37
|
+
"plugin_version": "1.8.0",
|
|
38
38
|
"pins_researched": "2026-08-02"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/permission-floor.json
CHANGED
|
@@ -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.
|
|
4
|
+
"generated_for_plugin_version": "1.8.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"
|
package/src/floorReconcile.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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 ${
|
|
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
|
-
|
|
226
|
-
'
|
|
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) {
|
package/src/permissionFloor.mjs
CHANGED
|
@@ -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-
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
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
|
|
262
|
+
source: { source: 'github', repo: pins.marketplace_repo },
|
|
228
263
|
autoUpdate: false,
|
|
229
264
|
};
|
|
230
265
|
return {
|