@f5-sales-demo/xcsh 20.4.5 → 20.4.6

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/CHANGELOG.md CHANGED
@@ -16,6 +16,8 @@
16
16
 
17
17
  - Prevalidated manifest batches before mutation, preserved strict create and update semantics, rejected unsupported server dry-run, and returned automation-grade exit codes for resource operations ([#2930](https://github.com/f5-sales-demo/xcsh/issues/2930))
18
18
  - Prevented sandbox false refusals from path-like Bash and Python source text while limiting account and data containers to discovery protection and preserving xcsh-private runtime isolation ([#2931](https://github.com/f5-sales-demo/xcsh/issues/2931))
19
+ - Validated matching chat-completion POST routes during LiteLLM discovery so Open WebUI model-management endpoints cannot be mistaken for inference endpoints ([#2996](https://github.com/f5-sales-demo/xcsh/issues/2996))
20
+ - Returned non-zero status from headless text and JSON sessions when the model turn fails or is aborted, while disposing each session exactly once ([#2996](https://github.com/f5-sales-demo/xcsh/issues/2996))
19
21
 
20
22
  ## [20.3.0] - 2026-08-04
21
23
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "20.4.5",
4
+ "version": "20.4.6",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -60,13 +60,13 @@
60
60
  "dependencies": {
61
61
  "@agentclientprotocol/sdk": "1.3.0",
62
62
  "@mozilla/readability": "^0.6",
63
- "@f5-sales-demo/xcsh-stats": "20.4.5",
64
- "@f5-sales-demo/pi-agent-core": "20.4.5",
65
- "@f5-sales-demo/pi-ai": "20.4.5",
66
- "@f5-sales-demo/pi-natives": "20.4.5",
67
- "@f5-sales-demo/pi-resource-management": "20.4.5",
68
- "@f5-sales-demo/pi-tui": "20.4.5",
69
- "@f5-sales-demo/pi-utils": "20.4.5",
63
+ "@f5-sales-demo/xcsh-stats": "20.4.6",
64
+ "@f5-sales-demo/pi-agent-core": "20.4.6",
65
+ "@f5-sales-demo/pi-ai": "20.4.6",
66
+ "@f5-sales-demo/pi-natives": "20.4.6",
67
+ "@f5-sales-demo/pi-resource-management": "20.4.6",
68
+ "@f5-sales-demo/pi-tui": "20.4.6",
69
+ "@f5-sales-demo/pi-utils": "20.4.6",
70
70
  "@sinclair/typebox": "^0.34",
71
71
  "@xterm/headless": "^6.0",
72
72
  "ajv": "^8.20",
package/src/cli/args.ts CHANGED
@@ -8,6 +8,7 @@ import { parseEffort } from "../thinking";
8
8
  import {
9
9
  flagNameForChar,
10
10
  flagSpec,
11
+ LAUNCH_FLAGS,
11
12
  type LaunchFlagName,
12
13
  normalizeFlagTokens,
13
14
  takesValue,
@@ -19,9 +20,9 @@ export type Mode = "text" | "json" | "rpc" | "acp";
19
20
  export interface Args {
20
21
  cwd?: string;
21
22
  allowHome?: boolean;
22
- /** Disable the session filesystem sandbox (widen to unrestricted access). */
23
+ /** Disable the session filesystem discovery guard; OS-user permissions are unchanged. */
23
24
  noSandbox?: boolean;
24
- /** Extra directories the session may read AND write, beyond its CWD subtree (repeatable). */
25
+ /** Extra directories whose entries the session may discover (repeatable). */
25
26
  allowPath?: string[];
26
27
  provider?: string;
27
28
  context?: string;
@@ -375,8 +376,8 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
375
376
  ask - Ask user questions (interactive mode only)
376
377
 
377
378
  ${chalk.bold("Sandbox Options:")}
378
- --no-sandbox Disable session filesystem isolation (allow access outside the CWD)
379
- --allow-path <path> Grant read+write access to an extra directory (repeatable)
379
+ --no-sandbox ${LAUNCH_FLAGS["no-sandbox"].description}
380
+ --allow-path <path> ${LAUNCH_FLAGS["allow-path"].description}
380
381
 
381
382
  ${chalk.bold("Plugin Options:")}
382
383
  --plugin-dir <path> Load plugin from directory (repeatable)
@@ -53,11 +53,11 @@ export const LAUNCH_FLAGS = defineFlags({
53
53
  "allow-home": { arity: "boolean", description: "Allow starting in ~ without auto-switching to a temp dir" },
54
54
  "no-sandbox": {
55
55
  arity: "boolean",
56
- description: "Disable session filesystem isolation (allow access outside the CWD)",
56
+ description: "Disable the session filesystem discovery guard",
57
57
  },
58
58
  "allow-path": {
59
59
  arity: "repeatable-value",
60
- description: "Grant read+write access to an extra directory (repeatable)",
60
+ description: "Allow directory discovery at an additional path (repeatable)",
61
61
  },
62
62
  mode: {
63
63
  arity: "value",
@@ -312,14 +312,25 @@ export interface ProbeResult {
312
312
  apiBasePath?: string;
313
313
  }
314
314
 
315
- /** Candidate paths to probe for the models endpoint (tried in order). */
316
- const MODELS_ENDPOINT_PATHS = ["/v1/models", "/api/v1/models"];
315
+ /** Candidate OpenAI-compatible API base paths, tried in order. */
316
+ const API_BASE_PATHS = ["/v1", "/api", "/api/v1"];
317
+
318
+ /**
319
+ * A route probe deliberately omits the required model and messages. OpenAI-compatible
320
+ * servers reject that payload before inference with 400 or 422. A 2xx response is also
321
+ * accepted for permissive proxies; 404/405 and server/auth failures reject the route.
322
+ */
323
+ function acceptsChatCompletionPost(status: number): boolean {
324
+ return (status >= 200 && status < 300) || status === 400 || status === 422;
325
+ }
317
326
 
318
327
  /**
319
328
  * Probe a LiteLLM proxy to validate connectivity and discover available models.
320
329
  *
321
- * Tries multiple endpoint paths in order (/v1/models, then /api/v1/models) to
322
- * handle deployments where a frontend like Open WebUI intercepts /v1/*.
330
+ * A models GET alone is not enough: Open WebUI also exposes management endpoints such
331
+ * as /api/v1/models that do not share an inference route. Each candidate must return an
332
+ * OpenAI-shaped model catalog and accept POST at the matching /chat/completions path.
333
+ * The POST carries an empty JSON object, so validation happens before model inference.
323
334
  *
324
335
  * Returns the list of model IDs on success, or an error on failure.
325
336
  * Uses a 3-second timeout per endpoint to avoid blocking startup.
@@ -333,11 +344,11 @@ export async function probeLiteLLMConnection(
333
344
  const normalizedUrl = baseUrl.replace(/\/+$/, "");
334
345
  let lastError = "";
335
346
 
336
- for (const endpointPath of MODELS_ENDPOINT_PATHS) {
337
- const url = `${normalizedUrl}${endpointPath}`;
347
+ for (const apiBasePath of API_BASE_PATHS) {
348
+ const modelsUrl = `${normalizedUrl}${apiBasePath}/models`;
338
349
  let response: Response;
339
350
  try {
340
- response = await fetchImpl(url, {
351
+ response = await fetchImpl(modelsUrl, {
341
352
  method: "GET",
342
353
  headers: {
343
354
  Accept: "application/json",
@@ -351,7 +362,7 @@ export async function probeLiteLLMConnection(
351
362
  }
352
363
 
353
364
  if (!response.ok) {
354
- lastError = `HTTP ${response.status} ${response.statusText} from ${url}`;
365
+ lastError = `HTTP ${response.status} ${response.statusText} from ${modelsUrl}`;
355
366
  continue;
356
367
  }
357
368
 
@@ -359,7 +370,7 @@ export async function probeLiteLLMConnection(
359
370
  try {
360
371
  payload = await response.json();
361
372
  } catch {
362
- lastError = `Non-JSON response from ${url}`;
373
+ lastError = `Non-JSON response from ${modelsUrl}`;
363
374
  continue;
364
375
  }
365
376
 
@@ -378,13 +389,35 @@ export async function probeLiteLLMConnection(
378
389
  }
379
390
  }
380
391
 
381
- if (models.length > 0) {
382
- // Derive the API base path from the endpoint that worked
383
- const apiBasePath = endpointPath.replace(/\/models$/, "");
384
- return { reachable: true, models, apiBasePath };
392
+ if (models.length === 0) {
393
+ lastError = `No models in response from ${modelsUrl}`;
394
+ continue;
395
+ }
396
+
397
+ const chatUrl = `${normalizedUrl}${apiBasePath}/chat/completions`;
398
+ let chatResponse: Response;
399
+ try {
400
+ chatResponse = await fetchImpl(chatUrl, {
401
+ method: "POST",
402
+ headers: {
403
+ Accept: "application/json",
404
+ Authorization: `Bearer ${apiKey}`,
405
+ "Content-Type": "application/json",
406
+ },
407
+ body: "{}",
408
+ signal: options?.signal ?? AbortSignal.timeout(3000),
409
+ });
410
+ } catch (err) {
411
+ lastError = err instanceof Error ? err.message : String(err);
412
+ continue;
413
+ }
414
+ await chatResponse.body?.cancel();
415
+ if (!acceptsChatCompletionPost(chatResponse.status)) {
416
+ lastError = `HTTP ${chatResponse.status} ${chatResponse.statusText} from ${chatUrl}`;
417
+ continue;
385
418
  }
386
419
 
387
- lastError = `No models in response from ${url}`;
420
+ return { reachable: true, models, apiBasePath };
388
421
  }
389
422
 
390
423
  return { reachable: false, models: [], error: lastError };
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "20.4.5",
21
- "commit": "0ccf19916f316fb84d06bd9a5a9ab36bd67d8759",
22
- "shortCommit": "0ccf199",
20
+ "version": "20.4.6",
21
+ "commit": "ef5e9295b8874494b812b1cc3c04aff523bd679e",
22
+ "shortCommit": "ef5e929",
23
23
  "branch": "main",
24
- "tag": "v20.4.5",
25
- "commitDate": "2026-08-05T09:48:31Z",
26
- "buildDate": "2026-08-05T10:16:36.684Z",
24
+ "tag": "v20.4.6",
25
+ "commitDate": "2026-08-05T18:21:54Z",
26
+ "buildDate": "2026-08-05T18:50:12.864Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5-sales-demo/xcsh",
30
30
  "repoSlug": "f5-sales-demo/xcsh",
31
- "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/0ccf19916f316fb84d06bd9a5a9ab36bd67d8759",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.4.5"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/ef5e9295b8874494b812b1cc3c04aff523bd679e",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.4.6"
33
33
  };
@@ -2,10 +2,10 @@
2
2
 
3
3
  import type { ConsoleCatalogData } from "./console-catalog-types";
4
4
 
5
- export const CONSOLE_CATALOG_VERSION = "ff8ed12d3f91d2ba0b3970c6fc7105fc75d7952d";
5
+ export const CONSOLE_CATALOG_VERSION = "5f41f84da613e409e89dd37fd5380a181cce95aa";
6
6
 
7
7
  export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
8
- version: "ff8ed12d3f91d2ba0b3970c6fc7105fc75d7952d",
8
+ version: "5f41f84da613e409e89dd37fd5380a181cce95aa",
9
9
  workflows: {
10
10
  "address-allocator/create":
11
11
  '---\nschema: urn:xcsh:console:workflow:v1\nid: address-allocator-create\nlabel: Create IP Address Allocators\nresource: address-allocator\noperation: create\npreconditions:\n - user_logged_in\n - "role_minimum: admin"\nparams:\n name:\n required: true\n description: IP Address Allocators name (lowercase alphanumeric and hyphens)\n example: example-address-allocator\n address_allocator_mode:\n required: true\n description: Address Allocator Mode\n allocation_unit:\n required: false\n description: Allocation Unit\n default: 0\n address_pool:\n required: false\n description: Address Pool\n default: value\n address_allocation_scheme:\n required: false\n description: "Server-required: Field should be not nil"\n default: value\nsteps:\n - id: navigate-to-list\n action: navigate\n url: /web/workspaces/multi-cloud-network-connect/manage/networking/legacy_network_configuration/address_allocators\n wait_for: text(\'IP Address Allocators\')\n description: Navigate to IP Address Allocators list page\n - id: click-add-tab\n action: click\n selector: text(\'Add IP Address Allocator\')\n wait_for: textbox[name=\'Name\']\n description: Click Add IP Address Allocator to open the create form\n - id: fill-name\n action: fill\n selector: textbox[name=\'Name\']\n value: "{name}"\n description: Enter Name\n - id: select-address_allocator_mode\n action: select\n selector: listbox\n context: Address Allocator Mode section\n value: "{address_allocator_mode}"\n description: Select Address Allocator Mode\n - id: fill-allocation_unit\n action: fill\n selector: spinbutton[name=\'Allocation Unit\']\n value: "{allocation_unit}"\n description: Set Allocation Unit\n - id: fill-address_pool\n action: fill\n selector: ngx-datatable input.form-control\n context: Address Pool table\n value: "{address_pool}"\n description: Enter Address Pool in the existing table row (no Add Item needed — the table ships one empty row)\n - id: select-address_allocation_scheme\n action: select\n selector: listbox\n context: Address Allocation Scheme section\n value: "{address_allocation_scheme}"\n description: Select Address Allocation Scheme\n - id: save\n action: click\n selector: "[class*=\'save-bt\'],[class*=\'submit-button\']"\n context: footer\n wait_for: text(\'{name}\')\n wait_timeout_ms: 30000\n description: Save/submit the form (union selector matches save-bt OR submit-button)\npostconditions:\n - resource_list_page_visible\n - "resource_name_in_list: {name}"\nmetadata:\n confidence: inferred\n discovered_at: 2026-06-24\n console_version: "2025.06"\n notes: Auto-generated by scripts/generate-workflows.ts from api-specs-enriched field metadata.\n',