Skip to content

feat(security): #501 — host-controlled per-package capability enforcement#969

Merged
proggeramlug merged 1 commit into
mainfrom
feat/501-per-package-capabilities
May 18, 2026
Merged

feat(security): #501 — host-controlled per-package capability enforcement#969
proggeramlug merged 1 commit into
mainfrom
feat/501-per-package-capabilities

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

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 --lockdown mode (#496), not per-package policy.

Test coverage

12 unit tests in perry-hir::capability::tests cover 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::capability and uses the same shape as the other supply-chain HIR walkers (lockdown, egress). Walks both dedicated HIR variants and the general-shape NativeMethodCall fallback.

Notes

No Cargo.toml version bump, no CLAUDE.md touch, no CHANGELOG.md entry — maintainer folds those in at merge time.

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")
@proggeramlug proggeramlug force-pushed the feat/501-per-package-capabilities branch from a60b150 to c05d905 Compare May 18, 2026 09:59
@proggeramlug proggeramlug merged commit 0eed539 into main May 18, 2026
@proggeramlug proggeramlug deleted the feat/501-per-package-capabilities branch May 18, 2026 09:59
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.
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-controlled per-package capability enforcement (compile-time)

1 participant