Operating Nika workflows
Operating prepares a workflow for unattended use: review spend exposure,
declared effects, secret destinations, model choice and available traces.
The check reports readiness; it does not authorize unattended execution.
Carry out execution already authorized by the user within its effects and
spending scope, through the normal engine and host gates. Ask only for a
decision still missing; permission to run does not supply a human-gate answer.
Judge clean, native_strict_clean, paid_ready and resolved-child coverage
separately, with the engine/spec identity that produced the report.
Spend (the envelope is part of the contract)
nika check <file> estimates output-token cost BEFORE any token: ≤ $X
bounds that output estimate, not prompt/input cost or the entire invoice.
≥ $X FLOOR means at least one task is unbounded — fix
the reason (a missing max_tokens, an uncataloged model, an
expression fan-out), never ship a floor to production.
- Cap the run:
nika run <file> --max-cost-usd <n> refuses a known
over-budget floor before execution. Crossing the metered budget during
execution stops new admissions; already-started calls finish and count.
A concurrent wave can overshoot, and unpriced work has no measured USD
bound. Choose concurrency and model limits with that exposure in mind.
- A local model is unpriced compute, not free — say "unpriced",
never "$0".
Permits (the boundary is mandatory)
Absent is not the unconfined floor — it is ZERO authority. A body
carrying any effect with no permits: block refuses NIKA-AUTH-006
at check, before a token is spent, and the runtime gates refuse before
any spawn. A pure-compute body states the zero explicitly:
permits: {}.
nika check <file> --infer-permits
prints the tightest permits: block the workflow needs — paste it
into the file. From then on the boundary is default-deny: a new host,
path or tool must be added consciously, in a reviewable diff. Permits
are data, not config — they travel with the file through PR review.
Three refusals to expect while tightening: a bound that is an
interpolation instead of a literal (NIKA-AUTH-007 — a self-serve
boundary is no boundary) · a *. subdomain wildcard
(NIKA-AUTH-010 — it hands the boundary to the zone operator; name
exact hosts) · a permits: { env: … } entry naming a dangerous-floor
variable the engine strips unconditionally (NIKA-AUTH-009 — an
inert dead grant).
A spawned child inherits NOTHING from the engine: its environment is
composed from a cleared slate — the runner floor ∪ the names in
permits: { env: [NAME] } ∪ the task's own env: map. A workflow
that leaned on an ambient variable must now name it.
Secrets (masked, declared, sunk)
Every credential rides ${{ secrets.X }}, declared in the
secrets: block (source: env + key: VAR_NAME) with its
egress: sinks — the engine masks it in logs and refuses to send
it anywhere but the declared sinks.
The taint FLOWS: the output of a task that used a secret is
secret-derived, and every downstream sink it reaches needs its own
egress: entry. An authed fetch whose output feeds an infer:
declares both:
secrets:
gh_token:
source: env
key: GITHUB_TOKEN
egress:
- to: "nika:fetch"
- to: "infer"
The checker names the exact chain when one is missing
(secrets.X → tasks.A.output → tasks.B.output).
For a host:-scoped egress finding, use the checker's actual diagnostic.
A literal host can remain provable when the path or query is interpolated.
If the authorized destination is known, express it with that host fixed
and re-check; if it is unknown, request the destination, not the secret.
Never remove or widen host: merely to pass a check. A broader destination
policy requires explicit authorization for that change. Preserve secret
egress, permits: and runtime re-gating; an unresolved refusal still blocks.
Use the declared secret source at run time; CI can inject an environment
source. Never put a credential literal in YAML. Public redaction is not
journal confidentiality: raw task outputs can contain sensitive data,
so protect trace storage and review evidence before sharing it.
nika doctor audits the machine: binary, PATH, provider env vars.
Models (a one-line swap, both directions)
- Models are
provider/name. nika catalog is the embedded registry:
providers · models · capabilities · which env var each needs.
- Shape with
mock/echo (offline, deterministic, zero keys). Prove
structure first, spend later. The CLI override changes the envelope
model only: inspect per-task model pins before calling a rehearsal
offline. Tools, writes, subprocesses and secret sources remain real.
- Sovereignty path:
--model ollama/<model> runs local — same file,
same check, same trace. Give local providers timeout: "300s"+.
- The file does not hardcode a vendor: swapping cloud↔local is a
--model flag or one line in the YAML, never a rewrite.
CI (the check is the gate, the golden is the pin)
- Gate every PR:
nika check <file> --json — exit 0 clean · 2
findings · 3 broken oracle. Parse clean, conformance[],
pricing, models_resolve from the payload.
- Pin behavior:
nika test <file> --update writes
<file>.golden.json from a mock run; nika test <file> replays
and compares — deterministic, zero model keys, CI-safe.
nika test uses the simulated plane and refuses network, subprocess
and write effects. Golden only workflows that fit that plane. For an
effecting rehearsal, use nika run <file> --model mock/echo in scratch:
that flag mocks the model, not the tools, so declared effects and secret
sources remain real. Inspect those artifacts and the trace separately.
--native-strict in CI keeps exec: honest: any shell task an
embedded builtin covers fails the gate (the exec ledger documents
the survivors).
- Schedule with the scheduler you already have (cron · CI · a
systemd timer): the engine is a binary, the workflow is a file, and
--var key=value carries the inputs: the file declares.
MCP servers and definition drift
A configured MCP server that changes its tool definitions after you
approved them is the rug pull. Nika pins every tool on first contact
(TOFU) into .nika/mcp_pins.json beside a reviewable snapshot: first
contact enrolls loudly, a match proceeds silently, ANY drift fails
closed with a diff naming the CHANGED field and returns no tools. A
hand-edited lockfile is NIKA-MCP-004, never a silent re-TOFU.
Re-pin after human review: nika mcp approve <server>.
A pin detects changes relative to the enrolled definitions. It does not
prove those descriptions are truthful, the server is harmless, or an effect
was delivered. Keep the intended permissions and review the actual result;
re-pinning is a review decision, not an automatic fix for drift.
Observability (the journal is exportable, not captive)
nika trace export <trace> projects the journal to OTLP/JSON
lines — drag into Jaeger UI (≥1.60) or POST to any OTLP/HTTP
endpoint. Local file, zero collector, zero vendor.
nika trace ls shows the store; retention never collects the ★
newest trace of each workflow. nika trace rm --older-than <dur>
prunes deliberately.
- Audits cite
nika trace verify <trace>, which reports the highest
tier honestly attained: chain OK · SEALED (the run_sealed
signature verifies against a custody key) · ANCHORED (the detached
sidecar verifies fully offline) · REPLAYED (--replay compares a
fresh run). A journal that never reached a terminal frame verifies
INCOMPLETE. Never a log screenshot.
nika trace evidence <trace> exports the auditor's pack — journal +
manifest + receipt + a VERIFY.md naming the exact commands. Hand
THAT over, not a summary you wrote.
nika trace anchor <trace> notarizes the journal head OUTSIDE the
journal (public transparency log + an RFC 3161 timestamp, written
to a detached sidecar). An explicit NETWORK act — the verb IS the
opt-in, never a default.
- Author-binding:
nika sign <file> mints a detached
<file>.minisig (--check verifies) and
nika run --require-signature refuses an unsigned or
invalidly-signed workflow at exit 2. nika key is the run-signing
key lifecycle (mint · TOFU fingerprint · rotate — old public halves
stay verifiable).
Production checklist
nika check <file> — clean, ceiling not floor.
nika check <file> --native-strict — exec ledger complete.
permits: declared — absent is ZERO authority, not a floor; a
pure-compute body still says permits: {}.
- Secrets in
secrets: with sinks — env-injected, never literal.
- Hermetic workflow: golden pinned (
nika test <file> --update,
committed). Effecting workflow: isolated authorized rehearsal and
artifact assertions instead; the simulated golden plane refuses effects.
- Spend cap on the run line (
--max-cost-usd).
- Trace store known (
.nika/traces/) — export wired if anyone
watches dashboards.
- For a run someone will audit: signed (
nika sign), verified to its
highest honest tier (nika trace verify), packed
(nika trace evidence).
1---2name: nika-operating3description: Operate Nika workflows day-2 — spend caps, permits boundaries, secrets, model swaps (cloud/local), CI wiring, trace export. Use when hardening a working workflow for production, wiring it into CI or a scheduler, capping cost, tightening the permits boundary, swapping models, or exporting traces to OpenTelemetry.4---56# Operating Nika workflows78Operating prepares a workflow for unattended use: review spend exposure,9declared effects, secret destinations, model choice and available traces.1011The check reports readiness; it does not authorize unattended execution.12Carry out execution already authorized by the user within its effects and13spending scope, through the normal engine and host gates. Ask only for a14decision still missing; permission to run does not supply a human-gate answer.15Judge `clean`, `native_strict_clean`, `paid_ready` and resolved-child coverage16separately, with the engine/spec identity that produced the report.1718## Spend (the envelope is part of the contract)1920- `nika check <file>` estimates output-token cost BEFORE any token: `≤ $X`21 bounds that output estimate, not prompt/input cost or the entire invoice.22 `≥ $X FLOOR` means at least one task is unbounded — fix23 the reason (a missing `max_tokens`, an uncataloged model, an24 expression fan-out), never ship a floor to production.25- Cap the run: `nika run <file> --max-cost-usd <n>` refuses a known26 over-budget floor before execution. Crossing the metered budget during27 execution stops new admissions; already-started calls finish and count.28 A concurrent wave can overshoot, and unpriced work has no measured USD29 bound. Choose concurrency and model limits with that exposure in mind.30- A local model is **unpriced compute, not free** — say "unpriced",31 never "$0".3233## Permits (the boundary is mandatory)3435**Absent is not the unconfined floor — it is ZERO authority.** A body36carrying any effect with no `permits:` block refuses `NIKA-AUTH-006`37at check, before a token is spent, and the runtime gates refuse before38any spawn. A pure-compute body states the zero explicitly:39`permits: {}`.4041```42nika check <file> --infer-permits43```4445prints the tightest `permits:` block the workflow needs — paste it46into the file. From then on the boundary is default-deny: a new host,47path or tool must be added consciously, in a reviewable diff. Permits48are data, not config — they travel with the file through PR review.4950Three refusals to expect while tightening: a bound that is an51interpolation instead of a literal (`NIKA-AUTH-007` — a self-serve52boundary is no boundary) · a `*.` subdomain wildcard53(`NIKA-AUTH-010` — it hands the boundary to the zone operator; name54exact hosts) · a `permits: { env: … }` entry naming a dangerous-floor55variable the engine strips unconditionally (`NIKA-AUTH-009` — an56inert dead grant).5758A spawned child inherits NOTHING from the engine: its environment is59composed from a cleared slate — the runner floor ∪ the names in60`permits: { env: [NAME] }` ∪ the task's own `env:` map. A workflow61that leaned on an ambient variable must now name it.6263## Secrets (masked, declared, sunk)6465- Every credential rides `${{ secrets.X }}`, declared in the66 `secrets:` block (`source: env` + `key: VAR_NAME`) with its67 `egress:` sinks — the engine masks it in logs and refuses to send68 it anywhere but the declared sinks.69- **The taint FLOWS**: the output of a task that used a secret is70 secret-derived, and every downstream sink it reaches needs its own71 `egress:` entry. An authed fetch whose output feeds an `infer:`72 declares both:7374 ```yaml75 secrets:76 gh_token:77 source: env78 key: GITHUB_TOKEN79 egress:80 - to: "nika:fetch"81 - to: "infer"82 ```8384 The checker names the exact chain when one is missing85 (`secrets.X → tasks.A.output → tasks.B.output`).86- For a `host:`-scoped egress finding, use the checker's actual diagnostic.87 A literal host can remain provable when the path or query is interpolated.88 If the authorized destination is known, express it with that host fixed89 and re-check; if it is unknown, request the destination, not the secret.90 Never remove or widen `host:` merely to pass a check. A broader destination91 policy requires explicit authorization for that change. Preserve secret92 egress, `permits:` and runtime re-gating; an unresolved refusal still blocks.93- Use the declared secret source at run time; CI can inject an environment94 source. Never put a credential literal in YAML. Public redaction is not95 journal confidentiality: raw task outputs can contain sensitive data,96 so protect trace storage and review evidence before sharing it.97- `nika doctor` audits the machine: binary, PATH, provider env vars.9899## Models (a one-line swap, both directions)100101- Models are `provider/name`. `nika catalog` is the embedded registry:102 providers · models · capabilities · which env var each needs.103- Shape with `mock/echo` (offline, deterministic, zero keys). Prove104 structure first, spend later. The CLI override changes the envelope105 model only: inspect per-task model pins before calling a rehearsal106 offline. Tools, writes, subprocesses and secret sources remain real.107- Sovereignty path: `--model ollama/<model>` runs local — same file,108 same check, same trace. Give local providers `timeout: "300s"`+.109- The file does not hardcode a vendor: swapping cloud↔local is a110 `--model` flag or one line in the YAML, never a rewrite.111112## CI (the check is the gate, the golden is the pin)113114- Gate every PR: `nika check <file> --json` — exit 0 clean · 2115 findings · 3 broken oracle. Parse `clean`, `conformance[]`,116 `pricing`, `models_resolve` from the payload.117- Pin behavior: `nika test <file> --update` writes118 `<file>.golden.json` from a mock run; `nika test <file>` replays119 and compares — deterministic, zero model keys, CI-safe.120- `nika test` uses the simulated plane and refuses network, subprocess121 and write effects. Golden only workflows that fit that plane. For an122 effecting rehearsal, use `nika run <file> --model mock/echo` in scratch:123 that flag mocks the model, not the tools, so declared effects and secret124 sources remain real. Inspect those artifacts and the trace separately.125- `--native-strict` in CI keeps `exec:` honest: any shell task an126 embedded builtin covers fails the gate (the exec ledger documents127 the survivors).128- Schedule with the scheduler you already have (cron · CI · a129 systemd timer): the engine is a binary, the workflow is a file, and130 `--var key=value` carries the `inputs:` the file declares.131132## MCP servers and definition drift133134A configured MCP server that changes its tool definitions after you135approved them is the rug pull. Nika pins every tool on first contact136(TOFU) into `.nika/mcp_pins.json` beside a reviewable snapshot: first137contact enrolls loudly, a match proceeds silently, ANY drift fails138closed with a diff naming the CHANGED field and returns no tools. A139hand-edited lockfile is `NIKA-MCP-004`, never a silent re-TOFU.140Re-pin after human review: `nika mcp approve <server>`.141142A pin detects changes relative to the enrolled definitions. It does not143prove those descriptions are truthful, the server is harmless, or an effect144was delivered. Keep the intended permissions and review the actual result;145re-pinning is a review decision, not an automatic fix for drift.146147## Observability (the journal is exportable, not captive)148149- `nika trace export <trace>` projects the journal to OTLP/JSON150 lines — drag into Jaeger UI (≥1.60) or POST to any OTLP/HTTP151 endpoint. Local file, zero collector, zero vendor.152- `nika trace ls` shows the store; retention never collects the `★`153 newest trace of each workflow. `nika trace rm --older-than <dur>`154 prunes deliberately.155- Audits cite `nika trace verify <trace>`, which reports the highest156 tier honestly attained: chain OK · SEALED (the `run_sealed`157 signature verifies against a custody key) · ANCHORED (the detached158 sidecar verifies fully offline) · REPLAYED (`--replay` compares a159 fresh run). A journal that never reached a terminal frame verifies160 INCOMPLETE. Never a log screenshot.161- `nika trace evidence <trace>` exports the auditor's pack — journal +162 manifest + receipt + a `VERIFY.md` naming the exact commands. Hand163 THAT over, not a summary you wrote.164- `nika trace anchor <trace>` notarizes the journal head OUTSIDE the165 journal (public transparency log + an RFC 3161 timestamp, written166 to a detached sidecar). An explicit NETWORK act — the verb IS the167 opt-in, never a default.168- Author-binding: `nika sign <file>` mints a detached169 `<file>.minisig` (`--check` verifies) and170 `nika run --require-signature` refuses an unsigned or171 invalidly-signed workflow at exit 2. `nika key` is the run-signing172 key lifecycle (mint · TOFU fingerprint · rotate — old public halves173 stay verifiable).174175## Production checklist1761771. `nika check <file>` — clean, ceiling not floor.1782. `nika check <file> --native-strict` — exec ledger complete.1793. `permits:` declared — absent is ZERO authority, not a floor; a180 pure-compute body still says `permits: {}`.1814. Secrets in `secrets:` with sinks — env-injected, never literal.1825. Hermetic workflow: golden pinned (`nika test <file> --update`,183 committed). Effecting workflow: isolated authorized rehearsal and184 artifact assertions instead; the simulated golden plane refuses effects.1856. Spend cap on the run line (`--max-cost-usd`).1867. Trace store known (`.nika/traces/`) — export wired if anyone187 watches dashboards.1888. For a run someone will audit: signed (`nika sign`), verified to its189 highest honest tier (`nika trace verify`), packed190 (`nika trace evidence`).