@blogic-cz/agent-tools 0.14.61 → 0.15.1

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.
@@ -0,0 +1,157 @@
1
+ import type { SanitizedVpnDriver } from "#shared/prerequisites/store";
2
+ import { VpnStore } from "#shared/prerequisites/store";
3
+ import { parseVpnStatus, vpnCommandSpec } from "#shared/prerequisites/driver-commands";
4
+ import type { VpnCleanupPolicy } from "#shared/prerequisites/types";
5
+
6
+ export type GuardianInitMessage = {
7
+ readonly type: "INIT";
8
+ readonly driver: SanitizedVpnDriver;
9
+ readonly runtimeRoot: string;
10
+ readonly leaseId: string;
11
+ readonly guardianId: string;
12
+ readonly ownerPid: number;
13
+ readonly cleanup: VpnCleanupPolicy;
14
+ readonly idleDisconnectMs: number;
15
+ readonly disconnectTimeoutMs: number;
16
+ };
17
+ export type GuardianReleaseMessage = { readonly type: "RELEASE"; readonly leaseId: string };
18
+ export type GuardianInboundMessage = GuardianInitMessage | GuardianReleaseMessage;
19
+ export type GuardianOutboundMessage =
20
+ | { readonly type: "READY"; readonly leaseId: string; readonly guardianId: string }
21
+ | { readonly type: "RELEASED"; readonly leaseId: string }
22
+ | { readonly type: "ERROR"; readonly message: string };
23
+
24
+ export type GuardianCommandRunner = (
25
+ action: "status" | "stop",
26
+ timeoutMs: number,
27
+ ) => Promise<{ readonly stdout: string; readonly stderr: string; readonly exitCode: number }>;
28
+
29
+ const safeEnvironment = (): Record<string, string> =>
30
+ Object.fromEntries(
31
+ ["PATH", "HOME", "TMPDIR", "TEMP", "TMP", "SYSTEMROOT", "WINDIR"].flatMap((name) => {
32
+ const value = process.env[name];
33
+ return value === undefined ? [] : [[name, value]];
34
+ }),
35
+ );
36
+
37
+ export const makeGuardianCommandRunner =
38
+ (driver: SanitizedVpnDriver): GuardianCommandRunner =>
39
+ async (action, timeoutMs) => {
40
+ const spec = vpnCommandSpec(driver, action);
41
+ const child = Bun.spawn([spec.executable, ...spec.args], {
42
+ env: safeEnvironment(),
43
+ stdin: "ignore",
44
+ stdout: "pipe",
45
+ stderr: "pipe",
46
+ timeout: timeoutMs,
47
+ killSignal: "SIGKILL",
48
+ });
49
+ const [exitCode, stdout, stderr] = await Promise.all([
50
+ child.exited,
51
+ new Response(child.stdout).text(),
52
+ new Response(child.stderr).text(),
53
+ ]);
54
+ return { stdout, stderr, exitCode };
55
+ };
56
+
57
+ const sleep = (milliseconds: number) =>
58
+ new Promise<void>((resolve) => {
59
+ setTimeout(resolve, milliseconds);
60
+ });
61
+
62
+ export async function stopWhenIdle(
63
+ store: VpnStore,
64
+ init: GuardianInitMessage,
65
+ runCommand: GuardianCommandRunner,
66
+ now: () => number = Date.now,
67
+ ): Promise<void> {
68
+ const snapshot = store.snapshot();
69
+ if (snapshot.lifecycle !== "IDLE" || snapshot.idleDeadline === null) return;
70
+ const delay = snapshot.idleDeadline - now();
71
+ if (delay > 0) await sleep(delay);
72
+
73
+ const operationId = crypto.randomUUID();
74
+ const token = crypto.randomUUID();
75
+ const guard = store.claimStop(operationId, token, process.pid, now());
76
+ if (!guard) return;
77
+
78
+ const deadline = now() + init.disconnectTimeoutMs;
79
+ let evidence = "VPN stop did not produce confirmed disconnected status.";
80
+ try {
81
+ let remaining = deadline - now();
82
+ if (remaining > 0) {
83
+ const stop = await runCommand("stop", remaining);
84
+ remaining = deadline - now();
85
+ if (stop.exitCode !== 0) {
86
+ evidence = "VPN stop command failed; ownership is unknown and stop will not be retried.";
87
+ } else {
88
+ const confirmDisconnected = async (): Promise<boolean> => {
89
+ const statusRemaining = deadline - now();
90
+ if (statusRemaining <= 0) return false;
91
+ const status = await runCommand("status", statusRemaining);
92
+ remaining = deadline - now();
93
+ const connected = parseVpnStatus(init.driver, status);
94
+ if (connected === false) return true;
95
+ if (connected === undefined || remaining <= 0) return false;
96
+ const sleepRemaining = deadline - now();
97
+ if (sleepRemaining <= 0) return false;
98
+ await sleep(Math.min(250, sleepRemaining));
99
+ remaining = deadline - now();
100
+ return remaining > 0 && confirmDisconnected();
101
+ };
102
+ if (await confirmDisconnected()) {
103
+ store.commitStop(guard, true, "VPN stop confirmed disconnected.", now());
104
+ return;
105
+ }
106
+ evidence = "VPN status failed, stayed connected, or was unparseable after stop.";
107
+ }
108
+ } else {
109
+ evidence = "VPN disconnect deadline expired before a command could safely start.";
110
+ }
111
+ } catch {
112
+ evidence = "VPN stop or confirmation timed out or failed; ownership is unknown.";
113
+ }
114
+ store.commitStop(guard, false, evidence, now());
115
+ }
116
+
117
+ export function runGuardian(
118
+ init: GuardianInitMessage,
119
+ send: (message: GuardianOutboundMessage) => void,
120
+ runCommand: GuardianCommandRunner = makeGuardianCommandRunner(init.driver),
121
+ ): Promise<{ release: () => Promise<void> }> {
122
+ return new Promise((resolve) => {
123
+ const store = VpnStore.open(init.driver, { root: init.runtimeRoot });
124
+ let released = false;
125
+ store.reserveLease({
126
+ leaseId: init.leaseId,
127
+ guardianId: init.guardianId,
128
+ ownerPid: init.ownerPid,
129
+ cleanup: init.cleanup,
130
+ now: Date.now(),
131
+ });
132
+
133
+ const release = async () => {
134
+ if (released) return;
135
+ released = true;
136
+ try {
137
+ const result = store.releaseLease({
138
+ leaseId: init.leaseId,
139
+ guardianId: init.guardianId,
140
+ idleDisconnectMs: init.idleDisconnectMs,
141
+ now: Date.now(),
142
+ });
143
+ const stop =
144
+ result.released && result.deadline !== null
145
+ ? stopWhenIdle(store, init, runCommand)
146
+ : undefined;
147
+ if (stop && init.idleDisconnectMs === 0) await stop;
148
+ send({ type: "RELEASED", leaseId: init.leaseId });
149
+ if (stop && init.idleDisconnectMs > 0) await stop;
150
+ } finally {
151
+ store.close();
152
+ }
153
+ };
154
+
155
+ resolve({ release });
156
+ });
157
+ }