@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.cjs
CHANGED
|
@@ -254,29 +254,41 @@ function captureEngine(bin, args, options) {
|
|
|
254
254
|
shell: false,
|
|
255
255
|
stdio: ["ignore", "pipe", "pipe"]
|
|
256
256
|
});
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
const stdout = [];
|
|
258
|
+
const stderr = [];
|
|
259
|
+
const bytes = { stdout: 0, stderr: 0 };
|
|
259
260
|
let overflow = false;
|
|
260
261
|
let spawnError;
|
|
262
|
+
let closed = false;
|
|
263
|
+
let killTimer;
|
|
264
|
+
const stop = () => {
|
|
265
|
+
child.kill("SIGTERM");
|
|
266
|
+
if (options.killGraceMs === void 0) return;
|
|
267
|
+
killTimer ??= setTimeout(() => {
|
|
268
|
+
if (!closed) child.kill("SIGKILL");
|
|
269
|
+
}, options.killGraceMs);
|
|
270
|
+
killTimer.unref();
|
|
271
|
+
};
|
|
261
272
|
const append = (stream, chunk) => {
|
|
262
273
|
if (overflow) return;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
|
|
274
|
+
bytes[stream] += chunk.byteLength;
|
|
275
|
+
if (bytes[stream] > options.bufferBytes) {
|
|
266
276
|
overflow = true;
|
|
267
|
-
|
|
277
|
+
stop();
|
|
278
|
+
return;
|
|
268
279
|
}
|
|
280
|
+
(stream === "stdout" ? stdout : stderr).push(chunk);
|
|
269
281
|
};
|
|
270
|
-
child.stdout.setEncoding("utf8");
|
|
271
|
-
child.stderr.setEncoding("utf8");
|
|
272
282
|
child.stdout.on("data", (chunk) => append("stdout", chunk));
|
|
273
283
|
child.stderr.on("data", (chunk) => append("stderr", chunk));
|
|
274
|
-
const abort = () =>
|
|
284
|
+
const abort = () => stop();
|
|
275
285
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
276
286
|
child.once("error", (cause) => {
|
|
277
287
|
spawnError = cause;
|
|
278
288
|
});
|
|
279
|
-
child.once("close", (code) => {
|
|
289
|
+
child.once("close", (code, exitSignal) => {
|
|
290
|
+
closed = true;
|
|
291
|
+
if (killTimer) clearTimeout(killTimer);
|
|
280
292
|
options.signal?.removeEventListener("abort", abort);
|
|
281
293
|
if (options.signal?.aborted) {
|
|
282
294
|
reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
|
|
@@ -292,7 +304,19 @@ function captureEngine(bin, args, options) {
|
|
|
292
304
|
`${options.label} exceeded ${options.bufferBytes} bytes`
|
|
293
305
|
));
|
|
294
306
|
} else {
|
|
295
|
-
|
|
307
|
+
let decoded;
|
|
308
|
+
try {
|
|
309
|
+
decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stdout, bytes.stdout));
|
|
310
|
+
} catch {
|
|
311
|
+
reject(new NikaProtocolError(options.transport, `${options.label} stdout was not valid UTF-8`));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
resolve({
|
|
315
|
+
exitCode: code ?? 3,
|
|
316
|
+
stdout: decoded,
|
|
317
|
+
stderr: Buffer.concat(stderr, bytes.stderr).toString("utf8"),
|
|
318
|
+
exitSignal
|
|
319
|
+
});
|
|
296
320
|
}
|
|
297
321
|
});
|
|
298
322
|
});
|
|
@@ -524,9 +548,9 @@ function incompatible(transport, message) {
|
|
|
524
548
|
|
|
525
549
|
// src/lib/binary/verify.ts
|
|
526
550
|
var IDENTITY_BUFFER_BYTES = 16 * 1024;
|
|
527
|
-
async function verifyNikaEngine(engine) {
|
|
551
|
+
async function verifyNikaEngine(engine, options = {}) {
|
|
528
552
|
const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
|
|
529
|
-
const identity = await probeIdentity(engine.bin);
|
|
553
|
+
const identity = await probeIdentity(engine.bin, options);
|
|
530
554
|
if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
|
|
531
555
|
throw incompatible2(
|
|
532
556
|
`Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
|
|
@@ -565,13 +589,15 @@ async function verifyManagedPayload(engine) {
|
|
|
565
589
|
}
|
|
566
590
|
return manifest.version;
|
|
567
591
|
}
|
|
568
|
-
async function probeIdentity(bin) {
|
|
592
|
+
async function probeIdentity(bin, options) {
|
|
569
593
|
const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
|
|
570
594
|
const captured = await captureEngine(bin, ["--sdk-identity"], {
|
|
595
|
+
...options,
|
|
571
596
|
bufferBytes: IDENTITY_BUFFER_BYTES,
|
|
572
597
|
transport: "native-process",
|
|
573
598
|
label: "Engine identity probe"
|
|
574
599
|
}).catch((cause) => {
|
|
600
|
+
if (options.signal?.aborted) throw cause;
|
|
575
601
|
throw incompatible2(
|
|
576
602
|
`Engine identity probe of ${bin} failed`,
|
|
577
603
|
cause
|
|
@@ -651,25 +677,26 @@ var NATIVE_ARRAY = Function.prototype.toString.call(Array);
|
|
|
651
677
|
var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
|
|
652
678
|
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
653
679
|
var PATH_LIMIT = 240;
|
|
654
|
-
function encodeLiteralInputs(inputs) {
|
|
680
|
+
function encodeLiteralInputs(inputs, label = "run({ inputs })") {
|
|
655
681
|
if (containerKind(inputs) !== "object") {
|
|
682
|
+
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";
|
|
656
683
|
throw new NikaConfigurationError(
|
|
657
|
-
|
|
684
|
+
`${label}: ${subject} to strict JSON values; received ${describe(inputs)}`
|
|
658
685
|
);
|
|
659
686
|
}
|
|
660
687
|
const parts = [];
|
|
661
688
|
let bytes = 0;
|
|
662
689
|
const write = (chunk) => {
|
|
663
690
|
bytes += Buffer.byteLength(chunk);
|
|
664
|
-
if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
|
|
691
|
+
if (bytes > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
|
|
665
692
|
parts.push(chunk);
|
|
666
693
|
};
|
|
667
694
|
const stack = [];
|
|
668
695
|
const open = /* @__PURE__ */ new Set();
|
|
669
696
|
const refuse = (segment, problem) => {
|
|
670
|
-
const
|
|
671
|
-
const shown =
|
|
672
|
-
return new NikaConfigurationError(
|
|
697
|
+
const path4 = stack.map((frame) => frame.segment).join("") + segment;
|
|
698
|
+
const shown = path4.length > PATH_LIMIT ? `${path4.slice(0, PATH_LIMIT / 2)}\u2026${path4.slice(-PATH_LIMIT / 2)}` : path4;
|
|
699
|
+
return new NikaConfigurationError(`${label}: ${shown} ${problem}; ${STRICT_JSON}`);
|
|
673
700
|
};
|
|
674
701
|
const enter = (value, segment, kind) => {
|
|
675
702
|
open.add(value);
|
|
@@ -722,7 +749,7 @@ function encodeLiteralInputs(inputs) {
|
|
|
722
749
|
const value = member.value;
|
|
723
750
|
switch (typeof value) {
|
|
724
751
|
case "string":
|
|
725
|
-
if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge();
|
|
752
|
+
if (value.length > LITERAL_INPUTS_MAX_BYTES) throw tooLarge(label);
|
|
726
753
|
if (LONE_SURROGATE.test(value)) {
|
|
727
754
|
throw refuse(segment, "contains a lone surrogate, which is not valid Unicode");
|
|
728
755
|
}
|
|
@@ -754,7 +781,17 @@ function encodeLiteralInputs(inputs) {
|
|
|
754
781
|
}
|
|
755
782
|
return { json: parts.join(""), bytes };
|
|
756
783
|
}
|
|
757
|
-
function tooLarge() {
|
|
784
|
+
function tooLarge(label) {
|
|
785
|
+
if (label === "compile({ change })") {
|
|
786
|
+
return new NikaConfigurationError(
|
|
787
|
+
`compile({ change }): the serialized literal exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB)`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (label === "compile({ answers })") {
|
|
791
|
+
return new NikaConfigurationError(
|
|
792
|
+
`compile({ answers }): the serialized answers exceed ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB); answers ride argv, one KEY=JSON element each, never a wire`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
758
795
|
return new NikaConfigurationError(
|
|
759
796
|
`run({ inputs }): the serialized inputs map exceeds ${LITERAL_INPUTS_MAX_BYTES} bytes (1 MiB), the bound both transports share with the native engine`
|
|
760
797
|
);
|
|
@@ -811,6 +848,364 @@ function refusedObject(value) {
|
|
|
811
848
|
return typeof name === "string" && IDENTIFIER.test(name) && name.length <= 64 ? `is a ${name} instance, not a plain object or array` : custom;
|
|
812
849
|
}
|
|
813
850
|
|
|
851
|
+
// src/lib/compile.ts
|
|
852
|
+
var import_promises2 = require("fs/promises");
|
|
853
|
+
var import_node_os = require("os");
|
|
854
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
855
|
+
var import_node_util2 = require("util");
|
|
856
|
+
var import_node_events = require("events");
|
|
857
|
+
var COMPILE_CAPABILITY = "compile";
|
|
858
|
+
var COMPILE_WIRE_VERSION = 1;
|
|
859
|
+
var COMPILE_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
860
|
+
var COMPILE_KILL_GRACE_MS = 2e3;
|
|
861
|
+
var STATUSES = ["ready", "incomplete", "refused"];
|
|
862
|
+
var signalAborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted").get;
|
|
863
|
+
var removeSignalListener = EventTarget.prototype.removeEventListener;
|
|
864
|
+
var signalInterface = /* @__PURE__ */ new Set([
|
|
865
|
+
"aborted",
|
|
866
|
+
"reason",
|
|
867
|
+
"throwIfAborted",
|
|
868
|
+
"addEventListener",
|
|
869
|
+
"removeEventListener",
|
|
870
|
+
"dispatchEvent"
|
|
871
|
+
]);
|
|
872
|
+
function normalizeCompileRequest(input) {
|
|
873
|
+
if (typeof input === "string") {
|
|
874
|
+
if (input.length === 0) {
|
|
875
|
+
throw new NikaConfigurationError("compile: an intent string must not be empty");
|
|
876
|
+
}
|
|
877
|
+
return { intent: input };
|
|
878
|
+
}
|
|
879
|
+
const record2 = dataRecord(input, "request");
|
|
880
|
+
for (const key of Reflect.ownKeys(record2)) {
|
|
881
|
+
if (key !== "intent" && key !== "workflow" && key !== "change" && key !== "answers") {
|
|
882
|
+
throw new NikaConfigurationError(
|
|
883
|
+
`compile: unknown request field ${String(key)}; the request is refused rather than silently reinterpreted`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const answers = record2.answers;
|
|
888
|
+
if (answers !== void 0) encodeLiteralInputs(answers, "compile({ answers })");
|
|
889
|
+
const intent = record2.intent;
|
|
890
|
+
const workflow = record2.workflow;
|
|
891
|
+
const change = record2.change;
|
|
892
|
+
if (intent !== void 0) {
|
|
893
|
+
if (workflow !== void 0 || change !== void 0) {
|
|
894
|
+
throw new NikaConfigurationError(
|
|
895
|
+
"compile: intent (create) never mixes with workflow/change (edit) in one request"
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
if (typeof intent !== "string" || intent.length === 0) {
|
|
899
|
+
throw new NikaConfigurationError("compile: intent must be a non-empty string");
|
|
900
|
+
}
|
|
901
|
+
return answers === void 0 ? { intent } : { intent, answers };
|
|
902
|
+
}
|
|
903
|
+
if (workflow === void 0 && change === void 0) {
|
|
904
|
+
throw new NikaConfigurationError(
|
|
905
|
+
"compile: the request carries neither intent nor workflow/change; nothing would compile"
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
if (typeof workflow !== "string" || workflow.length === 0) {
|
|
909
|
+
throw new NikaConfigurationError(
|
|
910
|
+
"compile: an edit needs the accepted workflow source as a non-empty string"
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
const normalizedChange = normalizeChange(change);
|
|
914
|
+
return answers === void 0 ? { workflow, change: normalizedChange } : { workflow, change: normalizedChange, answers };
|
|
915
|
+
}
|
|
916
|
+
function dataRecord(value, label) {
|
|
917
|
+
if (value === null || typeof value !== "object" || import_node_util2.types.isProxy(value) || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
918
|
+
throw new NikaConfigurationError(`compile: ${label} must be ${label === "request" ? "an intent string or " : ""}a plain object with own data fields`);
|
|
919
|
+
}
|
|
920
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
921
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
922
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
923
|
+
const descriptor = descriptors[key];
|
|
924
|
+
if (typeof key !== "string" || !("value" in descriptor) || !descriptor.enumerable) {
|
|
925
|
+
throw new NikaConfigurationError(`compile: ${label} must contain only enumerable string data fields`);
|
|
926
|
+
}
|
|
927
|
+
result[key] = descriptor.value;
|
|
928
|
+
}
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
function normalizeChange(change) {
|
|
932
|
+
if (typeof change === "string" && change.length > 0) return change;
|
|
933
|
+
if (change === void 0 || change === "") {
|
|
934
|
+
throw new NikaConfigurationError("compile: an edit needs a non-empty change request");
|
|
935
|
+
}
|
|
936
|
+
const encoded = encodeLiteralInputs({ change }, "compile({ change })");
|
|
937
|
+
const object = machineObject(JSON.parse(encoded.json).change);
|
|
938
|
+
const constant = machineObject(object?.set_constant);
|
|
939
|
+
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)) {
|
|
940
|
+
throw new NikaConfigurationError(
|
|
941
|
+
"compile: an edit needs a non-empty change string or set_constant with a bare name and JSON value"
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
return object;
|
|
945
|
+
}
|
|
946
|
+
function normalizeCompileOptions(options) {
|
|
947
|
+
const record2 = dataRecord(options, "options");
|
|
948
|
+
for (const key of Reflect.ownKeys(record2)) {
|
|
949
|
+
if (key !== "signal" && key !== "timeoutMs") {
|
|
950
|
+
throw new NikaConfigurationError(`compile: unknown option ${String(key)}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const timeoutMs = record2.timeoutMs;
|
|
954
|
+
if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)) {
|
|
955
|
+
throw new NikaConfigurationError(
|
|
956
|
+
"compile: timeoutMs must be a positive safe integer between 1 and 2147483647 milliseconds"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
const signal = record2.signal;
|
|
960
|
+
if (signal !== void 0) {
|
|
961
|
+
const invalidSignal = () => new NikaConfigurationError("compile: signal must be an AbortSignal");
|
|
962
|
+
if (signal === null || typeof signal !== "object" || import_node_util2.types.isProxy(signal) || Object.getPrototypeOf(signal) !== AbortSignal.prototype) throw invalidSignal();
|
|
963
|
+
const descriptors = Object.getOwnPropertyDescriptors(signal);
|
|
964
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
965
|
+
if (typeof key === "string" && signalInterface.has(key) || !("value" in descriptors[key])) throw invalidSignal();
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
signalAborted.call(signal);
|
|
969
|
+
} catch {
|
|
970
|
+
throw invalidSignal();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return record2;
|
|
974
|
+
}
|
|
975
|
+
function compileSignal(options) {
|
|
976
|
+
const caller = options.signal;
|
|
977
|
+
let bridge;
|
|
978
|
+
let abort;
|
|
979
|
+
if (caller !== void 0) {
|
|
980
|
+
bridge = new AbortController();
|
|
981
|
+
const controller = bridge;
|
|
982
|
+
abort = () => controller.abort();
|
|
983
|
+
if (signalAborted.call(caller)) abort();
|
|
984
|
+
else (0, import_node_events.addAbortListener)(caller, abort);
|
|
985
|
+
}
|
|
986
|
+
let timeout;
|
|
987
|
+
if (options.timeoutMs !== void 0) timeout = AbortSignal.timeout(options.timeoutMs);
|
|
988
|
+
const signals = [bridge?.signal, timeout].filter((s) => s !== void 0);
|
|
989
|
+
return {
|
|
990
|
+
signal: signals.length === 0 ? void 0 : AbortSignal.any(signals),
|
|
991
|
+
timedOut: () => timeout?.aborted === true,
|
|
992
|
+
// Use the intrinsic even if the caller shadows removeEventListener after
|
|
993
|
+
// starting Compile. Do not retain a listener on a long-lived caller signal.
|
|
994
|
+
dispose: () => {
|
|
995
|
+
if (caller && abort) removeSignalListener.call(caller, "abort", abort);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
async function compileArgv(request) {
|
|
1000
|
+
const args = ["compile", "--json"];
|
|
1001
|
+
for (const answer of compileAnswers(request.answers)) {
|
|
1002
|
+
args.push(`--answer=${answer}`);
|
|
1003
|
+
}
|
|
1004
|
+
if (request.intent !== void 0) {
|
|
1005
|
+
refuseNul(request.intent);
|
|
1006
|
+
args.push("--", request.intent);
|
|
1007
|
+
return { args };
|
|
1008
|
+
}
|
|
1009
|
+
const change = typeof request.change === "string" ? request.change : `Set const.${request.change.set_constant.name} to ${literalJson(request.change.set_constant.value)}`;
|
|
1010
|
+
refuseNul(change);
|
|
1011
|
+
const scratchDir = await (0, import_promises2.mkdtemp)(import_node_path3.default.join((0, import_node_os.tmpdir)(), "nika-sdk-compile-"));
|
|
1012
|
+
const base = import_node_path3.default.join(scratchDir, "base.nika");
|
|
1013
|
+
try {
|
|
1014
|
+
await (0, import_promises2.writeFile)(base, request.workflow, { encoding: "utf8", mode: 384 });
|
|
1015
|
+
} catch (cause) {
|
|
1016
|
+
await (0, import_promises2.rm)(scratchDir, { recursive: true, force: true });
|
|
1017
|
+
throw new NikaTransportError("native-process", "compile could not write its temporary base source", {
|
|
1018
|
+
cause: cause instanceof Error ? cause : void 0
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
args.push("--base", base, `--change=${change}`);
|
|
1022
|
+
return { args, scratchDir };
|
|
1023
|
+
}
|
|
1024
|
+
async function removeCompileScratch(invocation) {
|
|
1025
|
+
if (invocation.scratchDir !== void 0) {
|
|
1026
|
+
await (0, import_promises2.rm)(invocation.scratchDir, { recursive: true, force: true });
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function compileAnswers(answers) {
|
|
1030
|
+
if (answers === void 0) return [];
|
|
1031
|
+
encodeLiteralInputs(answers, "compile({ answers })");
|
|
1032
|
+
return Reflect.ownKeys(answers).map((key) => {
|
|
1033
|
+
const name = String(key);
|
|
1034
|
+
if (name.length === 0 || name.includes("=") || name.includes("\0")) {
|
|
1035
|
+
throw new NikaConfigurationError(
|
|
1036
|
+
"compile({ answers }): a question key must be non-empty and cannot carry '='; the engine splits KEY=JSON on its first equals sign"
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return `${name}=${literalJson(answers[name])}`;
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
function literalJson(value) {
|
|
1043
|
+
return encodeLiteralInputs({ value }, "compile({ change })").json.slice('{"value":'.length, -1);
|
|
1044
|
+
}
|
|
1045
|
+
function refuseNul(value) {
|
|
1046
|
+
if (value.includes("\0")) throw new NikaConfigurationError("compile: text cannot contain a NUL byte");
|
|
1047
|
+
}
|
|
1048
|
+
function compileBody(request) {
|
|
1049
|
+
compileAnswers(request.answers);
|
|
1050
|
+
const answers = request.answers === void 0 ? "" : `,"answers":${encodeLiteralInputs(request.answers, "compile({ answers })").json}`;
|
|
1051
|
+
if (request.intent !== void 0) {
|
|
1052
|
+
refuseNul(request.intent);
|
|
1053
|
+
return `{"compile_version":1,"mode":"create","intent":${JSON.stringify(request.intent)}${answers}}`;
|
|
1054
|
+
}
|
|
1055
|
+
if (typeof request.change === "string") refuseNul(request.change);
|
|
1056
|
+
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)}}}`;
|
|
1057
|
+
return `{"compile_version":1,"mode":"edit","source":${JSON.stringify(request.workflow)},"change":${change}${answers}}`;
|
|
1058
|
+
}
|
|
1059
|
+
function compileOutcomeFrom(captured, transport, engine) {
|
|
1060
|
+
const protocol = (message) => new NikaProtocolError(transport, `${message} (compile child at ${engine})`);
|
|
1061
|
+
if (captured.exitSignal !== null) {
|
|
1062
|
+
throw new NikaTransportError(
|
|
1063
|
+
transport,
|
|
1064
|
+
`compile child at ${engine} was ended externally by ${captured.exitSignal}`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const payload = machineObject(tryParseJson(captured.stdout));
|
|
1068
|
+
if (!payload) {
|
|
1069
|
+
const detail = excerpt(captured.stderr) || excerpt(captured.stdout);
|
|
1070
|
+
throw protocol(
|
|
1071
|
+
`the compile wire object is absent or malformed (exit ${captured.exitCode})` + (detail ? `: ${detail}` : "")
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
validateCompileVersion(payload.compile_version, transport, engine, protocol);
|
|
1075
|
+
const hasError = "error" in payload;
|
|
1076
|
+
const hasStatus = "status" in payload;
|
|
1077
|
+
if (hasError && hasStatus) {
|
|
1078
|
+
throw protocol("the payload carries both an outcome and an error");
|
|
1079
|
+
}
|
|
1080
|
+
if (hasError) {
|
|
1081
|
+
const error = machineObject(payload.error);
|
|
1082
|
+
if (!error || typeof error.code !== "string" || typeof error.message !== "string") {
|
|
1083
|
+
throw protocol("the error payload lacks its code/message strings");
|
|
1084
|
+
}
|
|
1085
|
+
if (captured.exitCode !== 2 && captured.exitCode !== 3) {
|
|
1086
|
+
throw protocol(
|
|
1087
|
+
`an engine-stamped error arrived with exit ${captured.exitCode}, not 2 or 3`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
throw new NikaOperationError("compile", transport, error.code, error.message, {
|
|
1091
|
+
status: captured.exitCode,
|
|
1092
|
+
machineCode: error.code
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const status = payload.status;
|
|
1096
|
+
if (typeof status !== "string" || !STATUSES.includes(status)) {
|
|
1097
|
+
throw protocol(`unknown compile status ${JSON.stringify(status ?? null)}`);
|
|
1098
|
+
}
|
|
1099
|
+
if (status === "ready" ? captured.exitCode !== 0 : captured.exitCode !== 2) {
|
|
1100
|
+
throw protocol(
|
|
1101
|
+
`status ${JSON.stringify(status)} contradicts exit ${captured.exitCode}`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
writtenFrom(payload.written, protocol);
|
|
1105
|
+
return compilePayloadFrom(payload, transport, engine);
|
|
1106
|
+
}
|
|
1107
|
+
function compilePayloadFrom(payload, transport, engine) {
|
|
1108
|
+
const protocol = (message) => new NikaProtocolError(transport, `compile at ${engine}: ${message}`);
|
|
1109
|
+
validateCompileVersion(payload.compile_version, transport, engine, protocol);
|
|
1110
|
+
if ("error" in payload) throw protocol("the payload carries an error instead of an outcome");
|
|
1111
|
+
if (transport === "http" && "written" in payload) throw protocol("HTTP outcome claims a written destination");
|
|
1112
|
+
const status = payload.status;
|
|
1113
|
+
if (typeof status !== "string" || !STATUSES.includes(status)) {
|
|
1114
|
+
throw protocol("unknown compile status");
|
|
1115
|
+
}
|
|
1116
|
+
const candidate = payload.candidate;
|
|
1117
|
+
if (candidate !== null && typeof candidate !== "string") {
|
|
1118
|
+
throw protocol("candidate is neither source text nor null");
|
|
1119
|
+
}
|
|
1120
|
+
if (status === "ready" && candidate === null) {
|
|
1121
|
+
throw protocol("a ready outcome carries no candidate");
|
|
1122
|
+
}
|
|
1123
|
+
const outcome = {
|
|
1124
|
+
compile_version: COMPILE_WIRE_VERSION,
|
|
1125
|
+
status,
|
|
1126
|
+
ready: status === "ready",
|
|
1127
|
+
candidate,
|
|
1128
|
+
questions: questionsFrom(payload.questions, protocol),
|
|
1129
|
+
diagnostics: diagnosticsFrom(payload.diagnostics, protocol),
|
|
1130
|
+
requested_boundary: nullableObject(payload.requested_boundary, "requested_boundary", protocol),
|
|
1131
|
+
check_preview: previewFrom(payload.check_preview, protocol),
|
|
1132
|
+
provenance: provenanceFrom(payload.provenance, protocol)
|
|
1133
|
+
};
|
|
1134
|
+
return outcome;
|
|
1135
|
+
}
|
|
1136
|
+
function validateCompileVersion(version, transport, engine, protocol) {
|
|
1137
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) {
|
|
1138
|
+
throw protocol("compile_version must be a positive safe integer");
|
|
1139
|
+
}
|
|
1140
|
+
if (version !== COMPILE_WIRE_VERSION) {
|
|
1141
|
+
throw new NikaCompatibilityError(
|
|
1142
|
+
COMPILE_CAPABILITY,
|
|
1143
|
+
transport,
|
|
1144
|
+
`Engine at ${engine} speaks compile wire ${version}; expected ${COMPILE_WIRE_VERSION}`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function tryParseJson(text) {
|
|
1149
|
+
const trimmed = text.trim();
|
|
1150
|
+
if (!trimmed) return void 0;
|
|
1151
|
+
try {
|
|
1152
|
+
return JSON.parse(trimmed);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return void 0;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
function excerpt(text) {
|
|
1158
|
+
const flat = text.trim().replace(/\s+/g, " ");
|
|
1159
|
+
return flat.length <= 240 ? flat : `${flat.slice(0, 240)}\u2026`;
|
|
1160
|
+
}
|
|
1161
|
+
function nullableObject(value, field, protocol) {
|
|
1162
|
+
if (value === null) return null;
|
|
1163
|
+
const object = machineObject(value);
|
|
1164
|
+
if (!object) throw protocol(`${field} is neither an object nor null`);
|
|
1165
|
+
return object;
|
|
1166
|
+
}
|
|
1167
|
+
function questionsFrom(value, protocol) {
|
|
1168
|
+
if (!Array.isArray(value)) throw protocol("questions is not an array");
|
|
1169
|
+
return value.map((entry) => {
|
|
1170
|
+
const question = machineObject(entry);
|
|
1171
|
+
if (!question || typeof question.key !== "string" || typeof question.label !== "string" || question.type !== "text" && question.type !== "literal" || typeof question.why !== "string" || typeof question.mandatory !== "boolean") {
|
|
1172
|
+
throw protocol("a question lacks its key/label/type/why/mandatory shape");
|
|
1173
|
+
}
|
|
1174
|
+
return question;
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
function diagnosticsFrom(value, protocol) {
|
|
1178
|
+
if (!Array.isArray(value)) throw protocol("diagnostics is not an array");
|
|
1179
|
+
return value.map((entry) => {
|
|
1180
|
+
const diagnostic = machineObject(entry);
|
|
1181
|
+
if (!diagnostic || typeof diagnostic.kind !== "string" || !["applied", "missed", "unknown", "requiresHuman", "refused"].includes(diagnostic.kind) || typeof diagnostic.target !== "string" || typeof diagnostic.message !== "string") {
|
|
1182
|
+
throw protocol("a diagnostic lacks its kind/target/message shape");
|
|
1183
|
+
}
|
|
1184
|
+
return diagnostic;
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
function previewFrom(value, protocol) {
|
|
1188
|
+
if (value === null) return null;
|
|
1189
|
+
const preview = machineObject(value);
|
|
1190
|
+
if (!preview || preview.scope !== "sourceOnly" || !machineObject(preview.report)) {
|
|
1191
|
+
throw protocol("check_preview lacks its scope/report shape");
|
|
1192
|
+
}
|
|
1193
|
+
return { ...preview, scope: preview.scope, report: preview.report };
|
|
1194
|
+
}
|
|
1195
|
+
function provenanceFrom(value, protocol) {
|
|
1196
|
+
const provenance = machineObject(value);
|
|
1197
|
+
if (!provenance || typeof provenance.compiler_version !== "string" || typeof provenance.spec_pin !== "string" || provenance.skeleton !== null && typeof provenance.skeleton !== "string" || provenance.cognition !== "deterministicOnly") {
|
|
1198
|
+
throw protocol("provenance lacks its compiler_version/spec_pin/skeleton/cognition shape");
|
|
1199
|
+
}
|
|
1200
|
+
return provenance;
|
|
1201
|
+
}
|
|
1202
|
+
function writtenFrom(value, protocol) {
|
|
1203
|
+
if (value !== null) {
|
|
1204
|
+
throw protocol("the outcome claims a written destination the SDK never asked for");
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
814
1209
|
// src/lib/sse/parser.ts
|
|
815
1210
|
var SseParseError = class extends Error {
|
|
816
1211
|
constructor(message, options) {
|
|
@@ -972,8 +1367,8 @@ async function* decodeSse(stream, limits, signal) {
|
|
|
972
1367
|
var WORKFLOW_SUFFIX = ".nika";
|
|
973
1368
|
var LEGACY_WORKFLOW_SUFFIXES = [".nika.yaml", ".nika.yml"];
|
|
974
1369
|
var CONTAINED_CHAR = /[A-Za-z0-9/._-]/;
|
|
975
|
-
function workflowBasename(
|
|
976
|
-
const parts =
|
|
1370
|
+
function workflowBasename(path4) {
|
|
1371
|
+
const parts = path4.split(/[\\/]/);
|
|
977
1372
|
return parts[parts.length - 1] ?? "";
|
|
978
1373
|
}
|
|
979
1374
|
function hasControlChar(value) {
|
|
@@ -985,14 +1380,14 @@ function hasControlChar(value) {
|
|
|
985
1380
|
}
|
|
986
1381
|
return false;
|
|
987
1382
|
}
|
|
988
|
-
function isLegacyWorkflowPath(
|
|
989
|
-
if (typeof
|
|
1383
|
+
function isLegacyWorkflowPath(path4) {
|
|
1384
|
+
if (typeof path4 !== "string" || path4.length === 0) {
|
|
990
1385
|
return false;
|
|
991
1386
|
}
|
|
992
|
-
if (
|
|
1387
|
+
if (path4.endsWith("/") || path4.endsWith("\\")) {
|
|
993
1388
|
return false;
|
|
994
1389
|
}
|
|
995
|
-
const base = workflowBasename(
|
|
1390
|
+
const base = workflowBasename(path4);
|
|
996
1391
|
return LEGACY_WORKFLOW_SUFFIXES.some((suffix) => base.endsWith(suffix));
|
|
997
1392
|
}
|
|
998
1393
|
function isCanonicalWorkflowFilename(basename) {
|
|
@@ -1013,17 +1408,17 @@ function isCanonicalWorkflowFilename(basename) {
|
|
|
1013
1408
|
}
|
|
1014
1409
|
return basename.length > WORKFLOW_SUFFIX.length;
|
|
1015
1410
|
}
|
|
1016
|
-
function isCanonicalWorkflowPath(
|
|
1017
|
-
if (typeof
|
|
1411
|
+
function isCanonicalWorkflowPath(path4) {
|
|
1412
|
+
if (typeof path4 !== "string" || path4.length === 0) {
|
|
1018
1413
|
return false;
|
|
1019
1414
|
}
|
|
1020
|
-
if (hasControlChar(
|
|
1415
|
+
if (hasControlChar(path4)) {
|
|
1021
1416
|
return false;
|
|
1022
1417
|
}
|
|
1023
|
-
if (
|
|
1418
|
+
if (path4.endsWith("/") || path4.endsWith("\\")) {
|
|
1024
1419
|
return false;
|
|
1025
1420
|
}
|
|
1026
|
-
return isCanonicalWorkflowFilename(workflowBasename(
|
|
1421
|
+
return isCanonicalWorkflowFilename(workflowBasename(path4));
|
|
1027
1422
|
}
|
|
1028
1423
|
function containedRelativeShape(value) {
|
|
1029
1424
|
if (value.includes("\\") || value.startsWith("/")) {
|
|
@@ -1114,6 +1509,69 @@ var HttpTransport = class {
|
|
|
1114
1509
|
}
|
|
1115
1510
|
return captured.report;
|
|
1116
1511
|
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Stateless authoring through the authenticated Serve compile door.
|
|
1514
|
+
* A remote connection never falls back to a local compile — the candidate
|
|
1515
|
+
* must come from the server the caller connected to, or not exist.
|
|
1516
|
+
*/
|
|
1517
|
+
async compile(request, options) {
|
|
1518
|
+
const body = compileBody(request);
|
|
1519
|
+
const timeoutMs = options.timeoutMs ?? this.options.requestTimeout;
|
|
1520
|
+
const composed = compileSignal({ ...options, timeoutMs });
|
|
1521
|
+
const signal = composed.signal;
|
|
1522
|
+
try {
|
|
1523
|
+
if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
|
|
1524
|
+
const identity = this.remoteIdentity ?? await this.probeServerIdentity(signal);
|
|
1525
|
+
if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
|
|
1526
|
+
throw this.gap(
|
|
1527
|
+
COMPILE_CAPABILITY,
|
|
1528
|
+
"The connected engine does not advertise compile; the SDK never compiles locally as a substitute"
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const path4 = "/v1/compile";
|
|
1532
|
+
const response = await this.fetchResponse(path4, {
|
|
1533
|
+
method: "POST",
|
|
1534
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1535
|
+
body,
|
|
1536
|
+
signal
|
|
1537
|
+
}, false, true, false);
|
|
1538
|
+
if (response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
|
|
1539
|
+
await discardResponse(response);
|
|
1540
|
+
throw new NikaProtocolError(this.kind, "HTTP compile returned an invalid content-type");
|
|
1541
|
+
}
|
|
1542
|
+
const object = await this.readObservationObject(
|
|
1543
|
+
response,
|
|
1544
|
+
path4,
|
|
1545
|
+
signal,
|
|
1546
|
+
Math.min(this.options.machineBufferBytes, COMPILE_RESPONSE_MAX_BYTES),
|
|
1547
|
+
false
|
|
1548
|
+
);
|
|
1549
|
+
if (signal.aborted) throw new NikaTransportError(this.kind, "compile aborted");
|
|
1550
|
+
if (response.status !== 200) {
|
|
1551
|
+
const error = machineObject(object.error);
|
|
1552
|
+
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")) {
|
|
1553
|
+
throw new NikaProtocolError(this.kind, "HTTP compile returned a malformed refusal or non-contract status");
|
|
1554
|
+
}
|
|
1555
|
+
throw this.refused(path4, {
|
|
1556
|
+
operation: "compile",
|
|
1557
|
+
status: response.status,
|
|
1558
|
+
refusal: { code: error.code, message: this.redact(error.message) }
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
return compilePayloadFrom(object, this.kind, this.options.url);
|
|
1562
|
+
} catch (cause) {
|
|
1563
|
+
if (signal.aborted) {
|
|
1564
|
+
throw new NikaTransportError(
|
|
1565
|
+
this.kind,
|
|
1566
|
+
composed.timedOut() ? `compile timed out after ${timeoutMs} ms` : "compile aborted by caller",
|
|
1567
|
+
{ cause: cause instanceof Error ? cause : void 0 }
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
throw cause;
|
|
1571
|
+
} finally {
|
|
1572
|
+
composed.dispose();
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1117
1575
|
async startRun(workflow, options) {
|
|
1118
1576
|
const inputs = literalInputs(options);
|
|
1119
1577
|
if (options.vars && Object.keys(options.vars).length > 0 || options.model !== void 0 || options.maxCostUsd !== void 0) {
|
|
@@ -1233,14 +1691,14 @@ var HttpTransport = class {
|
|
|
1233
1691
|
async schedule(workflow, options) {
|
|
1234
1692
|
refuseLegacyContainedName(workflow);
|
|
1235
1693
|
await this.ensureScheduleCapability();
|
|
1236
|
-
const
|
|
1694
|
+
const path4 = `/v1/schedules/${encodeURIComponent(options.id)}`;
|
|
1237
1695
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1238
1696
|
if (options.revision === void 0) {
|
|
1239
1697
|
headers.set("If-None-Match", "*");
|
|
1240
1698
|
} else {
|
|
1241
1699
|
headers.set("If-Match", `"${options.revision}"`);
|
|
1242
1700
|
}
|
|
1243
|
-
const object = await this.operationJson("schedule",
|
|
1701
|
+
const object = await this.operationJson("schedule", path4, {
|
|
1244
1702
|
method: "PUT",
|
|
1245
1703
|
headers,
|
|
1246
1704
|
body: JSON.stringify(scheduleBody(workflow, options))
|
|
@@ -1252,8 +1710,8 @@ var HttpTransport = class {
|
|
|
1252
1710
|
}
|
|
1253
1711
|
async scheduleStatus(id) {
|
|
1254
1712
|
await this.ensureScheduleCapability();
|
|
1255
|
-
const
|
|
1256
|
-
const object = await this.operationJson("scheduleStatus",
|
|
1713
|
+
const path4 = `/v1/schedules/${encodeURIComponent(id)}`;
|
|
1714
|
+
const object = await this.operationJson("scheduleStatus", path4, {
|
|
1257
1715
|
method: "GET",
|
|
1258
1716
|
headers: { Accept: "application/json" }
|
|
1259
1717
|
});
|
|
@@ -1586,16 +2044,16 @@ var HttpTransport = class {
|
|
|
1586
2044
|
}
|
|
1587
2045
|
return event;
|
|
1588
2046
|
}
|
|
1589
|
-
async inspectDurableJob(
|
|
2047
|
+
async inspectDurableJob(path4, expectedId, signal) {
|
|
1590
2048
|
let response;
|
|
1591
2049
|
try {
|
|
1592
|
-
response = await this.observationRequest(
|
|
2050
|
+
response = await this.observationRequest(path4, { headers: { Accept: "application/json" }, signal });
|
|
1593
2051
|
} catch {
|
|
1594
2052
|
return { retry: true };
|
|
1595
2053
|
}
|
|
1596
2054
|
if (response.status === 404) {
|
|
1597
2055
|
await discardResponse(response);
|
|
1598
|
-
throw new NikaTransportError(this.kind, `HTTP 404 for ${
|
|
2056
|
+
throw new NikaTransportError(this.kind, `HTTP 404 for ${path4}; job was durably absent`);
|
|
1599
2057
|
}
|
|
1600
2058
|
if (isRetryableStatus(response.status)) {
|
|
1601
2059
|
const retryAfterMilliseconds = retryAfter(response.headers.get("Retry-After"));
|
|
@@ -1605,9 +2063,9 @@ var HttpTransport = class {
|
|
|
1605
2063
|
...retryAfterMilliseconds !== void 0 ? { retryAfterMilliseconds } : {}
|
|
1606
2064
|
};
|
|
1607
2065
|
}
|
|
1608
|
-
this.assertObservationResponse(response,
|
|
2066
|
+
this.assertObservationResponse(response, path4, "application/json");
|
|
1609
2067
|
try {
|
|
1610
|
-
const object = await this.readObservationObject(response,
|
|
2068
|
+
const object = await this.readObservationObject(response, path4);
|
|
1611
2069
|
return durableJob(object, expectedId, this.kind);
|
|
1612
2070
|
} catch (cause) {
|
|
1613
2071
|
if (cause instanceof NikaTransportError && !(cause instanceof NikaProtocolError)) {
|
|
@@ -1616,31 +2074,31 @@ var HttpTransport = class {
|
|
|
1616
2074
|
throw cause;
|
|
1617
2075
|
}
|
|
1618
2076
|
}
|
|
1619
|
-
assertObservationResponse(response,
|
|
2077
|
+
assertObservationResponse(response, path4, contentType) {
|
|
1620
2078
|
if (response.status === 204) {
|
|
1621
2079
|
void discardResponse(response);
|
|
1622
|
-
throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${
|
|
2080
|
+
throw new NikaProtocolError(this.kind, `HTTP 204 permanently stopped observation for ${path4}`);
|
|
1623
2081
|
}
|
|
1624
2082
|
if (response.status === 401 || response.status === 403) {
|
|
1625
2083
|
void discardResponse(response);
|
|
1626
|
-
throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${
|
|
2084
|
+
throw new NikaTransportError(this.kind, `HTTP ${response.status} authorization failure for ${path4}`);
|
|
1627
2085
|
}
|
|
1628
2086
|
if (response.status !== 200) {
|
|
1629
2087
|
void discardResponse(response);
|
|
1630
|
-
throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${
|
|
2088
|
+
throw new NikaTransportError(this.kind, `HTTP ${response.status} permanently failed ${path4}`);
|
|
1631
2089
|
}
|
|
1632
2090
|
const actual = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1633
2091
|
if (actual !== contentType) {
|
|
1634
2092
|
void discardResponse(response);
|
|
1635
2093
|
throw new NikaProtocolError(
|
|
1636
2094
|
this.kind,
|
|
1637
|
-
`HTTP ${
|
|
2095
|
+
`HTTP ${path4} returned an invalid content-type`
|
|
1638
2096
|
);
|
|
1639
2097
|
}
|
|
1640
2098
|
}
|
|
1641
|
-
async readObservationObject(response,
|
|
2099
|
+
async readObservationObject(response, path4, signal, maxBytes = this.options.machineBufferBytes, useRequestTimeout = true) {
|
|
1642
2100
|
if (!response.body) {
|
|
1643
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2101
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} omitted its JSON body`);
|
|
1644
2102
|
}
|
|
1645
2103
|
const reader = response.body.getReader();
|
|
1646
2104
|
const chunks = [];
|
|
@@ -1650,38 +2108,38 @@ var HttpTransport = class {
|
|
|
1650
2108
|
rejectBoundary = reject;
|
|
1651
2109
|
});
|
|
1652
2110
|
const abort = () => {
|
|
1653
|
-
rejectBoundary(new NikaTransportError(this.kind, `HTTP ${
|
|
2111
|
+
rejectBoundary(new NikaTransportError(this.kind, `HTTP ${path4} response body was aborted`));
|
|
1654
2112
|
void reader.cancel().catch(() => {
|
|
1655
2113
|
});
|
|
1656
2114
|
};
|
|
1657
2115
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1658
|
-
const timer = setTimeout(() => {
|
|
2116
|
+
const timer = useRequestTimeout ? setTimeout(() => {
|
|
1659
2117
|
rejectBoundary(new NikaTransportError(
|
|
1660
2118
|
this.kind,
|
|
1661
2119
|
`HTTP response body timed out after ${this.options.requestTimeout}ms`
|
|
1662
2120
|
));
|
|
1663
2121
|
void reader.cancel().catch(() => {
|
|
1664
2122
|
});
|
|
1665
|
-
}, this.options.requestTimeout);
|
|
2123
|
+
}, this.options.requestTimeout) : void 0;
|
|
1666
2124
|
try {
|
|
1667
2125
|
if (signal?.aborted) abort();
|
|
1668
2126
|
while (true) {
|
|
1669
2127
|
const { done, value: value2 } = await Promise.race([reader.read(), boundary]);
|
|
1670
2128
|
if (done) break;
|
|
1671
2129
|
bytes += value2.byteLength;
|
|
1672
|
-
if (bytes >
|
|
2130
|
+
if (bytes > maxBytes) {
|
|
1673
2131
|
throw new NikaProtocolError(
|
|
1674
2132
|
this.kind,
|
|
1675
|
-
`HTTP ${
|
|
2133
|
+
`HTTP ${path4} JSON exceeded ${maxBytes} bytes`
|
|
1676
2134
|
);
|
|
1677
2135
|
}
|
|
1678
2136
|
chunks.push(value2);
|
|
1679
2137
|
}
|
|
1680
2138
|
} catch (cause) {
|
|
1681
2139
|
if (cause instanceof NikaProtocolError) throw cause;
|
|
1682
|
-
throw new NikaTransportError(this.kind, `HTTP ${
|
|
2140
|
+
throw new NikaTransportError(this.kind, `HTTP ${path4} response body reset`);
|
|
1683
2141
|
} finally {
|
|
1684
|
-
clearTimeout(timer);
|
|
2142
|
+
if (timer) clearTimeout(timer);
|
|
1685
2143
|
signal?.removeEventListener("abort", abort);
|
|
1686
2144
|
await reader.cancel().catch(() => {
|
|
1687
2145
|
});
|
|
@@ -1697,7 +2155,7 @@ var HttpTransport = class {
|
|
|
1697
2155
|
try {
|
|
1698
2156
|
text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
1699
2157
|
} catch (cause) {
|
|
1700
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2158
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} JSON was not valid UTF-8`, {
|
|
1701
2159
|
cause: cause instanceof Error ? cause : void 0
|
|
1702
2160
|
});
|
|
1703
2161
|
}
|
|
@@ -1705,15 +2163,15 @@ var HttpTransport = class {
|
|
|
1705
2163
|
try {
|
|
1706
2164
|
value = JSON.parse(text);
|
|
1707
2165
|
} catch (cause) {
|
|
1708
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2166
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return valid JSON`, {
|
|
1709
2167
|
cause: cause instanceof Error ? cause : void 0
|
|
1710
2168
|
});
|
|
1711
2169
|
}
|
|
1712
2170
|
const object = machineObject(value);
|
|
1713
|
-
if (!object) throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2171
|
+
if (!object) throw new NikaProtocolError(this.kind, `HTTP ${path4} did not return an object`);
|
|
1714
2172
|
return object;
|
|
1715
2173
|
}
|
|
1716
|
-
async observationRequest(
|
|
2174
|
+
async observationRequest(path4, init, withTimeout = true) {
|
|
1717
2175
|
const controller = withTimeout ? new AbortController() : void 0;
|
|
1718
2176
|
const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
|
|
1719
2177
|
const callerSignal = init.signal;
|
|
@@ -1722,7 +2180,7 @@ var HttpTransport = class {
|
|
|
1722
2180
|
const headers = new Headers(init.headers);
|
|
1723
2181
|
headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
1724
2182
|
try {
|
|
1725
|
-
return await this.options.fetch(`${this.options.url}${
|
|
2183
|
+
return await this.options.fetch(`${this.options.url}${path4}`, {
|
|
1726
2184
|
...init,
|
|
1727
2185
|
headers,
|
|
1728
2186
|
signal: controller?.signal ?? callerSignal
|
|
@@ -1781,8 +2239,18 @@ var HttpTransport = class {
|
|
|
1781
2239
|
this.serverIdentity ??= this.probeServerIdentity();
|
|
1782
2240
|
return this.serverIdentity;
|
|
1783
2241
|
}
|
|
1784
|
-
async probeServerIdentity() {
|
|
1785
|
-
const
|
|
2242
|
+
async probeServerIdentity(signal) {
|
|
2243
|
+
const outcome = await this.jsonOutcome(
|
|
2244
|
+
"/health",
|
|
2245
|
+
{ method: "GET", signal },
|
|
2246
|
+
false,
|
|
2247
|
+
[200],
|
|
2248
|
+
void 0,
|
|
2249
|
+
false,
|
|
2250
|
+
signal === void 0
|
|
2251
|
+
);
|
|
2252
|
+
if ("refusal" in outcome) throw this.refused("/health", outcome);
|
|
2253
|
+
const health = outcome.object;
|
|
1786
2254
|
if (health.status !== "ok" || health.service !== "nika-serve") {
|
|
1787
2255
|
throw new NikaCompatibilityError(
|
|
1788
2256
|
"engineIdentity",
|
|
@@ -1920,9 +2388,9 @@ var HttpTransport = class {
|
|
|
1920
2388
|
delete report.execution_snapshot;
|
|
1921
2389
|
return report;
|
|
1922
2390
|
}
|
|
1923
|
-
async json(
|
|
2391
|
+
async json(path4, init, authenticated = true, acceptedStatuses = [200], operation) {
|
|
1924
2392
|
const { object } = await this.jsonWithStatus(
|
|
1925
|
-
|
|
2393
|
+
path4,
|
|
1926
2394
|
init,
|
|
1927
2395
|
authenticated,
|
|
1928
2396
|
acceptedStatuses,
|
|
@@ -1935,48 +2403,54 @@ var HttpTransport = class {
|
|
|
1935
2403
|
* routes whose accepted statuses mean different things (cancel: 200 is a
|
|
1936
2404
|
* settled job, 202 an accepted request).
|
|
1937
2405
|
*/
|
|
1938
|
-
async jsonWithStatus(
|
|
1939
|
-
const outcome = await this.jsonOutcome(
|
|
1940
|
-
if ("refusal" in outcome) throw this.refused(
|
|
2406
|
+
async jsonWithStatus(path4, init, authenticated, acceptedStatuses, operation) {
|
|
2407
|
+
const outcome = await this.jsonOutcome(path4, init, authenticated, acceptedStatuses, operation);
|
|
2408
|
+
if ("refusal" in outcome) throw this.refused(path4, outcome);
|
|
1941
2409
|
return outcome;
|
|
1942
2410
|
}
|
|
1943
|
-
async jsonOutcome(
|
|
1944
|
-
const response = await this.fetchResponse(
|
|
2411
|
+
async jsonOutcome(path4, init, authenticated, acceptedStatuses, operation, strictRefusal = false, useRequestTimeout = true) {
|
|
2412
|
+
const response = await this.fetchResponse(path4, init, useRequestTimeout, authenticated, false);
|
|
1945
2413
|
if (!acceptedStatuses.includes(response.status)) {
|
|
1946
2414
|
if (response.ok) {
|
|
1947
2415
|
await discardResponse(response);
|
|
1948
2416
|
throw new NikaProtocolError(
|
|
1949
2417
|
this.kind,
|
|
1950
|
-
`HTTP ${
|
|
2418
|
+
`HTTP ${path4} returned non-contract status ${response.status}`
|
|
1951
2419
|
);
|
|
1952
2420
|
}
|
|
1953
|
-
const refusal = operation === void 0 ? void 0 : await this.readRefusal(response,
|
|
2421
|
+
const refusal = operation === void 0 ? void 0 : await this.readRefusal(response, path4, strictRefusal);
|
|
1954
2422
|
if (operation !== void 0 && refusal) {
|
|
1955
2423
|
return { operation, status: response.status, refusal };
|
|
1956
2424
|
}
|
|
1957
2425
|
await discardResponse(response);
|
|
1958
2426
|
throw new NikaTransportError(
|
|
1959
2427
|
this.kind,
|
|
1960
|
-
`HTTP ${response.status} for ${
|
|
2428
|
+
`HTTP ${response.status} for ${path4}: [REDACTED]`
|
|
1961
2429
|
);
|
|
1962
2430
|
}
|
|
1963
2431
|
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1964
2432
|
if (contentType !== "application/json") {
|
|
1965
2433
|
await discardResponse(response);
|
|
1966
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2434
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
|
|
1967
2435
|
}
|
|
1968
2436
|
return {
|
|
1969
|
-
object: await this.readObservationObject(
|
|
2437
|
+
object: await this.readObservationObject(
|
|
2438
|
+
response,
|
|
2439
|
+
path4,
|
|
2440
|
+
init.signal ?? void 0,
|
|
2441
|
+
this.options.machineBufferBytes,
|
|
2442
|
+
useRequestTimeout
|
|
2443
|
+
),
|
|
1970
2444
|
status: response.status
|
|
1971
2445
|
};
|
|
1972
2446
|
}
|
|
1973
|
-
refused(
|
|
2447
|
+
refused(path4, outcome) {
|
|
1974
2448
|
const { code, message } = outcome.refusal;
|
|
1975
2449
|
return new NikaOperationError(
|
|
1976
2450
|
outcome.operation,
|
|
1977
2451
|
this.kind,
|
|
1978
2452
|
code,
|
|
1979
|
-
`HTTP ${outcome.status} for ${
|
|
2453
|
+
`HTTP ${outcome.status} for ${path4}: ${code}` + (message ? ` (${message})` : ""),
|
|
1980
2454
|
{ status: outcome.status, machineCode: code }
|
|
1981
2455
|
);
|
|
1982
2456
|
}
|
|
@@ -1986,7 +2460,7 @@ var HttpTransport = class {
|
|
|
1986
2460
|
* plain identifier, a code that carries the bearer token) is discarded and
|
|
1987
2461
|
* the caller falls back to the redacted transport error.
|
|
1988
2462
|
*/
|
|
1989
|
-
async readRefusal(response,
|
|
2463
|
+
async readRefusal(response, path4, strict = false) {
|
|
1990
2464
|
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1991
2465
|
if (contentType !== "application/json") {
|
|
1992
2466
|
await discardResponse(response);
|
|
@@ -1994,7 +2468,7 @@ var HttpTransport = class {
|
|
|
1994
2468
|
}
|
|
1995
2469
|
let object;
|
|
1996
2470
|
try {
|
|
1997
|
-
object = await this.readObservationObject(response,
|
|
2471
|
+
object = await this.readObservationObject(response, path4);
|
|
1998
2472
|
} catch {
|
|
1999
2473
|
return void 0;
|
|
2000
2474
|
}
|
|
@@ -2012,14 +2486,14 @@ var HttpTransport = class {
|
|
|
2012
2486
|
const clean = text.replace(/[\u0000-\u001f\u007f]+/g, " ").split(this.options.token).join("[REDACTED]");
|
|
2013
2487
|
return clean.length > 240 ? `${clean.slice(0, 240)}...` : clean;
|
|
2014
2488
|
}
|
|
2015
|
-
async operationJson(operation,
|
|
2016
|
-
const response = await this.fetchResponse(
|
|
2489
|
+
async operationJson(operation, path4, init) {
|
|
2490
|
+
const response = await this.fetchResponse(path4, init, true, true, false);
|
|
2017
2491
|
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
2018
2492
|
if (contentType !== "application/json") {
|
|
2019
2493
|
await discardResponse(response);
|
|
2020
|
-
throw new NikaProtocolError(this.kind, `HTTP ${
|
|
2494
|
+
throw new NikaProtocolError(this.kind, `HTTP ${path4} returned an invalid content-type`);
|
|
2021
2495
|
}
|
|
2022
|
-
const object = await this.readObservationObject(response,
|
|
2496
|
+
const object = await this.readObservationObject(response, path4);
|
|
2023
2497
|
if (!response.ok) throw this.operationFailure(operation, response.status, object);
|
|
2024
2498
|
return object;
|
|
2025
2499
|
}
|
|
@@ -2064,16 +2538,17 @@ var HttpTransport = class {
|
|
|
2064
2538
|
{ status, machineCode: machine.code }
|
|
2065
2539
|
);
|
|
2066
2540
|
}
|
|
2067
|
-
async fetchResponse(
|
|
2541
|
+
async fetchResponse(path4, init, withTimeout, authenticated = true, requireOk = true) {
|
|
2068
2542
|
const controller = withTimeout ? new AbortController() : void 0;
|
|
2069
2543
|
const timer = controller ? setTimeout(() => controller.abort(), this.options.requestTimeout) : void 0;
|
|
2070
2544
|
const callerSignal = init.signal;
|
|
2071
2545
|
const abort = () => controller?.abort(callerSignal?.reason);
|
|
2072
2546
|
callerSignal?.addEventListener("abort", abort, { once: true });
|
|
2547
|
+
if (callerSignal?.aborted) abort();
|
|
2073
2548
|
const headers = new Headers(init.headers);
|
|
2074
2549
|
if (authenticated) headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
2075
2550
|
try {
|
|
2076
|
-
const response = await this.options.fetch(`${this.options.url}${
|
|
2551
|
+
const response = await this.options.fetch(`${this.options.url}${path4}`, {
|
|
2077
2552
|
...init,
|
|
2078
2553
|
headers,
|
|
2079
2554
|
signal: controller?.signal ?? callerSignal
|
|
@@ -2082,7 +2557,7 @@ var HttpTransport = class {
|
|
|
2082
2557
|
await discardResponse(response);
|
|
2083
2558
|
throw new NikaTransportError(
|
|
2084
2559
|
this.kind,
|
|
2085
|
-
`HTTP ${response.status} for ${
|
|
2560
|
+
`HTTP ${response.status} for ${path4}: [REDACTED]`
|
|
2086
2561
|
);
|
|
2087
2562
|
}
|
|
2088
2563
|
return response;
|
|
@@ -2528,15 +3003,65 @@ var NativeProcessTransport = class {
|
|
|
2528
3003
|
const captured = await this.capture(args, options.signal);
|
|
2529
3004
|
const report = reportObject(captured.stdout) ?? reportObject(stripEnginePrefix(captured.stderr));
|
|
2530
3005
|
if (!report) {
|
|
2531
|
-
const
|
|
3006
|
+
const excerpt2 = diagnosticExcerpt(captured.stderr);
|
|
2532
3007
|
throw new NikaCompatibilityError(
|
|
2533
3008
|
"check",
|
|
2534
3009
|
this.kind,
|
|
2535
|
-
`This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (
|
|
3010
|
+
`This engine did not emit the required JSON check report (exit ${captured.exitCode})` + (excerpt2 ? `: ${excerpt2}` : "")
|
|
2536
3011
|
);
|
|
2537
3012
|
}
|
|
2538
3013
|
return { ...report, exitCode: captured.exitCode };
|
|
2539
3014
|
}
|
|
3015
|
+
/**
|
|
3016
|
+
* Authoring, not execution (issue #128): one bounded `nika compile --json`
|
|
3017
|
+
* invocation, projected verbatim. The engine must advertise the compile
|
|
3018
|
+
* capability — an engine from before the door is refused here, before any
|
|
3019
|
+
* spawn, and there is never a TypeScript fallback. No destination is passed:
|
|
3020
|
+
* the candidate stays source in memory, nothing is written, and no Run
|
|
3021
|
+
* exists; `signal`/`timeoutMs` only stop this one child.
|
|
3022
|
+
*/
|
|
3023
|
+
async compile(request, options) {
|
|
3024
|
+
const composed = compileSignal(options);
|
|
3025
|
+
let invocation;
|
|
3026
|
+
try {
|
|
3027
|
+
if (composed.signal?.aborted) {
|
|
3028
|
+
throw new NikaTransportError(this.kind, "compile aborted");
|
|
3029
|
+
}
|
|
3030
|
+
invocation = await compileArgv(request);
|
|
3031
|
+
const identity = composed.signal ? await verifyNikaEngine(this.options.engine, {
|
|
3032
|
+
signal: composed.signal,
|
|
3033
|
+
killGraceMs: COMPILE_KILL_GRACE_MS
|
|
3034
|
+
}) : await this.ensureReady();
|
|
3035
|
+
if (!identity.supportedCapabilities.includes(COMPILE_CAPABILITY)) {
|
|
3036
|
+
throw new NikaCompatibilityError(
|
|
3037
|
+
COMPILE_CAPABILITY,
|
|
3038
|
+
this.kind,
|
|
3039
|
+
`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`
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
const captured = await captureEngine(this.options.engine.bin, invocation.args, {
|
|
3043
|
+
cwd: this.options.cwd,
|
|
3044
|
+
signal: composed.signal,
|
|
3045
|
+
bufferBytes: COMPILE_RESPONSE_MAX_BYTES,
|
|
3046
|
+
transport: this.kind,
|
|
3047
|
+
label: "compile",
|
|
3048
|
+
killGraceMs: COMPILE_KILL_GRACE_MS
|
|
3049
|
+
});
|
|
3050
|
+
return compileOutcomeFrom(captured, this.kind, this.options.engine.bin);
|
|
3051
|
+
} catch (cause) {
|
|
3052
|
+
if (composed.signal?.aborted && cause instanceof NikaTransportError) {
|
|
3053
|
+
throw new NikaTransportError(
|
|
3054
|
+
this.kind,
|
|
3055
|
+
composed.timedOut() ? `compile timed out after ${options.timeoutMs} ms` : "compile aborted by caller",
|
|
3056
|
+
{ cause }
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
throw cause;
|
|
3060
|
+
} finally {
|
|
3061
|
+
composed.dispose();
|
|
3062
|
+
if (invocation) await removeCompileScratch(invocation);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
2540
3065
|
async startRun(workflow, options) {
|
|
2541
3066
|
const inputs = literalInputs(options);
|
|
2542
3067
|
const identity = await this.ensureReady();
|
|
@@ -2977,8 +3502,8 @@ function quotedProtocolError(cause, text, kind) {
|
|
|
2977
3502
|
);
|
|
2978
3503
|
}
|
|
2979
3504
|
function quoted(verdict, text) {
|
|
2980
|
-
const
|
|
2981
|
-
return
|
|
3505
|
+
const excerpt2 = diagnosticExcerpt(text);
|
|
3506
|
+
return excerpt2 ? `${verdict}: ${excerpt2}` : verdict;
|
|
2982
3507
|
}
|
|
2983
3508
|
function runFlags(options, literal) {
|
|
2984
3509
|
const flags = [];
|
|
@@ -3373,6 +3898,26 @@ var Nika = class {
|
|
|
3373
3898
|
check(workflow, options = {}) {
|
|
3374
3899
|
return this.transport.check(workflowName(workflow), options);
|
|
3375
3900
|
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Authoring (issue #128): describe work — or name an accepted workflow plus
|
|
3903
|
+
* a change — and get the engine's checked candidate back, without running
|
|
3904
|
+
* anything. `compile()` never means `run()`: the outcome is data (candidate
|
|
3905
|
+
* source, questions, diagnostics, requested boundary, source-only Check
|
|
3906
|
+
* preview, provenance), `incomplete`/`refused` resolve instead of throwing,
|
|
3907
|
+
* and no workflow effect, approval or Proof exists.
|
|
3908
|
+
*
|
|
3909
|
+
* The candidate is ordinary `.nika` SOURCE. `run()` consumes a path,
|
|
3910
|
+
* so the caller materializes the candidate and `run(path)` re-admits it —
|
|
3911
|
+
* the compile preview is a review, never admission. Over `{ url }` this calls
|
|
3912
|
+
* the authenticated Serve compile door, gated by its advertised capability;
|
|
3913
|
+
* the SDK never compiles locally as a fallback.
|
|
3914
|
+
*/
|
|
3915
|
+
async compile(request, options = {}) {
|
|
3916
|
+
return this.transport.compile(
|
|
3917
|
+
normalizeCompileRequest(request),
|
|
3918
|
+
normalizeCompileOptions(options)
|
|
3919
|
+
);
|
|
3920
|
+
}
|
|
3376
3921
|
/**
|
|
3377
3922
|
* Resolves with the run's handle once the engine admitted it, and rejects
|
|
3378
3923
|
* without one when the engine refused it. The handle owns the lifecycle:
|