ci(publish): pin the trusted-publishing npm and add a token fallback - #732
Conversation
The npm publish already runs tokenless: no NPM_TOKEN secret exists on the repository or the tangle-network organization, and 0.173.0 carries an SLSA provenance attestation published by "GitHub Actions". This makes the path deterministic and gives it a break-glass fallback. Pin npm to the exact 11.18.0 that agent-sdk release.yml uses. A floating `npm@11` accepts any future 11.x, and npm 12.0.0 ships a provenance publish that fails with MODULE_NOT_FOUND for sigstore. The OIDC floor is 11.5.1. Set NPM_CONFIG_PROVENANCE so an attestation is requested on both the OIDC path and the fallback path. Read NPM_TOKEN as an explicit fallback. It is unset today, so the OIDC path is unchanged. A token set later takes precedence over OIDC, which is the agent-sdk #284 failure, so the step says that in the log. Release semantics do not change: the v* tag trigger, the tag/version lock in the verify job, and the PyPI job that runs after npm all stay as they were.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — f0b10af8
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
This approval is provisional. It rests on the audit running. If the audit cannot run — for example the CLI bridge rejects it — this approval is dismissed rather than left standing, so an unrun check never reads as a passing one.
tangletools · auto-approval · reason: drewstone_author · 2026-09-02T03:24:07Z
tangletools
left a comment
There was a problem hiding this comment.
🟢 Value Audit — sound
| Verdict | sound |
| Coverage | 1 of 2 lenses (value) |
| Concerns | 0 (none) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 161.7s (2 bridge agents) |
| Total | 161.7s |
⚠️ Partial audit — the verdict covers only value. usefulness: agent returned no usable verdict. Treat the missing lens as unexamined, not as clear.
💰 Value — sound
Makes the npm trusted-publishing path deterministic (exact npm pin + assertion, provenance env) and adds an unset-by-default NPM_TOKEN break-glass fallback — a coherent, well-documented hardening of an already-working OIDC publish.
- What it does: Three changes to the publish-npm job in .github/workflows/publish.yml: (1) pins the npm CLI to exact version 11.18.0 with a
test "$(npm --version)" = '11.18.0'assertion, replacing the floatingnpm install -g npm@11that was previously inlined in the publish step (publish.yml:180-183, previously 182); (2) adds job-levelNPM_CONFIG_PROVENANCE: 'true'so every publish — including one a lifecyc - Goals it achieves: Determinism and a safe repair path for an already-functioning tokenless publish. The floating
npm@11could silently pull a future 11.x regression (or the broken npm 12 provenance line); the exact pin plus assertion fails fast instead. The break-glass fallback lets a human repair a publish by hand via NPM_TOKEN without disturbing today's OIDC behavior, and the env-level provenance flag guarantees - Assessment: Good on its merits and fully in the grain of the file. The prior version already documented OIDC, the npm 12 sigstore break, and the >=11.5.1 floor — this change consolidates that scattered reasoning into a dedicated, named, asserted step rather than a bash comment inside the publish command. Comments are precise (explain precedence, the empty-_authToken gotcha, and the provenance-coverage rationa
- Better / existing approach: none — this is the right approach. I grepped
.github/forNPM_CONFIG_PROVENANCE|_authToken|npm install -g npm@|trusted publishing|OIDC; the only matches are in this file. The repo has only ci.yml and publish.yml (no shared release action/reusable workflow to extend), and the sibling patterns the PR cites (agent-sdk #284, agent-dev-container) live in other repositories, not here. The change is - Model: opencode/deepseek/deepseek-v4-pro
- Bridge attempts: 3
- Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error event without a message: {"type":"error","timestamp":1788319595710,"sessionID":"ses_f9fd88668ffeQSoy579BG16OtT","error":{"name":"UnknownError","data":{"message":"Unexpected server error. Check server logs for details.","ref":"err_9c92adbf"}}}; opencode/zai-coding-plan/glm-5.2: opencode: opencode error event without a message: {"type":"error",
🎯 Usefulness — error
usefulness agent produced no parseable value-audit JSON.
- Model: opencode/deepseek/deepseek-v4-pro
- Bridge attempts: 3
- Bridge error: opencode/zai-coding-plan/glm-5.2: opencode: opencode error event without a message: {"type":"error","timestamp":1788319669858,"sessionID":"ses_f9fd88676ffe0TNSo4YdqKJVVQ","error":{"name":"APIError","data":{"message":"Usage limit reached for 5 hour. Your limit will reset at 2026-09-02 12:05:20","statusCode":429,"isRetryable":true,"responseHeaders":{"alt-svc":"h3=":443"; ma=3600","connection":"kee
No concerns from the lens that ran (value). The missing lens examined nothing, so this is not a full clean bill of health.
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
| opencode DeepSeek v4 Pro | opencode DeepSeek v4 Flash | aggregate | |
|---|---|---|---|
| Readiness | 89 | 83 | 83 |
| Confidence | 65 | 65 | 65 |
| Correctness | 89 | 83 | 83 |
| Security | 89 | 83 | 83 |
| Testing | 89 | 83 | 83 |
| Architecture | 89 | 83 | 83 |
Reviewer score is advisory once the run is complete and the verdict has no blockers.
Full multi-shot audit completed 1/1 planned shots over 1 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 1/1 planned shots over 1 changed files. Global verifier still owns final merge decision.
🟡 LOW Break-glass token fallback branch is the one untested code path — .github/workflows/publish.yml
The NPM_TOKEN branch (
npm config set --location=user //registry.npmjs.org/:_authToken) is never exercised in CI because the secret is unset by default — yet it is the path a maintainer relies on to repair a publish by hand. Its success depends on the untested combination of npm 11.18.0 provenance signing with a legacy token (npm had a history of provenance+token failures). Since NPM_CONFIG_PROVENANCE=true is job-wide and id-token: write is always granted, provenance should still work, but this should be validated once with a scoped test token before it is needed in an outage.
🟡 LOW Exact npm pin is a hard single point of failure for every release — .github/workflows/publish.yml
npm install -g npm@11.18.0hard-pins one patch release. If that version is ever yanked from the registry, or carries a later-discovered regression, every tag/dispatch release hard-fails. Thetest "$(npm --version)" = '11.18.0'guard fails loud (good), but nothing prompts a bump when npm 11.x security fixes land. The previousnpm@11floated to the newest 11.x. Recommend documenting a bump cadence in the step comment, or a reusable workflow input, so the pin does not silently rot. Verified 11.18.0 exists today (npm view exit 0).
🟡 LOW NPM_CONFIG_PROVENANCE is redundant with the --provenance flag — .github/workflows/publish.yml
The job-level env sets NPM_CONFIG_PROVENANCE: 'true' while the publish command already passes
npm publish --provenance. Both request provenance, so the env var is redundant for the actual publish; as a job-level env it also blankets thenpm install -g npm@11.18.0andpnpm install/buildsteps, where the key is inert. The comment claims the variable additionally covers 'a publish an npm lifecycle script starts', which is accurate only if a lifecycle script invoked npm publish — it does not here (prepublishOnly ispnpm build). Harmless, no functional bug. Fix (optional): keep the flag only, or keep the env var and drop the flag, and tighten the comment to avoid implying an npm lifecycle publish that does not exist.
🟡 LOW Publish step name no longer matches its behavior — .github/workflows/publish.yml
The step is named 'Publish to npm (OIDC trusted publishing)', but after this change it publishes via OIDC only when NPM_TOKEN is unset and falls back to a legacy token when NPM_TOKEN is set. The name is now misleading for the fallback path (a reader skimming the log or this step name would not learn a token publish happened). Impact: confusion only, no functional effect. Fix: rename to 'Publish to npm' or 'Publish to npm (OIDC, token fallback)'.
🟡 LOW Token precedence is a silent footgun if NPM_TOKEN is ever set — .github/workflows/publish.yml
The design is 'leave NPM_TOKEN unset so OIDC wins', but the code does not enforce it: if NPM_TOKEN becomes available (repo or org-level secret inheritance), every publish silently switches from short-lived OIDC credentials to the long-lived token, with only an echo line announcing it. The agent-sdk #284 incident is cited as why precedence is intentional, but a short-lived-credential policy is weaker the moment the secret exists. Suggest emitting a
::warning::annotation (and consider failing) when the token path is taken, so the switch is visible in the run log.
🟡 LOW sed _authToken cleanup swallows failure that would break OIDC — .github/workflows/publish.yml
sed -i '/_authToken/d' "$HOME/.npmrc" 2>/dev/null || truediscards errors and missing-file status. If $HOME/.npmrc exists but is not writable, or a future setup-node step adds registry-url, a stale//registry.npmjs.org/:_authTokenline survives and npm silently disables the OIDC exchange, producing a confusing 404 on PUT (the exact agent-sdk #284 failure mode the comment cites). Since a token takes precedence over OIDC, the step should fail loud when it intended OIDC but finds a leftover token line it could not remove. Today the publish job writes no .npmrc, so this is latent, not live.
tangletools · 2026-09-02T03:30:02Z · trace
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 6 non-blocking findings — f0b10af8
Full multi-shot audit completed 1/1 planned shots over 1 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 1/1 planned shots over 1 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-09-02T03:30:02Z · immutable trace
What this changes
The npm publish is already tokenless. There is no
NPM_TOKENsecret on this repository or on thetangle-networkorganization, and@tangle-network/agent-eval@0.173.0carries an SLSA provenance attestation with_npmUser: "GitHub Actions"— the signature of an OIDC publish. So the trusted publisher for@tangle-network/agent-evalis already registered and working.This makes that path deterministic and adds a break-glass fallback, matching agent-sdk #284 and
agent-dev-container/.github/workflows/publish-sandbox-sdk.yml.npm install -g npm@11.18.0with an exact-version assertion, replacingnpm@11npm@11accepts any future 11.x. npm 12.0.0 ships a provenance publish that fails withMODULE_NOT_FOUNDforsigstore. The OIDC floor is 11.5.1. Same pin agent-sdkrelease.ymlcarries.NPM_CONFIG_PROVENANCE: 'true'on thepublish-npmjob--provenancestays on the command.Configure npm authstep that writes an_authTokenonly whenNPM_TOKENis setpermissions: contents: read+id-token: writewere already present onpublish-npmand are unchanged.Release semantics: unchanged
pushon av*tag orworkflow_dispatch.verifyjob still locks tag topackage.jsonversion and to the Python client version.publish-npmis still idempotent, andpublish-pypistill runs after it.Note: this repository has no tip-of-main rule for a release tag. It checks tag-to-version equality only, unlike agent-runtime, which also verifies that the tag sits on the tip of
mainor of itsrelease/<major>.<minor>.xline. Out of scope here; worth adding separately.PyPI: already trusted publishing, no change
publish-pypiusespypa/gh-action-pypi-publish@release/v1withid-token: writeand nopassword:input, and no PyPI token secret exists on the repository or the organization. That is PyPI trusted publishing, andagent-eval-rpc==0.173.0published on 2026-09-02. Left as is.Checks
actionlint .github/workflows/publish.yml— clean (shellcheck available).bash -non everyrunblock in the file — clean.Before merge
The npm trusted publisher already exists for
@tangle-network/agent-eval. Nothing to register.