Skip to content

feat(npm): distribute the ant CLI via npm as @withautonomi/ant - #192

Open
jacderida wants to merge 3 commits into
WithAutonomi:mainfrom
jacderida:chrisoneil/v2-1152-distribute-the-ant-cli-via-npm
Open

jacderida wants to merge 3 commits into
WithAutonomi:mainfrom
jacderida:chrisoneil/v2-1152-distribute-the-ant-cli-via-npm

Conversation

@jacderida

@jacderida jacderida commented Sep 4, 2026

Copy link
Copy Markdown
Member

Linear issue

Closes V2-1152

https://linear.app/autonominetwork/issue/V2-1152/distribute-the-ant-cli-via-npm
(mirrors #190)

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Mostly packaging and CI, which alone would be T0. Proposed as T1 because two
client-side behaviours do change: ant update refuses to self-replace on a
package-manager-owned install, and ant now falls back to a bootstrap peer list
compiled into the binary when no config file is present. Neither touches node
behaviour, the wire protocol, stored-data format, or payments. The upgrade
mechanism is narrowed rather than altered — self-replacement is unchanged for
every install that exists today, and is only declined where it would corrupt a
package manager's view of its own files.

Compatibility

  • Wire: none.
  • Storage: none. bootstrap_peers.toml moved from resources/ to
    ant-core/resources/ in the repo, but the release archives contain the same
    file with the same bytes at the same path inside the archive, and both
    installers copy it to the same config-directory destination as before.
  • API: additive only. New: ant_core::install (InstallMethod, detect,
    classify_path), ant_core::update::DeferredUpdate,
    ant_core::config::embedded_bootstrap_peers. No existing signature changed.
    ant_core::update::perform_update gains an early error return for
    package-manager-owned installs — a new failure mode on a path that previously
    always attempted replacement.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

Gates, mirroring what ci.yml runs:

Command Result
cargo fmt --all -- --check clean
cargo clippy --all-targets --all-features -- -D warnings clean
cargo test --lib --all 623 passed, 0 failed (run 4×)
cargo test -p ant-core --test unit_self_encrypt 16 passed, 2 ignored
cargo test -p ant-core --test merkle_unit 8 passed
cargo test -p ant-core --test daemon_integration --test node_add_integration -- --test-threads=1 6 + 3 passed

The devnet-dependent e2e suites (e2e_chunk, e2e_data, e2e_file, e2e_payment,
e2e_security, e2e_cost_estimate, e2e_adr0004, e2e_merkle) were not run
locally — they need a live devnet, and CI runs them in their own jobs. Nothing here
touches the upload, payment or chunk paths.

One flake seen and not reproduced: cached_single::roundtrip_save_load_delete
failed once in a full-workspace run, then passed in isolation and in four
subsequent --lib --all runs. It is a filesystem-receipt test untouched by this
change.

New unit tests: 11 in ant-core/src/install.rs covering npm global, npm local,
nested node_modules, Windows paths, install.sh/cargo/local-build locations,
and a directory whose name merely contains the substring; 2 in
ant-core/src/config.rs covering the embedded peer list parsing to usable
addresses, and an explicitly selected empty devnet manifest still erroring rather
than falling through to the embedded list.

End-to-end dry run

All six packages built by npm/build-packages.sh from staged release archives,
served from a local verdaccio registry, installed with a scratch npm_config_prefix
and XDG_CONFIG_HOME:

Acceptance criterion Result
AC1 — npm install -g @withautonomi/ant gives a working ant --version pass (ant 0.3.6)
AC2 — published binaries byte-identical to the release assets pass (cmp clean, sha256 7d7847f1…)
AC3 — npm uninstall -g removes everything except the config directory pass

Beyond the ACs:

  • With postinstall blocked (npm 12's default), ant -v chunk get … logged
    No bootstrap config file; using 7 embedded bootstrap peer(s) and connected.
  • With --allow-scripts, the postinstall wrote bootstrap_peers.toml to the
    config directory, and a reinstall over a user-edited file left it untouched.
  • ant update --json on the npm-installed binary returned
    "install_method": "npm" with "update_command": "npm update -g @withautonomi/ant";
    ant update --force explained the refusal instead of silently doing nothing.

dist-tag behaviour on a pre-release debut

Tested after the first commit, because the next release is a beta. npm sets latest
from a package's very first publish even when --tag names something else, and
later pre-release publishes move only their own tag:

first publish of 0.3.7-beta.1 with --tag beta
  -> {'latest': '0.3.7-beta.1', 'beta': '0.3.7-beta.1'}
then publish 0.3.8-beta.1 with --tag beta
  -> {'latest': '0.3.7-beta.1', 'beta': '0.3.8-beta.1'}

A bare npm install -g @withautonomi/ant against that registry resolved to
@withautonomi/ant@0.3.7-beta.1 — i.e. a beta debut would have made the command this
PR documents serve a beta, with latest stuck there until a stable release. Publishing
a stable 0.3.7 with --tag latest afterwards reclaimed the tag cleanly
({'beta': '0.3.8-beta.1', 'latest': '0.3.7'}).

The second commit adds a guard: a pre-release is not published when the package does not
yet exist on the registry, so the npm distribution debuts on a stable release. The
dist-tag mapping and gate were replayed offline across 0.3.7, -beta.N, -rc.N,
-alpha.N and -betamax.N against both a 200 and a 404 registry response, and the
registry probe itself was checked against the real registry (@withautonomi/ant → 404,
@stripe/cli → 200).

Not verified locally: cross-platform binaries (only the host target was built;
the other four packages carried a stand-in binary), the real npm registry, OIDC
provenance, and the publish job itself. install-test.yml gains four
workflow_dispatch jobs — npm install on Linux/macOS/Windows plus an
--ignore-scripts job — to cover those against a real published version.

New dependency

None — no new Rust crates. The npm packages declare no runtime dependencies;
the launcher and postinstall use only Node's standard library. Node ≥18 becomes a
requirement for the npm install path only, and the release workflow gains
actions/setup-node.

ADR

n/a — Tier 1.

Mitigation / rollback

The publish job is the last job in the release workflow and runs only after the
GitHub release has been created, so a failure there cannot affect the release. It
also skips itself with a warning when NPM_TOKEN is absent, so merging this before
the token is configured cannot turn a release red. Backing the distribution out
means deprecating the npm packages and deleting the job; no released artifact
changes.

The two client-side behaviours are independently reversible. The ant update
deferral only triggers on a node_modules path — no existing install can reach it.
The embedded peer fallback only runs where the previous code returned
Error::NoBootstrapPeers, so it can turn a hard failure into a success but never
change the outcome of a currently-working configuration.

🤖 Generated with Claude Code

https://claude.ai/code/session_012H3LsaffUstGkhHQdZeQD3

jacderida and others added 2 commits September 8, 2026 17:11
Agent sandboxes and locked-down CI allow package-manager traffic by default
while blocking direct binary downloads — Claude Code cloud sessions return 403
for release assets of repos not attached to the session, and skill-directory
scanners flag piped installers while passing package managers. This makes
getting `ant` onto such a machine a one-liner.

Packaging only: nothing is rebuilt. The npm tarballs carry the same binaries
the release workflow has already built and signed, verified against
SHA256SUMS.txt and their ML-DSA-65 signatures before packaging, so they are
byte-identical to the GitHub release assets. install.sh, install.ps1 and the
release page are untouched.

Six packages, following the mechanism esbuild established: a meta package
plus one per platform, guarded by os/cpu and declared as optionalDependencies,
so npm skips the four that do not match and a user downloads exactly one
binary. A JS launcher resolves and execs it rather than a postinstall copying
it into place, because install scripts cannot be relied on (below). Published
from ant-cli-release.yml after the GitHub release, with provenance via OIDC
and a dist-tag derived from the version: latest, beta, or rc — rc builds are
installed by nothing today and must never move latest.

`ant update` now recognises a package-manager-owned install and defers to it.
Self-replacing an npm-managed binary left npm's metadata describing a file
that was no longer there, so the next `npm update` silently rolled the user
back. The check still reports whether a newer version exists, then prints
`npm update -g @withautonomi/ant`; a second guard in ant-core refuses
self-replacement for any other consumer of the crate. Detection is by
resolved-path inspection rather than a build flag, since a build flag would
produce a binary that is not byte-identical to the release asset.

ant-core also falls back to a bootstrap peer list compiled into the binary
when no bootstrap_peers.toml is present. npm 12 blocks package install
scripts by default — allow-scripts is empty out of the box — so the step that
copies that file into the config directory frequently does not run, which
would otherwise leave a CLI that installs and reports its version but fails
every network command, in exactly the sandboxes this change targets.
Priority is unchanged for every existing install (explicit -b peers, then a
devnet manifest, then the config file, then the embedded list), and an
explicitly selected devnet manifest still errors rather than reaching for
mainnet peers. bootstrap_peers.toml moved to ant-core/resources/ because
include_str! cannot reach outside the crate and ant-core is published to
crates.io; the release archives carry the same bytes as before.

Verified end to end against a local registry: the documented one-liner
installs and runs, the installed binary is byte-identical to the archive it
came from, uninstalling leaves the config directory alone, a plain install
with postinstall blocked still reaches the network, and `ant update` on the
npm-installed binary reports install_method "npm".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012H3LsaffUstGkhHQdZeQD3
npm sets the `latest` dist-tag from a package's very first publish even when
`--tag` names something else, and later pre-release publishes move only their
own tag. Verified against a local registry:

  first publish of 0.3.7-beta.1 with --tag beta
    -> {'latest': '0.3.7-beta.1', 'beta': '0.3.7-beta.1'}
  then publish 0.3.8-beta.1 with --tag beta
    -> {'latest': '0.3.7-beta.1', 'beta': '0.3.8-beta.1'}

So if the npm distribution debuted on a beta, `npm install -g @withautonomi/ant`
— the command this repo's README documents — would hand everyone a beta build,
and `latest` would stay pinned to that one beta until a stable release moved it.
The stable channel must never serve a pre-release; that is the guarantee
install.sh and `ant update` already make, and npm should not be the exception.

The publish job now asks the registry whether the package exists and skips a
pre-release publish when it does not, with a warning rather than a failure. The
condition clears itself: at most the pre-releases before the first stable
release are skipped, and a stable publish with --tag latest reclaims the tag.

The registry is queried directly instead of through `npm view`, which resolves
a version spec (defaulting to `latest`) and so would conflate "no such package"
with "no latest tag".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJ7ezqh78fsBWG71pyYmGq
@jacderida
jacderida force-pushed the chrisoneil/v2-1152-distribute-the-ant-cli-via-npm branch from e458702 to 0d1b820 Compare September 8, 2026 16:11
`cargo doc` with `-D warnings`, as the Documentation CI job runs it, rejects
an intra-doc link from the public `embedded_bootstrap_peers` to the private
`EMBEDDED_BOOTSTRAP_PEERS`. Plain code formatting says the same thing without
asking rustdoc to resolve a link it cannot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DJ7ezqh78fsBWG71pyYmGq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant