@f5-sales-demo/xcsh 19.98.4 → 19.98.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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.98.
|
|
4
|
+
"version": "19.98.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",
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
59
59
|
"@mozilla/readability": "^0.6",
|
|
60
|
-
"@f5-sales-demo/xcsh-stats": "19.98.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "19.98.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "19.98.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "19.98.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "19.98.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "19.98.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "19.98.
|
|
60
|
+
"@f5-sales-demo/xcsh-stats": "19.98.6",
|
|
61
|
+
"@f5-sales-demo/pi-agent-core": "19.98.6",
|
|
62
|
+
"@f5-sales-demo/pi-ai": "19.98.6",
|
|
63
|
+
"@f5-sales-demo/pi-natives": "19.98.6",
|
|
64
|
+
"@f5-sales-demo/pi-resource-management": "19.98.6",
|
|
65
|
+
"@f5-sales-demo/pi-tui": "19.98.6",
|
|
66
|
+
"@f5-sales-demo/pi-utils": "19.98.6",
|
|
67
67
|
"@sinclair/typebox": "^0.34",
|
|
68
68
|
"@xterm/headless": "^6.0",
|
|
69
69
|
"ajv": "^8.20",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.98.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.98.6",
|
|
21
|
+
"commit": "9a4b5f250189b2d423b62f15f02c2403c070fb3d",
|
|
22
|
+
"shortCommit": "9a4b5f2",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.98.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.98.6",
|
|
25
|
+
"commitDate": "2026-07-28T11:16:35Z",
|
|
26
|
+
"buildDate": "2026-07-28T11:40:19.737Z",
|
|
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/v19.98.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/9a4b5f250189b2d423b62f15f02c2403c070fb3d",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.98.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 = "9eb4d3c57d88afbc41edb304b88ef3754301bc62";
|
|
6
6
|
|
|
7
7
|
export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
8
|
-
version: "
|
|
8
|
+
version: "9eb4d3c57d88afbc41edb304b88ef3754301bc62",
|
|
9
9
|
workflows: {
|
|
10
10
|
"address-allocator/create":
|
|
11
11
|
'schema: 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 - namespace_selected\n - "role_minimum: admin"\nparams:\n namespace:\n required: true\n description: Target namespace\n example: demo\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',
|
package/src/sandbox/enforce.ts
CHANGED
|
@@ -151,6 +151,21 @@ function deny(policy: SandboxPolicy, resolved: string, access: SandboxAccess): T
|
|
|
151
151
|
return { block: true, reason: policy.describe(resolved, access) };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* `$HOME` and `${HOME}` are spellings of `~`, which `looksLikePath` already treats as a
|
|
156
|
+
* path (#2534). Three ways to name one file, only one of them checked, is an oversight
|
|
157
|
+
* rather than a policy: `cat ~/.ssh/id_rsa` was blocked while `cat $HOME/.ssh/id_rsa`
|
|
158
|
+
* was not. Rewriting to `~` here means detection AND resolution both see the real path,
|
|
159
|
+
* so the denial names the file rather than a literal dollar sign.
|
|
160
|
+
*
|
|
161
|
+
* `\b` keeps `$HOMEBREW_PREFIX` and friends out of it.
|
|
162
|
+
*/
|
|
163
|
+
const HOME_EXPANSION = /^\$(?:HOME\b|\{HOME\})/;
|
|
164
|
+
|
|
165
|
+
function normalizeHomeExpansion(token: string): string {
|
|
166
|
+
return HOME_EXPANSION.test(token) ? token.replace(HOME_EXPANSION, "~") : token;
|
|
167
|
+
}
|
|
168
|
+
|
|
154
169
|
function looksLikePath(token: string): boolean {
|
|
155
170
|
return path.isAbsolute(token) || token.startsWith("~") || /(^|[/\\])\.\.([/\\]|$)/.test(token);
|
|
156
171
|
}
|
|
@@ -224,7 +239,8 @@ const OPTION_VALUE_FORMS: readonly RegExp[] = [
|
|
|
224
239
|
*/
|
|
225
240
|
function codePathOccurrences(command: string): PathOccurrence[] {
|
|
226
241
|
const found: PathOccurrence[] = [];
|
|
227
|
-
const add = (
|
|
242
|
+
const add = (raw: string, at: number, access?: SandboxAccess): void => {
|
|
243
|
+
const token = normalizeHomeExpansion(raw);
|
|
228
244
|
if (token.length > 0 && looksLikePath(token)) found.push({ token, at, access });
|
|
229
245
|
};
|
|
230
246
|
// Set when the previous token was nothing but a redirection operator, so this one is its operand.
|
|
@@ -306,11 +322,26 @@ function codePathCandidates(command: string): PathCandidate[] {
|
|
|
306
322
|
* `-exec` run — is not blanked, so it is still scanned. When the command cannot be lexed
|
|
307
323
|
* confidently, the floor stands alone.
|
|
308
324
|
*/
|
|
309
|
-
|
|
325
|
+
/**
|
|
326
|
+
* A redirect target the shell will certainly open, whose path cannot be resolved here —
|
|
327
|
+
* `printf x >"$TARGET"`. Unlike an operand, there is no reading under which this is data
|
|
328
|
+
* rather than a file, so it cannot be waved through (#2534).
|
|
329
|
+
*/
|
|
330
|
+
interface UnresolvableTarget {
|
|
331
|
+
text: string;
|
|
332
|
+
access: SandboxAccess;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface ShellScan {
|
|
336
|
+
candidates: PathCandidate[];
|
|
337
|
+
unresolvable: UnresolvableTarget[];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function shellPathCandidates(command: string): ShellScan {
|
|
310
341
|
const lexed = lexShellCommand(command);
|
|
311
342
|
// Unbalanced quotes mean every word boundary is a guess: neither the blanking nor the write
|
|
312
343
|
// marking below can be trusted, so fall back to the floor, checked as reads.
|
|
313
|
-
if (lexed.unterminated) return codePathCandidates(command);
|
|
344
|
+
if (lexed.unterminated) return { candidates: codePathCandidates(command), unresolvable: [] };
|
|
314
345
|
|
|
315
346
|
const exemptSpans = lexed.commands.flatMap(simpleCommand => provenExemptWords(simpleCommand));
|
|
316
347
|
let scanned = command;
|
|
@@ -335,13 +366,19 @@ function shellPathCandidates(command: string): PathCandidate[] {
|
|
|
335
366
|
// Not gated on `looksLikePath`: that test is for the floor's *guesses* about which fragments of a
|
|
336
367
|
// command might be filenames. A redirect target is one the shell will certainly open, so a bare
|
|
337
368
|
// `out.txt` is checked too — it resolves under the cwd, which a read-only cwd does not license.
|
|
369
|
+
const unresolvable: UnresolvableTarget[] = [];
|
|
338
370
|
for (const word of lexed.words) {
|
|
339
371
|
if (word.redirect === undefined || word.redirect === "here-string") continue;
|
|
340
372
|
for (const access of word.redirect === "read-write" ? WRITE_AND_READ : [word.redirect]) {
|
|
341
|
-
|
|
373
|
+
// `literal` is false when the word carries `$VAR`, a substitution, a glob or a brace
|
|
374
|
+
// expansion, so `text` is not a stand-in for one filesystem reference. Resolving it
|
|
375
|
+
// anyway is what let `>"$TARGET"` through: it became the literal string `$TARGET`
|
|
376
|
+
// under the cwd, which the boundary happily allowed.
|
|
377
|
+
if (word.literal) candidates.push({ token: word.text, access });
|
|
378
|
+
else unresolvable.push({ text: word.text, access });
|
|
342
379
|
}
|
|
343
380
|
}
|
|
344
|
-
return candidates;
|
|
381
|
+
return { candidates, unresolvable };
|
|
345
382
|
}
|
|
346
383
|
|
|
347
384
|
/** Base directories a search input would actually search, split like the tools do. */
|
|
@@ -383,8 +420,25 @@ function evaluateCodeTool(check: ToolCallCheck, fields: string[], shell: boolean
|
|
|
383
420
|
// Only bash gets the shell-aware treatment. Python is not shell: lexing `open('/x')` as
|
|
384
421
|
// shell yields one non-absolute word and would lose the check entirely, and it has no
|
|
385
422
|
// redirects, so every candidate it produces is a read.
|
|
423
|
+
const scan: ShellScan = shell
|
|
424
|
+
? shellPathCandidates(command)
|
|
425
|
+
: { candidates: codePathCandidates(command), unresolvable: [] };
|
|
426
|
+
// Refused before any candidate is resolved: there is no path to check, and that is
|
|
427
|
+
// precisely the problem. The residual this does NOT cover is an expansion in an
|
|
428
|
+
// operand rather than a redirect target — `cat "$SECRET"` — which cannot be resolved
|
|
429
|
+
// at this layer at all. That is the Phase 2 OS-sandbox's job; do not read the text
|
|
430
|
+
// boundary as complete (#2534).
|
|
431
|
+
for (const target of scan.unresolvable) {
|
|
432
|
+
return {
|
|
433
|
+
block: true,
|
|
434
|
+
reason:
|
|
435
|
+
`sandbox: refusing to ${target.access} a redirect target this layer cannot resolve: ` +
|
|
436
|
+
`${target.text}. The shell will open it, but its path comes from an expansion, so the ` +
|
|
437
|
+
"boundary cannot be checked. Write to an explicit path, or use --allow-path.",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
386
440
|
const seen = new Set<string>();
|
|
387
|
-
for (const { token, access } of
|
|
441
|
+
for (const { token, access } of scan.candidates) {
|
|
388
442
|
if (!seen.add(`${access}\0${token}`)) continue;
|
|
389
443
|
const resolved = resolveToCwd(token, base);
|
|
390
444
|
if (policy.isAllowed(resolved, access)) continue;
|