@forgezero/agent 0.1.20 → 0.1.22
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/README.md +6 -0
- package/dist/definition.d.ts +0 -1
- package/dist/definition.js +1 -6
- package/dist/deployment.d.ts +1 -0
- package/dist/fz-agent.js +51 -26
- package/dist/fz.js +1 -1
- package/dist/shutdown.d.ts +10 -0
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -144,6 +144,12 @@ read Vault data, accept arbitrary commands, or retain tenant credentials. Root
|
|
|
144
144
|
SSH is an operator-only platform-genesis/recovery path, not the normal tenant
|
|
145
145
|
dispatch mechanism.
|
|
146
146
|
|
|
147
|
+
Shutdown has one process-wide deadline. New deployment claims and background
|
|
148
|
+
sync stop first, claimed work drains under its lease fence, and only then do the
|
|
149
|
+
queues and application socket close. Vault/attestation calls or a stale local
|
|
150
|
+
socket cannot consume systemd's longer stop timeout: exceeding the Agent deadline
|
|
151
|
+
is reported and exits non-zero instead of being silently killed midway by PID 1.
|
|
152
|
+
|
|
147
153
|
Repository read authorization is explicit per pipeline: public HTTPS, the
|
|
148
154
|
compute's systemd-sealed SSH deploy key, or a fine-grained HTTPS token selected
|
|
149
155
|
by a project-vault secret name. A signed claim contains the mode and secret name
|
package/dist/definition.d.ts
CHANGED
package/dist/definition.js
CHANGED
|
@@ -50,17 +50,12 @@ function parseDeployDefinition(value) {
|
|
|
50
50
|
}
|
|
51
51
|
const roles = root.roles.map((raw, index) => {
|
|
52
52
|
const role = record(raw, `roles[${index}]`);
|
|
53
|
-
exactKeys(role, ["name", "
|
|
54
|
-
const count = Number(role.count);
|
|
55
|
-
if (!Number.isSafeInteger(count) || count < 1) {
|
|
56
|
-
throw new DefinitionError(`roles[${index}].count must be a positive integer.`);
|
|
57
|
-
}
|
|
53
|
+
exactKeys(role, ["name", "software"], `roles[${index}]`);
|
|
58
54
|
if (!Array.isArray(role.software)) {
|
|
59
55
|
throw new DefinitionError(`roles[${index}].software must be an array.`);
|
|
60
56
|
}
|
|
61
57
|
return {
|
|
62
58
|
name: text(role.name, `roles[${index}].name`),
|
|
63
|
-
count,
|
|
64
59
|
software: role.software.map((rawSoftware, softwareIndex) => {
|
|
65
60
|
const software = record(rawSoftware, `roles[${index}].software[${softwareIndex}]`);
|
|
66
61
|
exactKeys(software, ["name", "check", "install"], `roles[${index}].software[${softwareIndex}]`);
|
package/dist/deployment.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export declare function createDeploymentManager(options: DeploymentOptions): {
|
|
|
79
79
|
latestRevision(): Promise<string>;
|
|
80
80
|
deploy(request?: DeploymentRequest): QueueTask<DeploymentResult>;
|
|
81
81
|
snapshot: () => {
|
|
82
|
+
width: number;
|
|
82
83
|
running: number;
|
|
83
84
|
queued: number;
|
|
84
85
|
keys: number;
|
package/dist/fz-agent.js
CHANGED
|
@@ -473,17 +473,12 @@ function parseDeployDefinition(value) {
|
|
|
473
473
|
}
|
|
474
474
|
const roles = root.roles.map((raw, index) => {
|
|
475
475
|
const role = record(raw, `roles[${index}]`);
|
|
476
|
-
exactKeys(role, ["name", "
|
|
477
|
-
const count = Number(role.count);
|
|
478
|
-
if (!Number.isSafeInteger(count) || count < 1) {
|
|
479
|
-
throw new DefinitionError(`roles[${index}].count must be a positive integer.`);
|
|
480
|
-
}
|
|
476
|
+
exactKeys(role, ["name", "software"], `roles[${index}]`);
|
|
481
477
|
if (!Array.isArray(role.software)) {
|
|
482
478
|
throw new DefinitionError(`roles[${index}].software must be an array.`);
|
|
483
479
|
}
|
|
484
480
|
return {
|
|
485
481
|
name: text(role.name, `roles[${index}].name`),
|
|
486
|
-
count,
|
|
487
482
|
software: role.software.map((rawSoftware, softwareIndex) => {
|
|
488
483
|
const software = record(rawSoftware, `roles[${index}].software[${softwareIndex}]`);
|
|
489
484
|
exactKeys(software, ["name", "check", "install"], `roles[${index}].software[${softwareIndex}]`);
|
|
@@ -2714,8 +2709,31 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
|
|
|
2714
2709
|
}
|
|
2715
2710
|
}
|
|
2716
2711
|
|
|
2712
|
+
// src/shutdown.ts
|
|
2713
|
+
async function settleWithin(work, timeoutMs, setTimer = setTimeout, clearTimer = clearTimeout) {
|
|
2714
|
+
let timer;
|
|
2715
|
+
try {
|
|
2716
|
+
return await Promise.race([
|
|
2717
|
+
work.then(() => true),
|
|
2718
|
+
new Promise((resolve2) => {
|
|
2719
|
+
timer = setTimer(() => resolve2(false), Math.max(1, timeoutMs));
|
|
2720
|
+
timer.unref?.();
|
|
2721
|
+
})
|
|
2722
|
+
]);
|
|
2723
|
+
} finally {
|
|
2724
|
+
if (timer)
|
|
2725
|
+
clearTimer(timer);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async function closeServerWithin(server, timeoutMs) {
|
|
2729
|
+
const closed = await settleWithin(new Promise((resolve2) => server.close(() => resolve2())), timeoutMs);
|
|
2730
|
+
if (!closed)
|
|
2731
|
+
server.unref();
|
|
2732
|
+
return closed;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2717
2735
|
// src/version.ts
|
|
2718
|
-
var VERSION = "0.1.
|
|
2736
|
+
var VERSION = "0.1.22";
|
|
2719
2737
|
|
|
2720
2738
|
// src/index.ts
|
|
2721
2739
|
function loadOrCreateSeed(path) {
|
|
@@ -3150,24 +3168,26 @@ if (import.meta.main) {
|
|
|
3150
3168
|
return;
|
|
3151
3169
|
stopping = true;
|
|
3152
3170
|
console.log(`[agent] ${signal}: stopping deployment intake and draining`);
|
|
3153
|
-
if (control)
|
|
3154
|
-
await new Promise((resolve2) => control.close(() => resolve2()));
|
|
3155
3171
|
const deadlineMs = Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000));
|
|
3156
3172
|
const deadline = Date.now() + deadlineMs;
|
|
3173
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
3174
|
+
const controlClosed = control ? await settleWithin(new Promise((resolve2) => control.close(() => resolve2())), remaining()) : true;
|
|
3157
3175
|
const pullDrain = pull?.stop() ?? Promise.resolve();
|
|
3158
3176
|
const vaultDrain = vaultSync?.stop() ?? Promise.resolve();
|
|
3159
3177
|
const attestationDrain = attestationLoop?.stop() ?? Promise.resolve();
|
|
3160
|
-
const
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
const pullDrained = await pullWithinDeadline;
|
|
3165
|
-
await Promise.all([vaultDrain, attestationDrain]);
|
|
3166
|
-
const managerReports = await Promise.all([...new Set(managers.values())].map((manager) => manager.stop(Math.max(1, deadline - Date.now()))));
|
|
3167
|
-
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
3178
|
+
const pullDrained = pull ? await settleWithin(pullDrain, remaining()) : true;
|
|
3179
|
+
const backgroundDrained = await settleWithin(Promise.all([vaultDrain, attestationDrain]), remaining());
|
|
3180
|
+
const managerReports = await Promise.all([...new Set(managers.values())].map((manager) => manager.stop(remaining())));
|
|
3181
|
+
const socketClosed = await closeServerWithin(server, remaining());
|
|
3168
3182
|
const timedOut = managerReports.some((report) => report.timedOut);
|
|
3169
|
-
console.log(`[agent] drain ${JSON.stringify({
|
|
3170
|
-
|
|
3183
|
+
console.log(`[agent] drain ${JSON.stringify({
|
|
3184
|
+
managers: managerReports,
|
|
3185
|
+
controlClosed,
|
|
3186
|
+
pullDrained,
|
|
3187
|
+
backgroundDrained,
|
|
3188
|
+
socketClosed
|
|
3189
|
+
})}`);
|
|
3190
|
+
process.exit(timedOut || !controlClosed || !pullDrained || !backgroundDrained || !socketClosed ? 1 : 0);
|
|
3171
3191
|
};
|
|
3172
3192
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
3173
3193
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
@@ -3175,19 +3195,24 @@ if (import.meta.main) {
|
|
|
3175
3195
|
console.log("[agent] signing/vault mode only; deployment root and pull are not configured");
|
|
3176
3196
|
if (vaultSync || attestationLoop) {
|
|
3177
3197
|
let stopping = false;
|
|
3178
|
-
const stop = async () => {
|
|
3198
|
+
const stop = async (signal) => {
|
|
3179
3199
|
if (stopping)
|
|
3180
3200
|
return;
|
|
3181
3201
|
stopping = true;
|
|
3182
|
-
|
|
3202
|
+
const deadlineMs = Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000));
|
|
3203
|
+
const deadline = Date.now() + deadlineMs;
|
|
3204
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
3205
|
+
console.log(`[agent] ${signal}: stopping background intake and draining`);
|
|
3206
|
+
const backgroundDrained = await settleWithin(Promise.all([
|
|
3183
3207
|
vaultSync?.stop() ?? Promise.resolve(),
|
|
3184
3208
|
attestationLoop?.stop() ?? Promise.resolve()
|
|
3185
|
-
]);
|
|
3186
|
-
|
|
3187
|
-
|
|
3209
|
+
]), remaining());
|
|
3210
|
+
const socketClosed = await closeServerWithin(server, remaining());
|
|
3211
|
+
console.log(`[agent] drain ${JSON.stringify({ backgroundDrained, socketClosed })}`);
|
|
3212
|
+
process.exit(backgroundDrained && socketClosed ? 0 : 1);
|
|
3188
3213
|
};
|
|
3189
|
-
process.on("SIGTERM", () => void stop());
|
|
3190
|
-
process.on("SIGINT", () => void stop());
|
|
3214
|
+
process.on("SIGTERM", () => void stop("SIGTERM"));
|
|
3215
|
+
process.on("SIGINT", () => void stop("SIGINT"));
|
|
3191
3216
|
}
|
|
3192
3217
|
}
|
|
3193
3218
|
}
|
package/dist/fz.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Server } from 'node:net';
|
|
2
|
+
/**
|
|
3
|
+
* Wait for one shutdown stage without allowing it to consume systemd's entire
|
|
4
|
+
* stop timeout. The work is not cancelled: deployment/lease code retains its
|
|
5
|
+
* own fencing rules, while the caller decides whether an expired global drain
|
|
6
|
+
* deadline must terminate the process non-zero.
|
|
7
|
+
*/
|
|
8
|
+
export declare function settleWithin(work: Promise<unknown>, timeoutMs: number, setTimer?: typeof setTimeout, clearTimer?: typeof clearTimeout): Promise<boolean>;
|
|
9
|
+
/** Stop accepting local Vault clients, but never let one stale socket defeat shutdown. */
|
|
10
|
+
export declare function closeServerWithin(server: Server, timeoutMs: number): Promise<boolean>;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.22";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
|
|
3
3
|
"name": "@forgezero/agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.22",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"check": "tsc --noEmit",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@forgezero/access": "^0.1.0",
|
|
21
|
-
"@forgezero/runtime": "^0.1.
|
|
21
|
+
"@forgezero/runtime": "^0.1.4",
|
|
22
22
|
"@forgezero/vault": "^0.1.5",
|
|
23
23
|
"@noble/curves": "^2.2.0",
|
|
24
24
|
"@noble/hashes": "^2.2.0",
|