@blogic-cz/agent-tools 0.13.0 → 0.14.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/README.md +10 -0
- package/package.json +1 -1
- package/schemas/agent-tools.schema.json +173 -2
- package/src/config/loader.ts +57 -0
- package/src/config/types.ts +57 -2
- package/src/k8s-tool/index.ts +1 -1
- package/src/k8s-tool/service.ts +59 -35
- package/src/logs-tool/service.ts +16 -10
- package/src/shared/prerequisites/config.ts +38 -0
- package/src/shared/prerequisites/types.ts +21 -0
- package/src/shared/prerequisites/vpn.ts +119 -0
package/README.md
CHANGED
|
@@ -173,10 +173,20 @@ bun run agent-tools/example-tool/index.ts ping
|
|
|
173
173
|
retentionDays: 90,
|
|
174
174
|
dbPath: "~/.agent-tools/audit.sqlite",
|
|
175
175
|
},
|
|
176
|
+
vpns: {
|
|
177
|
+
blogic: {
|
|
178
|
+
// auto defaults to true:
|
|
179
|
+
// darwin -> macos-scutil, linux -> linux-nmcli, win32 -> windows-rasdial
|
|
180
|
+
name: "BLVPN",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
176
183
|
kubernetes: {
|
|
177
184
|
default: {
|
|
178
185
|
clusterId: "your-cluster-id",
|
|
179
186
|
namespaces: { test: "your-ns-test", prod: "your-ns-prod" },
|
|
187
|
+
prerequisites: [{ type: "vpn", key: "blogic" }],
|
|
188
|
+
// Prerequisites are currently decoded and validated as config metadata;
|
|
189
|
+
// automatic VPN connect/disconnect execution is planned for a follow-up release.
|
|
180
190
|
},
|
|
181
191
|
},
|
|
182
192
|
logs: {
|
package/package.json
CHANGED
|
@@ -72,6 +72,13 @@
|
|
|
72
72
|
"additionalProperties": {
|
|
73
73
|
"$ref": "#/definitions/GitHubRepoConfig"
|
|
74
74
|
}
|
|
75
|
+
},
|
|
76
|
+
"vpns": {
|
|
77
|
+
"description": "Named VPN definitions referenced by profile prerequisites.",
|
|
78
|
+
"type": "object",
|
|
79
|
+
"additionalProperties": {
|
|
80
|
+
"$ref": "#/definitions/VpnConfig"
|
|
81
|
+
}
|
|
75
82
|
}
|
|
76
83
|
},
|
|
77
84
|
"definitions": {
|
|
@@ -81,8 +88,14 @@
|
|
|
81
88
|
"additionalProperties": false,
|
|
82
89
|
"required": ["owner", "repo"],
|
|
83
90
|
"properties": {
|
|
84
|
-
"owner": {
|
|
85
|
-
|
|
91
|
+
"owner": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "GitHub organization or user name."
|
|
94
|
+
},
|
|
95
|
+
"repo": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Repository name."
|
|
98
|
+
}
|
|
86
99
|
}
|
|
87
100
|
},
|
|
88
101
|
"AzureConfig": {
|
|
@@ -125,6 +138,17 @@
|
|
|
125
138
|
"timeoutMs": {
|
|
126
139
|
"description": "Optional command timeout in milliseconds.",
|
|
127
140
|
"type": "number"
|
|
141
|
+
},
|
|
142
|
+
"prerequisites": {
|
|
143
|
+
"description": "Ordered prerequisite references required before remote tool execution.",
|
|
144
|
+
"type": "array",
|
|
145
|
+
"items": {
|
|
146
|
+
"$ref": "#/definitions/Prerequisite"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"vpn": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"description": "Convenience sugar for a VPN prerequisite key. Runtime normalizes this to prerequisites."
|
|
128
152
|
}
|
|
129
153
|
},
|
|
130
154
|
"required": ["clusterId", "namespaces"]
|
|
@@ -251,6 +275,21 @@
|
|
|
251
275
|
"remotePath": {
|
|
252
276
|
"description": "Remote logs path used in container or host environments.",
|
|
253
277
|
"type": "string"
|
|
278
|
+
},
|
|
279
|
+
"prerequisites": {
|
|
280
|
+
"description": "Ordered prerequisite references required before remote tool execution.",
|
|
281
|
+
"type": "array",
|
|
282
|
+
"items": {
|
|
283
|
+
"$ref": "#/definitions/Prerequisite"
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
"vpn": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"description": "Convenience sugar for a VPN prerequisite key. Runtime normalizes this to prerequisites."
|
|
289
|
+
},
|
|
290
|
+
"kubernetesProfile": {
|
|
291
|
+
"type": "string",
|
|
292
|
+
"description": "Kubernetes profile used for remote logs."
|
|
254
293
|
}
|
|
255
294
|
},
|
|
256
295
|
"required": ["localDir", "remotePath"]
|
|
@@ -336,6 +375,138 @@
|
|
|
336
375
|
}
|
|
337
376
|
}
|
|
338
377
|
}
|
|
378
|
+
},
|
|
379
|
+
"VpnPrerequisite": {
|
|
380
|
+
"description": "VPN prerequisite reference used by tool profiles.",
|
|
381
|
+
"type": "object",
|
|
382
|
+
"additionalProperties": false,
|
|
383
|
+
"properties": {
|
|
384
|
+
"type": {
|
|
385
|
+
"const": "vpn",
|
|
386
|
+
"description": "Prerequisite type."
|
|
387
|
+
},
|
|
388
|
+
"key": {
|
|
389
|
+
"type": "string",
|
|
390
|
+
"description": "Key from the top-level vpns registry."
|
|
391
|
+
},
|
|
392
|
+
"cleanup": {
|
|
393
|
+
"description": "VPN cleanup policy.",
|
|
394
|
+
"type": "string",
|
|
395
|
+
"enum": ["leave-running", "stop-if-started"]
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
"required": ["type", "key"]
|
|
399
|
+
},
|
|
400
|
+
"Prerequisite": {
|
|
401
|
+
"oneOf": [
|
|
402
|
+
{
|
|
403
|
+
"$ref": "#/definitions/VpnPrerequisite"
|
|
404
|
+
}
|
|
405
|
+
]
|
|
406
|
+
},
|
|
407
|
+
"MacosScutilVpnDriverConfig": {
|
|
408
|
+
"type": "object",
|
|
409
|
+
"additionalProperties": false,
|
|
410
|
+
"properties": {
|
|
411
|
+
"type": {
|
|
412
|
+
"const": "macos-scutil"
|
|
413
|
+
},
|
|
414
|
+
"serviceName": {
|
|
415
|
+
"type": "string"
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
"required": ["type"]
|
|
419
|
+
},
|
|
420
|
+
"LinuxNmcliVpnDriverConfig": {
|
|
421
|
+
"type": "object",
|
|
422
|
+
"additionalProperties": false,
|
|
423
|
+
"properties": {
|
|
424
|
+
"type": {
|
|
425
|
+
"const": "linux-nmcli"
|
|
426
|
+
},
|
|
427
|
+
"connectionName": {
|
|
428
|
+
"type": "string"
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
"required": ["type"]
|
|
432
|
+
},
|
|
433
|
+
"WindowsRasdialVpnDriverConfig": {
|
|
434
|
+
"type": "object",
|
|
435
|
+
"additionalProperties": false,
|
|
436
|
+
"properties": {
|
|
437
|
+
"type": {
|
|
438
|
+
"const": "windows-rasdial"
|
|
439
|
+
},
|
|
440
|
+
"entryName": {
|
|
441
|
+
"type": "string"
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
"required": ["type"]
|
|
445
|
+
},
|
|
446
|
+
"VpnDriverConfig": {
|
|
447
|
+
"oneOf": [
|
|
448
|
+
{
|
|
449
|
+
"$ref": "#/definitions/MacosScutilVpnDriverConfig"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"$ref": "#/definitions/LinuxNmcliVpnDriverConfig"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"$ref": "#/definitions/WindowsRasdialVpnDriverConfig"
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
"VpnConfig": {
|
|
460
|
+
"description": "VPN configuration. auto defaults to true and maps name to a deterministic OS-specific driver.",
|
|
461
|
+
"type": "object",
|
|
462
|
+
"additionalProperties": false,
|
|
463
|
+
"properties": {
|
|
464
|
+
"name": {
|
|
465
|
+
"type": "string",
|
|
466
|
+
"description": "VPN connection/service name used by auto driver mapping."
|
|
467
|
+
},
|
|
468
|
+
"auto": {
|
|
469
|
+
"type": "boolean",
|
|
470
|
+
"default": true,
|
|
471
|
+
"description": "Defaults to true. Maps by process.platform without heuristic discovery."
|
|
472
|
+
},
|
|
473
|
+
"defaultCleanup": {
|
|
474
|
+
"description": "VPN cleanup policy.",
|
|
475
|
+
"type": "string",
|
|
476
|
+
"enum": ["leave-running", "stop-if-started"]
|
|
477
|
+
},
|
|
478
|
+
"connectTimeoutMs": {
|
|
479
|
+
"type": "number"
|
|
480
|
+
},
|
|
481
|
+
"disconnectTimeoutMs": {
|
|
482
|
+
"type": "number"
|
|
483
|
+
},
|
|
484
|
+
"cooldownMs": {
|
|
485
|
+
"type": "number"
|
|
486
|
+
},
|
|
487
|
+
"leaseTtlMs": {
|
|
488
|
+
"type": "number"
|
|
489
|
+
},
|
|
490
|
+
"drivers": {
|
|
491
|
+
"type": "object",
|
|
492
|
+
"additionalProperties": false,
|
|
493
|
+
"properties": {
|
|
494
|
+
"darwin": {
|
|
495
|
+
"$ref": "#/definitions/MacosScutilVpnDriverConfig"
|
|
496
|
+
},
|
|
497
|
+
"linux": {
|
|
498
|
+
"$ref": "#/definitions/LinuxNmcliVpnDriverConfig"
|
|
499
|
+
},
|
|
500
|
+
"win32": {
|
|
501
|
+
"$ref": "#/definitions/WindowsRasdialVpnDriverConfig"
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
"driver": {
|
|
506
|
+
"$ref": "#/definitions/VpnDriverConfig"
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
"required": ["name"]
|
|
339
510
|
}
|
|
340
511
|
},
|
|
341
512
|
"examples": [
|
package/src/config/loader.ts
CHANGED
|
@@ -16,6 +16,56 @@ const CredentialGuardConfigSchema = Schema.Struct({
|
|
|
16
16
|
additionalDangerousBashPatterns: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
const CleanupPolicySchema = Schema.Literals(["leave-running", "stop-if-started"]);
|
|
20
|
+
|
|
21
|
+
const VpnPrerequisiteSchema = Schema.Struct({
|
|
22
|
+
type: Schema.Literal("vpn"),
|
|
23
|
+
key: Schema.String,
|
|
24
|
+
cleanup: Schema.optionalKey(CleanupPolicySchema),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const PrerequisiteSchema = VpnPrerequisiteSchema;
|
|
28
|
+
const PrerequisitesSchema = Schema.Array(PrerequisiteSchema);
|
|
29
|
+
|
|
30
|
+
const MacosScutilVpnDriverConfigSchema = Schema.Struct({
|
|
31
|
+
type: Schema.Literal("macos-scutil"),
|
|
32
|
+
serviceName: Schema.optionalKey(Schema.String),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const LinuxNmcliVpnDriverConfigSchema = Schema.Struct({
|
|
36
|
+
type: Schema.Literal("linux-nmcli"),
|
|
37
|
+
connectionName: Schema.optionalKey(Schema.String),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const WindowsRasdialVpnDriverConfigSchema = Schema.Struct({
|
|
41
|
+
type: Schema.Literal("windows-rasdial"),
|
|
42
|
+
entryName: Schema.optionalKey(Schema.String),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const VpnDriverConfigSchema = Schema.Union([
|
|
46
|
+
MacosScutilVpnDriverConfigSchema,
|
|
47
|
+
LinuxNmcliVpnDriverConfigSchema,
|
|
48
|
+
WindowsRasdialVpnDriverConfigSchema,
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const VpnConfigSchema = Schema.Struct({
|
|
52
|
+
name: Schema.String,
|
|
53
|
+
auto: Schema.optionalKey(Schema.Boolean),
|
|
54
|
+
defaultCleanup: Schema.optionalKey(CleanupPolicySchema),
|
|
55
|
+
connectTimeoutMs: Schema.optionalKey(Schema.Number),
|
|
56
|
+
disconnectTimeoutMs: Schema.optionalKey(Schema.Number),
|
|
57
|
+
cooldownMs: Schema.optionalKey(Schema.Number),
|
|
58
|
+
leaseTtlMs: Schema.optionalKey(Schema.Number),
|
|
59
|
+
drivers: Schema.optionalKey(
|
|
60
|
+
Schema.Struct({
|
|
61
|
+
darwin: Schema.optionalKey(MacosScutilVpnDriverConfigSchema),
|
|
62
|
+
linux: Schema.optionalKey(LinuxNmcliVpnDriverConfigSchema),
|
|
63
|
+
win32: Schema.optionalKey(WindowsRasdialVpnDriverConfigSchema),
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
driver: Schema.optionalKey(VpnDriverConfigSchema),
|
|
67
|
+
});
|
|
68
|
+
|
|
19
69
|
const AzureConfigSchema = Schema.Struct({
|
|
20
70
|
organization: Schema.String,
|
|
21
71
|
defaultProject: Schema.String,
|
|
@@ -26,6 +76,8 @@ const K8sConfigSchema = Schema.Struct({
|
|
|
26
76
|
clusterId: Schema.String,
|
|
27
77
|
namespaces: Schema.Record(Schema.String, Schema.String),
|
|
28
78
|
timeoutMs: Schema.optionalKey(Schema.Number),
|
|
79
|
+
prerequisites: Schema.optionalKey(PrerequisitesSchema),
|
|
80
|
+
vpn: Schema.optionalKey(Schema.String),
|
|
29
81
|
});
|
|
30
82
|
|
|
31
83
|
const DbEnvConfigSchema = Schema.Struct({
|
|
@@ -52,6 +104,9 @@ const DatabaseConfigSchema = Schema.Struct({
|
|
|
52
104
|
const LogsConfigSchema = Schema.Struct({
|
|
53
105
|
localDir: Schema.String,
|
|
54
106
|
remotePath: Schema.String,
|
|
107
|
+
kubernetesProfile: Schema.optionalKey(Schema.String),
|
|
108
|
+
prerequisites: Schema.optionalKey(PrerequisitesSchema),
|
|
109
|
+
vpn: Schema.optionalKey(Schema.String),
|
|
55
110
|
});
|
|
56
111
|
|
|
57
112
|
const ObservabilityEnvTargetSchema = Schema.Struct({
|
|
@@ -78,6 +133,7 @@ const GitHubRepoConfigSchema = Schema.Struct({
|
|
|
78
133
|
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
79
134
|
"$schema",
|
|
80
135
|
"azure",
|
|
136
|
+
"vpns",
|
|
81
137
|
"kubernetes",
|
|
82
138
|
"database",
|
|
83
139
|
"observability",
|
|
@@ -92,6 +148,7 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
|
92
148
|
const AgentToolsConfigSchema = Schema.Struct({
|
|
93
149
|
$schema: Schema.optionalKey(Schema.String),
|
|
94
150
|
azure: Schema.optionalKey(Schema.Record(Schema.String, AzureConfigSchema)),
|
|
151
|
+
vpns: Schema.optionalKey(Schema.Record(Schema.String, VpnConfigSchema)),
|
|
95
152
|
kubernetes: Schema.optionalKey(Schema.Record(Schema.String, K8sConfigSchema)),
|
|
96
153
|
database: Schema.optionalKey(Schema.Record(Schema.String, DatabaseConfigSchema)),
|
|
97
154
|
observability: Schema.optionalKey(Schema.Record(Schema.String, ObservabilityConfigSchema)),
|
package/src/config/types.ts
CHANGED
|
@@ -5,8 +5,60 @@ export type AzureConfig = {
|
|
|
5
5
|
timeoutMs?: number;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
export type CleanupPolicy = "leave-running" | "stop-if-started";
|
|
9
|
+
|
|
10
|
+
export type VpnPrerequisite = {
|
|
11
|
+
type: "vpn";
|
|
12
|
+
key: string;
|
|
13
|
+
cleanup?: CleanupPolicy;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type MacosScutilVpnDriverConfig = {
|
|
17
|
+
type: "macos-scutil";
|
|
18
|
+
serviceName?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type LinuxNmcliVpnDriverConfig = {
|
|
22
|
+
type: "linux-nmcli";
|
|
23
|
+
connectionName?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type WindowsRasdialVpnDriverConfig = {
|
|
27
|
+
type: "windows-rasdial";
|
|
28
|
+
entryName?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type VpnDriverConfig =
|
|
32
|
+
| MacosScutilVpnDriverConfig
|
|
33
|
+
| LinuxNmcliVpnDriverConfig
|
|
34
|
+
| WindowsRasdialVpnDriverConfig;
|
|
35
|
+
|
|
36
|
+
export type VpnConfig = {
|
|
37
|
+
name: string;
|
|
38
|
+
/** Defaults to true. Auto maps name to the current OS driver deterministically. */
|
|
39
|
+
auto?: boolean;
|
|
40
|
+
defaultCleanup?: CleanupPolicy;
|
|
41
|
+
connectTimeoutMs?: number;
|
|
42
|
+
disconnectTimeoutMs?: number;
|
|
43
|
+
cooldownMs?: number;
|
|
44
|
+
leaseTtlMs?: number;
|
|
45
|
+
drivers?: {
|
|
46
|
+
darwin?: MacosScutilVpnDriverConfig;
|
|
47
|
+
linux?: LinuxNmcliVpnDriverConfig;
|
|
48
|
+
win32?: WindowsRasdialVpnDriverConfig;
|
|
49
|
+
};
|
|
50
|
+
/** Explicit current-platform driver, required when auto is false and no per-OS driver is available. */
|
|
51
|
+
driver?: VpnDriverConfig;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ProfilePrerequisites = {
|
|
55
|
+
prerequisites?: readonly VpnPrerequisite[];
|
|
56
|
+
/** Convenience input sugar; normalize to prerequisites before execution. */
|
|
57
|
+
vpn?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
8
60
|
/** Kubernetes cluster profile configuration */
|
|
9
|
-
export type K8sConfig = {
|
|
61
|
+
export type K8sConfig = ProfilePrerequisites & {
|
|
10
62
|
clusterId: string;
|
|
11
63
|
/** Named namespaces, e.g. { test: "my-app-test", prod: "my-app-prod" } */
|
|
12
64
|
namespaces: Record<string, string>;
|
|
@@ -38,9 +90,10 @@ export type DatabaseConfig = {
|
|
|
38
90
|
};
|
|
39
91
|
|
|
40
92
|
/** Logs profile configuration */
|
|
41
|
-
export type LogsConfig = {
|
|
93
|
+
export type LogsConfig = ProfilePrerequisites & {
|
|
42
94
|
localDir: string;
|
|
43
95
|
remotePath: string;
|
|
96
|
+
kubernetesProfile?: string;
|
|
44
97
|
};
|
|
45
98
|
|
|
46
99
|
/** Single observability environment connection details */
|
|
@@ -93,6 +146,8 @@ export type AgentToolsConfig = {
|
|
|
93
146
|
$schema?: string;
|
|
94
147
|
/** Named Azure DevOps profiles. e.g. { default: { organization: "...", defaultProject: "..." } } */
|
|
95
148
|
azure?: Record<string, AzureConfig>;
|
|
149
|
+
/** Named VPN definitions referenced by profile prerequisites. */
|
|
150
|
+
vpns?: Record<string, VpnConfig>;
|
|
96
151
|
/** Named Kubernetes cluster profiles. e.g. { default: {...}, staging: {...} } */
|
|
97
152
|
kubernetes?: Record<string, K8sConfig>;
|
|
98
153
|
/** Named database profiles. e.g. { default: {...}, analytics: {...} } */
|
package/src/k8s-tool/index.ts
CHANGED
|
@@ -82,7 +82,7 @@ const executeK8sCommand = (command: string, options: CommonK8sCommandOptions) =>
|
|
|
82
82
|
const resolvedEnv = yield* resolveEnv(options.env, config);
|
|
83
83
|
|
|
84
84
|
const k8sService = yield* K8sService;
|
|
85
|
-
const result = yield* k8sService.runKubectl(command, options.dryRun).pipe(
|
|
85
|
+
const result = yield* k8sService.runKubectl(command, options.dryRun, profileName).pipe(
|
|
86
86
|
Effect.catchTags({
|
|
87
87
|
K8sContextError: (error) => {
|
|
88
88
|
const errorResult: CommandResult = {
|
package/src/k8s-tool/service.ts
CHANGED
|
@@ -19,6 +19,7 @@ export class K8sService extends Context.Service<
|
|
|
19
19
|
readonly runCommand: (
|
|
20
20
|
cmd: string,
|
|
21
21
|
env: Environment,
|
|
22
|
+
profile?: string,
|
|
22
23
|
) => Effect.Effect<
|
|
23
24
|
string,
|
|
24
25
|
K8sContextError | K8sCommandError | K8sTimeoutError | K8sDangerousCommandError
|
|
@@ -26,6 +27,7 @@ export class K8sService extends Context.Service<
|
|
|
26
27
|
readonly runKubectl: (
|
|
27
28
|
cmd: string,
|
|
28
29
|
dryRun: boolean,
|
|
30
|
+
profile?: string,
|
|
29
31
|
) => Effect.Effect<
|
|
30
32
|
CommandResult,
|
|
31
33
|
K8sContextError | K8sCommandError | K8sTimeoutError | K8sDangerousCommandError
|
|
@@ -39,24 +41,27 @@ export class K8sService extends Context.Service<
|
|
|
39
41
|
const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
40
42
|
|
|
41
43
|
const config = yield* ConfigService;
|
|
42
|
-
const k8sConfig = getToolConfig<K8sConfig>(config, "kubernetes");
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
const getK8sConfig = (profile?: string) =>
|
|
46
|
+
getToolConfig<K8sConfig>(config, "kubernetes", profile);
|
|
47
|
+
|
|
48
|
+
const requireK8sConfig = (profile?: string) =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const k8sConfig = getK8sConfig(profile);
|
|
51
|
+
if (!k8sConfig) {
|
|
52
|
+
return yield* new K8sContextError({
|
|
53
|
+
message: profile
|
|
54
|
+
? `No Kubernetes configuration found for profile: ${profile}.`
|
|
55
|
+
: "No Kubernetes configuration found. Add a 'kubernetes' section to agent-tools.json5.",
|
|
56
|
+
clusterId: profile ?? "unknown",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
return k8sConfig;
|
|
61
|
+
});
|
|
57
62
|
|
|
58
|
-
//
|
|
59
|
-
const contextRef = yield* Ref.make<string
|
|
63
|
+
// Cache context by selected profile/cluster instead of a single default profile.
|
|
64
|
+
const contextRef = yield* Ref.make<Record<string, string>>({});
|
|
60
65
|
|
|
61
66
|
// Helper that uses executor.spawn() to avoid ChildProcessSpawner requirement in return type
|
|
62
67
|
const runShellCommand = (commandStr: string, timeoutMs: number) =>
|
|
@@ -97,22 +102,27 @@ export class K8sService extends Context.Service<
|
|
|
97
102
|
),
|
|
98
103
|
);
|
|
99
104
|
|
|
100
|
-
const resolveContext = Effect.fn("K8sService.resolveContext")(function* (
|
|
101
|
-
|
|
105
|
+
const resolveContext = Effect.fn("K8sService.resolveContext")(function* (
|
|
106
|
+
profile: string | undefined,
|
|
107
|
+
k8sConfig: K8sConfig,
|
|
108
|
+
) {
|
|
109
|
+
const timeoutMs = k8sConfig.timeoutMs ?? 60000;
|
|
110
|
+
const cacheKey = profile ?? `cluster:${k8sConfig.clusterId}`;
|
|
102
111
|
const cached = yield* Ref.get(contextRef);
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
const cachedContext = cached[cacheKey];
|
|
113
|
+
if (cachedContext !== undefined) {
|
|
114
|
+
return cachedContext;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
const jqCommand = `kubectl config view -o json | jq -r '.contexts[] | select(.context.cluster == "${k8sConfig.clusterId}") | .name' | head -1`;
|
|
108
118
|
|
|
109
|
-
const contextResultOption = yield* runShellCommand(jqCommand,
|
|
119
|
+
const contextResultOption = yield* runShellCommand(jqCommand, timeoutMs);
|
|
110
120
|
|
|
111
121
|
if (Option.isNone(contextResultOption)) {
|
|
112
122
|
return yield* new K8sTimeoutError({
|
|
113
|
-
message: `Context resolution timed out after ${
|
|
123
|
+
message: `Context resolution timed out after ${timeoutMs}ms`,
|
|
114
124
|
command: jqCommand,
|
|
115
|
-
timeoutMs
|
|
125
|
+
timeoutMs,
|
|
116
126
|
});
|
|
117
127
|
}
|
|
118
128
|
|
|
@@ -120,19 +130,22 @@ export class K8sService extends Context.Service<
|
|
|
120
130
|
|
|
121
131
|
if (contextResult.exitCode === 0 && contextResult.stdout.trim()) {
|
|
122
132
|
const resolvedContextValue = contextResult.stdout.trim();
|
|
123
|
-
yield* Ref.
|
|
133
|
+
yield* Ref.update(contextRef, (contexts) => ({
|
|
134
|
+
...contexts,
|
|
135
|
+
[cacheKey]: resolvedContextValue,
|
|
136
|
+
}));
|
|
124
137
|
return resolvedContextValue;
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
const fallbackCommand = `kubectl config view -o json | jq -r '.contexts[] as $ctx | .clusters[] | select(.name == $ctx.context.cluster and (.cluster.server | contains("${k8sConfig.clusterId}"))) | $ctx.name' | head -1`;
|
|
128
141
|
|
|
129
|
-
const fallbackResultOption = yield* runShellCommand(fallbackCommand,
|
|
142
|
+
const fallbackResultOption = yield* runShellCommand(fallbackCommand, timeoutMs);
|
|
130
143
|
|
|
131
144
|
if (Option.isNone(fallbackResultOption)) {
|
|
132
145
|
return yield* new K8sTimeoutError({
|
|
133
|
-
message: `Context resolution timed out after ${
|
|
146
|
+
message: `Context resolution timed out after ${timeoutMs}ms`,
|
|
134
147
|
command: fallbackCommand,
|
|
135
|
-
timeoutMs
|
|
148
|
+
timeoutMs,
|
|
136
149
|
});
|
|
137
150
|
}
|
|
138
151
|
|
|
@@ -140,7 +153,10 @@ export class K8sService extends Context.Service<
|
|
|
140
153
|
|
|
141
154
|
if (fallbackResult.exitCode === 0 && fallbackResult.stdout.trim()) {
|
|
142
155
|
const resolvedContextValue = fallbackResult.stdout.trim();
|
|
143
|
-
yield* Ref.
|
|
156
|
+
yield* Ref.update(contextRef, (contexts) => ({
|
|
157
|
+
...contexts,
|
|
158
|
+
[cacheKey]: resolvedContextValue,
|
|
159
|
+
}));
|
|
144
160
|
return resolvedContextValue;
|
|
145
161
|
}
|
|
146
162
|
|
|
@@ -150,17 +166,22 @@ export class K8sService extends Context.Service<
|
|
|
150
166
|
});
|
|
151
167
|
});
|
|
152
168
|
|
|
153
|
-
const executeCommand = Effect.fn("K8sService.executeCommand")(function* (
|
|
154
|
-
|
|
169
|
+
const executeCommand = Effect.fn("K8sService.executeCommand")(function* (
|
|
170
|
+
cmd: string,
|
|
171
|
+
profile?: string,
|
|
172
|
+
) {
|
|
173
|
+
const k8sConfig = yield* requireK8sConfig(profile);
|
|
174
|
+
const timeoutMs = k8sConfig.timeoutMs ?? 60000;
|
|
175
|
+
const context = yield* resolveContext(profile, k8sConfig);
|
|
155
176
|
const fullCommand = `kubectl --context ${context} ${cmd}`;
|
|
156
177
|
|
|
157
|
-
const resultOption = yield* runShellCommand(fullCommand,
|
|
178
|
+
const resultOption = yield* runShellCommand(fullCommand, timeoutMs);
|
|
158
179
|
|
|
159
180
|
if (Option.isNone(resultOption)) {
|
|
160
181
|
return yield* new K8sTimeoutError({
|
|
161
|
-
message: `Command timed out after ${
|
|
182
|
+
message: `Command timed out after ${timeoutMs}ms`,
|
|
162
183
|
command: fullCommand,
|
|
163
|
-
timeoutMs
|
|
184
|
+
timeoutMs,
|
|
164
185
|
});
|
|
165
186
|
}
|
|
166
187
|
|
|
@@ -177,6 +198,7 @@ export class K8sService extends Context.Service<
|
|
|
177
198
|
const runCommand = Effect.fn("K8sService.runCommand")(function* (
|
|
178
199
|
cmd: string,
|
|
179
200
|
_env: Environment,
|
|
201
|
+
profile?: string,
|
|
180
202
|
) {
|
|
181
203
|
// Security: block dangerous commands before execution
|
|
182
204
|
const securityCheck = isKubectlCommandAllowed(cmd);
|
|
@@ -189,7 +211,7 @@ export class K8sService extends Context.Service<
|
|
|
189
211
|
});
|
|
190
212
|
}
|
|
191
213
|
|
|
192
|
-
const result = yield* executeCommand(cmd);
|
|
214
|
+
const result = yield* executeCommand(cmd, profile);
|
|
193
215
|
if (result.exitCode !== 0) {
|
|
194
216
|
return yield* new K8sCommandError({
|
|
195
217
|
message: result.stderr ?? `kubectl exited with code ${result.exitCode}`,
|
|
@@ -205,6 +227,7 @@ export class K8sService extends Context.Service<
|
|
|
205
227
|
const runKubectl = Effect.fn("K8sService.runKubectl")(function* (
|
|
206
228
|
cmd: string,
|
|
207
229
|
dryRun: boolean,
|
|
230
|
+
profile?: string,
|
|
208
231
|
) {
|
|
209
232
|
// Security: block dangerous commands before execution (even dry-run)
|
|
210
233
|
const securityCheck = isKubectlCommandAllowed(cmd);
|
|
@@ -219,7 +242,8 @@ export class K8sService extends Context.Service<
|
|
|
219
242
|
|
|
220
243
|
const startTime = Date.now();
|
|
221
244
|
if (dryRun) {
|
|
222
|
-
const
|
|
245
|
+
const k8sConfig = yield* requireK8sConfig(profile);
|
|
246
|
+
const context = yield* resolveContext(profile, k8sConfig);
|
|
223
247
|
const fullCommand = `kubectl --context ${context} ${cmd}`;
|
|
224
248
|
return {
|
|
225
249
|
success: true,
|
|
@@ -229,7 +253,7 @@ export class K8sService extends Context.Service<
|
|
|
229
253
|
};
|
|
230
254
|
}
|
|
231
255
|
|
|
232
|
-
const result = yield* executeCommand(cmd);
|
|
256
|
+
const result = yield* executeCommand(cmd, profile);
|
|
233
257
|
|
|
234
258
|
if (result.exitCode !== 0) {
|
|
235
259
|
return yield* new K8sCommandError({
|
package/src/logs-tool/service.ts
CHANGED
|
@@ -125,11 +125,13 @@ export class LogsService extends Context.Service<
|
|
|
125
125
|
logsConfig: LogsConfig,
|
|
126
126
|
) {
|
|
127
127
|
const remotePath = logsConfig.remotePath;
|
|
128
|
+
const kubernetesProfile = logsConfig.kubernetesProfile;
|
|
128
129
|
|
|
129
130
|
const podResult = yield* k8s
|
|
130
131
|
.runKubectl(
|
|
131
132
|
`get pods --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}'`,
|
|
132
133
|
false,
|
|
134
|
+
kubernetesProfile,
|
|
133
135
|
)
|
|
134
136
|
.pipe(
|
|
135
137
|
Effect.mapError(
|
|
@@ -143,15 +145,17 @@ export class LogsService extends Context.Service<
|
|
|
143
145
|
|
|
144
146
|
const pod = readCommandOutput(podResult.output).replace(/'/g, "");
|
|
145
147
|
|
|
146
|
-
const listResult = yield* k8s
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
const listResult = yield* k8s
|
|
149
|
+
.runKubectl(`exec ${pod} -- ls -la ${remotePath}`, false, kubernetesProfile)
|
|
150
|
+
.pipe(
|
|
151
|
+
Effect.mapError(
|
|
152
|
+
(error) =>
|
|
153
|
+
new LogsReadError({
|
|
154
|
+
message: error instanceof Error ? error.message : "Failed to list remote logs",
|
|
155
|
+
source: `${pod}:${remotePath}`,
|
|
156
|
+
}),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
155
159
|
|
|
156
160
|
const files = parseLogFiles(readCommandOutput(listResult.output));
|
|
157
161
|
if (files.length === 0) {
|
|
@@ -226,11 +230,13 @@ export class LogsService extends Context.Service<
|
|
|
226
230
|
logsConfig: LogsConfig,
|
|
227
231
|
) {
|
|
228
232
|
const remotePath = logsConfig.remotePath;
|
|
233
|
+
const kubernetesProfile = logsConfig.kubernetesProfile;
|
|
229
234
|
|
|
230
235
|
const podResult = yield* k8s
|
|
231
236
|
.runKubectl(
|
|
232
237
|
`get pods --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}'`,
|
|
233
238
|
false,
|
|
239
|
+
kubernetesProfile,
|
|
234
240
|
)
|
|
235
241
|
.pipe(
|
|
236
242
|
Effect.mapError(
|
|
@@ -252,7 +258,7 @@ export class LogsService extends Context.Service<
|
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
const execResult = yield* k8s
|
|
255
|
-
.runKubectl(`exec ${pod} -- sh -c "${command}"`, false)
|
|
261
|
+
.runKubectl(`exec ${pod} -- sh -c "${command}"`, false, kubernetesProfile)
|
|
256
262
|
.pipe(Effect.result);
|
|
257
263
|
|
|
258
264
|
return yield* Result.match(execResult, {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AgentToolsConfig, VpnPrerequisite, ProfilePrerequisites } from "#config/types";
|
|
2
|
+
import type { PrerequisiteResolution } from "#shared/prerequisites/types";
|
|
3
|
+
|
|
4
|
+
export function normalizeProfilePrerequisites(
|
|
5
|
+
profile: ProfilePrerequisites,
|
|
6
|
+
): readonly VpnPrerequisite[] {
|
|
7
|
+
const prerequisites = [...(profile.prerequisites ?? [])];
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
profile.vpn &&
|
|
11
|
+
!prerequisites.some(
|
|
12
|
+
(prerequisite) => prerequisite.type === "vpn" && prerequisite.key === profile.vpn,
|
|
13
|
+
)
|
|
14
|
+
) {
|
|
15
|
+
prerequisites.push({ type: "vpn", key: profile.vpn });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return prerequisites;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveProfilePrerequisites(
|
|
22
|
+
config: AgentToolsConfig,
|
|
23
|
+
profile: ProfilePrerequisites,
|
|
24
|
+
): PrerequisiteResolution {
|
|
25
|
+
const prerequisites = normalizeProfilePrerequisites(profile);
|
|
26
|
+
|
|
27
|
+
for (const prerequisite of prerequisites) {
|
|
28
|
+
if (prerequisite.type === "vpn" && !config.vpns?.[prerequisite.key]) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `VPN prerequisite "${prerequisite.key}" is not defined.`,
|
|
32
|
+
hint: `Add vpns.${prerequisite.key} to agent-tools.json5 or remove the prerequisite.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { success: true, prerequisites };
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LinuxNmcliVpnDriverConfig,
|
|
3
|
+
MacosScutilVpnDriverConfig,
|
|
4
|
+
VpnPrerequisite,
|
|
5
|
+
WindowsRasdialVpnDriverConfig,
|
|
6
|
+
} from "#config/types";
|
|
7
|
+
|
|
8
|
+
export type PrerequisiteResolution =
|
|
9
|
+
| { success: true; prerequisites: readonly VpnPrerequisite[] }
|
|
10
|
+
| { success: false; error: string; hint: string };
|
|
11
|
+
|
|
12
|
+
export type SupportedPlatform = "darwin" | "linux" | "win32";
|
|
13
|
+
|
|
14
|
+
export type ResolvedVpnDriver =
|
|
15
|
+
| (Required<MacosScutilVpnDriverConfig> & { platform: "darwin" })
|
|
16
|
+
| (Required<LinuxNmcliVpnDriverConfig> & { platform: "linux" })
|
|
17
|
+
| (Required<WindowsRasdialVpnDriverConfig> & { platform: "win32" });
|
|
18
|
+
|
|
19
|
+
export type VpnDriverResolution =
|
|
20
|
+
| { success: true; driver: ResolvedVpnDriver }
|
|
21
|
+
| { success: false; error: string; hint: string };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { VpnConfig, VpnDriverConfig } from "#config/types";
|
|
2
|
+
import type {
|
|
3
|
+
ResolvedVpnDriver,
|
|
4
|
+
SupportedPlatform,
|
|
5
|
+
VpnDriverResolution,
|
|
6
|
+
} from "#shared/prerequisites/types";
|
|
7
|
+
|
|
8
|
+
const isSupportedPlatform = (platform: typeof process.platform): platform is SupportedPlatform =>
|
|
9
|
+
platform === "darwin" || platform === "linux" || platform === "win32";
|
|
10
|
+
|
|
11
|
+
const resolveExplicitDriver = (
|
|
12
|
+
config: VpnConfig,
|
|
13
|
+
platform: SupportedPlatform,
|
|
14
|
+
driver: VpnDriverConfig,
|
|
15
|
+
): VpnDriverResolution => {
|
|
16
|
+
if (driver.type === "macos-scutil") {
|
|
17
|
+
if (platform !== "darwin") {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
error: `VPN driver "${driver.type}" is not supported on ${platform}.`,
|
|
21
|
+
hint: "Configure a driver for the current OS or enable auto detection.",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
success: true,
|
|
27
|
+
driver: { platform, type: driver.type, serviceName: driver.serviceName ?? config.name },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (driver.type === "linux-nmcli") {
|
|
32
|
+
if (platform !== "linux") {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
error: `VPN driver "${driver.type}" is not supported on ${platform}.`,
|
|
36
|
+
hint: "Configure a driver for the current OS or enable auto detection.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
driver: { platform, type: driver.type, connectionName: driver.connectionName ?? config.name },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (driver.type === "windows-rasdial") {
|
|
47
|
+
if (platform !== "win32") {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: `VPN driver "${driver.type}" is not supported on ${platform}.`,
|
|
51
|
+
hint: "Configure a driver for the current OS or enable auto detection.",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
driver: { platform, type: driver.type, entryName: driver.entryName ?? config.name },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const exhaustive: never = driver;
|
|
62
|
+
return exhaustive;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function resolveVpnDriverConfig(
|
|
66
|
+
config: VpnConfig,
|
|
67
|
+
platform: typeof process.platform = process.platform,
|
|
68
|
+
): VpnDriverResolution {
|
|
69
|
+
if (!isSupportedPlatform(platform)) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: `VPN auto detection is not supported on ${platform}.`,
|
|
73
|
+
hint: "Configure an explicit supported VPN driver for this platform.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const platformDriver = config.drivers?.[platform];
|
|
78
|
+
if (platformDriver) {
|
|
79
|
+
return resolveExplicitDriver(config, platform, platformDriver);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (config.driver) {
|
|
83
|
+
return resolveExplicitDriver(config, platform, config.driver);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const auto = config.auto ?? true;
|
|
87
|
+
if (!auto) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: "VPN auto detection is disabled, but no driver is configured for this platform.",
|
|
91
|
+
hint: `Add vpns.<key>.drivers.${platform} or enable auto detection.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (platform === "darwin") {
|
|
96
|
+
return { success: true, driver: { platform, type: "macos-scutil", serviceName: config.name } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (platform === "linux") {
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
driver: { platform, type: "linux-nmcli", connectionName: config.name },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { success: true, driver: { platform, type: "windows-rasdial", entryName: config.name } };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function missingVpnToolHint(driver: ResolvedVpnDriver): string {
|
|
110
|
+
if (driver.type === "macos-scutil") {
|
|
111
|
+
return "scutil was not found or is unavailable. Ensure macOS system tools are available and the VPN service name matches the Network service name.";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (driver.type === "linux-nmcli") {
|
|
115
|
+
return "nmcli was not found. Install or enable NetworkManager CLI, or configure an explicit supported VPN driver.";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return "rasdial was not found or is unavailable. Ensure Windows RAS tooling is available and the VPN entry name matches the configured connection.";
|
|
119
|
+
}
|