@kici-dev/agent 0.1.16 → 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/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 +9 -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 +58 -3
- package/dist/execution/sandbox/types.d.ts +9 -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 +472 -125
- package/dist/version.d.ts +2 -0
- package/dist/workflow-runner.js +749 -44
- package/dist/ws/orchestrator-client.d.ts +16 -1
- package/package.json +6 -5
- package/sbom.spdx.json +95 -65
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, normalizeRequireApproval, 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 {
|
|
@@ -1514,6 +1638,256 @@ async function runOneInit(spec, index, stepIndex, stepType, opts) {
|
|
|
1514
1638
|
}
|
|
1515
1639
|
}
|
|
1516
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
|
|
1517
1891
|
//#region src/execution/sandbox/job-deadline.ts
|
|
1518
1892
|
/**
|
|
1519
1893
|
* Arm a job-level wall-clock deadline. When `timeoutMs` is set and elapses
|
|
@@ -1962,6 +2336,17 @@ function isInsideRepo(repoRoot, target) {
|
|
|
1962
2336
|
*/
|
|
1963
2337
|
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
1964
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
|
+
}
|
|
1965
2350
|
const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
|
|
1966
2351
|
const unresolvable = [];
|
|
1967
2352
|
for (const dep of deps) {
|
|
@@ -1977,6 +2362,7 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
|
1977
2362
|
function formatUnresolvableDepError(offenders, packageManager) {
|
|
1978
2363
|
const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
|
|
1979
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.)`;
|
|
1980
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.`;
|
|
1981
2367
|
}
|
|
1982
2368
|
/**
|
|
@@ -1995,6 +2381,101 @@ async function assertResolvableDeps(args) {
|
|
|
1995
2381
|
throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
|
|
1996
2382
|
}
|
|
1997
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
|
|
1998
2479
|
//#region src/execution/dep-installer.ts
|
|
1999
2480
|
/**
|
|
2000
2481
|
* Inline dependency installation for graceful degradation.
|
|
@@ -2002,12 +2483,14 @@ async function assertResolvableDeps(args) {
|
|
|
2002
2483
|
* When the dep cache is unavailable or a download fails, the agent installs
|
|
2003
2484
|
* `.kici/` dependencies directly with the repository's package manager.
|
|
2004
2485
|
*
|
|
2005
|
-
* 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
|
|
2006
2487
|
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
2007
2488
|
* is the default and ships with every Node.js install; pnpm is used when the
|
|
2008
2489
|
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
2009
|
-
* `workspace:` siblings. yarn
|
|
2010
|
-
*
|
|
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.
|
|
2011
2494
|
*
|
|
2012
2495
|
* Security: the install runs with an isolated per-invocation cache/store
|
|
2013
2496
|
* directory to prevent cache poisoning across build jobs — a malicious
|
|
@@ -2056,7 +2539,6 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
2056
2539
|
dir: kiciDir
|
|
2057
2540
|
});
|
|
2058
2541
|
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
|
|
2059
|
-
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.");
|
|
2060
2542
|
await assertResolvableDeps({
|
|
2061
2543
|
kiciDir,
|
|
2062
2544
|
repoRoot,
|
|
@@ -2076,6 +2558,11 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
2076
2558
|
hasPrivateRegistry,
|
|
2077
2559
|
registryConfig
|
|
2078
2560
|
});
|
|
2561
|
+
else if (packageManager === PackageManager.Yarn) await runYarnInstall({
|
|
2562
|
+
kiciDir,
|
|
2563
|
+
hasPrivateRegistry,
|
|
2564
|
+
registryConfig
|
|
2565
|
+
});
|
|
2079
2566
|
else await runNpmInstall({
|
|
2080
2567
|
kiciDir,
|
|
2081
2568
|
hasPrivateRegistry,
|
|
@@ -2090,6 +2577,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
2090
2577
|
await registryConfig.cleanup();
|
|
2091
2578
|
}
|
|
2092
2579
|
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
2580
|
+
if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
|
|
2093
2581
|
const durationMs = Date.now() - startTime;
|
|
2094
2582
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
2095
2583
|
logger$2.info("Deps installed inline", {
|
|
@@ -2177,6 +2665,101 @@ async function runPnpmInstall(args) {
|
|
|
2177
2665
|
}).catch(() => {});
|
|
2178
2666
|
}
|
|
2179
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
|
+
}
|
|
2180
2763
|
/**
|
|
2181
2764
|
* Build the in-repo dependency closure of the `.kici/` package so a
|
|
2182
2765
|
* `workspace:` sibling's build output exists before the workflow that imports
|
|
@@ -2220,7 +2803,10 @@ function describeExecError(e) {
|
|
|
2220
2803
|
/** Throw an actionable error when the repo needs pnpm but it is not installed. */
|
|
2221
2804
|
async function assertPnpmAvailable() {
|
|
2222
2805
|
try {
|
|
2223
|
-
await execFileAsync("pnpm", ["--version"], {
|
|
2806
|
+
await execFileAsync("pnpm", ["--version"], {
|
|
2807
|
+
timeout: 3e4,
|
|
2808
|
+
cwd: tmpdir()
|
|
2809
|
+
});
|
|
2224
2810
|
} catch (e) {
|
|
2225
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)})`);
|
|
2226
2812
|
}
|
|
@@ -2240,8 +2826,8 @@ function logSubprocessStreams(e, tokens) {
|
|
|
2240
2826
|
* no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
|
|
2241
2827
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
2242
2828
|
*/
|
|
2243
|
-
const AGENT_SDK_VERSION = "0.1.
|
|
2244
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
2829
|
+
const AGENT_SDK_VERSION = "0.1.17";
|
|
2830
|
+
const AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
|
|
2245
2831
|
/**
|
|
2246
2832
|
* Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
|
|
2247
2833
|
* subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
|
|
@@ -2424,7 +3010,6 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
|
|
|
2424
3010
|
* runs without a preceding build (cache infrastructure unavailable, or a
|
|
2425
3011
|
* build job that failed but left dynamic dispatch in flight).
|
|
2426
3012
|
*/
|
|
2427
|
-
init_download();
|
|
2428
3013
|
init_dep_restore();
|
|
2429
3014
|
const logger$1 = createLogger({ prefix: "source-restore" });
|
|
2430
3015
|
async function extractSourceTarball(data, targetDir) {
|
|
@@ -2470,18 +3055,17 @@ async function restoreSource(workDir, sourceTarUrl) {
|
|
|
2470
3055
|
init_download();
|
|
2471
3056
|
const logger = createLogger({ prefix: "overlay-applier" });
|
|
2472
3057
|
const IV_LENGTH = 12;
|
|
2473
|
-
const AUTH_TAG_LENGTH = 16;
|
|
2474
3058
|
/**
|
|
2475
3059
|
* Decrypt an encrypted buffer using AES-256-GCM.
|
|
2476
3060
|
*
|
|
2477
3061
|
* Wire format: [12-byte IV][16-byte auth tag][ciphertext]
|
|
2478
3062
|
*/
|
|
2479
3063
|
function decryptBuffer(encrypted, aesKey) {
|
|
2480
|
-
if (encrypted.length <
|
|
3064
|
+
if (encrypted.length < 28) throw new Error(`Tarball decryption failed: encrypted data too short (${encrypted.length} bytes, minimum 28 bytes)`);
|
|
2481
3065
|
const iv = encrypted.subarray(0, IV_LENGTH);
|
|
2482
|
-
const authTag = encrypted.subarray(IV_LENGTH,
|
|
2483
|
-
const ciphertext = encrypted.subarray(
|
|
2484
|
-
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);
|
|
2485
3069
|
decipher.setAuthTag(authTag);
|
|
2486
3070
|
try {
|
|
2487
3071
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
@@ -2601,7 +3185,9 @@ async function applyOverlay(config) {
|
|
|
2601
3185
|
* This file is compiled alongside the agent by rolldown (existing build), but
|
|
2602
3186
|
* runs as a SEPARATE process spawned by the sandbox backend.
|
|
2603
3187
|
*/
|
|
3188
|
+
init_download();
|
|
2604
3189
|
init_dep_restore();
|
|
3190
|
+
const AGENT_VERSION = "0.1.17";
|
|
2605
3191
|
process.on("uncaughtException", (err) => {
|
|
2606
3192
|
process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
|
|
2607
3193
|
if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
|
|
@@ -2911,6 +3497,33 @@ function waitForApiResponse(requestId) {
|
|
|
2911
3497
|
const pendingCacheResponses = /* @__PURE__ */ new Map();
|
|
2912
3498
|
/** Default timeout for a cache request relay (matches the upload-URL request budget). */
|
|
2913
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
|
+
}
|
|
2914
3527
|
/** Wait for a cache.response from the agent with the given requestId. */
|
|
2915
3528
|
function waitForCacheResponse(requestId) {
|
|
2916
3529
|
return new Promise((resolve, reject) => {
|
|
@@ -3051,6 +3664,64 @@ function buildCacheTransport() {
|
|
|
3051
3664
|
}
|
|
3052
3665
|
};
|
|
3053
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
|
+
}
|
|
3054
3725
|
/**
|
|
3055
3726
|
* Build the declarative-cache phase dependencies and run the job-level cache
|
|
3056
3727
|
* restore (Phase 9b).
|
|
@@ -3108,6 +3779,9 @@ function dispatchAgentMessage(msg) {
|
|
|
3108
3779
|
} else if (msg.type === "cache.response") {
|
|
3109
3780
|
const pending = pendingCacheResponses.get(msg.requestId);
|
|
3110
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);
|
|
3111
3785
|
} else if (msg.type === "approval.resolved") {
|
|
3112
3786
|
const pending = pendingApprovalResolutions.get(msg.requestId);
|
|
3113
3787
|
if (pending) pending.resolve(msg);
|
|
@@ -3289,10 +3963,22 @@ function buildSandboxShell(cwd, stepIndex, maskedSendFn) {
|
|
|
3289
3963
|
* NOT serialized across the process boundary. This means zx $ runs natively
|
|
3290
3964
|
* inside this process with full shell access.
|
|
3291
3965
|
*/
|
|
3292
|
-
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) {
|
|
3293
3967
|
const step$ = buildSandboxShell(workDir, stepIndex, maskedSendFn);
|
|
3294
3968
|
const log = createIpcLogger(stepIndex, stepName, maskedSendFn);
|
|
3295
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 });
|
|
3296
3982
|
return {
|
|
3297
3983
|
$: step$,
|
|
3298
3984
|
log,
|
|
@@ -3344,18 +4030,11 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
3344
4030
|
setSecretOutput: (key, value) => {
|
|
3345
4031
|
secretOutputs.set(key, value);
|
|
3346
4032
|
},
|
|
3347
|
-
kici
|
|
3348
|
-
|
|
3349
|
-
sendMessage({
|
|
3350
|
-
type: "agent.api.request",
|
|
3351
|
-
requestId: reqId,
|
|
3352
|
-
method,
|
|
3353
|
-
params: params ?? {}
|
|
3354
|
-
});
|
|
3355
|
-
return waitForApiResponse(reqId);
|
|
3356
|
-
}),
|
|
4033
|
+
kici,
|
|
4034
|
+
attestProvenance: buildAttestProvenanceFn(request, workDir, (o) => kici.oidc.token(o)),
|
|
3357
4035
|
...rawPayload && { rawPayload },
|
|
3358
|
-
...request.provider && { provider: request.provider }
|
|
4036
|
+
...request.provider && { provider: request.provider },
|
|
4037
|
+
...request.matrixValues && { matrix: request.matrixValues }
|
|
3359
4038
|
};
|
|
3360
4039
|
}
|
|
3361
4040
|
/** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
|
|
@@ -3498,6 +4177,31 @@ async function applyOverlayIfRequested(request, workflowDir) {
|
|
|
3498
4177
|
trace(`overlay applied: ${overlayResult.filesApplied} files, ${overlayResult.filesDeleted} deletions`);
|
|
3499
4178
|
}
|
|
3500
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
|
+
/**
|
|
3501
4205
|
* Phase 2 — Restore deps from cache (with hash-mismatch hard-fail) OR fall
|
|
3502
4206
|
* back to inline install. Skipped when `.kici/package.json` doesn't exist.
|
|
3503
4207
|
* For global workflows deps come from the workflow repo (where `.kici/` lives).
|
|
@@ -3980,15 +4684,6 @@ function collectJobHooks(job) {
|
|
|
3980
4684
|
return jobHooks;
|
|
3981
4685
|
}
|
|
3982
4686
|
/**
|
|
3983
|
-
* Normalize `Job.init` (config | config[] | false | undefined) to an ordered
|
|
3984
|
-
* array of init specs. `false` is an explicit opt-out and `undefined` (no
|
|
3985
|
-
* config) both resolve to an empty list — the init phase is then a no-op.
|
|
3986
|
-
*/
|
|
3987
|
-
function resolveInitSpecs(job) {
|
|
3988
|
-
if (!job || job.init === void 0 || job.init === false) return [];
|
|
3989
|
-
return Array.isArray(job.init) ? [...job.init] : [job.init];
|
|
3990
|
-
}
|
|
3991
|
-
/**
|
|
3992
4687
|
* Base stepIndex for the `init:<n>` pseudo-steps. The step loop reserves the
|
|
3993
4688
|
* range starting at `steps.length` for hook pseudo-steps (`beforeStep` =
|
|
3994
4689
|
* `steps.length + i*2`, `afterStep` = `steps.length + i*2 + 1`, and job-level
|
|
@@ -4064,7 +4759,16 @@ function buildStepEnvFileHooks(envFiles, operatorSecretKeys, maskedSend) {
|
|
|
4064
4759
|
*/
|
|
4065
4760
|
async function runInitPhaseOrFailJob(args) {
|
|
4066
4761
|
const { job, stepCwd, envFiles, operatorSecretKeys, maskedSend } = args;
|
|
4067
|
-
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
|
+
});
|
|
4068
4772
|
if (initSpecs.length === 0) return;
|
|
4069
4773
|
const initResult = await runInitPhase({
|
|
4070
4774
|
specs: initSpecs,
|
|
@@ -4139,6 +4843,7 @@ async function main() {
|
|
|
4139
4843
|
});
|
|
4140
4844
|
await cloneRepoIfRequested(request, workDir, workflowDir, sourceDir, isGlobal);
|
|
4141
4845
|
await applyOverlayIfRequested(request, workflowDir);
|
|
4846
|
+
await makeOverlayGitUsable(request, workflowDir);
|
|
4142
4847
|
if (aborted) abortAndExit("aborted after clone");
|
|
4143
4848
|
await installDependenciesIfNeeded(workflowDir, request);
|
|
4144
4849
|
if (aborted) abortAndExit("aborted after deps");
|
|
@@ -4182,7 +4887,7 @@ async function main() {
|
|
|
4182
4887
|
const handle = buildStepSecrets(request, masker, () => {});
|
|
4183
4888
|
currentStepSecrets = handle.secrets;
|
|
4184
4889
|
currentStepDispose = handle.dispose;
|
|
4185
|
-
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);
|
|
4186
4891
|
if (globalRepoInfo) {
|
|
4187
4892
|
ctx.workflowRepo = globalRepoInfo.workflowRepo;
|
|
4188
4893
|
ctx.sourceRepo = globalRepoInfo.sourceRepo;
|
|
@@ -4306,6 +5011,6 @@ main().catch((error) => {
|
|
|
4306
5011
|
setTimeout(() => process.exit(1), 100);
|
|
4307
5012
|
});
|
|
4308
5013
|
//#endregion
|
|
4309
|
-
export {
|
|
5014
|
+
export { createSandboxStepContext, rawPayloadFromEvent };
|
|
4310
5015
|
|
|
4311
5016
|
//# sourceMappingURL=workflow-runner.js.map
|