@supernovae-st/nika 0.71.0 → 0.118.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +562 -30
- package/dist/bin/nika.js +407 -0
- package/dist/index.cjs +2889 -0
- package/dist/index.d.cts +593 -0
- package/dist/index.d.ts +593 -0
- package/dist/index.js +2838 -0
- package/docs/architecture.md +85 -0
- package/docs/http-api.md +87 -0
- package/docs/migrating-to-0.116.md +147 -0
- package/docs/testing.md +156 -0
- package/openapi.json +1 -0
- package/package.json +76 -24
- package/bin.js +0 -49
package/README.md
CHANGED
|
@@ -1,47 +1,579 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://nika.sh">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://nika.sh/brand/nika-logo-dark.svg">
|
|
5
|
+
<img src="https://nika.sh/brand/nika-logo-light.svg" alt="Nika" width="220">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
<h1 align="center">@supernovae-st/nika</h1>
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
<p align="center"><strong>One TypeScript surface for local Nika processes and authenticated Nika servers.</strong></p>
|
|
13
|
+
|
|
14
|
+
`Nika` exposes one lifecycle vocabulary: `check`, `run`, `attachRun`, `status`, `events`,
|
|
15
|
+
`cancel`, `traceVerify`, `listWorkflows`, `workflow`, `schedule`, and
|
|
16
|
+
`scheduleStatus`.
|
|
17
|
+
The engine remains authoritative for parsing, admission, execution, receipts,
|
|
18
|
+
traces, permits, scheduling, and cost. The SDK transports those facts; it does
|
|
19
|
+
not parse YAML or reconstruct proof in TypeScript.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Node.js 22 or newer (the tested floor; an older major is unsupported, not
|
|
24
|
+
refused, and `npm install` does not warn about it)
|
|
25
|
+
- for native execution or local snapshot capture, a compatible `nika` engine, resolved from `config.bin`, then `NIKA_BIN`
|
|
26
|
+
(absolute paths only), then the exact optional platform package; a bare
|
|
27
|
+
name or a relative path is refused because the operating system would
|
|
28
|
+
resolve it through `PATH` or the working directory, and a `nika` found on
|
|
29
|
+
`PATH` is deliberately never used
|
|
30
|
+
- a `.nika.yaml` workflow
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
- [Architecture](docs/architecture.md) — Modules, Interface, Seam, Adapters,
|
|
35
|
+
lifecycle, and authority boundaries
|
|
36
|
+
- [HTTP contract](docs/http-api.md) — every live route, recovery, security,
|
|
37
|
+
idempotency, and schedule CAS
|
|
38
|
+
- [Testing and release evidence](docs/testing.md) — layered gauntlets and the
|
|
39
|
+
Socratic risk matrix
|
|
40
|
+
- [Migrating to 0.116](docs/migrating-to-0.116.md) — intentional breaking
|
|
41
|
+
migration to the smaller durable client surface
|
|
6
42
|
|
|
7
43
|
## Install
|
|
8
44
|
|
|
9
|
-
```
|
|
10
|
-
#
|
|
11
|
-
npm install
|
|
45
|
+
```sh
|
|
46
|
+
npm view @supernovae-st/nika@0.118.7 version # must report 0.118.7
|
|
47
|
+
npm install @supernovae-st/nika@0.118.7
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If the registry reports any other version, the 0.118.7 release train is not
|
|
51
|
+
complete. Earlier packages expose the retired `LocalNika`/HTTP split and do
|
|
52
|
+
not implement the root facade documented below. The publication is complete
|
|
53
|
+
only when the four matching native payload packages and this root client are
|
|
54
|
+
all visible on npm.
|
|
12
55
|
|
|
13
|
-
|
|
14
|
-
|
|
56
|
+
This package carries the product's name. Up to 0.115.0 it was published as
|
|
57
|
+
`@supernovae-st/nika-client`; that name is deprecated on npm, stays installable
|
|
58
|
+
for the versions it already holds, and receives no further releases. The
|
|
59
|
+
native payloads were already `@supernovae-st/nika-<os>-<arch>`, and the
|
|
60
|
+
repository keeps its name (`supernovae-st/nika-client`).
|
|
15
61
|
|
|
16
|
-
|
|
17
|
-
|
|
62
|
+
Verify the package that the current project actually resolved:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
node -p "require('@supernovae-st/nika/package.json').version"
|
|
18
66
|
```
|
|
19
67
|
|
|
20
|
-
|
|
68
|
+
This package metadata subpath is exported for CommonJS, ESM build tools and CI
|
|
69
|
+
pin checks. It reports the installed dependency, not a moving registry tag.
|
|
70
|
+
|
|
71
|
+
## First local run
|
|
21
72
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
nika
|
|
26
|
-
nika
|
|
27
|
-
nika init # Interactive project setup
|
|
28
|
-
nika course next # Start the learning course
|
|
73
|
+
The lowest-friction creation door is the engine-owned scaffold:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
./node_modules/.bin/nika init --project-file
|
|
77
|
+
./node_modules/.bin/nika new 01-hello hello.nika.yaml
|
|
29
78
|
```
|
|
30
79
|
|
|
31
|
-
|
|
80
|
+
`nika.yaml` is the project control plane. `hello.nika.yaml` is executable
|
|
81
|
+
workflow intent and is the file passed to `check()` and `run()`. The scaffold
|
|
82
|
+
writes the engine's own annotated `01-hello` example (its task is named
|
|
83
|
+
`greet` and its prompt asks for French); the contract this README relies on is
|
|
84
|
+
the `outputs.greeting` key and the `mock/echo` model, and the same file can be
|
|
85
|
+
written by hand with this public envelope:
|
|
32
86
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
```yaml
|
|
88
|
+
nika: sdk-hello
|
|
89
|
+
model: mock/echo
|
|
90
|
+
permits: {}
|
|
91
|
+
tasks:
|
|
92
|
+
greeting:
|
|
93
|
+
infer:
|
|
94
|
+
prompt: "Say hello from the Nika SDK."
|
|
95
|
+
max_tokens: 32
|
|
96
|
+
outputs:
|
|
97
|
+
greeting: ${{ tasks.greeting.output }}
|
|
98
|
+
```
|
|
39
99
|
|
|
40
|
-
|
|
100
|
+
Then drive the installed engine:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Nika } from '@supernovae-st/nika';
|
|
104
|
+
|
|
105
|
+
const nika = new Nika({
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
// bin: '/absolute/path/to/nika', // or set NIKA_BIN
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const report = await nika.check('hello.nika.yaml', {
|
|
111
|
+
nativeStrict: true,
|
|
112
|
+
});
|
|
113
|
+
if (!report.clean) throw new Error('workflow did not pass nika check');
|
|
114
|
+
|
|
115
|
+
const run = await nika.run('hello.nika.yaml', { maxCostUsd: 0 });
|
|
116
|
+
const watching = (async () => {
|
|
117
|
+
for await (const event of nika.events(run)) {
|
|
118
|
+
// Native progress frames carry no status; only the terminal frame does.
|
|
119
|
+
console.log(event.kind, event.status ?? '');
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
|
|
123
|
+
const result = await run.done;
|
|
124
|
+
await watching;
|
|
125
|
+
console.log(result.status, result.outputs, result.receipt);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Expected output: `workflow_started`, `task_scheduled`, `task_started`,
|
|
129
|
+
`task_completed`, `workflow_completed`, then `run_settled succeeded`, then the
|
|
130
|
+
terminal `succeeded` line with the outputs and the receipt.
|
|
131
|
+
|
|
132
|
+
Native checks and explicit local snapshot checks preserve the engine's
|
|
133
|
+
`findings[]` and `exitCode`. A check by served name returns the resident's
|
|
134
|
+
compact acknowledgement with `clean: true`, or its typed workflow refusal
|
|
135
|
+
with `clean: false`; it does not invent local findings or an exit code.
|
|
136
|
+
|
|
137
|
+
`run()` returns after stable admission. `run.done` is the sole terminal result.
|
|
138
|
+
An admitted workflow failure is result data with `status: "failed"` and, when
|
|
139
|
+
the engine named the failing task, `error: { code, message, task }`; transport,
|
|
140
|
+
protocol, configuration, and compatibility failures throw typed SDK errors.
|
|
141
|
+
A `try { await run.done } catch {}` alone therefore never catches a failed
|
|
142
|
+
workflow: a CI job or an application must read `result.status` and treat
|
|
143
|
+
anything but `succeeded` as its own failure, or a red run passes silently.
|
|
144
|
+
|
|
145
|
+
## Verify a local trace
|
|
146
|
+
|
|
147
|
+
Local terminal results carry an engine-issued receipt when tracing is enabled.
|
|
148
|
+
Pass that receipt back unchanged:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
if (!result.receipt) throw new Error('run did not issue a receipt');
|
|
152
|
+
const proof = await nika.traceVerify(result.receipt);
|
|
153
|
+
if (!proof.verified) throw new Error(proof.output ?? 'trace verification failed');
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The SDK does not implement cryptography or inspect the trace itself. It asks the
|
|
157
|
+
engine to verify the receipt and its signed binding. A receipt from a native
|
|
158
|
+
run carries the proof-bearing fields (`chain_head`, `chain_len`, `sealed`,
|
|
159
|
+
`trace_path`) and verifies locally. A receipt from a `nika serve` job carries
|
|
160
|
+
identity only (`job_id`, `execution_id`, `trace_id`, `snapshot_digest`,
|
|
161
|
+
`origin`): the resident writes no trace journal yet, so that receipt verifies
|
|
162
|
+
through no door today, and the same `NikaReceipt` type covers both shapes.
|
|
163
|
+
Persist it as the job's identity, not as evidence. The remote endpoint
|
|
164
|
+
currently returns `{ verified: false, verdict: "unavailable", reason:
|
|
165
|
+
"trace_journal_unavailable" }` because the server has no path-free journal
|
|
166
|
+
authority; the typed verdict is preserved instead of being hidden as a 404.
|
|
167
|
+
`/health.supportedCapabilities` names authorities that can currently complete
|
|
168
|
+
their operation. It therefore does not advertise remote trace verification
|
|
169
|
+
while this diagnostic route can only return the typed unavailable verdict.
|
|
170
|
+
A resident with journal authority will answer the CLI's tiers (`OK`, `SEALED`,
|
|
171
|
+
`ANCHORED`, `REPLAYED` hold; `INCOMPLETE`, `TAMPERED` do not), with no
|
|
172
|
+
`reason` on a verdict that holds; `verified` reads them the same way.
|
|
173
|
+
|
|
174
|
+
Run-signing keys remain engine-owned. `nika key init`, `nika key trust`, and
|
|
175
|
+
`nika key rotate` manage their lifecycle. Nika prefers the OS keychain and uses
|
|
176
|
+
0600 files under `~/.nika/keys/` only as the local fallback; CI can inject an
|
|
177
|
+
explicit pair through `NIKA_RUN_KEY_FILE` and `NIKA_RUN_PUB_FILE`. Applications
|
|
178
|
+
should persist receipts and public trust material, never copy a private run key
|
|
179
|
+
into SDK configuration, source control, workflow inputs, or an HTTP request.
|
|
180
|
+
|
|
181
|
+
## Cancel a run
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const run = await nika.run('slow.nika.yaml');
|
|
185
|
+
const cancellation = await nika.cancel(run);
|
|
186
|
+
const result = await run.done;
|
|
187
|
+
|
|
188
|
+
console.log(cancellation.accepted, result.status);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Cancellation is idempotent per `NikaRun`. An `AbortSignal` passed to `check`,
|
|
192
|
+
`events`, or `traceVerify` only stops that request or observer; it never stands
|
|
193
|
+
in for `cancel(run)`.
|
|
194
|
+
|
|
195
|
+
Over HTTP a running job answers the request with 202: `cancellation` reads
|
|
196
|
+
`{ accepted: true, status: 'cancellation_requested' }` and `run.done` settles
|
|
197
|
+
on the terminal the resident records, `cancelled`, `succeeded`, `failed`, or
|
|
198
|
+
`interrupted` once its grace expired. A job that already ended replays its
|
|
199
|
+
result with `accepted: false` and `status: 'already_settled'`. The native
|
|
200
|
+
transport signals its process the same way and settles `interrupted`.
|
|
201
|
+
|
|
202
|
+
## Connect to `nika serve`
|
|
203
|
+
|
|
204
|
+
A contained workflow name such as `hello.nika.yaml` or
|
|
205
|
+
`daily/report.nika.yaml` is resolved by the resident registry. `check()` and
|
|
206
|
+
`run()` send that name without a local engine or a local workflow file.
|
|
207
|
+
Use `listWorkflows()` to discover the served names.
|
|
208
|
+
|
|
209
|
+
To capture your local file instead, pass an explicit path such as
|
|
210
|
+
`./hello.nika.yaml`. The compatible local engine captures an immutable
|
|
211
|
+
snapshot, and the SDK sends its exact bytes and verifies the acknowledgement.
|
|
212
|
+
Only this path needs `bin`, `NIKA_BIN`, or the exact optional native package.
|
|
213
|
+
Observation and scheduling also use the server identity alone.
|
|
214
|
+
|
|
215
|
+
The current persistent server requires a project file. If you ran
|
|
216
|
+
`nika init --project-file` above you already have one (it carries a default
|
|
217
|
+
cost ceiling); do not overwrite it. Otherwise a minimal `nika.yaml` is enough:
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
nika: my-project
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Create a private bearer-token file and start the listener:
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
mkdir -p .nika
|
|
227
|
+
umask 077
|
|
228
|
+
openssl rand -hex 24 > .nika/serve.token
|
|
229
|
+
chmod 600 .nika/serve.token
|
|
230
|
+
|
|
231
|
+
nika serve \
|
|
232
|
+
--bind 127.0.0.1:8787 \
|
|
233
|
+
--workflows . \
|
|
234
|
+
--token-file .nika/serve.token \
|
|
235
|
+
--state-root .nika/serve
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Connect from Node:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { readFile } from 'node:fs/promises';
|
|
242
|
+
import { Nika } from '@supernovae-st/nika';
|
|
243
|
+
|
|
244
|
+
const token = (await readFile('.nika/serve.token', 'utf8')).trim();
|
|
245
|
+
const nika = new Nika({
|
|
246
|
+
url: 'http://127.0.0.1:8787',
|
|
247
|
+
token,
|
|
248
|
+
allowInsecureHttp: true, // required for explicit loopback HTTP
|
|
249
|
+
cwd: process.cwd(),
|
|
250
|
+
// bin: '/absolute/path/to/nika',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const report = await nika.check('hello.nika.yaml');
|
|
254
|
+
const run = await nika.run('hello.nika.yaml', {
|
|
255
|
+
idempotencyKey: 'hello-2026-08-30',
|
|
256
|
+
});
|
|
257
|
+
for await (const event of nika.events(run)) {
|
|
258
|
+
console.log(event.sequence, event.kind, event.status);
|
|
259
|
+
}
|
|
260
|
+
console.log(await run.done);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
If the Node process restarts after admission, recover the durable job without
|
|
264
|
+
submitting the workflow again:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const recovered = await nika.attachRun(saved.jobId, {
|
|
268
|
+
lastEventId: saved.lastEventSequence,
|
|
269
|
+
});
|
|
270
|
+
for await (const event of nika.events(recovered)) {
|
|
271
|
+
await saveApplicationCheckpoint(recovered.id, event.sequence);
|
|
272
|
+
}
|
|
273
|
+
console.log(await recovered.done);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Persist the job id and last committed sequence in application state. The
|
|
277
|
+
idempotency namespace spans the server's entire `state-root` and currently has
|
|
278
|
+
no TTL; use globally unique business keys and do not recycle them between
|
|
279
|
+
workflows.
|
|
280
|
+
|
|
281
|
+
When observation loses connectivity past its retry budget, the SDK performs
|
|
282
|
+
one final durable read before giving up: a terminal record settles `run.done`
|
|
283
|
+
from the workflow's truth, and a still-running record rejects with
|
|
284
|
+
`NikaObservationInterrupted`, whose `lastSequence` feeds
|
|
285
|
+
`attachRun(id, { lastEventId })` to resume.
|
|
286
|
+
|
|
287
|
+
Plain HTTP is accepted only for a loopback host (`localhost`, `127.0.0.0/8`,
|
|
288
|
+
`[::1]`), and only when `allowInsecureHttp: true` is explicit. Every other host
|
|
289
|
+
must use HTTPS: the opt-in widens the scheme, never the destination, so the
|
|
290
|
+
bearer token never leaves the machine in plaintext. A URL may not contain
|
|
291
|
+
credentials, a query, or a fragment, and a 32–512 byte visible-ASCII token is
|
|
292
|
+
mandatory.
|
|
293
|
+
|
|
294
|
+
Remote snapshots currently do not have request envelopes for per-call `vars`
|
|
295
|
+
or `model`; declare those facts in the workflow. There is no per-run spend
|
|
296
|
+
bound over HTTP at all today: `maxCostUsd` is refused, the workflow language
|
|
297
|
+
has no budget field, and the resident applies its own server-wide default
|
|
298
|
+
ceiling. Bound a remote run by its model and `max_tokens` until the request
|
|
299
|
+
envelope carries a ceiling. Likewise, remote `check` does not accept `model`
|
|
300
|
+
or `nativeStrict` overrides. Supplying these options returns a typed
|
|
301
|
+
compatibility refusal instead of silently dropping them.
|
|
41
302
|
|
|
42
|
-
|
|
303
|
+
## Resident schedules
|
|
43
304
|
|
|
44
|
-
|
|
305
|
+
Scheduling belongs to the resident HTTP authority. A direct native-process
|
|
306
|
+
client refuses `schedule` and `scheduleStatus` because a short-lived process
|
|
307
|
+
cannot honestly own durable schedule state.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
const applied = await nika.schedule('hello.nika.yaml', {
|
|
311
|
+
id: 'weekday-hello',
|
|
312
|
+
when: { kind: 'cadence', expression: 'TZ=Europe/Paris 0 9 * * 1-5' },
|
|
313
|
+
maxCostUsd: 0.01,
|
|
314
|
+
missed: 'catch-up-once',
|
|
315
|
+
overlap: 'skip',
|
|
316
|
+
afterSkip: 'next_slot',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const status = await nika.scheduleStatus('weekday-hello');
|
|
320
|
+
console.log(applied.changed, status.next, status.lastDecision);
|
|
321
|
+
|
|
322
|
+
await nika.schedule('hello.nika.yaml', {
|
|
323
|
+
id: 'weekday-hello',
|
|
324
|
+
when: { kind: 'cadence', expression: 'TZ=Europe/Paris 0 9 * * 1-5' },
|
|
325
|
+
maxCostUsd: 0.01,
|
|
326
|
+
missed: 'catch-up-once',
|
|
327
|
+
overlap: 'skip',
|
|
328
|
+
afterSkip: 'next_slot',
|
|
329
|
+
revision: status.revision,
|
|
330
|
+
active: false,
|
|
331
|
+
pauseReason: 'maintenance',
|
|
332
|
+
pauseUntil: '2026-09-01',
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Creates use `If-None-Match: *`; updates use the exact prior revision through
|
|
337
|
+
`If-Match`. Revisions are the opaque `sha256:<64 lowercase hex>` values returned
|
|
338
|
+
by the engine; callers must not invent placeholders. Stale well-formed writers
|
|
339
|
+
receive a typed operation error with the current revision. Returned planning
|
|
340
|
+
facts are engine-owned and additive.
|
|
341
|
+
|
|
342
|
+
Treat any `status.finding` recovered from older state as non-runnable. New active
|
|
343
|
+
declarations the current engine cannot plan are refused before durable mutation.
|
|
344
|
+
Timed hash jitter is currently unsupported and returns a typed refusal.
|
|
345
|
+
Cron expressions carry their zone as `TZ=<IANA zone> ...`; `tolerance` uses
|
|
346
|
+
`m/k`; `afterSkip` requires `overlap: "skip"` (the engine's default, so an
|
|
347
|
+
omitted `overlap` satisfies it); and `active: false` requires a `pauseReason`
|
|
348
|
+
together with a `pauseUntil` ISO calendar date (`YYYY-MM-DD`). Schedules refuse
|
|
349
|
+
`maxCostUsd: 0` ("must be positive and finite") where a native `run()` accepts
|
|
350
|
+
it; a scheduled budget is always a real number.
|
|
351
|
+
|
|
352
|
+
## Transport matrix
|
|
353
|
+
|
|
354
|
+
| Operation | Native process | HTTP |
|
|
355
|
+
|---|---|---|
|
|
356
|
+
| `check` | yes; `model` and `nativeStrict` allowed | yes; those two overrides refused |
|
|
357
|
+
| `run` | yes; `vars`, `model`, `maxCostUsd` allowed | yes; `idempotencyKey` allowed |
|
|
358
|
+
| `attachRun` | typed refusal | reattach to a durable job with an optional SSE cursor |
|
|
359
|
+
| `status` | typed refusal; await `run.done` | durable status projection |
|
|
360
|
+
| `events` | raw engine lifecycle frames | sequenced SSE frames with bounded replay |
|
|
361
|
+
| `cancel` | signal-backed, idempotent | 200 settles the job; 202 accepts the request and `run.done` settles on the resident's terminal |
|
|
362
|
+
| `traceVerify` | engine verification + signed receipt binding | typed verdict: `unavailable` until remote journal authority exists, then the CLI's tiers |
|
|
363
|
+
| `schedule` / `scheduleStatus` | typed refusal | resident schedule authority |
|
|
364
|
+
| `listWorkflows` / `workflow` | typed refusal | contained path-free workflow catalog |
|
|
365
|
+
|
|
366
|
+
Event vocabulary is deliberately open. Native execution exposes detailed task
|
|
367
|
+
lifecycle frames; HTTP exposes durable sequenced execution frames. Consumers
|
|
368
|
+
must not assume identical cardinality across transports.
|
|
369
|
+
|
|
370
|
+
## API
|
|
371
|
+
|
|
372
|
+
### `new Nika(config?)`
|
|
373
|
+
|
|
374
|
+
Shared options:
|
|
375
|
+
|
|
376
|
+
- `cwd`: engine working directory and snapshot root
|
|
377
|
+
- `bin`: explicit engine path
|
|
378
|
+
- `eventBufferSize`: per-client observer ceiling, default 256
|
|
379
|
+
- `machineBufferBytes`: machine frame/diagnostic ceiling, default 64 KiB
|
|
380
|
+
|
|
381
|
+
Remote-only options:
|
|
382
|
+
|
|
383
|
+
- `url`, `token`
|
|
384
|
+
- `allowInsecureHttp`
|
|
385
|
+
- `requestTimeout`, default 30 seconds
|
|
386
|
+
- `fetch`, for a custom standards-compatible implementation
|
|
387
|
+
|
|
388
|
+
### Methods
|
|
389
|
+
|
|
390
|
+
| Method | Result |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `check(workflow, options?)` | `clean` plus the native check report or resident acknowledgement/refusal |
|
|
393
|
+
| `run(workflow, options?)` | admitted `NikaRun` |
|
|
394
|
+
| `attachRun(id, options?)` | reattached durable HTTP `NikaRun` |
|
|
395
|
+
| `status(run)` | current durable HTTP status |
|
|
396
|
+
| `events(run, options?)` | bounded `AsyncIterable<NikaEvent>` |
|
|
397
|
+
| `cancel(run)` | `NikaCancelResult` |
|
|
398
|
+
| `traceVerify(receipt, options?)` | `NikaTraceVerifyResult` |
|
|
399
|
+
| `schedule(workflow, options)` | durable apply acknowledgement |
|
|
400
|
+
| `scheduleStatus(id)` | fresh engine schedule projection |
|
|
401
|
+
| `listWorkflows()` | contained resident workflow names |
|
|
402
|
+
| `workflow(name)` | path-free resident workflow metadata |
|
|
403
|
+
|
|
404
|
+
### Typed events, outputs, and identities
|
|
405
|
+
|
|
406
|
+
`NikaEvent` is a discriminated union over the known lifecycle kinds of both
|
|
407
|
+
transports. A native engine process emits `workflow_started`,
|
|
408
|
+
`task_scheduled`, `task_started`, `task_completed`, `workflow_completed`,
|
|
409
|
+
`workflow_failed`, `workflow_interrupted`, `run_settled`, and `run_sealed`. A
|
|
410
|
+
`nika serve` job streams `execution.started`, `execution.settled`,
|
|
411
|
+
`execution.cancelled`, `execution.refused`, and `execution.interrupted` (a
|
|
412
|
+
resident that restarts marks an orphaned running job `interrupted`). Kinds
|
|
413
|
+
this SDK version does not know yet stay representable through the
|
|
414
|
+
`NikaUnknownEvent` fallback, so the union is intentionally non-exhaustive and
|
|
415
|
+
every variant keeps its future fields open.
|
|
416
|
+
|
|
417
|
+
`run`, `attachRun`, and `events` accept one `Outputs` type argument. It types
|
|
418
|
+
the terminal settlement — `run.done` and the `run_settled` /
|
|
419
|
+
`execution.settled` / `workflow_completed` frames — without any runtime
|
|
420
|
+
validation, and defaults to `Record<string, unknown>` so untyped callers see
|
|
421
|
+
no change:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const run = await nika.run<{ answer: number }>('flow.nika.yaml');
|
|
425
|
+
const result = await run.done; // result.outputs?: { answer: number }
|
|
426
|
+
|
|
427
|
+
for await (const event of nika.events(run)) {
|
|
428
|
+
if (isNikaRunSettledEvent(event)) {
|
|
429
|
+
// The settlement frame of either transport (`run_settled` natively,
|
|
430
|
+
// `execution.settled` over HTTP): status, outputs, and receipt typed
|
|
431
|
+
// together on the one frame that carries all three.
|
|
432
|
+
console.log(event.status, event.outputs?.answer, event.receipt);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
A run can also end without settling outputs — cancelled, refused, or
|
|
438
|
+
interrupted. `isNikaTerminalEvent(event)` narrows those too: it reads the
|
|
439
|
+
engine-reported `status` (`succeeded`, `failed`, `interrupted`, `cancelled`)
|
|
440
|
+
rather than the kind, so it holds on either transport and on kinds this SDK
|
|
441
|
+
version does not know yet:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
for await (const event of nika.events(run)) {
|
|
445
|
+
if (isNikaTerminalEvent(event)) {
|
|
446
|
+
console.log('no further frames for this run:', event.status);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Run, execution, and job identities are branded opaque strings (`NikaRunId`,
|
|
452
|
+
`NikaExecutionId`, `NikaJobId`). They remain assignable to `string`, but a
|
|
453
|
+
plain `string` no longer stands in for one. Four words name three things:
|
|
454
|
+
a **workflow** is the file (or resident name) you pass in; a **run** is this
|
|
455
|
+
client's handle on one admission (`run.id`), and over HTTP that same string
|
|
456
|
+
is the server's **job** id (`/v1/jobs/{id}`, `attachRun(jobId)`); an
|
|
457
|
+
**execution** is the engine's own identity for what actually ran
|
|
458
|
+
(`execution_id`, the `execution.*` event kinds), distinct from the run id and
|
|
459
|
+
carried by the receipt together with the `trace_id`.
|
|
460
|
+
|
|
461
|
+
## Errors
|
|
462
|
+
|
|
463
|
+
Every error the SDK raises for an engine, transport, configuration, or
|
|
464
|
+
compatibility condition extends `NikaError`. Misuse of the API itself (an empty
|
|
465
|
+
workflow name, a negative event cursor, a receipt that is not an object, a
|
|
466
|
+
workflow name that escapes the catalog) throws a plain `TypeError` or
|
|
467
|
+
`RangeError` before any engine or network work starts:
|
|
468
|
+
|
|
469
|
+
```text
|
|
470
|
+
NikaError
|
|
471
|
+
├── NikaConfigurationError
|
|
472
|
+
├── NikaEngineUnavailable
|
|
473
|
+
├── NikaTransportError
|
|
474
|
+
│ ├── NikaProtocolError
|
|
475
|
+
│ └── NikaObservationInterrupted
|
|
476
|
+
├── NikaCompatibilityError
|
|
477
|
+
├── NikaOperationError
|
|
478
|
+
├── NikaEventBufferOverflowError
|
|
479
|
+
└── NikaRunOwnershipError
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Native engine event vocabulary stays open. HTTP events instead enforce the
|
|
483
|
+
closed, redacted `JobEvent` projection advertised by the pinned OpenAPI contract;
|
|
484
|
+
unknown HTTP fields are rejected at the trust boundary. The SDK never turns an
|
|
485
|
+
unpriced model into `$0`.
|
|
486
|
+
|
|
487
|
+
A refusal that `nika serve` types as `{ error: { code, message } }` surfaces as
|
|
488
|
+
`NikaOperationError` with the HTTP `status`, the server `code` (for example
|
|
489
|
+
`unauthorized`, `job_not_found`, `idempotency_conflict`, `malformed_snapshot`,
|
|
490
|
+
or a stamped `NIKA-…` admission code) and the `operation` that was refused.
|
|
491
|
+
Server messages are engine-owned and path-free; a reflected bearer token is
|
|
492
|
+
redacted before it reaches an error message. A non-2xx answer without that
|
|
493
|
+
typed body stays a `NikaTransportError` whose body is redacted entirely.
|
|
494
|
+
|
|
495
|
+
An engine refusal printed before a run starts — a `NIKA-…` code line such as a
|
|
496
|
+
cost-floor refusal — settles `run.done` with a `NikaOperationError` carrying
|
|
497
|
+
`operation: 'run'`, the engine's code, and its full refusal line.
|
|
498
|
+
|
|
499
|
+
## Security boundaries
|
|
500
|
+
|
|
501
|
+
- Token files stay out of argv and must be private (`0600`, 32–512 visible
|
|
502
|
+
ASCII bytes).
|
|
503
|
+
- The constructor refuses plaintext HTTP off loopback, and requires the
|
|
504
|
+
explicit `allowInsecureHttp: true` opt-in on loopback.
|
|
505
|
+
- `permits` remain default-deny engine policy; SDK types do not grant authority.
|
|
506
|
+
- Machine frames, diagnostics, SSE lines, and observer queues are bounded.
|
|
507
|
+
- Receipts and traces are engine-issued proof. The SDK never synthesizes them.
|
|
508
|
+
- This package does not export a webhook-signature verifier. Verify webhook raw
|
|
509
|
+
bodies with the sender's official library before admitting a Nika workflow.
|
|
510
|
+
|
|
511
|
+
## Development proof
|
|
512
|
+
|
|
513
|
+
```sh
|
|
514
|
+
npm test
|
|
515
|
+
npx tsc --noEmit
|
|
516
|
+
npm run build
|
|
517
|
+
|
|
518
|
+
# Five clean installations from an npm tarball
|
|
519
|
+
NIKA_BIN=/absolute/path/to/nika npm run gauntlet:projects
|
|
520
|
+
|
|
521
|
+
# Concurrency, cancellation, corrupt streams/traces, redaction, and soak
|
|
522
|
+
NIKA_BIN=/absolute/path/to/nika npm run gauntlet:hostile
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
The repository also carries 100 distinct use-case workflows and provider proof
|
|
526
|
+
under `gauntlet/`.
|
|
527
|
+
|
|
528
|
+
<!-- engine hero pinned to the release tag it demonstrates · re-pin on lockstep bumps -->
|
|
529
|
+

|
|
530
|
+
|
|
531
|
+
## Keeping it fresh
|
|
532
|
+
|
|
533
|
+
The client and engine follow one release train. `nika doctor` reports installed
|
|
534
|
+
drift without treating it as a workflow failure.
|
|
535
|
+
|
|
536
|
+
```sh
|
|
537
|
+
nika doctor
|
|
538
|
+
brew upgrade nika
|
|
539
|
+
npm update @supernovae-st/nika
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
<!-- city:map -->
|
|
543
|
+
## The city · where this repo sits
|
|
544
|
+
|
|
545
|
+
```text
|
|
546
|
+
📜 nika-spec ──── language law and conformance
|
|
547
|
+
│
|
|
548
|
+
▼
|
|
549
|
+
⚙️ nika ───────── engine, admission, execution, receipts and schedules
|
|
550
|
+
│
|
|
551
|
+
▼
|
|
552
|
+
🔌 nika-client ── this TypeScript door: native process or authenticated HTTP
|
|
553
|
+
│
|
|
554
|
+
▼
|
|
555
|
+
🧩 Node.js applications
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
This repository consumes engine behavior and serves TypeScript/JavaScript
|
|
559
|
+
applications. It is not authoritative for the workflow language.
|
|
560
|
+
|
|
561
|
+
All the buildings: [nika-spec](https://github.com/supernovae-st/nika-spec) ·
|
|
562
|
+
[nika](https://github.com/supernovae-st/nika) ·
|
|
563
|
+
[nika.sh](https://github.com/supernovae-st/nika.sh) ·
|
|
564
|
+
[nika-docs](https://github.com/supernovae-st/nika-docs) ·
|
|
565
|
+
[nika-client](https://github.com/supernovae-st/nika-client) ·
|
|
566
|
+
[nika-vscode](https://github.com/supernovae-st/nika-vscode) ·
|
|
567
|
+
[nika-plugins](https://github.com/supernovae-st/nika-plugins) ·
|
|
568
|
+
[gh-nika](https://github.com/supernovae-st/gh-nika) ·
|
|
569
|
+
[homebrew-tap](https://github.com/supernovae-st/homebrew-tap) ·
|
|
570
|
+
[nika-action](https://github.com/supernovae-st/nika-action) ·
|
|
571
|
+
[nika-actions-starter](https://github.com/supernovae-st/nika-actions-starter) ·
|
|
572
|
+
[nika-registry](https://github.com/supernovae-st/nika-registry) ·
|
|
573
|
+
[nika-estate](https://github.com/supernovae-st/nika-estate).
|
|
574
|
+
<!-- /city:map -->
|
|
575
|
+
|
|
576
|
+
## License
|
|
45
577
|
|
|
46
|
-
-
|
|
47
|
-
|
|
578
|
+
[Apache-2.0](LICENSE). The engine remains AGPL-3.0-or-later; importing this SDK
|
|
579
|
+
does not impose the engine's copyleft license on your application.
|