@kici-dev/agent 0.4.0 → 0.6.0
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/checkout/clone-job-repos.d.ts +53 -0
- package/dist/checkout/credential-helper-bin.d.ts +28 -0
- package/dist/checkout/credential-helper-host.d.ts +48 -0
- package/dist/checkout/credential-helper.d.ts +44 -0
- package/dist/checkout/git-clone.d.ts +26 -0
- package/dist/checkout/grant-table.d.ts +30 -0
- package/dist/checkout/job-git-credentials.d.ts +49 -0
- package/dist/checkout/write-elevation.d.ts +40 -0
- package/dist/config.d.ts +38 -0
- package/dist/container-ts-loader-hook.js +2047 -1814
- package/dist/execution/between-jobs-controller.d.ts +50 -0
- package/dist/execution/between-jobs-reset.d.ts +25 -0
- package/dist/execution/cleanup-rerun.d.ts +21 -0
- package/dist/execution/download.d.ts +27 -2
- package/dist/execution/dynamic-job-serializer.d.ts +6 -2
- package/dist/execution/generator-context.d.ts +54 -0
- package/dist/execution/global-eval-runner.d.ts +92 -0
- package/dist/execution/global-workflow-env.d.ts +57 -0
- package/dist/execution/image-build/build-engine.d.ts +75 -0
- package/dist/execution/image-build/build-step.d.ts +57 -0
- package/dist/execution/image-build/resolve-build-spec.d.ts +41 -0
- package/dist/execution/image-build/runtime-facts.d.ts +31 -0
- package/dist/execution/init-runner.d.ts +60 -3
- package/dist/execution/job-runner.d.ts +146 -0
- package/dist/execution/sandbox/bare-metal-sandbox.d.ts +25 -1
- package/dist/execution/sandbox/container-sandbox.d.ts +97 -2
- package/dist/execution/sandbox/fork-runner.d.ts +55 -1
- package/dist/execution/sandbox/image-preflight.d.ts +38 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +96 -2
- package/dist/execution/sandbox/kici-runtime.d.ts +48 -0
- package/dist/execution/sandbox/step-loop.d.ts +17 -1
- package/dist/execution/sandbox/types.d.ts +55 -1
- package/dist/execution/sandbox/workflow-runner.d.ts +75 -4
- package/dist/execution/workflow-loader.d.ts +8 -1
- package/dist/idle-shutdown.d.ts +24 -0
- package/dist/index.js +221 -80
- package/dist/metrics/prometheus.d.ts +36 -10
- package/dist/server.js +3197 -462
- package/dist/workflow-runner-bundle.js +42207 -41104
- package/dist/workflow-runner.js +594 -196
- package/dist/ws/orchestrator-client.d.ts +95 -3
- package/package.json +10 -10
- package/sbom.spdx.json +464 -454
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ExecutionMode, BetweenJobsRunOn } from '../config.js';
|
|
2
|
+
import { runBetweenJobsReset, type ResetStatus } from './between-jobs-reset.js';
|
|
3
|
+
import { runDeclaredCleanupOutOfBand, type CleanupRerunStatus } from './cleanup-rerun.js';
|
|
4
|
+
interface ControllerConfig {
|
|
5
|
+
betweenJobsResetCommand?: string;
|
|
6
|
+
betweenJobsResetTimeoutMs: number;
|
|
7
|
+
betweenJobsResetRunOn: BetweenJobsRunOn;
|
|
8
|
+
orphanCleanup: boolean;
|
|
9
|
+
drainOnResetFailure: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface AfterJobContext {
|
|
12
|
+
completionHooksRan: boolean;
|
|
13
|
+
jobFailed: boolean;
|
|
14
|
+
backend: ExecutionMode;
|
|
15
|
+
declaresCleanup: boolean;
|
|
16
|
+
workDir: string;
|
|
17
|
+
reap: () => Promise<number>;
|
|
18
|
+
deleteWorkdir: () => Promise<void>;
|
|
19
|
+
cleanupSpawn?: (workDir: string, signal: AbortSignal) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface BetweenJobsOutcome {
|
|
22
|
+
rerun: CleanupRerunStatus;
|
|
23
|
+
reaped: number;
|
|
24
|
+
reset: ResetStatus;
|
|
25
|
+
consecutiveResetFailures: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Supervisor-owned between-jobs phase for a reused (bare-metal / in-place) agent.
|
|
29
|
+
* After every job it sequences: (1) out-of-band declared-cleanup re-run when the
|
|
30
|
+
* runner died before its completion hooks ran, (2) process-group reap of the
|
|
31
|
+
* job's descendant tree, (3) workdir deletion, (4) the operator reset command.
|
|
32
|
+
* Each phase is delegated so the class stays unit-testable, and a persistently
|
|
33
|
+
* failing reset can drain the agent via the consecutive-failure counter.
|
|
34
|
+
*/
|
|
35
|
+
export declare class BetweenJobsController {
|
|
36
|
+
private deps;
|
|
37
|
+
private _consecutiveResetFailures;
|
|
38
|
+
/** Consecutive between-jobs reset failures, for the supervisor's drain gate. */
|
|
39
|
+
get consecutiveResetFailures(): number;
|
|
40
|
+
constructor(deps: {
|
|
41
|
+
config: ControllerConfig;
|
|
42
|
+
rerun?: typeof runDeclaredCleanupOutOfBand;
|
|
43
|
+
reset?: typeof runBetweenJobsReset;
|
|
44
|
+
});
|
|
45
|
+
private runRerunPhase;
|
|
46
|
+
private runResetPhase;
|
|
47
|
+
afterJob(ctx: AfterJobContext): Promise<BetweenJobsOutcome>;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=between-jobs-controller.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BetweenJobsRunOn } from '../config.js';
|
|
2
|
+
export type ResetStatus = 'success' | 'failed' | 'timeout' | 'skipped';
|
|
3
|
+
export interface ExecResult {
|
|
4
|
+
code: number | null;
|
|
5
|
+
timedOut?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type ExecFn = (command: string, timeoutMs: number) => Promise<ExecResult>;
|
|
8
|
+
export interface ResetInput {
|
|
9
|
+
command?: string;
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
runOn: BetweenJobsRunOn;
|
|
12
|
+
jobFailed: boolean;
|
|
13
|
+
exec?: ExecFn;
|
|
14
|
+
}
|
|
15
|
+
export interface ResetResult {
|
|
16
|
+
status: ResetStatus;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Run the operator between-jobs reset command. Fail-open: never throws, always
|
|
21
|
+
* resolves to a status. Skips when unconfigured, or when runOn=on-failure and
|
|
22
|
+
* the job succeeded.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runBetweenJobsReset(input: ResetInput): Promise<ResetResult>;
|
|
25
|
+
//# sourceMappingURL=between-jobs-reset.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExecutionMode } from '../config.js';
|
|
2
|
+
export type CleanupRerunStatus = 'success' | 'failed' | 'timeout' | 'skipped';
|
|
3
|
+
export interface CleanupRerunInput {
|
|
4
|
+
workDir: string;
|
|
5
|
+
backend: ExecutionMode;
|
|
6
|
+
declaresCleanup: boolean;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
spawn: (workDir: string, signal: AbortSignal) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface CleanupRerunResult {
|
|
11
|
+
status: CleanupRerunStatus;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Re-run a hard-killed job's declared cleanup/onFailure hooks out-of-band,
|
|
16
|
+
* against its preserved workdir, in a fresh bounded child. Returns 'skipped'
|
|
17
|
+
* when there is nothing to do (no declared cleanup, wrong backend, or a
|
|
18
|
+
* non-positive timeout). Never throws — a failure is reported as a status.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runDeclaredCleanupOutOfBand(input: CleanupRerunInput): Promise<CleanupRerunResult>;
|
|
21
|
+
//# sourceMappingURL=cleanup-rerun.d.ts.map
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Extracted from workflow-loader.ts to avoid duplication across
|
|
5
5
|
* dep-restore.ts and workflow-loader.ts.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Retries a pre-signed upload makes after its first attempt, matching the
|
|
9
|
+
* dep-tarball download's ceiling (`MAX_RETRIES` in `dep-restore.ts`) so both
|
|
10
|
+
* halves of the agent's object-storage traffic give up at the same point.
|
|
11
|
+
*/
|
|
12
|
+
export declare const UPLOAD_MAX_RETRIES = 2;
|
|
7
13
|
/**
|
|
8
14
|
* Download content from an HTTP/HTTPS URL.
|
|
9
15
|
*
|
|
@@ -15,15 +21,34 @@
|
|
|
15
21
|
*/
|
|
16
22
|
export declare function downloadUrl(url: string): Promise<Buffer>;
|
|
17
23
|
/**
|
|
18
|
-
* Upload a buffer to a pre-signed S3 URL via HTTP PUT
|
|
24
|
+
* Upload a buffer to a pre-signed S3 URL via HTTP PUT, retrying a transient
|
|
25
|
+
* failure.
|
|
19
26
|
*
|
|
20
27
|
* Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
|
|
21
28
|
* 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
|
|
22
29
|
* filesystem cache backend's signed URLs work from container agents that
|
|
23
30
|
* can't reach the orchestrator's host loopback directly.
|
|
24
31
|
*
|
|
32
|
+
* **Why retrying is safe here.** A pre-signed PUT writes one whole object at a
|
|
33
|
+
* single key: there is no multipart session, no append, and no
|
|
34
|
+
* server-generated identity, so a repeat attempt writes the same bytes to the
|
|
35
|
+
* same key and the last write wins. S3 also only makes an object visible once
|
|
36
|
+
* the body has been received in full, so an attempt that died mid-body left
|
|
37
|
+
* nothing behind. A retry therefore cannot double-write or produce a torn
|
|
38
|
+
* object — which is why every AWS SDK retries PUTs by default.
|
|
39
|
+
*
|
|
40
|
+
* Only a failure that can plausibly differ next time is repeated — see
|
|
41
|
+
* {@link isRetryableUploadFailure}.
|
|
42
|
+
*
|
|
25
43
|
* @param url - The pre-signed URL to upload to
|
|
26
44
|
* @param data - The buffer to upload
|
|
45
|
+
* @param opts.baseDelayMs - Backoff before the first retry (doubles thereafter)
|
|
46
|
+
* @param opts.timeoutMs - Per-attempt socket-inactivity timeout (see
|
|
47
|
+
* {@link UPLOAD_TIMEOUT_MS}); an override exists so a test can drive the
|
|
48
|
+
* stall path without waiting out the production budget.
|
|
27
49
|
*/
|
|
28
|
-
export declare function uploadToPresignedUrl(url: string, data: Buffer
|
|
50
|
+
export declare function uploadToPresignedUrl(url: string, data: Buffer, opts?: {
|
|
51
|
+
baseDelayMs?: number;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
}): Promise<void>;
|
|
29
54
|
//# sourceMappingURL=download.d.ts.map
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Constraints:
|
|
15
15
|
* - Generated jobs are limited to MAX_DYNAMIC_JOBS per DynamicJobFn invocation
|
|
16
|
-
* - Generated job names must be unique within the same DynamicJobFn output
|
|
16
|
+
* - Generated job names must be unique within the same DynamicJobFn output, and
|
|
17
|
+
* across a whole eval round when the caller threads a shared `seenNames` set
|
|
17
18
|
*/
|
|
18
19
|
import type { $ as Shell } from 'zx';
|
|
19
20
|
import type { Job, Logger } from '@kici-dev/sdk';
|
|
@@ -54,9 +55,12 @@ export interface SerializerContext {
|
|
|
54
55
|
*
|
|
55
56
|
* @param jobs - Jobs returned by a DynamicJobFn
|
|
56
57
|
* @param ctx - Eval-time context used to resolve dynamic fields on generated jobs
|
|
58
|
+
* @param seenNames - Optional accumulator of job names already emitted earlier
|
|
59
|
+
* in the same eval round; when supplied, a name already present throws and
|
|
60
|
+
* every generated name is added so later generators in the round see it
|
|
57
61
|
* @returns Serialized LockJob array ready for orchestrator dispatch
|
|
58
62
|
* @throws Error if validation fails (duplicates, limit exceeded) or if a user-supplied
|
|
59
63
|
* dynamic function throws / times out / returns an unsupported value
|
|
60
64
|
*/
|
|
61
|
-
export declare function serializeJobsToLock(jobs: Job[], ctx: SerializerContext, staticNames?: Set<string>, allowedGroups?: Set<string>): Promise<LockJob[]>;
|
|
65
|
+
export declare function serializeJobsToLock(jobs: Job[], ctx: SerializerContext, staticNames?: Set<string>, allowedGroups?: Set<string>, seenNames?: Set<string>): Promise<LockJob[]>;
|
|
62
66
|
//# sourceMappingURL=dynamic-job-serializer.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single builder for a `DynamicJobFn`'s context.
|
|
3
|
+
*
|
|
4
|
+
* A generator is evaluated twice — once pre-dispatch to produce the job list,
|
|
5
|
+
* and once inside the sandbox to recover the step closures a `LockJob` cannot
|
|
6
|
+
* carry. `extractStepsFromDynamicJob` throws when the target job is missing on
|
|
7
|
+
* re-evaluation, so a generator whose two calls see different contexts is
|
|
8
|
+
* unsound by construction. Every call site builds its context here so the two
|
|
9
|
+
* cannot drift apart.
|
|
10
|
+
*/
|
|
11
|
+
import type { DynamicJobContext, RepoInfo } from '@kici-dev/sdk';
|
|
12
|
+
/**
|
|
13
|
+
* The source / workflow repo pair a global workflow's generator sees.
|
|
14
|
+
*
|
|
15
|
+
* `sourceRepo.path` is an absolute path into THIS evaluation's work directory:
|
|
16
|
+
* it is NOT stable across the two evaluations (a different `workDir`, possibly
|
|
17
|
+
* a different machine). Its *contents* are stable; the path is not. So read
|
|
18
|
+
* through the path — never embed it in a generated job name, a job output, or
|
|
19
|
+
* anything else the two evaluations are compared on.
|
|
20
|
+
*
|
|
21
|
+
* `RepoInfo.ref` and `.sha` are optional: an evaluation that has no checkout
|
|
22
|
+
* metadata still supplies the pair. Never assume either is present.
|
|
23
|
+
*/
|
|
24
|
+
export interface GeneratorRepoPair {
|
|
25
|
+
sourceRepo: RepoInfo;
|
|
26
|
+
workflowRepo: RepoInfo;
|
|
27
|
+
}
|
|
28
|
+
/** Input for {@link buildGeneratorContext}. */
|
|
29
|
+
export interface GeneratorContextInput {
|
|
30
|
+
workflowName: string;
|
|
31
|
+
/**
|
|
32
|
+
* The raw wire event. Untyped JSON that, per the unified event protocol,
|
|
33
|
+
* always carries the normalized event envelope.
|
|
34
|
+
*/
|
|
35
|
+
event: Record<string, unknown>;
|
|
36
|
+
env: Record<string, string | undefined>;
|
|
37
|
+
/** Present for a global workflow; omitted entirely otherwise. */
|
|
38
|
+
repos?: GeneratorRepoPair;
|
|
39
|
+
/** Frozen upstream outputs for a result-aware generator. */
|
|
40
|
+
needs?: NonNullable<DynamicJobContext['ctx']['needs']>;
|
|
41
|
+
$: DynamicJobContext['$'];
|
|
42
|
+
log: DynamicJobContext['log'];
|
|
43
|
+
kici: DynamicJobContext['kici'];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the context handed to a `DynamicJobFn`.
|
|
47
|
+
*
|
|
48
|
+
* Optional members are spread conditionally rather than assigned `undefined`,
|
|
49
|
+
* so an absent `needs` / repo pair leaves no key behind — a present-but-
|
|
50
|
+
* undefined key reads as "declared" to a generator and serializes differently
|
|
51
|
+
* between the two evaluations.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildGeneratorContext(input: GeneratorContextInput): DynamicJobContext;
|
|
54
|
+
//# sourceMappingURL=generator-context.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-run global eval round.
|
|
3
|
+
*
|
|
4
|
+
* One round runs per (event × workflow repo), on an agent that already holds
|
|
5
|
+
* both trees on disk. For each candidate global workflow it runs the workflow's
|
|
6
|
+
* `filter` — deciding whether the workflow applies to the source repo at all —
|
|
7
|
+
* and, for a survivor, its `DynamicJobFn`s, so the orchestrator can dispatch
|
|
8
|
+
* generated jobs it could not otherwise see. The round precedes every run row:
|
|
9
|
+
* a `filter` returning `false` means no run is created.
|
|
10
|
+
*
|
|
11
|
+
* Shape is modelled on `init-runner.ts` — load the module, resolve the target,
|
|
12
|
+
* evaluate under `withTimeout`, return a structured result. As there, author
|
|
13
|
+
* code runs in the agent's ordinary module loader: no `vm`, no `new Function`,
|
|
14
|
+
* no dynamic `eval`.
|
|
15
|
+
*
|
|
16
|
+
* Two properties are load-bearing and easy to lose:
|
|
17
|
+
*
|
|
18
|
+
* - **One bad candidate never sinks the round.** A candidate whose filter or
|
|
19
|
+
* generator throws, or that blows its budget, comes back
|
|
20
|
+
* `{ run: false, indeterminate: true, reason }` while its siblings carry real
|
|
21
|
+
* verdicts. Several unrelated org-wide workflows share a round; a single
|
|
22
|
+
* broken filter suppressing all of them would be a silent outage.
|
|
23
|
+
* - **Candidates are STARTED sequentially.** They share one checkout and one
|
|
24
|
+
* working directory, so a parallel `$` would race on cwd. This is a strict
|
|
25
|
+
* guarantee on the happy path and **best-effort past a timeout**: `withTimeout`
|
|
26
|
+
* races rather than cancels, so a candidate that blew its own budget is still
|
|
27
|
+
* running when the next one starts. Nothing can preempt user code mid-`await`,
|
|
28
|
+
* so the round bounds the damage rather than eliminating it — the loop stops
|
|
29
|
+
* at the round deadline (and on the caller's abort signal), which caps the
|
|
30
|
+
* overlap at one orphaned candidate instead of the whole remaining queue.
|
|
31
|
+
*/
|
|
32
|
+
import type { RepoInfo, DynamicJobContext, Logger } from '@kici-dev/sdk';
|
|
33
|
+
import { type $ as Shell } from 'zx';
|
|
34
|
+
import type { ChangedFilesStatus, GlobalEvalRoundResult } from '@kici-dev/engine';
|
|
35
|
+
/** One workflow the round must decide on, as the lock file describes it. */
|
|
36
|
+
export interface GlobalEvalCandidate {
|
|
37
|
+
workflowName: string;
|
|
38
|
+
/** Repo-relative path of the workflow module inside the workflow checkout. */
|
|
39
|
+
sourceFile: string;
|
|
40
|
+
/** From `LockWorkflow.hasFilter` — skip the filter call entirely when false. */
|
|
41
|
+
hasFilter: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Arguments for {@link runGlobalEvalRound}. */
|
|
44
|
+
export interface GlobalEvalRoundArgs {
|
|
45
|
+
/** Absolute path of the workflow-repo checkout (modules are loaded from here). */
|
|
46
|
+
workflowDir: string;
|
|
47
|
+
/** Absolute path of the source-repo checkout. */
|
|
48
|
+
sourceDir: string;
|
|
49
|
+
repos: {
|
|
50
|
+
sourceRepo: RepoInfo;
|
|
51
|
+
workflowRepo: RepoInfo;
|
|
52
|
+
};
|
|
53
|
+
candidates: GlobalEvalCandidate[];
|
|
54
|
+
/** The raw wire event, carrying the normalized event envelope. */
|
|
55
|
+
event: Record<string, unknown>;
|
|
56
|
+
changedFiles: string[];
|
|
57
|
+
/**
|
|
58
|
+
* Availability of `changedFiles`. Required, not defaulted: it feeds the
|
|
59
|
+
* throwing accessor on `FilterContext`, and defaulting it to `'fetched'`
|
|
60
|
+
* would let a diff-less event read as an empty diff and silently suppress
|
|
61
|
+
* every path-gated workflow in the round.
|
|
62
|
+
*/
|
|
63
|
+
changedFilesStatus: ChangedFilesStatus;
|
|
64
|
+
/** Wall-clock budget for the whole round. */
|
|
65
|
+
roundTimeoutMs: number;
|
|
66
|
+
/** Wall-clock budget for one candidate (its filter plus its generators). */
|
|
67
|
+
candidateTimeoutMs: number;
|
|
68
|
+
/**
|
|
69
|
+
* Caller's cancellation signal. The round stops starting new candidates once
|
|
70
|
+
* it fires; the one already in flight cannot be preempted (see the module
|
|
71
|
+
* doc comment) but its verdict is discarded.
|
|
72
|
+
*/
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
/** zx shell handed to filters and generators. Defaults to the ambient `$`. */
|
|
75
|
+
$?: typeof Shell;
|
|
76
|
+
/** Logger handed to generators. Defaults to a no-op. */
|
|
77
|
+
log?: Logger;
|
|
78
|
+
/** KiCI API handed to generators. Defaults to one that rejects every call. */
|
|
79
|
+
kici?: DynamicJobContext['kici'];
|
|
80
|
+
/**
|
|
81
|
+
* Module loader seam. Defaults to `loadWorkflowSource` against `workflowDir`;
|
|
82
|
+
* a caller that already holds the modules (or a unit test) supplies its own.
|
|
83
|
+
*/
|
|
84
|
+
loadModule?: (sourceFile: string) => Promise<Record<string, unknown>>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run one global eval round and return every candidate's verdict, in candidate
|
|
88
|
+
* order. Never throws: a round that exceeds `roundTimeoutMs` reports whatever
|
|
89
|
+
* it established and marks the rest indeterminate.
|
|
90
|
+
*/
|
|
91
|
+
export declare function runGlobalEvalRound(args: GlobalEvalRoundArgs): Promise<GlobalEvalRoundResult>;
|
|
92
|
+
//# sourceMappingURL=global-eval-runner.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single writer of the seven `KICI_*` ambient env keys a global workflow's
|
|
3
|
+
* user code sees.
|
|
4
|
+
*
|
|
5
|
+
* It lives here rather than in `sandbox/workflow-runner.ts` because that module
|
|
6
|
+
* calls `main()` at import time — it is the sandbox process entry point — so the
|
|
7
|
+
* agent's own process cannot import it. The pre-dispatch global eval round runs
|
|
8
|
+
* in the agent process and must set exactly the same keys the sandbox does.
|
|
9
|
+
*
|
|
10
|
+
* Why exactly the same keys: `extractAndNormalizeSteps` hands a `DynamicJobFn`
|
|
11
|
+
* `process.env` as its `env`, and a generator is evaluated twice — once
|
|
12
|
+
* pre-dispatch, once inside the sandbox to recover the step closures a `LockJob`
|
|
13
|
+
* cannot carry. A generator that reads `process.env.KICI_SOURCE_REPO_PATH` and
|
|
14
|
+
* gets a value on one call and `undefined` on the other has seen two different
|
|
15
|
+
* worlds, which `extractStepsFromDynamicJob` turns into a hard determinism
|
|
16
|
+
* failure. `buildGeneratorContext` keeps the two contexts' *shape* identical; it
|
|
17
|
+
* cannot keep the ambient env identical, which is what this does.
|
|
18
|
+
*/
|
|
19
|
+
import type { RepoInfo } from '@kici-dev/sdk';
|
|
20
|
+
/** The source / workflow repo pair a global-workflow evaluation runs against. */
|
|
21
|
+
export interface GlobalWorkflowRepos {
|
|
22
|
+
sourceRepo: RepoInfo;
|
|
23
|
+
workflowRepo: RepoInfo;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Derive an `owner/repo` identifier from a clone URL, stripping the trailing
|
|
27
|
+
* `.git` and any `http(s)://host/` prefix.
|
|
28
|
+
*/
|
|
29
|
+
export declare function repoIdentifierFromUrl(repoUrl: string): string;
|
|
30
|
+
/** Every env key {@link applyGlobalWorkflowEnv} writes, in one place. */
|
|
31
|
+
export declare const GLOBAL_WORKFLOW_ENV_KEYS: readonly ['KICI_IS_GLOBAL_WORKFLOW', 'KICI_WORKFLOW_REPO_PATH', 'KICI_SOURCE_REPO_PATH', 'KICI_SOURCE_REPO', 'KICI_SOURCE_BRANCH', 'KICI_SOURCE_SHA', 'KICI_WORKFLOW_REPO'];
|
|
32
|
+
/**
|
|
33
|
+
* Inject the seven global-workflow env keys and return a restorer that puts
|
|
34
|
+
* `process.env` back exactly as it was — each key reset to its prior value, or
|
|
35
|
+
* deleted if it had none.
|
|
36
|
+
*
|
|
37
|
+
* **The restorer is mandatory for any caller in a long-lived process.** The
|
|
38
|
+
* sandbox may ignore it: it runs one job per forked child, which exits. The
|
|
39
|
+
* pre-dispatch global eval round may NOT: it runs in the agent process, which
|
|
40
|
+
* serves many dispatches from one `JobRunner`. Leaving the keys set there is
|
|
41
|
+
* this module's own hazard running backwards — a later NON-global
|
|
42
|
+
* `DynamicJobFn` evaluation builds its generator context with
|
|
43
|
+
* `env: process.env` still carrying `KICI_IS_GLOBAL_WORKFLOW=true` and a
|
|
44
|
+
* `KICI_SOURCE_REPO_PATH` pointing at a deleted work directory, while that
|
|
45
|
+
* job's own sandbox re-evaluation sees neither (`buildSanitizedEnv` scrubs the
|
|
46
|
+
* whole `KICI_*` namespace on the trusted profile, and the default profile is
|
|
47
|
+
* allowlist-only). That is the same two-worlds determinism failure, injected
|
|
48
|
+
* into an unrelated job.
|
|
49
|
+
*
|
|
50
|
+
* `RepoInfo.ref` / `.sha` are optional, so an evaluation with no checkout
|
|
51
|
+
* metadata writes an empty string rather than leaving the key unset — matching
|
|
52
|
+
* how `KICI_WORKFLOW_REPO` already handles a missing identifier. Assigning
|
|
53
|
+
* `undefined` to a `process.env` key would stringify to `"undefined"`, which is
|
|
54
|
+
* worse than either.
|
|
55
|
+
*/
|
|
56
|
+
export declare function applyGlobalWorkflowEnv(repos: GlobalWorkflowRepos): () => void;
|
|
57
|
+
//# sourceMappingURL=global-workflow-env.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a job's container image with the host's build CLI.
|
|
3
|
+
*
|
|
4
|
+
* The CLI is REQUIRED — there is deliberately no socket-API fallback. One build
|
|
5
|
+
* path means one set of Dockerfile semantics: `.dockerignore`, BuildKit and
|
|
6
|
+
* every directive behave as they do on the author's own machine, instead of
|
|
7
|
+
* depending on which agent happened to pick the job up. A host without a CLI
|
|
8
|
+
* cannot run a Dockerfile job, and says so in as many words.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import type { JobImageBuildSpec } from './resolve-build-spec.js';
|
|
12
|
+
/** The build CLIs an agent host may provide. */
|
|
13
|
+
export declare const ContainerBuildCli: z.ZodEnum<{
|
|
14
|
+
docker: "docker";
|
|
15
|
+
podman: "podman";
|
|
16
|
+
}>;
|
|
17
|
+
export type ContainerBuildCli = z.infer<typeof ContainerBuildCli>;
|
|
18
|
+
/** Is `bin` executable somewhere on `PATH`? */
|
|
19
|
+
export declare function binaryOnPath(bin: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The container socket the SANDBOX will use.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors what dockerode's `new Docker()` resolves, and exists so build and run
|
|
24
|
+
* provably agree. A host with both runtimes whose sandbox socket points at
|
|
25
|
+
* podman would otherwise build with docker and then start the job container on
|
|
26
|
+
* a daemon that has never heard of that image — a failure that surfaces as
|
|
27
|
+
* "no such image" and names nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sandboxSocketPath(): string;
|
|
30
|
+
export declare function resolveBuildCli(args: {
|
|
31
|
+
configured?: ContainerBuildCli | undefined;
|
|
32
|
+
onPath?: (bin: string) => boolean;
|
|
33
|
+
}): ContainerBuildCli;
|
|
34
|
+
export declare function buildArgv(args: {
|
|
35
|
+
cli: ContainerBuildCli;
|
|
36
|
+
spec: JobImageBuildSpec;
|
|
37
|
+
socketPath?: string | undefined;
|
|
38
|
+
}): string[];
|
|
39
|
+
/** Registry credentials in the shape a build CLI's config file expects. */
|
|
40
|
+
export interface BuildAuthconfig {
|
|
41
|
+
username: string;
|
|
42
|
+
password: string;
|
|
43
|
+
serveraddress: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Split a chunk stream into whole lines, carrying the remainder between chunks.
|
|
47
|
+
*
|
|
48
|
+
* `flush` matters: a builder whose last line has no trailing newline — which is
|
|
49
|
+
* exactly the shape of an error written just before exit — would otherwise leave
|
|
50
|
+
* that line in the carry, and the one line the author most needs would be the
|
|
51
|
+
* one that never reaches the run log.
|
|
52
|
+
*/
|
|
53
|
+
export declare function makeLineSplitter(onLine: (line: string) => void): {
|
|
54
|
+
write: (chunk: Buffer) => void;
|
|
55
|
+
flush: () => void;
|
|
56
|
+
};
|
|
57
|
+
export interface BuildJobImageArgs {
|
|
58
|
+
spec: JobImageBuildSpec;
|
|
59
|
+
cli: ContainerBuildCli;
|
|
60
|
+
socketPath?: string | undefined;
|
|
61
|
+
authconfig?: BuildAuthconfig | undefined;
|
|
62
|
+
/** Every line of build output, in order, for the run log. */
|
|
63
|
+
onLog: (line: string) => void;
|
|
64
|
+
signal?: AbortSignal | undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Run the build. Resolves when the image is tagged; rejects with the builder's
|
|
68
|
+
* own last words otherwise.
|
|
69
|
+
*
|
|
70
|
+
* The rejection carries real output rather than an exit code because the author
|
|
71
|
+
* is the one who has to act on it, and "exit 1" tells them nothing about which
|
|
72
|
+
* `RUN` failed.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildJobImage(args: BuildJobImageArgs): Promise<void>;
|
|
75
|
+
//# sourceMappingURL=build-engine.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a job's image build and render it as a step in the run.
|
|
3
|
+
*
|
|
4
|
+
* The build happens on the AGENT, before the sandbox exists, so it cannot use
|
|
5
|
+
* the runner's IPC pseudo-step channel that the cache phase uses — that channel
|
|
6
|
+
* belongs to a process which has not started yet. It uses the agent-side seam
|
|
7
|
+
* the synthetic `build` / `init` / `global-eval` jobs already use: a step-status
|
|
8
|
+
* pair plus a log streamer bound to a step index.
|
|
9
|
+
*/
|
|
10
|
+
import { ExecutionStepStatus } from '@kici-dev/engine';
|
|
11
|
+
import type { LockJob } from '@kici-dev/engine';
|
|
12
|
+
import { type JobImageBuildSpec } from './resolve-build-spec.js';
|
|
13
|
+
/** Step name the run timeline shows for a job's image build. */
|
|
14
|
+
export declare const CONTAINER_BUILD_STEP_NAME = "container:build";
|
|
15
|
+
/**
|
|
16
|
+
* Step index the image build reports under.
|
|
17
|
+
*
|
|
18
|
+
* A large positive constant, for two reasons that are easy to trip over:
|
|
19
|
+
*
|
|
20
|
+
* - `step.status` carries `z.number().int().nonnegative()`, so a negative index
|
|
21
|
+
* is not available. An agent that sent one to an OLDER orchestrator would be
|
|
22
|
+
* disconnected mid-job (an invalid message closes the socket), and two
|
|
23
|
+
* consumers read a negative index as absent — the check-run reporter writes
|
|
24
|
+
* `steps[stepIndex]`, and the dashboard gates log fetching on `stepIndex >= 0`.
|
|
25
|
+
* - It must clear every index the in-sandbox allocators reach. The cache phase
|
|
26
|
+
* tops out around `stepCount * 3 + 100 + (stepCount + 1) * 1000`, so this
|
|
27
|
+
* clears a job of several hundred steps with room to spare.
|
|
28
|
+
*
|
|
29
|
+
* DISPLAY ORDER does not come from this index. The step is tagged with the
|
|
30
|
+
* `container:build` {@link SetupStepType}, and readers sort setup pseudo-steps
|
|
31
|
+
* ahead of the real steps by type — so the build renders where it ran, first,
|
|
32
|
+
* without a negative index or a protocol change.
|
|
33
|
+
*/
|
|
34
|
+
export declare const CONTAINER_BUILD_STEP_INDEX = 1000000;
|
|
35
|
+
export interface RunJobImageBuildArgs {
|
|
36
|
+
container: LockJob['container'];
|
|
37
|
+
workDir: string;
|
|
38
|
+
jobId: string;
|
|
39
|
+
jobName: string;
|
|
40
|
+
/** Perform the build. Injected so the step logic is testable without a host. */
|
|
41
|
+
build: (spec: JobImageBuildSpec, onLog: (line: string) => void) => Promise<void>;
|
|
42
|
+
/** Every line of build output, in order, for the run log. */
|
|
43
|
+
onLog: (line: string) => void;
|
|
44
|
+
sendStepStatus: (name: string, state: ExecutionStepStatus, data?: Record<string, unknown>) => void;
|
|
45
|
+
fileExists?: (p: string) => boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the job's image when it declared a Dockerfile, and return the tag the
|
|
49
|
+
* sandbox should run.
|
|
50
|
+
*
|
|
51
|
+
* Returns `undefined` when there is nothing to build — a job with no container,
|
|
52
|
+
* or one that names a finalized image. That is the common case and emits no
|
|
53
|
+
* step at all: a run timeline should not grow an empty entry for work that did
|
|
54
|
+
* not happen.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runJobImageBuild(args: RunJobImageBuildArgs): Promise<string | undefined>;
|
|
57
|
+
//# sourceMappingURL=build-step.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a job's `container.dockerfile` into an absolute, checked build spec.
|
|
3
|
+
*
|
|
4
|
+
* Pure on purpose. Every rule that decides what gets built — the anchoring, the
|
|
5
|
+
* escape refusal, the tag shape — is decided here and unit-tested without a
|
|
6
|
+
* container runtime anywhere near it. The half that needs a host lives in
|
|
7
|
+
* `build-engine.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import type { LockJob } from '@kici-dev/engine';
|
|
10
|
+
/** Everything the build CLI needs, resolved and checked. */
|
|
11
|
+
export interface JobImageBuildSpec {
|
|
12
|
+
/** Absolute path to the Dockerfile. */
|
|
13
|
+
dockerfilePath: string;
|
|
14
|
+
/** Absolute build-context directory. */
|
|
15
|
+
contextDir: string;
|
|
16
|
+
/** Build stage to stop at, when the job named one. */
|
|
17
|
+
target?: string;
|
|
18
|
+
/** Build arguments. Plain strings — never secret material. */
|
|
19
|
+
args: Record<string, string>;
|
|
20
|
+
/** Tag the built image gets, and the sandbox then runs. */
|
|
21
|
+
tag: string;
|
|
22
|
+
/** Labels that put the image in reach of the leak sweep. */
|
|
23
|
+
labels: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
export interface ResolveJobImageBuildSpecArgs {
|
|
26
|
+
container: LockJob['container'];
|
|
27
|
+
/** The cloned tree. Every path resolves against this and must stay inside it. */
|
|
28
|
+
workDir: string;
|
|
29
|
+
jobId: string;
|
|
30
|
+
jobName: string;
|
|
31
|
+
/** Injected for tests; defaults to a real filesystem check. */
|
|
32
|
+
fileExists?: (p: string) => boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the build spec for a job, or `undefined` when the job builds nothing.
|
|
36
|
+
*
|
|
37
|
+
* `undefined` is the common case and is not a failure: a job with no container,
|
|
38
|
+
* or one that names a finalized image, has nothing to build.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveJobImageBuildSpec(args: ResolveJobImageBuildSpecArgs): JobImageBuildSpec | undefined;
|
|
41
|
+
//# sourceMappingURL=resolve-build-spec.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this agent's own host can do with containers, discovered at startup.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator cannot answer this. An agent runs on its own machine, and
|
|
5
|
+
* whether that machine has a container runtime is the agent's fact — probing
|
|
6
|
+
* the orchestrator's filesystem answers a different question, and doing so once
|
|
7
|
+
* stranded container jobs that had been running fine, because the probe and the
|
|
8
|
+
* job ran in different places.
|
|
9
|
+
*
|
|
10
|
+
* Reported as `kici:runtime:*` labels, which the register-time scope gate
|
|
11
|
+
* accepts unchallenged as self-reported facts. Deliberately NOT
|
|
12
|
+
* `kici:capability:*` — that prefix grants a privilege and stays token-bound.
|
|
13
|
+
*/
|
|
14
|
+
import { RuntimeFact } from '@kici-dev/engine';
|
|
15
|
+
export interface DetectRuntimeFactsDeps {
|
|
16
|
+
/** Injected for tests; defaults to a real filesystem check. */
|
|
17
|
+
pathExists?: (p: string) => boolean;
|
|
18
|
+
binaryOnPath?: (bin: string) => boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Discover this host's runtime facts.
|
|
22
|
+
*
|
|
23
|
+
* Presence of the socket FILE, not a handshake: registration must not block on
|
|
24
|
+
* a daemon that is slow or wedged, and a job that reaches a broken runtime
|
|
25
|
+
* still fails with the runtime's own error. The label answers "is there a
|
|
26
|
+
* runtime here at all", which is the routing question.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectRuntimeFacts(deps?: DetectRuntimeFactsDeps): RuntimeFact[];
|
|
29
|
+
/** The `kici:runtime:*` labels this host should register with. */
|
|
30
|
+
export declare function runtimeFactLabels(deps?: DetectRuntimeFactsDeps): string[];
|
|
31
|
+
//# sourceMappingURL=runtime-facts.d.ts.map
|