Skip to content

security: host-controlled per-package capability enforcement (compile-time) #501

Description

@proggeramlug

The headline supply-chain feature. Most npm packages will never declare capabilities themselves — control sits entirely in the host app's package.json. The compiler walks each imported package's HIR, cross-references stdlib call sites against the host's policy, and refuses to compile on violation. Package authors do nothing.

Example host config

{
  "perry": {
    "permissions": {
      "lodash": [],
      "axios": ["net:https://api.example.com"],
      "left-pad": [],
      "@scope/utils": ["crypto"],
      "*": ["crypto"]
    }
  }
}

If lodash's compiled HIR contains a call to fs.readFile, the build fails at the offending line:

lodash calls fs.readFile at node_modules/lodash/template.js:42 but is not granted fs:read in your perry.permissions; either add the capability or replace the dep.

Most real-world npm supply-chain incidents (event-stream, ua-parser-js, several 2023/2024 incidents) would be caught by a default-deny policy on *.

Mechanism

HIR pass per imported source module; cross-reference each stdlib call site against the host's policy; fail compilation on violation. Zero runtime cost — pure compile-time refusal, no FFI-boundary checks emitted.

The user's own root code defaults to * (full capability); per-dep entries override the * default.

Capability tokens (initial set)

  • fs:read, fs:write, fs:exec
  • net:fetch, net:listen, net:<host> (literal or glob)
  • proc:env, proc:exec, proc:argv
  • crypto
  • time (Date.now / performance.now — optional, side-channel paranoia)
  • * for unrestricted

Token taxonomy is extensible as Perry's stdlib grows.

Acceptance

  • Host package.json perry.permissions: { "<pkg>": ["cap1", "cap2"], "*": ["default"] }
  • HIR pass walks each dep's source modules and cross-references stdlib calls
  • Violation fails build at the offending source span (file + line + capability + missing token)
  • User's own root code defaults to *, configurable
  • perry audit (separate issue) shows which capabilities each dep would need to compile cleanly
  • Wildcard host patterns: "net:*.example.com", "net:https://api.acme.com/v1/*"
  • Nested deps inherit policy: a transitive call from lodash that lands in fs.readFile is attributed to whichever package's source module contains the call site
  • Initial capability tokens documented + extension path defined
  • Composes with the URL allowlist issue (capability net:<host> and the URL allowlist enforce the same constraint at different granularities)

Part of the supply-chain hardening series. The big lever. Host-app-controlled. Zero runtime cost (compile-time analysis only).

Activity

gkgoat1 commented on May 18, 2026

@gkgoat1

The headline supply-chain feature. Most npm packages will never declare capabilities themselves — control sits entirely in the host app's package.json. The compiler walks each imported package's HIR, cross-references stdlib call sites against the host's policy, and refuses to compile on violation. Package authors do nothing.

Example host config

{
"perry": {
"permissions": {
"lodash": [],
"axios": ["net:https://api.example.com"],
"left-pad": [],
"@scope/utils": ["crypto"],
"*": ["crypto"]
}
}
}
If lodash's compiled HIR contains a call to fs.readFile, the build fails at the offending line:

lodash calls fs.readFile at node_modules/lodash/template.js:42 but is not granted fs:read in your perry.permissions; either add the capability or replace the dep.

Most real-world npm supply-chain incidents (event-stream, ua-parser-js, several 2023/2024 incidents) would be caught by a default-deny policy on *.

Mechanism

HIR pass per imported source module; cross-reference each stdlib call site against the host's policy; fail compilation on violation. Zero runtime cost — pure compile-time refusal, no FFI-boundary checks emitted.

Once again, analyzing code is liable to allow vulnerabilities or severely restrict code. My take is that the safest option is to emit runtime checks that get optimized out in reasonable cases.

The user's own root code defaults to * (full capability); per-dep entries override the * default.

Capability tokens (initial set)

  • fs:read, fs:write, fs:exec
  • net:fetch, net:listen, net:<host> (literal or glob)
  • proc:env, proc:exec, proc:argv
  • crypto
  • time (Date.now / performance.now — optional, side-channel paranoia)
  • * for unrestricted

Token taxonomy is extensible as Perry's stdlib grows.

Acceptance

  • Host package.json perry.permissions: { "<pkg>": ["cap1", "cap2"], "*": ["default"] }[ ] HIR pass walks each dep's source modules and cross-references stdlib calls[ ] Violation fails build at the offending source span (file + line + capability + missing token)[ ] User's own root code defaults to *, configurable[ ] perry audit (separate issue) shows which capabilities each dep would need to compile cleanly[ ] Wildcard host patterns: "net:*.example.com", "net:https://api.acme.com/v1/*"[ ] Nested deps inherit policy: a transitive call from lodash that lands in fs.readFile is attributed to whichever package's source module contains the call site[ ] Initial capability tokens documented + extension path defined[ ] Composes with the URL allowlist issue (capability net:<host> and the URL allowlist enforce the same constraint at different granularities)

Part of the supply-chain hardening series. The big lever. Host-app-controlled. Zero runtime cost (compile-time analysis only).

gkgoat1 commented on May 18, 2026

@gkgoat1

For context on my similar project, dreamcomp, the approach is to have a rich API for intercepting the APIs used by dependencies; we support plugins, are planning to have an effect system, and plan to have direct "override" APIs. This limits the blast radius of any module--it can only do the intersection of all certifications from all involved modules and all APIs given (ESM imports, parameters). jsaw will be the open-source, more app-oriented, implementation (with dreamcomp compatibility), using jsaw-core as a shared library

I will note that both projects are much more focused on safety (#855 cough cough) and purity than Perry; code is more reliable, but needs more customization and independent implementation (instead of inbuilt app APIs, for instance, everything happens either in pure JS or FFI)

added a commit that references this issue on May 18, 2026
added 4 commits that reference this issue on May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions