@forgezero/agent 0.1.20 → 0.1.21

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 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/fz-agent.js CHANGED
@@ -2714,8 +2714,31 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
2714
2714
  }
2715
2715
  }
2716
2716
 
2717
+ // src/shutdown.ts
2718
+ async function settleWithin(work, timeoutMs, setTimer = setTimeout, clearTimer = clearTimeout) {
2719
+ let timer;
2720
+ try {
2721
+ return await Promise.race([
2722
+ work.then(() => true),
2723
+ new Promise((resolve2) => {
2724
+ timer = setTimer(() => resolve2(false), Math.max(1, timeoutMs));
2725
+ timer.unref?.();
2726
+ })
2727
+ ]);
2728
+ } finally {
2729
+ if (timer)
2730
+ clearTimer(timer);
2731
+ }
2732
+ }
2733
+ async function closeServerWithin(server, timeoutMs) {
2734
+ const closed = await settleWithin(new Promise((resolve2) => server.close(() => resolve2())), timeoutMs);
2735
+ if (!closed)
2736
+ server.unref();
2737
+ return closed;
2738
+ }
2739
+
2717
2740
  // src/version.ts
2718
- var VERSION = "0.1.20";
2741
+ var VERSION = "0.1.21";
2719
2742
 
2720
2743
  // src/index.ts
2721
2744
  function loadOrCreateSeed(path) {
@@ -3150,24 +3173,26 @@ if (import.meta.main) {
3150
3173
  return;
3151
3174
  stopping = true;
3152
3175
  console.log(`[agent] ${signal}: stopping deployment intake and draining`);
3153
- if (control)
3154
- await new Promise((resolve2) => control.close(() => resolve2()));
3155
3176
  const deadlineMs = Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000));
3156
3177
  const deadline = Date.now() + deadlineMs;
3178
+ const remaining = () => Math.max(1, deadline - Date.now());
3179
+ const controlClosed = control ? await settleWithin(new Promise((resolve2) => control.close(() => resolve2())), remaining()) : true;
3157
3180
  const pullDrain = pull?.stop() ?? Promise.resolve();
3158
3181
  const vaultDrain = vaultSync?.stop() ?? Promise.resolve();
3159
3182
  const attestationDrain = attestationLoop?.stop() ?? Promise.resolve();
3160
- const pullWithinDeadline = pull ? Promise.race([
3161
- pullDrain.then(() => true),
3162
- new Promise((resolve2) => setTimeout(() => resolve2(false), deadlineMs))
3163
- ]) : Promise.resolve(true);
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()));
3183
+ const pullDrained = pull ? await settleWithin(pullDrain, remaining()) : true;
3184
+ const backgroundDrained = await settleWithin(Promise.all([vaultDrain, attestationDrain]), remaining());
3185
+ const managerReports = await Promise.all([...new Set(managers.values())].map((manager) => manager.stop(remaining())));
3186
+ const socketClosed = await closeServerWithin(server, remaining());
3168
3187
  const timedOut = managerReports.some((report) => report.timedOut);
3169
- console.log(`[agent] drain ${JSON.stringify({ managers: managerReports, pullDrained })}`);
3170
- process.exit(timedOut || !pullDrained ? 1 : 0);
3188
+ console.log(`[agent] drain ${JSON.stringify({
3189
+ managers: managerReports,
3190
+ controlClosed,
3191
+ pullDrained,
3192
+ backgroundDrained,
3193
+ socketClosed
3194
+ })}`);
3195
+ process.exit(timedOut || !controlClosed || !pullDrained || !backgroundDrained || !socketClosed ? 1 : 0);
3171
3196
  };
3172
3197
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
3173
3198
  process.on("SIGINT", () => void shutdown("SIGINT"));
@@ -3175,19 +3200,24 @@ if (import.meta.main) {
3175
3200
  console.log("[agent] signing/vault mode only; deployment root and pull are not configured");
3176
3201
  if (vaultSync || attestationLoop) {
3177
3202
  let stopping = false;
3178
- const stop = async () => {
3203
+ const stop = async (signal) => {
3179
3204
  if (stopping)
3180
3205
  return;
3181
3206
  stopping = true;
3182
- await Promise.all([
3207
+ const deadlineMs = Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000));
3208
+ const deadline = Date.now() + deadlineMs;
3209
+ const remaining = () => Math.max(1, deadline - Date.now());
3210
+ console.log(`[agent] ${signal}: stopping background intake and draining`);
3211
+ const backgroundDrained = await settleWithin(Promise.all([
3183
3212
  vaultSync?.stop() ?? Promise.resolve(),
3184
3213
  attestationLoop?.stop() ?? Promise.resolve()
3185
- ]);
3186
- await new Promise((resolve2) => server.close(() => resolve2()));
3187
- process.exit(0);
3214
+ ]), remaining());
3215
+ const socketClosed = await closeServerWithin(server, remaining());
3216
+ console.log(`[agent] drain ${JSON.stringify({ backgroundDrained, socketClosed })}`);
3217
+ process.exit(backgroundDrained && socketClosed ? 0 : 1);
3188
3218
  };
3189
- process.on("SIGTERM", () => void stop());
3190
- process.on("SIGINT", () => void stop());
3219
+ process.on("SIGTERM", () => void stop("SIGTERM"));
3220
+ process.on("SIGINT", () => void stop("SIGINT"));
3191
3221
  }
3192
3222
  }
3193
3223
  }
package/dist/fz.js CHANGED
@@ -774,7 +774,7 @@ async function resolveIdentity(selector, socketPath) {
774
774
  }
775
775
 
776
776
  // src/version.ts
777
- var VERSION = "0.1.20";
777
+ var VERSION = "0.1.21";
778
778
 
779
779
  // src/cli/index.ts
780
780
  var DEFAULT_MODE = THRESHOLD_MODES[0].id;
@@ -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.20";
2
+ export declare const VERSION = "0.1.21";
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.20",
4
+ "version": "0.1.21",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "check": "tsc --noEmit",