Agentic AI Atlasby a5c.ai
OverviewWikiGraphFor AgentsEdgesSearchWorkspace
/
GitHubDocsDiscord
iiRecord
Agentic AI Atlas · WP-RN-04 record: publish-isolation guards G1–G6
page:docs-specs-platform-gaps-phase0-publish-guardsa5c.ai
Search record views/
Record · tabs

Available views

II.Record viewspp. 1 - 1
overviewarticlejsongraph
III.Related pagespp. 1 - 1
II.
Page reference

page:docs-specs-platform-gaps-phase0-publish-guards

Reading · 12 min

WP-RN-04 record: publish-isolation guards G1–G6 reference

Executed 2026-09-28 on branch phase0/hard-split-registry-guards of a5c-ai/fabric. Spec: docs/specs/platform-gaps/15-private-isolation-rename.md §4.3 (guards, binding) and "WP-RN-04"; intent 00-intent.md D17–D21 and acceptance criterion 7. Prior work this builds on: WP-RN-02 (scripts/ci/public-push-guard.cjs), WP-RN-03 (docs/specs/platform-gaps/phase0/ar-provisioning.md, root .npmrc, .github/workflows/ar-smoke.yml). The public repository a5c-ai/babysitter was never written to (pushedat stayed 2026-09-16T19:59:50Z; only gh api GET calls were issued) and nothing was published anywhere: every npm pack in the tests is a local tarball and no npm publish ran.

Pagewiki/docs/specs/platform-gaps/phase0/publish-guards.mdOutgoing · 0Incoming · 1

WP-RN-04 record: publish-isolation guards G1–G6

**Scope re-locked to @fabricstack on 2026-09-28, see scope-relock.md.** This record was produced under the previous private scope @agentfabric; the scope strings below were rewritten mechanically at the re-lock (the quoted guard output originally showed the old name). The G4 collision that triggered the re-lock is kept in the G4 row and the "Scope collision" section as history.

Executed 2026-09-28 on branch phase0/hard-split-registry-guards of a5c-ai/fabric. Spec: docs/specs/platform-gaps/15-private-isolation-rename.md §4.3 (guards, binding) and "WP-RN-04"; intent 00-intent.md D17–D21 and acceptance criterion 7. Prior work this builds on: WP-RN-02 (scripts/ci/public-push-guard.cjs), WP-RN-03 (docs/specs/platform-gaps/phase0/ar-provisioning.md, root .npmrc, .github/workflows/ar-smoke.yml). The public repository a5c-ai/babysitter was never written to (pushed_at stayed 2026-09-16T19:59:50Z; only gh api GET calls were issued) and nothing was published anywhere: every npm pack in the tests is a local tarball and no npm publish ran.

One constant

The private scope and the Artifact Registry URL are declared once, in the root package.json:

json
"fabric": {
  "privateScope": "@fabricstack",
  "privateRegistry": "https://us-central1-npm.pkg.dev/boot-464019/fabric-npm/",
  "legacyPublicScope": "@a5c-ai"
}

scripts/lib/registry-pin.cjs reads it (readPrivateRegistryConfig) and every guard, the inventory selector and the release verifier consume it from there. scripts/ci/publish-guards.test.mjs pins that the committed .npmrc routing line equals <privateScope>:registry=<privateRegistry>. Re-locking the scope at Gate B (spec 15 §2) is therefore: change the three values, change the .npmrc line, regenerate the fixtures under scripts/ci/fixtures/publish-guards/ (they name the scope literally), and rename the smoke fixture — the guards follow.

Guard table

GuardMechanismFilesNegative fixture (must trip)Status
**G1** manifest-level pinningEvery publishable @fabricstack/* manifest pins publishConfig.registry to the exact AR URL and carries no publishConfig.access: "public"; nothing outside the scope pins AR. Rules private-scope-registry, private-scope-public-access, ar-registry-foreign-scope. Private (non-publishable) private-scope workspaces need no pin but may not declare public access or npmjs.scripts/lib/registry-pin.cjs (manifestProblems), scripts/ci/publish-isolation-guard.cjs (scans root workspaces manifests plus scripts/ci/fixtures/** and scripts/__tests__/fixtures/** package.json files)g1/negative/public-npmjs (access: public + npmjs registry), g1/negative/missing-registry, g1/negative/foreign-scope-on-ar**Done**; red on fixtures, green on the tree (101 files, 0 findings). Selector switch deferred — see below
**G2** CI enforcement jobRequired job publish-isolation in .github/workflows/ci.yml runs the guard self-test, npm run test:publish-guards, npm run test:binary-renames and the informational G4 probe. (a) = G1 over the workspace; (b) no publishable manifest carries @a5c-ai/* — legacy-scope-name, private train only; (c) no workflow touching the private scope has registry.npmjs.org in a publish context (registry-url: for npmjs combined with a publish command or token, npm publish --registry, //registry.npmjs.org/:_authToken) — npmjs-publish-context-private-scope; private train: any such context, npmjs-publish-context; (d) verify-manifests fails on any internal dependency referencing @a5c-ai/* — private train only.github/workflows/ci.yml job publish-isolation; scripts/ci/publish-isolation-guard.cjs; scripts/lib/release-version.cjs (collectVersionDivergences, privateTrain)g2/negative/npmjs-publish-private-scope, g2/negative/npmjs-explicit-registry, g2/negative/legacy-publish-private-train (flag), g1/negative/legacy-scope-private-train (flag); G2(d) synthetic root in the test**Done**; the job runs before npm ci (no dependencies)
**G3** credential starvationNo workflow touching the private scope references an npmjs token secret (secrets.NPM_TOKEN/NPMJS_TOKEN/NPM_PUBLISH_TOKEN/NPM_AUTH_TOKEN/NODE_AUTH_TOKEN next to npmjs, //registry.npmjs.org/:_authToken) — npmjs-credential-private-scope; no committed .npmrc (root, packages/, apps/, scripts/) carries an npmjs auth line — npmrc-npmjs-token; private train: any npmjs secret reference, npmjs-credential. Plus the read-only secret audit belowscripts/ci/publish-isolation-guard.cjsg3/negative/authtoken-line, g3/negative/npmrc-token**Done** for the CI assertion; **audit finding**: the org-level NPM_TOKEN is visible to this repository (below). Owner decided "Restrict now" at breakpoint phase0.npmjs-org; the restriction is **PENDING OWNER UI ACTION** — record, procedure, verification and rollback in g3-npm-token-restriction.md
**G4** defensive scope registrationExternal human action. Runbook + read-only status probe (--g4-status: registry search, unscoped package, /-/org/<scope>/user, org page) printing one of unregistered / third-party-packages-present / registered-no-packages / unknown; never fails the jobdocs/specs/platform-gaps/phase0/g4-npmjs-org-runbook.md; scripts/ci/publish-isolation-guard.cjs --g4-status; classifyG4 unit-tested— (informational)**PENDING OWNER ACTION — scope re-locked to @fabricstack on 2026-09-28 (scope-relock.md); the fabricstack org registration is the owner's, per the runbook.** History: the first probe on 2026-09-28 found the original scope @agentfabric third-party-owned (third-party-packages-present: its scoped package sdk 1.0.1 by defimessiah, created 2026-03-18; unscoped agentfabric 0.0.2 by mdirshadengineer, created 2026-04-25 — the only place in this record where the old name is spelled out, as history), which the owner resolved at breakpoint phase0.npmjs-org by re-locking. Probe after the re-lock: unregistered for @fabricstack. Nothing was created or published
**G5** bin-collision gatenpm run test:binary-renames (scripts/check-binary-renames.cjs) now also runs scripts/ci/bin-collision-guard.cjs over every workspace manifest: duplicate-bin (exactly one owner per bin; only the four pinned legacy @a5c-ai collisions babysitter, adapters, adapters-hooks, kradle-server are tolerated, with their exact owner sets, until the private train), public-train-bin (no private-scope package declares one of the 26 bins the public train publishes — spec 15 §3.1 current column, pinned as PUBLIC_TRAIN_BINS and cross-checked against the live tree by public-train-bins-drift), bin-not-in-spec (every private-scope bin is in the §3.1/§3.3 bin column), spec-bins-missing (private train: every §3.1 bin is shipped)scripts/ci/bin-collision-guard.cjs, scripts/check-binary-renames.cjsg5/negative/duplicate-bin, g5/negative/public-train-bin**Done**; red on fixtures, green on the tree (32 workspace bin names, 0 errors); --private-train on today's tree reports the 4 legacy collisions + 18 missing §3.1 bins, which is the RN-1 end-state WP-RN-06 must reach
**G6** packed-artifact scope checkscripts/verify-release-artifacts.mjs gained step scope right after pack: it extracts package/package.json from the exact tarball and applies the G1 rules (packedManifestProblems; private train additionally refuses any non-private-scope tarball). A failure skips every later step (no install of a mis-scoped artifact). .github/workflows/release-artifact-verifier.yml first runs the guard self-test and npm run test:publish-guards so the gate is proven red on its fixtures in the same lanescripts/verify-release-artifacts.mjs, scripts/lib/registry-pin.cjs, .github/workflows/release-artifact-verifier.ymlg6/negative/private-scope-npmjs (@fabricstack/leak → npmjs, public), g6/negative/legacy-scope-on-ar (@a5c-ai/mis-scoped → AR); both packed with npm pack in the test and run through the real verifier**Done**; verifier exits 1 with steps.scope.status = failed and install = skipped on both; g6/positive/pinned passes the full chain (scope, surfaces, install, imports = passed)

All fixtures live under scripts/ci/fixtures/publish-guards/<guard>/{negative,positive}/<case>/ with an expect.json naming the consuming guard, the mode and the rules; scripts/ci/publish-guards.test.mjs (npm run test:publish-guards, node:test, no dependencies beyond npm and tar) asserts every negative trips, every positive passes and the tree passes. The real-tree scans exclude the fixture directory; the inventory-equivalence oracle in scripts/__tests__/publishable-packages.test.mjs skips fixture manifests for the same reason.

Selector deferral (binding; spec 15 §4.3 G1, WP-RN-05/07)

Spec 15 says the inventory selector in scripts/lib/publishable-packages.cjs must change "in the same commit" as the manifests are pinned, keying on publishConfig.registry instead of publishConfig.access === "public". Today no manifest is renamed, so flipping the selector would make the current @a5c-ai release train see **zero** packages. The switch is therefore implemented but gated:

  • FABRIC_PRIVATE_TRAIN=1 (env; also --private-train on the guard CLIs) selects the RN-1 end-state everywhere: the inventory keys on publishConfig.registry === <AR URL> plus scope; G2(b), G2(d), the whole-tree npmjs rules and the G5 tolerance retirement become active; G6 refuses any non-private-scope tarball.
  • Default (flag unset) is byte-identical to the pre-WP-RN-04 behaviour. scripts/ci/publish-guards.test.mjs re-implements the old predicate as an oracle and asserts the CLI output of node scripts/lib/publishable-packages.cjs is byte-for-byte equal (43 @a5c-ai/* entries); it also asserts the flag yields [] on today's tree and exactly the pinned package on a synthetic renamed tree.
  • WP-RN-05/07 flip the default by setting the flag in the release workflows (or by making the flag the default once the rename lands) and delete the legacy branch of selectsForPublish. Until then scripts/__tests__/publishable-packages.test.mjs keeps asserting 43 @a5c-ai/* packages.

Scope collision risk (added after the G4 probe; resolved by the re-lock)

**Resolved 2026-09-28:** the original scope was found third-party-owned and the owner re-locked to @fabricstack, which is free on npmjs (scope-relock.md). The analysis below is kept because it applies to *any* private scope that is also claimed on the public registry, and it is why G4 (registering the fabricstack org) still matters. At the time it read: the original scope was already a third-party scope on the public registry (a scoped sdk package, "Decentralized cloud for AI agents", MIT, maintainer defimessiah; the unscoped CLI of the same name belonged to a different maintainer). Consequences:

  • A developer or CI job **without** the committed .npmrc scope routing that runs npm install @fabricstack/sdk (or any future private name that the stranger also publishes) gets the stranger's package from npmjs — a dependency-confusion vector. Inside this repository the routing line @fabricstack:registry=<AR URL> takes precedence for the scope (WP-RN-03 caveat), so in-tree installs resolve against AR only.
  • Mitigations in place: the committed .npmrc routing, G1 (no private manifest can point at npmjs), G2/G3 (no workflow can publish or authenticate there for the scope), G6 (no mis-scoped tarball passes the release gate), and docs/user-guide/private-install.md (consumers must carry the routing line). None of these stops an out-of-tree consumer from installing the stranger's package under a name we also use.
  • Recommendation (as made): decide the scope re-lock at breakpoint phase0.npmjs-org — either keep the colliding scope private-only and accept the public collision (document it in private-install.md and require the routing line in every consumer), or pick a free scope (probe with node scripts/ci/publish-isolation-guard.cjs --g4-status after changing the constant) and register its org per the runbook. The owner chose the second option; the re-lock was the one-constant change described above plus the mechanical sweep recorded in scope-relock.md.

Secret audit (read-only, 2026-09-28)

gh secret list -R a5c-ai/fabric:

text
KRADLE_TEST_AUTH_SECRET    2026-09-16T16:27:42Z

No npmjs credential at repository level — the G3 invariant holds there.

gh api orgs/a5c-ai/actions/secrets (28 secrets): A5C_AGENT_GITHUB_TOKEN, ANTHROPIC_API_KEY, AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET, AZURE_APPLICATION_CLIENT_ID, AZURE_APPLICATION_CLIENT_SECRET, AZURE_OPENAI_API_KEY, DISCORD_TOKEN, GEMINI_API_KEY, GOOGLE_API_KEY, GOOGLE_CLOUD_PROJECT, KRADLE_TEST_AUTH_SECRET (visibility private), KRATE_GITHUB_CLIENT_SECRET, KUBE_CONFIG, MY_GH_TOKEN (private), **NPM_TOKEN (visibility all, updated 2026-05-20)**, OPENAI_API_KEY, RUNNER_GITHUB_TOKEN, SUPABASE_ACCESS_TOKEN, SUPABASE_DB_PASSWORD, TAL_GH_TOKEN, TRELLO_API_KEY, TRELLO_SECRET, TRELLO_TOKEN, VERCEL_TOKEN, X_API_ACCESS_TOKEN, X_API_ACCESS_TOKEN_SECRET, X_BEARER_TOKEN (private); all others visibility all.

**Finding:** an organization secret with visibility all is readable by every repository in the organization, including a5c-ai/fabric. Credential starvation (spec 15 G3, "fabric's repo secrets contain no npmjs token") therefore does **not** hold at organization level: the inherited public-train workflows .github/workflows/publish-packages-from-tag.yml (lines 329, 413, 425) and .github/workflows/fix-npm-tags.yml (lines 35, 39) reference secrets.NPM_TOKEN and would receive a working npmjs token. Both are legacy @a5c-ai train workflows scheduled for retargeting to AR by WP-RN-07 and neither mentions the private scope, so the CI assertion classifies them as legacy-train notices (8 notice lines, 0 findings); .github/workflows/live-stack-published.yml sets registry-url for npmjs but only installs (no publish command, no token) and is read-only. Owner options, to be decided at the next breakpoint (no infra change was made here): (a) restrict the org secret NPM_TOKEN to selected repositories that exclude fabric, or (b) accept until WP-RN-07 removes the references, at which point --private-train makes any remaining reference a hard finding.

Verification transcript

Commands run from C:/work/fabric on 2026-09-28 (outputs abbreviated to the decisive lines):

text
$ node scripts/ci/publish-isolation-guard.cjs --root scripts/ci/fixtures/publish-guards/g1/negative/public-npmjs
packages/leak/package.json:1: [private-scope-public-access] @fabricstack/leak declares publishConfig.access "public"; ...
packages/leak/package.json:1: [private-scope-registry] @fabricstack/leak pins publishConfig.registry to the public npm registry (https://registry.npmjs.org/); ...
publish-isolation-guard: FAIL — @fabricstack/* packages must never reach registry.npmjs.org (spec 15 §4.3, intent D19)   (exit 1)

$ node scripts/ci/publish-isolation-guard.cjs --root scripts/ci/fixtures/publish-guards/g2/negative/npmjs-publish-private-scope
.github/workflows/leak.yml:14: [npmjs-publish-context-private-scope] ...  > registry-url: https://registry.npmjs.org/
.github/workflows/leak.yml:17: [npmjs-credential-private-scope] ...       > NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}   (exit 1)

$ node scripts/ci/publish-isolation-guard.cjs --root scripts/ci/fixtures/publish-guards/g3/negative/npmrc-token
.npmrc:2: [npmrc-npmjs-token] committed .npmrc carries an npmjs auth line   (exit 1)

$ node scripts/ci/publish-isolation-guard.cjs --self-test
publish-isolation-guard self-test: 14/14 assertions passed
publish-isolation-guard: self-test passed; repository scan clean (101 files, 0 findings, 8 legacy-train notice(s))   (exit 0)

$ node scripts/ci/bin-collision-guard.cjs --root scripts/ci/fixtures/publish-guards/g5/negative/duplicate-bin
[duplicate-bin] bin "orchestrator" is declared by @fabricstack/orchestrator and @fabricstack/orchestrator-sdk ...   (exit 1)
$ node scripts/ci/bin-collision-guard.cjs --root scripts/ci/fixtures/publish-guards/g5/negative/public-train-bin
[public-train-bin] @fabricstack/orchestrator declares bin "babysitter", which the public @a5c-ai train publishes ...
[bin-not-in-spec] @fabricstack/orchestrator declares bin "babysitter", which is not in the spec 15 §3.1/§3.3 bin column ...   (exit 1)
$ npm run test:binary-renames
Binary rename guardrail passed for 6 renamed binaries and 8 emitted bin targets; single-bin policy (G5) holds over 32 workspace bin names.

$ node scripts/verify-release-artifacts.mjs --package-dir scripts/ci/fixtures/publish-guards/g6/negative/private-scope-npmjs --skip-build --report-dir <tmp>
[FAILED] @fabricstack/leak: scope: packed package.json violates publish isolation (spec 15 §4.3 G6): [private-scope-public-access] ...; [private-scope-registry] ...   (exit 1)
report steps: build=skipped pack=passed scope=failed surfaces=skipped shebangs=skipped install=skipped ...
$ node scripts/verify-release-artifacts.mjs --package-dir scripts/ci/fixtures/publish-guards/g6/positive/pinned --skip-build --report-dir <tmp>
[verify] @fabricstack/pinned: passed   (steps: pack=passed scope=passed surfaces=passed install=passed imports=passed)

$ node scripts/ci/publish-isolation-guard.cjs --g4-status
publish-isolation-guard G4 status: third-party-packages-present — @fabricstack: third-party packages exist under this scope on registry.npmjs.org ...   (exit 0)

$ npm run test:publish-guards && npm run test:binary-renames && echo GUARDS-PASS
# tests 17 / pass 17 / fail 0
Binary rename guardrail passed ...; single-bin policy (G5) holds over 32 workspace bin names.
GUARDS-PASS

Other checks: node scripts/ci/public-push-guard.cjs → 179 files, 0 findings; both edited workflows load with js-yaml; npm run guard:packages and npm run verify:metadata pass (43 public packages); node --test scripts/__tests__/publishable-packages.test.mjs passes. Pre-existing failures, identical on the pristine tree and unrelated to this WP: 8 tests in release-matrix.test.mjs/release-version.test.mjs (they read publish.yml, which WP-RN-01 retargeted, and stub npm on PATH, impossible on Windows), and the Windows-only bins step (.cmd shim spawn) plus two publish.yml text assertions in verify-release-artifacts.test.mjs (13 failures on the pristine tree, 6 with this WP: the verifier now spawns npm.cmd through a shell and extracts tarballs with relative paths so GNU tar no longer reads C:\ as a host).

Not done here (by design)

  • G4 registration itself (was blocked by the scope collision; after the re-lock it is the owner's action per g4-npmjs-org-runbook.md).
  • Retargeting the 43 @a5c-ai manifests, the release workflows and the selector default (WP-RN-05/06/07).
  • Any change to organization secrets or repository settings (the NPM_TOKEN restriction is pending an owner UI action, g3-npm-token-restriction.md).

Article source

The article body is owned directly by this record.

Related pages

No related wiki pages for this record.

Shortcuts

Open overview
Open JSON
Open graph