@supernovae-st/nika 0.120.2 → 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 +48 -2
- 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/http-api.md +24 -0
- package/docs/testing.md +37 -0
- package/openapi.json +340 -2
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -203,29 +203,41 @@ function captureEngine(bin, args, options) {
|
|
|
203
203
|
shell: false,
|
|
204
204
|
stdio: ["ignore", "pipe", "pipe"]
|
|
205
205
|
});
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
const stdout = [];
|
|
207
|
+
const stderr = [];
|
|
208
|
+
const bytes = { stdout: 0, stderr: 0 };
|
|
208
209
|
let overflow = false;
|
|
209
210
|
let spawnError;
|
|
211
|
+
let closed = false;
|
|
212
|
+
let killTimer;
|
|
213
|
+
const stop = () => {
|
|
214
|
+
child.kill("SIGTERM");
|
|
215
|
+
if (options.killGraceMs === void 0) return;
|
|
216
|
+
killTimer ??= setTimeout(() => {
|
|
217
|
+
if (!closed) child.kill("SIGKILL");
|
|
218
|
+
}, options.killGraceMs);
|
|
219
|
+
killTimer.unref();
|
|
220
|
+
};
|
|
210
221
|
const append = (stream, chunk) => {
|
|
211
222
|
if (overflow) return;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
|
|
223
|
+
bytes[stream] += chunk.byteLength;
|
|
224
|
+
if (bytes[stream] > options.bufferBytes) {
|
|
215
225
|
overflow = true;
|
|
216
|
-
|
|
226
|
+
stop();
|
|
227
|
+
return;
|
|
217
228
|
}
|
|
229
|
+
(stream === "stdout" ? stdout : stderr).push(chunk);
|
|
218
230
|
};
|
|
219
|
-
child.stdout.setEncoding("utf8");
|
|
220
|
-
child.stderr.setEncoding("utf8");
|
|
221
231
|
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
222
232
|
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
223
|
-
const abort = () =>
|
|
233
|
+
const abort = () => stop();
|
|
224
234
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
225
235
|
child.once("error", (cause) => {
|
|
226
236
|
spawnError = cause;
|
|
227
237
|
});
|
|
228
|
-
child.once("close", (code) => {
|
|
238
|
+
child.once("close", (code, exitSignal) => {
|
|
239
|
+
closed = true;
|
|
240
|
+
if (killTimer) clearTimeout(killTimer);
|
|
229
241
|
options.signal?.removeEventListener("abort", abort);
|
|
230
242
|
if (options.signal?.aborted) {
|
|
231
243
|
reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
|
|
@@ -241,7 +253,19 @@ function captureEngine(bin, args, options) {
|
|
|
241
253
|
`${options.label} exceeded ${options.bufferBytes} bytes`
|
|
242
254
|
));
|
|
243
255
|
} else {
|
|
244
|
-
|
|
256
|
+
let decoded;
|
|
257
|
+
try {
|
|
258
|
+
decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stdout, bytes.stdout));
|
|
259
|
+
} catch {
|
|
260
|
+
reject(new NikaProtocolError(options.transport, `${options.label} stdout was not valid UTF-8`));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
resolve({
|
|
264
|
+
exitCode: code ?? 3,
|
|
265
|
+
stdout: decoded,
|
|
266
|
+
stderr: Buffer.concat(stderr, bytes.stderr).toString("utf8"),
|
|
267
|
+
exitSignal
|
|
268
|
+
});
|
|
245
269
|
}
|
|
246
270
|
});
|
|
247
271
|
});
|
|
@@ -473,9 +497,9 @@ function incompatible(transport, message) {
|
|
|
473
497
|
|
|
474
498
|
// src/lib/binary/verify.ts
|
|
475
499
|
var IDENTITY_BUFFER_BYTES = 16 * 1024;
|
|
476
|
-
async function verifyNikaEngine(engine) {
|
|
500
|
+
async function verifyNikaEngine(engine, options = {}) {
|
|
477
501
|
const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
|
|
478
|
-
const identity = await probeIdentity(engine.bin);
|
|
502
|
+
const identity = await probeIdentity(engine.bin, options);
|
|
479
503
|
if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
|
|
480
504
|
throw incompatible2(
|
|
481
505
|
`Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
|
|
@@ -514,13 +538,15 @@ async function verifyManagedPayload(engine) {
|
|
|
514
538
|
}
|
|
515
539
|
return manifest.version;
|
|
516
540
|
}
|
|
517
|
-
async function probeIdentity(bin) {
|
|
541
|
+
async function probeIdentity(bin, options) {
|
|
518
542
|
const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
|
|
519
543
|
const captured = await captureEngine(bin, ["--sdk-identity"], {
|
|
544
|
+
...options,
|
|
520
545
|
bufferBytes: IDENTITY_BUFFER_BYTES,
|
|
521
546
|
transport: "native-process",
|
|
522
547
|
label: "Engine identity probe"
|
|
523
548
|
}).catch((cause) => {
|
|
549
|
+
if (options.signal?.aborted) throw cause;
|
|
524
550
|
throw incompatible2(
|
|
525
551
|
`Engine identity probe of ${bin} failed`,
|
|
526
552
|
cause
|
|
@@ -600,25 +626,26 @@ var NATIVE_ARRAY = Function.prototype.toString.call(Array);
|
|
|
600
626
|
var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
|
|
601
627
|
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
602
628
|
var PATH_LIMIT = 240;
|
|
603
|
-
function encodeLiteralInputs(inputs) {
|
|
629
|
+
function encodeLiteralInputs(inputs, label = "run({ inputs })") {
|
|
604
630
|
if (containerKind(inputs) !== "object") {
|
|
631
|
+
const subject = label === "run({ inputs })" ? "inputs must be a plain object mapping declared workflow input names" : "answers must be a plain object mapping stable question keys";
|
|
605
632
|
throw new NikaConfigurationError(
|
|
606
|
-
|
|
633
|
+
`${label}: ${subject} to strict JSON values; received ${describe(inputs)}`
|
|
607
634
|
);
|
|
608
635
|
}
|
|
609
636
|
const parts = [];
|
|
610
637
|
let bytes = 0;
|
|
611
638
|
const write = (chunk) => {
|
|
612
639
|
bytes += Buffer.byteLength(chunk);
|
|
613
|
-
if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
|
|
640
|
+
if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
|
|
614
641
|
parts.push(chunk);
|
|
615
642
|
};
|
|
616
643
|
const stack = [];
|
|
617
644
|
const open = /* @__PURE__ */ new Set();
|
|
618
645
|
const refuse = (segment, problem) => {
|
|
619
|
-
const
|
|
620
|
-
const shown =
|
|
621
|
-
return new NikaConfigurationError(
|
|
646
|
+
const path4 = stack.map((frame) => frame.segment).join("") + segment;
|
|
647
|
+
const shown = path4.length > PATH_LIMIT ? `${path4.slice(0, PATH_LIMIT / 2)}\u2026${path4.slice(-PATH_LIMIT / 2)}` : path4;
|
|
648
|
+
return new NikaConfigurationError(`${label}: ${shown} ${problem}; ${STRICT_JSON}`);
|
|
622
649
|
};
|
|
623
650
|
const enter = (value, segment, kind) => {
|
|
624
651
|
open.add(value);
|
|
@@ -671,7 +698,7 @@ function encodeLiteralInputs(inputs) {
|
|
|
671
698
|
const value = member.value;
|
|
672
699
|
switch (typeof value) {
|
|
673
700
|
case "string":
|
|
674
|
-
if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
|
|
701
|
+
if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
|
|
675
702
|
if (LONE_SURROGATE.test(value)) {
|
|
676
703
|
throw refuse(segment, "contains a lone surrogate, which is not valid Unicode");
|
|
677
704
|
}
|
|
@@ -703,7 +730,17 @@ function encodeLiteralInputs(inputs) {
|
|
|
703
730
|
}
|
|
704
731
|
return { json: parts.join(""), bytes };
|
|
705
732
|
}
|
|
706
|
-
function tooLarge() {
|
|
733
|
+
function tooLarge(label) {
|
|
734
|
+
if (label === "compile({ change })") {
|
|
735
|
+
return new NikaConfigurationError(
|
|
736
|
+
`compile({ change }): the serialized literal exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB)`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (label === "compile({ answers })") {
|
|
740
|
+
return new NikaConfigurationError(
|
|
741
|
+
`compile({ answers }): the serialized answers exceed ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB); answers ride argv, one KEY=JSON element each, never a wire`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
707
744
|
return new NikaConfigurationError(
|
|
708
745
|
`run({ inputs }): the serialized inputs map exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB), the bound both transports share with the native engine`
|
|
709
746
|
);
|
|
@@ -760,6 +797,364 @@ function refusedObject(value) {
|
|
|
760
797
|
return typeof name === "string" && IDENTIFIER.test(name) && name.length <= 64 ? `is a ${name} instance, not a plain object or array` : custom;
|
|
761
798
|
}
|
|
762
799
|
|
|
800
|
+
// src/lib/compile.ts
|
|
801
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
802
|
+
import { tmpdir } from "os";
|
|
803
|
+
import path3 from "path";
|
|
804
|
+
import { types as utilTypes } from "util";
|
|
805
|
+
import { addAbortListener } from "events";
|
|
806
|
+
var COMPILE_CAPABILITY = "compile";
|
|
807
|
+
var COMPILE_WIRE_VERSION = 1;
|
|
808
|
+
var COMPILE_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
809
|
+
var COMPILE_KILL_GRACE_MS = 2e3;
|
|
810
|
+
var STATUSES = ["ready", "incomplete", "refused"];
|
|
811
|
+
var signalAborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted").get;
|
|
812
|
+
var removeSignalListener = EventTarget.prototype.removeEventListener;
|
|
813
|
+
var signalInterface = /* @__PURE__ */ new Set([
|
|
814
|
+
"aborted",
|
|
815
|
+
"reason",
|
|
816
|
+
"throwIfAborted",
|
|
817
|
+
"addEventListener",
|
|
818
|
+
"removeEventListener",
|
|
819
|
+
"dispatchEvent"
|
|
820
|
+
]);
|
|
821
|
+
function normalizeCompileRequest(input) {
|
|
822
|
+
if (typeof input === "string") {
|
|
823
|
+
if (input.length === 0) {
|
|
824
|
+
throw new NikaConfigurationError("compile: an intent string must not be empty");
|
|
825
|
+
}
|
|
826
|
+
return { intent: input };
|
|
827
|
+
}
|
|
828
|
+
const record2 = dataRecord(input, "request");
|
|
829
|
+
for (const key of Reflect.ownKeys(record2)) {
|
|
830
|
+
if (key !== "intent" && key !== "workflow" && key !== "change" && key !== "answers") {
|
|
831
|
+
throw new NikaConfigurationError(
|
|
832
|
+
`compile: unknown request field ${String(key)}; the request is refused rather than silently reinterpreted`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const answers = record2.answers;
|
|
837
|
+
if (answers !== void 0) encodeLiteralInputs(answers, "compile({ answers })");
|
|
838
|
+
const intent = record2.intent;
|
|
839
|
+
const workflow = record2.workflow;
|
|
840
|
+
const change = record2.change;
|
|
841
|
+
if (intent !== void 0) {
|
|
842
|
+
if (workflow !== void 0 || change !== void 0) {
|
|
843
|
+
throw new NikaConfigurationError(
|
|
844
|
+
"compile: intent (create) never mixes with workflow/change (edit) in one request"
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
if (typeof intent !== "string" || intent.length === 0) {
|
|
848
|
+
throw new NikaConfigurationError("compile: intent must be a non-empty string");
|
|
849
|
+
}
|
|
850
|
+
return answers === void 0 ? { intent } : { intent, answers };
|
|
851
|
+
}
|
|
852
|
+
if (workflow === void 0 && change === void 0) {
|
|
853
|
+
throw new NikaConfigurationError(
|
|
854
|
+
"compile: the request carries neither intent nor workflow/change; nothing would compile"
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (typeof workflow !== "string" || workflow.length === 0) {
|
|
858
|
+
throw new NikaConfigurationError(
|
|
859
|
+
"compile: an edit needs the accepted workflow source as a non-empty string"
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const normalizedChange = normalizeChange(change);
|
|
863
|
+
return answers === void 0 ? { workflow, change: normalizedChange } : { workflow, change: normalizedChange, answers };
|
|
864
|
+
}
|
|
865
|
+
function dataRecord(value, label) {
|
|
866
|
+
if (value === null || typeof value !== "object" || utilTypes.isProxy(value) || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
867
|
+
throw new NikaConfigurationError(`compile: ${label} must be ${label === "request" ? "an intent string or " : ""}a plain object with own data fields`);
|
|
868
|
+
}
|
|
869
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
870
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
871
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
872
|
+
const descriptor = descriptors[key];
|
|
873
|
+
if (typeof key !== "string" || !("value" in descriptor) || !descriptor.enumerable) {
|
|
874
|
+
throw new NikaConfigurationError(`compile: ${label} must contain only enumerable string data fields`);
|
|
875
|
+
}
|
|
876
|
+
result[key] = descriptor.value;
|
|
877
|
+
}
|
|
878
|
+
return result;
|
|
879
|
+
}
|
|
880
|
+
function normalizeChange(change) {
|
|
881
|
+
if (typeof change === "string" && change.length > 0) return change;
|
|
882
|
+
if (change === void 0 || change === "") {
|
|
883
|
+
throw new NikaConfigurationError("compile: an edit needs a non-empty change request");
|
|
884
|
+
}
|
|
885
|
+
const encoded = encodeLiteralInputs({ change }, "compile({ change })");
|
|
886
|
+
const object = machineObject(JSON.parse(encoded.json).change);
|
|
887
|
+
const constant = machineObject(object?.set_constant);
|
|
888
|
+
if (!object || Object.keys(object).length !== 1 || !constant || Object.keys(constant).length !== 2 || !("value" in constant) || typeof constant.name !== "string" || !/^[A-Za-z0-9_]+$/.test(constant.name)) {
|
|
889
|
+
throw new NikaConfigurationError(
|
|
890
|
+
"compile: an edit needs a non-empty change string or set_constant with a bare name and JSON value"
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
return object;
|
|
894
|
+
}
|
|
895
|
+
function normalizeCompileOptions(options) {
|
|
896
|
+
const record2 = dataRecord(options, "options");
|
|
897
|
+
for (const key of Reflect.ownKeys(record2)) {
|
|
898
|
+
if (key !== "signal" && key !== "timeoutMs") {
|
|
899
|
+
throw new NikaConfigurationError(`compile: unknown option ${String(key)}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const timeoutMs = record2.timeoutMs;
|
|
903
|
+
if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)) {
|
|
904
|
+
throw new NikaConfigurationError(
|
|
905
|
+
"compile: timeoutMs must be a positive safe integer between 1 and 2147483647 milliseconds"
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
const signal = record2.signal;
|
|
909
|
+
if (signal !== void 0) {
|
|
910
|
+
const invalidSignal = () => new NikaConfigurationError("compile: signal must be an AbortSignal");
|
|
911
|
+
if (signal === null || typeof signal !== "object" || utilTypes.isProxy(signal) || Object.getPrototypeOf(signal) !== AbortSignal.prototype) throw invalidSignal();
|
|
912
|
+
const descriptors = Object.getOwnPropertyDescriptors(signal);
|
|
913
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
914
|
+
if (typeof key === "string" && signalInterface.has(key) || !("value" in descriptors[key])) throw invalidSignal();
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
signalAborted.call(signal);
|
|
918
|
+
} catch {
|
|
919
|
+
throw invalidSignal();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return record2;
|
|
923
|
+
}
|
|
924
|
+
function compileSignal(options) {
|
|
925
|
+
const caller = options.signal;
|
|
926
|
+
let bridge;
|
|
927
|
+
let abort;
|
|
928
|
+
if (caller !== void 0) {
|
|
929
|
+
bridge = new AbortController();
|
|
930
|
+
const controller = bridge;
|
|
931
|
+
abort = () => controller.abort();
|
|
932
|
+
if (signalAborted.call(caller)) abort();
|
|
933
|
+
else addAbortListener(caller, abort);
|
|
934
|
+
}
|
|
935
|
+
let timeout;
|
|
936
|
+
if (options.timeoutMs !== void 0) timeout = AbortSignal.timeout(options.timeoutMs);
|
|
937
|
+
const signals = [bridge?.signal, timeout].filter((s) => s !== void 0);
|
|
938
|
+
return {
|
|
939
|
+
signal: signals.length === 0 ? void 0 : AbortSignal.any(signals),
|
|
940
|
+
timedOut: () => timeout?.aborted === true,
|
|
941
|
+
// Use the intrinsic even if the caller shadows removeEventListener after
|
|
942
|
+
// starting Compile. Do not retain a listener on a long-lived caller signal.
|
|
943
|
+
dispose: () => {
|
|
944
|
+
if (caller && abort) removeSignalListener.call(caller, "abort", abort);
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
async function compileArgv(request) {
|
|
949
|
+
const args = ["compile", "--json"];
|
|
950
|
+
for (const answer of compileAnswers(request.answers)) {
|
|
951
|
+
args.push(`--answer=${answer}`);
|
|
952
|
+
}
|
|
953
|
+
if (request.intent !== void 0) {
|
|
954
|
+
refuseNul(request.intent);
|
|
955
|
+
args.push("--", request.intent);
|
|
956
|
+
return { args };
|
|
957
|
+
}
|
|
958
|
+
const change = typeof request.change === "string" ? request.change : `Set const.${request.change.set_constant.name} to ${literalJson(request.change.set_constant.value)}`;
|
|
959
|
+
refuseNul(change);
|
|
960
|
+
const scratchDir = await mkdtemp(path3.join(tmpdir(), "nika-sdk-compile-"));
|
|
961
|
+
const base = path3.join(scratchDir, "base.nika");
|
|
962
|
+
try {
|
|
963
|
+
await writeFile(base, request.workflow, { encoding: "utf8", mode: 384 });
|
|
964
|
+
} catch (cause) {
|
|
965
|
+
await rm(scratchDir, { recursive: true, force: true });
|
|
966
|
+
throw new NikaTransportError("native-process", "compile could not write its temporary base source", {
|
|
967
|
+
cause: cause instanceof Error ? cause : void 0
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
args.push("--base", base, `--change=${change}`);
|
|
971
|
+
return { args, scratchDir };
|
|
972
|
+
}
|
|
973
|
+
async function removeCompileScratch(invocation) {
|
|
974
|
+
if (invocation.scratchDir !== void 0) {
|
|
975
|
+
await rm(invocation.scratchDir, { recursive: true, force: true });
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function compileAnswers(answers) {
|
|
979
|
+
if (answers === void 0) return [];
|
|
980
|
+
encodeLiteralInputs(answers, "compile({ answers })");
|
|
981
|
+
return Reflect.ownKeys(answers).map((key) => {
|
|
982
|
+
const name = String(key);
|
|
983
|
+
if (name.length === 0 || name.includes("=") || name.includes("\0")) {
|
|
984
|
+
throw new NikaConfigurationError(
|
|
985
|
+
"compile({ answers }): a question key must be non-empty and cannot carry '='; the engine splits KEY=JSON on its first equals sign"
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
return `${name}=${literalJson(answers[name])}`;
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function literalJson(value) {
|
|
992
|
+
return encodeLiteralInputs({ value }, "compile({ change })").json.slice('{"value":'.length, -1);
|
|
993
|
+
}
|
|
994
|
+
function refuseNul(value) {
|
|
995
|
+
if (value.includes("\0")) throw new NikaConfigurationError("compile: text cannot contain a NUL byte");
|
|
996
|
+
}
|
|
997
|
+
function compileBody(request) {
|
|
998
|
+
compileAnswers(request.answers);
|
|
999
|
+
const answers = request.answers === void 0 ? "" : `,"answers":${encodeLiteralInputs(request.answers, "compile({ answers })").json}`;
|
|
1000
|
+
if (request.intent !== void 0) {
|
|
1001
|
+
refuseNul(request.intent);
|
|
1002
|
+
return `{"compile_version":1,"mode":"create","intent":${JSON.stringify(request.intent)}${answers}}`;
|
|
1003
|
+
}
|
|
1004
|
+
if (typeof request.change === "string") refuseNul(request.change);
|
|
1005
|
+
const change = typeof request.change === "string" ? `{"text":${JSON.stringify(request.change)}}` : `{"set_constant":{"name":${JSON.stringify(request.change.set_constant.name)},"value":${literalJson(request.change.set_constant.value)}}}`;
|
|
1006
|
+
return `{"compile_version":1,"mode":"edit","source":${JSON.stringify(request.workflow)},"change":${change}${answers}}`;
|
|
1007
|
+
}
|
|
1008
|
+
function compileOutcomeFrom(captured, transport, engine) {
|
|
1009
|
+
const protocol = (message) => new NikaProtocolError(transport, `${message} (compile child at ${engine})`);
|
|
1010
|
+
if (captured.exitSignal !== null) {
|
|
1011
|
+
throw new NikaTransportError(
|
|
1012
|
+
transport,
|
|
1013
|
+
`compile child at ${engine} was ended externally by ${captured.exitSignal}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
const payload = machineObject(tryParseJson(captured.stdout));
|
|
1017
|
+
if (!payload) {
|
|
1018
|
+
const detail = excerpt(captured.stderr) || excerpt(captured.stdout);
|
|
1019
|
+
throw protocol(
|
|
1020
|
+
`the compile wire object is absent or malformed (exit ${captured.exitCode})` + (detail ? `: ${detail}` : "")
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
validateCompileVersion(payload.compile_version, transport, engine, protocol);
|
|
1024
|
+
const hasError = "error" in payload;
|
|
1025
|
+
const hasStatus = "status" in payload;
|
|
1026
|
+
if (hasError && hasStatus) {
|
|
1027
|
+
throw protocol("the payload carries both an outcome and an error");
|
|
1028
|
+
}
|
|
1029
|
+
if (hasError) {
|
|
1030
|
+
const error = machineObject(payload.error);
|
|
1031
|
+
if (!error || typeof error.code !== "string" || typeof error.message !== "string") {
|
|
1032
|
+
throw protocol("the error payload lacks its code/message strings");
|
|
1033
|
+
}
|
|
1034
|
+
if (captured.exitCode !== 2 && captured.exitCode !== 3) {
|
|
1035
|
+
throw protocol(
|
|
1036
|
+
`an engine-stamped error arrived with exit ${captured.exitCode}, not 2 or 3`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
throw new NikaOperationError("compile", transport, error.code, error.message, {
|
|
1040
|
+
status: captured.exitCode,
|
|
1041
|
+
machineCode: error.code
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
const status = payload.status;
|
|
1045
|
+
if (typeof status !== "string" || !STATUSES.includes(status)) {
|
|
1046
|
+
throw protocol(`unknown compile status ${JSON.stringify(status ?? null)}`);
|
|
1047
|
+
}
|
|
1048
|
+
if (status === "ready" ? captured.exitCode !== 0 : captured.exitCode !== 2) {
|
|
1049
|
+
throw protocol(
|
|
1050
|
+
`status ${JSON.stringify(status)} contradicts exit ${captured.exitCode}`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
writtenFrom(payload.written, protocol);
|
|
1054
|
+
return compilePayloadFrom(payload, transport, engine);
|
|
1055
|
+
}
|
|
1056
|
+
function compilePayloadFrom(payload, transport, engine) {
|
|
1057
|
+
const protocol = (message) => new NikaProtocolError(transport, `compile at ${engine}: ${message}`);
|
|
1058
|
+
validateCompileVersion(payload.compile_version, transport, engine, protocol);
|
|
1059
|
+
if ("error" in payload) throw protocol("the payload carries an error instead of an outcome");
|
|
1060
|
+
if (transport === "http" && "written" in payload) throw protocol("HTTP outcome claims a written destination");
|
|
1061
|
+
const status = payload.status;
|
|
1062
|
+
if (typeof status !== "string" || !STATUSES.includes(status)) {
|
|
1063
|
+
throw protocol("unknown compile status");
|
|
1064
|
+
}
|
|
1065
|
+
const candidate = payload.candidate;
|
|
1066
|
+
if (candidate !== null && typeof candidate !== "string") {
|
|
1067
|
+
throw protocol("candidate is neither source text nor null");
|
|
1068
|
+
}
|
|
1069
|
+
if (status === "ready" && candidate === null) {
|
|
1070
|
+
throw protocol("a ready outcome carries no candidate");
|
|
1071
|
+
}
|
|
1072
|
+
const outcome = {
|
|
1073
|
+
compile_version: COMPILE_WIRE_VERSION,
|
|
1074
|
+
status,
|
|
1075
|
+
ready: status === "ready",
|
|
1076
|
+
candidate,
|
|
1077
|
+
questions: questionsFrom(payload.questions, protocol),
|
|
1078
|
+
diagnostics: diagnosticsFrom(payload.diagnostics, protocol),
|
|
1079
|
+
requested_boundary: nullableObject(payload.requested_boundary, "requested_boundary", protocol),
|
|
1080
|
+
check_preview: previewFrom(payload.check_preview, protocol),
|
|
1081
|
+
provenance: provenanceFrom(payload.provenance, protocol)
|
|
1082
|
+
};
|
|
1083
|
+
return outcome;
|
|
1084
|
+
}
|
|
1085
|
+
function validateCompileVersion(version, transport, engine, protocol) {
|
|
1086
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) {
|
|
1087
|
+
throw protocol("compile_version must be a positive safe integer");
|
|
1088
|
+
}
|
|
1089
|
+
if (version !== COMPILE_WIRE_VERSION) {
|
|
1090
|
+
throw new NikaCompatibilityError(
|
|
1091
|
+
COMPILE_CAPABILITY,
|
|
1092
|
+
transport,
|
|
1093
|
+
`Engine at ${engine} speaks compile wire ${version}; expected ${COMPILE_WIRE_VERSION}`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function tryParseJson(text) {
|
|
1098
|
+
const trimmed = text.trim();
|
|
1099
|
+
if (!trimmed) return void 0;
|
|
1100
|
+
try {
|
|
1101
|
+
return JSON.parse(trimmed);
|
|
1102
|
+
} catch {
|
|
1103
|
+
return void 0;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function excerpt(text) {
|
|
1107
|
+
const flat = text.trim().replace(/\s+/g, " ");
|
|
1108
|
+
return flat.length <= 240 ? flat : `${flat.slice(0, 240)}\u2026`;
|
|
1109
|
+
}
|
|
1110
|
+
function nullableObject(value, field, protocol) {
|
|
1111
|
+
if (value === null) return null;
|
|
1112
|
+
const object = machineObject(value);
|
|
1113
|
+
if (!object) throw protocol(`${field} is neither an object nor null`);
|
|
1114
|
+
return object;
|
|
1115
|
+
}
|
|
1116
|
+
function questionsFrom(value, protocol) {
|
|
1117
|
+
if (!Array.isArray(value)) throw protocol("questions is not an array");
|
|
1118
|
+
return value.map((entry) => {
|
|
1119
|
+
const question = machineObject(entry);
|
|
1120
|
+
if (!question || typeof question.key !== "string" || typeof question.label !== "string" || question.type !== "text" && question.type !== "literal" || typeof question.why !== "string" || typeof question.mandatory !== "boolean") {
|
|
1121
|
+
throw protocol("a question lacks its key/label/type/why/mandatory shape");
|
|
1122
|
+
}
|
|
1123
|
+
return question;
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
function diagnosticsFrom(value, protocol) {
|
|
1127
|
+
if (!Array.isArray(value)) throw protocol("diagnostics is not an array");
|
|
1128
|
+
return value.map((entry) => {
|
|
1129
|
+
const diagnostic = machineObject(entry);
|
|
1130
|
+
if (!diagnostic || typeof diagnostic.kind !== "string" || !["applied", "missed", "unknown", "requiresHuman", "refused"].includes(diagnostic.kind) || typeof diagnostic.target !== "string" || typeof diagnostic.message !== "string") {
|
|
1131
|
+
throw protocol("a diagnostic lacks its kind/target/message shape");
|
|
1132
|
+
}
|
|
1133
|
+
return diagnostic;
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
function previewFrom(value, protocol) {
|
|
1137
|
+
if (value === null) return null;
|
|
1138
|
+
const preview = machineObject(value);
|
|
1139
|
+
if (!preview || preview.scope !== "sourceOnly" || !machineObject(preview.report)) {
|
|
1140
|
+
throw protocol("check_preview lacks its scope/report shape");
|
|
1141
|
+
}
|
|
1142
|
+
return { ...preview, scope: preview.scope, report: preview.report };
|
|
1143
|
+
}
|
|
1144
|
+
function provenanceFrom(value, protocol) {
|
|
1145
|
+
const provenance = machineObject(value);
|
|
1146
|
+
if (!provenance || typeof provenance.compiler_version !== "string" || typeof provenance.spec_pin !== "string" || provenance.skeleton !== null && typeof provenance.skeleton !== "string" || provenance.cognition !== "deterministicOnly") {
|
|
1147
|
+
throw protocol("provenance lacks its compiler_version/spec_pin/skeleton/cognition shape");
|
|
1148
|
+
}
|
|
1149
|
+
return provenance;
|
|
1150
|
+
}
|
|
1151
|
+
function writtenFrom(value, protocol) {
|
|
1152
|
+
if (value !== null) {
|
|
1153
|
+
throw protocol("the outcome claims a written destination the SDK never asked for");
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
763
1158
|
// src/lib/sse/parser.ts
|
|
764
1159
|
var SseParseError = class extends Error {
|
|
765
1160
|
constructor(message, options) {
|
|
@@ -921,8 +1316,8 @@ async function* decodeSse(stream, limits, signal) {
|
|
|
921
1316
|
var WORKFLOW_SUFFIX = ".nika";
|
|
922
1317
|
var LEGACY_WORKFLOW_SUFFIXES = [".nika.yaml", ".nika.yml"];
|
|
923
1318
|
var CONTAINED_CHAR = /[A-Za-z0-9/._-]/;
|
|
924
|
-
function workflowBasename(
|
|
925
|
-
const parts =
|
|
1319
|
+
function workflowBasename(path4) {
|
|
1320
|
+
const parts = path4.split(/[\\/]/);
|
|
926
1321
|
return parts[parts.length - 1] ?? "";
|
|
927
1322
|
}
|
|
928
1323
|
function hasControlChar(value) {
|
|
@@ -934,14 +1329,14 @@ function hasControlChar(value) {
|
|
|
934
1329
|
}
|
|
935
1330
|
return false;
|
|
936
1331
|
}
|
|
937
|
-
function isLegacyWorkflowPath(
|
|
938
|
-
if (typeof
|
|
1332
|
+
function isLegacyWorkflowPath(path4) {
|
|
1333
|
+
if (typeof path4 !== "string" || path4.length === 0) {
|
|
939
1334
|
return false;
|
|
940
1335
|
}
|
|
941
|
-
if (
|
|
1336
|
+
if (path4.endsWith("/") || path4.endsWith("\\")) {
|
|
942
1337
|
return false;
|
|
943
1338
|
}
|
|
944
|
-
const base = workflowBasename(
|
|
1339
|
+
const base = workflowBasename(path4);
|
|
945
1340
|
return LEGACY_WORKFLOW_SUFFIXES.some((suffix) => base.endsWith(suffix));
|
|
946
1341
|
}
|
|
947
1342
|
function isCanonicalWorkflowFilename(basename) {
|
|
@@ -962,17 +1357,17 @@ function isCanonicalWorkflowFilename(basename) {
|
|
|
962
1357
|
}
|
|
963
1358
|
return basename.length > WORKFLOW_SUFFIX.length;
|
|
964
1359
|
}
|
|
965
|
-
function isCanonicalWorkflowPath(
|
|
966
|
-
if (typeof
|
|
1360
|
+
function isCanonicalWorkflowPath(path4) {
|
|
1361
|
+
if (typeof path4 !== "string" || path4.length === 0) {
|
|
967
1362
|
return false;
|
|
968
1363
|
}
|
|
969
|
-
if (hasControlChar(
|
|
1364
|
+
if (hasControlChar(path4)) {
|
|
970
1365
|
return false;
|
|
971
1366
|
}
|
|
972
|
-
if (
|
|
1367
|
+
if (path4.endsWith("/") || path4.endsWith("\\")) {
|
|
973
1368
|
return false;
|
|
974
1369
|
}
|
|
975
|
-
return isCanonicalWorkflowFilename(workflowBasename(
|
|
1370
|
+
return isCanonicalWorkflowFilename(workflowBasename(path4));
|
|
976
1371
|
}
|
|
977
1372
|
function containedRelativeShape(value) {
|
|
978
1373
|
if (value.includes("\\") || value.startsWith("/")) {
|
|
@@ -1063,6 +1458,69 @@ var HttpTransport = class {
|
|
|
1063
1458
|
}
|
|
1064
1459
|
return captured.report;
|
|
1065
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Stateless authoring through the authenticated Serve compile door.
|
|
1463
|
+
* A remote connection never falls back to a local compile — the candidate
|
|
1464
|
+
* must come from the server the caller connected to, or not exist.
|
|
1465
|
+
*/
|
|
1466
|
+
async compile(request, options) {
|
|
1467
|
+
const body = compileBody(request);
|
|
1468
|
+
const timeoutMs = options.timeoutMs ?? this.options.requestTimeout;
|
|
1469
|
+
const composed = compileSignal({ ...options, timeoutMs });
|
|
1470
|
+
const signal = composed.signal;
|
|
1471
|
+
try {
|
|
1472
|
+
if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
|
|
1473
|
+
const identity = this.remoteIdentity ?? await this.probeServerIdentity(signal);
|
|
1474
|
+
if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
|
|
1475
|
+
throw this.gap(
|
|
1476
|
+
COMPILE_CAPABILITY,
|
|
1477
|
+
"The connected engine does not advertise compile; the SDK never compiles locally as a substitute"
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
const path4 = "/v1/compile";
|
|
1481
|
+
const response = await this.fetchResponse(path4, {
|
|
1482
|
+
method: "POST",
|
|
1483
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1484
|
+
body,
|
|
1485
|
+
signal
|
|
1486
|
+
}, false, true, false);
|
|
1487
|
+
if (response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
|
|
1488
|
+
await discardResponse(response);
|
|
1489
|
+
throw new NikaProtocolError(this.kind, "HTTP compile returned an invalid content-type");
|
|
1490
|
+
}
|
|
1491
|
+
const object = await this.readObservationObject(
|
|
1492
|
+
response,
|
|
1493
|
+
path4,
|
|
1494
|
+
signal,
|
|
1495
|
+
Math.min(this.options.machineBufferBytes, COMPILE_RESPONSE_MAX_BYTES),
|
|
1496
|
+
false
|
|
1497
|
+
);
|
|
1498
|
+
if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
|
|
1499
|
+
if (response.status !== 200) {
|
|
1500
|
+
const error = machineObject(object.error);
|
|
1501
|
+
if (response.ok || !error || typeof error.code !== "string" || !/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/.test(error.code) || error.code.includes(this.options.token) || typeof error.message !== "string" || Object.keys(object).some((key) => key !== "error")) {
|
|
1502
|
+
throw new NikaProtocolError(this.kind, "HTTP compile returned a malformed refusal or non-contract status");
|
|
1503
|
+
}
|
|
1504
|
+
throw this.refused(path4, {
|
|
1505
|
+
operation: "compile",
|
|
1506
|
+
status: response.status,
|
|
1507
|
+
refusal: { code: error.code, message: this.redact(error.message) }
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
return compilePayloadFrom(object, this.kind, this.options.url);
|
|
1511
|
+
} catch (cause) {
|
|
1512
|
+
if (signal.aborted) {
|
|
1513
|
+
throw new NikaTransportError(
|
|
1514
|
+
this.kind,
|
|
1515
|
+
composed.timedOut() ? `compile timed out after ${timeoutMs} ms` : "compile aborted by caller",
|
|
1516
|
+
{ cause: cause instanceof Error ? cause : void 0 }
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
throw cause;
|
|
1520
|
+
} finally {
|
|
1521
|
+
composed.dispose();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1066
1524
|
async startRun(workflow, options) {
|
|
1067
1525
|
const inputs = literalInputs(options);
|
|
1068
1526
|
if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
|
|
@@ -1182,14 +1640,14 @@ var HttpTransport = class {
|
|
|
1182
1640
|
async schedule(workflow, options) {
|
|
1183
1641
|
refuseLegacyContainedName(workflow);
|
|
1184
1642
|
await this.ensureScheduleCapability();
|
|
1185
|
-
const
|
|
1643
|
+
const path4 = `/v1/schedules/${encodeURIComponent(options.id)}`;
|
|
1186
1644
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1187
1645
|
if (options.revision === void 0) {
|
|
1188
1646
|
headers.set("If-None-Match", "*");
|
|
1189
1647
|
} else {
|
|
1190
1648
|
headers.set("If-Match", `"${options.revision}"`);
|
|
1191
1649
|
}
|
|
1192
|
-
const object = await this.operationJson("schedule",
|
|
1650
|
+
const object = await this.operationJson("schedule", path4, {
|
|
1193
1651
|
method: "PUT",
|
|
1194
1652
|
headers,
|
|
1195
1653
|
body: JSON.stringify(scheduleBody(workflow, options))
|
|
@@ -1201,8 +1659,8 @@ var HttpTransport = class {
|
|
|
1201
1659
|
}
|
|
1202
1660
|
async scheduleStatus(id) {
|
|
1203
1661
|
await this.ensureScheduleCapability();
|
|
1204
|
-
const
|
|
1205
|
-
const object = await this.operationJson("scheduleStatus",
|
|
1662
|
+
const path4 = `/v1/schedules/${encodeURIComponent(id)}`;
|
|
1663
|
+
const object = await this.operationJson("scheduleStatus", path4, {
|
|
1206
1664
|
method: "GET",
|
|
1207
1665
|
headers: { Accept: "application/json" }
|
|
1208
1666
|
});
|
|
@@ -1535,16 +1993,16 @@ var HttpTransport = class {
|
|
|
1535
1993
|
}
|
|
1536
1994
|
return event;
|
|
1537
1995
|
}
|
|
1538
|
-
async inspectDurableJob(
|
|
1996
|
+
async inspectDurableJob(path4, expectedId, signal) {
|
|
1539
1997
|
let response;
|
|
1540
1998
|
try {
|
|
1541
|
-
response = await this.observationRequest(
|
|
1999
|
+
response = await this.observationRequest(path4, { headers: { Accept: "application/json" }, signal });
|
|
1542
2000
|
} catch {
|
|
1543
2001
|
return { retry: true };
|
|
1544
2002
|
}
|
|
1545
2003
|
if (response.status === 404) {
|
|
1546
2004
|
await discardResponse(response);
|
|
1547
|
-
throw new NikaTransportError(this.kind, `HTTP 404 for ${
|
|
2005
|
+
throw new NikaTransportError(this.kind, `HTTP 404 for ${path4}; job was durably absent`);
|
|
1548
2006
|
}
|
|
1549
2007
|
if (isRetryableStatus(response.status)) {
|
|
1550
2008
|
const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
|
|
@@ -1554,9 +2012,9 @@ var HttpTransport = class {
|
|
|
1554
2012
|
...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
|
|
1555
2013
|
};
|
|
1556
2014
|
}
|
|
1557
|
-
this.assertObservationResponse(response,
|
|
2015
|
+
this.assertObservationResponse(response, path4, "application/json");
|
|
1558
2016
|
try {
|
|
1559
|
-
const object = await this.readObservationObject(response,
|
|
2017
|
+
const object = await this.readObservationObject(response, path4);
|
|
1560
2018
|
return durableJob(object, expectedId, this.kind);
|
|
1561
2019
|
} catch (cause) {
|
|
1562
2020
|
if (cause instanceof NikaTransportError && !(cause instanceof NikaProtocolError)) {
|
|
@@ -1565,31 +2023,31 @@ var HttpTransport = class {
|
|
|
1565
2023
|
throw cause;
|
|
1566
2024
|
}
|
|
1567
2025
|
}
|
|
1568
|
-
assertObservationResponse(response,
|
|
2026
|
+
assertObservationResponse(response, path4, contentType) {
|
|
1569
2027
|
if (response.status === 204) {
|
|
1570
2028
|
void discardResponse(response);
|
|
1571
|
-
throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${
|
|
2029
|
+
throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path4}`);
|
|
1572
2030
|
}
|
|
1573
2031
|
if (response.status === 401 || response.status === 403) {
|
|
1574
2032
|
void discardResponse(response);
|
|
1575
|
-
throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${
|
|
2033
|
+
throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path4}`);
|
|
1576
2034
|
}
|
|
1577
2035
|
if (response.status !== 200) {
|
|
1578
2036
|
void discardResponse(response);
|
|
1579
|
-
throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${
|
|
2037
|
+
throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path4}`);
|
|
1580
2038
|
}
|
|
1581
2039
|
const actual = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1582
2040
|
if (actual !== contentType) {
|
|
1583
2041
|
void discardResponse(response);
|
|
1584
2042
|
throw new NikaProtocolError(
|
|
1585
2043
|
this.kind,
|
|
1586
|
-
`HTTP ${
|
|
2044
|
+
`HTTP ${path4} returned an invalid content-type`
|
|
1587
2045
|
);
|
|
1588
2046
|
}
|
|
1589
2047
|
}
|
|
1590
|
-
async readObservationObject(response,
|
|
2048
|
+
async readObservationObject(response, path4, signal, maxBytes = this.options.machineBufferBytes, useRequestTimeout = true) {
|
|
1591
2049
|
if (!response.body) {
|
|
1592
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2050
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} omitted its JSON body`);
|
|
1593
2051
|
}
|
|
1594
2052
|
const reader = response.body.getReader();
|
|
1595
2053
|
const chunks = [];
|
|
@@ -1599,38 +2057,38 @@ var HttpTransport = class {
|
|
|
1599
2057
|
rejectBoundary = reject;
|
|
1600
2058
|
});
|
|
1601
2059
|
const abort = () => {
|
|
1602
|
-
rejectBoundary(new NikaTransportError(this.kind, `HTTP ${
|
|
2060
|
+
rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path4} response body was aborted`));
|
|
1603
2061
|
void reader.cancel().catch(() => {
|
|
1604
2062
|
});
|
|
1605
2063
|
};
|
|
1606
2064
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1607
|
-
const timer = setTimeout(() => {
|
|
2065
|
+
const timer = useRequestTimeout ? setTimeout(() => {
|
|
1608
2066
|
rejectBoundary(new NikaTransportError(
|
|
1609
2067
|
this.kind,
|
|
1610
2068
|
`HTTP response body timed out after ${this.options.requestTimeout}ms`
|
|
1611
2069
|
));
|
|
1612
2070
|
void reader.cancel().catch(() => {
|
|
1613
2071
|
});
|
|
1614
|
-
}, this.options.requestTimeout);
|
|
2072
|
+
}, this.options.requestTimeout) : void 0;
|
|
1615
2073
|
try {
|
|
1616
2074
|
if (signal?.aborted) abort();
|
|
1617
2075
|
while (true) {
|
|
1618
2076
|
const { done, value: value2 } = await Promise.race([reader.read(), boundary]);
|
|
1619
2077
|
if (done) break;
|
|
1620
2078
|
bytes += value2.byteLength;
|
|
1621
|
-
if (bytes >
|
|
2079
|
+
if (bytes > maxBytes) {
|
|
1622
2080
|
throw new NikaProtocolError(
|
|
1623
2081
|
this.kind,
|
|
1624
|
-
`HTTP ${
|
|
2082
|
+
`HTTP ${path4} JSON exceeded ${maxBytes} bytes`
|
|
1625
2083
|
);
|
|
1626
2084
|
}
|
|
1627
2085
|
chunks.push(value2);
|
|
1628
2086
|
}
|
|
1629
2087
|
} catch (cause) {
|
|
1630
2088
|
if (cause instanceof NikaProtocolError) throw cause;
|
|
1631
|
-
throw new NikaTransportError(this.kind, `HTTP ${
|
|
2089
|
+
throw new NikaTransportError(this.kind, `HTTP ${path4} response body reset`);
|
|
1632
2090
|
} finally {
|
|
1633
|
-
clearTimeout(timer);
|
|
2091
|
+
if (timer) clearTimeout(timer);
|
|
1634
2092
|
signal?.removeEventListener("abort", abort);
|
|
1635
2093
|
await reader.cancel().catch(() => {
|
|
1636
2094
|
});
|
|
@@ -1646,7 +2104,7 @@ var HttpTransport = class {
|
|
|
1646
2104
|
try {
|
|
1647
2105
|
text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
1648
2106
|
} catch (cause) {
|
|
1649
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2107
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} JSON was not valid UTF-8`, {
|
|
1650
2108
|
cause: cause instanceof Error ? cause : void 0
|
|
1651
2109
|
});
|
|
1652
2110
|
}
|
|
@@ -1654,15 +2112,15 @@ var HttpTransport = class {
|
|
|
1654
2112
|
try {
|
|
1655
2113
|
value = JSON.parse(text);
|
|
1656
2114
|
} catch (cause) {
|
|
1657
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2115
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return valid JSON`, {
|
|
1658
2116
|
cause: cause instanceof Error ? cause : void 0
|
|
1659
2117
|
});
|
|
1660
2118
|
}
|
|
1661
2119
|
const object = machineObject(value);
|
|
1662
|
-
if (!object) throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2120
|
+
if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return an object`);
|
|
1663
2121
|
return object;
|
|
1664
2122
|
}
|
|
1665
|
-
async observationRequest(
|
|
2123
|
+
async observationRequest(path4, init, withTimeout = true) {
|
|
1666
2124
|
const controller = withTimeout ? new AbortController() : void 0;
|
|
1667
2125
|
const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
|
|
1668
2126
|
const callerSignal = init.signal;
|
|
@@ -1671,7 +2129,7 @@ var HttpTransport = class {
|
|
|
1671
2129
|
const headers = new Headers(init.headers);
|
|
1672
2130
|
headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
1673
2131
|
try {
|
|
1674
|
-
return await this.options.fetch(`${this.options.url}${
|
|
2132
|
+
return await this.options.fetch(`${this.options.url}${path4}`, {
|
|
1675
2133
|
...init,
|
|
1676
2134
|
headers,
|
|
1677
2135
|
signal: controller?.signal ?? callerSignal
|
|
@@ -1730,8 +2188,18 @@ var HttpTransport = class {
|
|
|
1730
2188
|
this.serverIdentity ??= this.probeServerIdentity();
|
|
1731
2189
|
return this.serverIdentity;
|
|
1732
2190
|
}
|
|
1733
|
-
async probeServerIdentity() {
|
|
1734
|
-
const
|
|
2191
|
+
async probeServerIdentity(signal) {
|
|
2192
|
+
const outcome = await this.jsonOutcome(
|
|
2193
|
+
"/health",
|
|
2194
|
+
{ method: "GET", signal },
|
|
2195
|
+
false,
|
|
2196
|
+
[200],
|
|
2197
|
+
void 0,
|
|
2198
|
+
false,
|
|
2199
|
+
signal === void 0
|
|
2200
|
+
);
|
|
2201
|
+
if ("refusal" in outcome) throw this.refused("/health", outcome);
|
|
2202
|
+
const health = outcome.object;
|
|
1735
2203
|
if (health.status !== "ok" || health.service !== "nika-serve") {
|
|
1736
2204
|
throw new NikaCompatibilityError(
|
|
1737
2205
|
"engineIdentity",
|
|
@@ -1869,9 +2337,9 @@ var HttpTransport = class {
|
|
|
1869
2337
|
delete report.execution_snapshot;
|
|
1870
2338
|
return report;
|
|
1871
2339
|
}
|
|
1872
|
-
async json(
|
|
2340
|
+
async json(path4, init, authenticated = true, acceptedStatuses = [200], operation) {
|
|
1873
2341
|
const { object } = await this.jsonWithStatus(
|
|
1874
|
-
|
|
2342
|
+
path4,
|
|
1875
2343
|
init,
|
|
1876
2344
|
authenticated,
|
|
1877
2345
|
acceptedStatuses,
|
|
@@ -1884,48 +2352,54 @@ var HttpTransport = class {
|
|
|
1884
2352
|
* routes whose accepted statuses mean different things (cancel: 200 is a
|
|
1885
2353
|
* settled job, 202 an accepted request).
|
|
1886
2354
|
*/
|
|
1887
|
-
async jsonWithStatus(
|
|
1888
|
-
const outcome = await this.jsonOutcome(
|
|
1889
|
-
if ("refusal" in outcome) throw this.refused(
|
|
2355
|
+
async jsonWithStatus(path4, init, authenticated, acceptedStatuses, operation) {
|
|
2356
|
+
const outcome = await this.jsonOutcome(path4, init, authenticated, acceptedStatuses, operation);
|
|
2357
|
+
if ("refusal" in outcome) throw this.refused(path4, outcome);
|
|
1890
2358
|
return outcome;
|
|
1891
2359
|
}
|
|
1892
|
-
async jsonOutcome(
|
|
1893
|
-
const response = await this.fetchResponse(
|
|
2360
|
+
async jsonOutcome(path4, init, authenticated, acceptedStatuses, operation, strictRefusal = false, useRequestTimeout = true) {
|
|
2361
|
+
const response = await this.fetchResponse(path4, init, useRequestTimeout, authenticated, false);
|
|
1894
2362
|
if (!acceptedStatuses.includes(response.status)) {
|
|
1895
2363
|
if (response.ok) {
|
|
1896
2364
|
await discardResponse(response);
|
|
1897
2365
|
throw new NikaProtocolError(
|
|
1898
2366
|
this.kind,
|
|
1899
|
-
`HTTP ${
|
|
2367
|
+
`HTTP ${path4} returned non-contract status ${response.status}`
|
|
1900
2368
|
);
|
|
1901
2369
|
}
|
|
1902
|
-
const refusal = operation === void 0 ? void 0 : await this.readRefusal(response,
|
|
2370
|
+
const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path4, strictRefusal);
|
|
1903
2371
|
if (operation !== void 0 && refusal) {
|
|
1904
2372
|
return { operation, status: response.status, refusal };
|
|
1905
2373
|
}
|
|
1906
2374
|
await discardResponse(response);
|
|
1907
2375
|
throw new NikaTransportError(
|
|
1908
2376
|
this.kind,
|
|
1909
|
-
`HTTP ${response.status} for ${
|
|
2377
|
+
`HTTP ${response.status} for ${path4}: [REDACTED]`
|
|
1910
2378
|
);
|
|
1911
2379
|
}
|
|
1912
2380
|
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1913
2381
|
if (contentType !== "application/json") {
|
|
1914
2382
|
await discardResponse(response);
|
|
1915
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2383
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
|
|
1916
2384
|
}
|
|
1917
2385
|
return {
|
|
1918
|
-
object: await this.readObservationObject(
|
|
2386
|
+
object: await this.readObservationObject(
|
|
2387
|
+
response,
|
|
2388
|
+
path4,
|
|
2389
|
+
init.signal ?? void 0,
|
|
2390
|
+
this.options.machineBufferBytes,
|
|
2391
|
+
useRequestTimeout
|
|
2392
|
+
),
|
|
1919
2393
|
status: response.status
|
|
1920
2394
|
};
|
|
1921
2395
|
}
|
|
1922
|
-
refused(
|
|
2396
|
+
refused(path4, outcome) {
|
|
1923
2397
|
const { code, message } = outcome.refusal;
|
|
1924
2398
|
return new NikaOperationError(
|
|
1925
2399
|
outcome.operation,
|
|
1926
2400
|
this.kind,
|
|
1927
2401
|
code,
|
|
1928
|
-
`HTTP ${outcome.status} for ${
|
|
2402
|
+
`HTTP ${outcome.status} for ${path4}: ${code}` + (message ? ` (${message})` : ""),
|
|
1929
2403
|
{ status: outcome.status, machineCode: code }
|
|
1930
2404
|
);
|
|
1931
2405
|
}
|
|
@@ -1935,7 +2409,7 @@ var HttpTransport = class {
|
|
|
1935
2409
|
* plain identifier, a code that carries the bearer token) is discarded and
|
|
1936
2410
|
* the caller falls back to the redacted transport error.
|
|
1937
2411
|
*/
|
|
1938
|
-
async readRefusal(response,
|
|
2412
|
+
async readRefusal(response, path4, strict = false) {
|
|
1939
2413
|
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1940
2414
|
if (contentType !== "application/json") {
|
|
1941
2415
|
await discardResponse(response);
|
|
@@ -1943,7 +2417,7 @@ var HttpTransport = class {
|
|
|
1943
2417
|
}
|
|
1944
2418
|
let object;
|
|
1945
2419
|
try {
|
|
1946
|
-
object = await this.readObservationObject(response,
|
|
2420
|
+
object = await this.readObservationObject(response, path4);
|
|
1947
2421
|
} catch {
|
|
1948
2422
|
return void 0;
|
|
1949
2423
|
}
|
|
@@ -1961,14 +2435,14 @@ var HttpTransport = class {
|
|
|
1961
2435
|
const clean = text.replace(/[\u0000-\u001f\u007f]+/g, " ").split(this.options.token).join("[REDACTED]");
|
|
1962
2436
|
return clean.length > 240 ? `${clean.slice(0, 240)}...` : clean;
|
|
1963
2437
|
}
|
|
1964
|
-
async operationJson(operation,
|
|
1965
|
-
const response = await this.fetchResponse(
|
|
2438
|
+
async operationJson(operation, path4, init) {
|
|
2439
|
+
const response = await this.fetchResponse(path4, init, true, true, false);
|
|
1966
2440
|
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1967
2441
|
if (contentType !== "application/json") {
|
|
1968
2442
|
await discardResponse(response);
|
|
1969
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2443
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
|
|
1970
2444
|
}
|
|
1971
|
-
const object = await this.readObservationObject(response,
|
|
2445
|
+
const object = await this.readObservationObject(response, path4);
|
|
1972
2446
|
if (!response.ok) throw this.operationFailure(operation, response.status, object);
|
|
1973
2447
|
return object;
|
|
1974
2448
|
}
|
|
@@ -2013,16 +2487,17 @@ var HttpTransport = class {
|
|
|
2013
2487
|
{ status, machineCode: machine.code }
|
|
2014
2488
|
);
|
|
2015
2489
|
}
|
|
2016
|
-
async fetchResponse(
|
|
2490
|
+
async fetchResponse(path4, init, withTimeout, authenticated = true, requireOk = true) {
|
|
2017
2491
|
const controller = withTimeout ? new AbortController() : void 0;
|
|
2018
2492
|
const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
|
|
2019
2493
|
const callerSignal = init.signal;
|
|
2020
2494
|
const abort = () => controller?.abort(callerSignal?.reason);
|
|
2021
2495
|
callerSignal?.addEventListener("abort", abort, { once: true });
|
|
2496
|
+
if (callerSignal?.aborted) abort();
|
|
2022
2497
|
const headers = new Headers(init.headers);
|
|
2023
2498
|
if (authenticated) headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
2024
2499
|
try {
|
|
2025
|
-
const response = await this.options.fetch(`${this.options.url}${
|
|
2500
|
+
const response = await this.options.fetch(`${this.options.url}${path4}`, {
|
|
2026
2501
|
...init,
|
|
2027
2502
|
headers,
|
|
2028
2503
|
signal: controller?.signal ?? callerSignal
|
|
@@ -2031,7 +2506,7 @@ var HttpTransport = class {
|
|
|
2031
2506
|
await discardResponse(response);
|
|
2032
2507
|
throw new NikaTransportError(
|
|
2033
2508
|
this.kind,
|
|
2034
|
-
`HTTP ${response.status} for ${
|
|
2509
|
+
`HTTP ${response.status} for ${path4}: [REDACTED]`
|
|
2035
2510
|
);
|
|
2036
2511
|
}
|
|
2037
2512
|
return response;
|
|
@@ -2477,15 +2952,65 @@ var NativeProcessTransport = class {
|
|
|
2477
2952
|
const captured = await this.capture(args, options.signal);
|
|
2478
2953
|
const report = reportObject(captured.stdout) ?? reportObject(stripEnginePrefix(captured.stderr));
|
|
2479
2954
|
if (!report) {
|
|
2480
|
-
const
|
|
2955
|
+
const excerpt2 = diagnosticExcerpt(captured.stderr);
|
|
2481
2956
|
throw new NikaCompatibilityError(
|
|
2482
2957
|
"check",
|
|
2483
2958
|
this.kind,
|
|
2484
|
-
`This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (
|
|
2959
|
+
`This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt2 ? `: ${excerpt2}` : "")
|
|
2485
2960
|
);
|
|
2486
2961
|
}
|
|
2487
2962
|
return { ...report, exitCode: captured.exitCode };
|
|
2488
2963
|
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Authoring, not execution (issue #128): one bounded `nika compile --json`
|
|
2966
|
+
* invocation, projected verbatim. The engine must advertise the compile
|
|
2967
|
+
* capability — an engine from before the door is refused here, before any
|
|
2968
|
+
* spawn, and there is never a TypeScript fallback. No destination is passed:
|
|
2969
|
+
* the candidate stays source in memory, nothing is written, and no Run
|
|
2970
|
+
* exists; `signal`/`timeoutMs` only stop this one child.
|
|
2971
|
+
*/
|
|
2972
|
+
async compile(request, options) {
|
|
2973
|
+
const composed = compileSignal(options);
|
|
2974
|
+
let invocation;
|
|
2975
|
+
try {
|
|
2976
|
+
if (composed.signal?.aborted) {
|
|
2977
|
+
throw new NikaTransportError(this.kind, "compile aborted");
|
|
2978
|
+
}
|
|
2979
|
+
invocation = await compileArgv(request);
|
|
2980
|
+
const identity = composed.signal ? await verifyNikaEngine(this.options.engine, {
|
|
2981
|
+
signal: composed.signal,
|
|
2982
|
+
killGraceMs: COMPILE_KILL_GRACE_MS
|
|
2983
|
+
}) : await this.ensureReady();
|
|
2984
|
+
if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
|
|
2985
|
+
throw new NikaCompatibilityError(
|
|
2986
|
+
COMPILE_CAPABILITY,
|
|
2987
|
+
this.kind,
|
|
2988
|
+
`Engine ${identity.engineVersion} at ${this.options.engine.bin} does not advertise ${COMPILE_CAPABILITY} (advertised: ${identity.supportedCapabilities.join(", ") || "nothing"}); compile needs an engine that speaks the \`compile_version: 1\` wire (engine nika#1663). The SDK never compiles in TypeScript and never falls back to another channel`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
const captured = await captureEngine(this.options.engine.bin, invocation.args, {
|
|
2992
|
+
cwd: this.options.cwd,
|
|
2993
|
+
signal: composed.signal,
|
|
2994
|
+
bufferBytes: COMPILE_RESPONSE_MAX_BYTES,
|
|
2995
|
+
transport: this.kind,
|
|
2996
|
+
label: "compile",
|
|
2997
|
+
killGraceMs: COMPILE_KILL_GRACE_MS
|
|
2998
|
+
});
|
|
2999
|
+
return compileOutcomeFrom(captured, this.kind, this.options.engine.bin);
|
|
3000
|
+
} catch (cause) {
|
|
3001
|
+
if (composed.signal?.aborted && cause instanceof NikaTransportError) {
|
|
3002
|
+
throw new NikaTransportError(
|
|
3003
|
+
this.kind,
|
|
3004
|
+
composed.timedOut() ? `compile timed out after ${options.timeoutMs} ms` : "compile aborted by caller",
|
|
3005
|
+
{ cause }
|
|
3006
|
+
);
|
|
3007
|
+
}
|
|
3008
|
+
throw cause;
|
|
3009
|
+
} finally {
|
|
3010
|
+
composed.dispose();
|
|
3011
|
+
if (invocation) await removeCompileScratch(invocation);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
2489
3014
|
async startRun(workflow, options) {
|
|
2490
3015
|
const inputs = literalInputs(options);
|
|
2491
3016
|
const identity = await this.ensureReady();
|
|
@@ -2926,8 +3451,8 @@ function quotedProtocolError(cause, text, kind) {
|
|
|
2926
3451
|
);
|
|
2927
3452
|
}
|
|
2928
3453
|
function quoted(verdict, text) {
|
|
2929
|
-
const
|
|
2930
|
-
return
|
|
3454
|
+
const excerpt2 = diagnosticExcerpt(text);
|
|
3455
|
+
return excerpt2 ? `${verdict}: ${excerpt2}` : verdict;
|
|
2931
3456
|
}
|
|
2932
3457
|
function runFlags(options, literal) {
|
|
2933
3458
|
const flags = [];
|
|
@@ -3322,6 +3847,26 @@ var Nika = class {
|
|
|
3322
3847
|
check(workflow, options = {}) {
|
|
3323
3848
|
return this.transport.check(workflowName(workflow), options);
|
|
3324
3849
|
}
|
|
3850
|
+
/**
|
|
3851
|
+
* Authoring (issue #128): describe work — or name an accepted workflow plus
|
|
3852
|
+
* a change — and get the engine's checked candidate back, without running
|
|
3853
|
+
* anything. `compile()` never means `run()`: the outcome is data (candidate
|
|
3854
|
+
* source, questions, diagnostics, requested boundary, source-only Check
|
|
3855
|
+
* preview, provenance), `incomplete`/`refused` resolve instead of throwing,
|
|
3856
|
+
* and no workflow effect, approval or Proof exists.
|
|
3857
|
+
*
|
|
3858
|
+
* The candidate is ordinary `.nika` SOURCE. `run()` consumes a path,
|
|
3859
|
+
* so the caller materializes the candidate and `run(path)` re-admits it —
|
|
3860
|
+
* the compile preview is a review, never admission. Over `{ url }` this calls
|
|
3861
|
+
* the authenticated Serve compile door, gated by its advertised capability;
|
|
3862
|
+
* the SDK never compiles locally as a fallback.
|
|
3863
|
+
*/
|
|
3864
|
+
async compile(request, options = {}) {
|
|
3865
|
+
return this.transport.compile(
|
|
3866
|
+
normalizeCompileRequest(request),
|
|
3867
|
+
normalizeCompileOptions(options)
|
|
3868
|
+
);
|
|
3869
|
+
}
|
|
3325
3870
|
/**
|
|
3326
3871
|
* Resolves with the run's handle once the engine admitted it, and rejects
|
|
3327
3872
|
* without one when the engine refused it. The handle owns the lifecycle:
|