@kici-dev/orchestrator 0.18.0 → 0.19.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/agent/registry.d.ts +22 -3
- package/dist/cli/commands/shared/versioned-upgrade.d.ts +7 -7
- package/dist/cli/service/instance/manifest.d.ts +30 -4
- package/dist/cli.js +122 -40
- package/dist/dashboard/handler.d.ts +2 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +1 -1
- package/dist/pipeline/global-eval-round.d.ts +0 -8
- package/dist/scaler/bare-metal-backend.d.ts +2 -1
- package/dist/scaler/container-hostconfig.d.ts +9 -0
- package/dist/scaler/warm-pool.d.ts +4 -1
- package/dist/server.js +1161 -973
- package/dist/standalone.js +562 -478
- package/dist/ws/execution-status-frame.d.ts +2 -1
- package/dist/ws/job-status-forward-frame.d.ts +30 -0
- package/dist/ws/outbound-frame-guard.d.ts +4 -0
- package/dist/ws/platform-client.d.ts +11 -1
- package/dist/ws/test-relay-handlers.d.ts +15 -5
- package/dist/ws/test-run-log-cursor.d.ts +18 -0
- package/installer-image-digests.json +3 -3
- package/package.json +5 -5
- package/sbom.spdx.json +53 -53
package/dist/agent/registry.d.ts
CHANGED
|
@@ -70,8 +70,16 @@ export interface AgentEntry {
|
|
|
70
70
|
* In memory rather than DB-backed, unlike reboot-pending: the destroy is issued by
|
|
71
71
|
* this process, lasts seconds, and ends with the WebSocket close that removes the
|
|
72
72
|
* entry — there is nothing to outlive.
|
|
73
|
+
*
|
|
74
|
+
* Also set when the agent itself reports `draining` on `agent.status` (a SIGUSR1
|
|
75
|
+
* drain, or a single-job agent that accepted its job); see {@link drainReported}.
|
|
73
76
|
*/
|
|
74
77
|
draining: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The agent reported `draining` itself. Its drain is terminal for this connection,
|
|
80
|
+
* so {@link AgentRegistry.clearDraining} (the warm-pool undo) leaves it draining.
|
|
81
|
+
*/
|
|
82
|
+
drainReported?: boolean;
|
|
75
83
|
/**
|
|
76
84
|
* Epoch ms until which the agent is held out of routing after it rejected a
|
|
77
85
|
* dispatch as busy. Unset when no hold is in force. See {@link AgentRegistry.markBusyHeld}.
|
|
@@ -337,8 +345,11 @@ export declare class AgentRegistry {
|
|
|
337
345
|
*/
|
|
338
346
|
isRegistered(agentId: string): boolean;
|
|
339
347
|
/**
|
|
340
|
-
* Condemn an agent
|
|
341
|
-
*
|
|
348
|
+
* Condemn an agent: it stays registered and keeps finishing its current work,
|
|
349
|
+
* but takes no new dispatches.
|
|
350
|
+
*
|
|
351
|
+
* Called before a warm-pool destroy is issued. An agent that reports its own
|
|
352
|
+
* drain goes through {@link markDrainReported} instead.
|
|
342
353
|
*
|
|
343
354
|
* @returns the agent's `activeJobs`, so the caller can see a job claimed between its
|
|
344
355
|
* idle check and this mark and back out; `undefined` when not registered
|
|
@@ -382,7 +393,15 @@ export declare class AgentRegistry {
|
|
|
382
393
|
* free slot now.
|
|
383
394
|
*/
|
|
384
395
|
clearExpiredBusyHold(agentId: string, now?: number): void;
|
|
385
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* The agent reported on `agent.status` that it takes no new job: a drain, or a
|
|
398
|
+
* single-job agent that has accepted its job. Terminal for this connection.
|
|
399
|
+
*/
|
|
400
|
+
markDrainReported(agentId: string): void;
|
|
401
|
+
/**
|
|
402
|
+
* Undo {@link markDraining} — a destroy that was rejected leaves the agent alive.
|
|
403
|
+
* An agent that reported its own drain stays draining.
|
|
404
|
+
*/
|
|
386
405
|
clearDraining(agentId: string): void;
|
|
387
406
|
get(agentId: string): AgentEntry | undefined;
|
|
388
407
|
getByWs(ws: WsLike): AgentEntry | undefined;
|
|
@@ -265,13 +265,13 @@ export declare function npmReleaseReadsEnvFile(target: NpmInstallTarget, version
|
|
|
265
265
|
error: string;
|
|
266
266
|
};
|
|
267
267
|
/**
|
|
268
|
-
* Install `<owningPackage>@<version>` globally
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
268
|
+
* Install `<owningPackage>@<version>` globally into the prefix the unit's
|
|
269
|
+
* ExecStart resolves from, with the unit's pinned node. Without an explicit
|
|
270
|
+
* prefix npm derives one from the node that runs npm-cli.js, or takes the
|
|
271
|
+
* `npm_config_prefix` the caller's environment carries (`npx` exports its
|
|
272
|
+
* own), so the install would land where the unit never reads it. The pinned
|
|
273
|
+
* node runs npm-cli.js directly, skipping a version-manager wrapper at
|
|
274
|
+
* `<pinned>/bin/npm`, which also keeps the shell's active node out of it.
|
|
275
275
|
*/
|
|
276
276
|
export declare function installGlobalPackage(target: NpmInstallTarget, version: string, run?: SpawnRunner): {
|
|
277
277
|
ok: boolean;
|
|
@@ -57,16 +57,42 @@ export declare function writeInstallManifest(instanceDir: string, fresh: Instanc
|
|
|
57
57
|
* Where an npm-source upgrade installs, from a unit's launch spec.
|
|
58
58
|
* `owningPackage` is the top-level global package ExecStart runs (`kici-admin`
|
|
59
59
|
* when the component loads through its nested node_modules, else
|
|
60
|
-
* `@kici-dev/<component>`). `
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* `@kici-dev/<component>`). `globalPrefix` is the npm prefix the launch entry
|
|
61
|
+
* lives in; every npm call passes it as `--prefix`, because the caller's
|
|
62
|
+
* environment can name another one (`npx` exports `npm_config_prefix`).
|
|
63
|
+
* `npmInvocation` runs `npm-cli.js` with the pinned node when it sits beside
|
|
64
|
+
* that node, so a version-manager wrapper at `npmPath` (mise's reshim needs
|
|
65
|
+
* mise on PATH) is never started.
|
|
63
66
|
*/
|
|
64
67
|
export interface NpmInstallTarget {
|
|
65
68
|
nodeExecPath: string;
|
|
66
69
|
npmPath: string;
|
|
67
70
|
owningPackage: string;
|
|
71
|
+
globalPrefix: string;
|
|
72
|
+
npmInvocation: {
|
|
73
|
+
cmd: string;
|
|
74
|
+
preArgs: string[];
|
|
75
|
+
};
|
|
68
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The resolution of a launch spec to an install target. `no-entry` covers an
|
|
79
|
+
* opaque `--binary` install and a path outside `node_modules`; `not-global`
|
|
80
|
+
* an entry under a local `node_modules` tree, whose prefix the unit's npm
|
|
81
|
+
* cannot install into with `-g`.
|
|
82
|
+
*/
|
|
83
|
+
export type NpmInstallResolution = {
|
|
84
|
+
ok: true;
|
|
85
|
+
target: NpmInstallTarget;
|
|
86
|
+
} | {
|
|
87
|
+
ok: false;
|
|
88
|
+
reason: 'no-entry';
|
|
89
|
+
} | {
|
|
90
|
+
ok: false;
|
|
91
|
+
reason: 'not-global';
|
|
92
|
+
entry: string;
|
|
93
|
+
};
|
|
69
94
|
export declare function resolveNpmInstallTarget(spec: LaunchSpec, component: Component, opts: {
|
|
70
95
|
windows: boolean;
|
|
71
|
-
|
|
96
|
+
exists?: (p: string) => boolean;
|
|
97
|
+
}): NpmInstallResolution;
|
|
72
98
|
//# sourceMappingURL=manifest.d.ts.map
|
package/dist/cli.js
CHANGED
|
@@ -15246,19 +15246,60 @@ function writeInstallManifest(instanceDir, fresh, launch, opts = {}) {
|
|
|
15246
15246
|
function resolveNpmInstallTarget(spec, component, opts) {
|
|
15247
15247
|
const entryRe = new RegExp(`[/\\\\]@kici-dev[/\\\\]${component}[/\\\\]dist[/\\\\](?:server|standalone)\\.js$`);
|
|
15248
15248
|
const entry = [spec.execPath, ...spec.args].find((a) => entryRe.test(a));
|
|
15249
|
-
if (!entry) return
|
|
15249
|
+
if (!entry) return {
|
|
15250
|
+
ok: false,
|
|
15251
|
+
reason: "no-entry"
|
|
15252
|
+
};
|
|
15250
15253
|
const owningPackage = firstGlobalPackage(entry);
|
|
15251
|
-
if (!owningPackage) return
|
|
15254
|
+
if (!owningPackage) return {
|
|
15255
|
+
ok: false,
|
|
15256
|
+
reason: "no-entry"
|
|
15257
|
+
};
|
|
15252
15258
|
const p = opts.windows ? path.win32 : path.posix;
|
|
15253
|
-
const
|
|
15254
|
-
|
|
15259
|
+
const globalPrefix = globalPrefixOf(entry, opts.windows);
|
|
15260
|
+
if (!globalPrefix) return {
|
|
15261
|
+
ok: false,
|
|
15262
|
+
reason: "not-global",
|
|
15263
|
+
entry
|
|
15264
|
+
};
|
|
15265
|
+
const nodeDir = p.dirname(spec.execPath);
|
|
15266
|
+
const npmPath = p.join(nodeDir, opts.windows ? "npm.cmd" : "npm");
|
|
15267
|
+
const npmCliJs = opts.windows ? p.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js") : p.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
15268
|
+
const npmInvocation = (opts.exists ?? fs.existsSync)(npmCliJs) ? {
|
|
15269
|
+
cmd: spec.execPath,
|
|
15270
|
+
preArgs: [npmCliJs]
|
|
15271
|
+
} : {
|
|
15272
|
+
cmd: npmPath,
|
|
15273
|
+
preArgs: []
|
|
15274
|
+
};
|
|
15255
15275
|
return {
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15276
|
+
ok: true,
|
|
15277
|
+
target: {
|
|
15278
|
+
nodeExecPath: spec.execPath,
|
|
15279
|
+
npmPath,
|
|
15280
|
+
owningPackage,
|
|
15281
|
+
globalPrefix,
|
|
15282
|
+
npmInvocation
|
|
15283
|
+
}
|
|
15259
15284
|
};
|
|
15260
15285
|
}
|
|
15261
15286
|
/**
|
|
15287
|
+
* The npm global prefix holding `entry`, split on its FIRST `node_modules`
|
|
15288
|
+
* segment like {@link firstGlobalPackage}. Windows lays a global install out as
|
|
15289
|
+
* `<prefix>\node_modules\<pkg>`; posix as `<prefix>/lib/node_modules/<pkg>`, so
|
|
15290
|
+
* there the segment before must be `lib`, which tells a global install from a
|
|
15291
|
+
* local `node_modules` tree. Null for anything else.
|
|
15292
|
+
*/
|
|
15293
|
+
function globalPrefixOf(entry, windows) {
|
|
15294
|
+
const p = windows ? path.win32 : path.posix;
|
|
15295
|
+
const segments = entry.split(/[/\\]/);
|
|
15296
|
+
const idx = segments.indexOf("node_modules");
|
|
15297
|
+
if (idx < 1) return null;
|
|
15298
|
+
if (windows) return segments.slice(0, idx).join(p.sep) || null;
|
|
15299
|
+
if (idx < 2 || segments[idx - 1] !== "lib") return null;
|
|
15300
|
+
return segments.slice(0, idx - 1).join(p.sep) || p.sep;
|
|
15301
|
+
}
|
|
15302
|
+
/**
|
|
15262
15303
|
* The package under the FIRST `node_modules` segment (the global root), which
|
|
15263
15304
|
* `npm install -g` must target; `@scope/name` keeps both segments. Null
|
|
15264
15305
|
* without a `node_modules` segment.
|
|
@@ -24448,14 +24489,18 @@ function planNpmSourceUpgrade(opts) {
|
|
|
24448
24489
|
kind: "error",
|
|
24449
24490
|
reason: "self-driving upgrade requires --version <target> (the version to install). Pass --restart-only to restart the already-installed package without installing."
|
|
24450
24491
|
};
|
|
24451
|
-
const
|
|
24452
|
-
if (!
|
|
24492
|
+
const resolution = spec ? resolveNpmInstallTarget(spec, component, { windows }) : null;
|
|
24493
|
+
if (resolution && !resolution.ok && resolution.reason === "not-global") return {
|
|
24494
|
+
kind: "error",
|
|
24495
|
+
reason: `npm-source upgrade cannot self-install: the unit's launch target ${resolution.entry} is not inside a global npm install, so the unit's npm has no global prefix that holds it. The unit runs ${spec.execPath}; this kici-admin runs ${process.execPath}. Use an archive upgrade (--from <archive> / --url <url> + --version), or pre-install the package and re-run with --restart-only.`
|
|
24496
|
+
};
|
|
24497
|
+
if (!resolution?.ok) return {
|
|
24453
24498
|
kind: "error",
|
|
24454
24499
|
reason: "npm-source upgrade cannot self-install: the unit's launch target is a custom --binary install or a non-node_modules path, so there is no global package to update. Use an archive upgrade (--from <archive> / --url <url> + --version), or pre-install the package and re-run with --restart-only."
|
|
24455
24500
|
};
|
|
24456
24501
|
return {
|
|
24457
24502
|
kind: "self-drive",
|
|
24458
|
-
target,
|
|
24503
|
+
target: resolution.target,
|
|
24459
24504
|
version: requestedVersion
|
|
24460
24505
|
};
|
|
24461
24506
|
}
|
|
@@ -24471,9 +24516,14 @@ function windowsShellCommand(cmd, args) {
|
|
|
24471
24516
|
for (const arg of args) if (!/^[A-Za-z0-9@/._+-]+$/.test(arg)) throw new Error(`cannot pass "${arg}" to npm: it holds a character cmd.exe reads as syntax`);
|
|
24472
24517
|
return [`"${cmd}"`, ...args].join(" ");
|
|
24473
24518
|
}
|
|
24519
|
+
/**
|
|
24520
|
+
* Only a Windows batch file (`npm.cmd`) goes through cmd.exe; the pinned
|
|
24521
|
+
* `node.exe` running npm-cli.js spawns directly, with no shell to parse its
|
|
24522
|
+
* `C:\…` arguments.
|
|
24523
|
+
*/
|
|
24474
24524
|
const defaultSpawnRunner = (cmd, args, opts) => {
|
|
24475
24525
|
const env = opts?.env ?? process.env;
|
|
24476
|
-
const r = process.platform === "win32" ? spawnSync(windowsShellCommand(cmd, args), {
|
|
24526
|
+
const r = process.platform === "win32" && /\.cmd$/i.test(cmd) ? spawnSync(windowsShellCommand(cmd, args), {
|
|
24477
24527
|
encoding: "utf-8",
|
|
24478
24528
|
shell: true,
|
|
24479
24529
|
env
|
|
@@ -24488,15 +24538,40 @@ const defaultSpawnRunner = (cmd, args, opts) => {
|
|
|
24488
24538
|
};
|
|
24489
24539
|
};
|
|
24490
24540
|
/**
|
|
24491
|
-
* The environment npm runs in: the unit's pinned node first on PATH,
|
|
24492
|
-
*
|
|
24541
|
+
* The environment npm runs in: the unit's pinned node first on PATH, so a
|
|
24542
|
+
* wrapper fallback (`npmPath`, an `env node` shebang) resolves that node. The
|
|
24543
|
+
* caller's `npm_config_*` stay, so an operator-set registry or proxy still
|
|
24544
|
+
* applies.
|
|
24545
|
+
*
|
|
24546
|
+
* On the `npm.cmd` fallback the prefix goes through `npm_config_prefix`:
|
|
24547
|
+
* {@link windowsShellCommand} refuses the `\`, `:` and `=` a `--prefix=C:\…`
|
|
24548
|
+
* argument holds. Every other invocation passes `--prefix` on the command
|
|
24549
|
+
* line ({@link npmArgs}), which npm ranks above the environment.
|
|
24493
24550
|
*/
|
|
24494
24551
|
function npmEnv(target) {
|
|
24495
24552
|
const pinnedBinDir = path.dirname(target.nodeExecPath);
|
|
24496
|
-
|
|
24553
|
+
const env = {
|
|
24497
24554
|
...process.env,
|
|
24498
24555
|
PATH: `${pinnedBinDir}${path.delimiter}${process.env.PATH ?? ""}`
|
|
24499
24556
|
};
|
|
24557
|
+
if (prefixViaEnv(target)) {
|
|
24558
|
+
for (const key of Object.keys(env)) if (/^npm_config_(global_)?prefix$/i.test(key)) delete env[key];
|
|
24559
|
+
env.npm_config_prefix = target.globalPrefix;
|
|
24560
|
+
}
|
|
24561
|
+
return env;
|
|
24562
|
+
}
|
|
24563
|
+
/** Whether the prefix travels in the environment: the `npm.cmd` fallback. */
|
|
24564
|
+
function prefixViaEnv(target) {
|
|
24565
|
+
return target.npmInvocation.preArgs.length === 0 && /\.cmd$/i.test(target.npmInvocation.cmd);
|
|
24566
|
+
}
|
|
24567
|
+
/** The argv for one npm call: npm-cli.js first when the pinned node runs it, `--prefix` last. */
|
|
24568
|
+
function npmArgs(target, args) {
|
|
24569
|
+
const prefix = prefixViaEnv(target) ? [] : [`--prefix=${target.globalPrefix}`];
|
|
24570
|
+
return [
|
|
24571
|
+
...target.npmInvocation.preArgs,
|
|
24572
|
+
...args,
|
|
24573
|
+
...prefix
|
|
24574
|
+
];
|
|
24500
24575
|
}
|
|
24501
24576
|
/**
|
|
24502
24577
|
* Whether `version` reads KICI_ENV_FILE, asked of the unit's npm registry. Such
|
|
@@ -24504,12 +24579,12 @@ function npmEnv(target) {
|
|
|
24504
24579
|
* KiCI package of that version depends on.
|
|
24505
24580
|
*/
|
|
24506
24581
|
function npmReleaseReadsEnvFile(target, version, run = defaultSpawnRunner) {
|
|
24507
|
-
const res = run(target.
|
|
24582
|
+
const res = run(target.npmInvocation.cmd, npmArgs(target, [
|
|
24508
24583
|
"view",
|
|
24509
24584
|
`@kici-dev/shared@${version}`,
|
|
24510
24585
|
"exports",
|
|
24511
24586
|
"--json"
|
|
24512
|
-
], { env: npmEnv(target) });
|
|
24587
|
+
]), { env: npmEnv(target) });
|
|
24513
24588
|
if (res.status !== 0) return {
|
|
24514
24589
|
ok: false,
|
|
24515
24590
|
error: res.stderr.trim() || `npm view exited with ${res.status}`
|
|
@@ -24538,20 +24613,20 @@ function npmReleaseReadsEnvFile(target, version, run = defaultSpawnRunner) {
|
|
|
24538
24613
|
};
|
|
24539
24614
|
}
|
|
24540
24615
|
/**
|
|
24541
|
-
* Install `<owningPackage>@<version>` globally
|
|
24542
|
-
*
|
|
24543
|
-
*
|
|
24544
|
-
*
|
|
24545
|
-
*
|
|
24546
|
-
*
|
|
24547
|
-
*
|
|
24616
|
+
* Install `<owningPackage>@<version>` globally into the prefix the unit's
|
|
24617
|
+
* ExecStart resolves from, with the unit's pinned node. Without an explicit
|
|
24618
|
+
* prefix npm derives one from the node that runs npm-cli.js, or takes the
|
|
24619
|
+
* `npm_config_prefix` the caller's environment carries (`npx` exports its
|
|
24620
|
+
* own), so the install would land where the unit never reads it. The pinned
|
|
24621
|
+
* node runs npm-cli.js directly, skipping a version-manager wrapper at
|
|
24622
|
+
* `<pinned>/bin/npm`, which also keeps the shell's active node out of it.
|
|
24548
24623
|
*/
|
|
24549
24624
|
function installGlobalPackage(target, version, run = defaultSpawnRunner) {
|
|
24550
|
-
const res = run(target.
|
|
24625
|
+
const res = run(target.npmInvocation.cmd, npmArgs(target, [
|
|
24551
24626
|
"install",
|
|
24552
24627
|
"-g",
|
|
24553
24628
|
`${target.owningPackage}@${version}`
|
|
24554
|
-
], { env: npmEnv(target) });
|
|
24629
|
+
]), { env: npmEnv(target) });
|
|
24555
24630
|
return {
|
|
24556
24631
|
ok: res.status === 0,
|
|
24557
24632
|
stderr: res.stderr
|
|
@@ -24886,15 +24961,14 @@ async function performSelfDrivingInstall(args) {
|
|
|
24886
24961
|
if (!verdict.reads) throw new Error(`refusing to install ${release} for "${config.name}": ${envFileRefusal(release, config.envFilePath)} The service and the installed package were not changed.`);
|
|
24887
24962
|
}
|
|
24888
24963
|
if (!opts.yes) {
|
|
24889
|
-
console.log(`This will install ${target.owningPackage}@${version}
|
|
24890
|
-
console.log(` (node runtime: ${target.nodeExecPath}), then restart "${config.name}".`);
|
|
24964
|
+
console.log(`This will install ${target.owningPackage}@${version} with ${target.nodeExecPath} into ${target.globalPrefix} (the unit's runtime), then restart "${config.name}".`);
|
|
24891
24965
|
console.log("");
|
|
24892
24966
|
if (!await confirm$1("Proceed with upgrade?")) {
|
|
24893
24967
|
console.log("Upgrade cancelled.");
|
|
24894
24968
|
return;
|
|
24895
24969
|
}
|
|
24896
24970
|
}
|
|
24897
|
-
console.log(`Installing ${target.owningPackage}@${version}
|
|
24971
|
+
console.log(`Installing ${target.owningPackage}@${version} with ${target.nodeExecPath} into ${target.globalPrefix} (the unit's runtime)...`);
|
|
24898
24972
|
const result = installGlobalPackage(target, version, args.run);
|
|
24899
24973
|
if (!result.ok) {
|
|
24900
24974
|
const hint = /EACCES|permission denied|EPERM/i.test(result.stderr) ? " The unit's global prefix is not writable — re-run with privileges matching the unit's runtime." : "";
|
|
@@ -26603,7 +26677,7 @@ init_manifest();
|
|
|
26603
26677
|
const ALL_PLATFORMS = AgentPlatform.options;
|
|
26604
26678
|
/** The orchestrator's own version (single-version invariant) = the packaged agent version. */
|
|
26605
26679
|
function resolveKiciVersion() {
|
|
26606
|
-
return "0.
|
|
26680
|
+
return "0.19.0";
|
|
26607
26681
|
}
|
|
26608
26682
|
/** Parse the --platform value: default set | single | CSV | `all`. */
|
|
26609
26683
|
function parsePlatforms(raw) {
|
|
@@ -35310,6 +35384,7 @@ function buildAgentContainerHostConfig(input) {
|
|
|
35310
35384
|
...typeof cpus === "number" && cpus > 0 ? { NanoCpus: Math.round(cpus * 1e9) } : {},
|
|
35311
35385
|
...input.binds && input.binds.length > 0 ? { Binds: input.binds } : {},
|
|
35312
35386
|
...input.extraHosts && input.extraHosts.length > 0 ? { ExtraHosts: input.extraHosts } : {},
|
|
35387
|
+
...input.supplementaryGroups && input.supplementaryGroups.length > 0 ? { GroupAdd: input.supplementaryGroups.map(String) } : {},
|
|
35313
35388
|
AutoRemove: false,
|
|
35314
35389
|
PidsLimit: AGENT_CONTAINER_PIDS_LIMIT
|
|
35315
35390
|
};
|
|
@@ -35374,6 +35449,19 @@ async function detectRuntime(runtimeHint) {
|
|
|
35374
35449
|
* Create a LogCapture from a running container's stdout/stderr streams.
|
|
35375
35450
|
* Uses dockerode's demuxStream to strip Docker's 8-byte multiplexed headers.
|
|
35376
35451
|
*/
|
|
35452
|
+
/**
|
|
35453
|
+
* The cpu / memory ceiling for one spawn. `effectiveLimits` from ScalerManager
|
|
35454
|
+
* wins: it already folds in job overrides, label-set and scaler defaults.
|
|
35455
|
+
* Otherwise the label set's (or the scaler's default) `resources.limits` apply.
|
|
35456
|
+
*/
|
|
35457
|
+
function resolveSpawnLimits(effectiveLimits, resources) {
|
|
35458
|
+
if (effectiveLimits && (effectiveLimits.cpus || effectiveLimits.memBytes)) return effectiveLimits;
|
|
35459
|
+
const limits = resources?.limits;
|
|
35460
|
+
return {
|
|
35461
|
+
...limits?.memory ? { memBytes: parseMemoryString(limits.memory) } : {},
|
|
35462
|
+
...limits?.cpus ? { cpus: limits.cpus } : {}
|
|
35463
|
+
};
|
|
35464
|
+
}
|
|
35377
35465
|
async function createContainerLogCapture(docker, containerId) {
|
|
35378
35466
|
const stream = await docker.getContainer(containerId).logs({
|
|
35379
35467
|
follow: true,
|
|
@@ -35661,15 +35749,7 @@ var ContainerScalerBackend = class ContainerScalerBackend {
|
|
|
35661
35749
|
];
|
|
35662
35750
|
const binds = [...matchedLabelSet.volumes ?? []];
|
|
35663
35751
|
if (matchedLabelSet.containerSocket && this.resolvedSocketPath) binds.push(`${this.resolvedSocketPath}:${this.resolvedSocketPath}`);
|
|
35664
|
-
|
|
35665
|
-
if (effectiveLimits && (effectiveLimits.cpus || effectiveLimits.memBytes)) resolvedLimits = effectiveLimits;
|
|
35666
|
-
else {
|
|
35667
|
-
const limits = (matchedLabelSet.resources ?? this.defaultResources)?.limits;
|
|
35668
|
-
resolvedLimits = {
|
|
35669
|
-
...limits?.memory ? { memBytes: parseMemoryString(limits.memory) } : {},
|
|
35670
|
-
...limits?.cpus ? { cpus: limits.cpus } : {}
|
|
35671
|
-
};
|
|
35672
|
-
}
|
|
35752
|
+
const resolvedLimits = resolveSpawnLimits(effectiveLimits, matchedLabelSet.resources ?? this.defaultResources);
|
|
35673
35753
|
const jobContainer = spawnContext?.container;
|
|
35674
35754
|
const spawnImage = jobContainer?.image ?? matchedLabelSet.image;
|
|
35675
35755
|
await pullImageIfMissing({
|
|
@@ -35692,6 +35772,7 @@ var ContainerScalerBackend = class ContainerScalerBackend {
|
|
|
35692
35772
|
binds.push(runtimeInjectBind(runtimeVolume));
|
|
35693
35773
|
agentInit = runtimeInitCommand(await agentImageLabels(this.docker, matchedLabelSet.image));
|
|
35694
35774
|
}
|
|
35775
|
+
if (!jobContainer) env.push("KICI_SCALER_SINGLE_JOB=1");
|
|
35695
35776
|
emit(ScalerEventType.enum["scaler.provisioning"], "creating container");
|
|
35696
35777
|
const normalizedLabelSetStr = normalizeLabelSet(labelSet);
|
|
35697
35778
|
const container = await this.docker.createContainer({
|
|
@@ -35710,7 +35791,8 @@ var ContainerScalerBackend = class ContainerScalerBackend {
|
|
|
35710
35791
|
HostConfig: buildAgentContainerHostConfig({
|
|
35711
35792
|
limits: resolvedLimits,
|
|
35712
35793
|
binds,
|
|
35713
|
-
...this.extraHosts ? { extraHosts: this.extraHosts } : {}
|
|
35794
|
+
...this.extraHosts ? { extraHosts: this.extraHosts } : {},
|
|
35795
|
+
...!jobContainer && { supplementaryGroups: [1990] }
|
|
35714
35796
|
}),
|
|
35715
35797
|
...this.networkIsolation && { NetworkingConfig: { EndpointsConfig: { ["kici-agent-net"]: {} } } }
|
|
35716
35798
|
});
|
|
@@ -36844,7 +36926,7 @@ function routeRootArgs(root, args) {
|
|
|
36844
36926
|
* build entry; the fallback covers a source run (tests, `tsx`), the same shape
|
|
36845
36927
|
* `server.ts` and `standalone.ts` use for `ORCHESTRATOR_VERSION`.
|
|
36846
36928
|
*/
|
|
36847
|
-
const CLI_VERSION = "0.
|
|
36929
|
+
const CLI_VERSION = "0.19.0";
|
|
36848
36930
|
/**
|
|
36849
36931
|
* Root command whose option parsing first routes argv through
|
|
36850
36932
|
* {@link routeRootArgs}: a leading `--version` prints the CLI version, and an
|
|
@@ -156,6 +156,8 @@ export declare class DashboardHandler {
|
|
|
156
156
|
private enforcePolicy;
|
|
157
157
|
/** dashboard.run.detail: the run's nested job/step tree. */
|
|
158
158
|
handleRunDetail(msg: DashboardRunDetailRequest): Promise<void>;
|
|
159
|
+
/** Answer a run-detail read for a run this orchestrator does not hold. */
|
|
160
|
+
private sendRunDetailNotFound;
|
|
159
161
|
/**
|
|
160
162
|
* dashboard.run.structured: the user-plane twin of the orchestrator-admin
|
|
161
163
|
* `/runs/:id/structured` endpoint, byte-identical because it reuses the
|
|
@@ -71,7 +71,7 @@ export interface WorkflowDispatchContext {
|
|
|
71
71
|
fullLockFile: {
|
|
72
72
|
workflows: readonly LockWorkflow[];
|
|
73
73
|
lockfileHash?: string;
|
|
74
|
-
/** In-repo
|
|
74
|
+
/** In-repo workspace sibling closure digest; part of the dep pointer key. */
|
|
75
75
|
siblingsDigest?: string;
|
|
76
76
|
source: {
|
|
77
77
|
file: string;
|
|
@@ -193,14 +193,6 @@ export declare function narrowVerdict(raw: unknown, workflowName: string): Globa
|
|
|
193
193
|
* best-effort post, so the check would vanish.
|
|
194
194
|
*/
|
|
195
195
|
export declare const MAX_ROUND_REASON_CHARS = 4000;
|
|
196
|
-
/**
|
|
197
|
-
* Truncate `text` to at most `max` UTF-16 code units, marking that it was cut.
|
|
198
|
-
*
|
|
199
|
-
* - A `max` no larger than the marker returns the truncated marker, never a
|
|
200
|
-
* string longer than `max`.
|
|
201
|
-
* - The cut backs off one unit rather than split a surrogate pair.
|
|
202
|
-
*/
|
|
203
|
-
export declare function truncateReasonText(text: string, max: number): string;
|
|
204
196
|
/**
|
|
205
197
|
* One round that produced no verdicts after every attempt. Exactly one per
|
|
206
198
|
* failed round, never one per candidate (the round exists to collapse N
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Bare-metal scaler backend: ephemeral agent processes via child_process.spawn.
|
|
3
3
|
* On Linux and macOS each agent leads its own process group, which teardown
|
|
4
4
|
* signals; on Windows teardown ends the agent's process tree with taskkill.
|
|
5
|
-
*
|
|
5
|
+
* An agent exits once it is idle past its idle window (a follow-up job that
|
|
6
|
+
* arrives within the window reuses it), and the backend destroys it on disconnect.
|
|
6
7
|
*/
|
|
7
8
|
import { type ToolRequirement } from '@kici-dev/shared';
|
|
8
9
|
import type { AgentTokenStore } from '../agent/token-store.js';
|
|
@@ -15,6 +15,13 @@ import type { EffectiveLimits } from './types.js';
|
|
|
15
15
|
* its own docs and validation, and nothing yet says the constant is wrong.
|
|
16
16
|
*/
|
|
17
17
|
export declare const AGENT_CONTAINER_PIDS_LIMIT = 4096;
|
|
18
|
+
/**
|
|
19
|
+
* Group id of `kici-sudo` in the KiCI agent image (`packages/agent/Dockerfile`
|
|
20
|
+
* creates it). `/etc/sudoers.d/kici-sudo` grants that group passwordless sudo.
|
|
21
|
+
* The container scaler adds the group only to the single-job containers it
|
|
22
|
+
* starts from a pool's own image, so root never outlives one job.
|
|
23
|
+
*/
|
|
24
|
+
export declare const AGENT_SUDO_GROUP_GID = 1990;
|
|
18
25
|
/**
|
|
19
26
|
* Why this container is NOT capability-dropped, though the job containers the
|
|
20
27
|
* agent starts are.
|
|
@@ -42,6 +49,8 @@ export interface AgentContainerHostConfigInput {
|
|
|
42
49
|
binds?: string[];
|
|
43
50
|
/** Extra `host:ip` entries (e.g. a private registry alias). */
|
|
44
51
|
extraHosts?: string[];
|
|
52
|
+
/** Extra group ids for the container's process (`GroupAdd`). */
|
|
53
|
+
supplementaryGroups?: number[];
|
|
45
54
|
}
|
|
46
55
|
/**
|
|
47
56
|
* Build the agent container's `HostConfig`.
|
|
@@ -19,7 +19,10 @@ export interface WarmPoolCallbacks {
|
|
|
19
19
|
countAvailable: (labels: string[]) => number;
|
|
20
20
|
/**
|
|
21
21
|
* Agents this backend spawned that match the label set and have never run a
|
|
22
|
-
* job. `registeredAt`
|
|
22
|
+
* job. `registeredAt` stands in for "idle since". That is exact for a
|
|
23
|
+
* container-backend agent, which exits after its one job. An agent of another
|
|
24
|
+
* backend can finish a job and wait out its idle window before it exits, so it
|
|
25
|
+
* can read as idle for longer than it has been.
|
|
23
26
|
*
|
|
24
27
|
* Scoped to the backend because the reaper destroys what it is given; a
|
|
25
28
|
* matching static agent is ready capacity, not this pool's to tear down.
|