Skip to content

publish-npm stages instead of publishing; a maintainer approves each version with 2FA (nxf 6j6v.c7dd) - #2

Merged
cabcookie merged 1 commit into
mainfrom
feat/npm-staged-publish
Sep 24, 2026
Merged

cabcookie merged 1 commit into
mainfrom
feat/npm-staged-publish

Conversation

@cabcookie

Copy link
Copy Markdown
Member

Why

The first release from the public repo (v0.200.0, run 36008146938) was green except for publish-npm:

  1. 404 … PUT @nexus-flow/mcp: the trusted-publisher entry still pointed at the pre-cutover repo. The owner re-added it.
  2. 403 … OIDC permission denied for this action: trusted publishers created since 2026-09-03 allow only staged publishing by default, and the job ran npm publish.

The token exchange and the provenance both worked (sigstore logIndex 2943855459), so it is not the immutable-subject problem npm/cli#9969, which fails earlier, at the exchange.

What

Follow npm's way instead of allowing direct publish again:

  • npm stage publish --access public --provenance: the version lands on npm unavailable to the public until a maintainer approves it with 2FA (npmjs.com, or npm stage approve <id>).
  • npm floor raised to >=11.15.0 <12 (staged publishing).
  • The job summary now says the version is staged, not live, and gives the approval commands.
  • A version that is staged but not yet approved is not skipped on a re-run. The job's OIDC token can only stage or publish, not list the queue, so it cannot tell "already staged" from a real refusal. It fails rather than guess.
  • Runbook docs/specs/release-management.md §12 item 9: stage-only allowed actions (c), npm version (f), the approval step (g), and the finding that the trusted-publisher entry is bound to the repository, not just its name.

Staging rehearsals are unchanged (npm pack, registry-free).

Evidence

actionlint .github/workflows/release.yml parses the file; its four info-level shellcheck notes are at lines 720 and 815, which this diff does not touch. The staged path itself can only be exercised by a real tag release. A rerun of 36008146938 runs the tagged commit's old npm publish, so the first staged version will be the next release.

skip-changelog: release.yml is a carved-out delivery file, but nothing a user installs changes. The npm package still appears, just after an approval.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NhwrYjEtdnS3jCneb57X3k

…version with 2FA (nxf 6j6v.c7dd)

The first release from the public repo failed only at npm: a trusted
publisher created since 2026-09-03 is stage-only by default, and the job
ran `npm publish`, so npm answered 403 "OIDC permission denied for this
action" after the token exchange and the provenance had worked.

Follow npm's way instead of ticking direct publish: `npm stage publish`
(npm >= 11.15.0) puts the version on npm unavailable to the public, and
only a maintainer's 2FA approval makes it live. No CI run can put a
package in front of npx users on its own. The job summary carries the
approval steps. The runbook in release-management.md §12 item 9 now
covers the allowed-actions setting, the repo binding and the approval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhwrYjEtdnS3jCneb57X3k
@cabcookie cabcookie added the skip-changelog Opt out of changelog-check (PRs with no user impact) label Sep 24, 2026
@cabcookie
cabcookie merged commit eecfe29 into main Sep 24, 2026
11 of 12 checks passed
@cabcookie
cabcookie deleted the feat/npm-staged-publish branch September 24, 2026 21:31
cabcookie added a commit that referenced this pull request Oct 2, 2026
A re-ship with no user impact: since v0.200.0 only CI and the changelog feed
changed (#2-#6). It is the first release through staged npm publishing
(6j6v.c7dd) and the first tag whose promote.yml opens the stable feed PR (#3).


Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
cabcookie added a commit that referenced this pull request Oct 3, 2026
…p knows the old phrases (nxf 6j6v.jfgy)

- sweep-retired-vocabulary.sh lists the four retired phrases of the old notice,
  each on its own, with the supersession reason (Code Quality #1).
- The facade test now pins the shape as well as the volume: the notice leads
  with `Parents: <ids>` and carries no second sentence after the ids, where an
  imperative could sit. Mutation-checked: "… Read them first" turns it red
  (Test Quality #3).
- The CLI tests share one PARENTS_NOTICE_LEAD: the positive test asserts it is
  present, the negative tests that it is absent, so a rewording cannot leave
  them checking for a string nobody prints (Test Quality #4).
- Two doc comments no longer call the notice "verbatim" (Code Quality #2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
cabcookie added a commit that referenced this pull request Oct 3, 2026
… order (nxf 6j6v.jfgy) (#11)

* nxf show points at an item's parents in a neutral line, not an urgent order (nxf 6j6v.jfgy)

The parents_notice said "URGENT RECOMMENDATION: ALSO READ THESE ITEMS TO GET
THE COMPLETE PICTURE!!!". Downstream, two independent agent roles flagged it
as a possible prompt injection in the ticket data, and agents learned to skim
the urgent line at the end of board output, which blunts them against a real
one (reported upstream from the agents project, 6gfd.cpw4).

It now reads "Parents: <ids> (read for full context)", in the human output and
in the --json / facade field alike; the field's name and when it appears are
unchanged. This supersedes the verbatim wording 6j6v.zvd0 specified. A facade
test pins the principle itself: no exclamation mark, no word in capitals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

* Mitigate the review of PR #11: the notice's shape is pinned, the sweep knows the old phrases (nxf 6j6v.jfgy)

- sweep-retired-vocabulary.sh lists the four retired phrases of the old notice,
  each on its own, with the supersession reason (Code Quality #1).
- The facade test now pins the shape as well as the volume: the notice leads
  with `Parents: <ids>` and carries no second sentence after the ids, where an
  imperative could sit. Mutation-checked: "… Read them first" turns it red
  (Test Quality #3).
- The CLI tests share one PARENTS_NOTICE_LEAD: the positive test asserts it is
  present, the negative tests that it is absent, so a rewording cannot leave
  them checking for a string nobody prints (Test Quality #4).
- Two doc comments no longer call the notice "verbatim" (Code Quality #2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
cabcookie added a commit that referenced this pull request Oct 4, 2026
…ly policy reads, named revoked-trust cause, group stop

- A build reads a user-level folder only under a NAMED development instance, never the
  production one, and an unresolvable home or instance means no folder rather than an error
  (Code #2, #3; Integrity #3, #4). Guard test: a build under the production instance does not
  see personas planted in the production folder.
- An existing but unreadable user-level folder is an error, not a silence (Integrity #5).
- `threads show`, `search`, `Engine::thread` and `Engine::search` read the merged channels
  only (`merged_channels`), so a malformed persona file cannot make a thread unreadable (Code #1).
- `nxc prime`, `Engine::prime_as` and `nxs prime --persona` resolve the catalogue once
  (`prime_with`) (Code #5).
- The revoked-trust cancel fires only on an answer not yet served, so a question delivered
  before the revocation does not cancel; two negative controls, mutation-checked (Test #1,
  Integrity #7).
- A stuck peer handover runs in its own process group, which is killed as a whole and then
  reaped by polling; the sidecar it started has a group of its own and survives; start-idempotence
  rests on the admission claim (Code #7, Integrity #6). Test drops the wall-clock assertion and
  gates `true` on unix (Test #3, #4).
- The freeze test now edits a user-level hurdle too (Code #6); loader failing cases for
  channels, a missing and an unreadable folder, a dangling member (Test #2).
- A user-level `external` admits only from workspaces the running repository trusts — tested
  through the binary and written in the guide (Integrity #2); the guide no longer calls a hiding
  "never silent" but says where it is shown (Integrity #1, the lock question filed as 6j6v.3mgy).
- Stale prose and wraps (Code #4, #9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT
cabcookie added a commit that referenced this pull request Oct 4, 2026
…s (nxf 6j6v.7k58, 6j6v.dcpd) (#17)

* One persona definition for every repository: a user-level declaration folder, merged per name (nxf 6j6v.7k58)

Beside a workspace's own `.nxs-personas/`, every workspace reads the user-level folder
`<service home>/personas` — `~/.nexusflow/personas` for the installed suite and an embedding
app, `~/.nexusflow-<name>/personas` for a development build. A persona or channel the
repository declares hides the user-level one of the same name; everything else is added.

- The merge lives in `Definitions::resolve` alone, so the CLI, every `Engine` verb, `nxs prime`
  and a spawned persona's own `nxc` see one team. Duplicates are judged per folder (naming it);
  handle form and flow cycles on the merged team.
- `DeclarationSource` carries `user_path`, `from_user` and `shadowed`, all off the wire when
  there is no user-level declaration, so such a machine reads exactly as before (point 8).
- `nxc list` / `Engine::directory` stamp `origin: user` per entry and print the account
  whenever the user-level folder takes part; `nxs prime` says it under "Declarations".
- A user-level persona's session start says where its declaration lives: a relative path in a
  prompt means the repository the session runs in, never the declaration's folder.
- The freeze projects the merged catalogue, so it holds for user-level hurdles and prompts;
  proved by an edit mid-operation.
- The readers that loaded the folder directly (thread/search channel policy, list, prime) go
  through the merged catalogue now.

Proved through the real binary: two repositories without a pm file both run the user-level
pm, the commission scenario runs on it, a repository file hides it out loud; the live test
with real sessions now uses one user-level pm for both repositories (22 s, green).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

* Border follow-ups from the review of PR #14: revoked trust is named, a stuck handover is stopped, the cooperative depth is written down (nxf 6j6v.dcpd)

1. When the caller's workspace stops trusting the receiver while a border thread is open, the
   receiver's newest message is unvouched and steers nothing. The caller's coordinator now
   cancels at once with "the answer arrived from <peer>, a workspace no longer trusted here"
   and wakes the commissioner, instead of "no sign of life" two hours later.
2. The depth across the border rests on the trusted coordinator's stamp — within the trust
   model (spec section 4); said where `external` and the depth guard are documented.
3. A peer handover past HANDOVER_WAIT is killed and reaped, so the long-lived service no
   longer accumulates stuck children.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

* Pin the in-process home through pinned_home_env, and mark the dcpd fragment's facade impact

The two single-test binaries set HOME by hand, which the source gate
every_pinned_home_pins_the_instance refuses: a pinned home without the instance resolves
another directory under this repo's .envrc. They now apply nxs_test_support::pinned_home_env()
to their own process. The dcpd fragment says `facade: changed` (a behaviour change behind
unchanged signatures: an unvouched answer now cancels the border thread).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

* Mitigate the review of PR #17: hermetic user-level folder, channel-only policy reads, named revoked-trust cause, group stop

- A build reads a user-level folder only under a NAMED development instance, never the
  production one, and an unresolvable home or instance means no folder rather than an error
  (Code #2, #3; Integrity #3, #4). Guard test: a build under the production instance does not
  see personas planted in the production folder.
- An existing but unreadable user-level folder is an error, not a silence (Integrity #5).
- `threads show`, `search`, `Engine::thread` and `Engine::search` read the merged channels
  only (`merged_channels`), so a malformed persona file cannot make a thread unreadable (Code #1).
- `nxc prime`, `Engine::prime_as` and `nxs prime --persona` resolve the catalogue once
  (`prime_with`) (Code #5).
- The revoked-trust cancel fires only on an answer not yet served, so a question delivered
  before the revocation does not cancel; two negative controls, mutation-checked (Test #1,
  Integrity #7).
- A stuck peer handover runs in its own process group, which is killed as a whole and then
  reaped by polling; the sidecar it started has a group of its own and survives; start-idempotence
  rests on the admission claim (Code #7, Integrity #6). Test drops the wall-clock assertion and
  gates `true` on unix (Test #3, #4).
- The freeze test now edits a user-level hurdle too (Code #6); loader failing cases for
  channels, a missing and an unreadable folder, a dangling member (Test #2).
- A user-level `external` admits only from workspaces the running repository trusts — tested
  through the binary and written in the guide (Integrity #2); the guide no longer calls a hiding
  "never silent" but says where it is shown (Integrity #1, the lock question filed as 6j6v.3mgy).
- Stale prose and wraps (Code #4, #9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sipejn8YQhKgngxeHpMhT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Opt out of changelog-check (PRs with no user impact)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant