feat(security): #501 — host-controlled per-package capability enforcement#969
Merged
Merged
Conversation
7 tasks
The headline supply-chain hardening lever. Most npm packages will
never declare capabilities themselves; control sits entirely in the
host application's package.json. This adds a compile-time HIR pass
that walks each imported dep's source modules, derives the capability
tokens its stdlib calls would need, and refuses to compile any call
site whose required token isn't in the per-package allow-list.
Host config:
{
"perry": {
"permissions": {
"lodash": [],
"axios": ["net:fetch"],
"@scope/utils": ["crypto"],
"*": []
}
}
}
Capability tokens (MVP): fs:read, fs:write, crypto, proc:env,
proc:argv, proc:exec, net:fetch, net:listen, net:connect. Plus "*"
as the universal escape hatch. Token taxonomy is extensible as
perry's stdlib grows.
Opt-in semantics: empty perry.permissions = pass disabled (existing
builds compile unchanged). Set the map (even to {} with just "*"
defaults) to enable enforcement. Host's own code is always granted
"*" unconditionally — gating host code is the --lockdown mode
(#496), not per-package policy.
Diagnostic surfaces every violation across every module in one
combined error (capped at 12 entries) so the reviewer can fix the
whole surface at once. Each entry names the owning package, the
specific call (fs.readFileSync / child_process.execSync / fetch / ...),
the source span, and the required capability token.
Cross-platform: runs in the platform-agnostic compile_command driver
before any backend (LLVM / WASM / ArkTS / HarmonyOS / Glance / SwiftUI
/ JS) is invoked. Every target inherits the protection from one
choke point.
Standalone implementation — doesn't depend on the in-flight #495
SBOM PR. The walker is in perry-hir::capability and uses the same
shape as the other supply-chain HIR walkers (lockdown, egress).
Walks both dedicated HIR variants (FsReadFileSync, ChildProcessExecSync,
ProcessEnv, FetchWithOptions, NetConnect, ...) and the general-shape
NativeMethodCall fallback for namespaces without dedicated variants.
12 unit tests in perry-hir::capability::tests:
- host_code_unconditionally_allowed
- host_named_package_is_host (host package even when in node_modules)
- unlisted_dep_inherits_star_default
- explicit_deny_blocks_call
- granted_capability_passes
- star_token_grants_everything
- dep_specific_overrides_star_default
- scoped_package_name_extracted (@scope/pkg attribution)
- child_process_requires_proc_exec
- process_env_requires_proc_env
- fetch_requires_net_fetch
- general_native_call_through_crypto (NativeMethodCall fallback)
Acceptance:
- [x] Host package.json perry.permissions: { "<pkg>": ["cap1", "cap2"], "*": ["default"] }
- [x] HIR pass walks each dep's source modules and cross-references stdlib calls
- [x] Violation fails build at the offending source span (file + capability + missing token)
- [x] User's own root code defaults to *, configurable via the policy
- [deferred] perry audit (separate issue) shows capabilities each dep would need — landed via #495 SBOM walker; cross-link in error message
- [deferred] Per-host net:<host> patterns — depends on URL extraction (#502 territory); token taxonomy is forward-compatible
- [x] Nested deps inherit policy via the path-based attribution (a call from node_modules/<pkg>/lib/x.ts is attributed to <pkg>)
- [x] Initial capability tokens documented (capability.rs module doc) + extension path defined
- [x] Composes with the URL allowlist issue (capability net:fetch enforces "can fetch at all"; #502's allowlist constrains "to which hosts")
a60b150 to
c05d905
Compare
3 tasks
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
…Rs (#1019) #981 (PERRY_SANDBOX_BUILDRS), #988 (--emit-attest), and #969 (perry.permissions) each landed via admin-bypass with their SUMMARY.md entries intact but without the actual .md content files (or, for #969, without any docs entry at all). docs/src/cli/lockdown.md and docs/src/cli/emit-sandbox.md did make it into main and are fine; the others left dead links. Separately, #976 / #972 / #974 added perry/system runtime methods (getOSVersion, shareText, shareUrl, appGroupSet/Get/Delete) but never updated the hand-maintained types/perry/system/index.d.ts. TypeScript users importing those APIs from `perry/system` get a type error today. This PR: - Creates docs/src/cli/sandbox-buildrs.md (#505) - Creates docs/src/cli/emit-attest.md (#504) - Creates docs/src/cli/capabilities.md (#501) and adds the SUMMARY.md entry - Adds the six new perry/system signatures to types/perry/system/index.d.ts The auto-generated docs/api/perry.d.ts + docs/src/api/reference.md were regenerated during the original PRs and are already current. Pure docs-only diff. No code changes.
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.
Closes #501.
Summary
The headline supply-chain hardening lever. Most npm packages will never declare capabilities themselves; control sits entirely in the host application's
package.json. This adds a compile-time HIR pass that walks each imported dep's source modules, derives the capability tokens its stdlib calls would need, and refuses to compile any call site whose required token isn't in the per-package allow-list.Zero runtime cost. Cross-platform — runs in the platform-agnostic compile_command driver before any backend (LLVM / WASM / ArkTS / HarmonyOS / Glance / SwiftUI / JS) is invoked.
Host config
{ "perry": { "permissions": { "lodash": [], "axios": ["net:fetch"], "@scope/utils": ["crypto"], "*": [] } } }Capability tokens (MVP)
fs:read,fs:write,crypto,proc:env,proc:argv,proc:exec,net:fetch,net:listen,net:connect, plus universal*.Diagnostic
Combined error across every module (capped at 12 entries) names the owning package, the call, the source span, and the missing token.
Opt-in semantics
Empty
perry.permissions→ pass disabled (existing builds unchanged). Set the map to enable. Host's own code is always granted*unconditionally — gating host code is the--lockdownmode (#496), not per-package policy.Test coverage
12 unit tests in
perry-hir::capability::testscover host pass-through, scope-aware package attribution, explicit deny / grant / wildcard, dep-specific override of*default, and the major per-capability families (fs:read, proc:exec, proc:env, net:fetch, crypto). All pass.Standalone implementation
Doesn't depend on the in-flight #495 SBOM PR. The walker is in
perry-hir::capabilityand uses the same shape as the other supply-chain HIR walkers (lockdown, egress). Walks both dedicated HIR variants and the general-shapeNativeMethodCallfallback.Notes
No
Cargo.tomlversion bump, noCLAUDE.mdtouch, noCHANGELOG.mdentry — maintainer folds those in at merge time.