feat(packs): support unsigned packs from a static internal registry - #1389
Open
vjeantet wants to merge 4 commits into
Open
feat(packs): support unsigned packs from a static internal registry#1389vjeantet wants to merge 4 commits into
vjeantet wants to merge 4 commits into
Conversation
Contributor
PR Review SummarySize
Affected crates
Blast radius — ContainedThis PR touches: source code Updated automatically on each push to this PR. |
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 8, 2026 21:33
78741d2 to
22bae4c
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 14, 2026 12:28
22bae4c to
1cdf71e
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 14, 2026 14:37
1cdf71e to
c67caa8
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 15, 2026 18:09
c67caa8 to
4cf3e71
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 16, 2026 12:10
4cf3e71 to
fb755b3
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 16, 2026 18:08
fb755b3 to
f4d788a
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 17, 2026 12:13
f4d788a to
788905c
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
July 17, 2026 18:09
788905c to
c5b7437
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
August 2, 2026 13:03
853f8b3 to
cbb53a2
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
August 5, 2026 07:10
cbb53a2 to
9d64e32
Compare
vjeantet
force-pushed
the
feat/static-registry
branch
from
August 5, 2026 19:56
9d64e32 to
3fc6ef2
Compare
vjeantet
marked this pull request as ready for review
August 5, 2026 20:13
There was a problem hiding this comment.
nogent code review
2 potential bugs bypassing resolve_registry, 1 minor robustness issue in highest_published_version.
Findings (not tied to a changed line):
- 🐛 [MEDIUM · bug]
crates/nono-cli/src/package_cmd.rs:515— Incrates/nono-cli/src/package_cmd.rsat linesL515andL744, thenono updateandnono outdatedcommands rely solely onstatus.installed_statusfrom the registry response. However, static/internal registries serve flat JSON status files whereinstalled_statusis omitted (deserializing toNone). Inrun_update(lineL515),Nonefalls into the wildcard match_and is treated as outdated, triggering redundant/pullHTTP requests for every package even if they are already up-to-date. Inrun_outdated(lineL744),Nonedefaults to"unknown", preventing the command from identifying packages as up-to-date. Fix this by performing a client-side semver comparison (pkg.version == latest) whenstatus.installed_statusisNoneor"unknown".
Automated code + security review. CI already covers clippy, rustfmt, tests, cargo-audit and commit-lint.
vjeantet
added a commit
to vjeantet/nono
that referenced
this pull request
Aug 5, 2026
The zero-network guards in the migration provider lookup and the pack update hint read config.registry.verify straight from config.toml. That skipped the rest of the precedence chain, so an operator who opted into the air-gapped posture with NONO_REGISTRY_INSECURE=1 alone still paid a background request to a registry that cannot answer it. Route both through resolve_registry, the single place that folds the flag, the env var and the config together. Not a weakening of any check: disabling verification stays an explicit operator decision, and nothing that was verified before is skipped now. Also skip non-directories in highest_published_version. A stray file named like a semver next to the real version directories could win the comparison, and the follow-up read of versions/<latest>/pull would then fail with ENOTDIR. file_type() is an lstat, so a symlinked version directory is rejected too - the tree is generated by publish-static itself, and the stricter reading avoids reopening a TOCTOU surface. Reported in review of nolabs-ai#1389. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
vjeantet
added a commit
to vjeantet/nono
that referenced
this pull request
Aug 6, 2026
The zero-network guards in the migration provider lookup and the pack update hint read config.registry.verify straight from config.toml. That skipped the rest of the precedence chain, so an operator who opted into the air-gapped posture with NONO_REGISTRY_INSECURE=1 alone still paid a background request to a registry that cannot answer it. Route both through resolve_registry, the single place that folds the flag, the env var and the config together. Not a weakening of any check: disabling verification stays an explicit operator decision, and nothing that was verified before is skipped now. Also skip non-directories in highest_published_version. A stray file named like a semver next to the real version directories could win the comparison, and the follow-up read of versions/<latest>/pull would then fail with ENOTDIR. file_type() is an lstat, so a symlinked version directory is rejected too - the tree is generated by publish-static itself, and the stricter reading avoids reopening a TOCTOU surface. Reported in review of nolabs-ai#1389. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
vjeantet
force-pushed
the
feat/static-registry
branch
from
August 6, 2026 18:48
6ae5a77 to
826f5ec
Compare
vjeantet
added a commit
to vjeantet/nono
that referenced
this pull request
Aug 8, 2026
The zero-network guards in the migration provider lookup and the pack update hint read config.registry.verify straight from config.toml. That skipped the rest of the precedence chain, so an operator who opted into the air-gapped posture with NONO_REGISTRY_INSECURE=1 alone still paid a background request to a registry that cannot answer it. Route both through resolve_registry, the single place that folds the flag, the env var and the config together. Not a weakening of any check: disabling verification stays an explicit operator decision, and nothing that was verified before is skipped now. Also skip non-directories in highest_published_version. A stray file named like a semver next to the real version directories could win the comparison, and the follow-up read of versions/<latest>/pull would then fail with ENOTDIR. file_type() is an lstat, so a symlinked version directory is rejected too - the tree is generated by publish-static itself, and the stricter reading avoids reopening a TOCTOU surface. Reported in review of nolabs-ai#1389. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
vjeantet
force-pushed
the
feat/static-registry
branch
from
August 8, 2026 07:53
826f5ec to
ef43ca9
Compare
vjeantet
added a commit
to vjeantet/nono
that referenced
this pull request
Aug 8, 2026
The zero-network guards in the migration provider lookup and the pack update hint read config.registry.verify straight from config.toml. That skipped the rest of the precedence chain, so an operator who opted into the air-gapped posture with NONO_REGISTRY_INSECURE=1 alone still paid a background request to a registry that cannot answer it. Route both through resolve_registry, the single place that folds the flag, the env var and the config together. Not a weakening of any check: disabling verification stays an explicit operator decision, and nothing that was verified before is skipped now. Also skip non-directories in highest_published_version. A stray file named like a semver next to the real version directories could win the comparison, and the follow-up read of versions/<latest>/pull would then fail with ENOTDIR. file_type() is an lstat, so a symlinked version directory is rejected too - the tree is generated by publish-static itself, and the stricter reading avoids reopening a TOCTOU surface. Reported in review of nolabs-ai#1389. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
vjeantet
force-pushed
the
feat/static-registry
branch
from
August 8, 2026 17:39
ef43ca9 to
47dffd8
Compare
vjeantet
added a commit
to vjeantet/nono
that referenced
this pull request
Aug 10, 2026
The zero-network guards in the migration provider lookup and the pack update hint read config.registry.verify straight from config.toml. That skipped the rest of the precedence chain, so an operator who opted into the air-gapped posture with NONO_REGISTRY_INSECURE=1 alone still paid a background request to a registry that cannot answer it. Route both through resolve_registry, the single place that folds the flag, the env var and the config together. Not a weakening of any check: disabling verification stays an explicit operator decision, and nothing that was verified before is skipped now. Also skip non-directories in highest_published_version. A stray file named like a semver next to the real version directories could win the comparison, and the follow-up read of versions/<latest>/pull would then fail with ENOTDIR. file_type() is an lstat, so a symlinked version directory is rejected too - the tree is generated by publish-static itself, and the stricter reading avoids reopening a TOCTOU surface. Reported in review of nolabs-ai#1389. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
Enable air-gapped macOS fleets to distribute and update nono profiles, Claude Code skills/plugins/hooks, and arbitrary enterprise files from an internal static (nginx) registry, with signature verification disabled and integrity guaranteed by per-artifact SHA-256. - Add a `[registry]` config.toml section (url + verify, fail-secure true) and a `resolve_registry` helper with precedence --registry > NONO_REGISTRY > config > default; verify off via --insecure flag, NONO_REGISTRY_INSECURE, or [registry].verify=false. - Pull path: when verify is off, skip Sigstore bundle download/verify, namespace assertion, and subject-membership; keep the SHA-256 check. Record an `unsigned:internal-registry` signer sentinel in the lockfile and write no `.nono-trust.bundle`. - Run-time guard: `verify_profile_packs` skips bundle re-verification for packs whose lockfile signer is the sentinel, keyed on the lockfile so signed packs always get full re-verification. SHA-256 still enforced. - Make PullResponse tolerant of the minimal static JSON (optional provenance, default bundle_url/scan_passed/size_bytes). - Suppress registry phone-home (update hints + migration provider lookup) when verification is disabled. - Add `nono pack publish-static` to emit the static registry tree (versions/<v>/pull, versions/latest/pull, status, files/...) reusing the serde types, with `latest` tracking the highest published semver. - Document the air-gapped/internal-registry workflow. Adds unit tests (registry precedence, fail-secure verify, minimal deserialization, generator round-trip, latest tracking, sentinel guard + tamper detection). Signed-off-by: vjeantet <valere.jeantet@gmail.com>
Show an air-gapped GitLab pipeline that seeds the docroot from the nginx host, runs nono pack publish-static to add the current version, then rsyncs back. Seeding keeps latest/status at the true highest semver and makes rsync --delete safe. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
Pulling from a static or internal registry without --insecure failed with "failed to parse bundle: JSON error: expected value at line 1 column 1". publish-static emits an empty bundle_url by design, and handing that empty URL to the bundle loader surfaced the JSON parser's error instead of the actual situation. This is the first thing an operator hits when pointing nono at an internal registry, so the message mattered. Return NonoError::PackageVerification from download_and_verify_artifacts when verification is requested but the pull response advertises no bundle, before any network round-trip. The message names the cause and both ways out: --insecure for a single command, [registry] verify = false fleet-wide. Security behaviour is unchanged: the install is still refused and nothing is written. Only the diagnostic changes. The guard is gated on `verify`, so an absent bundle remains the expected shape for unsigned installs. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
The zero-network guards in the migration provider lookup and the pack update hint read config.registry.verify straight from config.toml. That skipped the rest of the precedence chain, so an operator who opted into the air-gapped posture with NONO_REGISTRY_INSECURE=1 alone still paid a background request to a registry that cannot answer it. Route both through resolve_registry, the single place that folds the flag, the env var and the config together. Not a weakening of any check: disabling verification stays an explicit operator decision, and nothing that was verified before is skipped now. Also skip non-directories in highest_published_version. A stray file named like a semver next to the real version directories could win the comparison, and the follow-up read of versions/<latest>/pull would then fail with ENOTDIR. file_type() is an lstat, so a symlinked version directory is rejected too - the tree is generated by publish-static itself, and the stricter reading avoids reopening a TOCTOU surface. Reported in review of nolabs-ai#1389. Signed-off-by: vjeantet <valere.jeantet@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(packs): support unsigned packs from a static internal registry
Closes #1388. Related to #857.
What
Enables air-gapped macOS fleets to distribute and update nono profiles,
Claude Code skills/plugins/hooks, and arbitrary enterprise files from an
internal static (nginx) registry, with signature verification disabled
and integrity guaranteed by per-artifact SHA-256.
This is the lower-friction, integrity-only transport discussed in
#1388 (a complement to the OCI direction of #857, not a replacement).
The keyed ECDSA trust mode is a separate follow-up PR layered on the same
[registry]config.Changes
[registry]section (url+verify, fail-secureverify = trueby default) and aresolve_registryhelper with precedence--registry>NONO_REGISTRY> config > default. Verification is turnedoff only via explicit
--insecure,NONO_REGISTRY_INSECURE, or[registry].verify = false.download/verify, namespace assertion, and subject-membership check; keep
the SHA-256 check. Record an
unsigned:internal-registrysigner sentinelin the lockfile and write no
.nono-trust.bundle.verify_profile_packsskips bundle re-verificationonly for packs whose lockfile signer is the sentinel, keyed on the
lockfile so signed packs always get full re-verification. SHA-256 stays
enforced.
PullResponsetolerates the minimal static JSON (optionalprovenance; default
bundle_url/scan_passed/size_bytes).migration provider lookup) when verification is disabled.
nono pack publish-staticemits the static registry tree(
versions/<v>/pull,versions/latest/pull,status,files/…) reusingthe serde types, with
latesttracking the highest published semver.Security notes
never the default.
verify = true; SHA-256 enforced atinstall and at runtime; the lockfile-keyed sentinel guard prevents a
signed pack from being silently downgraded to integrity-only.
Tests
Unit tests: registry precedence, fail-secure verify resolution, minimal
static JSON deserialization,
publish-staticgenerator round-trip,latesttracking, sentinel runtime guard + tamper detection.
cargo check -p nono-cliand the crate test suite pass locally on macOS.Stacking
Base branch:
feat/static-registryoffupstream/main. The keyed-trust PRis stacked on top of this one and should be reviewed/merged after it.
Agent compliance check (AGENTS.md)
maintainer guidance (see risks section of the issue) before merge
existing
nono::trust/ registry serde types; no external code copied)not yet approved (issue is a proposal pending maintainer direction)