v0.114.0
What
Remote execution as a loopback door. Default nika serve stays the
resident ARM firer. --bind + --workflows + --token-file opens
authenticated HTTP: jobs, SSE, OpenAPI. The worker runs the POST-time
snapshot, not live files. Cancel, artifacts and /v1/run stay 404.
The workflow envelope is unchanged since v0.113.0. The project file's
retired nika: v1 tag now refuses. Pre-1.0 real semver puts this at a
minor bump.
Added
nika serve --bindis authenticated loopback HTTP. Pair it with
--workflowsand--token-file(credential bytes never enter argv).
Defaultnika serveis still the resident ARM firer. The verb is on
nika --help. Listen line prints the bound address, including port 0.
Job cancel and artifacts stay 404 until those authorities exist.- Job events project over SSE at
GET /v1/jobs/{id}/events. Frames
are{sequence,kind,status}only.GET /v1/openapi.jsonis the live
route table; cancel, artifacts and/v1/runare omitted. nika doctornames the HTTP door when a token file is present.
Owner-only mode is OK; group/world-readable is a fail with the umask
077 fix. The row never claims TLS — that is the reverse proxy.
Silent when the cwd has no.nika/serve.token(the door is opt-in).
systemd and Caddy examples live indocs/ops/.
Changed
nika checknames the ancestornika.yamlspend cap.nika run
already filled--max-cost-usdfrom that file; check printed « no
total ceiling » and advised adding the flag, on a tree that already
had one (issue 1050). The COST line still prices the workflow; a
BUDGETfootnote (and a presence-gatedrun_budgetobject on
--json) now says the number andnika.yaml:line.--max-cost-usd
still wins.run --dry-runand--helpstay silent — this is the
surface that contradicted the file.- A piped
nika try/nika runno longer waits on the macOS keychain
after a green card. The card printed, thenSecKeychainFindGenericPassword
blocked the main thread: a pipe cannot complete an ACL prompt. The
keychain is skipped when stderr is not a TTY (file/env custody stay);
a disabled journal (nika try·--no-trace-file) does not consult it
at all. - The README says the trace inherits what the run read. A
nika:read
of a file puts those bytes into.nika/traces/in the clear — correct
for a replayable journal, unnamed until an auditor derived it (issue
1047). The receipt paragraph now says hash-chained is not confidential. nika spec --canonsays the error-code count is the floor. An agent
readcount: 103plus twoNIKA-BUILTIN-*rows as the builtin-code
family. The spec now carries ascope:sibling (nika-spec 282); this pin
vendors it. Per-builtin and per-provider codes still live innika explain.- BREAKING — the project file names itself (
nika.yaml).nika:carried a
frozenv1tag while a workflow'snika:names the file: one key, two
grammars, and every surface reading both had to know a special rule. It now
carries the project's kebab-case name, the same grammar the workflow
envelope gives its ownnika:. The reasoning is this repo's own, written
when the workflow envelope was nuked — a field with one legal value is not a
version, so nothing is traded away, and the project gains a name it never
had. The retired tag refuses here and nowhere else: a pre-nuke workflow
carried aworkflow:block beside itsnika: v1and still refuses on that
key, sov1stays free to be an ordinary workflow name; a pre-nuke project
file had no companion key, so without a refusal the same bytes would quietly
stop meaning « schema v1 » and start meaning « a project named v1 ». Only a
whole marker qualifies —vault,v2rayandv1-migrationstay names.
Migration is one line:nika: v1→nika: <your-project-name>.
Fixed
- HTTP cancel now reaches the blocking worker. Timeout and SIGTERM
no longer mark interrupted while effects continue. Queued jobs persist
their workflow name and are re-enqueued on the next incarnation. - POST captures the execution world. A symlink or rewrite after 202
cannot run bytes the client never admitted. GET namesexecution_id
andtrace_idafter readmit. The service sandbox is the workflow root.
Added
nika checknames the exact builtin when a utility is 1:1.sleep,
date,uuidgen, the digest family,yq,grep/rg/agandfind
used to be silent — or, fordate/sha256sum, a family catalogue.
Rulenative-first/006answers with one builtin and its argument shape
(nika:wait·duration:) so the author types the next line, not a
menu. It sits at the foot of the ladder and never steals a more specific
family.echostays silent on purpose: it is the universal placeholder,
not a builtin.- Tool-result spill in the
agent:loop (opt-in seam). Past 16 KiB a
tool result's full text leaves the conversation for the blob store —
the content hash IS the locator — and the model keeps a 2 KiB preview
plus the pointer, so the context window stops re-paying bytes it cannot
use. Nothing is discarded, and a store refusal keeps the full text:
the spill is an optimization of the model's reading, never a gate on
the data. Seat it withAgentVerb::with_spill; without the seam the
loop is byte-unchanged. nika checkjudges a project file as a project. It applied the nine-key
WORKFLOW envelope to every document, so a project file came back
NIKA-PARSE-002 missing required envelope field: tasks— on a file
nika init --project-filehad just written — and following that finding's
own advice converted a correct project file into a broken workflow. The
discriminant is the spec's, normative and covering every document
(01-envelope§The type discriminant): atasks:key means WORKFLOW, its
absence means PROJECT. Deliberately not the filename, so it still holds for
a registry blob, an HTTP body or a fence pasted into a chat. A project now
audits in its own vocabulary — exit 0 clean, 2 on a finding — and says what
it governs rather than a count it never stated.nika checknotes a file whose name and filename have drifted apart.
Copyfoo.nika.yamltobar.nika.yaml, forget the header, and every trace
and journal event keeps sayingfoo. It is a NOTE and never a finding:
divergence is usually deliberate (an ordering prefix such as01-hellois
stripped before comparing), and the exit code is untouched. The filename is
a locationgit mvmay change; the name is an identity that rides traces.
Fixed
nika check's audited line names the declared blast radius. The
default card saidpermits declared; the grants themselves lived
behind--infer-permitsand--json(persona 4). Cost was already
on the card. The cell now lists the exec / tools / fs / net / env
grants (an explicit empty block is{}, absent is stillnone).- A recovered run is no longer a green tick at a glance.
--quiet
and the shareable card titled✔/✓on a run that repaired a
task (task_recoveredthen Ok). Exit 0 is still correct — recovered
is a success cause. The headline and the card title now carry the
warn mark; the storyboard already namedN recovered(persona 14).
trace lsstill sayscompleted(no newTraceStatethis cut). --infer-permitsno longer pastes a host-file grant. A
nika:readof/etc/passwd(or~/.ssh/…) under an absent
permits:block printedfs.read: ["/etc/passwd"]; applying that
block greened check and run (persona 7). G-09 already withheld the
shovel on a declared boundary; the AUTH-006 companion and the
inferred YAML still handed it. Escaping paths (absolute · home ·
..-climb) stay a note. The printed repair is the tool conjunct
only.nika checkno longer panics on a decorative verb glyph. Copying
⛨permits:/◇infer:from nika.sh into a file, then running the
advertisednika check, dumpedannotate-snippets(byte index N is not a char boundary) with no NIKA code and no--fixhint. The
snippet painter widened a point span by one BYTE into a 3-byte glyph;
it now snaps to a char boundary and widens by one CHAR. MCP already
taughtdid you mean permits?— the CLI default path matches that
calm.nika try 10-compose-pipelinestages the child it invokes. The
rehearsal room used to hold only the parent; check then died
NIKA-COMP-001on./10-compose-child.nika.yaml(e2e S2 17/18 on
81c1138). Fixture materialize already carriedexamples/fixtures/
ingredients; a relativeworkflow: "./….nika.yaml"pack sibling is
the same class.for_eachover a constant that is not an array is refused at check, not
at dispatch.const: { items: "x" }withfor_each: ${{ const.items }}
audited clean, then died at the run withNIKA-VAR-006— a linter's answer,
not a verifier's, and exactly the gap ADR-092 exists to close. The static
lane already caught a typed non-array var but exempted every untyped one,
on the rationale that « a--varoverride could pass an array ». That rule
is inputs-only:--varsets aninputs:value and refuses unknown keys, and
spec 01 §const is normative — a constant is « immutable across the run and
never caller-supplied ». An untyped constant's literal IS its run value, so
a non-array can never become one. Untyped entries are legal inconst:
alone, arrays and typed declarations are untouched, and all 59 shipped
templates and examples still audit clean.nika explainanswers the tokennika checkprinted in[brackets],
including a hint. A HINT row putjq-as-map(ornative-first/006)
in the same slot asNIKA-PARSE-019, so the next gesture was
nika explain jq-as-mapand the answer wasunknown code. A finding
carriescode; a hint carrieskind— real in the data, invisible
in the render. Explain now resolves the printed identity (the kind,
or the numbered native-first rule) and teaches the class; MCP
nika_explainspeaks the same text.nika checknames the line of a PARSE refusal. A CONFORM finding
already carried a rustc-grade frame (path:line:col+ caret). A PARSE
finding —NIKA-PARSE-017duplicate key,NIKA-PARSE-005unknown
field, any span the parser held — printed the code and left the author
to find the site. Duplicate keys are the worst of that class: the
message says"a" appears twice, so greppinga:returns both, and
neither is wrong on its own. The colliding key's span was in the
loader's hand and discarded (span: None). It now rides the same
frame CONFORM uses, under the samePARSE ✗first line.nika armno longer refuses a project that sets its retention or its
provenance floor. Two readers parsenika.yaml— the project reader
(ceiling·traces.keep·registry.floor·arm) and the cadence
grammar. The grammar refusedtraces:andregistry:by name as
« round 2 » keys with a remedy that was false (« retention stays with
the env vars ») while the project reader had just accepted them and the
retention ladder and the provenance gate consume them — the project
starter's owntraces:line madenika armexit 2. Measured
2026-08-18. The grammar now admits the project's other rungs OPAQUE and
judges nothing about them (they are judged where they are owned); the
cadence domain's own deferred keys (signature:·budget:) stay
refused by name. A test derived from the project reader's closed key
set pins the parity: every key it admits,nika armreads green.
Install
brew install supernovae-st/tap/nika # macOS · Linux
curl -LsSf https://nika.sh/install.sh | sh # script install
docker run --rm ghcr.io/supernovae-st/nika:0.114.0 --versionTarballs below: macOS arm64 / x64 · Linux x64 / arm64, plus SHA256SUMS.
Verify: three independent proofs
# 1 · checksum: the bytes you hold are the bytes CI hashed
sha256sum -c SHA256SUMS --ignore-missing # macOS: shasum -a 256 -c
# 2 · attestation: GitHub-signed build provenance for this exact artifact
gh attestation verify nika-<platform>-0.114.0.tar.gz --repo supernovae-st/nika
# 3 · SLSA provenance: the intoto asset, verifiable offline
slsa-verifier verify-artifact nika-<platform>-0.114.0.tar.gz \
--provenance-path multiple.intoto.jsonl \
--source-uri github.com/supernovae-st/nika --source-tag v0.114.0Provenance
Built from tag v0.114.0 by release.yml
on GitHub-hosted runners. Provenance is published twice: GitHub's native
build attestation (proof 2) and the SLSA generator's multiple.intoto.jsonl
release asset (proof 3). The release itself is a claim on the
machine-verified timeline: https://nika.sh/timeline
What's Changed
- feat(project): the manifest names itself, and the retired tag refuses by @ThibautMelen in #1037
- feat(agent): spill oversized tool results to the blob store by @ThibautMelen in #1019
- fix(nika-cadence): the grammar admits the project's other rungs — arm reads a shipped file green by @ThibautMelen in #982
- feat(check): a utility with an exact builtin gets named, not handed a menu by @ThibautMelen in #1053
- fix(check): a PARSE refusal names the line the parser already held by @ThibautMelen in #1105
- fix(check): explain the hint kind nika check just printed by @ThibautMelen in #1106
- fix(check): a slash hint id teaches instead of looking like a path by @ThibautMelen in #1108
- fix(check): a map piped from a field is not jq-as-map by @ThibautMelen in #1109
- fix(display): a recovered write is not fruit by @ThibautMelen in #1111
- fix(check): a non-array constant fails for_each at check, not at dispatch by @ThibautMelen in #1107
- docs(pack): the error_codes count says it is the floor by @ThibautMelen in #1112
- docs(readme): the trace inherits what the run read by @ThibautMelen in #1113
- fix(dap): a piped run does not wait on the keychain by @ThibautMelen in #1114
- fix(cli): check names the ancestor project ceiling by @ThibautMelen in #1115
- fix(onboard): try stages the compose child beside the parent by @ThibautMelen in #1119
- fix(display): check paints multibyte glyphs instead of panicking by @ThibautMelen in #1120
- fix(check): infer-permits never pastes a host-file grant by @ThibautMelen in #1121
- fix(display): a recovered run is not a green tick by @ThibautMelen in #1122
- fix(display): check names the declared blast radius by @ThibautMelen in #1125
- feat(harness): add kimi-code as a native acp seat by @ThibautMelen in #1123
- feat(execution): add owned execution snapshots by @ThibautMelen in #1130
- fix(harness): harden kimi code acp readiness by @ThibautMelen in #1132
- fix(security): close remote execution prerequisites by @ThibautMelen in #1131
- feat(execution): route cli and arm through owned snapshots by @ThibautMelen in #1133
- feat(nika): add durable remote job store by @ThibautMelen in #1134
- feat(remote): add durable jobs and shared execution by @ThibautMelen in #1139
- fix(arm): re-emit launchd keeps the env wrapper by @ThibautMelen in #1140
- fix(arm): rewrite emit prose the typos gate flags by @ThibautMelen in #1141
- feat(serve): admit loopback http with bearer admission by @ThibautMelen in #1142
- feat(serve): project job events over sse by @ThibautMelen in #1143
- feat(serve): publish the live routes as openapi 3.1 by @ThibautMelen in #1146
- feat(cli): show nika serve on the help surface by @ThibautMelen in #1145
- feat(cli): doctor names the serve http door by @ThibautMelen in #1147
- fix(serve): cancel the blocking worker and reschedule queued jobs by @ThibautMelen in #1148
- fix(serve): capture the execution world at post by @ThibautMelen in #1149
Full Changelog: v0.113.0...v0.114.0