@f5-sales-demo/xcsh 19.88.0 → 19.89.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 +8 -8
- package/src/browser/chat-handler.ts +110 -2
- package/src/browser/chat-protocol.ts +50 -0
- package/src/browser/headless-bridge.ts +20 -0
- package/src/browser/host-profiles.ts +2 -0
- package/src/browser/native-picker.ts +47 -0
- package/src/extensibility/extensions/bundled/sandbox-guard.ts +0 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/terraform-index.generated.ts +13 -1
- package/src/session/agent-session.ts +15 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.89.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",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
58
58
|
"@mozilla/readability": "^0.6",
|
|
59
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.89.0",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.89.0",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.89.0",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.89.0",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.89.0",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.89.0",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.89.0",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import type { AssistantMessage, ImageContent } from "@f5-sales-demo/pi-ai";
|
|
4
|
+
import { settings } from "../config/settings";
|
|
2
5
|
import { DEFAULT_MODEL_ROLE } from "../config/settings-schema";
|
|
3
6
|
import {
|
|
4
7
|
isRpcHostToolResult,
|
|
@@ -24,11 +27,13 @@ import {
|
|
|
24
27
|
isChatRequest,
|
|
25
28
|
isChatStop,
|
|
26
29
|
isConfigure,
|
|
30
|
+
isListSkills,
|
|
27
31
|
isSetHostTools,
|
|
28
32
|
type PageContextSnapshot,
|
|
29
33
|
type SetHostTools,
|
|
30
34
|
type SetHostToolsAck,
|
|
31
35
|
type SetHostToolsError,
|
|
36
|
+
type SkillsList,
|
|
32
37
|
} from "./chat-protocol";
|
|
33
38
|
import { CONSOLE_ROUTES } from "./console-routes.generated";
|
|
34
39
|
import type { BridgeServer } from "./extension-bridge";
|
|
@@ -101,6 +106,9 @@ export class ChatHandler {
|
|
|
101
106
|
// Provider configuration channel (#2095): swap the LLM provider/model in
|
|
102
107
|
// session memory at runtime (never persisted), then ack or nack.
|
|
103
108
|
else if (isConfigure(msg)) this.#handleConfigure(msg as unknown as Configure);
|
|
109
|
+
// Skills enumeration (#2311): the pane asks for the loaded skills to populate
|
|
110
|
+
// the composer's Skills submenu.
|
|
111
|
+
else if (isListSkills(msg)) this.#handleListSkills();
|
|
104
112
|
else if (isRpcHostToolResult(msg)) this.#hostToolBridge.handleResult(msg as unknown as HostToolResult);
|
|
105
113
|
else if (isRpcHostToolUpdate(msg)) this.#hostToolBridge.handleUpdate(msg as unknown as HostToolUpdate);
|
|
106
114
|
});
|
|
@@ -164,7 +172,13 @@ export class ChatHandler {
|
|
|
164
172
|
});
|
|
165
173
|
chat.unsubscribe = unsubscribe;
|
|
166
174
|
|
|
167
|
-
|
|
175
|
+
// Sanitize the attached paths (absolute, `..`-collapsed, confined to the user's
|
|
176
|
+
// own space, no control chars) — the engine must not blindly widen its sandbox to
|
|
177
|
+
// a client-supplied path. Grant the safe set BEFORE composing the prompt so the
|
|
178
|
+
// model's read/grep/bash calls pass the gate. Session-scoped, in-memory, deduped.
|
|
179
|
+
const contextPaths = Array.isArray(req.contextPaths) ? sanitizeContextPaths(req.contextPaths) : [];
|
|
180
|
+
if (contextPaths.length > 0) grantSandboxPaths(contextPaths);
|
|
181
|
+
const prompt = composeChatPrompt(req.text, req.context, req.mode, this.#server.clientHost, contextPaths);
|
|
168
182
|
// Photo/image attachments ride as base64 vision blocks (the model is
|
|
169
183
|
// vision-capable); text attachments are already folded into req.text upstream.
|
|
170
184
|
const images: ImageContent[] | undefined = req.images?.map(img => ({
|
|
@@ -172,10 +186,16 @@ export class ChatHandler {
|
|
|
172
186
|
data: img.data,
|
|
173
187
|
mimeType: img.mimeType,
|
|
174
188
|
}));
|
|
189
|
+
// "Search the web" toggle → add Anthropic's server-side web-search tool for this
|
|
190
|
+
// turn only. The gateway executes it and returns cited results; source URLs the
|
|
191
|
+
// model writes inline flow to the pane's Sources chips via extractReferences.
|
|
192
|
+
const serverTools = req.web_search
|
|
193
|
+
? [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }]
|
|
194
|
+
: undefined;
|
|
175
195
|
|
|
176
196
|
try {
|
|
177
197
|
chat.promptAt = Date.now();
|
|
178
|
-
await this.#session.prompt(prompt, { expandPromptTemplates: false, synthetic: false, images });
|
|
198
|
+
await this.#session.prompt(prompt, { expandPromptTemplates: false, synthetic: false, images, serverTools });
|
|
179
199
|
} catch (err: unknown) {
|
|
180
200
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
181
201
|
this.#sendTerminal(chat, { type: "chat_error", id, error: message, reason: classifyChatErrorReason(message) });
|
|
@@ -377,6 +397,15 @@ export class ChatHandler {
|
|
|
377
397
|
this.#session.agent.abort();
|
|
378
398
|
}
|
|
379
399
|
|
|
400
|
+
/** Reply to `list_skills` with the session's live skills (name + description) so
|
|
401
|
+
* the pane can populate the composer's Skills submenu. Pure read — skills are
|
|
402
|
+
* already loaded on the session; the model actions them via the read tool + the
|
|
403
|
+
* system prompt (Phase 2A enabled `read`), so this is enumeration only. */
|
|
404
|
+
#handleListSkills(): void {
|
|
405
|
+
const skills = this.#session.skills.map(s => ({ name: s.name, description: s.description }));
|
|
406
|
+
this.#server.send({ type: "skills", skills } satisfies SkillsList);
|
|
407
|
+
}
|
|
408
|
+
|
|
380
409
|
#sendTerminal(chat: ActiveChat, frame: ChatDone | ChatError): void {
|
|
381
410
|
if (chat.terminalSent) return;
|
|
382
411
|
chat.terminalSent = true;
|
|
@@ -455,11 +484,79 @@ const MODE_INSTRUCTIONS: Record<InteractionMode, string> = {
|
|
|
455
484
|
annotation: "Create on-page teaching annotations that highlight key elements and explain their purpose.",
|
|
456
485
|
};
|
|
457
486
|
|
|
487
|
+
/** Bases a user-attached context path must fall under. Confines grants to the user's
|
|
488
|
+
* own space (home, the project cwd, temp, external volumes, /opt) and thereby blocks
|
|
489
|
+
* a client from widening the sandbox to system/credential dirs (`/etc`, `/var`,
|
|
490
|
+
* `/usr`, `/System`, other users' homes, `/`). */
|
|
491
|
+
function contextPathAllowedBases(): string[] {
|
|
492
|
+
const bases = [process.cwd(), "/tmp", "/private/tmp", "/Volumes", "/opt"];
|
|
493
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
494
|
+
if (home) bases.push(home);
|
|
495
|
+
// Canonicalize with realpath so BOTH sides of the containment check are symlink-free
|
|
496
|
+
// (a symlink base like /tmp→/private/tmp must compare against the real target).
|
|
497
|
+
return bases.map(b => {
|
|
498
|
+
try {
|
|
499
|
+
return fs.realpathSync(b);
|
|
500
|
+
} catch {
|
|
501
|
+
return path.resolve(b);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Canonicalize + validate the user-attached context paths BEFORE they widen the
|
|
508
|
+
* sandbox or are named in the prompt. Rejects anything that isn't an absolute path
|
|
509
|
+
* confined to {@link contextPathAllowedBases} (after collapsing `..`), and anything
|
|
510
|
+
* carrying control characters (a prompt-injection vector). Deduped. This is the trust
|
|
511
|
+
* boundary: the engine must not blindly grant a client-supplied path to its own
|
|
512
|
+
* filesystem sandbox.
|
|
513
|
+
*/
|
|
514
|
+
export function sanitizeContextPaths(paths: string[]): string[] {
|
|
515
|
+
const bases = contextPathAllowedBases();
|
|
516
|
+
const seen = new Set<string>();
|
|
517
|
+
const out: string[] = [];
|
|
518
|
+
for (const p of paths) {
|
|
519
|
+
if (typeof p !== "string" || p.length === 0 || /[\n\r\0]/.test(p) || !path.isAbsolute(p)) continue;
|
|
520
|
+
// realpathSync resolves symlinks — a link INSIDE an allowed base that points out to
|
|
521
|
+
// /etc canonicalizes to /etc and fails the containment check — AND requires the
|
|
522
|
+
// path to exist, so a non-existent (typo'd/spoofed) attachment is skipped.
|
|
523
|
+
let real: string;
|
|
524
|
+
try {
|
|
525
|
+
real = fs.realpathSync(p);
|
|
526
|
+
} catch {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const within = bases.some(base => real === base || real.startsWith(base + path.sep));
|
|
530
|
+
if (!within || seen.has(real)) continue;
|
|
531
|
+
seen.add(real);
|
|
532
|
+
out.push(real);
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Append absolute context paths to the session's sandbox read-allowlist. In-memory
|
|
539
|
+
* (`override`, never persisted) and deduped, so re-attaching a path is a no-op. The
|
|
540
|
+
* sandbox-guard keys its policy cache on the allow-list, so the grant takes effect on
|
|
541
|
+
* the model's next file tool call. Best-effort: a pre-init settings proxy is tolerated.
|
|
542
|
+
* Callers MUST pass paths through {@link sanitizeContextPaths} first.
|
|
543
|
+
*/
|
|
544
|
+
export function grantSandboxPaths(paths: string[]): void {
|
|
545
|
+
try {
|
|
546
|
+
const current = (settings.get("sandbox.allowRead") as string[] | undefined) ?? [];
|
|
547
|
+
const merged = Array.from(new Set([...current, ...paths]));
|
|
548
|
+
if (merged.length !== current.length) settings.override("sandbox.allowRead", merged);
|
|
549
|
+
} catch {
|
|
550
|
+
/* settings not initialized (some SDK/test contexts) — the grant is best-effort */
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
458
554
|
export function composeChatPrompt(
|
|
459
555
|
text: string,
|
|
460
556
|
context: PageContextSnapshot | null,
|
|
461
557
|
mode: InteractionMode,
|
|
462
558
|
host: ClientHost | null,
|
|
559
|
+
contextPaths?: string[],
|
|
463
560
|
): string {
|
|
464
561
|
const parts: string[] = [];
|
|
465
562
|
|
|
@@ -477,6 +574,17 @@ export function composeChatPrompt(
|
|
|
477
574
|
if (context) composeBrowserPageContext(parts, context);
|
|
478
575
|
}
|
|
479
576
|
|
|
577
|
+
// User-attached local context paths (files/folders). They're granted to the
|
|
578
|
+
// sandbox alongside this, so the model may read them on demand — tell it they exist.
|
|
579
|
+
if (contextPaths && contextPaths.length > 0) {
|
|
580
|
+
parts.push("");
|
|
581
|
+
// JSON.stringify each path: an unambiguous, escaped boundary so a path can't
|
|
582
|
+
// blur into surrounding prompt text (belt-and-suspenders — they're pre-sanitized).
|
|
583
|
+
parts.push(
|
|
584
|
+
`The user attached these local paths as context; read them with your tools (read/grep/bash) as needed:\n${contextPaths.map(p => `- ${JSON.stringify(p)}`).join("\n")}`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
480
588
|
parts.push("");
|
|
481
589
|
parts.push(text);
|
|
482
590
|
return parts.join("\n");
|
|
@@ -85,6 +85,48 @@ export interface ChatRequest {
|
|
|
85
85
|
history_hint?: string;
|
|
86
86
|
/** Optional photo/image attachments, sent to the model as vision blocks. */
|
|
87
87
|
images?: ChatImage[];
|
|
88
|
+
/** Absolute local paths (files/folders) the user attached as context. The engine
|
|
89
|
+
* grants them to the filesystem sandbox for the session and tells the model they
|
|
90
|
+
* are available to read on demand. */
|
|
91
|
+
contextPaths?: string[];
|
|
92
|
+
/** When true, the engine adds Anthropic's server-side web-search tool to this
|
|
93
|
+
* turn's request (the "Search the web" composer toggle). */
|
|
94
|
+
web_search?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Client → engine: open a native OS file/folder picker on the machine running the
|
|
98
|
+
* bridge and return the chosen absolute path. */
|
|
99
|
+
export interface PickPath {
|
|
100
|
+
type: "pick_path";
|
|
101
|
+
mode: "file" | "folder";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Engine → client: the picker result. `path` is set on success; `canceled` when the
|
|
105
|
+
* user dismissed the dialog; `unsupported` when the platform has no native picker
|
|
106
|
+
* (the pane then falls back to manual path entry). */
|
|
107
|
+
export interface PathPicked {
|
|
108
|
+
type: "path_picked";
|
|
109
|
+
path?: string;
|
|
110
|
+
canceled?: boolean;
|
|
111
|
+
unsupported?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Client → engine: enumerate the session's loaded skills for the composer's
|
|
115
|
+
* Skills submenu. Sent once after the pane connects. */
|
|
116
|
+
export interface ListSkills {
|
|
117
|
+
type: "list_skills";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One skill surfaced to the pane's Skills submenu (name + human description). */
|
|
121
|
+
export interface SkillInfo {
|
|
122
|
+
name: string;
|
|
123
|
+
description: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Engine → client: the session's live skills, in load order. */
|
|
127
|
+
export interface SkillsList {
|
|
128
|
+
type: "skills";
|
|
129
|
+
skills: SkillInfo[];
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
export interface ChatStop {
|
|
@@ -266,6 +308,14 @@ export function isChatStop(msg: Record<string, unknown>): boolean {
|
|
|
266
308
|
return msg.type === "chat_stop" && hasChatIdPrefix(msg.id);
|
|
267
309
|
}
|
|
268
310
|
|
|
311
|
+
export function isListSkills(msg: Record<string, unknown>): boolean {
|
|
312
|
+
return msg.type === "list_skills";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function isPickPath(msg: Record<string, unknown>): boolean {
|
|
316
|
+
return msg.type === "pick_path" && (msg.mode === "file" || msg.mode === "folder");
|
|
317
|
+
}
|
|
318
|
+
|
|
269
319
|
export function isSetHostTools(msg: Record<string, unknown>): boolean {
|
|
270
320
|
return msg.type === "set_host_tools" && Array.isArray(msg.tools);
|
|
271
321
|
}
|
|
@@ -20,8 +20,10 @@ import { ContextService } from "../services/xcsh-context";
|
|
|
20
20
|
import { deriveTenantEnv } from "../services/xcsh-env";
|
|
21
21
|
import { resolveBridgeTls } from "./bridge-cert";
|
|
22
22
|
import { ChatHandler } from "./chat-handler";
|
|
23
|
+
import { isPickPath, type PathPicked } from "./chat-protocol";
|
|
23
24
|
import { type BridgeServer, OFFICE_PORT_RANGE, startBridgeServer } from "./extension-bridge";
|
|
24
25
|
import { OFFICE_TOOL_NAMES } from "./extension-bridge-tools";
|
|
26
|
+
import { pickPathNative } from "./native-picker";
|
|
25
27
|
import { setSharedBridgeServer } from "./provider";
|
|
26
28
|
|
|
27
29
|
/** A running headless bridge + a teardown that disposes the chat handler and
|
|
@@ -69,6 +71,9 @@ export interface HeadlessBridgeDeps {
|
|
|
69
71
|
setSharedBridgeServer: typeof setSharedBridgeServer;
|
|
70
72
|
createAgentSession: typeof createAgentSession;
|
|
71
73
|
ChatHandlerCtor: typeof ChatHandler;
|
|
74
|
+
/** Native OS file/folder picker (macOS osascript by default). Injectable so tests
|
|
75
|
+
* don't open a real dialog. */
|
|
76
|
+
pickPath: typeof pickPathNative;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
const defaultDeps: HeadlessBridgeDeps = {
|
|
@@ -96,6 +101,7 @@ const defaultDeps: HeadlessBridgeDeps = {
|
|
|
96
101
|
setSharedBridgeServer,
|
|
97
102
|
createAgentSession,
|
|
98
103
|
ChatHandlerCtor: ChatHandler,
|
|
104
|
+
pickPath: pickPathNative,
|
|
99
105
|
};
|
|
100
106
|
|
|
101
107
|
/**
|
|
@@ -157,9 +163,23 @@ export async function startHeadlessChatBridge(deps: HeadlessBridgeDeps = default
|
|
|
157
163
|
const chatHandler = new deps.ChatHandlerCtor(bridge, session);
|
|
158
164
|
chatHandler.attach();
|
|
159
165
|
|
|
166
|
+
// Second bridge subscriber (the bridge fans out to all): serve `pick_path` by
|
|
167
|
+
// opening a native OS picker and replying `path_picked`. Pure — it only returns
|
|
168
|
+
// the chosen path; the sandbox grant + prompt note happen in ChatHandler when the
|
|
169
|
+
// path rides the next `chat_request.contextPaths` (kept atomic there). `disposed`
|
|
170
|
+
// guards against a superseded serve firing the picker on a closed bridge.
|
|
171
|
+
let disposed = false;
|
|
172
|
+
bridge.onMessage(async msg => {
|
|
173
|
+
if (disposed || !isPickPath(msg)) return;
|
|
174
|
+
const { path, canceled, unsupported } = await deps.pickPath((msg as { mode: "file" | "folder" }).mode);
|
|
175
|
+
if (disposed) return;
|
|
176
|
+
bridge.send({ type: "path_picked", path, canceled, unsupported } satisfies PathPicked);
|
|
177
|
+
});
|
|
178
|
+
|
|
160
179
|
return {
|
|
161
180
|
bridge,
|
|
162
181
|
dispose: async () => {
|
|
182
|
+
disposed = true;
|
|
163
183
|
chatHandler.dispose();
|
|
164
184
|
deps.setSharedBridgeServer(null);
|
|
165
185
|
await bridge.close();
|
|
@@ -94,6 +94,8 @@ SAFETY — NEVER DO THESE:
|
|
|
94
94
|
const OFFICE_NATIVE_TOOLS_NOTE = `
|
|
95
95
|
NATIVE TOOLS: Beyond the document host tools, you have xcsh's full local toolset — \`bash\` (run shell commands, including CLIs like \`az\`, \`gh\`, \`terraform\`, \`git\` when installed and authenticated), file tools (\`read\`/\`write\`/\`edit\`), and \`grep\` — plus any skills available in this workspace. Reach for them when the task genuinely needs them (pull live data with a CLI, read a local file the user points you at). Prefer the document host tools for document work. Your file tools and shell are confined to the folder xcsh was launched from.
|
|
96
96
|
|
|
97
|
+
SKILLS: When a message begins with \`/<skill-name>\` naming one of your available skills, treat it as a request to USE that skill — read its instructions (open \`skill://<skill-name>\`, or its SKILL.md via \`read\`) and follow them, applying any text after the name as the skill's input.
|
|
98
|
+
|
|
97
99
|
SAFETY:
|
|
98
100
|
- NEVER kill, stop, inspect, or manage the xcsh \`office serve\` process, its bridge ports, or any xcsh process — that bridge IS you; ending it ends the session.
|
|
99
101
|
- NEVER run \`lsof\`/\`fuser\`/\`kill\`/\`pkill\` against the bridge ports or use the shell to manage xcsh itself.`;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native OS file/folder picker for the local Office bridge.
|
|
3
|
+
*
|
|
4
|
+
* An Office task-pane WebView cannot open a local file dialog, but the bridge runs
|
|
5
|
+
* in-process on the user's machine, so it can. macOS uses `osascript`'s `choose
|
|
6
|
+
* file`/`choose folder`; other platforms have no one-liner equivalent, so the pane
|
|
7
|
+
* falls back to manual path entry there.
|
|
8
|
+
*
|
|
9
|
+
* ASYNC on purpose: the dialog blocks until the user responds, so a synchronous
|
|
10
|
+
* spawn would stall the bridge's event loop (heartbeats, in-flight turns) for the
|
|
11
|
+
* whole time it's open. `Bun.spawn` + `await proc.exited` keeps the bridge live.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface PickResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
/** Absolute path chosen (no trailing slash), when `ok`. */
|
|
17
|
+
path?: string;
|
|
18
|
+
/** The user dismissed the dialog. */
|
|
19
|
+
canceled?: boolean;
|
|
20
|
+
/** This platform has no native picker; the pane should prompt for a path instead. */
|
|
21
|
+
unsupported?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Open a native picker and resolve the chosen absolute path. Never throws — every
|
|
26
|
+
* failure maps to a `PickResult` flag so the caller can respond gracefully.
|
|
27
|
+
*/
|
|
28
|
+
export async function pickPathNative(mode: "file" | "folder"): Promise<PickResult> {
|
|
29
|
+
// macOS only for now — osascript is not present on win32/linux.
|
|
30
|
+
if (process.platform !== "darwin") return { ok: false, unsupported: true };
|
|
31
|
+
const script = mode === "folder" ? "POSIX path of (choose folder)" : "POSIX path of (choose file)";
|
|
32
|
+
try {
|
|
33
|
+
const proc = Bun.spawn(["osascript", "-e", script], { stdout: "pipe", stderr: "ignore" });
|
|
34
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
35
|
+
// Cancel raises AppleScript error -128 → non-zero exit, empty stdout.
|
|
36
|
+
if (code !== 0) return { ok: false, canceled: true };
|
|
37
|
+
let path = out.trim();
|
|
38
|
+
if (!path) return { ok: false, canceled: true };
|
|
39
|
+
// `choose folder` yields a trailing slash; normalize to a bare dir path so it
|
|
40
|
+
// matches the sandbox allow-rule root exactly.
|
|
41
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
42
|
+
return { ok: true, path };
|
|
43
|
+
} catch {
|
|
44
|
+
// osascript missing/blocked — treat as unsupported so the pane offers manual entry.
|
|
45
|
+
return { ok: false, unsupported: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
Binary file
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.89.0",
|
|
21
|
+
"commit": "e3c72edcac3935dc928e0599909ce1fc9bdc4832",
|
|
22
|
+
"shortCommit": "e3c72ed",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.89.0",
|
|
25
|
+
"commitDate": "2026-07-24T23:47:33Z",
|
|
26
|
+
"buildDate": "2026-07-25T00:13:14.668Z",
|
|
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.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/e3c72edcac3935dc928e0599909ce1fc9bdc4832",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.89.0"
|
|
33
33
|
};
|
|
@@ -167,7 +167,7 @@ export const TERRAFORM_INDEX: TerraformIndex = {
|
|
|
167
167
|
name: "Uncategorized",
|
|
168
168
|
slug: "uncategorized",
|
|
169
169
|
description: "Resources pending categorization",
|
|
170
|
-
resource_count:
|
|
170
|
+
resource_count: 7,
|
|
171
171
|
resources: [
|
|
172
172
|
"application_profiles",
|
|
173
173
|
"authorization_server",
|
|
@@ -175,6 +175,7 @@ export const TERRAFORM_INDEX: TerraformIndex = {
|
|
|
175
175
|
"mitigated_domain",
|
|
176
176
|
"protected_application",
|
|
177
177
|
"protected_domain",
|
|
178
|
+
"registration_approval",
|
|
178
179
|
],
|
|
179
180
|
},
|
|
180
181
|
{
|
|
@@ -1427,6 +1428,17 @@ export const TERRAFORM_INDEX: TerraformIndex = {
|
|
|
1427
1428
|
},
|
|
1428
1429
|
import_syntax: "terraform import xcsh_registration.example namespace/name",
|
|
1429
1430
|
},
|
|
1431
|
+
registration_approval: {
|
|
1432
|
+
category: "uncategorized",
|
|
1433
|
+
description: "Request for admission approval. configuration",
|
|
1434
|
+
required: ["name", "namespace"],
|
|
1435
|
+
minimal_config:
|
|
1436
|
+
'resource "xcsh_registration_approval" "example" {\n name = "example-registration-approval"\n namespace = "staging"\n}',
|
|
1437
|
+
dependencies: {
|
|
1438
|
+
requires: [],
|
|
1439
|
+
},
|
|
1440
|
+
import_syntax: "terraform import xcsh_registration_approval.example namespace/name",
|
|
1441
|
+
},
|
|
1430
1442
|
route: {
|
|
1431
1443
|
category: "load-balancing",
|
|
1432
1444
|
description:
|
|
@@ -277,6 +277,12 @@ export interface PromptOptions {
|
|
|
277
277
|
attribution?: MessageAttribution;
|
|
278
278
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
279
279
|
skipCompactionCheck?: boolean;
|
|
280
|
+
/**
|
|
281
|
+
* Raw provider "server tool" specs to inject into THIS turn's request tools, e.g.
|
|
282
|
+
* Anthropic's `{type:"web_search_20250305", name:"web_search", max_uses:N}`. Rides
|
|
283
|
+
* the provider `onPayload` seam (per-turn, no client-side tool registration).
|
|
284
|
+
*/
|
|
285
|
+
serverTools?: Record<string, unknown>[];
|
|
280
286
|
}
|
|
281
287
|
|
|
282
288
|
/** Result from cycleModel() */
|
|
@@ -2632,7 +2638,7 @@ export class AgentSession {
|
|
|
2632
2638
|
async #promptWithMessage(
|
|
2633
2639
|
message: AgentMessage,
|
|
2634
2640
|
expandedText: string,
|
|
2635
|
-
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
|
|
2641
|
+
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck" | "serverTools"> & {
|
|
2636
2642
|
prependMessages?: AgentMessage[];
|
|
2637
2643
|
skipPostPromptRecoveryWait?: boolean;
|
|
2638
2644
|
},
|
|
@@ -2753,7 +2759,10 @@ export class AgentSession {
|
|
|
2753
2759
|
return;
|
|
2754
2760
|
}
|
|
2755
2761
|
|
|
2756
|
-
const agentPromptOptions =
|
|
2762
|
+
const agentPromptOptions =
|
|
2763
|
+
options?.toolChoice || options?.serverTools
|
|
2764
|
+
? { toolChoice: options?.toolChoice, serverTools: options?.serverTools }
|
|
2765
|
+
: undefined;
|
|
2757
2766
|
await logger.ttftAttr("ttft.agent-loop-total", () =>
|
|
2758
2767
|
this.#promptAgentWithIdleRetry(messages, agentPromptOptions),
|
|
2759
2768
|
);
|
|
@@ -5601,7 +5610,10 @@ export class AgentSession {
|
|
|
5601
5610
|
this.#resolveRetry();
|
|
5602
5611
|
}
|
|
5603
5612
|
|
|
5604
|
-
async #promptAgentWithIdleRetry(
|
|
5613
|
+
async #promptAgentWithIdleRetry(
|
|
5614
|
+
messages: AgentMessage[],
|
|
5615
|
+
options?: { toolChoice?: ToolChoice; serverTools?: Record<string, unknown>[] },
|
|
5616
|
+
): Promise<void> {
|
|
5605
5617
|
const deadline = Date.now() + 30_000;
|
|
5606
5618
|
for (;;) {
|
|
5607
5619
|
try {
|