@f5-sales-demo/xcsh 20.0.3 → 20.0.4

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [20.0.4] - 2026-08-01
6
+
7
+ ### Fixed
8
+
9
+ - Reset model bash tool calls to the session working directory so a command-local `cd` cannot silently relocate later calls ([#2724](https://github.com/f5-sales-demo/xcsh/issues/2724))
10
+
5
11
  ## [20.0.3] - 2026-08-01
6
12
 
7
13
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/xcsh",
4
- "version": "20.0.3",
4
+ "version": "20.0.4",
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": "20.0.3",
61
- "@f5-sales-demo/pi-agent-core": "20.0.3",
62
- "@f5-sales-demo/pi-ai": "20.0.3",
63
- "@f5-sales-demo/pi-natives": "20.0.3",
64
- "@f5-sales-demo/pi-resource-management": "20.0.3",
65
- "@f5-sales-demo/pi-tui": "20.0.3",
66
- "@f5-sales-demo/pi-utils": "20.0.3",
60
+ "@f5-sales-demo/xcsh-stats": "20.0.4",
61
+ "@f5-sales-demo/pi-agent-core": "20.0.4",
62
+ "@f5-sales-demo/pi-ai": "20.0.4",
63
+ "@f5-sales-demo/pi-natives": "20.0.4",
64
+ "@f5-sales-demo/pi-resource-management": "20.0.4",
65
+ "@f5-sales-demo/pi-tui": "20.0.4",
66
+ "@f5-sales-demo/pi-utils": "20.0.4",
67
67
  "@sinclair/typebox": "^0.34",
68
68
  "@xterm/headless": "^6.0",
69
69
  "ajv": "^8.20",
@@ -64,7 +64,7 @@ export interface BashResult {
64
64
  outputLines: number;
65
65
  outputBytes: number;
66
66
  artifactId?: string;
67
- /** Actual working directory after the command ran (persistent shell only). */
67
+ /** Actual working directory after the command ran; direct operator-shell callers may persist it. */
68
68
  newCwd?: string;
69
69
  }
70
70
 
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "20.0.3",
21
- "commit": "eb145863a59404dfa28bf93241e5dde4f206bbfc",
22
- "shortCommit": "eb14586",
20
+ "version": "20.0.4",
21
+ "commit": "c1606427884c61f77f5f1fbf1c4ab7511920061b",
22
+ "shortCommit": "c160642",
23
23
  "branch": "main",
24
- "tag": "v20.0.3",
25
- "commitDate": "2026-08-01T05:30:12Z",
26
- "buildDate": "2026-08-01T05:59:20.955Z",
24
+ "tag": "v20.0.4",
25
+ "commitDate": "2026-08-01T06:54:53Z",
26
+ "buildDate": "2026-08-01T07:20:58.905Z",
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/eb145863a59404dfa28bf93241e5dde4f206bbfc",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.0.3"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/c1606427884c61f77f5f1fbf1c4ab7511920061b",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.0.4"
33
33
  };
@@ -149,13 +149,10 @@ export interface CwdEventSource {
149
149
  }
150
150
 
151
151
  /**
152
- * A getter for the working directory the *session* is in, not the one the process
153
- * started in.
152
+ * A getter for the working directory the *session* is rooted in, not the one the process started in.
154
153
  *
155
- * The bash tool keeps its own cwd and updates it on `cd`, emitting `cwd:changed`
156
- * (`tools/bash.ts`); `process.chdir` is never called for it. Classifying against
157
- * `process.cwd()` would therefore keep reporting the startup repository, so a
158
- * `content` grant could survive after moving into a `developer` repository.
154
+ * Model bash calls reset to this root, so a command-local `cd` must not emit this event. The event is
155
+ * reserved for an explicit session relocation; `process.chdir` is never used for either case.
159
156
  */
160
157
  export function createLiveCwdGetter(initialCwd: string, events?: CwdEventSource): () => string {
161
158
  let current = initialCwd;
@@ -212,7 +212,7 @@ export class StatusLineComponent implements Component {
212
212
  this.#invalidateGitCaches();
213
213
  }
214
214
 
215
- /** Update the displayed working directory (e.g. after a user !cd command). */
215
+ /** Update the displayed working directory after an operator command or explicit session move. */
216
216
  setCwd(cwd: string): void {
217
217
  this.#cwd = cwd;
218
218
  this.#invalidateGitCaches();
@@ -2,6 +2,8 @@ Executes bash command in shell session for terminal operations like git, bun, ca
2
2
 
3
3
  <instruction>
4
4
  - You **MUST** use `cwd` parameter to set working directory instead of `cd dir && …`
5
+ - Every call starts in the session working directory, or in `cwd` when provided. A `cd` affects only
6
+ that call and does not relocate later tool calls.
5
7
  - Always provide a `description` parameter: a short, human-readable summary of what the command does (e.g. "Install dependencies", "Run test suite"). This is displayed in compact mode and during execution.
6
8
  - Prefer `env: { NAME: "…" }` for multiline, quote-heavy, or untrusted values instead of inlining them into shell syntax; reference them from the command as `$NAME`
7
9
  - Quote variable expansions like `"$NAME"` to preserve exact content and avoid shell parsing bugs
package/src/sdk.ts CHANGED
@@ -1170,8 +1170,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1170
1170
  },
1171
1171
  }),
1172
1172
  getPluginRoots: () => listXcshPluginRoots(os.homedir(), cwd).then(r => r.roots),
1173
- // Classification must follow the session's `cd`, not the process cwd, which
1174
- // never moves. The bash tool emits `cwd:changed` when it relocates.
1173
+ // Classification follows explicit session relocation events, not command-local `cd` and not the
1174
+ // process cwd. Model bash calls reset to this session root on every invocation (#2724).
1175
1175
  fleetDeps: { cwd: createLiveCwdGetter(cwd, eventBus) },
1176
1176
  }),
1177
1177
  );
package/src/tools/bash.ts CHANGED
@@ -7,7 +7,7 @@ import type {
7
7
  } from "@f5-sales-demo/pi-agent-core";
8
8
  import type { Component } from "@f5-sales-demo/pi-tui";
9
9
  import { ImageProtocol, TERMINAL, Text } from "@f5-sales-demo/pi-tui";
10
- import { $env, getProjectDir, isEnoent, prompt, setShellPwd } from "@f5-sales-demo/pi-utils";
10
+ import { $env, getProjectDir, isEnoent, prompt } from "@f5-sales-demo/pi-utils";
11
11
  import { Type } from "@sinclair/typebox";
12
12
  import { Settings } from "../config/settings";
13
13
  import { type BashResult, executeBash } from "../exec/bash-executor";
@@ -516,17 +516,13 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
516
516
  /**
517
517
  * Where this session's boundary is anchored.
518
518
  *
519
- * **Not the live cwd.** The fence used to be rebuilt from `session.cwd`, which line ~717 replaces
520
- * with whatever PWD the command ended in so the model could move the boundary with a `cd`. Measured
521
- * on a workspace outside the home tree, which is the layout the sibling-checkout deny exists for:
522
- * with `/work/custA` as the workspace, `/work/custB/secret.env` was denied; `cd /usr` is permitted
523
- * (correctly — `/usr` is not sensitive), and the *next* fence was rooted at `/usr`, where the parent
524
- * deny of `/work` cannot be expressed because `dirname("/usr")` is `/` and denying the root is refused
525
- * as too broad. `/work/custB` then became readable **and** writable. Two tool calls, no exotic
526
- * spelling, and the tenant boundary was gone.
519
+ * **Not a command's final PWD.** Every model tool call starts from `session.cwd` and a command-local
520
+ * `cd` is discarded when that call ends (#2724). Before that reset existed, writing the final PWD
521
+ * back into `session.cwd` also moved this fence: after `cd /usr`, the next boundary was rooted at
522
+ * `/usr` and could no longer protect a workspace under `/work` (#2589).
527
523
  *
528
- * So the anchor is captured once per session and never follows the shell. It is keyed on the session
529
- * object rather than held on this instance because the tool is built by a factory
524
+ * The anchor remains captured once per session as a defensive invariant and never follows the shell.
525
+ * It is keyed on the session object rather than held on this instance because the tool is built by a factory
530
526
  * (`bash: s => new BashTool(s)`) and must not depend on how long an instance happens to live.
531
527
  *
532
528
  * An *operator* moving the project — startup, or the slash command that calls `setProjectDir` — does
@@ -546,8 +542,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
546
542
  return this.session.cwd;
547
543
  }
548
544
  if (anchor.project !== project) {
549
- // The operator moved the project. Re-anchor there rather than at `session.cwd`, which a
550
- // model `cd` may have moved in the meantime.
545
+ // The operator moved the project. This is an explicit boundary relocation, unlike a model's
546
+ // command-local `cd`.
551
547
  FENCE_ANCHORS.set(this.session, { root: project, project });
552
548
  return project;
553
549
  }
@@ -787,13 +783,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
787
783
  }
788
784
  },
789
785
  });
790
- // Update working directory if the persistent shell changed it
791
- if ("newCwd" in result && result.newCwd && result.newCwd !== this.session.cwd) {
792
- this.session.cwd = result.newCwd;
793
- setShellPwd(result.newCwd);
794
- this.session.eventBus?.emit("cwd:changed", result.newCwd);
795
- }
796
-
797
786
  if (result.cancelled) {
798
787
  if (signal?.aborted) {
799
788
  throw new ToolAbortError(normalizeResultOutput(result) || "Command aborted");