Skip to content

feat(security)!: enforce trustPolicy by default, add paranoid bundle, security docs - #333

Merged
jdx merged 11 commits into
mainfrom
claude/interesting-swanson-9a4e02
Apr 27, 2026
Merged

jdx merged 11 commits into
mainfrom
claude/interesting-swanson-9a4e02

Conversation

@jdx

@jdx jdx commented Apr 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Activates trustPolicy=no-downgrade end-to-end (previously a documented but no-op setting), adds a paranoid bundle switch for the strict-security posture, ships a security overview doc + GitHub-recognized SECURITY.md, and flips the trustPolicy default to on. Mirrors pnpm's failIfTrustDowngraded algorithm exactly — verified against pnpm's source + test suite at /tmp/pnpm.

BREAKING CHANGE: installs of packages whose newest version lost trust evidence (provenance attestation or trusted-publisher OIDC link) compared to any earlier published version will now fail unless the user opts out via trustPolicy = off, trustPolicyExclude, or trustPolicyIgnoreAfter. Drop the ! from the title before merge to delay this to a later major.

Trust enforcement

Trust evidence ranks: _npmUser.trustedPublisher (2) > dist.attestations.provenance (1). Install fails when any earlier-published version of the same package carried stronger evidence than the picked one.

  • Algorithm in crates/aube-resolver/src/trust.rs — exact mirror of pnpm's failIfTrustDowngraded including the v10.24.0+ "skip prior prereleases when picking stable" rule and the JS-style truthiness on both evidence fields.
  • trustPolicyExclude parser supports the full pnpm syntax: name, name@1.0.0, name@1.0.0 || 1.0.1 (exact versions only — ^/~/>= rejected), is-* name glob, scoped names. Exclude check runs before the time lookup so excluded packages don't error on registries that omit time.
  • Resolver integration at resolve.rs:1271 — fires immediately after PickResult::Found, mirrors pnpm's placement. Forces the full-packument fetch path so per-version time is available, including suppressing the corgi-shortcircuit when both minimumReleaseAge and trustPolicy are active.
  • Graceful degradation when the registry doesn't ship time at all (Verdaccio fixtures, private mirrors). Per-version time gaps still error — that's the registry-quirk shape pnpm's check catches.

paranoid master switch

paranoid: true forces every setting in the strict-security bundle on, regardless of how each is configured individually:

Setting Without paranoid With paranoid
trustPolicy no-downgrade (default) no-downgrade (forced)
jailBuilds false true
minimumReleaseAgeStrict falls back to lowest satisfying version fails the install
strictStoreIntegrity warns on missing dist.integrity fails the install
strictDepBuilds silently skips unreviewed dep build scripts fails after link

Default flip

trustPolicy defaults to no-downgrade. Both the codegen settings layer and TrustPolicy::default() agree (the latter aligned with the former in 30e3cd8 so library consumers calling Resolver::new directly inherit the documented behavior).

Cost analysis: trust check itself is microseconds (a few BTreeMap::get lookups). Cold-cache installs pay one full-packument fetch instead of corgi (~3-5× larger response); warm-cache installs are free. Same hit minimumReleaseAge already takes today.

Documentation

  • SECURITY.md at the repo root — GitHub-recognized vulnerability disclosure path (private advisory form, since issues are disabled) + at-a-glance feature table.
  • docs/security.md — comprehensive overview covering paranoid, default-deny lifecycle scripts, jailed builds, trust policy, minimum release age, exotic-deps blocking, tarball/CAS integrity, auth-token handling, aube audit. Each section links to the relevant setting.
  • VitePress sidebar gets a new "Security" section.
  • README + landing-hero copy point at /security and accurately list defaults vs opt-ins.
  • Settings docstrings rewritten in settings.toml and regenerated into docs/settings/index.md.

Test plan

  • cargo test --workspace — 297 + 162 + ~700 more tests pass, including 30+ new trust unit tests (truthiness, exclude parsing, prerelease handling, exclude-bypasses-missing-time, empty-time-map skip, ignore-after cutoff, downgrade detection)
  • Two resolver-level integration tests using a TCP test server: one asserts Error::TrustDowngrade fires + trustPolicyExclude unblocks, one is the regression test for the corgi-shortcircuit interaction with minimumReleaseAge
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • All bats files affected by the default flip pass locally (bundled_dependencies, add, global_install, node_gyp_rebuild)
  • End-to-end smoke against npmjs.org with a real package known to have lost provenance between versions

Review-comment fixes landed

  • Cursor Bugbot — corgi-shortcircuit suppression with concurrent minimumReleaseAge + trustPolicy
  • Cursor Bugbot — provenance evidence now is_truthy-gated to match trustedPublisher
  • Cursor Bugbot — TrustPolicy::default() aligned with settings.toml default
  • Greptile — is_truthy covers all JS falsy values (0, "", NaN)
  • Greptile — TrustExcludeRules::parse_lossy returns (rules, errors) so we don't double-parse with a silent fail-open fallback
  • Greptile — README/hero copy no longer implies trust-downgrade detection is default-on (it is now, after the default flip — copy updated again accordingly)
  • Greptile — SECURITY.md table corrected (trustPolicy is a string enum, not a bool)

🤖 Generated with Claude Code

jdx and others added 5 commits April 26, 2026 19:55
…ust evidence

Mirrors pnpm's `failIfTrustDowngraded` algorithm. Two evidence sources
ranked `_npmUser.trustedPublisher` (2) > `dist.attestations.provenance`
(1); installation fails when an earlier-published version of the same
package carried stronger evidence than the version being picked.

Previously `trustPolicy`, `trustPolicyExclude`, and `trustPolicyIgnoreAfter`
were plumbed end-to-end into `DependencyPolicy` but the resolver never
read them — a silent no-op that made the documented setting misleading.
The settings.toml docstring even acknowledged the gap.

`trustPolicyExclude` parser supports the full pnpm syntax: `name`,
`name@1.0.0`, `name@1.0.0 || 1.0.1` (exact versions only — ranges
rejected), `is-*` name globs, and scoped names. Exclude check runs
before the time lookup so excluded packages don't error on registries
that omit `time`. Picking a stable version ignores prior prerelease
evidence (matches pnpm v10.24.0+).

Default stays `off`; planned to flip to `no-downgrade` in the next
major version (mirrors the `jailBuilds` v2-default plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Builds

`paranoid=true` forces both `trustPolicy = no-downgrade` (override) and
`jailBuilds = true` (logical OR), regardless of how those settings are
configured individually. One switch for the strict-security posture
without remembering each underlying setting name.

Also fixes formatting that CI rejected on the previous commit (rustfmt
wanted alternate line breaks in three trustpolicy files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the full security feature surface in one place: paranoid switch,
default-deny lifecycle scripts, jailed builds, trustPolicy, minimumReleaseAge,
blockExoticSubdeps, tarball/CAS integrity, auth-token handling, and
`aube audit`. Each section links to the settings reference.

Root SECURITY.md is the GitHub-recognized vulnerability disclosure file —
points reporters at the private security advisory form (issues are disabled
on this repo) and gives a one-glance feature table linking to the docs page.

Sidebar gets a new "Security" section with the overview + jailed builds entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README "Secure defaults" bullet now points at the security overview rather
than just the jailed-builds page, lists trust downgrades alongside the
existing protections, and surfaces the `paranoid: true` master switch.

Hero proof-item retargeted from /package-manager/jailed-builds to /security,
with the visual rows updated to reflect what's actually default-on today
(exotic deps blocked, trust downgrades blocked, jailed builds opt-in).
Headline broadened from "Jailed builds for dependency scripts" to "Supply-
chain defaults across the install path".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/aube-resolver/src/resolve.rs Resolved
@greptile-apps

ghost commented Apr 27, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Wires the previously-stubbed trustPolicy=no-downgrade enforcement end-to-end: new aube_registry fields extract _npmUser.trustedPublisher (rank 2) and dist.attestations.provenance (rank 1) from packuments, aube-resolver/src/trust.rs implements the pnpm-mirrored downgrade check, and the resolver integration correctly suppresses the corgi-packument shortcircuit when the policy is active so per-version time data is always present. The paranoid bundle switch is fully wired for all five documented settings (trustPolicy, jailBuilds, minimumReleaseAgeStrict, strictStoreIntegrity, strictDepBuilds). Test coverage is thorough: 25+ unit tests in trust::tests plus two resolver-level integration tests with a real TCP server.

Confidence Score: 5/5

Safe to merge; all findings are P2 (cosmetic error-message quality and a PR-description discrepancy).

No P0 or P1 bugs found. The algorithm correctly mirrors pnpm's failIfTrustDowngraded, the corgi-shortcircuit suppression is properly guarded and regression-tested, the is_truthy implementation correctly handles all JS falsy values including 0/""/[], and parse_lossy prevents a single malformed exclude from silently dropping all rules. Only P2 findings remain.

No files require special attention; crates/aube-resolver/src/types.rs carries the default-flip that is intentional but contradicts the PR description prose.

Important Files Changed

Filename Overview
crates/aube-resolver/src/trust.rs New trust-policy module: evidence_for, check_no_downgrade, TrustExcludeRules; 25+ unit tests covering all edge cases; JS truthiness implementation is correct (empty arrays are truthy in JS, addressing the previous review thread concern).
crates/aube-resolver/src/resolve.rs Trust check integrated immediately after PickResult::Found; minimum_release_age_only shortcircuit correctly suppressed when trustPolicy=NoDowngrade to prevent corgi packument from omitting per-version time data needed by the check.
crates/aube-resolver/src/types.rs TrustPolicy::NoDowngrade is now the #[default] variant (previously Off), and trust_policy_exclude changed from BTreeSet<String> to TrustExcludeRules; breaking change for library consumers relying on DependencyPolicy::default().
crates/aube/src/commands/install/settings.rs paranoid flag wires all five bundle settings; trust_policy_exclude now parsed with parse_lossy so one malformed entry doesn't silently drop all exclude rules (addresses previous review thread concern).
crates/aube-registry/src/lib.rs Adds NpmUser and Attestations structs with tolerant _npmUser deserializer that degrades pre-2010 string-form packuments to None instead of failing the whole parse; well-tested with 5 new unit tests.
crates/aube-resolver/src/tests.rs Two new integration tests: one verifying the corgi-shortcircuit suppression and one asserting TrustDowngrade fires for a 3-version packument with evidence loss plus verifying trustPolicyExclude unblocks it.
crates/aube-resolver/src/error.rs Two new error variants TrustDowngrade and TrustCheckMissingTime with miette help text suggesting all three escape hatches.
crates/aube-resolver/src/builder.rs should_record_times predicate extended to include trust_policy == NoDowngrade so the full-packument fetch path activates when trust policy is on.
crates/aube-settings/settings.toml Default for trustPolicy changed from "off" to "no-downgrade", new paranoid bundle setting added, exclude pattern syntax documented; default flip is a user-visible breaking change.
crates/aube-manifest/src/workspace.rs Adds paranoid: Option<bool> field with serde rename/default for workspace config.
crates/aube/src/commands/install/lifecycle.rs paranoid=true forces jail builds on via `

Fix All in Claude Code

Reviews (4): Last reviewed commit: "fix(resolver): align TrustPolicy::defaul..." | Re-trigger Greptile

Comment thread README.md Resolved
Comment thread crates/aube-resolver/src/trust.rs Resolved
jdx and others added 2 commits April 26, 2026 20:20
The corgi-shortcircuit at resolve.rs:219-235 returns the abbreviated
packument early when the top-level `modified` timestamp predates the
`minimumReleaseAge` cutoff, on the assumption that every selectable
version is old enough to skip the per-version time check. That
assumption breaks when `trustPolicy=NoDowngrade` is also active —
the trust check needs per-version `time` entries to compare evidence
across versions, and corgi packuments don't carry them.

Without the fix, the install fails with a spurious
`TrustCheckMissingTime` error on every version when both
`minimumReleaseAge` and `trustPolicy=NoDowngrade` are set.

Reported by Cursor Bugbot on PR #333. Regression test in
trust_policy_disables_minimum_release_age_short_circuit verifies the
shortcircuit is suppressed by serving differentiated corgi vs full
responses based on the Accept header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…E copy

Three review-comment fixes from greptile-apps[bot] on PR #333:

1. `is_truthy` now matches JS semantics — falsy includes `0`, `0.0`, `""`,
   `NaN`, not just `null`/`false`. In practice `_npmUser.trustedPublisher`
   is always an object or absent, but matching JS keeps a hostile or
   buggy registry from sneaking trust evidence past us via `0` or `""`.

2. `TrustExcludeRules::parse_lossy` returns `(Self, Vec<Error>)` so the
   caller can log per-pattern errors without parsing the list twice. The
   previous code parsed each entry to filter, then re-parsed the whole
   list and silently fell back to empty rules on any error — exactly the
   security-regression shape the comment warned against.

3. README and landing-hero copy no longer imply that trust-downgrade
   detection and the build jail are default-on. Both genuinely default
   to off (planned to flip in v2). The bullet now separates real
   defaults (exotic deps blocked, lifecycle scripts deny-by-default)
   from `paranoid: true` opt-ins (trust-downgrade detection, jailed
   approved scripts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread crates/aube-resolver/src/trust.rs Resolved
jdx and others added 2 commits April 26, 2026 20:37
… bundle

Two behavior changes for v1.next:

1. `trustPolicy` default flipped from `off` to `no-downgrade`. Installs
   that previously succeeded will now fail at resolve when a picked
   version carries weaker trust evidence than any earlier-published
   version of the same package. Escape hatches unchanged:
   `trustPolicy = off`, `trustPolicyExclude`, or `trustPolicyIgnoreAfter`.
   Settings.toml's "planned to default in next major" disclaimer is gone;
   this is now the live default.

2. `paranoid` bundle extended from {trustPolicy, jailBuilds} to also force
   `minimumReleaseAgeStrict`, `strictStoreIntegrity`, and `strictDepBuilds`
   on. The bundle now turns every soft gate into a hard fail.

Also fixes a Cursor Bugbot catch from the prior commit: the `provenance`
trust-evidence check used `.is_some()` while `trustedPublisher` used
`is_truthy`. Pnpm's source evaluates both fields in JS boolean context,
so `provenance` is now `is_truthy`-gated too — `provenance: false` no
longer counts as evidence.

README and landing-hero copy updated to reflect that trust-downgrade
detection is now default-on, not gated behind paranoid.

BREAKING CHANGE: installs of packages whose newest version lost trust
evidence (provenance attestation or trusted-publisher OIDC link) compared
to any earlier published version will now fail unless the user opts out.
The trust check fires only when the registry serves per-version `time`
entries; cold-cache installs see a slightly larger packument fetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the existing `evidence_falsy_trusted_publisher_is_none` test
for the provenance code path, locking in the `is_truthy` gate that
089631a added so a future refactor can't silently regress to
`.is_some()` and let `provenance: false` count as evidence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread SECURITY.md Resolved

ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit adf2b61. Configure here.

Comment thread crates/aube-settings/settings.toml Resolved
jdx and others added 2 commits April 26, 2026 20:48
Bats failures on PR #333: every install against the local Verdaccio
fixtures was failing with TrustCheckMissingTime because the fixtures
ship packuments without per-version `time` entries. Same pattern
exists for private registry forks and proxies that strip the field.

When the entire `time` map is empty, skip the trust check rather than
failing every install. Diverges from pnpm's strict-throw — defensible
because pnpm requires opt-in (so users explicitly accepted the
strictness contract), while aube now defaults trust on (so a single
non-compliant registry would brick first-install for every aube user).

Per-version time gaps (where `time` IS shipped but lacks the picked
version specifically) still error — that's the registry-quirk shape
pnpm's check is designed to catch, and skipping it would let an
attacker with a tampered packument bypass the check by stripping
just the relevant time entry.

Also fixes greptile P1: SECURITY.md table had `trustPolicy` listed
with default `true` (a boolean) — but trustPolicy is a string-enum
setting. Now reads `"no-downgrade"`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The settings.toml default flipped to `no-downgrade` in 089631a, but
the Rust-side `#[default]` on `TrustPolicy` was still `Off`. The install
command path overrides via the codegen accessor so end users were fine,
but library consumers calling `Resolver::new(...)` (which uses
`DependencyPolicy::default()`) were silently getting trust checking
disabled despite the docs.

Caught by Cursor Bugbot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx changed the title feat(resolver): enforce trustPolicy=no-downgrade against packument trust evidence feat(security)!: enforce trustPolicy by default, add paranoid bundle, security docs Apr 27, 2026
@jdx
jdx merged commit cc15fc4 into main Apr 27, 2026
@jdx
jdx deleted the claude/interesting-swanson-9a4e02 branch April 27, 2026 01:59
@greptile-apps greptile-apps Bot mentioned this pull request Jun 11, 2026
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