@kici-dev/agent 0.1.22 → 0.1.24
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/dist/bootstrap/api-intercept.d.ts +20 -0
- package/dist/bootstrap/ensure-init-runner.d.ts +39 -0
- package/dist/bootstrap/pre-boot-send.d.ts +25 -0
- package/dist/bootstrap/reach.d.ts +17 -0
- package/dist/bootstrap/ssh-exec.d.ts +58 -0
- package/dist/execution/cache/cache-phase.d.ts +21 -4
- package/dist/execution/cache/index.d.ts +1 -1
- package/dist/execution/init-runner.d.ts +2 -1
- package/dist/execution/job-runner.d.ts +12 -0
- package/dist/execution/rule-evaluator.d.ts +4 -2
- package/dist/execution/sandbox/capture-context.d.ts +11 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +31 -3
- package/dist/execution/sandbox/parallel-scheduler.d.ts +34 -0
- package/dist/execution/sandbox/step-loop.d.ts +96 -17
- package/dist/execution/sandbox/step-task-registry.d.ts +26 -0
- package/dist/execution/sandbox/types.d.ts +2 -2
- package/dist/execution/sandbox/workflow-runner.d.ts +29 -2
- package/dist/server.js +433 -48
- package/dist/workflow-runner.js +499 -125
- package/package.json +6 -6
- package/sbom.spdx.json +68 -68
package/dist/server.js
CHANGED
|
@@ -25,7 +25,7 @@ import { format, promisify } from "node:util";
|
|
|
25
25
|
import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
|
|
26
26
|
import fs$1, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
|
|
27
27
|
import Docker from "dockerode";
|
|
28
|
-
import { buildKiciApi, buildNeedsContext, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
|
|
28
|
+
import { buildKiciApi, buildNeedsContext, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject } from "@kici-dev/sdk";
|
|
29
29
|
import { c, x } from "tar";
|
|
30
30
|
import https from "node:https";
|
|
31
31
|
import http from "node:http";
|
|
@@ -1310,14 +1310,14 @@ var init_console_capture = __esmMin((() => {
|
|
|
1310
1310
|
init_console_capture();
|
|
1311
1311
|
function safe(name, fallback = "unknown") {
|
|
1312
1312
|
switch (name) {
|
|
1313
|
-
case "version": return "0.1.
|
|
1314
|
-
case "buildCommit": return "
|
|
1315
|
-
case "sdkVersion": return "0.1.
|
|
1316
|
-
case "sdkBundleHash": return "
|
|
1317
|
-
case "sharedVersion": return "0.1.
|
|
1318
|
-
case "sharedBundleHash": return "
|
|
1319
|
-
case "engineVersion": return "0.1.
|
|
1320
|
-
case "engineBundleHash": return "
|
|
1313
|
+
case "version": return "0.1.24";
|
|
1314
|
+
case "buildCommit": return "73592f67f";
|
|
1315
|
+
case "sdkVersion": return "0.1.24";
|
|
1316
|
+
case "sdkBundleHash": return "031712cf8cd02f483365113aad7e946da982110d544b177eb05edcc0d4421253";
|
|
1317
|
+
case "sharedVersion": return "0.1.24";
|
|
1318
|
+
case "sharedBundleHash": return "b977224129c767c4851458a795fa470264b2fc51255baf06cf14640e29e2f44c";
|
|
1319
|
+
case "engineVersion": return "0.1.24";
|
|
1320
|
+
case "engineBundleHash": return "734acca885cd70eed07a1a9426b08c04c07a9bf99484c18100d3d797b3eb8f39";
|
|
1321
1321
|
default: return fallback;
|
|
1322
1322
|
}
|
|
1323
1323
|
}
|
|
@@ -2080,8 +2080,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
|
|
|
2080
2080
|
}
|
|
2081
2081
|
var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
|
|
2082
2082
|
var init_workflow_loader = __esmMin((() => {
|
|
2083
|
-
AGENT_SDK_VERSION = "0.1.
|
|
2084
|
-
AGENT_SDK_BUNDLE_HASH = "
|
|
2083
|
+
AGENT_SDK_VERSION = "0.1.24";
|
|
2084
|
+
AGENT_SDK_BUNDLE_HASH = "031712cf8cd02f483365113aad7e946da982110d544b177eb05edcc0d4421253";
|
|
2085
2085
|
hookRegistered = false;
|
|
2086
2086
|
}));
|
|
2087
2087
|
//#endregion
|
|
@@ -2548,9 +2548,16 @@ async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs
|
|
|
2548
2548
|
if (job.include || job.exclude) combos = applyIncludeExclude(combos, job.include, job.exclude);
|
|
2549
2549
|
result.matrixValues = combos;
|
|
2550
2550
|
}
|
|
2551
|
-
if (flags.dynamicEnvironment
|
|
2552
|
-
const
|
|
2553
|
-
if (
|
|
2551
|
+
if (flags.dynamicEnvironment) {
|
|
2552
|
+
const envRefs = job.environments ?? (job.environment !== void 0 ? [job.environment] : void 0);
|
|
2553
|
+
if (envRefs && envRefs.length > 0) {
|
|
2554
|
+
const names = [];
|
|
2555
|
+
for (const ref of envRefs) if (typeof ref === "function") {
|
|
2556
|
+
const value = await withTimeout(() => ref(event), timeoutMs, `dynamicEnvironment for job '${jobName}'`);
|
|
2557
|
+
if (value !== void 0 && value !== null) names.push(value);
|
|
2558
|
+
} else if (typeof ref === "string") names.push(ref);
|
|
2559
|
+
if (names.length > 0) result.environmentNames = names;
|
|
2560
|
+
}
|
|
2554
2561
|
}
|
|
2555
2562
|
if (flags.dynamicEnv && typeof job.env === "function") {
|
|
2556
2563
|
const value = await withTimeout(() => job.env(event), timeoutMs, `dynamicEnv for job '${jobName}'`);
|
|
@@ -2566,6 +2573,274 @@ var init_init_runner = __esmMin((() => {
|
|
|
2566
2573
|
init_timeout_util();
|
|
2567
2574
|
}));
|
|
2568
2575
|
//#endregion
|
|
2576
|
+
//#region src/bootstrap/ssh-exec.ts
|
|
2577
|
+
/**
|
|
2578
|
+
* Agent-side SSH helper for bootstrap bring-up.
|
|
2579
|
+
*
|
|
2580
|
+
* Lifts the discipline from `infra/prod/hw/ssh.sh` + the deploy-prod
|
|
2581
|
+
* `runOnBox` helper into the agent: the bring-up private key is piped into an
|
|
2582
|
+
* **ephemeral ssh-agent** (`ssh-add -` reads it from stdin) and the agent is
|
|
2583
|
+
* torn down in a `finally`, so the key never lands on disk and never enters a
|
|
2584
|
+
* long-lived process environment. The agent only holds the key for the
|
|
2585
|
+
* lifetime of the one `ssh` / `scp` invocation.
|
|
2586
|
+
*
|
|
2587
|
+
* `sshExec` runs a remote command; `sshPush` ships local bytes to a remote
|
|
2588
|
+
* path (`ssh 'cat > path'`). Both accept a resolved private key string
|
|
2589
|
+
* (supplied by the caller — the orchestrator resolves the scoped secret and
|
|
2590
|
+
* hands the key down). `sshExec` supports `{ stdin, port, hostKeyMode }` so it
|
|
2591
|
+
* can also drive a pre-boot dropbear / initramfs prompt (a forced-command
|
|
2592
|
+
* endpoint such as `cryptroot-unlock` on port 2222, which accepts the unlock
|
|
2593
|
+
* input on stdin and uses a transient host key distinct from the OS sshd key).
|
|
2594
|
+
*/
|
|
2595
|
+
/** Common `-o` flags every bootstrap SSH connection carries. */
|
|
2596
|
+
function baseSshOptions(hostKeyMode) {
|
|
2597
|
+
return [
|
|
2598
|
+
"-o",
|
|
2599
|
+
`StrictHostKeyChecking=${HOST_KEY_FLAG[hostKeyMode]}`,
|
|
2600
|
+
"-o",
|
|
2601
|
+
"ConnectTimeout=10",
|
|
2602
|
+
"-o",
|
|
2603
|
+
"BatchMode=yes"
|
|
2604
|
+
];
|
|
2605
|
+
}
|
|
2606
|
+
/** Resolve the `user@address` target + port from reach metadata. */
|
|
2607
|
+
function resolveTarget(reach, portOverride) {
|
|
2608
|
+
if (!reach.address) throw new Error(`host ${reach.agentId} has no SSH reach address declared`);
|
|
2609
|
+
const user = reach.sshUser ?? SSH_USER_DEFAULT;
|
|
2610
|
+
const port = portOverride ?? reach.sshPort ?? SSH_PORT_DEFAULT;
|
|
2611
|
+
return {
|
|
2612
|
+
dest: `${user}@${reach.address}`,
|
|
2613
|
+
port
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Run a command on the target over SSH using an ephemeral, in-memory key.
|
|
2618
|
+
*
|
|
2619
|
+
* The key is loaded into a per-call ssh-agent (never written to disk) and the
|
|
2620
|
+
* agent is killed in `finally`. The remote command's exit code / stdout /
|
|
2621
|
+
* stderr are returned verbatim — a non-zero exit is reported, not swallowed
|
|
2622
|
+
* (a pre-boot unlock legitimately drops the session, so the caller decides
|
|
2623
|
+
* what a "success" looks like).
|
|
2624
|
+
*/
|
|
2625
|
+
async function sshExec(reach, privateKey, command, opts = {}, deps = {}) {
|
|
2626
|
+
const spawnFn = deps.spawnFn ?? defaultSpawn;
|
|
2627
|
+
const hostKeyMode = opts.hostKeyMode ?? "accept-new";
|
|
2628
|
+
const { dest, port } = resolveTarget(reach, opts.port);
|
|
2629
|
+
return withEphemeralAgent(privateKey, spawnFn, async (env) => {
|
|
2630
|
+
return spawnFn("ssh", [
|
|
2631
|
+
...baseSshOptions(hostKeyMode),
|
|
2632
|
+
"-p",
|
|
2633
|
+
String(port),
|
|
2634
|
+
dest,
|
|
2635
|
+
command
|
|
2636
|
+
], {
|
|
2637
|
+
env,
|
|
2638
|
+
stdin: opts.stdin
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Ship local bytes to a remote path over SSH (`ssh 'cat > path'`), using the
|
|
2644
|
+
* same ephemeral-key discipline. Throws on a non-zero exit (a push must
|
|
2645
|
+
* succeed end-to-end, unlike a pre-boot unlock).
|
|
2646
|
+
*/
|
|
2647
|
+
async function sshPush(reach, privateKey, localBytes, remotePath, opts = {}, deps = {}) {
|
|
2648
|
+
const spawnFn = deps.spawnFn ?? defaultSpawn;
|
|
2649
|
+
const hostKeyMode = opts.hostKeyMode ?? "accept-new";
|
|
2650
|
+
const { dest, port } = resolveTarget(reach, opts.port);
|
|
2651
|
+
const result = await withEphemeralAgent(privateKey, spawnFn, async (env) => {
|
|
2652
|
+
return spawnFn("ssh", [
|
|
2653
|
+
...baseSshOptions(hostKeyMode),
|
|
2654
|
+
"-p",
|
|
2655
|
+
String(port),
|
|
2656
|
+
dest,
|
|
2657
|
+
`cat > '${remotePath.replace(/'/g, `'\\''`)}'`
|
|
2658
|
+
], {
|
|
2659
|
+
env,
|
|
2660
|
+
stdin: localBytes
|
|
2661
|
+
});
|
|
2662
|
+
});
|
|
2663
|
+
if (result.exitCode !== 0) throw new Error(`sshPush(${reach.agentId}:${remotePath}): exit ${result.exitCode}${result.stderr ? `\n${result.stderr}` : ""}`);
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Start a per-call ephemeral ssh-agent, load the key via stdin (never a file),
|
|
2667
|
+
* run `body` with `SSH_AUTH_SOCK` in env, and kill the agent in `finally`.
|
|
2668
|
+
*/
|
|
2669
|
+
async function withEphemeralAgent(privateKey, spawnFn, body) {
|
|
2670
|
+
const baseEnv = { ...process.env };
|
|
2671
|
+
const start = await spawnFn("ssh-agent", ["-s"], { env: baseEnv });
|
|
2672
|
+
if (start.exitCode !== 0) throw new Error(`ssh-agent start failed: exit ${start.exitCode}\n${start.stderr}`);
|
|
2673
|
+
const sock = parseAgentSocket(start.stdout);
|
|
2674
|
+
const pid = parseAgentPid(start.stdout);
|
|
2675
|
+
const agentEnv = {
|
|
2676
|
+
...baseEnv,
|
|
2677
|
+
SSH_AUTH_SOCK: sock,
|
|
2678
|
+
...pid ? { SSH_AGENT_PID: pid } : {},
|
|
2679
|
+
SSH_ASKPASS: "/bin/false",
|
|
2680
|
+
DISPLAY: ""
|
|
2681
|
+
};
|
|
2682
|
+
try {
|
|
2683
|
+
const add = await spawnFn("ssh-add", ["-"], {
|
|
2684
|
+
env: agentEnv,
|
|
2685
|
+
stdin: privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`
|
|
2686
|
+
});
|
|
2687
|
+
if (add.exitCode !== 0) throw new Error(`ssh-add failed: exit ${add.exitCode}\n${add.stderr}`);
|
|
2688
|
+
return await body(agentEnv);
|
|
2689
|
+
} finally {
|
|
2690
|
+
await spawnFn("ssh-agent", ["-k"], { env: agentEnv }).catch(() => {});
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
/** Extract `SSH_AUTH_SOCK=<path>;` from `ssh-agent -s` output. */
|
|
2694
|
+
function parseAgentSocket(out) {
|
|
2695
|
+
const m = out.match(/SSH_AUTH_SOCK=([^;\n]+)/);
|
|
2696
|
+
if (!m) throw new Error("ssh-agent -s did not emit SSH_AUTH_SOCK");
|
|
2697
|
+
return m[1];
|
|
2698
|
+
}
|
|
2699
|
+
/** Extract `SSH_AGENT_PID=<n>;` from `ssh-agent -s` output (best-effort). */
|
|
2700
|
+
function parseAgentPid(out) {
|
|
2701
|
+
return out.match(/SSH_AGENT_PID=([^;\n]+)/)?.[1];
|
|
2702
|
+
}
|
|
2703
|
+
var defaultSpawn, SSH_USER_DEFAULT, SSH_PORT_DEFAULT, HOST_KEY_FLAG;
|
|
2704
|
+
var init_ssh_exec = __esmMin((() => {
|
|
2705
|
+
defaultSpawn = (command, args, opts) => new Promise((resolve, reject) => {
|
|
2706
|
+
const child = spawn(command, args, {
|
|
2707
|
+
env: opts.env,
|
|
2708
|
+
stdio: [
|
|
2709
|
+
opts.stdin !== void 0 ? "pipe" : "ignore",
|
|
2710
|
+
"pipe",
|
|
2711
|
+
"pipe"
|
|
2712
|
+
]
|
|
2713
|
+
});
|
|
2714
|
+
let stdout = "";
|
|
2715
|
+
let stderr = "";
|
|
2716
|
+
child.stdout?.on("data", (b) => {
|
|
2717
|
+
stdout += b.toString();
|
|
2718
|
+
});
|
|
2719
|
+
child.stderr?.on("data", (b) => {
|
|
2720
|
+
stderr += b.toString();
|
|
2721
|
+
});
|
|
2722
|
+
child.on("error", reject);
|
|
2723
|
+
child.on("close", (code) => resolve({
|
|
2724
|
+
exitCode: code ?? -1,
|
|
2725
|
+
stdout,
|
|
2726
|
+
stderr
|
|
2727
|
+
}));
|
|
2728
|
+
if (opts.stdin !== void 0) child.stdin?.end(opts.stdin);
|
|
2729
|
+
});
|
|
2730
|
+
SSH_USER_DEFAULT = "root";
|
|
2731
|
+
SSH_PORT_DEFAULT = 22;
|
|
2732
|
+
HOST_KEY_FLAG = {
|
|
2733
|
+
"accept-new": "accept-new",
|
|
2734
|
+
strict: "yes"
|
|
2735
|
+
};
|
|
2736
|
+
}));
|
|
2737
|
+
//#endregion
|
|
2738
|
+
//#region src/bootstrap/ensure-init-runner.ts
|
|
2739
|
+
/**
|
|
2740
|
+
* Build the launcher script that starts the init-runner on the target with its
|
|
2741
|
+
* bootstrap env. Detached (`setsid … &`) so the SSH session can return while
|
|
2742
|
+
* the agent keeps running and dials the orchestrator.
|
|
2743
|
+
*/
|
|
2744
|
+
function buildLauncher(material, agentCommand) {
|
|
2745
|
+
return [
|
|
2746
|
+
"#!/usr/bin/env bash",
|
|
2747
|
+
"set -euo pipefail",
|
|
2748
|
+
`setsid env ${[
|
|
2749
|
+
`KICI_AGENT_TOKEN=${shQuote(material.bootstrapToken)}`,
|
|
2750
|
+
`KICI_AGENT_ID=${shQuote(material.targetAgentId)}`,
|
|
2751
|
+
`KICI_ORCHESTRATOR_URL=${shQuote(material.orchestratorUrl)}`,
|
|
2752
|
+
`KICI_LABELS=${shQuote(material.labels.join(","))}`,
|
|
2753
|
+
"KICI_EXECUTION_MODE=bare-metal",
|
|
2754
|
+
"KICI_PORT=0"
|
|
2755
|
+
].join(" \\\n ")} \\`,
|
|
2756
|
+
` ${agentCommand} >/tmp/kici-init-runner.log 2>&1 &`,
|
|
2757
|
+
"echo \"init-runner started pid=$!\""
|
|
2758
|
+
].join("\n");
|
|
2759
|
+
}
|
|
2760
|
+
/** Single-quote a value for safe embedding in the launcher's env assignment. */
|
|
2761
|
+
function shQuote(v) {
|
|
2762
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
2763
|
+
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Bring up a temporary init-runner on `targetAgentId`. Returns `{ broughtUp }`:
|
|
2766
|
+
* false when the target already had a live agent (the orchestrator no-op'd),
|
|
2767
|
+
* true when this call dropped + started the init-runner.
|
|
2768
|
+
*/
|
|
2769
|
+
async function ensureInitRunner(transport, targetAgentId, deps = {}) {
|
|
2770
|
+
const material = await transport("kici.ensureInitRunner", { targetAgentId });
|
|
2771
|
+
if (!material.broughtUp) return { broughtUp: false };
|
|
2772
|
+
const { reach, privateKey, bootstrapToken, orchestratorUrl, labels } = material;
|
|
2773
|
+
if (!reach || !privateKey || !bootstrapToken || !orchestratorUrl || !labels) throw new Error(`orchestrator returned incomplete bring-up material for ${targetAgentId}`);
|
|
2774
|
+
const agentCommand = deps.agentCommand ?? DEFAULT_AGENT_COMMAND;
|
|
2775
|
+
await sshPush(reach, privateKey, buildLauncher({
|
|
2776
|
+
bootstrapToken,
|
|
2777
|
+
targetAgentId,
|
|
2778
|
+
orchestratorUrl,
|
|
2779
|
+
labels
|
|
2780
|
+
}, agentCommand), LAUNCHER_REMOTE_PATH, {}, deps);
|
|
2781
|
+
const run = await sshExec(reach, privateKey, `chmod 0700 ${LAUNCHER_REMOTE_PATH} && ${LAUNCHER_REMOTE_PATH}`, {}, deps);
|
|
2782
|
+
if (run.exitCode !== 0) throw new Error(`init-runner launch on ${targetAgentId} failed: exit ${run.exitCode}${run.stderr ? `\n${run.stderr}` : ""}`);
|
|
2783
|
+
return { broughtUp: true };
|
|
2784
|
+
}
|
|
2785
|
+
var DEFAULT_AGENT_COMMAND, LAUNCHER_REMOTE_PATH;
|
|
2786
|
+
var init_ensure_init_runner = __esmMin((() => {
|
|
2787
|
+
init_ssh_exec();
|
|
2788
|
+
DEFAULT_AGENT_COMMAND = "kici-agent";
|
|
2789
|
+
LAUNCHER_REMOTE_PATH = "/tmp/kici-init-runner.sh";
|
|
2790
|
+
}));
|
|
2791
|
+
//#endregion
|
|
2792
|
+
//#region src/bootstrap/pre-boot-send.ts
|
|
2793
|
+
/**
|
|
2794
|
+
* Ship a pre-boot input to the target's dropbear/initramfs SSH channel. The
|
|
2795
|
+
* input plaintext is resolved server-side and never logged. Resolves once the
|
|
2796
|
+
* send completes (the SSH session legitimately drops as the box boots).
|
|
2797
|
+
*/
|
|
2798
|
+
async function preBootSend(transport, targetAgentId, opts, deps = {}) {
|
|
2799
|
+
const material = await transport("kici.preBootSend", {
|
|
2800
|
+
targetAgentId,
|
|
2801
|
+
inputSecret: opts.inputSecret,
|
|
2802
|
+
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
2803
|
+
...opts.command !== void 0 ? { command: opts.command } : {}
|
|
2804
|
+
});
|
|
2805
|
+
await sshExec(material.reach, material.privateKey, material.command, {
|
|
2806
|
+
stdin: material.input,
|
|
2807
|
+
port: material.port,
|
|
2808
|
+
hostKeyMode: "accept-new"
|
|
2809
|
+
}, deps);
|
|
2810
|
+
}
|
|
2811
|
+
var init_pre_boot_send = __esmMin((() => {
|
|
2812
|
+
init_ssh_exec();
|
|
2813
|
+
}));
|
|
2814
|
+
//#endregion
|
|
2815
|
+
//#region src/bootstrap/api-intercept.ts
|
|
2816
|
+
/**
|
|
2817
|
+
* Wrap the orchestrator API transport so the two bootstrap methods are handled
|
|
2818
|
+
* in-process (SSH transport here; privileged resolve relayed to the
|
|
2819
|
+
* orchestrator). `relay` is the raw orchestrator transport (the WS
|
|
2820
|
+
* `sendApiRequest`).
|
|
2821
|
+
*/
|
|
2822
|
+
function withBootstrapInterception(relay, deps = {}) {
|
|
2823
|
+
return async (method, params = {}) => {
|
|
2824
|
+
if (method === ENSURE_INIT_RUNNER) return ensureInitRunner(relay, String(params.targetAgentId ?? ""), deps);
|
|
2825
|
+
if (method === PRE_BOOT_SEND) {
|
|
2826
|
+
await preBootSend(relay, String(params.targetAgentId ?? ""), {
|
|
2827
|
+
inputSecret: String(params.inputSecret ?? ""),
|
|
2828
|
+
...typeof params.port === "number" ? { port: params.port } : {},
|
|
2829
|
+
...typeof params.command === "string" ? { command: params.command } : {}
|
|
2830
|
+
}, deps);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
return relay(method, params);
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
var ENSURE_INIT_RUNNER, PRE_BOOT_SEND;
|
|
2837
|
+
var init_api_intercept = __esmMin((() => {
|
|
2838
|
+
init_ensure_init_runner();
|
|
2839
|
+
init_pre_boot_send();
|
|
2840
|
+
ENSURE_INIT_RUNNER = "kici.ensureInitRunner";
|
|
2841
|
+
PRE_BOOT_SEND = "kici.preBootSend";
|
|
2842
|
+
}));
|
|
2843
|
+
//#endregion
|
|
2569
2844
|
//#region src/execution/dynamic-job-serializer.ts
|
|
2570
2845
|
/**
|
|
2571
2846
|
* Convert an array of SDK Job objects into LockJob format for the orchestrator.
|
|
@@ -2589,11 +2864,22 @@ async function serializeJobsToLock(jobs, ctx, staticNames, allowedGroups) {
|
|
|
2589
2864
|
}
|
|
2590
2865
|
async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups) {
|
|
2591
2866
|
const { include: runsOn, exclude: excludeLabels } = normalizeRunsOnToMatchers(job.runsOn, `generated job '${job.name}' runsOn`);
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2867
|
+
const envRefs = job.environments ?? (job.environment !== void 0 ? [job.environment] : void 0);
|
|
2868
|
+
let resolvedEnvironments;
|
|
2869
|
+
if (envRefs !== void 0 && envRefs.length > 0) {
|
|
2870
|
+
const resolved = [];
|
|
2871
|
+
for (const ref of envRefs) if (typeof ref === "function") {
|
|
2872
|
+
const value = await withTimeout(() => ref(ctx.event), DYNAMIC_FIELD_TIMEOUT_MS, `dynamic environment for generated job '${job.name}'`);
|
|
2873
|
+
if (value !== void 0 && value !== null) resolved.push({
|
|
2874
|
+
value,
|
|
2875
|
+
dynamic: false
|
|
2876
|
+
});
|
|
2877
|
+
} else if (typeof ref === "string") resolved.push({
|
|
2878
|
+
value: ref,
|
|
2879
|
+
dynamic: false
|
|
2880
|
+
});
|
|
2881
|
+
if (resolved.length > 0) resolvedEnvironments = resolved;
|
|
2882
|
+
}
|
|
2597
2883
|
let resolvedEnv;
|
|
2598
2884
|
if (typeof job.env === "function") {
|
|
2599
2885
|
const value = await withTimeout(() => job.env(ctx.event), DYNAMIC_FIELD_TIMEOUT_MS, `dynamic env for generated job '${job.name}'`);
|
|
@@ -2619,7 +2905,7 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
|
|
|
2619
2905
|
...job.include ? { include: job.include } : {},
|
|
2620
2906
|
...job.exclude ? { exclude: job.exclude } : {},
|
|
2621
2907
|
...job.description ? { description: job.description } : {},
|
|
2622
|
-
...
|
|
2908
|
+
...resolvedEnvironments !== void 0 ? { environments: resolvedEnvironments } : {},
|
|
2623
2909
|
...resolvedEnv !== void 0 ? { env: resolvedEnv } : {},
|
|
2624
2910
|
...resolvedConcurrencyGroup !== void 0 ? { concurrencyGroup: resolvedConcurrencyGroup } : {}
|
|
2625
2911
|
};
|
|
@@ -2677,20 +2963,41 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
|
|
|
2677
2963
|
* are loaded from the workflow bundle at execution time.
|
|
2678
2964
|
*/
|
|
2679
2965
|
function serializeSteps(steps) {
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
}
|
|
2966
|
+
let flatIndex = 0;
|
|
2967
|
+
return steps.map((entry) => {
|
|
2968
|
+
if (isParallelGroup(entry)) {
|
|
2969
|
+
const children = entry.steps.map((child) => serializeSequentialStep(child, flatIndex++));
|
|
2970
|
+
return {
|
|
2971
|
+
kind: "parallel",
|
|
2972
|
+
name: entry.name ?? `parallel-${children[0]?.name ?? "group"}`,
|
|
2973
|
+
failFast: entry.failFast,
|
|
2974
|
+
...entry.maxParallel !== void 0 ? { maxParallel: entry.maxParallel } : {},
|
|
2975
|
+
children
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
return serializeSequentialStep(entry, flatIndex++);
|
|
2692
2979
|
});
|
|
2693
2980
|
}
|
|
2981
|
+
/** Serialize one sequential step (or bare function) to a flat `LockStep`. */
|
|
2982
|
+
function serializeSequentialStep(stepOrFn, index) {
|
|
2983
|
+
if (typeof stepOrFn === "function") return {
|
|
2984
|
+
name: `step-${index}`,
|
|
2985
|
+
hasOutputs: false
|
|
2986
|
+
};
|
|
2987
|
+
const step = stepOrFn;
|
|
2988
|
+
return {
|
|
2989
|
+
name: step.name || `step-${index}`,
|
|
2990
|
+
hasOutputs: !!step.outputs,
|
|
2991
|
+
...step.continueOnError ? { continueOnError: true } : {},
|
|
2992
|
+
...step.timeout ? { timeout: step.timeout } : {},
|
|
2993
|
+
...step.retry ? { retry: {
|
|
2994
|
+
maxAttempts: step.retry.maxAttempts,
|
|
2995
|
+
delayMs: step.retry.delayMs,
|
|
2996
|
+
backoff: step.retry.backoff,
|
|
2997
|
+
maxDelayMs: step.retry.maxDelayMs
|
|
2998
|
+
} } : {}
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
2694
3001
|
/**
|
|
2695
3002
|
* Serialize matrix configuration. Static array/object matrices are embedded as-is;
|
|
2696
3003
|
* dynamic matrix functions are invoked against the eval context (mirroring the
|
|
@@ -4263,6 +4570,9 @@ function buildRequest(dispatch, workDir) {
|
|
|
4263
4570
|
matrixValues: jobConfig.matrixValues,
|
|
4264
4571
|
host: jobConfig.host,
|
|
4265
4572
|
agent: jobConfig.agent,
|
|
4573
|
+
dispatchInputs: jobConfig.dispatchInputs,
|
|
4574
|
+
fanoutIndex: jobConfig.fanoutIndex,
|
|
4575
|
+
fanoutTotal: jobConfig.fanoutTotal,
|
|
4266
4576
|
secrets: dispatch.secrets,
|
|
4267
4577
|
namespacedSecrets: dispatch.namespacedSecrets,
|
|
4268
4578
|
sourceFile: jobConfig.source?.file,
|
|
@@ -4606,10 +4916,17 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
|
|
|
4606
4916
|
case "log.line":
|
|
4607
4917
|
ctx.execOptions.onLogLine(msg.stepIndex, msg.line);
|
|
4608
4918
|
return;
|
|
4609
|
-
case "step.start":
|
|
4919
|
+
case "step.start": {
|
|
4610
4920
|
ctx.stepNames.set(msg.stepIndex, msg.stepName);
|
|
4611
|
-
|
|
4921
|
+
const startState = msg.state === "pending" ? ExecutionStepStatus.enum.pending : ExecutionStepStatus.enum.running;
|
|
4922
|
+
const startData = {
|
|
4923
|
+
...msg.concurrencyKind && { concurrencyKind: msg.concurrencyKind },
|
|
4924
|
+
...msg.groupId && { groupId: msg.groupId }
|
|
4925
|
+
};
|
|
4926
|
+
if (Object.keys(startData).length > 0) ctx.execOptions.onStepStatus(msg.stepIndex, msg.stepName, startState, startData);
|
|
4927
|
+
else ctx.execOptions.onStepStatus(msg.stepIndex, msg.stepName, startState);
|
|
4612
4928
|
return;
|
|
4929
|
+
}
|
|
4613
4930
|
case "step.complete":
|
|
4614
4931
|
ctx.execOptions.onStepStatus(msg.stepIndex, ctx.stepNames.get(msg.stepIndex) ?? "", msg.status, {
|
|
4615
4932
|
durationMs: msg.durationMs,
|
|
@@ -4619,6 +4936,8 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
|
|
|
4619
4936
|
...msg.checkOutcome !== void 0 && { checkOutcome: msg.checkOutcome },
|
|
4620
4937
|
...msg.driftSummary !== void 0 && { driftSummary: msg.driftSummary },
|
|
4621
4938
|
...msg.drift !== void 0 && { drift: msg.drift },
|
|
4939
|
+
...msg.concurrencyKind && { concurrencyKind: msg.concurrencyKind },
|
|
4940
|
+
...msg.groupId && { groupId: msg.groupId },
|
|
4622
4941
|
...msg.data && msg.data
|
|
4623
4942
|
});
|
|
4624
4943
|
return;
|
|
@@ -5332,10 +5651,17 @@ var init_container_sandbox = __esmMin((() => {
|
|
|
5332
5651
|
case "ready":
|
|
5333
5652
|
this.sendExecuteRequest(stream, options);
|
|
5334
5653
|
return false;
|
|
5335
|
-
case "step.start":
|
|
5654
|
+
case "step.start": {
|
|
5336
5655
|
stepNames.set(msg.stepIndex, msg.stepName);
|
|
5337
|
-
|
|
5656
|
+
const startState = msg.state === "pending" ? ExecutionStepStatus.enum.pending : ExecutionStepStatus.enum.running;
|
|
5657
|
+
const startData = {
|
|
5658
|
+
...msg.concurrencyKind && { concurrencyKind: msg.concurrencyKind },
|
|
5659
|
+
...msg.groupId && { groupId: msg.groupId }
|
|
5660
|
+
};
|
|
5661
|
+
if (Object.keys(startData).length > 0) options.onStepStatus(msg.stepIndex, msg.stepName, startState, startData);
|
|
5662
|
+
else options.onStepStatus(msg.stepIndex, msg.stepName, startState);
|
|
5338
5663
|
return false;
|
|
5664
|
+
}
|
|
5339
5665
|
case "step.complete": {
|
|
5340
5666
|
const name = stepNames.get(msg.stepIndex) ?? `step-${msg.stepIndex}`;
|
|
5341
5667
|
options.onStepStatus(msg.stepIndex, name, msg.status, {
|
|
@@ -5346,6 +5672,8 @@ var init_container_sandbox = __esmMin((() => {
|
|
|
5346
5672
|
...msg.checkOutcome !== void 0 && { checkOutcome: msg.checkOutcome },
|
|
5347
5673
|
...msg.driftSummary !== void 0 && { driftSummary: msg.driftSummary },
|
|
5348
5674
|
...msg.drift !== void 0 && { drift: msg.drift },
|
|
5675
|
+
...msg.concurrencyKind && { concurrencyKind: msg.concurrencyKind },
|
|
5676
|
+
...msg.groupId && { groupId: msg.groupId },
|
|
5349
5677
|
...msg.data && msg.data
|
|
5350
5678
|
});
|
|
5351
5679
|
stepResults.push({
|
|
@@ -5543,6 +5871,8 @@ var init_job_runner = __esmMin((() => {
|
|
|
5543
5871
|
init_source_packer();
|
|
5544
5872
|
init_source_restore();
|
|
5545
5873
|
init_init_runner();
|
|
5874
|
+
init_api_intercept();
|
|
5875
|
+
init_ensure_init_runner();
|
|
5546
5876
|
init_timeout_util();
|
|
5547
5877
|
init_dynamic_job_serializer();
|
|
5548
5878
|
init_log_streamer();
|
|
@@ -5655,6 +5985,10 @@ var init_job_runner = __esmMin((() => {
|
|
|
5655
5985
|
await this.handleDynamicJobFn(dispatch, workDir, abortController);
|
|
5656
5986
|
return true;
|
|
5657
5987
|
}
|
|
5988
|
+
if (jobConfig.bringupOnly === true) {
|
|
5989
|
+
await this.handleBringupJob(dispatch);
|
|
5990
|
+
return true;
|
|
5991
|
+
}
|
|
5658
5992
|
if (jobConfig.buildOnly === true) {
|
|
5659
5993
|
if (jobConfig.fullRepo) {
|
|
5660
5994
|
logger$2.warn("Build job received for fullRepo run -- skipping (should not happen)", {
|
|
@@ -5827,7 +6161,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
5827
6161
|
reason: ack.reason
|
|
5828
6162
|
};
|
|
5829
6163
|
},
|
|
5830
|
-
onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
|
|
6164
|
+
onApiRequest: this._sendApiRequest ? withBootstrapInterception(async (method, params) => this._sendApiRequest(method, params)) : void 0,
|
|
5831
6165
|
onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
|
|
5832
6166
|
onProvenanceRequest: this._relayProvenance ? async (request) => this._relayProvenance(jobId, request) : void 0,
|
|
5833
6167
|
onApprovalRequest: this._sendStepApproval ? async (request) => this._sendStepApproval(dispatch.runId, dispatch.jobId, request) : void 0,
|
|
@@ -5889,6 +6223,53 @@ var init_job_runner = __esmMin((() => {
|
|
|
5889
6223
|
}, result.secretOutputs);
|
|
5890
6224
|
}
|
|
5891
6225
|
/**
|
|
6226
|
+
* Handle a bring-up job: bring up a temporary init-runner on a declared-but-
|
|
6227
|
+
* un-agented host over SSH (fresh-box bootstrap convergence). The orchestrator
|
|
6228
|
+
* dispatches this synthetic `__bringup__` job to an agent holding the
|
|
6229
|
+
* `kici:capability:ssh-transport` capability; here we run the agent-side
|
|
6230
|
+
* `ensureInitRunner` helper (the privileged resolve is relayed to the
|
|
6231
|
+
* orchestrator, the SSH transport happens in this agent process — never
|
|
6232
|
+
* reaching workflow code). No clone, no sandbox — the init-runner then
|
|
6233
|
+
* connects under the target's agent id and the orchestrator's pinned-hold
|
|
6234
|
+
* drains the target's bootstrap steps onto it.
|
|
6235
|
+
*/
|
|
6236
|
+
async handleBringupJob(dispatch) {
|
|
6237
|
+
const { runId, jobId, jobConfig } = dispatch;
|
|
6238
|
+
const targetAgentId = String(jobConfig.bringupTarget ?? "");
|
|
6239
|
+
logger$2.info("Starting bring-up job", {
|
|
6240
|
+
jobId,
|
|
6241
|
+
runId,
|
|
6242
|
+
targetAgentId
|
|
6243
|
+
});
|
|
6244
|
+
this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
|
|
6245
|
+
const streamer = this.createStepStreamer(dispatch, 0);
|
|
6246
|
+
this.sendStepStatus(dispatch, 0, "bring-up", ExecutionStepStatus.enum.running);
|
|
6247
|
+
try {
|
|
6248
|
+
if (!targetAgentId) throw new Error("bring-up job missing bringupTarget");
|
|
6249
|
+
if (!this._sendApiRequest) throw new Error("bring-up job requires an orchestrator API transport");
|
|
6250
|
+
streamer.addLine(`Bringing up init-runner on ${targetAgentId}…`);
|
|
6251
|
+
const result = await ensureInitRunner(async (method, params) => this._sendApiRequest(method, params), targetAgentId);
|
|
6252
|
+
streamer.addLine(result.broughtUp ? `Init-runner brought up on ${targetAgentId}.` : `${targetAgentId} already has a live agent — no bring-up needed.`);
|
|
6253
|
+
await streamer.flush();
|
|
6254
|
+
this.sendStepStatus(dispatch, 0, "bring-up", ExecutionStepStatus.enum.success, void 0, streamer.getTotalBytes());
|
|
6255
|
+
streamer.destroy();
|
|
6256
|
+
this.sendJobStatus(dispatch, ExecutionJobStatus.enum.success);
|
|
6257
|
+
} catch (err) {
|
|
6258
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6259
|
+
streamer.addLine(`Bring-up failed: ${message}`);
|
|
6260
|
+
await streamer.flush();
|
|
6261
|
+
this.sendStepStatus(dispatch, 0, "bring-up", ExecutionStepStatus.enum.failed, void 0, streamer.getTotalBytes());
|
|
6262
|
+
streamer.destroy();
|
|
6263
|
+
this.sendJobStatus(dispatch, ExecutionJobStatus.enum.failed, { error: message });
|
|
6264
|
+
logger$2.warn("Bring-up job failed", {
|
|
6265
|
+
jobId,
|
|
6266
|
+
runId,
|
|
6267
|
+
targetAgentId,
|
|
6268
|
+
error: message
|
|
6269
|
+
});
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
/**
|
|
5892
6273
|
* Handle a build-only job.
|
|
5893
6274
|
*
|
|
5894
6275
|
* Build jobs install dependencies, pack them into a tarball,
|
|
@@ -6164,7 +6545,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
6164
6545
|
});
|
|
6165
6546
|
logger$2.info("Init job completed successfully", {
|
|
6166
6547
|
jobId,
|
|
6167
|
-
hasEnvironment: initResult.
|
|
6548
|
+
hasEnvironment: initResult.environmentNames !== void 0,
|
|
6168
6549
|
hasEnv: initResult.env !== void 0,
|
|
6169
6550
|
hasConcurrencyGroup: initResult.concurrencyGroup !== void 0
|
|
6170
6551
|
});
|
|
@@ -6281,7 +6662,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
6281
6662
|
error: (msg, ..._args) => evalLog(`ERROR: ${msg}`),
|
|
6282
6663
|
debug: (msg, ..._args) => evalLog(`DEBUG: ${msg}`)
|
|
6283
6664
|
};
|
|
6284
|
-
const kici = buildKiciApi(this._sendApiRequest ? (method, params) => this._sendApiRequest(method, params ?? {}) : () => Promise.reject(/* @__PURE__ */ new Error("Agent API not available")));
|
|
6665
|
+
const kici = buildKiciApi(this._sendApiRequest ? withBootstrapInterception((method, params) => this._sendApiRequest(method, params ?? {})) : () => Promise.reject(/* @__PURE__ */ new Error("Agent API not available")));
|
|
6285
6666
|
const lockJobs = await runCaptured(evalSink, async () => {
|
|
6286
6667
|
const { module } = await loadWorkflowSource(workDir, config.source.file, config.contentHash, config.resolvedHashFiles);
|
|
6287
6668
|
evalLog("Workflow loaded");
|
|
@@ -6471,7 +6852,9 @@ var init_job_runner = __esmMin((() => {
|
|
|
6471
6852
|
*/
|
|
6472
6853
|
sendStepStatus(dispatch, stepIndex, stepName, state, data, logBytesStreamed) {
|
|
6473
6854
|
const secretsAccessed = data?.secretsAccessed;
|
|
6474
|
-
const
|
|
6855
|
+
const concurrencyKind = data?.concurrencyKind;
|
|
6856
|
+
const groupId = data?.groupId;
|
|
6857
|
+
const { secretsAccessed: _s, concurrencyKind: _c, groupId: _g, ...restData } = data ?? {};
|
|
6475
6858
|
const hasRestData = Object.keys(restData).length > 0;
|
|
6476
6859
|
this.sendDirect({
|
|
6477
6860
|
type: "step.status",
|
|
@@ -6484,6 +6867,8 @@ var init_job_runner = __esmMin((() => {
|
|
|
6484
6867
|
timestamp: Date.now(),
|
|
6485
6868
|
...hasRestData && { data: restData },
|
|
6486
6869
|
...secretsAccessed !== void 0 && { secretsAccessed },
|
|
6870
|
+
...concurrencyKind !== void 0 && { concurrencyKind },
|
|
6871
|
+
...groupId !== void 0 && { groupId },
|
|
6487
6872
|
...logBytesStreamed !== void 0 && { logBytesStreamed }
|
|
6488
6873
|
});
|
|
6489
6874
|
}
|
|
@@ -6511,14 +6896,14 @@ var init_job_runner = __esmMin((() => {
|
|
|
6511
6896
|
*/
|
|
6512
6897
|
init_console_capture();
|
|
6513
6898
|
init_npm_resolver();
|
|
6514
|
-
const AGENT_VERSION = "0.1.
|
|
6515
|
-
const BUILD_COMMIT = "
|
|
6516
|
-
const SDK_VERSION = "0.1.
|
|
6517
|
-
const SDK_BUNDLE_HASH = "
|
|
6518
|
-
const SHARED_VERSION = "0.1.
|
|
6519
|
-
const SHARED_BUNDLE_HASH = "
|
|
6520
|
-
const ENGINE_VERSION = "0.1.
|
|
6521
|
-
const ENGINE_BUNDLE_HASH = "
|
|
6899
|
+
const AGENT_VERSION = "0.1.24";
|
|
6900
|
+
const BUILD_COMMIT = "73592f67f";
|
|
6901
|
+
const SDK_VERSION = "0.1.24";
|
|
6902
|
+
const SDK_BUNDLE_HASH = "031712cf8cd02f483365113aad7e946da982110d544b177eb05edcc0d4421253";
|
|
6903
|
+
const SHARED_VERSION = "0.1.24";
|
|
6904
|
+
const SHARED_BUNDLE_HASH = "b977224129c767c4851458a795fa470264b2fc51255baf06cf14640e29e2f44c";
|
|
6905
|
+
const ENGINE_VERSION = "0.1.24";
|
|
6906
|
+
const ENGINE_BUNDLE_HASH = "734acca885cd70eed07a1a9426b08c04c07a9bf99484c18100d3d797b3eb8f39";
|
|
6522
6907
|
initTelemetry({
|
|
6523
6908
|
serviceName: "kici-agent",
|
|
6524
6909
|
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|