@kici-dev/agent 0.1.15 → 0.1.17
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/diagnostics/mini-bundle.d.ts +9 -0
- package/dist/execution/dep-installer.d.ts +7 -3
- package/dist/execution/dep-packer.d.ts +6 -1
- package/dist/execution/env-init/presets/directives.d.ts +20 -0
- package/dist/execution/env-init/presets/expand.d.ts +16 -0
- package/dist/execution/env-init/presets/mise/cache-key.d.ts +7 -0
- package/dist/execution/env-init/presets/mise/expander.d.ts +16 -0
- package/dist/execution/env-init/presets/mise/templates.d.ts +16 -0
- package/dist/execution/env-init/presets/mise/windows-install.d.ts +18 -0
- package/dist/execution/env-init/presets/registry.d.ts +31 -0
- package/dist/execution/init-runner.d.ts +7 -0
- package/dist/execution/job-runner.d.ts +18 -1
- package/dist/execution/sandbox/env-delta.d.ts +2 -0
- package/dist/execution/sandbox/index.d.ts +1 -1
- package/dist/execution/sandbox/ipc-protocol.d.ts +101 -3
- package/dist/execution/sandbox/step-loop.d.ts +24 -1
- package/dist/execution/sandbox/types.d.ts +18 -1
- package/dist/execution/sandbox/workflow-runner.d.ts +12 -7
- package/dist/execution/validate-kici-deps.d.ts +5 -0
- package/dist/execution/workspace-siblings.d.ts +33 -0
- package/dist/index.js +223 -6
- package/dist/provenance/attest.d.ts +30 -0
- package/dist/provenance/sign.d.ts +21 -0
- package/dist/provenance/statement-builder.d.ts +38 -0
- package/dist/server.js +698 -146
- package/dist/version.d.ts +2 -0
- package/dist/workflow-runner.js +877 -45
- package/dist/ws/orchestrator-client.d.ts +53 -4
- package/package.json +8 -5
- package/sbom.spdx.json +1957 -152
package/dist/workflow-runner.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { register } from "node:module";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import crypto, { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import crypto$1, { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
|
-
import fsPromises, { access, cp, mkdir, mkdtemp, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import fsPromises, { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
6
6
|
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
10
|
import { CacheOutcome, CacheStepType, ExecutionJobStatus, ExecutionStepStatus, TimeoutReason } from "@kici-dev/engine";
|
|
11
|
-
import { buildKiciApi, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeCacheSpecs, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
11
|
+
import { buildKiciApi, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeCacheSpecs, normalizeRequireApproval, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
12
|
+
import { OIDC_TOKEN_REQUEST_METHOD } from "@kici-dev/engine/protocol/messages/oidc-token-relay";
|
|
13
|
+
import { sha256File as sha256File$1 } from "@kici-dev/core";
|
|
14
|
+
import { calculateJwkThumbprint, decodeJwt, exportJWK, generateKeyPair } from "jose";
|
|
15
|
+
import { IN_TOTO_PAYLOAD_TYPE, KICI_PROVENANCE_AUDIENCE, KICI_PROVENANCE_BUNDLE_MEDIA_TYPE } from "@kici-dev/engine/provenance/bundle";
|
|
16
|
+
import { IN_TOTO_STATEMENT_TYPE, KICI_WORKFLOW_BUILD_TYPE, SLSA_PROVENANCE_PREDICATE_TYPE } from "@kici-dev/engine/provenance/schema";
|
|
17
|
+
import { buildDsseEnvelope, dssePae } from "@kici-dev/engine/provenance/dsse";
|
|
18
|
+
import https from "node:https";
|
|
19
|
+
import http from "node:http";
|
|
12
20
|
import { Readable, Transform } from "node:stream";
|
|
13
21
|
import { pipeline } from "node:stream/promises";
|
|
14
22
|
import { createGunzip } from "node:zlib";
|
|
15
|
-
import { c, x } from "tar";
|
|
16
|
-
import https from "node:https";
|
|
17
|
-
import http from "node:http";
|
|
18
23
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
|
+
import { c, x } from "tar";
|
|
19
25
|
import { execFile } from "node:child_process";
|
|
20
26
|
import { promisify } from "node:util";
|
|
21
27
|
import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
|
|
@@ -32,6 +38,122 @@ var __exportAll = (all, no_symbols) => {
|
|
|
32
38
|
};
|
|
33
39
|
import.meta.url;
|
|
34
40
|
//#endregion
|
|
41
|
+
//#region src/provenance/statement-builder.ts
|
|
42
|
+
/**
|
|
43
|
+
* Build a SLSA v1.0 in-toto provenance statement from the server-truth identity
|
|
44
|
+
* token claims plus the caller-supplied subject. The build context comes
|
|
45
|
+
* entirely from the JWT claims (Platform-minted, unforgeable), so the
|
|
46
|
+
* statement's identity equals the token's identity by construction.
|
|
47
|
+
*/
|
|
48
|
+
/** Build a KiCI SLSA v1.0 provenance statement (validates against the P1.1 schema). */
|
|
49
|
+
function buildProvenanceStatement(input) {
|
|
50
|
+
const c = input.tokenClaims;
|
|
51
|
+
return {
|
|
52
|
+
_type: IN_TOTO_STATEMENT_TYPE,
|
|
53
|
+
subject: [{
|
|
54
|
+
name: input.subject.name,
|
|
55
|
+
digest: input.subject.digest
|
|
56
|
+
}],
|
|
57
|
+
predicateType: SLSA_PROVENANCE_PREDICATE_TYPE,
|
|
58
|
+
predicate: {
|
|
59
|
+
buildDefinition: {
|
|
60
|
+
buildType: KICI_WORKFLOW_BUILD_TYPE,
|
|
61
|
+
externalParameters: { workflow: {
|
|
62
|
+
repository: c.repository ?? "",
|
|
63
|
+
ref: c.ref ?? "",
|
|
64
|
+
path: c.workflow_ref ?? ""
|
|
65
|
+
} },
|
|
66
|
+
internalParameters: {
|
|
67
|
+
...c.sha ? { commit: c.sha } : {},
|
|
68
|
+
runId: c.kici_run_id,
|
|
69
|
+
jobId: c.kici_job_id
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
runDetails: {
|
|
73
|
+
builder: {
|
|
74
|
+
id: `${c.iss}/orchestrator/${c.orchestrator_id ?? "unknown"}`,
|
|
75
|
+
version: input.builderVersions
|
|
76
|
+
},
|
|
77
|
+
metadata: {
|
|
78
|
+
invocationId: c.kici_run_id,
|
|
79
|
+
startedOn: input.startedOn,
|
|
80
|
+
finishedOn: input.finishedOn
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/provenance/sign.ts
|
|
88
|
+
/**
|
|
89
|
+
* Ephemeral-key DSSE signer for KiCI provenance (Mode A).
|
|
90
|
+
*
|
|
91
|
+
* Generates a fresh in-process ES256 keypair (never persisted), DSSE-signs the
|
|
92
|
+
* PAE of the statement bytes with the private half, and returns the envelope
|
|
93
|
+
* plus the public JWK. The public key travels in the bundle so the verifier can
|
|
94
|
+
* check the signature; the key needs no separate trust root because the bundle's
|
|
95
|
+
* identity JWT (verified against the Platform JWKS) anchors the whole package.
|
|
96
|
+
*/
|
|
97
|
+
/** DSSE-sign `statementBytes` with a fresh in-process ephemeral ES256 key. */
|
|
98
|
+
async function signStatementDsse(payloadType, statementBytes) {
|
|
99
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: true });
|
|
100
|
+
const publicJwk = await exportJWK(publicKey);
|
|
101
|
+
publicJwk.alg = "ES256";
|
|
102
|
+
publicJwk.use = "sig";
|
|
103
|
+
const kid = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
104
|
+
publicJwk.kid = kid;
|
|
105
|
+
const pae = dssePae(payloadType, statementBytes);
|
|
106
|
+
return {
|
|
107
|
+
envelope: buildDsseEnvelope(payloadType, statementBytes, [{
|
|
108
|
+
keyid: kid,
|
|
109
|
+
sig: new Uint8Array(await crypto.subtle.sign({
|
|
110
|
+
name: "ECDSA",
|
|
111
|
+
hash: "SHA-256"
|
|
112
|
+
}, privateKey, pae))
|
|
113
|
+
}]),
|
|
114
|
+
publicJwk
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/provenance/attest.ts
|
|
119
|
+
/**
|
|
120
|
+
* Provenance attestation orchestration (Mode A): request the identity token,
|
|
121
|
+
* build the in-toto statement from its claims, DSSE-sign it with an ephemeral
|
|
122
|
+
* key, assemble the KiCI bundle, and persist it.
|
|
123
|
+
*/
|
|
124
|
+
async function attestProvenance(deps, input) {
|
|
125
|
+
const audience = input.audience ?? KICI_PROVENANCE_AUDIENCE;
|
|
126
|
+
const { token } = await deps.getIdToken({ audience });
|
|
127
|
+
const claims = decodeJwt(token);
|
|
128
|
+
const now = (deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
|
|
129
|
+
const statement = buildProvenanceStatement({
|
|
130
|
+
tokenClaims: claims,
|
|
131
|
+
subject: input.subject,
|
|
132
|
+
builderVersions: deps.builderVersions,
|
|
133
|
+
startedOn: now,
|
|
134
|
+
finishedOn: now
|
|
135
|
+
});
|
|
136
|
+
const { envelope, publicJwk } = await signStatementDsse(IN_TOTO_PAYLOAD_TYPE, new TextEncoder().encode(JSON.stringify(statement)));
|
|
137
|
+
const bundle = {
|
|
138
|
+
mediaType: KICI_PROVENANCE_BUNDLE_MEDIA_TYPE,
|
|
139
|
+
dsseEnvelope: envelope,
|
|
140
|
+
verificationMaterial: {
|
|
141
|
+
publicKey: publicJwk,
|
|
142
|
+
identityToken: token
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const subjectDigest = subjectDigestString(input.subject);
|
|
146
|
+
return {
|
|
147
|
+
storageKey: await deps.persist(bundle, subjectDigest),
|
|
148
|
+
bundle,
|
|
149
|
+
subjectDigest
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** Pick the primary digest (`sha256` preferred) as the storage-key discriminator. */
|
|
153
|
+
function subjectDigestString(subject) {
|
|
154
|
+
return subject.digest.sha256 ?? Object.values(subject.digest)[0];
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
35
157
|
//#region src/execution/dep-restore.ts
|
|
36
158
|
/**
|
|
37
159
|
* Dependency restoration from cached tarballs.
|
|
@@ -370,6 +492,7 @@ var init_download = __esmMin((() => {
|
|
|
370
492
|
}));
|
|
371
493
|
//#endregion
|
|
372
494
|
//#region src/execution/cache/cache-engine.ts
|
|
495
|
+
init_download();
|
|
373
496
|
/**
|
|
374
497
|
* User-facing cache engine (sandbox-side).
|
|
375
498
|
*
|
|
@@ -769,7 +892,8 @@ function applyEnvDelta(delta, options) {
|
|
|
769
892
|
}
|
|
770
893
|
const appliedPaths = [];
|
|
771
894
|
if (delta.pathPrepends.length > 0) {
|
|
772
|
-
|
|
895
|
+
const sep = options.pathSeparator ?? (process.platform === "win32" ? ";" : ":");
|
|
896
|
+
for (const dir of [...delta.pathPrepends].reverse()) target.PATH = target.PATH ? `${dir}${sep}${target.PATH}` : dir;
|
|
773
897
|
appliedPaths.push(...delta.pathPrepends);
|
|
774
898
|
}
|
|
775
899
|
return {
|
|
@@ -1123,6 +1247,65 @@ async function evaluateStepRulesAndMaybeSkip(step, stepIndex, opts) {
|
|
|
1123
1247
|
};
|
|
1124
1248
|
}
|
|
1125
1249
|
/**
|
|
1250
|
+
* Manual approval gate for a step. When the step declares `requireApproval`
|
|
1251
|
+
* and the harness wired `awaitStepApproval`, block until the orchestrator
|
|
1252
|
+
* resolves the hold. Returns a failed `StepIterationOutcome` (breaking the
|
|
1253
|
+
* loop) on reject/expired; returns null when approved or when no gate applies.
|
|
1254
|
+
*/
|
|
1255
|
+
async function maybeGateStepApproval(step, stepIndex, opts) {
|
|
1256
|
+
if (step.requireApproval === void 0 || !opts.awaitStepApproval) return null;
|
|
1257
|
+
const normalized = normalizeRequireApproval(step.requireApproval);
|
|
1258
|
+
opts.sendIpc({
|
|
1259
|
+
type: "log.line",
|
|
1260
|
+
stepIndex,
|
|
1261
|
+
line: `[kici] Step '${step.name}' awaiting approval...`
|
|
1262
|
+
});
|
|
1263
|
+
const resolution = await opts.awaitStepApproval({
|
|
1264
|
+
stepIndex,
|
|
1265
|
+
stepName: step.name,
|
|
1266
|
+
clauses: normalized.clauses,
|
|
1267
|
+
reason: normalized.reason ?? `Approval required for step '${step.name}'`,
|
|
1268
|
+
timeoutSeconds: normalized.timeoutSeconds
|
|
1269
|
+
});
|
|
1270
|
+
if (resolution.outcome === "approved") {
|
|
1271
|
+
opts.sendIpc({
|
|
1272
|
+
type: "log.line",
|
|
1273
|
+
stepIndex,
|
|
1274
|
+
line: `[kici] Step '${step.name}' approved.`
|
|
1275
|
+
});
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
const why = resolution.outcome === "expired" ? "approval expired" : `approval rejected${resolution.reason ? `: ${resolution.reason}` : ""}`;
|
|
1279
|
+
opts.sendIpc({
|
|
1280
|
+
type: "step.start",
|
|
1281
|
+
stepIndex,
|
|
1282
|
+
stepName: step.name
|
|
1283
|
+
});
|
|
1284
|
+
opts.sendIpc({
|
|
1285
|
+
type: "step.complete",
|
|
1286
|
+
stepIndex,
|
|
1287
|
+
status: ExecutionStepStatus.enum.failed,
|
|
1288
|
+
durationMs: 0
|
|
1289
|
+
});
|
|
1290
|
+
opts.sendIpc({
|
|
1291
|
+
type: "log.line",
|
|
1292
|
+
stepIndex,
|
|
1293
|
+
line: `[kici] Step '${step.name}' ${why}.`
|
|
1294
|
+
});
|
|
1295
|
+
await opts.disposeStepResources?.();
|
|
1296
|
+
return {
|
|
1297
|
+
result: {
|
|
1298
|
+
name: step.name,
|
|
1299
|
+
stepIndex,
|
|
1300
|
+
status: ExecutionStepStatus.enum.failed,
|
|
1301
|
+
durationMs: 0,
|
|
1302
|
+
error: { message: `Step '${step.name}' ${why}` }
|
|
1303
|
+
},
|
|
1304
|
+
shouldBreak: true,
|
|
1305
|
+
failedStepName: step.name
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1126
1309
|
* Run a single observer hook (beforeStep / afterStep). Failures only emit a
|
|
1127
1310
|
* log line — they never change job status. Centralises the per-call boilerplate
|
|
1128
1311
|
* so the per-step body can stay flat.
|
|
@@ -1169,6 +1352,8 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1169
1352
|
shouldBreak: false
|
|
1170
1353
|
};
|
|
1171
1354
|
}
|
|
1355
|
+
const gate = await maybeGateStepApproval(step, stepIndex, opts);
|
|
1356
|
+
if (gate) return gate;
|
|
1172
1357
|
if (opts.jobHooks?.beforeStep) await runObserverHook({
|
|
1173
1358
|
hook: opts.jobHooks.beforeStep,
|
|
1174
1359
|
hookType: "beforeStep",
|
|
@@ -1453,6 +1638,256 @@ async function runOneInit(spec, index, stepIndex, stepType, opts) {
|
|
|
1453
1638
|
}
|
|
1454
1639
|
}
|
|
1455
1640
|
//#endregion
|
|
1641
|
+
//#region src/execution/env-init/presets/directives.ts
|
|
1642
|
+
function isPresetString(item) {
|
|
1643
|
+
return item === "mise";
|
|
1644
|
+
}
|
|
1645
|
+
function isMiseObject(item) {
|
|
1646
|
+
return typeof item === "object" && item !== null && "mise" in item;
|
|
1647
|
+
}
|
|
1648
|
+
function normalizeOne(item) {
|
|
1649
|
+
if (isPresetString(item)) return {
|
|
1650
|
+
kind: "preset",
|
|
1651
|
+
name: "mise",
|
|
1652
|
+
config: {}
|
|
1653
|
+
};
|
|
1654
|
+
if (isMiseObject(item)) return {
|
|
1655
|
+
kind: "preset",
|
|
1656
|
+
name: "mise",
|
|
1657
|
+
config: item.mise
|
|
1658
|
+
};
|
|
1659
|
+
return {
|
|
1660
|
+
kind: "generic",
|
|
1661
|
+
config: item
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Normalize `Job.init` to an ordered list of directives, without touching the
|
|
1666
|
+
* filesystem. `false`/`undefined` -> []; `'auto'` -> one auto directive;
|
|
1667
|
+
* presets/generic configs -> their directive; arrays map element-wise.
|
|
1668
|
+
* `'auto'` is a scalar only — finding it inside an array throws.
|
|
1669
|
+
*/
|
|
1670
|
+
function normalizeInitItems(job) {
|
|
1671
|
+
const init = job?.init;
|
|
1672
|
+
if (init === void 0 || init === false) return [];
|
|
1673
|
+
if (init === "auto") return [{ kind: "auto" }];
|
|
1674
|
+
if (Array.isArray(init)) return init.map((item) => {
|
|
1675
|
+
if (item === "auto") throw new Error("init: 'auto' cannot be combined in an array — use it as the sole value");
|
|
1676
|
+
return normalizeOne(item);
|
|
1677
|
+
});
|
|
1678
|
+
return [normalizeOne(init)];
|
|
1679
|
+
}
|
|
1680
|
+
//#endregion
|
|
1681
|
+
//#region src/execution/env-init/presets/mise/cache-key.ts
|
|
1682
|
+
/** mise config files, in the fixed order they feed the content hash. */
|
|
1683
|
+
const MISE_CONFIG_FILES = [
|
|
1684
|
+
"mise.toml",
|
|
1685
|
+
".mise.toml",
|
|
1686
|
+
".tool-versions"
|
|
1687
|
+
];
|
|
1688
|
+
/**
|
|
1689
|
+
* Derive the default mise cache key from the committed mise config under
|
|
1690
|
+
* `cloneRoot`. Concatenates whichever of {@link MISE_CONFIG_FILES} exist (in
|
|
1691
|
+
* fixed order) and hashes them. Returns `mise-noconfig` when none exist.
|
|
1692
|
+
*/
|
|
1693
|
+
async function miseCacheKey(cloneRoot) {
|
|
1694
|
+
const hash = createHash("sha256");
|
|
1695
|
+
let found = false;
|
|
1696
|
+
for (const name of MISE_CONFIG_FILES) try {
|
|
1697
|
+
const buf = await readFile(join(cloneRoot, name));
|
|
1698
|
+
hash.update(name);
|
|
1699
|
+
hash.update(buf);
|
|
1700
|
+
found = true;
|
|
1701
|
+
} catch {}
|
|
1702
|
+
if (!found) return "mise-noconfig";
|
|
1703
|
+
return `mise-${hash.digest("hex").slice(0, 16)}`;
|
|
1704
|
+
}
|
|
1705
|
+
//#endregion
|
|
1706
|
+
//#region src/execution/env-init/presets/mise/templates.ts
|
|
1707
|
+
const BASH_RUN = `set -euo pipefail
|
|
1708
|
+
command -v mise >/dev/null || curl -fsSL https://mise.run | sh
|
|
1709
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
1710
|
+
# Trust the committed config at the clone root (CWD): mise refuses to load an
|
|
1711
|
+
# untrusted config, and the author committing it to their repo is the trust signal.
|
|
1712
|
+
mise trust
|
|
1713
|
+
mise install
|
|
1714
|
+
mise env -s bash | sed -n 's/^export //p' | sed '/^PATH=/d' \\
|
|
1715
|
+
| sed -E 's/^([A-Za-z_][A-Za-z0-9_]*)="(.*)"$/\\1=\\2/' >> "$KICI_ENV"
|
|
1716
|
+
echo "$HOME/.local/share/mise/shims" >> "$KICI_PATH"`;
|
|
1717
|
+
const PWSH_RUN = `$ErrorActionPreference = 'Stop'
|
|
1718
|
+
# mise writes informational output (\`mise trusted …\`, install progress) to
|
|
1719
|
+
# stderr even on success. Under \`$ErrorActionPreference = 'Stop'\` PowerShell
|
|
1720
|
+
# turns any native-command stderr line into a terminating error, so a
|
|
1721
|
+
# successful \`mise trust\` would abort the step. Run each mise invocation with
|
|
1722
|
+
# the preference relaxed and gate on the real exit code via \`$LASTEXITCODE\`.
|
|
1723
|
+
function Invoke-Mise {
|
|
1724
|
+
param([Parameter(ValueFromRemainingArguments = $true)][string[]] $MiseArgs)
|
|
1725
|
+
$prev = $ErrorActionPreference
|
|
1726
|
+
$ErrorActionPreference = 'Continue'
|
|
1727
|
+
try {
|
|
1728
|
+
$output = & mise @MiseArgs 2>&1
|
|
1729
|
+
$code = $LASTEXITCODE
|
|
1730
|
+
} finally {
|
|
1731
|
+
$ErrorActionPreference = $prev
|
|
1732
|
+
}
|
|
1733
|
+
if ($code -ne 0) {
|
|
1734
|
+
throw "mise $($MiseArgs -join ' ') failed (exit $code): $($output -join ' | ')"
|
|
1735
|
+
}
|
|
1736
|
+
return $output
|
|
1737
|
+
}
|
|
1738
|
+
if (-not (Get-Command mise -ErrorAction SilentlyContinue)) {
|
|
1739
|
+
# The standalone Windows zip extracts to mise/bin/mise.exe, so prepend the
|
|
1740
|
+
# nested bin dir (not the extraction root) to PATH.
|
|
1741
|
+
$dest = Join-Path $env:USERPROFILE '.local\\mise'
|
|
1742
|
+
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
|
1743
|
+
$zip = Join-Path $env:TEMP 'mise.zip'
|
|
1744
|
+
Invoke-WebRequest -Uri '<ASSET_URL>' -OutFile $zip
|
|
1745
|
+
Expand-Archive -Path $zip -DestinationPath $dest -Force
|
|
1746
|
+
$env:PATH = "$dest\\mise\\bin;$env:PATH"
|
|
1747
|
+
}
|
|
1748
|
+
# Trust the committed config at the clone root (CWD) — mise refuses to load an
|
|
1749
|
+
# untrusted config; the author committing it to their repo is the trust signal.
|
|
1750
|
+
Invoke-Mise trust | Out-Null
|
|
1751
|
+
Invoke-Mise install | Out-Null
|
|
1752
|
+
Invoke-Mise env -s pwsh | ForEach-Object {
|
|
1753
|
+
if ($_ -match '^\\$env:([^=]+) = ''(.*)''$' -and $Matches[1] -ne 'PATH') { "$($Matches[1])=$($Matches[2])" }
|
|
1754
|
+
} | Add-Content -Path $env:KICI_ENV
|
|
1755
|
+
# Add the real tool install dirs (not the shims dir): the standalone mise lives
|
|
1756
|
+
# in a temp dir that is gone by step time, so the shim wrappers (which re-invoke
|
|
1757
|
+
# mise) cannot resolve it. bin-paths points straight at the installed binaries.
|
|
1758
|
+
Invoke-Mise bin-paths | Add-Content -Path $env:KICI_PATH`;
|
|
1759
|
+
/**
|
|
1760
|
+
* Pick the mise template for a host platform (Node `process.platform` value).
|
|
1761
|
+
* The Windows `run` carries an `<ASSET_URL>` placeholder the expander replaces
|
|
1762
|
+
* with the resolved GitHub-release zip URL.
|
|
1763
|
+
*/
|
|
1764
|
+
function selectMiseTemplate(platform) {
|
|
1765
|
+
if (platform === "win32") return {
|
|
1766
|
+
run: PWSH_RUN,
|
|
1767
|
+
shell: "pwsh",
|
|
1768
|
+
cachePaths: ["~/AppData/Local/mise"]
|
|
1769
|
+
};
|
|
1770
|
+
if (platform === "linux" || platform === "darwin") return {
|
|
1771
|
+
run: BASH_RUN,
|
|
1772
|
+
shell: "bash",
|
|
1773
|
+
cachePaths: ["~/.local/share/mise"]
|
|
1774
|
+
};
|
|
1775
|
+
throw new Error(`unsupported platform for mise preset: ${platform}`);
|
|
1776
|
+
}
|
|
1777
|
+
//#endregion
|
|
1778
|
+
//#region src/execution/env-init/presets/mise/windows-install.ts
|
|
1779
|
+
/** Map a Windows `PROCESSOR_ARCHITECTURE` value to mise's asset arch slug. */
|
|
1780
|
+
function miseWindowsArch(processorArch) {
|
|
1781
|
+
return processorArch?.toUpperCase() === "ARM64" ? "arm64" : "x64";
|
|
1782
|
+
}
|
|
1783
|
+
const LATEST_RELEASE_URL = "https://api.github.com/repos/jdx/mise/releases/latest";
|
|
1784
|
+
/**
|
|
1785
|
+
* Resolve the download URL of the latest mise standalone Windows zip for `arch`.
|
|
1786
|
+
* `fetchJson` is injected (defaults to a real fetch) so the resolution is
|
|
1787
|
+
* unit-testable without network.
|
|
1788
|
+
*/
|
|
1789
|
+
async function resolveLatestMiseWindowsAsset(arch, fetchJson = defaultFetchJson) {
|
|
1790
|
+
const release = await fetchJson(LATEST_RELEASE_URL);
|
|
1791
|
+
const suffix = `-windows-${arch}.zip`;
|
|
1792
|
+
const asset = release.assets.find((a) => a.name.endsWith(suffix));
|
|
1793
|
+
if (!asset) throw new Error(`no mise windows ${arch} asset in latest release`);
|
|
1794
|
+
return asset.browser_download_url;
|
|
1795
|
+
}
|
|
1796
|
+
async function defaultFetchJson(url) {
|
|
1797
|
+
const res = await fetch(url, { headers: { "user-agent": "kici-agent" } });
|
|
1798
|
+
if (!res.ok) throw new Error(`mise release lookup failed: ${res.status}`);
|
|
1799
|
+
return await res.json();
|
|
1800
|
+
}
|
|
1801
|
+
//#endregion
|
|
1802
|
+
//#region src/execution/env-init/presets/mise/expander.ts
|
|
1803
|
+
async function buildRun(args, template) {
|
|
1804
|
+
if ((args.platform ?? process.platform) !== "win32") return template.run;
|
|
1805
|
+
const arch = miseWindowsArch(process.env.PROCESSOR_ARCHITECTURE);
|
|
1806
|
+
const url = await (args.resolveWindowsAsset ?? ((a) => resolveLatestMiseWindowsAsset(a)))(arch);
|
|
1807
|
+
return template.run.replace("<ASSET_URL>", url);
|
|
1808
|
+
}
|
|
1809
|
+
async function defaultCache(cloneRoot, paths) {
|
|
1810
|
+
return {
|
|
1811
|
+
key: await miseCacheKey(cloneRoot),
|
|
1812
|
+
paths,
|
|
1813
|
+
restoreKeys: ["mise-"]
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
//#endregion
|
|
1817
|
+
//#region src/execution/env-init/presets/registry.ts
|
|
1818
|
+
/**
|
|
1819
|
+
* The set of typed presets. Nix is added here (one row) once its provider lands.
|
|
1820
|
+
*/
|
|
1821
|
+
const PRESET_REGISTRY = { mise: { async expand(args) {
|
|
1822
|
+
const template = selectMiseTemplate(args.platform ?? process.platform);
|
|
1823
|
+
const run = await buildRun(args, template);
|
|
1824
|
+
const cache = args.config.cache === false ? void 0 : args.config.cache ?? await defaultCache(args.cloneRoot, template.cachePaths);
|
|
1825
|
+
const cfg = {
|
|
1826
|
+
run,
|
|
1827
|
+
shell: args.config.shell ?? template.shell,
|
|
1828
|
+
timeout: args.config.timeout ?? 6e5
|
|
1829
|
+
};
|
|
1830
|
+
if (cache) cfg.cache = cache;
|
|
1831
|
+
if (args.config.env) cfg.env = args.config.env;
|
|
1832
|
+
return cfg;
|
|
1833
|
+
} } };
|
|
1834
|
+
/**
|
|
1835
|
+
* Ordered auto-detect table: `init: 'auto'` tries each row against the clone
|
|
1836
|
+
* root and accumulates matches in this order. (nix row added with its provider.)
|
|
1837
|
+
*/
|
|
1838
|
+
const AUTO_DETECT_TABLE = [{
|
|
1839
|
+
markers: [
|
|
1840
|
+
"mise.toml",
|
|
1841
|
+
".mise.toml",
|
|
1842
|
+
".tool-versions"
|
|
1843
|
+
],
|
|
1844
|
+
preset: "mise"
|
|
1845
|
+
}];
|
|
1846
|
+
//#endregion
|
|
1847
|
+
//#region src/execution/env-init/presets/expand.ts
|
|
1848
|
+
async function fileExists$2(p) {
|
|
1849
|
+
try {
|
|
1850
|
+
await access(p);
|
|
1851
|
+
return true;
|
|
1852
|
+
} catch {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
/** Scan the clone root for marker files and return matched presets in table order. */
|
|
1857
|
+
async function autoDetect(cloneRoot) {
|
|
1858
|
+
const matched = [];
|
|
1859
|
+
for (const row of AUTO_DETECT_TABLE) for (const marker of row.markers) if (await fileExists$2(join(cloneRoot, marker))) {
|
|
1860
|
+
matched.push(row.preset);
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
return matched;
|
|
1864
|
+
}
|
|
1865
|
+
async function expandPreset(name, config, opts) {
|
|
1866
|
+
return PRESET_REGISTRY[name].expand({
|
|
1867
|
+
cloneRoot: opts.cloneRoot,
|
|
1868
|
+
config,
|
|
1869
|
+
...opts.platform ? { platform: opts.platform } : {}
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Expand normalized directives into concrete generic init configs, reading the
|
|
1874
|
+
* clone root for preset cache keys and `'auto'` marker detection.
|
|
1875
|
+
*/
|
|
1876
|
+
async function expandInitDirectives(directives, opts) {
|
|
1877
|
+
const out = [];
|
|
1878
|
+
for (const d of directives) if (d.kind === "generic") out.push(d.config);
|
|
1879
|
+
else if (d.kind === "preset") out.push(await expandPreset(d.name, d.config, opts));
|
|
1880
|
+
else {
|
|
1881
|
+
const presets = await autoDetect(opts.cloneRoot);
|
|
1882
|
+
if (presets.length === 0) {
|
|
1883
|
+
opts.log?.("[kici] init: auto — no toolchain detected (no mise.toml / .tool-versions)");
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
for (const name of presets) out.push(await expandPreset(name, {}, opts));
|
|
1887
|
+
}
|
|
1888
|
+
return out;
|
|
1889
|
+
}
|
|
1890
|
+
//#endregion
|
|
1456
1891
|
//#region src/execution/sandbox/job-deadline.ts
|
|
1457
1892
|
/**
|
|
1458
1893
|
* Arm a job-level wall-clock deadline. When `timeoutMs` is set and elapses
|
|
@@ -1901,6 +2336,17 @@ function isInsideRepo(repoRoot, target) {
|
|
|
1901
2336
|
*/
|
|
1902
2337
|
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
1903
2338
|
if (packageManager === PackageManager.Npm) return [...deps];
|
|
2339
|
+
if (packageManager === PackageManager.Yarn) {
|
|
2340
|
+
const unresolvable = [];
|
|
2341
|
+
for (const dep of deps) {
|
|
2342
|
+
if (dep.protocol === "workspace:" || dep.protocol === "portal:") {
|
|
2343
|
+
unresolvable.push(dep);
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2346
|
+
if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
|
|
2347
|
+
}
|
|
2348
|
+
return unresolvable;
|
|
2349
|
+
}
|
|
1904
2350
|
const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
|
|
1905
2351
|
const unresolvable = [];
|
|
1906
2352
|
for (const dep of deps) {
|
|
@@ -1916,6 +2362,7 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
|
1916
2362
|
function formatUnresolvableDepError(offenders, packageManager) {
|
|
1917
2363
|
const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
|
|
1918
2364
|
if (packageManager === PackageManager.Npm) return `These .kici/ dependencies use local-protocol specifiers npm cannot resolve from a registry: ${list}. npm has no workspace protocol — pin a published version, publish the package to your registry, or use pnpm so an in-repo workspace sibling can be resolved.`;
|
|
2365
|
+
if (packageManager === PackageManager.Yarn) return `These .kici/ dependencies use specifiers yarn classic cannot resolve: ${list}. yarn classic has no workspace: or portal: protocol — reference an in-repo sibling by a version range (yarn links matching workspace members), use pnpm, or keep file:/link: paths inside this repository. (yarn berry support is planned.)`;
|
|
1919
2366
|
return `These .kici/ dependencies point outside the cloned repository, which the agent never has: ${list}. A workspace: dependency requires a pnpm-workspace.yaml at the repo root, and file:/link:/portal: paths must stay inside this repository.`;
|
|
1920
2367
|
}
|
|
1921
2368
|
/**
|
|
@@ -1934,6 +2381,101 @@ async function assertResolvableDeps(args) {
|
|
|
1934
2381
|
throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
|
|
1935
2382
|
}
|
|
1936
2383
|
//#endregion
|
|
2384
|
+
//#region src/execution/workspace-siblings.ts
|
|
2385
|
+
/**
|
|
2386
|
+
* In-repo workspace-sibling discovery for the agent's dependency handling.
|
|
2387
|
+
*
|
|
2388
|
+
* A pnpm or yarn-classic workspace lays out a `.kici/` member's `workspace:`
|
|
2389
|
+
* (pnpm) or version-range (yarn) siblings as symlinks pointing at package
|
|
2390
|
+
* directories that live inside the clone but outside `.kici/` and outside the
|
|
2391
|
+
* `node_modules` store. The dep-cache packer must travel those sibling dirs with
|
|
2392
|
+
* the closure (their symlinks would dangle otherwise), and the yarn install path
|
|
2393
|
+
* must build them (the install links a sibling but does not build it).
|
|
2394
|
+
*
|
|
2395
|
+
* `collectInRepoSiblings` walks a starting `node_modules` (and transitively each
|
|
2396
|
+
* discovered sibling's `node_modules`), returning each in-repo sibling directory
|
|
2397
|
+
* once, repo-root-relative, in breadth-first discovery order. The starting
|
|
2398
|
+
* `node_modules` is a parameter so it serves pnpm + yarn-standalone (seeded at
|
|
2399
|
+
* `.kici/node_modules`) and yarn-workspace-member (seeded at the hoisted root
|
|
2400
|
+
* `node_modules`).
|
|
2401
|
+
*/
|
|
2402
|
+
/**
|
|
2403
|
+
* The directory yarn lays `.kici`'s dependencies into. A standalone `.kici`
|
|
2404
|
+
* (own lockfile, no parent workspace) gets `.kici/node_modules`; a workspace
|
|
2405
|
+
* member hoists everything to the repo-root `node_modules`, leaving no
|
|
2406
|
+
* `.kici/node_modules`.
|
|
2407
|
+
*/
|
|
2408
|
+
function resolveYarnNodeModulesRoot(repoRoot, kiciDir) {
|
|
2409
|
+
const kiciNm = join(kiciDir, "node_modules");
|
|
2410
|
+
return existsSync(kiciNm) ? kiciNm : join(repoRoot, "node_modules");
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Walk `seedNodeModules` (and transitively each in-repo sibling's
|
|
2414
|
+
* `node_modules`) collecting the repo-root-relative directories of workspace
|
|
2415
|
+
* siblings — package dirs that live inside the clone but outside `.kici/` and
|
|
2416
|
+
* outside the repo-root `node_modules/` store. Returns each dir once, in
|
|
2417
|
+
* discovery (BFS) order.
|
|
2418
|
+
*/
|
|
2419
|
+
async function collectInRepoSiblings(workDir, kiciDir, seedNodeModules = join(kiciDir, "node_modules")) {
|
|
2420
|
+
const repoRoot = resolve(workDir);
|
|
2421
|
+
const kiciResolved = resolve(kiciDir);
|
|
2422
|
+
const rootNodeModules = resolve(join(workDir, "node_modules"));
|
|
2423
|
+
const found = /* @__PURE__ */ new Set();
|
|
2424
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2425
|
+
const queue = [seedNodeModules];
|
|
2426
|
+
while (queue.length > 0) {
|
|
2427
|
+
const nmDir = queue.shift();
|
|
2428
|
+
const real = await realpath(nmDir).catch(() => null);
|
|
2429
|
+
if (!real || visited.has(real)) continue;
|
|
2430
|
+
visited.add(real);
|
|
2431
|
+
for (const target of await resolveNodeModulesLinks(nmDir)) {
|
|
2432
|
+
if (!isInside(repoRoot, target)) continue;
|
|
2433
|
+
if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
|
|
2434
|
+
const rel = relative(workDir, target);
|
|
2435
|
+
if (!found.has(rel)) {
|
|
2436
|
+
found.add(rel);
|
|
2437
|
+
queue.push(join(target, "node_modules"));
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return [...found];
|
|
2442
|
+
}
|
|
2443
|
+
/** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
|
|
2444
|
+
async function resolveNodeModulesLinks(nmDir) {
|
|
2445
|
+
const targets = [];
|
|
2446
|
+
for (const entry of await readdir(nmDir).catch(() => [])) {
|
|
2447
|
+
if (entry.startsWith(".")) continue;
|
|
2448
|
+
const entryPath = join(nmDir, entry);
|
|
2449
|
+
if (entry.startsWith("@")) {
|
|
2450
|
+
for (const scoped of await readdir(entryPath).catch(() => [])) {
|
|
2451
|
+
const target = await resolveIfSymlink(join(entryPath, scoped));
|
|
2452
|
+
if (target) targets.push(target);
|
|
2453
|
+
}
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
const target = await resolveIfSymlink(entryPath);
|
|
2457
|
+
if (target) targets.push(target);
|
|
2458
|
+
}
|
|
2459
|
+
return targets;
|
|
2460
|
+
}
|
|
2461
|
+
/** Return the real path of `p` if it is a symlink, else null. */
|
|
2462
|
+
async function resolveIfSymlink(p) {
|
|
2463
|
+
try {
|
|
2464
|
+
if (!(await lstat(p)).isSymbolicLink()) return null;
|
|
2465
|
+
return await realpath(p);
|
|
2466
|
+
} catch {
|
|
2467
|
+
return null;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
/** Whether `target` is `root` itself or a path inside it. */
|
|
2471
|
+
function isInside(root, target) {
|
|
2472
|
+
const rel = relative(root, target);
|
|
2473
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
|
|
2474
|
+
}
|
|
2475
|
+
function isAbsoluteRel(rel) {
|
|
2476
|
+
return rel.length > 1 && rel[1] === ":";
|
|
2477
|
+
}
|
|
2478
|
+
//#endregion
|
|
1937
2479
|
//#region src/execution/dep-installer.ts
|
|
1938
2480
|
/**
|
|
1939
2481
|
* Inline dependency installation for graceful degradation.
|
|
@@ -1941,12 +2483,14 @@ async function assertResolvableDeps(args) {
|
|
|
1941
2483
|
* When the dep cache is unavailable or a download fails, the agent installs
|
|
1942
2484
|
* `.kici/` dependencies directly with the repository's package manager.
|
|
1943
2485
|
*
|
|
1944
|
-
* The package manager is detected from the cloned repo (npm / pnpm); the
|
|
2486
|
+
* The package manager is detected from the cloned repo (npm / pnpm / yarn); the
|
|
1945
2487
|
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
1946
2488
|
* is the default and ships with every Node.js install; pnpm is used when the
|
|
1947
2489
|
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
1948
|
-
* `workspace:` siblings. yarn
|
|
1949
|
-
*
|
|
2490
|
+
* `workspace:` siblings. yarn classic (v1) is supported for registry
|
|
2491
|
+
* dependencies and version-range workspace siblings (which it links but does
|
|
2492
|
+
* not build, so the agent builds the in-repo closure after install). yarn
|
|
2493
|
+
* berry (v2+) is not yet supported.
|
|
1950
2494
|
*
|
|
1951
2495
|
* Security: the install runs with an isolated per-invocation cache/store
|
|
1952
2496
|
* directory to prevent cache poisoning across build jobs — a malicious
|
|
@@ -1995,7 +2539,6 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
1995
2539
|
dir: kiciDir
|
|
1996
2540
|
});
|
|
1997
2541
|
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
|
|
1998
|
-
if (packageManager === PackageManager.Yarn) throw new Error("This repository uses yarn, which the KiCI agent does not yet support for .kici/ dependency installation. Use npm or pnpm for the .kici/ project, or open a feature request for yarn support.");
|
|
1999
2542
|
await assertResolvableDeps({
|
|
2000
2543
|
kiciDir,
|
|
2001
2544
|
repoRoot,
|
|
@@ -2015,6 +2558,11 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
2015
2558
|
hasPrivateRegistry,
|
|
2016
2559
|
registryConfig
|
|
2017
2560
|
});
|
|
2561
|
+
else if (packageManager === PackageManager.Yarn) await runYarnInstall({
|
|
2562
|
+
kiciDir,
|
|
2563
|
+
hasPrivateRegistry,
|
|
2564
|
+
registryConfig
|
|
2565
|
+
});
|
|
2018
2566
|
else await runNpmInstall({
|
|
2019
2567
|
kiciDir,
|
|
2020
2568
|
hasPrivateRegistry,
|
|
@@ -2029,6 +2577,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
2029
2577
|
await registryConfig.cleanup();
|
|
2030
2578
|
}
|
|
2031
2579
|
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
2580
|
+
if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
|
|
2032
2581
|
const durationMs = Date.now() - startTime;
|
|
2033
2582
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
2034
2583
|
logger$2.info("Deps installed inline", {
|
|
@@ -2116,6 +2665,101 @@ async function runPnpmInstall(args) {
|
|
|
2116
2665
|
}).catch(() => {});
|
|
2117
2666
|
}
|
|
2118
2667
|
}
|
|
2668
|
+
/** Pure: argv for `yarn install` with an isolated cache folder. */
|
|
2669
|
+
function buildYarnInstallArgs(cacheDir, hasPrivateRegistry) {
|
|
2670
|
+
const a = [
|
|
2671
|
+
"install",
|
|
2672
|
+
"--cache-folder",
|
|
2673
|
+
cacheDir,
|
|
2674
|
+
"--non-interactive",
|
|
2675
|
+
"--no-progress"
|
|
2676
|
+
];
|
|
2677
|
+
if (hasPrivateRegistry) a.push("--ignore-scripts");
|
|
2678
|
+
return a;
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Run `yarn install` from `.kici/` with an isolated cache folder. yarn classic
|
|
2682
|
+
* reads the synthesized `.kici/.npmrc` (registry + `${VAR}` token expansion) for
|
|
2683
|
+
* private-registry auth. A workspace member hoists deps to the repo-root
|
|
2684
|
+
* node_modules; a standalone `.kici` gets `.kici/node_modules`. Not
|
|
2685
|
+
* `--frozen-lockfile` (resolved URLs in the lockfile may point at a different
|
|
2686
|
+
* registry than the synthesized `.npmrc`, e.g. localhost tunnel vs direct IP).
|
|
2687
|
+
*/
|
|
2688
|
+
async function runYarnInstall(args) {
|
|
2689
|
+
await assertYarnAvailable();
|
|
2690
|
+
const { nodeDir } = resolveNpm();
|
|
2691
|
+
const cacheDir = await mkdtemp(join(tmpdir(), "kici-yarn-cache-"));
|
|
2692
|
+
const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
|
|
2693
|
+
const argv = buildYarnInstallArgs(cacheDir, args.hasPrivateRegistry);
|
|
2694
|
+
try {
|
|
2695
|
+
process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")}\n`);
|
|
2696
|
+
await execFileAsync("yarn", argv, {
|
|
2697
|
+
cwd: args.kiciDir,
|
|
2698
|
+
env,
|
|
2699
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
2700
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
2701
|
+
});
|
|
2702
|
+
} finally {
|
|
2703
|
+
await rm(cacheDir, {
|
|
2704
|
+
recursive: true,
|
|
2705
|
+
force: true
|
|
2706
|
+
}).catch(() => {});
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
/** Throw an actionable error when the repo needs yarn but it is not installed. */
|
|
2710
|
+
async function assertYarnAvailable() {
|
|
2711
|
+
try {
|
|
2712
|
+
await execFileAsync("yarn", ["--version"], {
|
|
2713
|
+
timeout: 3e4,
|
|
2714
|
+
cwd: tmpdir()
|
|
2715
|
+
});
|
|
2716
|
+
} catch (e) {
|
|
2717
|
+
throw new Error(`This repository uses yarn, but yarn is not available on this agent. Install yarn (e.g. \`corepack enable\`) or run on a container/Firecracker agent that bundles it. (${toErrorMessage(e)})`);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Build the in-repo workspace siblings `.kici` depends on (yarn links them on
|
|
2722
|
+
* install but does not build them). Walks siblings from the resolved
|
|
2723
|
+
* node_modules root and runs each sibling's `build` script in leaf-first
|
|
2724
|
+
* (reverse-discovery) order with a clean env (no synthesized registry tokens).
|
|
2725
|
+
* Deep cross-sibling build chains may build out of strict topological order —
|
|
2726
|
+
* real `.kici` closures are shallow.
|
|
2727
|
+
*/
|
|
2728
|
+
async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
|
|
2729
|
+
const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
|
|
2730
|
+
if (siblings.length === 0) return;
|
|
2731
|
+
const { nodeDir } = resolveNpm();
|
|
2732
|
+
const env = envWithNodeOnPath({}, nodeDir);
|
|
2733
|
+
for (const rel of [...siblings].reverse()) {
|
|
2734
|
+
const sibDir = join(repoRoot, rel);
|
|
2735
|
+
if (!await siblingHasBuildScript(sibDir)) continue;
|
|
2736
|
+
process.stderr.write(`[dep-installer:trace] building yarn sibling: yarn --cwd ${sibDir} run build\n`);
|
|
2737
|
+
try {
|
|
2738
|
+
await execFileAsync("yarn", [
|
|
2739
|
+
"--cwd",
|
|
2740
|
+
sibDir,
|
|
2741
|
+
"run",
|
|
2742
|
+
"build"
|
|
2743
|
+
], {
|
|
2744
|
+
cwd: repoRoot,
|
|
2745
|
+
env,
|
|
2746
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
2747
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
2748
|
+
});
|
|
2749
|
+
} catch (e) {
|
|
2750
|
+
logSubprocessStreams(e, []);
|
|
2751
|
+
throw new Error(`Failed to build .kici yarn workspace sibling ${rel}: ${describeExecError(e)}`);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
/** Whether a sibling package.json declares a `build` script. */
|
|
2756
|
+
async function siblingHasBuildScript(sibDir) {
|
|
2757
|
+
try {
|
|
2758
|
+
return typeof JSON.parse(await readFile(join(sibDir, "package.json"), "utf-8")).scripts?.build === "string";
|
|
2759
|
+
} catch {
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2119
2763
|
/**
|
|
2120
2764
|
* Build the in-repo dependency closure of the `.kici/` package so a
|
|
2121
2765
|
* `workspace:` sibling's build output exists before the workflow that imports
|
|
@@ -2159,7 +2803,10 @@ function describeExecError(e) {
|
|
|
2159
2803
|
/** Throw an actionable error when the repo needs pnpm but it is not installed. */
|
|
2160
2804
|
async function assertPnpmAvailable() {
|
|
2161
2805
|
try {
|
|
2162
|
-
await execFileAsync("pnpm", ["--version"], {
|
|
2806
|
+
await execFileAsync("pnpm", ["--version"], {
|
|
2807
|
+
timeout: 3e4,
|
|
2808
|
+
cwd: tmpdir()
|
|
2809
|
+
});
|
|
2163
2810
|
} catch (e) {
|
|
2164
2811
|
throw new Error(`This repository is a pnpm workspace, but pnpm is not available on this agent. Install pnpm (e.g. \`corepack enable\`) or run on a container/ Firecracker agent that bundles it. (${toErrorMessage(e)})`);
|
|
2165
2812
|
}
|
|
@@ -2179,8 +2826,8 @@ function logSubprocessStreams(e, tokens) {
|
|
|
2179
2826
|
* no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
|
|
2180
2827
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
2181
2828
|
*/
|
|
2182
|
-
const AGENT_SDK_VERSION = "0.1.
|
|
2183
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
2829
|
+
const AGENT_SDK_VERSION = "0.1.17";
|
|
2830
|
+
const AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
|
|
2184
2831
|
/**
|
|
2185
2832
|
* Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
|
|
2186
2833
|
* subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
|
|
@@ -2363,7 +3010,6 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
|
|
|
2363
3010
|
* runs without a preceding build (cache infrastructure unavailable, or a
|
|
2364
3011
|
* build job that failed but left dynamic dispatch in flight).
|
|
2365
3012
|
*/
|
|
2366
|
-
init_download();
|
|
2367
3013
|
init_dep_restore();
|
|
2368
3014
|
const logger$1 = createLogger({ prefix: "source-restore" });
|
|
2369
3015
|
async function extractSourceTarball(data, targetDir) {
|
|
@@ -2409,18 +3055,17 @@ async function restoreSource(workDir, sourceTarUrl) {
|
|
|
2409
3055
|
init_download();
|
|
2410
3056
|
const logger = createLogger({ prefix: "overlay-applier" });
|
|
2411
3057
|
const IV_LENGTH = 12;
|
|
2412
|
-
const AUTH_TAG_LENGTH = 16;
|
|
2413
3058
|
/**
|
|
2414
3059
|
* Decrypt an encrypted buffer using AES-256-GCM.
|
|
2415
3060
|
*
|
|
2416
3061
|
* Wire format: [12-byte IV][16-byte auth tag][ciphertext]
|
|
2417
3062
|
*/
|
|
2418
3063
|
function decryptBuffer(encrypted, aesKey) {
|
|
2419
|
-
if (encrypted.length <
|
|
3064
|
+
if (encrypted.length < 28) throw new Error(`Tarball decryption failed: encrypted data too short (${encrypted.length} bytes, minimum 28 bytes)`);
|
|
2420
3065
|
const iv = encrypted.subarray(0, IV_LENGTH);
|
|
2421
|
-
const authTag = encrypted.subarray(IV_LENGTH,
|
|
2422
|
-
const ciphertext = encrypted.subarray(
|
|
2423
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
3066
|
+
const authTag = encrypted.subarray(IV_LENGTH, 28);
|
|
3067
|
+
const ciphertext = encrypted.subarray(28);
|
|
3068
|
+
const decipher = crypto$1.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
2424
3069
|
decipher.setAuthTag(authTag);
|
|
2425
3070
|
try {
|
|
2426
3071
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
@@ -2540,7 +3185,9 @@ async function applyOverlay(config) {
|
|
|
2540
3185
|
* This file is compiled alongside the agent by rolldown (existing build), but
|
|
2541
3186
|
* runs as a SEPARATE process spawned by the sandbox backend.
|
|
2542
3187
|
*/
|
|
3188
|
+
init_download();
|
|
2543
3189
|
init_dep_restore();
|
|
3190
|
+
const AGENT_VERSION = "0.1.17";
|
|
2544
3191
|
process.on("uncaughtException", (err) => {
|
|
2545
3192
|
process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
|
|
2546
3193
|
if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
|
|
@@ -2850,6 +3497,33 @@ function waitForApiResponse(requestId) {
|
|
|
2850
3497
|
const pendingCacheResponses = /* @__PURE__ */ new Map();
|
|
2851
3498
|
/** Default timeout for a cache request relay (matches the upload-URL request budget). */
|
|
2852
3499
|
const CACHE_RESPONSE_TIMEOUT_MS = 3e4;
|
|
3500
|
+
/**
|
|
3501
|
+
* Pending promises for provenance.response messages from the agent.
|
|
3502
|
+
* Key: requestId (correlates provenance.request -> provenance.response).
|
|
3503
|
+
*/
|
|
3504
|
+
const pendingProvenanceResponses = /* @__PURE__ */ new Map();
|
|
3505
|
+
/** Wait for a provenance.response from the agent with the given requestId. */
|
|
3506
|
+
function waitForProvenanceResponse(requestId) {
|
|
3507
|
+
return new Promise((resolve, reject) => {
|
|
3508
|
+
const timer = setTimeout(() => {
|
|
3509
|
+
pendingProvenanceResponses.delete(requestId);
|
|
3510
|
+
reject(/* @__PURE__ */ new Error(`Provenance request timed out after ${CACHE_RESPONSE_TIMEOUT_MS}ms`));
|
|
3511
|
+
}, CACHE_RESPONSE_TIMEOUT_MS);
|
|
3512
|
+
pendingProvenanceResponses.set(requestId, {
|
|
3513
|
+
resolve: (response) => {
|
|
3514
|
+
clearTimeout(timer);
|
|
3515
|
+
pendingProvenanceResponses.delete(requestId);
|
|
3516
|
+
resolve(response);
|
|
3517
|
+
},
|
|
3518
|
+
reject: (err) => {
|
|
3519
|
+
clearTimeout(timer);
|
|
3520
|
+
pendingProvenanceResponses.delete(requestId);
|
|
3521
|
+
reject(err);
|
|
3522
|
+
},
|
|
3523
|
+
timer
|
|
3524
|
+
});
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
2853
3527
|
/** Wait for a cache.response from the agent with the given requestId. */
|
|
2854
3528
|
function waitForCacheResponse(requestId) {
|
|
2855
3529
|
return new Promise((resolve, reject) => {
|
|
@@ -2873,6 +3547,68 @@ function waitForCacheResponse(requestId) {
|
|
|
2873
3547
|
});
|
|
2874
3548
|
}
|
|
2875
3549
|
/**
|
|
3550
|
+
* Pending promises for approval.resolved messages from the agent.
|
|
3551
|
+
* Key: requestId (correlates approval.request -> approval.resolved).
|
|
3552
|
+
*/
|
|
3553
|
+
const pendingApprovalResolutions = /* @__PURE__ */ new Map();
|
|
3554
|
+
/**
|
|
3555
|
+
* Hard ceiling on how long the runner blocks a step waiting for an approval
|
|
3556
|
+
* resolution. The orchestrator enforces the real (org-/SDK-configured) expiry
|
|
3557
|
+
* and sends `expired` when it lapses; this is a safety net well above any sane
|
|
3558
|
+
* approval window so the runner cannot hang forever if the resolution is lost.
|
|
3559
|
+
*/
|
|
3560
|
+
const APPROVAL_RESOLUTION_TIMEOUT_MS = 10080 * 60 * 1e3;
|
|
3561
|
+
/** Wait for an approval.resolved from the agent with the given requestId. */
|
|
3562
|
+
function waitForApprovalResolution(requestId) {
|
|
3563
|
+
return new Promise((resolve) => {
|
|
3564
|
+
const timer = setTimeout(() => {
|
|
3565
|
+
pendingApprovalResolutions.delete(requestId);
|
|
3566
|
+
resolve({
|
|
3567
|
+
type: "approval.resolved",
|
|
3568
|
+
requestId,
|
|
3569
|
+
outcome: "expired"
|
|
3570
|
+
});
|
|
3571
|
+
}, APPROVAL_RESOLUTION_TIMEOUT_MS);
|
|
3572
|
+
pendingApprovalResolutions.set(requestId, {
|
|
3573
|
+
resolve: (response) => {
|
|
3574
|
+
clearTimeout(timer);
|
|
3575
|
+
pendingApprovalResolutions.delete(requestId);
|
|
3576
|
+
resolve(response);
|
|
3577
|
+
},
|
|
3578
|
+
timer
|
|
3579
|
+
});
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Build the `awaitStepApproval` callback the step loop uses to block on a
|
|
3584
|
+
* `requireApproval` step. Sends an `approval.request` IPC (relayed by the agent
|
|
3585
|
+
* over the WS as a `step.approval-request`) and awaits the matching
|
|
3586
|
+
* `approval.resolved`. A relay error is treated as a fail-closed reject.
|
|
3587
|
+
*/
|
|
3588
|
+
function buildAwaitStepApproval() {
|
|
3589
|
+
return async (req) => {
|
|
3590
|
+
const requestId = randomUUID();
|
|
3591
|
+
sendMessage({
|
|
3592
|
+
type: "approval.request",
|
|
3593
|
+
requestId,
|
|
3594
|
+
stepIndex: req.stepIndex,
|
|
3595
|
+
stepName: req.stepName,
|
|
3596
|
+
clauses: req.clauses,
|
|
3597
|
+
reason: req.reason,
|
|
3598
|
+
...req.timeoutSeconds !== void 0 && { timeoutSeconds: req.timeoutSeconds }
|
|
3599
|
+
});
|
|
3600
|
+
const resolution = await waitForApprovalResolution(requestId);
|
|
3601
|
+
if (resolution.error) return {
|
|
3602
|
+
outcome: "rejected",
|
|
3603
|
+
reason: resolution.error
|
|
3604
|
+
};
|
|
3605
|
+
return {
|
|
3606
|
+
outcome: resolution.outcome ?? "rejected",
|
|
3607
|
+
...resolution.reason !== void 0 && { reason: resolution.reason }
|
|
3608
|
+
};
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
2876
3612
|
* Build the {@link CacheTransport} the sandbox-side cache engine uses to reach
|
|
2877
3613
|
* the orchestrator. Each method sends a `cache.request` IPC (relayed by the
|
|
2878
3614
|
* agent over the WS as a `cache.user.*` message) and awaits the matching
|
|
@@ -2928,6 +3664,64 @@ function buildCacheTransport() {
|
|
|
2928
3664
|
}
|
|
2929
3665
|
};
|
|
2930
3666
|
}
|
|
3667
|
+
/** Send a `provenance.request` IPC and await the matching `provenance.response`. */
|
|
3668
|
+
async function relayProvenanceIpc(request) {
|
|
3669
|
+
const requestId = randomUUID();
|
|
3670
|
+
sendMessage({
|
|
3671
|
+
type: "provenance.request",
|
|
3672
|
+
requestId,
|
|
3673
|
+
...request
|
|
3674
|
+
});
|
|
3675
|
+
const response = await waitForProvenanceResponse(requestId);
|
|
3676
|
+
if (response.error) throw new Error(`Provenance relay failed: ${response.error}`);
|
|
3677
|
+
return response;
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Build the `ctx.attestProvenance` step helper. Resolves a `path` subject to a
|
|
3681
|
+
* SHA-256 digest, threads the identity token via the supplied OIDC getter, and
|
|
3682
|
+
* persists the bundle over the IPC -> WS provenance-upload relay.
|
|
3683
|
+
*/
|
|
3684
|
+
function buildAttestProvenanceFn(request, workDir, getIdToken) {
|
|
3685
|
+
return async (opts) => {
|
|
3686
|
+
const subject = provenanceSubjectIsPath(opts.subject) ? {
|
|
3687
|
+
name: opts.subject.name,
|
|
3688
|
+
digest: { sha256: await sha256File$1(join(workDir, opts.subject.path)) }
|
|
3689
|
+
} : {
|
|
3690
|
+
name: opts.subject.name,
|
|
3691
|
+
digest: opts.subject.digest
|
|
3692
|
+
};
|
|
3693
|
+
const result = await attestProvenance({
|
|
3694
|
+
getIdToken,
|
|
3695
|
+
builderVersions: {
|
|
3696
|
+
"kici-agent": AGENT_VERSION,
|
|
3697
|
+
"kici-orchestrator": "unknown"
|
|
3698
|
+
},
|
|
3699
|
+
persist: async (bundle, subjectDigest) => {
|
|
3700
|
+
const urlResponse = await relayProvenanceIpc({
|
|
3701
|
+
op: "requestUploadUrl",
|
|
3702
|
+
subjectDigest
|
|
3703
|
+
});
|
|
3704
|
+
if (!urlResponse.uploadUrl) throw new Error("Orchestrator returned no provenance upload URL");
|
|
3705
|
+
await uploadToPresignedUrl(urlResponse.uploadUrl, Buffer.from(JSON.stringify(bundle)));
|
|
3706
|
+
await relayProvenanceIpc({
|
|
3707
|
+
op: "complete",
|
|
3708
|
+
subjectDigest,
|
|
3709
|
+
subjectName: subject.name,
|
|
3710
|
+
mediaType: bundle.mediaType
|
|
3711
|
+
});
|
|
3712
|
+
return `provenance/${request.runId}/${request.jobId}/${subjectDigest}.kici.json`;
|
|
3713
|
+
}
|
|
3714
|
+
}, {
|
|
3715
|
+
subject,
|
|
3716
|
+
...opts.audience !== void 0 && { audience: opts.audience }
|
|
3717
|
+
});
|
|
3718
|
+
return {
|
|
3719
|
+
storageKey: result.storageKey,
|
|
3720
|
+
subjectDigest: result.subjectDigest,
|
|
3721
|
+
bundleMediaType: result.bundle.mediaType
|
|
3722
|
+
};
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
2931
3725
|
/**
|
|
2932
3726
|
* Build the declarative-cache phase dependencies and run the job-level cache
|
|
2933
3727
|
* restore (Phase 9b).
|
|
@@ -2985,6 +3779,12 @@ function dispatchAgentMessage(msg) {
|
|
|
2985
3779
|
} else if (msg.type === "cache.response") {
|
|
2986
3780
|
const pending = pendingCacheResponses.get(msg.requestId);
|
|
2987
3781
|
if (pending) pending.resolve(msg);
|
|
3782
|
+
} else if (msg.type === "provenance.response") {
|
|
3783
|
+
const pending = pendingProvenanceResponses.get(msg.requestId);
|
|
3784
|
+
if (pending) pending.resolve(msg);
|
|
3785
|
+
} else if (msg.type === "approval.resolved") {
|
|
3786
|
+
const pending = pendingApprovalResolutions.get(msg.requestId);
|
|
3787
|
+
if (pending) pending.resolve(msg);
|
|
2988
3788
|
}
|
|
2989
3789
|
}
|
|
2990
3790
|
if (isForkMode) process.on("message", (msg) => {
|
|
@@ -3163,10 +3963,22 @@ function buildSandboxShell(cwd, stepIndex, maskedSendFn) {
|
|
|
3163
3963
|
* NOT serialized across the process boundary. This means zx $ runs natively
|
|
3164
3964
|
* inside this process with full shell access.
|
|
3165
3965
|
*/
|
|
3166
|
-
function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedSendFn, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, secrets) {
|
|
3966
|
+
function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedSendFn, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, secrets, masker) {
|
|
3167
3967
|
const step$ = buildSandboxShell(workDir, stepIndex, maskedSendFn);
|
|
3168
3968
|
const log = createIpcLogger(stepIndex, stepName, maskedSendFn);
|
|
3169
3969
|
const rawPayload = rawPayloadFromEvent(request.event);
|
|
3970
|
+
const kici = buildKiciApi(async (method, params) => {
|
|
3971
|
+
const reqId = randomUUID();
|
|
3972
|
+
sendMessage({
|
|
3973
|
+
type: "agent.api.request",
|
|
3974
|
+
requestId: reqId,
|
|
3975
|
+
method,
|
|
3976
|
+
params: params ?? {}
|
|
3977
|
+
});
|
|
3978
|
+
const result = await waitForApiResponse(reqId);
|
|
3979
|
+
if (method === OIDC_TOKEN_REQUEST_METHOD && result && typeof result.token === "string") masker.registerSecrets({ __oidc_token__: result.token });
|
|
3980
|
+
return result;
|
|
3981
|
+
}, { jobId: request.jobId });
|
|
3170
3982
|
return {
|
|
3171
3983
|
$: step$,
|
|
3172
3984
|
log,
|
|
@@ -3218,18 +4030,11 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
3218
4030
|
setSecretOutput: (key, value) => {
|
|
3219
4031
|
secretOutputs.set(key, value);
|
|
3220
4032
|
},
|
|
3221
|
-
kici
|
|
3222
|
-
|
|
3223
|
-
sendMessage({
|
|
3224
|
-
type: "agent.api.request",
|
|
3225
|
-
requestId: reqId,
|
|
3226
|
-
method,
|
|
3227
|
-
params: params ?? {}
|
|
3228
|
-
});
|
|
3229
|
-
return waitForApiResponse(reqId);
|
|
3230
|
-
}),
|
|
4033
|
+
kici,
|
|
4034
|
+
attestProvenance: buildAttestProvenanceFn(request, workDir, (o) => kici.oidc.token(o)),
|
|
3231
4035
|
...rawPayload && { rawPayload },
|
|
3232
|
-
...request.provider && { provider: request.provider }
|
|
4036
|
+
...request.provider && { provider: request.provider },
|
|
4037
|
+
...request.matrixValues && { matrix: request.matrixValues }
|
|
3233
4038
|
};
|
|
3234
4039
|
}
|
|
3235
4040
|
/** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
|
|
@@ -3372,6 +4177,31 @@ async function applyOverlayIfRequested(request, workflowDir) {
|
|
|
3372
4177
|
trace(`overlay applied: ${overlayResult.filesApplied} files, ${overlayResult.filesDeleted} deletions`);
|
|
3373
4178
|
}
|
|
3374
4179
|
/**
|
|
4180
|
+
* Phase 1c — Make git usable in a full-repo overlay workspace.
|
|
4181
|
+
*
|
|
4182
|
+
* `kici run remote` uploads the developer's working tree (including `.git`) as
|
|
4183
|
+
* a self-contained overlay; no clone happens, so the extracted `.git` directory
|
|
4184
|
+
* is owned by whatever UID wrote the tarball. Under rootless podman that UID may
|
|
4185
|
+
* not match the container UID, which trips git's "dubious ownership" /
|
|
4186
|
+
* `safe.directory` check and makes every step `git` command fail.
|
|
4187
|
+
*
|
|
4188
|
+
* Mirroring the `file://`-clone fix in checkout/git-clone.ts, we point
|
|
4189
|
+
* `GIT_CONFIG_GLOBAL` at a temp config carrying `safe.directory = *`. Setting it
|
|
4190
|
+
* on `process.env` here (before the step loop) means every step subprocess —
|
|
4191
|
+
* each zx `$` snapshots `process.env` at context creation — inherits it, so git
|
|
4192
|
+
* works in steps exactly as it does locally. We also register the dep-restore
|
|
4193
|
+
* scratch-dir exclude now that a real `.git` exists in the workspace.
|
|
4194
|
+
*/
|
|
4195
|
+
async function makeOverlayGitUsable(request, workspaceDir) {
|
|
4196
|
+
if (!request.fullRepo) return;
|
|
4197
|
+
if (!existsSync(join(workspaceDir, ".git"))) return;
|
|
4198
|
+
const cfgPath = join(await fsPromises.mkdtemp(join(tmpdir(), "kici-gitcfg-")), "config");
|
|
4199
|
+
await fsPromises.writeFile(cfgPath, "[safe]\n directory = *\n", { mode: 384 });
|
|
4200
|
+
process.env.GIT_CONFIG_GLOBAL = cfgPath;
|
|
4201
|
+
trace(`fullRepo git safe.directory configured via GIT_CONFIG_GLOBAL=${cfgPath}`);
|
|
4202
|
+
await excludeScratchFromGit(workspaceDir);
|
|
4203
|
+
}
|
|
4204
|
+
/**
|
|
3375
4205
|
* Phase 2 — Restore deps from cache (with hash-mismatch hard-fail) OR fall
|
|
3376
4206
|
* back to inline install. Skipped when `.kici/package.json` doesn't exist.
|
|
3377
4207
|
* For global workflows deps come from the workflow repo (where `.kici/` lives).
|
|
@@ -3854,15 +4684,6 @@ function collectJobHooks(job) {
|
|
|
3854
4684
|
return jobHooks;
|
|
3855
4685
|
}
|
|
3856
4686
|
/**
|
|
3857
|
-
* Normalize `Job.init` (config | config[] | false | undefined) to an ordered
|
|
3858
|
-
* array of init specs. `false` is an explicit opt-out and `undefined` (no
|
|
3859
|
-
* config) both resolve to an empty list — the init phase is then a no-op.
|
|
3860
|
-
*/
|
|
3861
|
-
function resolveInitSpecs(job) {
|
|
3862
|
-
if (!job || job.init === void 0 || job.init === false) return [];
|
|
3863
|
-
return Array.isArray(job.init) ? [...job.init] : [job.init];
|
|
3864
|
-
}
|
|
3865
|
-
/**
|
|
3866
4687
|
* Base stepIndex for the `init:<n>` pseudo-steps. The step loop reserves the
|
|
3867
4688
|
* range starting at `steps.length` for hook pseudo-steps (`beforeStep` =
|
|
3868
4689
|
* `steps.length + i*2`, `afterStep` = `steps.length + i*2 + 1`, and job-level
|
|
@@ -3938,7 +4759,16 @@ function buildStepEnvFileHooks(envFiles, operatorSecretKeys, maskedSend) {
|
|
|
3938
4759
|
*/
|
|
3939
4760
|
async function runInitPhaseOrFailJob(args) {
|
|
3940
4761
|
const { job, stepCwd, envFiles, operatorSecretKeys, maskedSend } = args;
|
|
3941
|
-
const
|
|
4762
|
+
const directives = normalizeInitItems(job);
|
|
4763
|
+
if (directives.length === 0) return;
|
|
4764
|
+
const initSpecs = await expandInitDirectives(directives, {
|
|
4765
|
+
cloneRoot: stepCwd,
|
|
4766
|
+
log: (line) => maskedSend({
|
|
4767
|
+
type: "log.line",
|
|
4768
|
+
stepIndex: -1,
|
|
4769
|
+
line
|
|
4770
|
+
})
|
|
4771
|
+
});
|
|
3942
4772
|
if (initSpecs.length === 0) return;
|
|
3943
4773
|
const initResult = await runInitPhase({
|
|
3944
4774
|
specs: initSpecs,
|
|
@@ -4013,6 +4843,7 @@ async function main() {
|
|
|
4013
4843
|
});
|
|
4014
4844
|
await cloneRepoIfRequested(request, workDir, workflowDir, sourceDir, isGlobal);
|
|
4015
4845
|
await applyOverlayIfRequested(request, workflowDir);
|
|
4846
|
+
await makeOverlayGitUsable(request, workflowDir);
|
|
4016
4847
|
if (aborted) abortAndExit("aborted after clone");
|
|
4017
4848
|
await installDependenciesIfNeeded(workflowDir, request);
|
|
4018
4849
|
if (aborted) abortAndExit("aborted after deps");
|
|
@@ -4056,7 +4887,7 @@ async function main() {
|
|
|
4056
4887
|
const handle = buildStepSecrets(request, masker, () => {});
|
|
4057
4888
|
currentStepSecrets = handle.secrets;
|
|
4058
4889
|
currentStepDispose = handle.dispose;
|
|
4059
|
-
const ctx = createSandboxStepContext(stepCwd, stepIndex, stepName, request, maskedSend, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, handle.secrets);
|
|
4890
|
+
const ctx = createSandboxStepContext(stepCwd, stepIndex, stepName, request, maskedSend, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, handle.secrets, masker);
|
|
4060
4891
|
if (globalRepoInfo) {
|
|
4061
4892
|
ctx.workflowRepo = globalRepoInfo.workflowRepo;
|
|
4062
4893
|
ctx.sourceRepo = globalRepoInfo.sourceRepo;
|
|
@@ -4093,7 +4924,8 @@ async function main() {
|
|
|
4093
4924
|
if (disposeFn) await disposeFn();
|
|
4094
4925
|
},
|
|
4095
4926
|
beforeStepEnvFiles: stepEnvHooks.beforeStepEnvFiles,
|
|
4096
|
-
afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles
|
|
4927
|
+
afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles,
|
|
4928
|
+
awaitStepApproval: buildAwaitStepApproval()
|
|
4097
4929
|
});
|
|
4098
4930
|
jobDeadline.clear();
|
|
4099
4931
|
await maybeSaveJobCache(jobCacheSpecs, jobCacheRestore, cachePhaseDeps, !aborted && loopResult.status === ExecutionStepStatus.enum.success);
|
|
@@ -4179,6 +5011,6 @@ main().catch((error) => {
|
|
|
4179
5011
|
setTimeout(() => process.exit(1), 100);
|
|
4180
5012
|
});
|
|
4181
5013
|
//#endregion
|
|
4182
|
-
export {
|
|
5014
|
+
export { createSandboxStepContext, rawPayloadFromEvent };
|
|
4183
5015
|
|
|
4184
5016
|
//# sourceMappingURL=workflow-runner.js.map
|