NIKA-SEC-* error your workflow can read, not a stack trace.
The threat model in one table
Permits — declare the blast radius
On top of the hardened mechanisms, apermits: block makes the file
itself declare its blast radius. Four families, one shape:
- No
permits:block → zero authority, not an unrestricted floor. Hardened defaults (SSRF, shell blocklist, secret masking) still apply, and they do not grant exec, net, fs, or tools. Measured on published 0.120.0 and GitHub 0.120.1:exec: ["printf", "hello"]with nopermits:isNIKA-AUTH-006at check, exit 2. Spec 01-envelope · LAW-AUTH-0324. - Any
permits:block → default-deny for the whole file. Anything not listed is refused. A task reaching beyond the boundary is caught bynika check(NIKA-SEC-004orNIKA-AUTH-006, with the exact fix) and again at run time.
The file is the boundary. The permits: block, drawn as a map of what each task may reach. One task fetches a host outside the fence: nika check refuses the file (NIKA-SEC-004) and names the one-line fix. Widen the fence on purpose and the same check says the file is ready to run. Output captured from the real CLI; the map is drawn from the file.
nika check --infer-permits
computes the tightest boundary the file actually needs and prints it,
ready to paste. The SSRF floor below stays on regardless of
permits — a permitted host still cannot bounce into private space.
SSRF defense (on by default)
nika:fetch refuses to be bounced into private space. Three layers,
verbatim from the engine (nika-http):
Static checks (pure)
http/https only), blocked hostnames, and
range checks for implicit loopback, RFC 1918, link-local/metadata
(169.254.169.254), CGN, IPv6 local, v4-mapped v6. Spec
01-envelope Exact-loopback declassification:
the only SSRF carve-out is an exact permits.net.http literal —
the closed set localhost, a 127.x.y.z v4 literal, or ::1 /
[::1]. Clearing is exact-host: a permitted localhost does not
clear 127.0.0.1 (measured on 0.120.1: that grant + fetch to
127.0.0.1 is NIKA-SEC-005; an exact 127.0.0.1 grant fetches);
a public name that resolves to loopback stays refused; RFC1918 /
metadata named in the list stay floor-blocked (NIKA-SEC-005).DNS resolution check
http://2130706433/)
and public names that resolve to private addresses.Per-hop redirect re-check
exec: the data-channel rule
exec in shell mode runs every command against a blocklist before
spawn. But the structural defense is in how you write workflows:
tainted data goes through data channels, never code channels.
The two authored doors
Some legitimate workflows do need to cross one of these lines. Nika refuses to let that happen silently: there is exactly one door,lift:, authored,
naming the law it opens, requiring a written justification, recorded in the
run receipt.
law: taint does not widen
permits: — the value is still matched against the declared boundary, so it
can never become a permit bypass, and there is no implicit declassification.
law: data-as-code does not touch the net.http boundary or the SSRF floor.
(declassify: and inert: were the two earlier spellings; both merged into
lift: in 0.109 — the law is a parameter of one door, not two keywords.)
Because it is one plain YAML key, grep -rn 'lift:' over a repo returns
every place a law was opened, with the reason attached. That is the point:
the escape hatch is auditable by construction.
Trust levels + prompt injection
Content entering a workflow carries a trust level. Untrusted content (fetched pages, user comments) is tracked as it flows into prompts and tools:NIKA-SEC family at three codes. The catalog on
Error codes is the live set (NIKA-SEC-001
blocklist, NIKA-SEC-002 agent whitelist, NIKA-SEC-003 recursion,
NIKA-SEC-004 permit miss, and later SEC laws). Runtime trust layers
emit in that namespace as they land.
Every failure is a typed error with a stable
code and a transient flag. Your on_error: can act on it.
Secrets
${{ secrets.* }} is a first-class namespace (vault/env/file-backed).
Secret values are masked in logs and traces at the binding layer:
they never appear in the event stream, and they are not readable back
from a task’s recorded output.
Supply-chain hardening (the engine itself)
unsafe_code = "forbid"workspace-wide · zero.unwrap()insrc/(CI-enforced)cargo denyon every PR ·cargo auditon every dependency change- Sealed kernel traits · external code cannot impersonate engine I/O
- lib tests · clippy warnings · every crate passes 12 admission gates
- AGPL-3.0-or-later · the source travels with the binary, always auditable
Reporting a vulnerability
security@supernovae.studio (please not via public issues). Acknowledgement ≤72h, triage ≤7 days, disclosure ≤90 days. Full policy, disclosure process and the out-of-scope list: SECURITY.md.See also
Error codes
NIKA-SEC namespace + every typed failure.Bindings
secrets.* namespace.Builtins · fetch
nika:fetch contract (engines MUST ship SSRF defense).