@f5-sales-demo/xcsh 20.4.4 → 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 +2 -0
- package/package.json +8 -8
- package/src/browser/extension-bridge-tools.ts +5 -3
- package/src/browser/headless-bridge.ts +1 -1
- package/src/cli/args.ts +5 -4
- package/src/cli/flag-spec.ts +2 -2
- package/src/config/auto-config.ts +50 -14
- package/src/config/model-registry.ts +6 -0
- package/src/config/settings-schema.ts +4 -4
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/console-catalog.generated.ts +2 -2
- package/src/internal-urls/terraform-index.generated.ts +1505 -1107
- package/src/main.ts +2 -2
- package/src/modes/print-mode.ts +11 -16
- package/src/prompts/system/workspace-boundary.md +11 -0
- package/src/prompts/tools/task-summary.md +5 -1
- package/src/sandbox/enforce.ts +12 -2
- package/src/task/index.ts +4 -36
- package/src/task/summary.ts +53 -0
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.
|
|
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.
|
|
64
|
-
"@f5-sales-demo/pi-agent-core": "20.4.
|
|
65
|
-
"@f5-sales-demo/pi-ai": "20.4.
|
|
66
|
-
"@f5-sales-demo/pi-natives": "20.4.
|
|
67
|
-
"@f5-sales-demo/pi-resource-management": "20.4.
|
|
68
|
-
"@f5-sales-demo/pi-tui": "20.4.
|
|
69
|
-
"@f5-sales-demo/pi-utils": "20.4.
|
|
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",
|
|
@@ -104,9 +104,10 @@ export const BROWSER_TOOL_NAMES: readonly string[] = [
|
|
|
104
104
|
* local xcsh agent, so it gets the same general-purpose native tools the CLI has —
|
|
105
105
|
* `bash` (so it can shell out to `az`, `gh`, terraform, git, …), the file tools
|
|
106
106
|
* (`read`/`write`/`edit`), search (`grep`), plus planning (`todo_write`,
|
|
107
|
-
* `task`) and `
|
|
108
|
-
* tool is omitted because its name collides with a
|
|
109
|
-
*
|
|
107
|
+
* `task`), `calc`, and image analysis (`inspect_image`). (File-finding is covered
|
|
108
|
+
* by `bash`; the builtin `find` tool is omitted because its name collides with a
|
|
109
|
+
* browser tool.) The document's own Excel/PowerPoint/Word tools arrive at runtime
|
|
110
|
+
* over the bridge via `set_host_tools`.
|
|
110
111
|
*
|
|
111
112
|
* DELIBERATELY EXCLUDED:
|
|
112
113
|
* - Every {@link BROWSER_TOOL_NAMES} entry — there is no browser to drive in a
|
|
@@ -143,4 +144,5 @@ export const OFFICE_TOOL_NAMES: readonly string[] = [
|
|
|
143
144
|
"todo_write",
|
|
144
145
|
"task",
|
|
145
146
|
"calc",
|
|
147
|
+
"inspect_image",
|
|
146
148
|
];
|
|
@@ -142,7 +142,7 @@ export async function startHeadlessChatBridge(deps: HeadlessBridgeDeps = default
|
|
|
142
142
|
// the rethrow as a non-fatal "pane only" fallback.
|
|
143
143
|
try {
|
|
144
144
|
// Create ONE headless Office session with the full CLI-parity builtin set
|
|
145
|
-
// (OFFICE_TOOL_NAMES: bash/read/write/edit/grep/
|
|
145
|
+
// (OFFICE_TOOL_NAMES: bash/read/write/edit/grep/inspect_image/… — NO browser tools, which
|
|
146
146
|
// would be hallucinated in a document task pane). The document's own tools
|
|
147
147
|
// (Excel/Word/PowerPoint) arrive at runtime via set_host_tools.
|
|
148
148
|
const { session } = await deps.createAgentSession({
|
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
|
|
23
|
+
/** Disable the session filesystem discovery guard; OS-user permissions are unchanged. */
|
|
23
24
|
noSandbox?: boolean;
|
|
24
|
-
/** Extra directories the session may
|
|
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
|
|
379
|
-
--allow-path <path>
|
|
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)
|
package/src/cli/flag-spec.ts
CHANGED
|
@@ -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
|
|
56
|
+
description: "Disable the session filesystem discovery guard",
|
|
57
57
|
},
|
|
58
58
|
"allow-path": {
|
|
59
59
|
arity: "repeatable-value",
|
|
60
|
-
description: "
|
|
60
|
+
description: "Allow directory discovery at an additional path (repeatable)",
|
|
61
61
|
},
|
|
62
62
|
mode: {
|
|
63
63
|
arity: "value",
|
|
@@ -81,6 +81,9 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
|
|
|
81
81
|
" modelOverrides:",
|
|
82
82
|
" gpt-5.6-sol:",
|
|
83
83
|
" reasoning: true",
|
|
84
|
+
" input:",
|
|
85
|
+
" - text",
|
|
86
|
+
" - image",
|
|
84
87
|
" thinking:",
|
|
85
88
|
" mode: effort",
|
|
86
89
|
" minLevel: low",
|
|
@@ -309,14 +312,25 @@ export interface ProbeResult {
|
|
|
309
312
|
apiBasePath?: string;
|
|
310
313
|
}
|
|
311
314
|
|
|
312
|
-
/** Candidate
|
|
313
|
-
const
|
|
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
|
+
}
|
|
314
326
|
|
|
315
327
|
/**
|
|
316
328
|
* Probe a LiteLLM proxy to validate connectivity and discover available models.
|
|
317
329
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
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.
|
|
320
334
|
*
|
|
321
335
|
* Returns the list of model IDs on success, or an error on failure.
|
|
322
336
|
* Uses a 3-second timeout per endpoint to avoid blocking startup.
|
|
@@ -330,11 +344,11 @@ export async function probeLiteLLMConnection(
|
|
|
330
344
|
const normalizedUrl = baseUrl.replace(/\/+$/, "");
|
|
331
345
|
let lastError = "";
|
|
332
346
|
|
|
333
|
-
for (const
|
|
334
|
-
const
|
|
347
|
+
for (const apiBasePath of API_BASE_PATHS) {
|
|
348
|
+
const modelsUrl = `${normalizedUrl}${apiBasePath}/models`;
|
|
335
349
|
let response: Response;
|
|
336
350
|
try {
|
|
337
|
-
response = await fetchImpl(
|
|
351
|
+
response = await fetchImpl(modelsUrl, {
|
|
338
352
|
method: "GET",
|
|
339
353
|
headers: {
|
|
340
354
|
Accept: "application/json",
|
|
@@ -348,7 +362,7 @@ export async function probeLiteLLMConnection(
|
|
|
348
362
|
}
|
|
349
363
|
|
|
350
364
|
if (!response.ok) {
|
|
351
|
-
lastError = `HTTP ${response.status} ${response.statusText} from ${
|
|
365
|
+
lastError = `HTTP ${response.status} ${response.statusText} from ${modelsUrl}`;
|
|
352
366
|
continue;
|
|
353
367
|
}
|
|
354
368
|
|
|
@@ -356,7 +370,7 @@ export async function probeLiteLLMConnection(
|
|
|
356
370
|
try {
|
|
357
371
|
payload = await response.json();
|
|
358
372
|
} catch {
|
|
359
|
-
lastError = `Non-JSON response from ${
|
|
373
|
+
lastError = `Non-JSON response from ${modelsUrl}`;
|
|
360
374
|
continue;
|
|
361
375
|
}
|
|
362
376
|
|
|
@@ -375,13 +389,35 @@ export async function probeLiteLLMConnection(
|
|
|
375
389
|
}
|
|
376
390
|
}
|
|
377
391
|
|
|
378
|
-
if (models.length
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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;
|
|
382
418
|
}
|
|
383
419
|
|
|
384
|
-
|
|
420
|
+
return { reachable: true, models, apiBasePath };
|
|
385
421
|
}
|
|
386
422
|
|
|
387
423
|
return { reachable: false, models: [], error: lastError };
|
|
@@ -2,6 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import {
|
|
3
3
|
type Api,
|
|
4
4
|
type AssistantMessageEventStream,
|
|
5
|
+
applyGeneratedModelPolicies,
|
|
5
6
|
type Context,
|
|
6
7
|
createModelManager,
|
|
7
8
|
DEFAULT_LOCAL_TOKEN,
|
|
@@ -1227,6 +1228,7 @@ export class ModelRegistry {
|
|
|
1227
1228
|
return {
|
|
1228
1229
|
...model,
|
|
1229
1230
|
baseUrl: existing.baseUrl,
|
|
1231
|
+
input: existing.input.includes("image") ? ["text", "image"] : model.input,
|
|
1230
1232
|
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
|
1231
1233
|
};
|
|
1232
1234
|
}
|
|
@@ -1810,6 +1812,10 @@ export class ModelRegistry {
|
|
|
1810
1812
|
});
|
|
1811
1813
|
}
|
|
1812
1814
|
#applyHardcodedModelPolicies(models: Model<Api>[]): Model<Api>[] {
|
|
1815
|
+
// Discovery caches contain provider-advertised metadata, which can lag the
|
|
1816
|
+
// bundled corrections. Reapply generated-model policy before a cached entry
|
|
1817
|
+
// replaces its bundled peer; explicit user overrides still run afterward.
|
|
1818
|
+
applyGeneratedModelPolicies(models);
|
|
1813
1819
|
return models.map(model => {
|
|
1814
1820
|
if (model.id !== "gpt-5.4" || model.provider === "github-copilot") {
|
|
1815
1821
|
return model;
|
|
@@ -157,16 +157,16 @@ const DEFAULT_CYCLE_ORDER: string[] = ["smol", "default", "slow"];
|
|
|
157
157
|
/**
|
|
158
158
|
* Binary-baked default model role. Ships in the binary so a fresh install needs
|
|
159
159
|
* NO `~/.xcsh/agent/config.yml` — `/login` only supplies the (PII) proxy URL + key.
|
|
160
|
-
*
|
|
161
|
-
*
|
|
160
|
+
* GPT-5.6 Sol High is the vision-capable production default through the OpenAI
|
|
161
|
+
* Chat Completions route on LiteLLM. Keep the effort explicit in the role so returning
|
|
162
162
|
* from a lower-effort role cannot inherit that role's effort.
|
|
163
163
|
*/
|
|
164
|
-
export const DEFAULT_MODEL_ROLE = "
|
|
164
|
+
export const DEFAULT_MODEL_ROLE = "litellm/gpt-5.6-sol:high";
|
|
165
165
|
/** Fast role for lightweight work (commit messages, titles, memory summaries). */
|
|
166
166
|
const SMOL_MODEL_ROLE = "litellm/gpt-5.6-sol:low";
|
|
167
167
|
/**
|
|
168
168
|
* Baked role map. `smol` keeps GPT-5.6 Sol at low effort for latency-sensitive
|
|
169
|
-
* work; both `default` and `slow` restore vision-capable
|
|
169
|
+
* work; both `default` and `slow` restore vision-capable GPT-5.6 Sol High.
|
|
170
170
|
*/
|
|
171
171
|
const DEFAULT_MODEL_ROLES: Record<string, string> = {
|
|
172
172
|
default: DEFAULT_MODEL_ROLE,
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "20.4.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.4.6",
|
|
21
|
+
"commit": "ef5e9295b8874494b812b1cc3c04aff523bd679e",
|
|
22
|
+
"shortCommit": "ef5e929",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.4.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.4.
|
|
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 = "
|
|
5
|
+
export const CONSOLE_CATALOG_VERSION = "5f41f84da613e409e89dd37fd5380a181cce95aa";
|
|
6
6
|
|
|
7
7
|
export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
8
|
-
version: "
|
|
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',
|