@blogic-cz/agent-tools 0.14.38 → 0.14.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.14.38",
3
+ "version": "0.14.40",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -112,6 +112,10 @@
112
112
  "./audit": {
113
113
  "types": "./dist/shared/audit.d.ts",
114
114
  "default": "./src/shared/audit.ts"
115
+ },
116
+ "./k8s-probe": {
117
+ "types": "./dist/shared/k8s-probe.d.ts",
118
+ "default": "./src/shared/k8s-probe.ts"
115
119
  }
116
120
  },
117
121
  "publishConfig": {
@@ -80,6 +80,7 @@ const K8sConfigSchema = Schema.Struct({
80
80
  clusterId: Schema.String,
81
81
  namespaces: Schema.Record(Schema.String, Schema.String),
82
82
  timeoutMs: Schema.optionalKey(Schema.Number),
83
+ apiProbeTimeoutMs: Schema.optionalKey(Schema.Number),
83
84
  prerequisites: Schema.optionalKey(PrerequisitesSchema),
84
85
  vpn: Schema.optionalKey(Schema.String),
85
86
  });
@@ -71,6 +71,13 @@ export type K8sConfig = ProfilePrerequisites & {
71
71
  /** Named namespaces, e.g. { test: "my-app-test", prod: "my-app-prod" } */
72
72
  namespaces: Record<string, string>;
73
73
  timeoutMs?: number;
74
+ /**
75
+ * Timeout in milliseconds for the cheap Kubernetes API-server reachability probe run
76
+ * before each command. When the cluster API is unreachable (VPN down / off the office
77
+ * network, or the API degraded) a real kubectl command hangs until `timeoutMs`; the
78
+ * probe lets it fail fast with a clear message instead. Set to 0 to disable. Defaults to 2000.
79
+ */
80
+ apiProbeTimeoutMs?: number;
74
81
  };
75
82
 
76
83
  /** Single database environment connection details */
@@ -8,6 +8,7 @@ import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
8
8
  import { resolveEnvTemplate } from "#shared/env-template";
9
9
  import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
10
10
  import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
11
+ import { buildApiProbeArgs } from "#shared/k8s-probe";
11
12
  import { DbConfigService, DbConfigServiceLayer, TUNNEL_CHECK_INTERVAL_MS } from "./config-service";
12
13
  import {
13
14
  DbConnectionError,
@@ -51,26 +52,8 @@ export function resolveDbAccessMode(
51
52
  };
52
53
  }
53
54
 
54
- /**
55
- * Build the kubectl args for the lightweight API-server reachability probe.
56
- * Hitting `/version` via `--raw` is the cheapest authenticated round-trip; `--request-timeout`
57
- * bounds it so an unreachable server (VPN down, off the office network) fails fast instead of
58
- * letting a subsequent `kubectl port-forward` hang on a silent TCP connect.
59
- */
60
- export function buildApiProbeArgs(
61
- kubeconfig: string | undefined,
62
- context: string,
63
- timeoutMs: number,
64
- ): string[] {
65
- return [
66
- ...(kubeconfig ? ["--kubeconfig", kubeconfig] : []),
67
- "--context",
68
- context,
69
- "get",
70
- "--raw=/version",
71
- `--request-timeout=${timeoutMs}ms`,
72
- ];
73
- }
55
+ // Re-exported from the shared probe so existing `#db/service` consumers keep working.
56
+ export { buildApiProbeArgs } from "#shared/k8s-probe";
74
57
 
75
58
  export class DbService extends Context.Service<
76
59
  DbService,
@@ -15,6 +15,7 @@ import { collectProcessOutput, quoteShellArg } from "#shared/exec";
15
15
  import { resolveEnvTemplate } from "#shared/env-template";
16
16
  import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
17
17
  import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
18
+ import { buildApiProbeArgs } from "#shared/k8s-probe";
18
19
  import { isKubectlCommandAllowed } from "./security";
19
20
 
20
21
  export class K8sService extends Context.Service<
@@ -146,6 +147,40 @@ export class K8sService extends Context.Service<
146
147
  ),
147
148
  );
148
149
 
150
+ /**
151
+ * Cheap pre-flight: is the cluster API reachable right now? Returns true when the probe
152
+ * is disabled (apiProbeTimeoutMs <= 0) so behaviour is unchanged unless a probe can run.
153
+ * A false result lets the command fail fast instead of hanging until the full timeoutMs.
154
+ */
155
+ const probeApiReachable = (
156
+ context: string,
157
+ kubeconfig: string | undefined,
158
+ timeoutMs: number,
159
+ ) =>
160
+ Effect.gen(function* () {
161
+ if (timeoutMs <= 0) {
162
+ return true;
163
+ }
164
+
165
+ const probe = ChildProcess.make(
166
+ "kubectl",
167
+ buildApiProbeArgs(kubeconfig, context, timeoutMs),
168
+ {
169
+ stdout: "pipe",
170
+ stderr: "pipe",
171
+ },
172
+ );
173
+
174
+ const result = yield* Effect.scoped(
175
+ Effect.gen(function* () {
176
+ const process = yield* executor.spawn(probe);
177
+ return yield* collectProcessOutput(process);
178
+ }),
179
+ ).pipe(Effect.catch(() => Effect.succeed({ stdout: "", stderr: "", exitCode: 1 })));
180
+
181
+ return result.exitCode === 0;
182
+ });
183
+
149
184
  const resolveContext = Effect.fn("K8sService.resolveContext")(function* (
150
185
  profile: string | undefined,
151
186
  k8sConfig: K8sConfig,
@@ -223,12 +258,23 @@ export class K8sService extends Context.Service<
223
258
  ) {
224
259
  const k8sConfig = yield* requireK8sConfig(profile);
225
260
  const timeoutMs = k8sConfig.timeoutMs ?? 60000;
261
+ const apiProbeTimeoutMs = k8sConfig.apiProbeTimeoutMs ?? 2000;
226
262
  return yield* runWithProfilePrerequisites(
227
263
  config ?? {},
228
264
  k8sConfig,
229
265
  runPrerequisiteCommand,
230
266
  Effect.gen(function* () {
231
267
  const { context, kubeconfig } = yield* resolveContext(profile, k8sConfig);
268
+
269
+ const reachable = yield* probeApiReachable(context, kubeconfig, apiProbeTimeoutMs);
270
+ if (!reachable) {
271
+ return yield* new K8sContextError({
272
+ message: `Kubernetes API server (${k8sConfig.clusterId}) not reachable within ${apiProbeTimeoutMs}ms. VPN likely not connected, or the cluster API is degraded.`,
273
+ clusterId: k8sConfig.clusterId,
274
+ hint: "Check the VPN connection and cluster health, then retry. Set kubernetes.apiProbeTimeoutMs to 0 in agent-tools.json5 to disable this pre-flight probe.",
275
+ });
276
+ }
277
+
232
278
  const fullCommand = withKubeconfig(`kubectl --context ${context} ${cmd}`, kubeconfig);
233
279
 
234
280
  const resultOption = yield* runShellCommand(fullCommand, timeoutMs);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared kubectl API-server reachability probe, used by every VPN-gated tool
3
+ * (db-tool, k8s-tool) to fail fast instead of hanging on a silently unreachable cluster.
4
+ *
5
+ * Hitting `/version` via `--raw` is the cheapest authenticated round-trip; `--request-timeout`
6
+ * bounds it so an unreachable server (VPN down / off the office network, or the cluster API
7
+ * degraded) fails in ~`timeoutMs` with a clear message instead of waiting out the tool's full
8
+ * command/tunnel timeout.
9
+ */
10
+ export function buildApiProbeArgs(
11
+ kubeconfig: string | undefined,
12
+ context: string,
13
+ timeoutMs: number,
14
+ ): string[] {
15
+ return [
16
+ ...(kubeconfig ? ["--kubeconfig", kubeconfig] : []),
17
+ "--context",
18
+ context,
19
+ "get",
20
+ "--raw=/version",
21
+ `--request-timeout=${timeoutMs}ms`,
22
+ ];
23
+ }