@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",
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.4",
61
- "@f5-sales-demo/pi-agent-core": "19.98.4",
62
- "@f5-sales-demo/pi-ai": "19.98.4",
63
- "@f5-sales-demo/pi-natives": "19.98.4",
64
- "@f5-sales-demo/pi-resource-management": "19.98.4",
65
- "@f5-sales-demo/pi-tui": "19.98.4",
66
- "@f5-sales-demo/pi-utils": "19.98.4",
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.4",
21
- "commit": "c049a0c4d91d1246ef39e74b2aaad17839c240e1",
22
- "shortCommit": "c049a0c",
20
+ "version": "19.98.6",
21
+ "commit": "9a4b5f250189b2d423b62f15f02c2403c070fb3d",
22
+ "shortCommit": "9a4b5f2",
23
23
  "branch": "main",
24
- "tag": "v19.98.4",
25
- "commitDate": "2026-07-28T09:47:36Z",
26
- "buildDate": "2026-07-28T10:15:07.707Z",
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/c049a0c4d91d1246ef39e74b2aaad17839c240e1",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.98.4"
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 = "6bc37113b222827033ee81fc8f009344ca11058b";
5
+ export const CONSOLE_CATALOG_VERSION = "9eb4d3c57d88afbc41edb304b88ef3754301bc62";
6
6
 
7
7
  export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
8
- version: "6bc37113b222827033ee81fc8f009344ca11058b",
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',
@@ -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 = (token: string, at: number, access?: SandboxAccess): void => {
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
- function shellPathCandidates(command: string): PathCandidate[] {
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
- candidates.push({ token: word.text, access });
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 shell ? shellPathCandidates(command) : codePathCandidates(command)) {
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;