Compile-time capability linter for node bodies.
A node's pure_core and effect_shell are ordinary Elixir, which means
nothing in the language stops them from calling File.write!/2,
System.cmd/2, :httpc.request/4, or Req.get/1 directly — sidestepping the
declared-capability facade (ctx.effects.*) entirely. This pass closes that
gap: it walks the clause bodies and rejects, at compile time, any call that
reaches the world outside the facade, so the facade is the only legal path
to IO.
What is allowed
A remote call M.f(...) in a node body is permitted iff M is:
- the effect facade (
Bloccs.Effects.HTTP/DB/Time/Random) —effect_shellonly;pure_coremust be side-effect-free, so the facade is rejected there; - a curated, known-pure stdlib module (
Enum,Map,String,Date, …); Applicationfor read-only config (its mutators are denied);- a module in this node's own OTP application (resolves local helpers), or sharing the node module's top-level namespace;
- a module explicitly listed in the manifest's
[lint] allow.
Everything else world-touching is a CompileError. Dynamic dispatch
(apply/3, a call on a variable module, captures of denied modules) is
rejected because it is not statically analyzable. Process/messaging escapes
(spawn, send, receive, Process.*, Task.*) are rejected — concurrency
is the generated supervision tree's job, not a node body's.
Honest scope (the TCB)
The trusted computing base is this pass plus the effect adapters. Within it the
capability declaration is unbypassable for the restricted module surface. It is
not a soundness proof: a too-permissive allow entry, a node calling its own
app's Repo directly (allowed by rule 4), or a compromised dependency at
runtime are out of scope. Runtime isolation remains OTP's job.
Opting out
[lint] effects = "off" downgrades violations to a single loud warning so a
node with a vetted exception still compiles; the opt-out is recorded on the
manifest and surfaced by tooling so the residual review stays visible.
Summary
Functions
Lint the clause bodies of one node function. kind is :pure_core or
:effect_shell. lint is the manifest [lint] config (or nil → enforced
with no extra allows). Raises CompileError on a violation when enforcing;
emits a warning when opted out; returns :ok otherwise.
Functions
@spec lint( Macro.Env.t(), :pure_core | :effect_shell, [Macro.t()], Bloccs.Manifest.Lint.t() | nil ) :: :ok
Lint the clause bodies of one node function. kind is :pure_core or
:effect_shell. lint is the manifest [lint] config (or nil → enforced
with no extra allows). Raises CompileError on a violation when enforcing;
emits a warning when opted out; returns :ok otherwise.