Skip to main content
Workflows run attacker-influenced inputs by nature: they fetch URLs, shell out, and feed model output into tools. Nika’s position is that the mechanism must be safe before policy makes it configurable: the dangerous primitives ship hardened by default, and a violation is a typed 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, a permits: block makes the file itself declare its blast radius. Four families, one shape:
Two laws govern it:
  1. 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 no permits: is NIKA-AUTH-006 at check, exit 2. Spec 01-envelope · LAW-AUTH-0324.
  2. Any permits: block → default-deny for the whole file. Anything not listed is refused. A task reaching beyond the boundary is caught by nika check (NIKA-SEC-004 or NIKA-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.

Predict: a file with one exec: and no permits: — does it run? It does not. nika check --infer-permits prints the tightest block to paste. Infer-only mock/echo with permits: {} is the declared zero, and that is enough for a first run.
You never have to write it by hand: 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):
1

Static checks (pure)

Scheme allow-list (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).
2

DNS resolution check

Non-literal hosts are resolved and every address is range-checked: this kills decimal-IP tricks (http://2130706433/) and public names that resolve to private addresses.
3

Per-hop redirect re-check

Client-level redirects are disabled; the engine follows redirects itself and re-runs layers 1+2 on every hop. A public host cannot 302 the client into your VPC.
Response sizes are capped (64 MiB default) and self-signed TLS is rejected by default. Private-network access is opt-in configuration, never the default.

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.
Passing tainted data to a privileged sink without going through a data channel raises a typed taint violation: taint is a property of the data, not of who runs the workflow.

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.
Each entry lifts one law and nothing else. 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: Do not freeze the 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() in src/ (CI-enforced)
  • cargo deny on every PR · cargo audit on 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

The NIKA-SEC namespace + every typed failure.

Bindings

Taint, scopes, and the secrets.* namespace.

Builtins · fetch

The nika:fetch contract (engines MUST ship SSRF defense).

SECURITY.md

Reporting, disclosure timeline, hall of fame.