Skip to content

feat(security): #497 — host allowlist for perry.nativeLibrary + perry.compilePackages#951

Merged
proggeramlug merged 1 commit into
mainfrom
feat/497-host-allowlist
May 18, 2026
Merged

feat(security): #497 — host allowlist for perry.nativeLibrary + perry.compilePackages#951
proggeramlug merged 1 commit into
mainfrom
feat/497-host-allowlist

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #497.

Summary

The two attack surfaces Perry itself introduced over Node — linking arbitrary native code (perry.nativeLibrary) and compiling untrusted TS source into the binary (perry.compilePackages) — were silently honored from any package in the dep graph. This PR makes both privileged operations require explicit host-app opt-in via two new perry.allow.* arrays in the host package.json.

Zero runtime cost — purely compile-time refusal.

Cross-platform: both gates run in the platform-agnostic compile_command driver and collect_modules.rs before any backend (LLVM / WASM / ArkTS / HarmonyOS / Glance / SwiftUI / JS) is invoked.

Refusal diagnostic example

Error: package `hono` is in `perry.compilePackages` but not in
`perry.allow.compilePackages` — compiling untrusted TS into the
binary is a privileged operation and requires explicit host
opt-in. (#497)

Review the package, then add it to your host `package.json`:

  {
    "perry": {
      "allow": { "compilePackages": ["hono"] }
    }
  }

Scope wildcard (`"@scope/*"`) and the universal `"*"` escape hatch
are both supported.

For a one-off build, set `PERRY_ALLOW_PERRY_FEATURES=1` in the
environment.

Pattern syntax

  • Exact match — "lodash".
  • Scope wildcard — "@scope/*".
  • Universal escape hatch — "*".

Default-empty allowlist = nothing allowed (per the issue's "greenfield projects: nothing allowed" acceptance bullet).

Env-var override

PERRY_ALLOW_PERRY_FEATURES=1 opts every name into both allowlists for one build. =0 enforces refusal even when package.json opted in (fail-closed CI gate).

Test coverage

7 unit tests for allowlist_matches:

  • empty_allowlist_blocks_everything — default-deny invariant.
  • exact_match, universal_wildcard, scope_wildcard — primary patterns.
  • non_scoped_wildcards_dont_match — documents the matcher's intentionally narrow shape (no freeform prefix-*).
  • multiple_patterns_or_together — OR semantics across the list.

End-to-end smoke against five shapes (all pass against the release binary):

  1. No compilePackages → clean.
  2. compilePackages without allow → fails with full diagnostic.
  3. compilePackages + matching allow → compiles.
  4. Scope wildcard ("@foo/*" matches @foo/bar, @foo/baz) → compiles.
  5. PERRY_ALLOW_PERRY_FEATURES=1 → compiles regardless of package.json.

Test-fixture migration

7 existing release-test fixtures updated to add perry.allow.compilePackages alongside their existing compilePackages:

  • hono-basic, ink-link-smoke, nestjs-hello, redis-pubsub, axios-get, drizzle-sqlite, test-files/issue_652.

NestJS fixture uses the scope-wildcard form ("@nestjs/*") to demonstrate that mechanism in a real fixture and keep the allow-list short.

Acceptance

  • Host package.json keys: perry.allow.nativeLibrary, perry.allow.compilePackages.
  • Transitive dep introducing either fails build with clear message naming the package + exact JSON to add.
  • Wildcards supported (@scope/* + universal *).
  • Default behavior on greenfield projects: nothing allowed.
  • Documented (docs/src/cli/allow-perry-features.md).

perry audit integration (also in the acceptance list) deferred — the perry audit subcommand itself is #495, not yet implemented.

Notes

No Cargo.toml version bump, no CLAUDE.md version line touch, no CHANGELOG.md entry — maintainer folds those in at merge time to avoid patch-version collisions.

…rry.compilePackages`

The two attack surfaces Perry itself introduced over Node — linking
arbitrary native code (`perry.nativeLibrary`) and compiling untrusted
TS source into the binary (`perry.compilePackages`) — were both
silently honored from any package in the dep graph. This PR makes
both privileged operations require explicit host-app opt-in.

Two new `perry.allow.*` arrays in the host `package.json` (read only
from the host, never from deps):

  {
    "perry": {
      "compilePackages": ["hono"],
      "allow": {
        "compilePackages": ["hono"],
        "nativeLibrary": ["@bloomengine/engine"]
      }
    }
  }

- **`perry.nativeLibrary`** (transitive dep declares it):
  `collect_modules.rs` gates the manifest push on
  `allow.nativeLibrary` containing the package name. Failure bails
  with a diagnostic naming the package and the exact JSON to add.
- **`perry.compilePackages`** (host or workspace declares the list):
  every entry must also be in `allow.compilePackages` — a two-key
  opt-in. Validation deferred until after env-var overrides apply.

Patterns: exact name, scope wildcard `"@scope/*"`, or universal `"*"`
escape hatch. Default-empty = nothing allowed (matches the
"greenfield projects: nothing allowed" acceptance bullet).

`PERRY_ALLOW_PERRY_FEATURES=1` opts every name into both allowlists
for the current build (emergency one-off knob); `=0` enforces refusal
even when `package.json` opted in (fail-closed CI gate).

Both gates run in the platform-agnostic `compile_command` driver and
`collect_modules.rs` — before any backend (LLVM / WASM / ArkTS /
HarmonyOS / Glance / SwiftUI / JS) is invoked. Every target inherits
the protection from one choke point.

7 unit tests for `allowlist_matches`:
- `empty_allowlist_blocks_everything` — the default-deny invariant.
- `exact_match`, `universal_wildcard`, `scope_wildcard` — primary patterns.
- `non_scoped_wildcards_dont_match` — documents the matcher's
  intentionally narrow shape (no freeform `prefix-*`).
- `multiple_patterns_or_together` — OR semantics across the list.

End-to-end smoke against five shapes:
- No `compilePackages` → clean.
- `compilePackages` without `allow` → fails with full diagnostic.
- `compilePackages` + matching `allow` → compiles.
- `compilePackages: ["@foo/bar","@foo/baz"]` + `allow: ["@foo/*"]` → compiles.
- `PERRY_ALLOW_PERRY_FEATURES=1` → compiles regardless of package.json.

Updated 7 release-test fixtures to add `perry.allow.compilePackages`
alongside their existing `compilePackages`:
- hono-basic, ink-link-smoke, nestjs-hello, redis-pubsub, axios-get,
  drizzle-sqlite, test-files/issue_652.

NestJS fixture uses the scope-wildcard form (`"@nestjs/*"`) to
demonstrate that mechanism in a real fixture.

- [x] Host `package.json` keys: `perry.allow.nativeLibrary`,
      `perry.allow.compilePackages`.
- [x] Transitive dep introducing either fails build with clear
      message naming the package + exact JSON to add.
- [x] Wildcards supported (`@scope/*` + universal `*`).
- [x] Default behavior on greenfield projects: nothing allowed.
- [x] Documented (`docs/src/cli/allow-perry-features.md`).

`perry audit` integration (also in the acceptance list) deferred —
the `perry audit` subcommand itself is #495, not yet implemented.
@proggeramlug proggeramlug force-pushed the feat/497-host-allowlist branch from c3d9b07 to 2604f33 Compare May 18, 2026 11:19
@proggeramlug proggeramlug merged commit c13eb90 into main May 18, 2026
@proggeramlug proggeramlug deleted the feat/497-host-allowlist branch May 18, 2026 11:19
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.

security: host-app allowlist for perry.nativeLibrary and perry.compilePackages

1 participant