@blogic-cz/agent-tools 0.14.5 → 0.14.8
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 +2 -0
- package/package.json +1 -1
- package/schemas/agent-tools.schema.json +8 -0
- package/src/config/loader.ts +2 -0
- package/src/config/types.ts +4 -0
- package/src/shared/prerequisites/runtime.ts +103 -63
- package/src/shared/prerequisites/types.ts +3 -3
- package/src/shared/prerequisites/vpn.ts +15 -2
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ bun run agent-tools/example-tool/index.ts ping
|
|
|
178
178
|
// auto defaults to true:
|
|
179
179
|
// darwin -> macos-scutil, linux -> linux-nmcli, win32 -> windows-rasdial
|
|
180
180
|
name: "ExampleVPN",
|
|
181
|
+
// Optional: pass IPSec shared secret to macOS scutil from env without storing the value in config.
|
|
182
|
+
secretEnvVar: "EXAMPLE_VPN_IPSEC_SHARED_SECRET",
|
|
181
183
|
},
|
|
182
184
|
},
|
|
183
185
|
kubernetes: {
|
package/package.json
CHANGED
|
@@ -428,6 +428,10 @@
|
|
|
428
428
|
},
|
|
429
429
|
"serviceName": {
|
|
430
430
|
"type": "string"
|
|
431
|
+
},
|
|
432
|
+
"secretEnvVar": {
|
|
433
|
+
"type": "string",
|
|
434
|
+
"description": "Name of environment variable holding the IPSec shared secret for scutil --nc start."
|
|
431
435
|
}
|
|
432
436
|
},
|
|
433
437
|
"required": ["type"]
|
|
@@ -502,6 +506,10 @@
|
|
|
502
506
|
"leaseTtlMs": {
|
|
503
507
|
"type": "number"
|
|
504
508
|
},
|
|
509
|
+
"secretEnvVar": {
|
|
510
|
+
"type": "string",
|
|
511
|
+
"description": "Name of environment variable holding the VPN shared secret for supported drivers."
|
|
512
|
+
},
|
|
505
513
|
"drivers": {
|
|
506
514
|
"type": "object",
|
|
507
515
|
"additionalProperties": false,
|
package/src/config/loader.ts
CHANGED
|
@@ -30,6 +30,7 @@ const PrerequisitesSchema = Schema.Array(PrerequisiteSchema);
|
|
|
30
30
|
const MacosScutilVpnDriverConfigSchema = Schema.Struct({
|
|
31
31
|
type: Schema.Literal("macos-scutil"),
|
|
32
32
|
serviceName: Schema.optionalKey(Schema.String),
|
|
33
|
+
secretEnvVar: Schema.optionalKey(Schema.String),
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
const LinuxNmcliVpnDriverConfigSchema = Schema.Struct({
|
|
@@ -56,6 +57,7 @@ const VpnConfigSchema = Schema.Struct({
|
|
|
56
57
|
disconnectTimeoutMs: Schema.optionalKey(Schema.Number),
|
|
57
58
|
cooldownMs: Schema.optionalKey(Schema.Number),
|
|
58
59
|
leaseTtlMs: Schema.optionalKey(Schema.Number),
|
|
60
|
+
secretEnvVar: Schema.optionalKey(Schema.String),
|
|
59
61
|
drivers: Schema.optionalKey(
|
|
60
62
|
Schema.Struct({
|
|
61
63
|
darwin: Schema.optionalKey(MacosScutilVpnDriverConfigSchema),
|
package/src/config/types.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type VpnPrerequisite = {
|
|
|
16
16
|
export type MacosScutilVpnDriverConfig = {
|
|
17
17
|
type: "macos-scutil";
|
|
18
18
|
serviceName?: string;
|
|
19
|
+
/** Name of environment variable holding the IPSec shared secret for scutil --nc start. */
|
|
20
|
+
secretEnvVar?: string;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export type LinuxNmcliVpnDriverConfig = {
|
|
@@ -42,6 +44,8 @@ export type VpnConfig = {
|
|
|
42
44
|
disconnectTimeoutMs?: number;
|
|
43
45
|
cooldownMs?: number;
|
|
44
46
|
leaseTtlMs?: number;
|
|
47
|
+
/** Name of environment variable holding the VPN shared secret for supported drivers. */
|
|
48
|
+
secretEnvVar?: string;
|
|
45
49
|
drivers?: {
|
|
46
50
|
darwin?: MacosScutilVpnDriverConfig;
|
|
47
51
|
linux?: LinuxNmcliVpnDriverConfig;
|
|
@@ -8,18 +8,25 @@ import { normalizeProfilePrerequisites } from "#shared/prerequisites/config";
|
|
|
8
8
|
import { PrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
9
9
|
import { missingVpnToolHint, resolveVpnDriverConfig } from "#shared/prerequisites/vpn";
|
|
10
10
|
|
|
11
|
+
const readEnv = (name: string) => Bun.env[name];
|
|
12
|
+
|
|
11
13
|
const makeVpnCommand = (driver: ResolvedVpnDriver, action: "status" | "start" | "stop") => {
|
|
12
14
|
if (driver.type === "macos-scutil") {
|
|
15
|
+
const secret = driver.secretEnvVar ? readEnv(driver.secretEnvVar) : undefined;
|
|
16
|
+
const secretArgs = action === "start" && secret ? ["--secret", secret] : [];
|
|
17
|
+
const redactedSecretArgs = secretArgs.length > 0 ? ["--secret", "<redacted>"] : [];
|
|
13
18
|
const args =
|
|
14
19
|
action === "status"
|
|
15
20
|
? ["--nc", "status", driver.serviceName]
|
|
16
21
|
: action === "start"
|
|
17
|
-
? ["--nc", "start", driver.serviceName]
|
|
22
|
+
? ["--nc", "start", driver.serviceName, ...secretArgs]
|
|
18
23
|
: ["--nc", "stop", driver.serviceName];
|
|
24
|
+
const labelArgs =
|
|
25
|
+
action === "start" ? ["--nc", "start", driver.serviceName, ...redactedSecretArgs] : args;
|
|
19
26
|
|
|
20
27
|
return {
|
|
21
28
|
command: ChildProcess.make("scutil", args, { stdout: "pipe", stderr: "pipe" }),
|
|
22
|
-
label: ["scutil", ...
|
|
29
|
+
label: ["scutil", ...labelArgs].join(" "),
|
|
23
30
|
};
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -126,83 +133,116 @@ export const runWithProfilePrerequisites = <A, E, CommandError>(
|
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
return Effect.gen(function* () {
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
const shouldTryDirect = options?.tryWithoutPrerequisites === true;
|
|
137
|
+
|
|
138
|
+
const tryDirect = () => effect.pipe(Effect.result);
|
|
139
|
+
|
|
140
|
+
if (shouldTryDirect) {
|
|
141
|
+
const directResult = yield* tryDirect();
|
|
131
142
|
if (Result.isSuccess(directResult)) {
|
|
132
143
|
return directResult.success;
|
|
133
144
|
}
|
|
134
145
|
}
|
|
135
146
|
|
|
136
|
-
const
|
|
147
|
+
const prerequisiteResult = yield* Effect.gen(function* () {
|
|
148
|
+
const startedDrivers: Array<{ driver: ResolvedVpnDriver; cooldownMs: number }> = [];
|
|
137
149
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
for (const prerequisite of vpnPrerequisites) {
|
|
151
|
+
const vpnConfig = config.vpns?.[prerequisite.key];
|
|
152
|
+
if (!vpnConfig) {
|
|
153
|
+
return yield* new PrerequisiteRunError({
|
|
154
|
+
message: `VPN prerequisite "${prerequisite.key}" is not defined.`,
|
|
155
|
+
hint: `Add vpns.${prerequisite.key} to agent-tools.json5 or remove the prerequisite.`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
146
158
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
const driverResolution = resolveVpnDriverConfig(vpnConfig);
|
|
160
|
+
if (!driverResolution.success) {
|
|
161
|
+
return yield* new PrerequisiteRunError({
|
|
162
|
+
message: driverResolution.error,
|
|
163
|
+
hint: driverResolution.hint,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
154
166
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
const driver = driverResolution.driver;
|
|
168
|
+
const wasConnected = yield* isVpnConnected(driver, runCommand);
|
|
169
|
+
if (wasConnected) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (startResult.exitCode !== 0) {
|
|
173
|
-
const stderr = startResult.stderr.trim();
|
|
174
|
-
return yield* new PrerequisiteRunError({
|
|
175
|
-
message:
|
|
176
|
-
stderr !== "" ? stderr : `Failed to start VPN prerequisite "${prerequisite.key}".`,
|
|
177
|
-
hint: missingVpnToolHint(driver),
|
|
178
|
-
});
|
|
179
|
-
}
|
|
173
|
+
if (
|
|
174
|
+
driver.type === "macos-scutil" &&
|
|
175
|
+
driver.secretEnvVar &&
|
|
176
|
+
!readEnv(driver.secretEnvVar)
|
|
177
|
+
) {
|
|
178
|
+
return yield* new PrerequisiteRunError({
|
|
179
|
+
message: `VPN secret environment variable "${driver.secretEnvVar}" is not set.`,
|
|
180
|
+
hint: `Set ${driver.secretEnvVar} before running this tool or remove secretEnvVar from the VPN config.`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
const startCommand = makeVpnCommand(driver, "start");
|
|
185
|
+
const startResult = yield* runCommand(startCommand.command, startCommand.label).pipe(
|
|
186
|
+
Effect.mapError(
|
|
187
|
+
() =>
|
|
188
|
+
new PrerequisiteRunError({
|
|
189
|
+
message: `Failed to start VPN prerequisite "${prerequisite.key}".`,
|
|
190
|
+
hint: missingVpnToolHint(driver),
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (startResult.exitCode !== 0) {
|
|
196
|
+
const stderr = startResult.stderr.trim();
|
|
197
|
+
return yield* new PrerequisiteRunError({
|
|
198
|
+
message:
|
|
199
|
+
stderr !== "" ? stderr : `Failed to start VPN prerequisite "${prerequisite.key}".`,
|
|
200
|
+
hint: missingVpnToolHint(driver),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
188
203
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
204
|
+
const ready = yield* waitForVpn(driver, vpnConfig.connectTimeoutMs ?? 30000, runCommand);
|
|
205
|
+
if (!ready) {
|
|
206
|
+
return yield* new PrerequisiteRunError({
|
|
207
|
+
message: `VPN prerequisite "${prerequisite.key}" did not connect within timeout.`,
|
|
208
|
+
hint: missingVpnToolHint(driver),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const cleanup = prerequisite.cleanup ?? vpnConfig.defaultCleanup ?? "stop-if-started";
|
|
213
|
+
if (cleanup === "stop-if-started") {
|
|
214
|
+
startedDrivers.push({ driver, cooldownMs: vpnConfig.cooldownMs ?? 0 });
|
|
215
|
+
}
|
|
192
216
|
}
|
|
193
|
-
}
|
|
194
217
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
const cleanup = Effect.gen(function* () {
|
|
219
|
+
for (const started of startedDrivers.toReversed()) {
|
|
220
|
+
if (started.cooldownMs > 0) {
|
|
221
|
+
yield* Effect.sleep(Duration.millis(started.cooldownMs));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const stopCommand = makeVpnCommand(started.driver, "stop");
|
|
225
|
+
yield* runCommand(stopCommand.command, stopCommand.label).pipe(Effect.ignore);
|
|
199
226
|
}
|
|
227
|
+
});
|
|
200
228
|
|
|
201
|
-
|
|
202
|
-
|
|
229
|
+
return yield* effect.pipe(Effect.ensuring(cleanup));
|
|
230
|
+
}).pipe(Effect.result);
|
|
231
|
+
|
|
232
|
+
if (Result.isSuccess(prerequisiteResult)) {
|
|
233
|
+
return prerequisiteResult.success;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (shouldTryDirect && prerequisiteResult.failure instanceof PrerequisiteRunError) {
|
|
237
|
+
const directRetryResult = yield* tryDirect();
|
|
238
|
+
if (Result.isSuccess(directRetryResult)) {
|
|
239
|
+
return directRetryResult.success;
|
|
203
240
|
}
|
|
204
|
-
|
|
241
|
+
if (!(directRetryResult.failure instanceof PrerequisiteRunError)) {
|
|
242
|
+
return yield* Effect.fail(directRetryResult.failure);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
205
245
|
|
|
206
|
-
return yield*
|
|
246
|
+
return yield* Effect.fail(prerequisiteResult.failure);
|
|
207
247
|
});
|
|
208
248
|
};
|
|
@@ -15,9 +15,9 @@ export type PrerequisiteResolution =
|
|
|
15
15
|
export type SupportedPlatform = "darwin" | "linux" | "win32";
|
|
16
16
|
|
|
17
17
|
export type ResolvedVpnDriver =
|
|
18
|
-
| (
|
|
19
|
-
| (
|
|
20
|
-
| (
|
|
18
|
+
| (MacosScutilVpnDriverConfig & { platform: "darwin"; serviceName: string })
|
|
19
|
+
| (LinuxNmcliVpnDriverConfig & { platform: "linux"; connectionName: string })
|
|
20
|
+
| (WindowsRasdialVpnDriverConfig & { platform: "win32"; entryName: string });
|
|
21
21
|
|
|
22
22
|
export type VpnDriverResolution =
|
|
23
23
|
| { success: true; driver: ResolvedVpnDriver }
|
|
@@ -24,7 +24,12 @@ const resolveExplicitDriver = (
|
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
success: true,
|
|
27
|
-
driver: {
|
|
27
|
+
driver: {
|
|
28
|
+
platform,
|
|
29
|
+
type: driver.type,
|
|
30
|
+
serviceName: driver.serviceName ?? config.name,
|
|
31
|
+
secretEnvVar: driver.secretEnvVar ?? config.secretEnvVar,
|
|
32
|
+
},
|
|
28
33
|
};
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -93,7 +98,15 @@ export function resolveVpnDriverConfig(
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
if (platform === "darwin") {
|
|
96
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
driver: {
|
|
104
|
+
platform,
|
|
105
|
+
type: "macos-scutil",
|
|
106
|
+
serviceName: config.name,
|
|
107
|
+
secretEnvVar: config.secretEnvVar,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
if (platform === "linux") {
|