@supernovae-st/nika 0.120.0 → 0.120.3
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/README.md +177 -75
- package/dist/bin/nika.js +40 -14
- package/dist/index.cjs +635 -90
- package/dist/index.d.cts +133 -2
- package/dist/index.d.ts +133 -2
- package/dist/index.js +635 -90
- package/docs/architecture.md +3 -1
- package/docs/http-api.md +24 -0
- package/docs/testing.md +46 -1
- package/openapi.json +340 -2
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
<h1 align="center">@supernovae-st/nika</h1>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<strong>
|
|
14
|
-
|
|
13
|
+
<strong>Run AI workflows from TypeScript.</strong><br>
|
|
14
|
+
Audit a <code>.nika</code> file, run it, read the result, verify the receipt — locally
|
|
15
|
+
or against authenticated <code>nika serve</code>.
|
|
15
16
|
</p>
|
|
16
17
|
|
|
17
18
|
<p align="center">
|
|
@@ -29,22 +30,17 @@
|
|
|
29
30
|
<a href="https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/supernovae-st/nika-client"><img src="https://archive.softwareheritage.org/badge/origin/https://github.com/supernovae-st/nika-client/" alt="Archived by Software Heritage"></a>
|
|
30
31
|
</p>
|
|
31
32
|
|
|
32
|
-
##
|
|
33
|
+
## Run a workflow from your app
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
Put repeatable AI work in a `.nika` file. From Node, run it and read the
|
|
36
|
+
result. No server and no API key for the first run: `mock/echo` is a
|
|
37
|
+
**local simulation** (output is prefixed `mock(echo) ·`, not a model answer).
|
|
36
38
|
|
|
37
39
|
```sh
|
|
38
|
-
npm install @supernovae-st/nika
|
|
39
|
-
./node_modules/.bin/nika --version
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
nika 0.118.7 (f3a31a6ee)
|
|
40
|
+
npm install @supernovae-st/nika
|
|
44
41
|
```
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
network:
|
|
43
|
+
Save as `hello.nika`:
|
|
48
44
|
|
|
49
45
|
```yaml
|
|
50
46
|
nika: hello
|
|
@@ -59,7 +55,35 @@ outputs:
|
|
|
59
55
|
greeting: ${{ tasks.greeting.output }}
|
|
60
56
|
```
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
Save as `demo.mjs`:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
import { Nika, isNikaRunSucceeded } from '@supernovae-st/nika';
|
|
62
|
+
|
|
63
|
+
const run = await new Nika({ cwd: process.cwd() }).run('hello.nika', { maxCostUsd: 0 });
|
|
64
|
+
const result = await run.result();
|
|
65
|
+
if (!isNikaRunSucceeded(result)) {
|
|
66
|
+
console.error(result.status, result.error?.code, result.error?.message);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
} else {
|
|
69
|
+
console.log(result.outputs);
|
|
70
|
+
// { greeting: "mock(echo) · Say hello from the Nika SDK." }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
node demo.mjs
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`run()` already admits: a red file throws `NikaOperationError` and never
|
|
79
|
+
returns a handle. The `.nika` file is the contract; the SDK does not parse
|
|
80
|
+
YAML. Pin the version you tested — see [Install](#install): the SDK and the
|
|
81
|
+
standalone engine CLI release on independent clocks.
|
|
82
|
+
|
|
83
|
+
`check()`, `run.events()`, and `traceVerify()` are the next steps. They stay
|
|
84
|
+
taught and tested; they are not required to see the first result.
|
|
85
|
+
|
|
86
|
+
### Next: audit without running (`check`)
|
|
63
87
|
|
|
64
88
|
```sh
|
|
65
89
|
./node_modules/.bin/nika check hello.nika
|
|
@@ -74,45 +98,31 @@ Audit it before anything runs:
|
|
|
74
98
|
layers · valid ✔ · access ready ✔ · capacity fit ✔ · run ready ✔
|
|
75
99
|
```
|
|
76
100
|
|
|
77
|
-
|
|
101
|
+
The same report is `await nika.check('hello.nika', { nativeStrict: true })`.
|
|
102
|
+
A red check never becomes a run.
|
|
78
103
|
|
|
79
|
-
|
|
80
|
-
import { Nika } from '@supernovae-st/nika';
|
|
81
|
-
|
|
82
|
-
const nika = new Nika({
|
|
83
|
-
cwd: process.cwd(),
|
|
84
|
-
// bin: '/absolute/path/to/nika', // or set NIKA_BIN
|
|
85
|
-
});
|
|
104
|
+
### Next: watch the run (`run.events()`)
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
nativeStrict: true,
|
|
89
|
-
});
|
|
90
|
-
if (!report.clean) throw new Error('workflow did not pass nika check');
|
|
91
|
-
|
|
92
|
-
const run = await nika.run('hello.nika', { maxCostUsd: 0 });
|
|
106
|
+
```ts
|
|
93
107
|
for await (const event of run.events()) {
|
|
94
|
-
// The same lifecycle words on both transports; the engine's own frame,
|
|
95
|
-
// in its protocol vocabulary, stays on event.raw.
|
|
96
108
|
console.log([event.kind, event.task, event.status].filter(Boolean).join(' '));
|
|
97
109
|
}
|
|
98
|
-
|
|
99
|
-
const result = await run.result();
|
|
100
|
-
console.log(result.status, result.outputs, result.receipt);
|
|
101
110
|
```
|
|
102
111
|
|
|
103
|
-
Expected
|
|
104
|
-
`task.
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
That is six frames for one task; the same shape with 90 tasks was measured at
|
|
111
|
-
273 (`3N + 3`). A session retains the most recent 4096 frames by default, so
|
|
112
|
-
`run.events()` opened after `run.result()` replays them all, and is refused,
|
|
113
|
-
never shortened, past that bound; see
|
|
112
|
+
Expected native events for this one-task file: `run.started`,
|
|
113
|
+
`task.scheduled greeting`, `task.started greeting`,
|
|
114
|
+
`task.completed greeting`, `engine.event` (`workflow_completed` on
|
|
115
|
+
`event.raw.kind`), then `run.settled succeeded`. Six `run.events()` frames,
|
|
116
|
+
sealed or not (probed on 0.120.0). A session retains the most recent 4096
|
|
117
|
+
frames by default; see
|
|
114
118
|
[Observing a run after the fact](#observing-a-run-after-the-fact).
|
|
115
119
|
|
|
120
|
+
### Next: verify a seal (`traceVerify`)
|
|
121
|
+
|
|
122
|
+
`traceVerify` **verifies** an existing seal. It does not create one. A keyless
|
|
123
|
+
machine still **succeeds**; the receipt is unsealed — see
|
|
124
|
+
[Admission, execution, seal](#admission-execution-seal).
|
|
125
|
+
|
|
116
126
|
<p align="center">
|
|
117
127
|
<img src="https://raw.githubusercontent.com/supernovae-st/nika-client/main/media/local-driver.gif" alt="The typed driver over the released binary: check the workflow, gate on the report, run it to the end under a cost ceiling, count the events" width="960">
|
|
118
128
|
</p>
|
|
@@ -183,8 +193,11 @@ An engine without the capability rejects with `NikaCompatibilityError`
|
|
|
183
193
|
(`capability: 'inputsLiteral'` or `'jobInputs'`) and nothing runs. The SDK
|
|
184
194
|
never falls back to `--var`, and a resident that merely answers 202 has
|
|
185
195
|
negotiated nothing: one from before the envelope accepts the field and ignores
|
|
186
|
-
its values.
|
|
187
|
-
|
|
196
|
+
its values.
|
|
197
|
+
|
|
198
|
+
The **published 0.120.0** payload advertises `inputsLiteral` (`nika --sdk-identity`).
|
|
199
|
+
Native `run({ inputs })` works on it. It does **not** advertise `jobInputs`;
|
|
200
|
+
HTTP literal inputs stay a compatibility refusal until a resident that does.
|
|
188
201
|
|
|
189
202
|
`vars` is deprecated. It remains the native `--var KEY=VALUE` operator channel,
|
|
190
203
|
unchanged: the engine reads `@env:NAME` from its environment and coerces text
|
|
@@ -197,10 +210,9 @@ A `try { await run.result() } catch {}` alone therefore never catches a failed
|
|
|
197
210
|
workflow: a CI job or an application must read `result.status` and treat
|
|
198
211
|
anything but `succeeded` as its own failure, or a red run passes silently.
|
|
199
212
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
For CI built from this branch:
|
|
213
|
+
`isNikaRunSucceeded` is exported by published **0.120.0**. Use it (or
|
|
214
|
+
`result.status === 'succeeded'`) before treating a result as success.
|
|
215
|
+
Paused, failed, cancelled, and interrupted all return false:
|
|
204
216
|
|
|
205
217
|
```ts
|
|
206
218
|
import { Nika, isNikaRunSucceeded } from '@supernovae-st/nika';
|
|
@@ -229,17 +241,40 @@ into a fabricated output map.
|
|
|
229
241
|
secret and the cost floor. A red check never becomes a run.
|
|
230
242
|
- **Sovereign by default.** The same file runs on local models (Ollama,
|
|
231
243
|
llama.cpp, vLLM), on Mistral, Hugging Face, OpenAI, xAI, Anthropic and the
|
|
232
|
-
rest of the engine's catalog; `mock/echo`
|
|
233
|
-
network.
|
|
234
|
-
- **Traced after.**
|
|
235
|
-
|
|
236
|
-
re-implements the proof.
|
|
244
|
+
rest of the engine's catalog; `mock/echo` is a local simulation with no key
|
|
245
|
+
and no network.
|
|
246
|
+
- **Traced after.** A native run writes a hash-chained journal and a receipt.
|
|
247
|
+
`traceVerify()` asks the engine to verify that evidence. A succeeded run can
|
|
248
|
+
still be unsealed. The SDK never re-implements the proof.
|
|
237
249
|
- **One vocabulary, two transports.** `check`, `run`, `events`, `cancel`,
|
|
238
250
|
`traceVerify` and `schedule` read the same against a local process and an
|
|
239
251
|
authenticated `nika serve`; only the constructor changes. Run events carry
|
|
240
252
|
the same lifecycle words on both (`run.started`, `run.waiting`,
|
|
241
253
|
`run.settled`), with the engine's own frame kept on `event.raw`.
|
|
242
254
|
|
|
255
|
+
## Admission, execution, seal
|
|
256
|
+
|
|
257
|
+
Three different facts. Do not collapse them.
|
|
258
|
+
|
|
259
|
+
| Fact | How you see it | What it is not |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| **Admission** | `check()` returns `clean: false` and findings. `run()` **throws** `NikaOperationError` (`NIKA-PARSE-005`, `NIKA-1708`, …) and yields no handle. | Not a successful execution. |
|
|
262
|
+
| **Execution** | `run.result()` / `isNikaRunSucceeded(result)`. Admitted failure is **data** (`status: "failed"`). A human gate is `paused` (`run.waiting`), not success. | Not proof the journal is sealed. |
|
|
263
|
+
| **Seal** | `result.receipt.sealed` and `traceVerify(receipt)`. Tamper-evident, not tamper-proof; not replayed unless you pass `--replay`; not anchored without a sidecar. | Not “the workflow was correct” and not “a human read the output”. |
|
|
264
|
+
|
|
265
|
+
Probed on published 0.120.0 (2026-09-19): `run.events()` still yields the
|
|
266
|
+
same six lifecycle frames with or without a signing key. With
|
|
267
|
+
`~/.nika/keys/run-signing.*`, the hello fixture sealed and `traceVerify`
|
|
268
|
+
returned `{ verified: true, verdict: "verified" }`. With an isolated `HOME`
|
|
269
|
+
and no key files (keychain skipped: stderr not a TTY), the same file
|
|
270
|
+
**succeeded**, `sealed: false`, and `traceVerify` returned
|
|
271
|
+
`{ verified: false, verdict: "invalid", reason: "receipt_mismatch" }` while
|
|
272
|
+
the engine still said the journal chain was OK and **UNSEALED**. In this
|
|
273
|
+
keyless case that reason means no signed binding, not a failed workflow.
|
|
274
|
+
`receipt_mismatch` is also the engine's word for a tampered or
|
|
275
|
+
field-mismatched receipt — do not treat every occurrence as merely unsigned.
|
|
276
|
+
The CLI verify line that counts journal events is not `run.events()`.
|
|
277
|
+
|
|
243
278
|
## One vocabulary
|
|
244
279
|
|
|
245
280
|
`Nika` exposes `check`, `run`, `attachRun`, `traceVerify`, `listWorkflows`,
|
|
@@ -287,19 +322,34 @@ not parse YAML or reconstruct proof in TypeScript.
|
|
|
287
322
|
Socratic risk matrix
|
|
288
323
|
- [Migrating to 0.116](docs/migrating-to-0.116.md) · the intentional breaking
|
|
289
324
|
migration to the smaller durable client surface
|
|
290
|
-
- [docs.nika.sh](https://docs.nika.sh) ·
|
|
291
|
-
|
|
325
|
+
- [docs.nika.sh](https://docs.nika.sh) · language and engine
|
|
326
|
+
- [SDK quickstart](https://docs.nika.sh/sdk/start/quickstart) · app walkthrough
|
|
327
|
+
(install **npm `@supernovae-st/nika`**, matching this README)
|
|
292
328
|
|
|
293
329
|
## Install
|
|
294
330
|
|
|
295
|
-
|
|
296
|
-
|
|
331
|
+
```sh
|
|
332
|
+
npm install @supernovae-st/nika
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Every SDK package bundles its own matching engine: the `nika` binary under
|
|
336
|
+
`node_modules/.bin` is the exact engine that package was qualified against.
|
|
337
|
+
The standalone engine CLI (GitHub releases, brew, install script) releases on
|
|
338
|
+
an independent clock, so its newest tag can be ahead of or behind the engine
|
|
339
|
+
bundled in the latest npm package. The
|
|
340
|
+
[npm registry](https://www.npmjs.com/package/@supernovae-st/nika?activeTab=versions)
|
|
341
|
+
lists current published versions. Pin the version you tested, then verify the
|
|
342
|
+
package the project actually resolved:
|
|
297
343
|
|
|
298
344
|
```sh
|
|
299
|
-
npm install @supernovae-st/nika@0.118.7
|
|
300
345
|
node -p "require('@supernovae-st/nika/package.json').version"
|
|
346
|
+
./node_modules/.bin/nika --version
|
|
301
347
|
```
|
|
302
348
|
|
|
349
|
+
This repository's source train is **0.120.3** (`package.json`), lockstep with
|
|
350
|
+
public engine tag `v0.120.3` (`578352a31`). A source train is not a published
|
|
351
|
+
npm version until the release workflow publishes it.
|
|
352
|
+
|
|
303
353
|
This package metadata subpath is exported for CommonJS, ESM build tools and CI
|
|
304
354
|
pin checks. It reports the installed dependency, not a moving registry tag.
|
|
305
355
|
The native payloads `@supernovae-st/nika-<os>-<arch>` are optional
|
|
@@ -318,19 +368,66 @@ deprecated`. The repository keeps its name (`supernovae-st/nika-client`).
|
|
|
318
368
|
|
|
319
369
|
## Scaffold with the engine
|
|
320
370
|
|
|
321
|
-
|
|
371
|
+
On the published 0.120.0 binary, `nika new` is **retired** (`unrecognized
|
|
372
|
+
subcommand`). The creation door is `compile`. Exact skeleton name or `hello`;
|
|
373
|
+
free-text intent stays incomplete and writes nothing:
|
|
322
374
|
|
|
323
375
|
```sh
|
|
324
376
|
./node_modules/.bin/nika init --project-file
|
|
325
|
-
./node_modules/.bin/nika
|
|
377
|
+
./node_modules/.bin/nika compile hello hello.nika
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
No destination means preview only. `nika.yaml` is the project control plane
|
|
381
|
+
(needed for `nika serve`). `hello.nika` is the executable contract passed to
|
|
382
|
+
`check()` and `run()`. The engine's `hello` skeleton names its task `greet`
|
|
383
|
+
and asks for French; the hand-written file above is enough if it keeps
|
|
384
|
+
`outputs.greeting` and `model: mock/echo`.
|
|
385
|
+
|
|
386
|
+
## Compile a candidate without running it
|
|
387
|
+
|
|
388
|
+
`compile()` requires an engine that advertises the `compile` capability. The
|
|
389
|
+
released 0.120.3 engine supports native compilation and its Serve advertises
|
|
390
|
+
HTTP compilation (the route landed in engine commit `4334e58b`, first published
|
|
391
|
+
in v0.120.3); a 0.120.2 or older Serve predates that route and is refused
|
|
392
|
+
without fallback. This SDK method first publishes with 0.120.3. This foundation resolves
|
|
393
|
+
exact embedded skeleton names (including `hello`) and edits existing constants;
|
|
394
|
+
unsupported intent remains `incomplete`.
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
const candidate = await nika.compile({
|
|
398
|
+
intent: 'classify-and-route',
|
|
399
|
+
answers: { 'const.request': 'An outage affects support customers.' },
|
|
400
|
+
}, { timeoutMs: 15_000 });
|
|
401
|
+
|
|
402
|
+
// The outcome is source and review data. Incomplete/refused are also outcomes.
|
|
403
|
+
console.log(candidate.status, candidate.questions, candidate.diagnostics);
|
|
404
|
+
if (candidate.ready && candidate.candidate !== null) {
|
|
405
|
+
const edited = await nika.compile({
|
|
406
|
+
workflow: candidate.candidate,
|
|
407
|
+
change: { set_constant: { name: 'request', value: 'One customer is affected.' } },
|
|
408
|
+
});
|
|
409
|
+
console.log(edited.candidate);
|
|
410
|
+
}
|
|
326
411
|
```
|
|
327
412
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
413
|
+
Edits also accept a string such as `Set const.request to "One customer"`.
|
|
414
|
+
Answers and structured values are strict JSON values, preserving numbers,
|
|
415
|
+
booleans, null, strings, arrays and objects. The native adapter uses the engine's
|
|
416
|
+
CLI; the HTTP adapter sends authenticated `POST /v1/compile`. An unavailable
|
|
417
|
+
remote capability raises `NikaCompatibilityError` without local fallback.
|
|
418
|
+
|
|
419
|
+
`candidate` is `.nika` source, distinct from the path accepted by `run()`.
|
|
420
|
+
The caller reviews and materializes it before calling `run(path)`, which performs
|
|
421
|
+
normal admission. `requested_boundary` and the source-only `check_preview` grant
|
|
422
|
+
no execution authority. Compile creates no Run, job, approval or Proof, and the
|
|
423
|
+
SDK writes no persistent candidate file. `signal`/`timeoutMs` stop only the
|
|
424
|
+
compile request. The common outcome has no `exitCode` or `written` field.
|
|
425
|
+
|
|
426
|
+
Compile accepts standard `AbortSignal`s, including `AbortSignal.any` composites.
|
|
427
|
+
It rejects direct signal interface overrides and proxies before starting work.
|
|
428
|
+
Composite sources must retain their standard interfaces: Node may read their
|
|
429
|
+
public fields while inspecting or subscribing to a composite, so the SDK cannot
|
|
430
|
+
validate a modified hidden source graph without invoking those fields.
|
|
334
431
|
|
|
335
432
|
## Verify a local trace
|
|
336
433
|
|
|
@@ -340,7 +437,12 @@ Pass that receipt back unchanged:
|
|
|
340
437
|
```ts
|
|
341
438
|
if (!result.receipt) throw new Error('run did not issue a receipt');
|
|
342
439
|
const proof = await nika.traceVerify(result.receipt);
|
|
343
|
-
|
|
440
|
+
// proof.verified is the seal/binding, not the workflow outcome.
|
|
441
|
+
if (isNikaRunSucceeded(result) && proof.verified) {
|
|
442
|
+
// run succeeded and the journal is sealed
|
|
443
|
+
} else if (isNikaRunSucceeded(result) && !proof.verified) {
|
|
444
|
+
// succeeded, unsigned or otherwise unverified — read proof.reason
|
|
445
|
+
}
|
|
344
446
|
```
|
|
345
447
|
|
|
346
448
|
The SDK does not implement cryptography or inspect the trace itself. It asks the
|
|
@@ -388,9 +490,8 @@ settles on the terminal the resident records, `cancelled`, `succeeded`,
|
|
|
388
490
|
`failed`, or `interrupted` once its grace expired. A job that already ended
|
|
389
491
|
replays its result with `accepted: false` and `status: 'already_settled'`. The
|
|
390
492
|
native transport signals its process the same way; the result is whatever the
|
|
391
|
-
engine then wrote, `cancelled` when it settled the request
|
|
392
|
-
|
|
393
|
-
settlement frame.
|
|
493
|
+
engine then wrote, `cancelled` when it settled the request, or `interrupted`
|
|
494
|
+
when the process ended with no settlement frame.
|
|
394
495
|
|
|
395
496
|
## Connect to `nika serve`
|
|
396
497
|
|
|
@@ -646,8 +747,9 @@ A view opened late is seeded with every frame the session observed, or it is
|
|
|
646
747
|
refused. It is never handed a shortened replay. How many frames a session
|
|
647
748
|
retains is `eventBufferSize`, **4096 by default**.
|
|
648
749
|
|
|
649
|
-
**The measured frame count: `3N + 3`, for one shape.** On the released
|
|
650
|
-
engine
|
|
750
|
+
**The measured frame count: `3N + 3`, for one sealed shape.** On the released
|
|
751
|
+
0.118.7 engine (and still the native 0.120.0 hello fixture when sealed), a
|
|
752
|
+
clean run of N independent `mock/echo` `infer` tasks wrote
|
|
651
753
|
`workflow_started`, then `task_scheduled`, `task_started` and `task_completed`
|
|
652
754
|
per task, then `workflow_completed` and `run_settled`. Two points were
|
|
653
755
|
measured: 1 task is 6 frames, 90 tasks are 273. That is this fixture, not a
|
|
@@ -957,7 +1059,7 @@ The repository also carries 100 distinct use-case workflows and provider proof
|
|
|
957
1059
|
under `gauntlet/`.
|
|
958
1060
|
|
|
959
1061
|
<!-- engine hero pinned to the release tag it demonstrates · re-pin on lockstep bumps -->
|
|
960
|
-

|
|
961
1063
|
|
|
962
1064
|
## Keeping it fresh
|
|
963
1065
|
|
package/dist/bin/nika.js
CHANGED
|
@@ -150,29 +150,41 @@ function captureEngine(bin, args, options) {
|
|
|
150
150
|
shell: false,
|
|
151
151
|
stdio: ["ignore", "pipe", "pipe"]
|
|
152
152
|
});
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
const stdout = [];
|
|
154
|
+
const stderr = [];
|
|
155
|
+
const bytes = { stdout: 0, stderr: 0 };
|
|
155
156
|
let overflow = false;
|
|
156
157
|
let spawnError;
|
|
158
|
+
let closed = false;
|
|
159
|
+
let killTimer;
|
|
160
|
+
const stop = () => {
|
|
161
|
+
child.kill("SIGTERM");
|
|
162
|
+
if (options.killGraceMs === void 0) return;
|
|
163
|
+
killTimer ??= setTimeout(() => {
|
|
164
|
+
if (!closed) child.kill("SIGKILL");
|
|
165
|
+
}, options.killGraceMs);
|
|
166
|
+
killTimer.unref();
|
|
167
|
+
};
|
|
157
168
|
const append = (stream, chunk) => {
|
|
158
169
|
if (overflow) return;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
|
|
170
|
+
bytes[stream] += chunk.byteLength;
|
|
171
|
+
if (bytes[stream] > options.bufferBytes) {
|
|
162
172
|
overflow = true;
|
|
163
|
-
|
|
173
|
+
stop();
|
|
174
|
+
return;
|
|
164
175
|
}
|
|
176
|
+
(stream === "stdout" ? stdout : stderr).push(chunk);
|
|
165
177
|
};
|
|
166
|
-
child.stdout.setEncoding("utf8");
|
|
167
|
-
child.stderr.setEncoding("utf8");
|
|
168
178
|
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
169
179
|
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
170
|
-
const abort = () =>
|
|
180
|
+
const abort = () => stop();
|
|
171
181
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
172
182
|
child.once("error", (cause) => {
|
|
173
183
|
spawnError = cause;
|
|
174
184
|
});
|
|
175
|
-
child.once("close", (code) => {
|
|
185
|
+
child.once("close", (code, exitSignal) => {
|
|
186
|
+
closed = true;
|
|
187
|
+
if (killTimer) clearTimeout(killTimer);
|
|
176
188
|
options.signal?.removeEventListener("abort", abort);
|
|
177
189
|
if (options.signal?.aborted) {
|
|
178
190
|
reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
|
|
@@ -188,7 +200,19 @@ function captureEngine(bin, args, options) {
|
|
|
188
200
|
`${options.label} exceeded ${options.bufferBytes} bytes`
|
|
189
201
|
));
|
|
190
202
|
} else {
|
|
191
|
-
|
|
203
|
+
let decoded;
|
|
204
|
+
try {
|
|
205
|
+
decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stdout, bytes.stdout));
|
|
206
|
+
} catch {
|
|
207
|
+
reject(new NikaProtocolError(options.transport, `${options.label} stdout was not valid UTF-8`));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
resolve({
|
|
211
|
+
exitCode: code ?? 3,
|
|
212
|
+
stdout: decoded,
|
|
213
|
+
stderr: Buffer.concat(stderr, bytes.stderr).toString("utf8"),
|
|
214
|
+
exitSignal
|
|
215
|
+
});
|
|
192
216
|
}
|
|
193
217
|
});
|
|
194
218
|
});
|
|
@@ -252,9 +276,9 @@ function incompatible(transport, message) {
|
|
|
252
276
|
|
|
253
277
|
// src/lib/binary/verify.ts
|
|
254
278
|
var IDENTITY_BUFFER_BYTES = 16 * 1024;
|
|
255
|
-
async function verifyNikaEngine(engine) {
|
|
279
|
+
async function verifyNikaEngine(engine, options = {}) {
|
|
256
280
|
const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
|
|
257
|
-
const identity = await probeIdentity(engine.bin);
|
|
281
|
+
const identity = await probeIdentity(engine.bin, options);
|
|
258
282
|
if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
|
|
259
283
|
throw incompatible2(
|
|
260
284
|
`Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
|
|
@@ -293,13 +317,15 @@ async function verifyManagedPayload(engine) {
|
|
|
293
317
|
}
|
|
294
318
|
return manifest.version;
|
|
295
319
|
}
|
|
296
|
-
async function probeIdentity(bin) {
|
|
320
|
+
async function probeIdentity(bin, options) {
|
|
297
321
|
const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
|
|
298
322
|
const captured = await captureEngine(bin, ["--sdk-identity"], {
|
|
323
|
+
...options,
|
|
299
324
|
bufferBytes: IDENTITY_BUFFER_BYTES,
|
|
300
325
|
transport: "native-process",
|
|
301
326
|
label: "Engine identity probe"
|
|
302
327
|
}).catch((cause) => {
|
|
328
|
+
if (options.signal?.aborted) throw cause;
|
|
303
329
|
throw incompatible2(
|
|
304
330
|
`Engine identity probe of ${bin} failed`,
|
|
305
331
|
cause
|