@f5-sales-demo/xcsh 21.1.0 → 21.2.0

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": "21.1.0",
4
+ "version": "21.2.0",
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",
@@ -61,13 +61,13 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@agentclientprotocol/sdk": "1.3.0",
64
- "@f5-sales-demo/pi-agent-core": "21.1.0",
65
- "@f5-sales-demo/pi-ai": "21.1.0",
66
- "@f5-sales-demo/pi-natives": "21.1.0",
67
- "@f5-sales-demo/pi-resource-management": "21.1.0",
68
- "@f5-sales-demo/pi-tui": "21.1.0",
69
- "@f5-sales-demo/pi-utils": "21.1.0",
70
- "@f5-sales-demo/xcsh-stats": "21.1.0",
64
+ "@f5-sales-demo/pi-agent-core": "21.2.0",
65
+ "@f5-sales-demo/pi-ai": "21.2.0",
66
+ "@f5-sales-demo/pi-natives": "21.2.0",
67
+ "@f5-sales-demo/pi-resource-management": "21.2.0",
68
+ "@f5-sales-demo/pi-tui": "21.2.0",
69
+ "@f5-sales-demo/pi-utils": "21.2.0",
70
+ "@f5-sales-demo/xcsh-stats": "21.2.0",
71
71
  "@mozilla/readability": "^0.6",
72
72
  "@sinclair/typebox": "^0.34",
73
73
  "@xterm/headless": "^6.0",
@@ -15,13 +15,33 @@ export interface ExtensionToolDef {
15
15
  readonly flags?: ExtensionToolFlags;
16
16
  }
17
17
 
18
+ export type ExtensionInteractionMode =
19
+ | "educational"
20
+ | "presentation"
21
+ | "configuration"
22
+ | "screenshot"
23
+ | "annotation";
24
+
25
+ export interface ExtensionChatPromptHints {
26
+ readonly role: string;
27
+ readonly grounding: string;
28
+ readonly referenceLinks: string;
29
+ readonly toolUse: string;
30
+ readonly modes: Readonly<Record<ExtensionInteractionMode, string>>;
31
+ }
32
+
33
+ export interface ExtensionFeatures {
34
+ readonly chat: { readonly promptHints: ExtensionChatPromptHints; readonly [key: string]: unknown };
35
+ readonly [key: string]: unknown;
36
+ }
37
+
18
38
  export interface ExtensionCapabilities {
19
39
  readonly version: string;
20
40
  readonly contractVersion: string;
21
41
  readonly multiPortDiscovery?: boolean;
22
42
  readonly protocol: string;
23
43
  readonly tools: readonly ExtensionToolDef[];
24
- readonly features: Record<string, unknown>;
44
+ readonly features: ExtensionFeatures;
25
45
  }
26
46
 
27
47
  export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
@@ -21,6 +21,7 @@ import {
21
21
  import { LITELLM_LOGIN_MODEL_CHOICES } from "../modes/controllers/login-model";
22
22
  import { extractReferences } from "../references";
23
23
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
24
+ import { EXTENSION_CAPABILITIES } from "./capabilities.generated";
24
25
  import {
25
26
  type ChatDelta,
26
27
  type ChatDone,
@@ -689,14 +690,6 @@ export function classifyChatErrorReason(message: string): ChatErrorReason {
689
690
  return "provider-5xx";
690
691
  }
691
692
 
692
- const MODE_INSTRUCTIONS: Record<InteractionMode, string> = {
693
- educational: "Explain concepts and settings in depth. Help the user understand what they're looking at and why.",
694
- presentation: "Guide a structured walkthrough. Narrate each step clearly for a live audience.",
695
- configuration: "Help the user build or modify F5 XC configuration. Be precise and action-oriented.",
696
- screenshot: "Focus on capturing annotated screenshots that document the current state.",
697
- annotation: "Create on-page teaching annotations that highlight key elements and explain their purpose.",
698
- };
699
-
700
693
  /** Bases a user-attached context path must fall under. Confines grants to the user's
701
694
  * own space (home, the project cwd, temp, external volumes, /opt) and thereby blocks
702
695
  * a client from widening the sandbox to system/credential dirs (`/etc`, `/var`,
@@ -779,11 +772,22 @@ export function composeChatPrompt(
779
772
  const profile = hostProfile(host);
780
773
  parts.push(profile.systemPrompt);
781
774
 
775
+ const promptHints = EXTENSION_CAPABILITIES.features.chat.promptHints;
776
+
782
777
  // Browser hosts ALSO get an interaction mode + the page-context block. Document
783
778
  // hosts get NEITHER: Office sends no page context and has no browser modes; its
784
779
  // tools + document state arrive at runtime via set_host_tools.
785
780
  if (profile.kind === "browser") {
786
- parts.push(`[Chat mode: ${mode}] ${MODE_INSTRUCTIONS[mode]}`);
781
+ parts.push(
782
+ [
783
+ "[Published extension chat contract]",
784
+ promptHints.role,
785
+ promptHints.grounding,
786
+ promptHints.referenceLinks,
787
+ promptHints.toolUse,
788
+ ].join("\n"),
789
+ );
790
+ parts.push(`[Chat mode: ${mode}] ${promptHints.modes[mode]}`);
787
791
  if (context) composeBrowserPageContext(parts, context);
788
792
  }
789
793
 
@@ -46,3 +46,65 @@ export function extractRequestedTools(source: string): string[] {
46
46
  while ((m = re.exec(source)) !== null) out.add(m[1]);
47
47
  return [...out];
48
48
  }
49
+
50
+ interface ToolReferenceManifest {
51
+ readonly contractVersion: string;
52
+ readonly tools: readonly {
53
+ readonly name: string;
54
+ readonly summary: string;
55
+ readonly category: string;
56
+ readonly params: Readonly<Record<string, unknown>>;
57
+ readonly flags?: {
58
+ readonly readOnly?: boolean;
59
+ readonly mutates?: boolean;
60
+ readonly requiresExplainMode?: boolean;
61
+ };
62
+ }[];
63
+ }
64
+
65
+ function compareText(left: string, right: string): number {
66
+ return left < right ? -1 : left > right ? 1 : 0;
67
+ }
68
+
69
+ /** Stable JSON with object keys sorted recursively and array order preserved. */
70
+ function canonicalJson(value: unknown): string {
71
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
72
+ if (value !== null && typeof value === "object") {
73
+ const entries = Object.entries(value as Record<string, unknown>).sort(([left], [right]) =>
74
+ compareText(left, right),
75
+ );
76
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
77
+ }
78
+ return JSON.stringify(value) ?? "null";
79
+ }
80
+
81
+ /** Render the complete extension tool surface as deterministic Markdown. */
82
+ export function renderToolReference(manifest: ToolReferenceManifest): string {
83
+ const categories = new Map<string, ToolReferenceManifest["tools"][number][]>();
84
+ for (const tool of manifest.tools) {
85
+ const tools = categories.get(tool.category) ?? [];
86
+ tools.push(tool);
87
+ categories.set(tool.category, tools);
88
+ }
89
+
90
+ const lines = [
91
+ "# Chrome Extension Tool Signatures",
92
+ "",
93
+ `Generated from extension capability contract \`${manifest.contractVersion}\`.`,
94
+ ];
95
+ for (const category of [...categories.keys()].sort(compareText)) {
96
+ lines.push("", `## ${category}`);
97
+ for (const tool of (categories.get(category) ?? []).sort((left, right) => compareText(left.name, right.name))) {
98
+ lines.push(
99
+ "",
100
+ `### \`${tool.name}\``,
101
+ "",
102
+ tool.summary,
103
+ "",
104
+ `- Parameters: \`${canonicalJson(tool.params)}\``,
105
+ `- Semantic flags: \`${canonicalJson(tool.flags ?? {})}\``,
106
+ );
107
+ }
108
+ }
109
+ return `${lines.join("\n")}\n`;
110
+ }
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "21.1.0",
21
- "commit": "5c6e84a195bcf81ba561bf85c5ecf7a47d8c62c1",
22
- "shortCommit": "5c6e84a",
20
+ "version": "21.2.0",
21
+ "commit": "d217f62f68fae54fe18d88c7d90a8f7dc4703c0d",
22
+ "shortCommit": "d217f62",
23
23
  "branch": "main",
24
- "tag": "v21.1.0",
25
- "commitDate": "2026-08-28T19:03:09+00:00",
26
- "buildDate": "2026-08-28T19:42:43.325Z",
24
+ "tag": "v21.2.0",
25
+ "commitDate": "2026-08-30T03:18:04+00:00",
26
+ "buildDate": "2026-08-30T03:46:25.618Z",
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/5c6e84a195bcf81ba561bf85c5ecf7a47d8c62c1",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.1.0"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/d217f62f68fae54fe18d88c7d90a8f7dc4703c0d",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.2.0"
33
33
  };
@@ -0,0 +1,4 @@
1
+ // Auto-generated by scripts/generate-extension-capabilities.ts - DO NOT EDIT
2
+ // Source: src/browser/capabilities.json (the Chrome extension's published contract).
3
+
4
+ export const EXTENSION_TOOL_REFERENCE = "# Chrome Extension Tool Signatures\n\nGenerated from extension capability contract `2.1.0`.\n\n## annotation\n\n### `annotate`\n\nDraw an overlay annotation (fingerprint/highlight). No-op unless explain mode is on.\n\n- Parameters: `{\"properties\":{\"h\":{\"type\":\"number\"},\"kind\":{\"type\":\"string\"},\"ref\":{\"type\":\"string\"},\"w\":{\"type\":\"number\"},\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"}},\"required\":[\"kind\"],\"type\":\"object\"}`\n- Semantic flags: `{\"requiresExplainMode\":true}`\n\n### `set_explain_mode`\n\nEnter/leave explain mode — the gate for all on-page annotation overlays.\n\n- Parameters: `{\"properties\":{\"enabled\":{\"type\":\"boolean\"}},\"type\":\"object\"}`\n- Semantic flags: `{}`\n\n## interaction\n\n### `click`\n\nDeterministic click of an AX-ref element (layout-engine coords + hit-test).\n\n- Parameters: `{\"properties\":{\"ref\":{\"type\":\"string\"}},\"required\":[\"ref\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `click_element`\n\nClick the element returned by a JS expression (polls; occlusion-safe).\n\n- Parameters: `{\"properties\":{\"js\":{\"type\":\"string\"},\"wait_ms\":{\"type\":\"number\"}},\"required\":[\"js\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `click_xy`\n\nTrusted click at explicit viewport coordinates.\n\n- Parameters: `{\"properties\":{\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"}},\"required\":[\"x\",\"y\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `file_upload`\n\nUpload files (base64 data URIs) to a file input by AX ref.\n\n- Parameters: `{\"properties\":{\"files\":{\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"ref\":{\"type\":\"string\"}},\"required\":[\"ref\",\"files\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `form_input`\n\nSet a form field value by AX ref.\n\n- Parameters: `{\"properties\":{\"ref\":{\"type\":\"string\"},\"value\":{\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `key_press`\n\nDispatch a key press.\n\n- Parameters: `{\"properties\":{\"key\":{\"type\":\"string\"}},\"required\":[\"key\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `label_select`\n\nType into a CDK-portal typeahead and click the matching option.\n\n- Parameters: `{\"properties\":{\"label_value\":{\"type\":\"string\"},\"selector\":{\"type\":\"string\"},\"value\":{\"type\":\"string\"},\"wait_ms\":{\"type\":\"number\"}},\"required\":[\"selector\",\"value\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `select_option`\n\nSelect an option in a native <select> by AX ref.\n\n- Parameters: `{\"properties\":{\"ref\":{\"type\":\"string\"},\"value\":{\"type\":\"string\"}},\"required\":[\"ref\",\"value\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `type_text`\n\nType text into the focused element (trusted input).\n\n- Parameters: `{\"properties\":{\"text\":{\"type\":\"string\"}},\"required\":[\"text\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n## meta\n\n### `capabilities`\n\nReturn this self-describing capability manifest (tools + features + versions).\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `debug_exec`\n\nDiagnostic: probe in-page bridge availability.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `detach`\n\nDetach the debugger from the target tab.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `ping`\n\nLiveness check; returns { ok, version }.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `reload`\n\nReload the extension (re-reads dist/ from disk).\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `set_bridge_port`\n\nSet the WebSocket bridge port (persists across reload; enables multi-session on different ports).\n\n- Parameters: `{\"properties\":{\"port\":{\"type\":\"number\"}},\"required\":[\"port\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n## navigation\n\n### `navigate`\n\nNavigate the console tab to a scoped https URL.\n\n- Parameters: `{\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `resize_window`\n\nResize the browser window.\n\n- Parameters: `{\"properties\":{\"height\":{\"type\":\"number\"},\"width\":{\"type\":\"number\"}},\"required\":[\"width\",\"height\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `scroll_to`\n\nScroll an AX-ref element into view.\n\n- Parameters: `{\"properties\":{\"ref\":{\"type\":\"string\"}},\"required\":[\"ref\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `tabs_close`\n\nClose a tab by id.\n\n- Parameters: `{\"properties\":{\"tabId\":{\"type\":\"number\"}},\"required\":[\"tabId\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `tabs_create`\n\nOpen a new tab at a scoped URL.\n\n- Parameters: `{\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `tabs_list`\n\nList scoped console tabs.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{}`\n\n## read\n\n### `assert_text`\n\nAssert an element contains expected text.\n\n- Parameters: `{\"properties\":{\"context\":{\"type\":\"string\"},\"expected\":{\"type\":\"string\"},\"selector\":{\"type\":\"string\"}},\"required\":[\"selector\",\"expected\"],\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `diag_activation`\n\nDiagnostic: per-gate tab-activation readiness timings (bridge/worker/page), cold/warm.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{}`\n\n### `diag_bridges`\n\nList discovered xcsh bridge health without tenant, environment, or session identifiers.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{}`\n\n### `diag_suspension`\n\nDiagnostic: SW-lifecycle event buffer + suspension summary (Phase 0a).\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{}`\n\n### `diag_ttft`\n\nDiagnostic: init→first-token timeline (per-stage ms, total, dominant, cold/warm).\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{}`\n\n### `find`\n\nFind AX nodes matching a locator.\n\n- Parameters: `{\"properties\":{\"selector\":{\"type\":\"string\"}},\"required\":[\"selector\"],\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `get_page_context`\n\nReturn a snapshot of the active console page (url, AX tree, captured XC API body) for chat grounding.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `get_page_text`\n\nReturn the page text.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `query_dom`\n\nDirect DOM.querySelector at wire speed — bypasses Runtime.evaluate for simple CSS selectors.\n\n- Parameters: `{\"properties\":{\"selector\":{\"type\":\"string\"}},\"required\":[\"selector\"],\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `read_ax`\n\nRead the accessibility tree of the page.\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `read_console`\n\nRead buffered console messages, optionally filtered by pattern.\n\n- Parameters: `{\"properties\":{\"pattern\":{\"type\":\"string\"}},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `read_network`\n\nRead buffered network events, optionally filtered by pattern.\n\n- Parameters: `{\"properties\":{\"pattern\":{\"type\":\"string\"}},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `screenshot`\n\nCapture a screenshot (base64 PNG).\n\n- Parameters: `{\"properties\":{},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `wait_for`\n\nWait for an AX node matching a locator to appear.\n\n- Parameters: `{\"properties\":{\"context\":{\"type\":\"string\"},\"selector\":{\"type\":\"string\"},\"timeoutMs\":{\"type\":\"number\"}},\"required\":[\"selector\"],\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n### `wait_for_api_response`\n\nWait for a network response whose URL matches a pattern.\n\n- Parameters: `{\"properties\":{\"pattern\":{\"type\":\"string\"},\"timeout_ms\":{\"type\":\"number\"}},\"type\":\"object\"}`\n- Semantic flags: `{\"readOnly\":true}`\n\n## script\n\n### `browser_batch`\n\nRun a batch of { tool, params } actions in sequence.\n\n- Parameters: `{\"properties\":{\"actions\":{\"items\":{\"properties\":{\"params\":{},\"tool\":{\"type\":\"string\"}},\"required\":[\"tool\"],\"type\":\"object\"},\"type\":\"array\"}},\"required\":[\"actions\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n\n### `javascript_tool`\n\nEvaluate arbitrary JS in the page (length-capped).\n\n- Parameters: `{\"properties\":{\"code\":{\"type\":\"string\"}},\"required\":[\"code\"],\"type\":\"object\"}`\n- Semantic flags: `{\"mutates\":true}`\n";
@@ -44,6 +44,7 @@ import { type ConsoleFieldMetadataData, EMPTY_CONSOLE_FIELD_METADATA } from "./c
44
44
  import { type ConsoleResolver, createConsoleResolver } from "./console-resolve";
45
45
  import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
46
46
  import extensionApiContent from "./extension-api.md" with { type: "text" };
47
+ import { EXTENSION_TOOL_REFERENCE } from "./extension-tools.generated";
47
48
  import { createFleetResolver, type FleetDeps, type FleetResolver } from "./fleet-resolve";
48
49
  import { createPluginResolver, type GetPluginRoots, type PluginResolver } from "./plugin-resolve";
49
50
  import { createRegistryResolver, type RegistryResolver, type RegistryResolverDeps } from "./registry-resolve";
@@ -66,6 +67,7 @@ const CHANGES_HOST = "changes";
66
67
  const SOURCE_HOST = "source";
67
68
  const FLEET_HOST = "fleet";
68
69
  const SITECLI_HOST = "sitecli";
70
+ const EXTENSION_CONTENT = `${extensionApiContent.trimEnd()}\n\n---\n\n${EXTENSION_TOOL_REFERENCE}`;
69
71
  const EMPTY_INDEX: ApiSpecIndex = { version: "unavailable", timestamp: "", domains: [] };
70
72
  const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
71
73
  version: "unavailable",
@@ -490,15 +492,14 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
490
492
  return this.#readDoc(filename, url);
491
493
  }
492
494
 
493
- /** Serve the Chrome extension bridge tool API reference. The hand-written
494
- * guidance (extension-api.md) is imported at compile time and served on demand
495
- * at `xcsh://extension`. */
495
+ /** Serve the Chrome extension bridge guidance plus generated tool signatures.
496
+ * Both are imported at compile time and served on demand at `xcsh://extension`. */
496
497
  #resolveExtension(url: InternalUrl): InternalResource {
497
498
  return {
498
499
  url: url.href,
499
- content: extensionApiContent,
500
+ content: EXTENSION_CONTENT,
500
501
  contentType: "text/markdown",
501
- size: Buffer.byteLength(extensionApiContent, "utf-8"),
502
+ size: Buffer.byteLength(EXTENSION_CONTENT, "utf-8"),
502
503
  sourcePath: `${SCHEME_PREFIX}${EXTENSION_HOST}`,
503
504
  };
504
505
  }
@@ -61,6 +61,10 @@ Without a runtime backend, Bash child-process reads cannot enforce protected-con
61
61
  xcsh-private-root denial. Structured tools still enforce those rules, and ordinary work remains
62
62
  unrestricted. The persistent `python` kernel is likewise unfenced beyond its explicit `cwd` check.
63
63
 
64
+ On Windows, mounted drive letters other than the workspace drive lose root enumeration in structured
65
+ tools. A UNC share that is not mapped to a drive letter cannot be enumerated and is outside this
66
+ courtesy boundary; map it to a drive or grant a known path explicitly when it belongs to the task.
67
+
64
68
  Treat the boundary as a statement of intent rather than a guarantee, and do not go looking for paths
65
69
  outside the session directory on the assumption that something would stop you.
66
70
  {{/if}}
@@ -352,12 +352,11 @@ function resolveGrants(roots: readonly string[] | undefined, home: string): Set<
352
352
  * kernel backend, so this exact-enumeration rule is enforced for structured tools but cannot confine a
353
353
  * Bash child process.
354
354
  *
355
- * **Unverified on Windows.** The probe below cannot run on the platforms this fleet uses, so what is
356
- * tested is the exact-enumeration protection, not discovery — see the test, which injects the list.
355
+ * Native Windows UAT exercises this production probe against a real second mounted volume.
357
356
  *
358
357
  * UNC paths (`\\server\share`) have no enumerable root and are not covered.
359
358
  */
360
- function otherFilesystemRoots(fsRoot: string): string[] {
359
+ export function otherFilesystemRoots(fsRoot: string): string[] {
361
360
  if (process.platform !== "win32") return [];
362
361
  const roots: string[] = [];
363
362
  for (let letter = "A".charCodeAt(0); letter <= "Z".charCodeAt(0); letter++) {
@@ -532,8 +531,16 @@ export function buildContainmentFence(options: ContainmentOptions): ContainmentF
532
531
  if (resolved === fsRoot) continue; // never the root the workspace lives on
533
532
  // A directory containing home is normally too broad to protect because it would hide unrelated
534
533
  // operational entries. Account containers are the deliberate exception: their listing is the
535
- // discovery surface, while every named account remains reachable (#2788, #2931).
536
- if (home !== undefined && pathIsWithin(resolved, home) && !accountRoots.has(resolved)) continue;
534
+ // discovery surface, while every named account remains reachable (#2788, #2931). A separate
535
+ // Windows drive is also an exception: only its exact listing is hidden, so home and every named
536
+ // descendant retain their normal access.
537
+ if (
538
+ !rootScoped.has(resolved) &&
539
+ home !== undefined &&
540
+ pathIsWithin(resolved, home) &&
541
+ !accountRoots.has(resolved)
542
+ )
543
+ continue;
537
544
  // The session workspace and explicit read/full grants retain enumeration. A write-only grant does
538
545
  // not imply permission to learn directory entries.
539
546
  if (resolved === workspace) continue;