@kici-dev/agent 0.1.23 → 0.1.25
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/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/sandbox/capture-context.d.ts +11 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +44 -9
- package/dist/execution/sandbox/parallel-scheduler.d.ts +34 -0
- package/dist/execution/sandbox/step-loop.d.ts +91 -16
- 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 +22 -1
- package/dist/index.js +8 -2
- package/dist/provenance/attest.d.ts +27 -4
- package/dist/provenance/statement-builder.d.ts +50 -0
- package/dist/server.js +204 -101
- package/dist/workflow-runner.js +577 -131
- package/dist/ws/orchestrator-client.d.ts +6 -0
- package/package.json +6 -6
- package/sbom.spdx.json +68 -68
package/dist/workflow-runner.js
CHANGED
|
@@ -7,12 +7,13 @@ import os, { homedir, tmpdir } from "node:os";
|
|
|
7
7
|
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { $ } from "zx";
|
|
9
9
|
import { createLogger, deriveSharedSecret, initZx, normalizeLineEndings, sha256, sha256File, toErrorMessage } from "@kici-dev/shared";
|
|
10
|
-
import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, TimeoutReason } from "@kici-dev/engine";
|
|
11
|
-
import { buildKiciApi, buildNeedsContext, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeApproval, normalizeCacheSpecs, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
10
|
+
import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, StepConcurrencyKind, TimeoutReason } from "@kici-dev/engine";
|
|
11
|
+
import { buildKiciApi, buildNeedsContext, createStepSecrets, evaluateRules, isDynamicJobFn, isParallelGroup, normalizeApproval, normalizeCacheSpecs, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
12
12
|
import { OIDC_TOKEN_REQUEST_METHOD } from "@kici-dev/engine/protocol/messages/oidc-token-relay";
|
|
13
13
|
import { computeBackoffDelay, sha256File as sha256File$1 } from "@kici-dev/core";
|
|
14
14
|
import { calculateJwkThumbprint, decodeJwt, exportJWK, generateKeyPair } from "jose";
|
|
15
15
|
import { IN_TOTO_PAYLOAD_TYPE, KICI_PROVENANCE_AUDIENCE, KICI_PROVENANCE_BUNDLE_MEDIA_TYPE } from "@kici-dev/engine/provenance/bundle";
|
|
16
|
+
import { computeStatementHash } from "@kici-dev/engine/provenance/statement-hash";
|
|
16
17
|
import { IN_TOTO_STATEMENT_TYPE, KICI_WORKFLOW_BUILD_TYPE, SLSA_PROVENANCE_PREDICATE_TYPE } from "@kici-dev/engine/provenance/schema";
|
|
17
18
|
import { buildDsseEnvelope, dssePae } from "@kici-dev/engine/provenance/dsse";
|
|
18
19
|
import https from "node:https";
|
|
@@ -23,12 +24,20 @@ import { createGunzip } from "node:zlib";
|
|
|
23
24
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
25
|
import { c, x } from "tar";
|
|
25
26
|
import { runIdempotentStep } from "@kici-dev/core/idempotency";
|
|
27
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
28
|
import { execFile } from "node:child_process";
|
|
27
29
|
import { promisify } from "node:util";
|
|
28
30
|
import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
|
|
29
31
|
import { parse, stringify } from "yaml";
|
|
30
32
|
var __defProp = Object.defineProperty;
|
|
31
|
-
var __esmMin = (fn, res) => () =>
|
|
33
|
+
var __esmMin = (fn, res, err) => () => {
|
|
34
|
+
if (err) throw err[0];
|
|
35
|
+
try {
|
|
36
|
+
return fn && (res = fn(fn = 0)), res;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
throw err = [e], e;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
32
41
|
var __exportAll = (all, no_symbols) => {
|
|
33
42
|
let target = {};
|
|
34
43
|
for (var name in all) __defProp(target, name, {
|
|
@@ -38,7 +47,6 @@ var __exportAll = (all, no_symbols) => {
|
|
|
38
47
|
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
39
48
|
return target;
|
|
40
49
|
};
|
|
41
|
-
import.meta.url;
|
|
42
50
|
//#endregion
|
|
43
51
|
//#region src/provenance/statement-builder.ts
|
|
44
52
|
/**
|
|
@@ -47,6 +55,53 @@ import.meta.url;
|
|
|
47
55
|
* entirely from the JWT claims (Platform-minted, unforgeable), so the
|
|
48
56
|
* statement's identity equals the token's identity by construction.
|
|
49
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* Build a frozen SLSA v1.0 provenance statement from agent-local job context,
|
|
60
|
+
* for a deferred attestation (no minted identity token yet). Marks
|
|
61
|
+
* `attestationOrigin: 'deferred'` in the internal parameters. The caller
|
|
62
|
+
* DSSE-signs the returned statement immediately and computes its statement hash
|
|
63
|
+
* — the binding the later OIDC mint commits to (truth-contract property 2).
|
|
64
|
+
*/
|
|
65
|
+
function buildLocalProvenanceStatement(input) {
|
|
66
|
+
const c = input.context;
|
|
67
|
+
return {
|
|
68
|
+
_type: IN_TOTO_STATEMENT_TYPE,
|
|
69
|
+
subject: [{
|
|
70
|
+
name: input.subject.name,
|
|
71
|
+
digest: input.subject.digest
|
|
72
|
+
}],
|
|
73
|
+
predicateType: SLSA_PROVENANCE_PREDICATE_TYPE,
|
|
74
|
+
predicate: {
|
|
75
|
+
buildDefinition: {
|
|
76
|
+
buildType: KICI_WORKFLOW_BUILD_TYPE,
|
|
77
|
+
externalParameters: { workflow: {
|
|
78
|
+
repository: c.repository,
|
|
79
|
+
ref: c.ref,
|
|
80
|
+
path: c.workflowRef
|
|
81
|
+
} },
|
|
82
|
+
internalParameters: {
|
|
83
|
+
...c.sha ? { commit: c.sha } : {},
|
|
84
|
+
runId: c.runId,
|
|
85
|
+
jobId: c.jobId,
|
|
86
|
+
...c.orgId ? { orgId: c.orgId } : {},
|
|
87
|
+
...c.sourceOrigin ? { sourceOrigin: c.sourceOrigin } : {},
|
|
88
|
+
attestationOrigin: "deferred"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
runDetails: {
|
|
92
|
+
builder: {
|
|
93
|
+
id: `${c.issuer}/orchestrator/unknown`,
|
|
94
|
+
version: input.builderVersions
|
|
95
|
+
},
|
|
96
|
+
metadata: {
|
|
97
|
+
invocationId: c.runId,
|
|
98
|
+
startedOn: input.startedOn,
|
|
99
|
+
finishedOn: input.finishedOn
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
50
105
|
/** Build a KiCI SLSA v1.0 provenance statement (validates against the P1.1 schema). */
|
|
51
106
|
function buildProvenanceStatement(input) {
|
|
52
107
|
const c = input.tokenClaims;
|
|
@@ -60,15 +115,20 @@ function buildProvenanceStatement(input) {
|
|
|
60
115
|
predicate: {
|
|
61
116
|
buildDefinition: {
|
|
62
117
|
buildType: KICI_WORKFLOW_BUILD_TYPE,
|
|
63
|
-
externalParameters: {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
118
|
+
externalParameters: {
|
|
119
|
+
workflow: {
|
|
120
|
+
repository: c.repository ?? "",
|
|
121
|
+
ref: c.ref ?? "",
|
|
122
|
+
path: c.workflow_ref ?? ""
|
|
123
|
+
},
|
|
124
|
+
...c.provider ? { provider: c.provider } : {}
|
|
125
|
+
},
|
|
68
126
|
internalParameters: {
|
|
69
127
|
...c.sha ? { commit: c.sha } : {},
|
|
70
128
|
runId: c.kici_run_id,
|
|
71
|
-
jobId: c.kici_job_id
|
|
129
|
+
jobId: c.kici_job_id,
|
|
130
|
+
...c.org_id ? { orgId: c.org_id } : {},
|
|
131
|
+
...c.source_origin ? { sourceOrigin: c.source_origin } : {}
|
|
72
132
|
}
|
|
73
133
|
},
|
|
74
134
|
runDetails: {
|
|
@@ -126,7 +186,9 @@ async function signStatementDsse(payloadType, statementBytes) {
|
|
|
126
186
|
async function attestProvenance(deps, input) {
|
|
127
187
|
const audience = input.audience ?? KICI_PROVENANCE_AUDIENCE;
|
|
128
188
|
const subjectDigest = subjectDigestString(input.subject);
|
|
129
|
-
const
|
|
189
|
+
const tokenResult = await deps.getIdToken({ audience });
|
|
190
|
+
if ("deferred" in tokenResult) return deferAttestation(deps, input, subjectDigest, audience);
|
|
191
|
+
const { token } = tokenResult;
|
|
130
192
|
const claims = decodeJwt(token);
|
|
131
193
|
const now = (deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
|
|
132
194
|
const statement = buildProvenanceStatement({
|
|
@@ -152,6 +214,39 @@ async function attestProvenance(deps, input) {
|
|
|
152
214
|
};
|
|
153
215
|
}
|
|
154
216
|
/**
|
|
217
|
+
* Freeze + DSSE-sign the provenance statement from agent-local job facts and
|
|
218
|
+
* report it for later minting. The step does NOT throw — the job completes
|
|
219
|
+
* green and the attestation surfaces as `deferred`.
|
|
220
|
+
*/
|
|
221
|
+
async function deferAttestation(deps, input, subjectDigest, audience) {
|
|
222
|
+
if (!deps.reportDeferred || !deps.localContext) throw new Error("provenance mint deferred but no reportDeferred/localContext wired to capture it");
|
|
223
|
+
const now = (deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
|
|
224
|
+
const statement = buildLocalProvenanceStatement({
|
|
225
|
+
context: deps.localContext,
|
|
226
|
+
subject: input.subject,
|
|
227
|
+
builderVersions: deps.builderVersions,
|
|
228
|
+
startedOn: now,
|
|
229
|
+
finishedOn: now
|
|
230
|
+
});
|
|
231
|
+
const statementBytes = new TextEncoder().encode(JSON.stringify(statement));
|
|
232
|
+
const { envelope, publicJwk } = await signStatementDsse(IN_TOTO_PAYLOAD_TYPE, statementBytes);
|
|
233
|
+
const statementHash = await computeStatementHash(statementBytes);
|
|
234
|
+
await deps.reportDeferred({
|
|
235
|
+
subjectName: input.subject.name,
|
|
236
|
+
subjectDigest,
|
|
237
|
+
audience,
|
|
238
|
+
mediaType: KICI_PROVENANCE_BUNDLE_MEDIA_TYPE,
|
|
239
|
+
statementHash,
|
|
240
|
+
dsseEnvelope: envelope,
|
|
241
|
+
publicKey: publicJwk
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
deferred: true,
|
|
245
|
+
statementHash,
|
|
246
|
+
subjectDigest
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
155
250
|
* Pick the primary digest (`sha256` preferred) as the storage-key discriminator.
|
|
156
251
|
* Throws when the subject carries no digest: an empty digest set would otherwise
|
|
157
252
|
* yield an `undefined` storage-key segment (`provenance/<run>/<job>/undefined.kici.json`)
|
|
@@ -689,29 +784,34 @@ function createCacheApi(workDir, transport, roots) {
|
|
|
689
784
|
}
|
|
690
785
|
};
|
|
691
786
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
787
|
+
/** Block size reserved per owner for cache pseudo-step indices. */
|
|
788
|
+
const CACHE_INDEX_BLOCK = 1e3;
|
|
789
|
+
/**
|
|
790
|
+
* Build the cache pseudo-step index allocator. Each owner (a real step index, or
|
|
791
|
+
* {@link JOB_CACHE_OWNER}) gets its own disjoint block of {@link CACHE_INDEX_BLOCK}
|
|
792
|
+
* indices, all above every real-step and hook index (`stepCount * 3 + 100`). A
|
|
793
|
+
* step's two-or-more cache pseudo-steps are a pure function of its own owner
|
|
794
|
+
* index, so concurrent children never collide. Under sequential execution the
|
|
795
|
+
* emitted indices stay above all real/hook indices exactly as before.
|
|
796
|
+
*/
|
|
797
|
+
function createCacheStepIndexAllocator(stepCount) {
|
|
798
|
+
const cacheBase = stepCount * 3 + 100;
|
|
799
|
+
const counters = /* @__PURE__ */ new Map();
|
|
800
|
+
return (ownerStepIndex) => {
|
|
801
|
+
const n = counters.get(ownerStepIndex) ?? 0;
|
|
802
|
+
counters.set(ownerStepIndex, n + 1);
|
|
803
|
+
return cacheBase + (ownerStepIndex + 1) * CACHE_INDEX_BLOCK + n;
|
|
804
|
+
};
|
|
805
|
+
}
|
|
706
806
|
/**
|
|
707
807
|
* Restore every spec, surfacing each as a `cache:restore` pseudo-step. Returns
|
|
708
808
|
* a map keyed by spec key recording whether the EXACT key hit (so the save
|
|
709
809
|
* phase can skip a redundant save of an entry that already exists).
|
|
710
810
|
*/
|
|
711
|
-
async function restoreCacheSpecs(specs, deps) {
|
|
811
|
+
async function restoreCacheSpecs(specs, deps, ownerStepIndex) {
|
|
712
812
|
const results = /* @__PURE__ */ new Map();
|
|
713
813
|
for (const spec of specs) {
|
|
714
|
-
const stepIndex = deps.nextStepIndex();
|
|
814
|
+
const stepIndex = deps.nextStepIndex(ownerStepIndex);
|
|
715
815
|
deps.sendIpc({
|
|
716
816
|
type: "step.start",
|
|
717
817
|
stepIndex,
|
|
@@ -761,10 +861,10 @@ async function restoreCacheSpecs(specs, deps) {
|
|
|
761
861
|
* whose restore matched a different key via a `restoreKeys` prefix is still
|
|
762
862
|
* saved under its exact key.
|
|
763
863
|
*/
|
|
764
|
-
async function saveCacheSpecs(specs, restoreResults, deps) {
|
|
864
|
+
async function saveCacheSpecs(specs, restoreResults, deps, ownerStepIndex) {
|
|
765
865
|
for (const spec of specs) {
|
|
766
866
|
if (restoreResults.get(spec.key)?.matchedKey === spec.key) continue;
|
|
767
|
-
const stepIndex = deps.nextStepIndex();
|
|
867
|
+
const stepIndex = deps.nextStepIndex(ownerStepIndex);
|
|
768
868
|
deps.sendIpc({
|
|
769
869
|
type: "step.start",
|
|
770
870
|
stepIndex,
|
|
@@ -1120,7 +1220,146 @@ function createRuleContext(event, changedFiles = [], env = {}, dispatchInputs =
|
|
|
1120
1220
|
};
|
|
1121
1221
|
}
|
|
1122
1222
|
//#endregion
|
|
1223
|
+
//#region src/execution/sandbox/parallel-scheduler.ts
|
|
1224
|
+
/**
|
|
1225
|
+
* Concurrency-aware scheduler for `parallel()` step groups.
|
|
1226
|
+
*
|
|
1227
|
+
* A parallel group's children each run as their own observable step (own logs,
|
|
1228
|
+
* status, timing, retry, cache, hooks — all task-scoped by the Phase 0 per-task
|
|
1229
|
+
* isolation) through the same `runStepIteration` machinery the sequential loop
|
|
1230
|
+
* uses. Children launch behind a `maxParallel` window (queued children report
|
|
1231
|
+
* `pending`); the group joins at a barrier. On the first non-`continueOnError`
|
|
1232
|
+
* child failure when `failFast`, every in-flight sibling's per-task abort
|
|
1233
|
+
* controller is fired so its step race rejects and it is reported `cancelled`
|
|
1234
|
+
* (which is NOT a failure).
|
|
1235
|
+
*/
|
|
1236
|
+
/**
|
|
1237
|
+
* Wrap `sendIpc` so a child's own `step.start` / `step.complete` messages carry
|
|
1238
|
+
* the parallel-child concurrency role + the group id. Cache/secret pseudo-step
|
|
1239
|
+
* messages (different stepIndex) pass through untouched.
|
|
1240
|
+
*/
|
|
1241
|
+
function stampChildSend(sendIpc, childStepIndex, groupId) {
|
|
1242
|
+
return (msg) => {
|
|
1243
|
+
if ((msg.type === "step.start" || msg.type === "step.complete") && msg.stepIndex === childStepIndex) {
|
|
1244
|
+
sendIpc({
|
|
1245
|
+
...msg,
|
|
1246
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1247
|
+
groupId
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
sendIpc(msg);
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
/** Announce a child queued behind the `maxParallel` window as `pending`. */
|
|
1255
|
+
function emitPending(opts, child, groupId) {
|
|
1256
|
+
opts.sendIpc({
|
|
1257
|
+
type: "step.start",
|
|
1258
|
+
stepIndex: child.stepIndex,
|
|
1259
|
+
stepName: child.step.name,
|
|
1260
|
+
state: "pending",
|
|
1261
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1262
|
+
groupId
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
/** Mark a child that never launched (fail-fast already tripped) as `cancelled`. */
|
|
1266
|
+
function emitCancelledSkip(opts, child, groupId) {
|
|
1267
|
+
opts.sendIpc({
|
|
1268
|
+
type: "step.start",
|
|
1269
|
+
stepIndex: child.stepIndex,
|
|
1270
|
+
stepName: child.step.name,
|
|
1271
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1272
|
+
groupId
|
|
1273
|
+
});
|
|
1274
|
+
opts.sendIpc({
|
|
1275
|
+
type: "step.complete",
|
|
1276
|
+
stepIndex: child.stepIndex,
|
|
1277
|
+
status: "cancelled",
|
|
1278
|
+
durationMs: 0,
|
|
1279
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1280
|
+
groupId
|
|
1281
|
+
});
|
|
1282
|
+
return {
|
|
1283
|
+
name: child.step.name,
|
|
1284
|
+
stepIndex: child.stepIndex,
|
|
1285
|
+
status: "cancelled",
|
|
1286
|
+
durationMs: 0
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Run a parallel group: launch children with a bounded-concurrency window, join
|
|
1291
|
+
* at a barrier, and fail-fast-cancel in-flight siblings on the first hard
|
|
1292
|
+
* failure.
|
|
1293
|
+
*/
|
|
1294
|
+
async function runParallelGroup(node, opts) {
|
|
1295
|
+
const { children, failFast, groupId } = node;
|
|
1296
|
+
const limit = node.maxParallel && node.maxParallel > 0 ? node.maxParallel : children.length;
|
|
1297
|
+
const results = new Array(children.length);
|
|
1298
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1299
|
+
let failed = false;
|
|
1300
|
+
let failedStepName;
|
|
1301
|
+
for (let i = limit; i < children.length; i++) emitPending(opts, children[i], groupId);
|
|
1302
|
+
let cursor = 0;
|
|
1303
|
+
const worker = async () => {
|
|
1304
|
+
for (;;) {
|
|
1305
|
+
const idx = cursor++;
|
|
1306
|
+
if (idx >= children.length) return;
|
|
1307
|
+
const child = children[idx];
|
|
1308
|
+
if (failFast && failed) {
|
|
1309
|
+
results[idx] = emitCancelledSkip(opts, child, groupId);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const childOpts = {
|
|
1313
|
+
...opts,
|
|
1314
|
+
sendIpc: stampChildSend(opts.sendIpc, child.stepIndex, groupId)
|
|
1315
|
+
};
|
|
1316
|
+
inFlight.add(child.stepIndex);
|
|
1317
|
+
try {
|
|
1318
|
+
const outcome = await runStepIteration(child.step, child.stepIndex, childOpts);
|
|
1319
|
+
results[idx] = outcome.result;
|
|
1320
|
+
if (outcome.shouldBreak) {
|
|
1321
|
+
if (!failed) {
|
|
1322
|
+
failed = true;
|
|
1323
|
+
failedStepName = outcome.failedStepName ?? child.step.name;
|
|
1324
|
+
}
|
|
1325
|
+
if (failFast) {
|
|
1326
|
+
for (const sibling of inFlight) if (sibling !== child.stepIndex) opts.abortStep?.(sibling);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
} finally {
|
|
1330
|
+
inFlight.delete(child.stepIndex);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
await Promise.all(Array.from({ length: Math.min(limit, children.length) }, () => worker()));
|
|
1335
|
+
return {
|
|
1336
|
+
failed,
|
|
1337
|
+
failedStepName,
|
|
1338
|
+
results
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
//#endregion
|
|
1123
1342
|
//#region src/execution/sandbox/step-loop.ts
|
|
1343
|
+
/**
|
|
1344
|
+
* Thrown inside the step race when a step's own per-task abort controller fires
|
|
1345
|
+
* (parallel fail-fast cancels an in-flight sibling). Distinguished from a
|
|
1346
|
+
* timeout/job-deadline reject so the loop reports the step as `cancelled`
|
|
1347
|
+
* (which is NOT a failure) rather than `failed`.
|
|
1348
|
+
*/
|
|
1349
|
+
var StepCancelledError = class StepCancelledError extends Error {
|
|
1350
|
+
name = "StepCancelledError";
|
|
1351
|
+
constructor(stepName) {
|
|
1352
|
+
super(`Step '${stepName}' was cancelled by parallel fail-fast`);
|
|
1353
|
+
Object.setPrototypeOf(this, StepCancelledError.prototype);
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
/**
|
|
1357
|
+
* Resolve the capture-scope wrapper from options, falling back to a direct call
|
|
1358
|
+
* when no capture wiring is present (unit harnesses).
|
|
1359
|
+
*/
|
|
1360
|
+
function captureWrap(opts) {
|
|
1361
|
+
return opts.runWithStepCapture ?? ((_stepIndex, fn) => fn());
|
|
1362
|
+
}
|
|
1124
1363
|
/** Result of a rejected drift gate: the run was declined by a reviewer. */
|
|
1125
1364
|
var DriftGateRejectedError = class extends Error {
|
|
1126
1365
|
constructor(reason) {
|
|
@@ -1215,6 +1454,7 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1215
1454
|
const startTime = Date.now();
|
|
1216
1455
|
const abortController = new AbortController();
|
|
1217
1456
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
1457
|
+
const stepAbortSignal = opts.getStepAbortSignal?.(stepIndex);
|
|
1218
1458
|
try {
|
|
1219
1459
|
const phase = await Promise.race([
|
|
1220
1460
|
runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn, opts),
|
|
@@ -1232,14 +1472,22 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1232
1472
|
jobDeadlineSignal.addEventListener("abort", () => {
|
|
1233
1473
|
reject(/* @__PURE__ */ new Error(`Step '${step.name}' aborted: job timeout exceeded`));
|
|
1234
1474
|
});
|
|
1475
|
+
}),
|
|
1476
|
+
new Promise((_, reject) => {
|
|
1477
|
+
if (!stepAbortSignal) return;
|
|
1478
|
+
if (stepAbortSignal.aborted) {
|
|
1479
|
+
reject(new StepCancelledError(step.name));
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
stepAbortSignal.addEventListener("abort", () => reject(new StepCancelledError(step.name)));
|
|
1235
1483
|
})
|
|
1236
1484
|
]);
|
|
1237
1485
|
clearTimeout(timeoutId);
|
|
1238
1486
|
const durationMs = Date.now() - startTime;
|
|
1239
1487
|
const outputsPayload = phase.outputs != null ? phase.outputs : void 0;
|
|
1240
1488
|
if (outputsPayload) outputsMap.set(step.name, outputsPayload);
|
|
1241
|
-
const secretsAccessed = getSecretsAccessLog?.();
|
|
1242
|
-
emitSecretMountEvents(getSecretMountRecords?.(), stepIndex, sendFn);
|
|
1489
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1490
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1243
1491
|
const stepStatus = phase.status === "skipped" ? ExecutionStepStatus.enum.skipped : ExecutionStepStatus.enum.success;
|
|
1244
1492
|
sendFn({
|
|
1245
1493
|
type: "step.complete",
|
|
@@ -1263,10 +1511,27 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1263
1511
|
clearTimeout(timeoutId);
|
|
1264
1512
|
const durationMs = Date.now() - startTime;
|
|
1265
1513
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
1514
|
+
if (e instanceof StepCancelledError) {
|
|
1515
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1516
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1517
|
+
sendFn({
|
|
1518
|
+
type: "step.complete",
|
|
1519
|
+
stepIndex,
|
|
1520
|
+
status: ExecutionStepStatus.enum.cancelled,
|
|
1521
|
+
durationMs,
|
|
1522
|
+
...secretsAccessed !== void 0 && { secretsAccessed }
|
|
1523
|
+
});
|
|
1524
|
+
return {
|
|
1525
|
+
name: step.name,
|
|
1526
|
+
stepIndex,
|
|
1527
|
+
status: ExecutionStepStatus.enum.cancelled,
|
|
1528
|
+
durationMs
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1266
1531
|
const exitCode = extractExitCode(e);
|
|
1267
1532
|
const signal = extractSignal(e);
|
|
1268
|
-
const secretsAccessed = getSecretsAccessLog?.();
|
|
1269
|
-
emitSecretMountEvents(getSecretMountRecords?.(), stepIndex, sendFn);
|
|
1533
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1534
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1270
1535
|
sendFn({
|
|
1271
1536
|
type: "step.complete",
|
|
1272
1537
|
stepIndex,
|
|
@@ -1395,7 +1660,7 @@ async function maybeGateStepApproval(step, stepIndex, opts) {
|
|
|
1395
1660
|
stepIndex,
|
|
1396
1661
|
line: `[kici] Step '${step.name}' ${why}.`
|
|
1397
1662
|
});
|
|
1398
|
-
await opts.disposeStepResources?.();
|
|
1663
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1399
1664
|
return {
|
|
1400
1665
|
result: {
|
|
1401
1666
|
name: step.name,
|
|
@@ -1421,15 +1686,16 @@ async function runObserverHook(args) {
|
|
|
1421
1686
|
startTime: opts.startTime ?? Date.now(),
|
|
1422
1687
|
...failedStep !== void 0 && { failedStep }
|
|
1423
1688
|
});
|
|
1424
|
-
const
|
|
1689
|
+
const ctx = opts.createStepContext(stepIndex, step.name);
|
|
1690
|
+
const hookResult = await captureWrap(opts)(hookStepIndex, () => executeHook({
|
|
1425
1691
|
hook,
|
|
1426
|
-
stepContext:
|
|
1692
|
+
stepContext: ctx,
|
|
1427
1693
|
outcome,
|
|
1428
1694
|
hookType,
|
|
1429
1695
|
stepIndex: hookStepIndex,
|
|
1430
1696
|
sendIpc: opts.sendIpc,
|
|
1431
1697
|
timeout: 3e5
|
|
1432
|
-
});
|
|
1698
|
+
}));
|
|
1433
1699
|
if (!hookResult.success) opts.sendIpc({
|
|
1434
1700
|
type: "log.line",
|
|
1435
1701
|
stepIndex,
|
|
@@ -1482,7 +1748,7 @@ async function runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts) {
|
|
|
1482
1748
|
async function runStepIteration(step, stepIndex, opts) {
|
|
1483
1749
|
const skippedResult = await evaluateStepRulesAndMaybeSkip(step, stepIndex, opts);
|
|
1484
1750
|
if (skippedResult) {
|
|
1485
|
-
await opts.disposeStepResources?.();
|
|
1751
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1486
1752
|
return {
|
|
1487
1753
|
result: skippedResult,
|
|
1488
1754
|
shouldBreak: false
|
|
@@ -1499,18 +1765,18 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1499
1765
|
opts
|
|
1500
1766
|
});
|
|
1501
1767
|
const stepCacheSpecs = opts.cachePhaseDeps ? normalizeCacheSpecs(step.cache) : [];
|
|
1502
|
-
const stepCacheRestore = stepCacheSpecs.length > 0 && opts.cachePhaseDeps ? await restoreCacheSpecs(stepCacheSpecs, opts.cachePhaseDeps) : /* @__PURE__ */ new Map();
|
|
1768
|
+
const stepCacheRestore = stepCacheSpecs.length > 0 && opts.cachePhaseDeps ? await restoreCacheSpecs(stepCacheSpecs, opts.cachePhaseDeps, stepIndex) : /* @__PURE__ */ new Map();
|
|
1503
1769
|
try {
|
|
1504
|
-
await opts.beforeStepEnvFiles?.();
|
|
1770
|
+
await opts.beforeStepEnvFiles?.(stepIndex);
|
|
1505
1771
|
const ctx = opts.createStepContext(stepIndex, step.name);
|
|
1506
1772
|
const timeoutMs = step.timeout ?? opts.defaultTimeoutMs;
|
|
1507
1773
|
let result;
|
|
1508
1774
|
try {
|
|
1509
|
-
result = await runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts);
|
|
1775
|
+
result = await captureWrap(opts)(stepIndex, () => runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts));
|
|
1510
1776
|
} finally {
|
|
1511
|
-
await opts.afterStepApplyEnvFiles?.();
|
|
1777
|
+
await opts.afterStepApplyEnvFiles?.(stepIndex);
|
|
1512
1778
|
}
|
|
1513
|
-
if (stepCacheSpecs.length > 0 && opts.cachePhaseDeps && result.status === ExecutionStepStatus.enum.success) await saveCacheSpecs(stepCacheSpecs, stepCacheRestore, opts.cachePhaseDeps);
|
|
1779
|
+
if (stepCacheSpecs.length > 0 && opts.cachePhaseDeps && result.status === ExecutionStepStatus.enum.success) await saveCacheSpecs(stepCacheSpecs, stepCacheRestore, opts.cachePhaseDeps, stepIndex);
|
|
1514
1780
|
if (opts.jobHooks?.afterStep) await runObserverHook({
|
|
1515
1781
|
hook: opts.jobHooks.afterStep,
|
|
1516
1782
|
hookType: "afterStep",
|
|
@@ -1530,7 +1796,7 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1530
1796
|
shouldBreak: false
|
|
1531
1797
|
};
|
|
1532
1798
|
} finally {
|
|
1533
|
-
await opts.disposeStepResources?.();
|
|
1799
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1534
1800
|
}
|
|
1535
1801
|
}
|
|
1536
1802
|
/**
|
|
@@ -1546,14 +1812,15 @@ async function runCompletionHook(args) {
|
|
|
1546
1812
|
stepIndex: -1,
|
|
1547
1813
|
line: `[kici] Running ${hookType} hook...`
|
|
1548
1814
|
});
|
|
1549
|
-
const
|
|
1815
|
+
const ctx = opts.createStepContext(hookStepIndex, hookType);
|
|
1816
|
+
const hookResult = await captureWrap(opts)(hookStepIndex, () => executeHook({
|
|
1550
1817
|
hook,
|
|
1551
|
-
stepContext:
|
|
1818
|
+
stepContext: ctx,
|
|
1552
1819
|
outcome,
|
|
1553
1820
|
hookType,
|
|
1554
1821
|
stepIndex: hookStepIndex,
|
|
1555
1822
|
sendIpc: opts.sendIpc
|
|
1556
|
-
});
|
|
1823
|
+
}));
|
|
1557
1824
|
if (hookResult.success) {
|
|
1558
1825
|
opts.sendIpc({
|
|
1559
1826
|
type: "log.line",
|
|
@@ -1651,9 +1918,24 @@ async function executeStepLoop(opts) {
|
|
|
1651
1918
|
const startTime = opts.startTime ?? Date.now();
|
|
1652
1919
|
const stepResults = [];
|
|
1653
1920
|
const state = { failed: false };
|
|
1654
|
-
|
|
1921
|
+
const nodes = opts.stepNodes ?? opts.steps.map((step, i) => ({
|
|
1922
|
+
kind: "sequential",
|
|
1923
|
+
step,
|
|
1924
|
+
stepIndex: i
|
|
1925
|
+
}));
|
|
1926
|
+
for (const node of nodes) {
|
|
1655
1927
|
if (opts.isAborted?.()) break;
|
|
1656
|
-
|
|
1928
|
+
if (node.kind === "parallel") {
|
|
1929
|
+
const groupOutcome = await runParallelGroup(node, opts);
|
|
1930
|
+
stepResults.push(...groupOutcome.results);
|
|
1931
|
+
if (groupOutcome.failed) {
|
|
1932
|
+
state.failed = true;
|
|
1933
|
+
state.failedStepName = groupOutcome.failedStepName;
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
const outcome = await runStepIteration(node.step, node.stepIndex, opts);
|
|
1657
1939
|
stepResults.push(outcome.result);
|
|
1658
1940
|
if (outcome.failedStepName) {
|
|
1659
1941
|
state.failed = true;
|
|
@@ -1674,6 +1956,69 @@ async function executeStepLoop(opts) {
|
|
|
1674
1956
|
};
|
|
1675
1957
|
}
|
|
1676
1958
|
//#endregion
|
|
1959
|
+
//#region src/execution/sandbox/capture-context.ts
|
|
1960
|
+
/**
|
|
1961
|
+
* Attributes captured console output to the step whose run is currently on the
|
|
1962
|
+
* async call stack.
|
|
1963
|
+
*
|
|
1964
|
+
* The monkey-patched `process.stdout/stderr.write` reads
|
|
1965
|
+
* {@link currentCaptureStepIndex} to decide which `step-N.log` a console line
|
|
1966
|
+
* belongs to. Keying this on the async execution context (rather than a single
|
|
1967
|
+
* module global) makes attribution correct when more than one step body runs
|
|
1968
|
+
* concurrently: each step's `run` executes inside its own
|
|
1969
|
+
* {@link runInStepCapture} scope, so its writes resolve to its own index even
|
|
1970
|
+
* while a sibling step is mid-flight. Under sequential execution exactly one
|
|
1971
|
+
* scope is active at a time, identical to the former global.
|
|
1972
|
+
*/
|
|
1973
|
+
const stepCaptureStore = new AsyncLocalStorage();
|
|
1974
|
+
/**
|
|
1975
|
+
* Run `fn` with `stepIndex` as the active console-capture attribution for the
|
|
1976
|
+
* duration of its async execution (including everything it awaits).
|
|
1977
|
+
*/
|
|
1978
|
+
function runInStepCapture(stepIndex, fn) {
|
|
1979
|
+
return stepCaptureStore.run(stepIndex, fn);
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* The step index whose run is currently on the async stack, or `-1` when no
|
|
1983
|
+
* capture scope is active (workflow-level / between-steps output).
|
|
1984
|
+
*/
|
|
1985
|
+
function currentCaptureStepIndex() {
|
|
1986
|
+
return stepCaptureStore.getStore() ?? -1;
|
|
1987
|
+
}
|
|
1988
|
+
//#endregion
|
|
1989
|
+
//#region src/execution/sandbox/step-task-registry.ts
|
|
1990
|
+
/**
|
|
1991
|
+
* Per-task replacement for the runner's former `currentStepSecrets` /
|
|
1992
|
+
* `currentStepDispose` single-slots.
|
|
1993
|
+
*
|
|
1994
|
+
* The runner used to remember only the *most recent* step's secrets handle and
|
|
1995
|
+
* dispose closure; the access-log / mount-record / dispose reader callbacks read
|
|
1996
|
+
* that single slot. Under sequential execution that is correct (one step at a
|
|
1997
|
+
* time), but two concurrently-running steps would clobber each other's
|
|
1998
|
+
* secrets-audit trail. Keying every slot by the step's index keeps each step's
|
|
1999
|
+
* audit trail and teardown isolated — sequential behavior is identical, Phase 1
|
|
2000
|
+
* concurrency is correct.
|
|
2001
|
+
*/
|
|
2002
|
+
var StepTaskRegistry = class {
|
|
2003
|
+
#slots = /* @__PURE__ */ new Map();
|
|
2004
|
+
set(stepIndex, slot) {
|
|
2005
|
+
this.#slots.set(stepIndex, slot);
|
|
2006
|
+
}
|
|
2007
|
+
getAccessLog(stepIndex) {
|
|
2008
|
+
return this.#slots.get(stepIndex)?.secrets.getAccessLog() ?? [];
|
|
2009
|
+
}
|
|
2010
|
+
getMountRecords(stepIndex) {
|
|
2011
|
+
const slot = this.#slots.get(stepIndex);
|
|
2012
|
+
return slot ? [...slot.secrets.getMountRecords()] : [];
|
|
2013
|
+
}
|
|
2014
|
+
async dispose(stepIndex) {
|
|
2015
|
+
const slot = this.#slots.get(stepIndex);
|
|
2016
|
+
if (!slot) return;
|
|
2017
|
+
this.#slots.delete(stepIndex);
|
|
2018
|
+
await slot.dispose();
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
//#endregion
|
|
1677
2022
|
//#region src/execution/env-init/init-phase.ts
|
|
1678
2023
|
/** Default init timeout when a spec sets none: 10 minutes. */
|
|
1679
2024
|
const DEFAULT_INIT_TIMEOUT_MS = 600 * 1e3;
|
|
@@ -3148,8 +3493,8 @@ function logSubprocessStreams(e, tokens) {
|
|
|
3148
3493
|
* no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
|
|
3149
3494
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
3150
3495
|
*/
|
|
3151
|
-
const AGENT_SDK_VERSION = "0.1.
|
|
3152
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
3496
|
+
const AGENT_SDK_VERSION = "0.1.25";
|
|
3497
|
+
const AGENT_SDK_BUNDLE_HASH = "e06bd1acf349ba365bdb357f3d865e0cbb4ad4604a0270b707a681a4f440954e";
|
|
3153
3498
|
/**
|
|
3154
3499
|
* Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
|
|
3155
3500
|
* subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
|
|
@@ -3511,7 +3856,7 @@ async function applyOverlay(config) {
|
|
|
3511
3856
|
*/
|
|
3512
3857
|
init_download();
|
|
3513
3858
|
init_dep_restore();
|
|
3514
|
-
const AGENT_VERSION = "0.1.
|
|
3859
|
+
const AGENT_VERSION = "0.1.25";
|
|
3515
3860
|
process.on("uncaughtException", (err) => {
|
|
3516
3861
|
process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
|
|
3517
3862
|
if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
|
|
@@ -3534,12 +3879,6 @@ const isForkMode = typeof process.send === "function";
|
|
|
3534
3879
|
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
3535
3880
|
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
3536
3881
|
/**
|
|
3537
|
-
* Current step index for console.log/console.error capture.
|
|
3538
|
-
* When >= 0, process.stdout/stderr writes are intercepted and sent as
|
|
3539
|
-
* log.line IPC messages for the given step. Set to -1 outside step execution.
|
|
3540
|
-
*/
|
|
3541
|
-
let captureStepIndex = -1;
|
|
3542
|
-
/**
|
|
3543
3882
|
* Workflow-level capture flag for the pre-step `prepare` phase
|
|
3544
3883
|
* (module load, concurrency-group evaluation, rule evaluation).
|
|
3545
3884
|
*
|
|
@@ -3550,17 +3889,18 @@ let captureStepIndex = -1;
|
|
|
3550
3889
|
* rule check functions lands in the job's workflow-level log file
|
|
3551
3890
|
* (`executions/{runId}/job-{name}/step--1.log`) alongside runner narration.
|
|
3552
3891
|
*
|
|
3553
|
-
* Mutually exclusive with
|
|
3554
|
-
*
|
|
3892
|
+
* Mutually exclusive with a step capture scope: the step loop resets this flag
|
|
3893
|
+
* to false before any step runs inside its {@link runInStepCapture} scope (the
|
|
3894
|
+
* async-context source of {@link currentCaptureStepIndex}).
|
|
3555
3895
|
*/
|
|
3556
3896
|
let capturePrepareActive = false;
|
|
3557
3897
|
/** The maskedSend function used by the output capture. Set during main(). */
|
|
3558
3898
|
let captureSendFn = null;
|
|
3559
3899
|
function captureIsActive() {
|
|
3560
|
-
return (
|
|
3900
|
+
return (currentCaptureStepIndex() >= 0 || capturePrepareActive) && captureSendFn !== null;
|
|
3561
3901
|
}
|
|
3562
3902
|
function captureTargetIndex() {
|
|
3563
|
-
return
|
|
3903
|
+
return currentCaptureStepIndex();
|
|
3564
3904
|
}
|
|
3565
3905
|
/**
|
|
3566
3906
|
* Install monkey-patches on process.stdout.write and process.stderr.write
|
|
@@ -3709,6 +4049,14 @@ let jobTimedOutMs;
|
|
|
3709
4049
|
*/
|
|
3710
4050
|
const jobDeadlineAbort = new AbortController();
|
|
3711
4051
|
/**
|
|
4052
|
+
* Aborted when the job is being torn down by cancellation (abort IPC / SIGTERM),
|
|
4053
|
+
* as distinct from the wall-clock deadline (`jobDeadlineAbort`). Each step's
|
|
4054
|
+
* `ctx.signal` composes this with the deadline signal and its own per-step
|
|
4055
|
+
* controller, so a cancelled job cooperatively unwinds in-flight step bodies
|
|
4056
|
+
* that opted into `ctx.signal`. Inert for the rest of the runner today.
|
|
4057
|
+
*/
|
|
4058
|
+
const jobCancelAbort = new AbortController();
|
|
4059
|
+
/**
|
|
3712
4060
|
* Pending promises for event.emit requests awaiting responses from the agent.
|
|
3713
4061
|
*
|
|
3714
4062
|
* Key: requestId (correlates EventEmitRequest -> EventEmitResponse)
|
|
@@ -4015,9 +4363,20 @@ async function relayProvenanceIpc(request) {
|
|
|
4015
4363
|
return response;
|
|
4016
4364
|
}
|
|
4017
4365
|
/**
|
|
4366
|
+
* Extract an `owner/repo` identifier from a git clone URL for a deferred
|
|
4367
|
+
* statement's `externalParameters.workflow.repository` (the live path reads this
|
|
4368
|
+
* from the minted token claim; the deferred path has no token yet).
|
|
4369
|
+
*/
|
|
4370
|
+
function extractRepoIdentifier(repoUrl) {
|
|
4371
|
+
const match = repoUrl.match(/(?:github|gitlab|bitbucket)\.\w+\/([^/]+\/[^/.]+)/);
|
|
4372
|
+
return match ? match[1] : "unknown/unknown";
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4018
4375
|
* Build the `ctx.attestProvenance` step helper. Resolves a `path` subject to a
|
|
4019
4376
|
* SHA-256 digest, threads the identity token via the supplied OIDC getter, and
|
|
4020
|
-
* persists the bundle over the IPC -> WS provenance-upload relay.
|
|
4377
|
+
* persists the bundle over the IPC -> WS provenance-upload relay. On a transient
|
|
4378
|
+
* mint failure the statement is frozen + reported for later minting (deferred);
|
|
4379
|
+
* the step still completes.
|
|
4021
4380
|
*/
|
|
4022
4381
|
function buildAttestProvenanceFn(request, workDir, getIdToken) {
|
|
4023
4382
|
return async (opts) => {
|
|
@@ -4034,6 +4393,27 @@ function buildAttestProvenanceFn(request, workDir, getIdToken) {
|
|
|
4034
4393
|
"kici-agent": AGENT_VERSION,
|
|
4035
4394
|
"kici-orchestrator": "unknown"
|
|
4036
4395
|
},
|
|
4396
|
+
localContext: {
|
|
4397
|
+
repository: extractRepoIdentifier(request.repoUrl),
|
|
4398
|
+
ref: request.ref,
|
|
4399
|
+
sha: request.sha || null,
|
|
4400
|
+
workflowRef: request.workflowRef ?? request.workflowName,
|
|
4401
|
+
runId: request.runId,
|
|
4402
|
+
jobId: request.jobId,
|
|
4403
|
+
issuer: request.provenanceIssuer ?? ""
|
|
4404
|
+
},
|
|
4405
|
+
reportDeferred: async (report) => {
|
|
4406
|
+
await relayProvenanceIpc({
|
|
4407
|
+
op: "defer",
|
|
4408
|
+
subjectDigest: report.subjectDigest,
|
|
4409
|
+
subjectName: report.subjectName,
|
|
4410
|
+
mediaType: report.mediaType,
|
|
4411
|
+
audience: report.audience,
|
|
4412
|
+
statementHash: report.statementHash,
|
|
4413
|
+
dsseEnvelope: report.dsseEnvelope,
|
|
4414
|
+
publicKey: report.publicKey
|
|
4415
|
+
});
|
|
4416
|
+
},
|
|
4037
4417
|
persist: async (bundle, subjectDigest) => {
|
|
4038
4418
|
const urlResponse = await relayProvenanceIpc({
|
|
4039
4419
|
op: "requestUploadUrl",
|
|
@@ -4053,6 +4433,11 @@ function buildAttestProvenanceFn(request, workDir, getIdToken) {
|
|
|
4053
4433
|
subject,
|
|
4054
4434
|
...opts.audience !== void 0 && { audience: opts.audience }
|
|
4055
4435
|
});
|
|
4436
|
+
if ("deferred" in result) return {
|
|
4437
|
+
deferred: true,
|
|
4438
|
+
subjectDigest: result.subjectDigest,
|
|
4439
|
+
statementHash: result.statementHash
|
|
4440
|
+
};
|
|
4056
4441
|
return {
|
|
4057
4442
|
storageKey: result.storageKey,
|
|
4058
4443
|
subjectDigest: result.subjectDigest,
|
|
@@ -4072,17 +4457,16 @@ function buildAttestProvenanceFn(request, workDir, getIdToken) {
|
|
|
4072
4457
|
* post-loop save phase skip exact-key hits.
|
|
4073
4458
|
*/
|
|
4074
4459
|
async function setupJobCache(stepCwd, stepCount, jobCache, sendIpc) {
|
|
4075
|
-
let cacheStepCursor = stepCount * 3 + 100;
|
|
4076
4460
|
const cachePhaseDeps = {
|
|
4077
4461
|
cache: createCacheApi(stepCwd, buildCacheTransport()),
|
|
4078
4462
|
sendIpc,
|
|
4079
|
-
nextStepIndex: ()
|
|
4463
|
+
nextStepIndex: createCacheStepIndexAllocator(stepCount)
|
|
4080
4464
|
};
|
|
4081
4465
|
const jobCacheSpecs = normalizeCacheSpecs(jobCache);
|
|
4082
4466
|
return {
|
|
4083
4467
|
cachePhaseDeps,
|
|
4084
4468
|
jobCacheSpecs,
|
|
4085
|
-
jobCacheRestore: jobCacheSpecs.length > 0 ? await restoreCacheSpecs(jobCacheSpecs, cachePhaseDeps) : /* @__PURE__ */ new Map()
|
|
4469
|
+
jobCacheRestore: jobCacheSpecs.length > 0 ? await restoreCacheSpecs(jobCacheSpecs, cachePhaseDeps, -1) : /* @__PURE__ */ new Map()
|
|
4086
4470
|
};
|
|
4087
4471
|
}
|
|
4088
4472
|
/**
|
|
@@ -4095,7 +4479,7 @@ async function setupJobCache(stepCwd, stepCount, jobCache, sendIpc) {
|
|
|
4095
4479
|
*/
|
|
4096
4480
|
async function maybeSaveJobCache(specs, restoreResults, deps, succeeded) {
|
|
4097
4481
|
if (specs.length === 0 || !succeeded) return;
|
|
4098
|
-
await saveCacheSpecs(specs, restoreResults, deps);
|
|
4482
|
+
await saveCacheSpecs(specs, restoreResults, deps, -1);
|
|
4099
4483
|
}
|
|
4100
4484
|
/**
|
|
4101
4485
|
* Dispatch an agent-to-runner message to the appropriate pending handler.
|
|
@@ -4104,6 +4488,7 @@ async function maybeSaveJobCache(specs, restoreResults, deps, succeeded) {
|
|
|
4104
4488
|
function dispatchAgentMessage(msg) {
|
|
4105
4489
|
if (msg.type === "abort") {
|
|
4106
4490
|
aborted = true;
|
|
4491
|
+
jobCancelAbort.abort();
|
|
4107
4492
|
if (msg.force) forceAborted = true;
|
|
4108
4493
|
} else if (msg.type === "event.emit.response") {
|
|
4109
4494
|
const pending = pendingEmitResponses.get(msg.requestId);
|
|
@@ -4136,6 +4521,7 @@ else getStdinRl().on("line", (line) => {
|
|
|
4136
4521
|
});
|
|
4137
4522
|
process.on("SIGTERM", () => {
|
|
4138
4523
|
aborted = true;
|
|
4524
|
+
jobCancelAbort.abort();
|
|
4139
4525
|
});
|
|
4140
4526
|
/**
|
|
4141
4527
|
* Create a Logger that sends log lines via IPC.
|
|
@@ -4370,7 +4756,7 @@ function buildSandboxShell(cwd, stepIndex, maskedSendFn) {
|
|
|
4370
4756
|
* NOT serialized across the process boundary. This means zx $ runs natively
|
|
4371
4757
|
* inside this process with full shell access.
|
|
4372
4758
|
*/
|
|
4373
|
-
function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedSendFn, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, secrets, masker) {
|
|
4759
|
+
function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedSendFn, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, secrets, masker, signal) {
|
|
4374
4760
|
const step$ = buildSandboxShell(workDir, stepIndex, maskedSendFn);
|
|
4375
4761
|
const log = createIpcLogger(stepIndex, stepName, maskedSendFn);
|
|
4376
4762
|
const rawPayload = rawPayloadFromEvent(request.event);
|
|
@@ -4389,6 +4775,7 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
4389
4775
|
return {
|
|
4390
4776
|
$: step$,
|
|
4391
4777
|
log,
|
|
4778
|
+
signal,
|
|
4392
4779
|
env: process.env,
|
|
4393
4780
|
setEnv: (key, value) => {
|
|
4394
4781
|
applyEnvDelta({
|
|
@@ -4962,7 +5349,7 @@ async function runCancelPathHooks(args) {
|
|
|
4962
5349
|
const ctx = createStepCtxWithCapture(hookStepIndex, label);
|
|
4963
5350
|
let hookResult;
|
|
4964
5351
|
try {
|
|
4965
|
-
hookResult = await executeHook({
|
|
5352
|
+
hookResult = await runInStepCapture(hookStepIndex, () => executeHook({
|
|
4966
5353
|
hook,
|
|
4967
5354
|
stepContext: ctx,
|
|
4968
5355
|
outcome: failedStep ? {
|
|
@@ -4972,9 +5359,9 @@ async function runCancelPathHooks(args) {
|
|
|
4972
5359
|
hookType,
|
|
4973
5360
|
stepIndex: hookStepIndex,
|
|
4974
5361
|
sendIpc: maskedSend
|
|
4975
|
-
});
|
|
5362
|
+
}));
|
|
4976
5363
|
} finally {
|
|
4977
|
-
await disposeStepResources();
|
|
5364
|
+
await disposeStepResources(hookStepIndex);
|
|
4978
5365
|
}
|
|
4979
5366
|
hookStepIndex++;
|
|
4980
5367
|
if (!hookResult.success) {
|
|
@@ -5025,29 +5412,66 @@ async function extractAndNormalizeSteps(workflow, request, apiTransport) {
|
|
|
5025
5412
|
} else rawSteps = extractSteps(workflow, request.jobName);
|
|
5026
5413
|
const refMap = /* @__PURE__ */ new WeakMap();
|
|
5027
5414
|
let stepCounter = 0;
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5415
|
+
const coerce = (stepOrFn) => {
|
|
5416
|
+
if (typeof stepOrFn === "function") {
|
|
5417
|
+
stepCounter++;
|
|
5418
|
+
const name = `step-${stepCounter}`;
|
|
5419
|
+
refMap.set(stepOrFn, name);
|
|
5420
|
+
return {
|
|
5421
|
+
_tag: "Step",
|
|
5422
|
+
name,
|
|
5423
|
+
run: stepOrFn,
|
|
5424
|
+
outputs: void 0
|
|
5425
|
+
};
|
|
5426
|
+
}
|
|
5427
|
+
const s = stepOrFn;
|
|
5428
|
+
if (!s.name) {
|
|
5429
|
+
stepCounter++;
|
|
5430
|
+
return {
|
|
5431
|
+
...s,
|
|
5432
|
+
name: `step-${stepCounter}`
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
return s;
|
|
5436
|
+
};
|
|
5437
|
+
const normalizedSteps = [];
|
|
5438
|
+
const nodes = [];
|
|
5439
|
+
let flatIndex = 0;
|
|
5440
|
+
let groupOrdinal = 0;
|
|
5441
|
+
for (const entry of rawSteps) {
|
|
5442
|
+
if (isParallelGroup(entry)) {
|
|
5443
|
+
const groupId = `g${groupOrdinal++}`;
|
|
5444
|
+
const children = entry.steps.map((child) => {
|
|
5445
|
+
const step = coerce(child);
|
|
5446
|
+
const stepIndex = flatIndex++;
|
|
5447
|
+
normalizedSteps.push(step);
|
|
5044
5448
|
return {
|
|
5045
|
-
|
|
5046
|
-
|
|
5449
|
+
step,
|
|
5450
|
+
stepIndex
|
|
5047
5451
|
};
|
|
5048
|
-
}
|
|
5049
|
-
|
|
5050
|
-
|
|
5452
|
+
});
|
|
5453
|
+
nodes.push({
|
|
5454
|
+
kind: "parallel",
|
|
5455
|
+
groupId,
|
|
5456
|
+
name: entry.name ?? groupId,
|
|
5457
|
+
failFast: entry.failFast,
|
|
5458
|
+
...entry.maxParallel !== void 0 && { maxParallel: entry.maxParallel },
|
|
5459
|
+
children
|
|
5460
|
+
});
|
|
5461
|
+
continue;
|
|
5462
|
+
}
|
|
5463
|
+
const step = coerce(entry);
|
|
5464
|
+
const stepIndex = flatIndex++;
|
|
5465
|
+
normalizedSteps.push(step);
|
|
5466
|
+
nodes.push({
|
|
5467
|
+
kind: "sequential",
|
|
5468
|
+
step,
|
|
5469
|
+
stepIndex
|
|
5470
|
+
});
|
|
5471
|
+
}
|
|
5472
|
+
return {
|
|
5473
|
+
normalizedSteps,
|
|
5474
|
+
nodes,
|
|
5051
5475
|
refMap,
|
|
5052
5476
|
driftDroppedJobs
|
|
5053
5477
|
};
|
|
@@ -5144,22 +5568,40 @@ async function applyEnvFilesDelta(envFiles, operatorSecretKeys, maskedSend) {
|
|
|
5144
5568
|
await truncateEnvFiles(envFiles);
|
|
5145
5569
|
}
|
|
5146
5570
|
/**
|
|
5147
|
-
* Build the step loop's KICI_ENV/KICI_PATH callbacks
|
|
5148
|
-
*
|
|
5149
|
-
*
|
|
5571
|
+
* Build the step loop's KICI_ENV/KICI_PATH callbacks with a per-step delta-file
|
|
5572
|
+
* pair (keyed by step index). `beforeStepEnvFiles(stepIndex)` lazily creates the
|
|
5573
|
+
* step's pair and points the runner's process.env at it (each step's zx $
|
|
5574
|
+
* snapshots process.env at context creation, which happens AFTER this
|
|
5150
5575
|
* before-hook, so the shell sees them; the pre-fork env allowlist does not
|
|
5151
|
-
* re-filter runtime-set vars). `afterStepApplyEnvFiles` applies
|
|
5152
|
-
* delta
|
|
5153
|
-
|
|
5154
|
-
|
|
5576
|
+
* re-filter runtime-set vars). `afterStepApplyEnvFiles(stepIndex)` applies that
|
|
5577
|
+
* step's delta and releases the pair.
|
|
5578
|
+
*
|
|
5579
|
+
* Env-isolation contract: under sequential execution this is identical to a
|
|
5580
|
+
* single shared pair truncated between steps — each step still sees only its own
|
|
5581
|
+
* delta. The pair is now per-step so two concurrently-running steps cannot
|
|
5582
|
+
* corrupt each other's delta file. `process.env.KICI_ENV` / `process.env.KICI_PATH`
|
|
5583
|
+
* remain process-global, so Phase 1 forbids `setEnv` / `addPath` / `$KICI_ENV`
|
|
5584
|
+
* writes inside `parallel()` children (compile-time validation); Phase 0 only
|
|
5585
|
+
* makes the file pair per-task.
|
|
5586
|
+
*/
|
|
5587
|
+
function buildStepEnvFileHooks(operatorSecretKeys, maskedSend) {
|
|
5588
|
+
const perStep = /* @__PURE__ */ new Map();
|
|
5155
5589
|
return {
|
|
5156
|
-
beforeStepEnvFiles: async () => {
|
|
5157
|
-
|
|
5158
|
-
|
|
5590
|
+
beforeStepEnvFiles: async (stepIndex) => {
|
|
5591
|
+
let files = perStep.get(stepIndex);
|
|
5592
|
+
if (!files) {
|
|
5593
|
+
files = await createEnvFiles(tmpdir());
|
|
5594
|
+
perStep.set(stepIndex, files);
|
|
5595
|
+
}
|
|
5596
|
+
process.env.KICI_ENV = files.envFile;
|
|
5597
|
+
process.env.KICI_PATH = files.pathFile;
|
|
5159
5598
|
},
|
|
5160
|
-
afterStepApplyEnvFiles: async () => {
|
|
5599
|
+
afterStepApplyEnvFiles: async (stepIndex) => {
|
|
5600
|
+
const files = perStep.get(stepIndex);
|
|
5601
|
+
if (!files) return;
|
|
5602
|
+
perStep.delete(stepIndex);
|
|
5161
5603
|
try {
|
|
5162
|
-
await applyEnvFilesDelta(
|
|
5604
|
+
await applyEnvFilesDelta(files, operatorSecretKeys, maskedSend);
|
|
5163
5605
|
} catch (err) {
|
|
5164
5606
|
maskedSend({
|
|
5165
5607
|
type: "log.line",
|
|
@@ -5294,7 +5736,7 @@ async function main() {
|
|
|
5294
5736
|
});
|
|
5295
5737
|
return waitForApiResponse(reqId);
|
|
5296
5738
|
};
|
|
5297
|
-
const { normalizedSteps, refMap, driftDroppedJobs } = await extractAndNormalizeSteps(workflow, request, apiTransport);
|
|
5739
|
+
const { normalizedSteps, nodes, refMap, driftDroppedJobs } = await extractAndNormalizeSteps(workflow, request, apiTransport);
|
|
5298
5740
|
const { operatorSecretKeys, outputsMap, secretOutputs, jobOutputsMap } = buildOutputInfrastructure(request, refMap);
|
|
5299
5741
|
const job = findJob(workflow, request.jobName);
|
|
5300
5742
|
await maybeSkipJobOnRules(job, request, normalizedSteps);
|
|
@@ -5304,33 +5746,43 @@ async function main() {
|
|
|
5304
5746
|
flushOutputCapture();
|
|
5305
5747
|
capturePrepareActive = false;
|
|
5306
5748
|
const stepCwd = sourceDir;
|
|
5307
|
-
const envFiles = await createEnvFiles(tmpdir());
|
|
5308
5749
|
await runInitPhaseOrFailJob({
|
|
5309
5750
|
job,
|
|
5310
5751
|
stepCwd,
|
|
5311
|
-
envFiles,
|
|
5752
|
+
envFiles: await createEnvFiles(tmpdir()),
|
|
5312
5753
|
operatorSecretKeys,
|
|
5313
5754
|
maskedSend
|
|
5314
5755
|
});
|
|
5315
5756
|
const { cachePhaseDeps, jobCacheSpecs, jobCacheRestore } = await setupJobCache(stepCwd, normalizedSteps.length, job?.cache, maskedSend);
|
|
5316
|
-
|
|
5317
|
-
|
|
5757
|
+
const stepTasks = new StepTaskRegistry();
|
|
5758
|
+
const stepAbortControllers = /* @__PURE__ */ new Map();
|
|
5318
5759
|
const createStepCtxWithCapture = (stepIndex, stepName) => {
|
|
5319
|
-
|
|
5760
|
+
const stepAbort = new AbortController();
|
|
5761
|
+
stepAbortControllers.set(stepIndex, stepAbort);
|
|
5762
|
+
const signal = AbortSignal.any([
|
|
5763
|
+
stepAbort.signal,
|
|
5764
|
+
jobCancelAbort.signal,
|
|
5765
|
+
jobDeadlineAbort.signal
|
|
5766
|
+
]);
|
|
5320
5767
|
const handle = buildStepSecrets(request, masker, () => {});
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5768
|
+
stepTasks.set(stepIndex, {
|
|
5769
|
+
secrets: handle.secrets,
|
|
5770
|
+
dispose: handle.dispose
|
|
5771
|
+
});
|
|
5772
|
+
const ctx = createSandboxStepContext(stepCwd, stepIndex, stepName, request, maskedSend, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, handle.secrets, masker, signal);
|
|
5324
5773
|
if (globalRepoInfo) {
|
|
5325
5774
|
ctx.workflowRepo = globalRepoInfo.workflowRepo;
|
|
5326
5775
|
ctx.sourceRepo = globalRepoInfo.sourceRepo;
|
|
5327
5776
|
}
|
|
5328
5777
|
return ctx;
|
|
5329
5778
|
};
|
|
5330
|
-
const stepEnvHooks = buildStepEnvFileHooks(
|
|
5779
|
+
const stepEnvHooks = buildStepEnvFileHooks(operatorSecretKeys, maskedSend);
|
|
5331
5780
|
const jobStartTime = Date.now();
|
|
5332
5781
|
const loopResult = await executeStepLoop({
|
|
5333
5782
|
steps: normalizedSteps,
|
|
5783
|
+
stepNodes: nodes,
|
|
5784
|
+
abortStep: (stepIndex) => stepAbortControllers.get(stepIndex)?.abort(),
|
|
5785
|
+
getStepAbortSignal: (stepIndex) => stepAbortControllers.get(stepIndex)?.signal,
|
|
5334
5786
|
checkMode: request.checkMode,
|
|
5335
5787
|
createStepContext: createStepCtxWithCapture,
|
|
5336
5788
|
sendIpc: maskedSend,
|
|
@@ -5345,19 +5797,15 @@ async function main() {
|
|
|
5345
5797
|
isAborted: () => aborted,
|
|
5346
5798
|
jobDeadlineSignal: jobDeadlineAbort.signal,
|
|
5347
5799
|
startTime: jobStartTime,
|
|
5348
|
-
|
|
5800
|
+
runWithStepCapture: runInStepCapture,
|
|
5801
|
+
getSecretsAccessLog: (stepIndex) => {
|
|
5349
5802
|
flushOutputCapture();
|
|
5350
|
-
|
|
5351
|
-
return currentStepSecrets?.getAccessLog() ?? [];
|
|
5352
|
-
},
|
|
5353
|
-
getSecretMountRecords: () => {
|
|
5354
|
-
return currentStepSecrets ? [...currentStepSecrets.getMountRecords()] : [];
|
|
5803
|
+
return stepTasks.getAccessLog(stepIndex);
|
|
5355
5804
|
},
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
if (disposeFn) await disposeFn();
|
|
5805
|
+
getSecretMountRecords: (stepIndex) => stepTasks.getMountRecords(stepIndex),
|
|
5806
|
+
disposeStepResources: (stepIndex) => {
|
|
5807
|
+
stepAbortControllers.delete(stepIndex);
|
|
5808
|
+
return stepTasks.dispose(stepIndex);
|
|
5361
5809
|
},
|
|
5362
5810
|
beforeStepEnvFiles: stepEnvHooks.beforeStepEnvFiles,
|
|
5363
5811
|
afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles,
|
|
@@ -5377,11 +5825,9 @@ async function main() {
|
|
|
5377
5825
|
jobStartTime,
|
|
5378
5826
|
outputsMap,
|
|
5379
5827
|
createStepCtxWithCapture,
|
|
5380
|
-
disposeStepResources:
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
currentStepSecrets = null;
|
|
5384
|
-
if (disposeFn) await disposeFn();
|
|
5828
|
+
disposeStepResources: (stepIndex) => {
|
|
5829
|
+
stepAbortControllers.delete(stepIndex);
|
|
5830
|
+
return stepTasks.dispose(stepIndex);
|
|
5385
5831
|
},
|
|
5386
5832
|
maskedSend
|
|
5387
5833
|
});
|
|
@@ -5448,6 +5894,6 @@ main().catch((error) => {
|
|
|
5448
5894
|
setTimeout(() => process.exit(1), 100);
|
|
5449
5895
|
});
|
|
5450
5896
|
//#endregion
|
|
5451
|
-
export { buildStepNeedsContext, createSandboxStepContext, deriveFanout, rawPayloadFromEvent };
|
|
5897
|
+
export { buildStepEnvFileHooks, buildStepNeedsContext, createSandboxStepContext, deriveFanout, rawPayloadFromEvent };
|
|
5452
5898
|
|
|
5453
5899
|
//# sourceMappingURL=workflow-runner.js.map
|