@f5-sales-demo/xcsh 19.74.2 → 19.75.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/capabilities.generated.ts +2 -2
- package/src/browser/capabilities.json +1 -1
- package/src/browser/chat-conformance.json +1 -1
- package/src/browser/chat-handler.ts +78 -103
- package/src/browser/chat-protocol.ts +5 -0
- package/src/browser/extension-bridge-tools.ts +16 -0
- package/src/browser/extension-bridge.ts +17 -1
- package/src/browser/headless-bridge.ts +9 -9
- package/src/browser/host-profiles.ts +164 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
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.75.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.75.0",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.75.0",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.75.0",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.75.0",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.75.0",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.75.0",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.75.0",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -26,7 +26,7 @@ export interface ExtensionCapabilities {
|
|
|
26
26
|
|
|
27
27
|
export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
|
|
28
28
|
"version": "0.1.0",
|
|
29
|
-
"contractVersion": "1.
|
|
29
|
+
"contractVersion": "1.10.0",
|
|
30
30
|
"multiPortDiscovery": true,
|
|
31
31
|
"protocol": "tool_request/result",
|
|
32
32
|
"tools": [
|
|
@@ -855,6 +855,6 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
|
|
|
855
855
|
}
|
|
856
856
|
};
|
|
857
857
|
|
|
858
|
-
export const EXTENSION_CONTRACT_VERSION = "1.
|
|
858
|
+
export const EXTENSION_CONTRACT_VERSION = "1.10.0";
|
|
859
859
|
|
|
860
860
|
export const EXTENSION_TOOL_NAMES: readonly string[] = ["ping","capabilities","reload","debug_exec","detach","set_bridge_port","navigate","login","scroll_to","resize_window","tabs_list","tabs_create","tabs_close","click","click_element","click_xy","type_text","form_input","key_press","select_option","label_select","file_upload","read_ax","get_page_text","query_dom","find","wait_for","assert_text","screenshot","read_console","read_network","diag_suspension","diag_bridges","diag_activation","diag_ttft","capture_login_flow","wait_for_api_response","get_page_context","javascript_tool","browser_batch","set_explain_mode","annotate"];
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "./chat-protocol";
|
|
33
33
|
import { CONSOLE_ROUTES } from "./console-routes.generated";
|
|
34
34
|
import type { BridgeServer } from "./extension-bridge";
|
|
35
|
+
import { type ClientHost, hostProfile } from "./host-profiles";
|
|
35
36
|
import { interpretPageState } from "./page-state-interpreter";
|
|
36
37
|
import { chatSpans } from "./ttft-spans";
|
|
37
38
|
|
|
@@ -163,7 +164,7 @@ export class ChatHandler {
|
|
|
163
164
|
});
|
|
164
165
|
chat.unsubscribe = unsubscribe;
|
|
165
166
|
|
|
166
|
-
const prompt = composeChatPrompt(req.text, req.context, req.mode);
|
|
167
|
+
const prompt = composeChatPrompt(req.text, req.context, req.mode, this.#server.clientHost);
|
|
167
168
|
|
|
168
169
|
try {
|
|
169
170
|
chat.promptAt = Date.now();
|
|
@@ -435,49 +436,6 @@ export function classifyChatErrorReason(message: string): ChatErrorReason | unde
|
|
|
435
436
|
return undefined;
|
|
436
437
|
}
|
|
437
438
|
|
|
438
|
-
/**
|
|
439
|
-
* Chrome-extension self-awareness prompt. Injected when xcsh is serving a browser
|
|
440
|
-
* chat (not the CLI TUI). Tells the LLM it's in a Chrome side panel alongside the
|
|
441
|
-
* F5 XC console, what tools it has, and how to behave differently from the CLI.
|
|
442
|
-
*/
|
|
443
|
-
const CHROME_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant for the F5 Distributed Cloud console, running as a Chrome browser side panel — not a terminal CLI.
|
|
444
|
-
|
|
445
|
-
CRITICAL: ALWAYS respond with TEXT FIRST. Do NOT jump straight to tool calls. The user sees a chat panel and expects a conversational text response, not silence while tools run in the background. For questions ("what page am I on?", "what is this?"), answer with text using the page context below — no tools needed. Only use tools when the user explicitly asks you to DO something (create, navigate, click, modify).
|
|
446
|
-
|
|
447
|
-
CONTEXT: The user sees a small chat window alongside the F5 XC admin console. You receive page-aware context each turn: the current URL (interpreted as workspace/resource/CRUD operation/namespace), the API resource JSON, and the accessibility tree. USE THIS CONTEXT to answer questions — don't call tools to find information you already have.
|
|
448
|
-
|
|
449
|
-
BEHAVIOR:
|
|
450
|
-
- Respond concisely with markdown. The chat panel is narrow — avoid long code blocks.
|
|
451
|
-
- You KNOW which page the user is on (injected below). Don't ask "what page are you on?" — tell them.
|
|
452
|
-
- For questions about the page/resource: answer from the injected context. No tools.
|
|
453
|
-
- If a blocking popup/survey appears, dismiss it by clicking the close button.
|
|
454
|
-
- If on the LOGIN page: use the login tool to log in. The login tool handles ALL environments — production (*.console.ves.volterra.io) AND staging (*.staging.volterra.us, login-staging.volterra.us). Do NOT claim the login tool is broken, unsupported, or doesn't work for staging — it does.
|
|
455
|
-
|
|
456
|
-
BROWSER AUTOMATION (when the user asks to create/modify/navigate resources):
|
|
457
|
-
- You are IN a Chrome browser. The active console tab is your workspace — use IT.
|
|
458
|
-
- For create/modify/delete: call catalog_workflow_runner IMMEDIATELY with ONE tool call per resource:
|
|
459
|
-
{"resource": "health-check", "operation": "create", "params": {"name": "foo", "namespace": "demo"}, "presentation": "guided"}
|
|
460
|
-
Do NOT read API specs first, do NOT create todos, do NOT orchestrate multi-step tool chains. The catalog_workflow_runner handles ALL the form navigation internally.
|
|
461
|
-
- Say a brief text message BEFORE the tool call: "Creating health check **foo** — watch the browser." Then call the tool. Nothing else.
|
|
462
|
-
- The human is WATCHING the form automation (fingerprint-before-click, highlights, ~1.5s/step). Do NOT use background API calls.
|
|
463
|
-
- The browser may be at 85% zoom — automation handles coordinates at any zoom.
|
|
464
|
-
- The console catalog has workflows for 100+ F5 XC resources.
|
|
465
|
-
- Do NOT open new tabs — drive the existing console tab.
|
|
466
|
-
|
|
467
|
-
MULTI-RESOURCE REQUESTS (when the user asks to create several resources in one prompt):
|
|
468
|
-
- Create resources in DEPENDENCY ORDER: health checks first, then origin pools (which reference health checks), then load balancers (which reference origin pools and app firewalls).
|
|
469
|
-
- After each catalog_workflow_runner call completes, IMMEDIATELY proceed to the next resource. Do NOT inspect, verify, click into, or navigate to the resource you just created. Do NOT open the JSON view. Do NOT read the page to confirm — the tool already confirmed success. Move directly to the next creation.
|
|
470
|
-
- Between resources, say ONE short line: "Health check created. Now creating origin pool **bar** — watch the browser." Then call the next tool.
|
|
471
|
-
- NEVER navigate to a list/detail/JSON view between creations. Stay on the automation path.
|
|
472
|
-
|
|
473
|
-
SAFETY — NEVER DO THESE:
|
|
474
|
-
- NEVER kill, stop, or manage processes on port 19222 — that is YOUR OWN bridge. Killing it kills you.
|
|
475
|
-
- NEVER run lsof, fuser, kill, or pkill on the bridge port. You ARE the bridge.
|
|
476
|
-
- NEVER use bash/shell tools to manage xcsh processes, ports, or the debugger connection.
|
|
477
|
-
- NEVER run commands that would terminate your own process or the WebSocket server.]
|
|
478
|
-
|
|
479
|
-
`;
|
|
480
|
-
|
|
481
439
|
const MODE_INSTRUCTIONS: Record<InteractionMode, string> = {
|
|
482
440
|
educational: "Explain concepts and settings in depth. Help the user understand what they're looking at and why.",
|
|
483
441
|
presentation: "Guide a structured walkthrough. Narrate each step clearly for a live audience.",
|
|
@@ -486,77 +444,94 @@ const MODE_INSTRUCTIONS: Record<InteractionMode, string> = {
|
|
|
486
444
|
annotation: "Create on-page teaching annotations that highlight key elements and explain their purpose.",
|
|
487
445
|
};
|
|
488
446
|
|
|
489
|
-
export function composeChatPrompt(
|
|
447
|
+
export function composeChatPrompt(
|
|
448
|
+
text: string,
|
|
449
|
+
context: PageContextSnapshot | null,
|
|
450
|
+
mode: InteractionMode,
|
|
451
|
+
host: ClientHost | null,
|
|
452
|
+
): string {
|
|
490
453
|
const parts: string[] = [];
|
|
491
454
|
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
455
|
+
// Host-aware self-awareness: the Chrome extension gets the browser panel prompt;
|
|
456
|
+
// an Office add-in (excel/powerpoint/word) gets its document-assistant prompt. A
|
|
457
|
+
// null/unannounced host falls back to the Chrome profile.
|
|
458
|
+
const profile = hostProfile(host);
|
|
459
|
+
parts.push(profile.systemPrompt);
|
|
460
|
+
|
|
461
|
+
// Browser hosts ALSO get an interaction mode + the page-context block. Document
|
|
462
|
+
// hosts get NEITHER: Office sends no page context and has no browser modes; its
|
|
463
|
+
// tools + document state arrive at runtime via set_host_tools.
|
|
464
|
+
if (profile.kind === "browser") {
|
|
465
|
+
parts.push(`[Chat mode: ${mode}] ${MODE_INSTRUCTIONS[mode]}`);
|
|
466
|
+
if (context) composeBrowserPageContext(parts, context);
|
|
467
|
+
}
|
|
495
468
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// raw URL it must guess about.
|
|
501
|
-
const pageState = interpretPageState(context.url, null, CONSOLE_ROUTES);
|
|
469
|
+
parts.push("");
|
|
470
|
+
parts.push(text);
|
|
471
|
+
return parts.join("\n");
|
|
472
|
+
}
|
|
502
473
|
|
|
503
|
-
|
|
504
|
-
|
|
474
|
+
/** Append the Chrome-only page-context block (interpreted page state + API body +
|
|
475
|
+
* accessibility tree) to `parts`. Browser hosts only — Office sends no context. */
|
|
476
|
+
function composeBrowserPageContext(parts: string[], context: PageContextSnapshot): void {
|
|
477
|
+
// Interpret the raw URL into structured page state (workspace, resource,
|
|
478
|
+
// CRUD operation, namespace) using deterministic route-pattern matching
|
|
479
|
+
// against console_ui.yaml — the LLM sees "origin_pool LIST in demo" not a
|
|
480
|
+
// raw URL it must guess about.
|
|
481
|
+
const pageState = interpretPageState(context.url, null, CONSOLE_ROUTES);
|
|
505
482
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
parts.push(`Tenant: ${pageState.tenant ?? "unknown"} (${pageState.environment ?? "unknown"} environment)`);
|
|
509
|
-
}
|
|
483
|
+
parts.push("");
|
|
484
|
+
parts.push(`[Page context — captured at ${new Date(context.capturedAt).toISOString()}]`);
|
|
510
485
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
"Use the login tool with their email and password to log in. The login tool handles both production and staging Keycloak (including login-staging.volterra.us). Do NOT bypass it or claim it doesn't support staging.",
|
|
516
|
-
);
|
|
517
|
-
} else if (pageState.resource) {
|
|
518
|
-
const opLabel = pageState.operation.toUpperCase();
|
|
519
|
-
const nsLabel = pageState.namespace ? ` in namespace "${pageState.namespace}"` : "";
|
|
520
|
-
const nameLabel = pageState.resourceName ? ` — instance "${pageState.resourceName}"` : "";
|
|
521
|
-
parts.push(`Page: ${pageState.resource} ${opLabel}${nameLabel} (workspace: ${pageState.workspace}${nsLabel})`);
|
|
522
|
-
} else {
|
|
523
|
-
parts.push(`Page: ${context.title} (unrecognized resource)`);
|
|
524
|
-
}
|
|
525
|
-
if (pageState.modalBlocking) {
|
|
526
|
-
parts.push(`⚠ Modal blocking: ${pageState.modalText ?? "unknown overlay"}`);
|
|
527
|
-
}
|
|
528
|
-
parts.push(`URL: ${context.url}`);
|
|
529
|
-
parts.push(`Title: ${context.title}`);
|
|
530
|
-
|
|
531
|
-
if (context.api) {
|
|
532
|
-
parts.push(
|
|
533
|
-
`API resource (${context.api.resourceType ?? "unknown"}, status ${context.api.status}): ${context.api.url}`,
|
|
534
|
-
);
|
|
535
|
-
if (context.api.body) {
|
|
536
|
-
const body =
|
|
537
|
-
typeof context.api.body === "string" ? context.api.body : JSON.stringify(context.api.body, null, 2);
|
|
538
|
-
parts.push(body);
|
|
539
|
-
}
|
|
540
|
-
if (context.api.truncated) {
|
|
541
|
-
parts.push("[API body was truncated]");
|
|
542
|
-
}
|
|
543
|
-
}
|
|
486
|
+
// Tenant + environment (the LLM knows WHICH tenant on WHICH environment).
|
|
487
|
+
if (pageState.tenant || pageState.environment) {
|
|
488
|
+
parts.push(`Tenant: ${pageState.tenant ?? "unknown"} (${pageState.environment ?? "unknown"} environment)`);
|
|
489
|
+
}
|
|
544
490
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
491
|
+
// Structured page state (the interpreted context the LLM acts on).
|
|
492
|
+
if (pageState.operation === "login") {
|
|
493
|
+
parts.push("Page: LOGIN — session expired or first login. The user is on the Keycloak authentication page.");
|
|
494
|
+
parts.push(
|
|
495
|
+
"Use the login tool with their email and password to log in. The login tool handles both production and staging Keycloak (including login-staging.volterra.us). Do NOT bypass it or claim it doesn't support staging.",
|
|
496
|
+
);
|
|
497
|
+
} else if (pageState.resource) {
|
|
498
|
+
const opLabel = pageState.operation.toUpperCase();
|
|
499
|
+
const nsLabel = pageState.namespace ? ` in namespace "${pageState.namespace}"` : "";
|
|
500
|
+
const nameLabel = pageState.resourceName ? ` — instance "${pageState.resourceName}"` : "";
|
|
501
|
+
parts.push(`Page: ${pageState.resource} ${opLabel}${nameLabel} (workspace: ${pageState.workspace}${nsLabel})`);
|
|
502
|
+
} else {
|
|
503
|
+
parts.push(`Page: ${context.title} (unrecognized resource)`);
|
|
504
|
+
}
|
|
505
|
+
if (pageState.modalBlocking) {
|
|
506
|
+
parts.push(`⚠ Modal blocking: ${pageState.modalText ?? "unknown overlay"}`);
|
|
507
|
+
}
|
|
508
|
+
parts.push(`URL: ${context.url}`);
|
|
509
|
+
parts.push(`Title: ${context.title}`);
|
|
510
|
+
|
|
511
|
+
if (context.api) {
|
|
512
|
+
parts.push(
|
|
513
|
+
`API resource (${context.api.resourceType ?? "unknown"}, status ${context.api.status}): ${context.api.url}`,
|
|
514
|
+
);
|
|
515
|
+
if (context.api.body) {
|
|
516
|
+
const body =
|
|
517
|
+
typeof context.api.body === "string" ? context.api.body : JSON.stringify(context.api.body, null, 2);
|
|
518
|
+
parts.push(body);
|
|
548
519
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
parts.push("[Page context was truncated]");
|
|
520
|
+
if (context.api.truncated) {
|
|
521
|
+
parts.push("[API body was truncated]");
|
|
552
522
|
}
|
|
523
|
+
}
|
|
553
524
|
|
|
554
|
-
|
|
525
|
+
if (context.ax) {
|
|
526
|
+
const ax = typeof context.ax === "string" ? context.ax : JSON.stringify(context.ax);
|
|
527
|
+
parts.push(`Accessibility tree: ${ax}`);
|
|
555
528
|
}
|
|
556
529
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
530
|
+
if (context.truncated) {
|
|
531
|
+
parts.push("[Page context was truncated]");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
parts.push("---");
|
|
560
535
|
}
|
|
561
536
|
|
|
562
537
|
export function classifyReferenceKind(url: string): "doc" | "console" {
|
|
@@ -15,6 +15,11 @@ import type {
|
|
|
15
15
|
RpcHostToolUpdate,
|
|
16
16
|
} from "../host-tools/types";
|
|
17
17
|
|
|
18
|
+
// The client host announced on the `hello` handshake (contract 1.10.0). Re-exported
|
|
19
|
+
// (type-only, fully erased) so browser-safe consumers — the office-pane bundle —
|
|
20
|
+
// can share the wire vocabulary without importing the host-profiles prompt data.
|
|
21
|
+
export type { ClientHost } from "./host-profiles";
|
|
22
|
+
|
|
18
23
|
// ---------------------------------------------------------------------------
|
|
19
24
|
// Page context snapshot (auto-attached by extension to every chat_request)
|
|
20
25
|
// ---------------------------------------------------------------------------
|
|
@@ -97,3 +97,19 @@ export const BROWSER_TOOL_NAMES: readonly string[] = [
|
|
|
97
97
|
"annotate",
|
|
98
98
|
"set_explain_mode",
|
|
99
99
|
];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builtin agent tools scoped into the headless OFFICE bridge (`xcsh office serve`).
|
|
103
|
+
* The Office task pane drives a document (Excel/PowerPoint/Word), NOT a browser, so
|
|
104
|
+
* it must get NONE of the {@link BROWSER_TOOL_NAMES} — those would be hallucinated
|
|
105
|
+
* against a host with no browser to drive. The document's own tools arrive at
|
|
106
|
+
* runtime over the bridge via `set_host_tools`; this list is only the minimal
|
|
107
|
+
* general-purpose, host-neutral builtin toolset a document assistant can safely use.
|
|
108
|
+
*
|
|
109
|
+
* NOTE: an EMPTY list cannot express "no builtin tools" — createAgentSession/createTools
|
|
110
|
+
* treat `[]` as "unscoped" and hand back the FULL builtin registry (bash/edit/python/
|
|
111
|
+
* browser/…). So we pass an explicit minimal set instead. `calc` is the one builtin
|
|
112
|
+
* that is pure computation — no browser, no shell, no filesystem, no network — and is
|
|
113
|
+
* genuinely useful for spreadsheet/document math.
|
|
114
|
+
*/
|
|
115
|
+
export const OFFICE_TOOL_NAMES: readonly string[] = ["calc"];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { Server, ServerWebSocket } from "bun";
|
|
3
3
|
import { LOCALIP_HOST } from "./bridge-cert";
|
|
4
|
+
import { type ClientHost, isClientHost } from "./host-profiles";
|
|
4
5
|
|
|
5
6
|
export interface ToolResult {
|
|
6
7
|
content: unknown;
|
|
@@ -177,6 +178,9 @@ export class BridgeServer {
|
|
|
177
178
|
#onMessage: Array<(msg: Record<string, unknown>) => void> = [];
|
|
178
179
|
/** Heartbeat interval that sends pings to keep the MV3 service worker alive (sweep + chat). */
|
|
179
180
|
#heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
181
|
+
/** The client host learned from the `hello` handshake (null until a client
|
|
182
|
+
* announces one; the Chrome extension omits it → stays null → chrome profile). */
|
|
183
|
+
#clientHost: ClientHost | null = null;
|
|
180
184
|
/** Provider of this process's tenant identity, answering the `hello` handshake. */
|
|
181
185
|
#sessionInfo:
|
|
182
186
|
| (() => {
|
|
@@ -203,6 +207,12 @@ export class BridgeServer {
|
|
|
203
207
|
return this.#clients.size > 0;
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
/** The client host announced by the connected client's `hello` (null until a
|
|
211
|
+
* client announces one). Read by the ChatHandler to pick the host-aware prompt. */
|
|
212
|
+
get clientHost(): ClientHost | null {
|
|
213
|
+
return this.#clientHost;
|
|
214
|
+
}
|
|
215
|
+
|
|
206
216
|
/** Number of connected extension clients (channels). */
|
|
207
217
|
get connectedCount(): number {
|
|
208
218
|
return this.#clients.size;
|
|
@@ -364,7 +374,7 @@ export class BridgeServer {
|
|
|
364
374
|
|
|
365
375
|
#handleMessage(ws: ServerWebSocket<undefined>, message: string | Buffer): void {
|
|
366
376
|
const text = typeof message === "string" ? message : message.toString("utf8");
|
|
367
|
-
let msg: { type?: string; id?: string; content?: unknown; is_error?: boolean };
|
|
377
|
+
let msg: { type?: string; id?: string; content?: unknown; is_error?: boolean; host?: unknown };
|
|
368
378
|
try {
|
|
369
379
|
msg = JSON.parse(text);
|
|
370
380
|
} catch {
|
|
@@ -379,6 +389,11 @@ export class BridgeServer {
|
|
|
379
389
|
ws.send(JSON.stringify({ type: "pong" }));
|
|
380
390
|
} else if (msg.type === "hello") {
|
|
381
391
|
// Identity handshake: tell the extension which tenant this process serves.
|
|
392
|
+
// Record the announced client host (contract 1.10.0): Office sends its
|
|
393
|
+
// lowercased Office.context.host ("excel"|"powerpoint"|"word"); the Chrome
|
|
394
|
+
// extension omits it → clientHost stays null → the browser profile. Invalid
|
|
395
|
+
// values are ignored (null retained). Echoed back so the client can confirm.
|
|
396
|
+
if (isClientHost(msg.host)) this.#clientHost = msg.host;
|
|
382
397
|
const info = this.#sessionInfo?.() ?? {
|
|
383
398
|
tenant: null,
|
|
384
399
|
env: null,
|
|
@@ -396,6 +411,7 @@ export class BridgeServer {
|
|
|
396
411
|
env: info.env,
|
|
397
412
|
apiUrl: info.apiUrl,
|
|
398
413
|
contextBound: info.contextBound,
|
|
414
|
+
host: this.#clientHost,
|
|
399
415
|
pid: process.pid,
|
|
400
416
|
wssPort: this.wssPort,
|
|
401
417
|
canConfigureProvider: true,
|
|
@@ -21,7 +21,7 @@ import { deriveTenantEnv } from "../services/xcsh-env";
|
|
|
21
21
|
import { resolveBridgeTls } from "./bridge-cert";
|
|
22
22
|
import { ChatHandler } from "./chat-handler";
|
|
23
23
|
import { type BridgeServer, startBridgeServer } from "./extension-bridge";
|
|
24
|
-
import {
|
|
24
|
+
import { OFFICE_TOOL_NAMES } from "./extension-bridge-tools";
|
|
25
25
|
import { setSharedBridgeServer } from "./provider";
|
|
26
26
|
|
|
27
27
|
/** A running headless bridge + a teardown that disposes the chat handler and
|
|
@@ -67,7 +67,6 @@ export interface HeadlessBridgeDeps {
|
|
|
67
67
|
resolveBridgeTls: typeof resolveBridgeTls;
|
|
68
68
|
startBridgeServer: typeof startBridgeServer;
|
|
69
69
|
setSharedBridgeServer: typeof setSharedBridgeServer;
|
|
70
|
-
createExtensionBridgeTools: typeof createExtensionBridgeTools;
|
|
71
70
|
createAgentSession: typeof createAgentSession;
|
|
72
71
|
ChatHandlerCtor: typeof ChatHandler;
|
|
73
72
|
}
|
|
@@ -95,7 +94,6 @@ const defaultDeps: HeadlessBridgeDeps = {
|
|
|
95
94
|
resolveBridgeTls,
|
|
96
95
|
startBridgeServer,
|
|
97
96
|
setSharedBridgeServer,
|
|
98
|
-
createExtensionBridgeTools,
|
|
99
97
|
createAgentSession,
|
|
100
98
|
ChatHandlerCtor: ChatHandler,
|
|
101
99
|
};
|
|
@@ -130,15 +128,17 @@ export async function startHeadlessChatBridge(deps: HeadlessBridgeDeps = default
|
|
|
130
128
|
// selectProvider() reuses a dead bridge. The caller (startOfficeServe) treats
|
|
131
129
|
// the rethrow as a non-fatal "pane only" fallback.
|
|
132
130
|
try {
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
131
|
+
// Create ONE headless Office session scoped to the minimal general builtin set
|
|
132
|
+
// (OFFICE_TOOL_NAMES) with NO browser tools — neither the browser BUILTINS
|
|
133
|
+
// (navigate/click/…) nor the bridge-proxying browser CUSTOM tools, both of which
|
|
134
|
+
// would be hallucinated in a document task pane (there is no browser to drive).
|
|
135
|
+
// The document's own tools (Excel/Word/PowerPoint) arrive at runtime via
|
|
136
|
+
// set_host_tools; chat over xcsh's configured provider needs no browser tooling.
|
|
137
137
|
const { session } = await deps.createAgentSession({
|
|
138
138
|
cwd,
|
|
139
139
|
hasUI: false,
|
|
140
|
-
toolNames: [...
|
|
141
|
-
customTools:
|
|
140
|
+
toolNames: [...OFFICE_TOOL_NAMES],
|
|
141
|
+
customTools: [],
|
|
142
142
|
// Headless: no MCP/LSP/extension discovery — lean, no network/blocking prompts.
|
|
143
143
|
enableMCP: false,
|
|
144
144
|
enableLsp: false,
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-aware system prompts for the bridge chat engine.
|
|
3
|
+
*
|
|
4
|
+
* The same `hello`/`hello_ack` bridge serves two very different clients: the
|
|
5
|
+
* Chrome extension (a browser side panel driving the F5 XC console) and the
|
|
6
|
+
* Office add-in task pane (Excel / PowerPoint / Word, driving the open
|
|
7
|
+
* document). A `host` field on the handshake tells the engine which one it is,
|
|
8
|
+
* so `composeChatPrompt` can inject the RIGHT self-awareness prompt instead of
|
|
9
|
+
* unconditionally telling every client it is a Chrome browser panel.
|
|
10
|
+
*
|
|
11
|
+
* Browser-safe: pure string/data module, no node/bun imports — so it can be
|
|
12
|
+
* type-imported by the office-pane's browser bundle via the native contract.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The client hosts the bridge can serve, as lowercase wire values. Office maps
|
|
17
|
+
* its `Office.context.host` ("Excel"|"PowerPoint"|"Word") to these; the Chrome
|
|
18
|
+
* extension is "chrome". Outlook is intentionally absent — the add-in omits the
|
|
19
|
+
* host for hosts without a document assistant, so the engine falls back to the
|
|
20
|
+
* default profile.
|
|
21
|
+
*/
|
|
22
|
+
export type ClientHost = "chrome" | "excel" | "powerpoint" | "word";
|
|
23
|
+
|
|
24
|
+
/** Every {@link ClientHost} value, for iteration + validation. */
|
|
25
|
+
export const CLIENT_HOSTS: readonly ClientHost[] = ["chrome", "excel", "powerpoint", "word"];
|
|
26
|
+
|
|
27
|
+
/** Runtime guard: true when `v` is a valid {@link ClientHost} wire value. */
|
|
28
|
+
export function isClientHost(v: unknown): v is ClientHost {
|
|
29
|
+
return typeof v === "string" && (CLIENT_HOSTS as readonly string[]).includes(v);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A host profile pairs a `kind` (browser vs document) with the self-awareness
|
|
34
|
+
* system prompt for that host. `kind` gates browser-only behavior in
|
|
35
|
+
* `composeChatPrompt` (interaction modes + the page-context block) — document
|
|
36
|
+
* hosts get neither.
|
|
37
|
+
*/
|
|
38
|
+
export interface HostProfile {
|
|
39
|
+
kind: "browser" | "document";
|
|
40
|
+
systemPrompt: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Chrome-extension self-awareness prompt. Injected when xcsh is serving a browser
|
|
45
|
+
* chat (not the CLI TUI). Tells the LLM it's in a Chrome side panel alongside the
|
|
46
|
+
* F5 XC console, what tools it has, and how to behave differently from the CLI.
|
|
47
|
+
*/
|
|
48
|
+
export const CHROME_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant for the F5 Distributed Cloud console, running as a Chrome browser side panel — not a terminal CLI.
|
|
49
|
+
|
|
50
|
+
CRITICAL: ALWAYS respond with TEXT FIRST. Do NOT jump straight to tool calls. The user sees a chat panel and expects a conversational text response, not silence while tools run in the background. For questions ("what page am I on?", "what is this?"), answer with text using the page context below — no tools needed. Only use tools when the user explicitly asks you to DO something (create, navigate, click, modify).
|
|
51
|
+
|
|
52
|
+
CONTEXT: The user sees a small chat window alongside the F5 XC admin console. You receive page-aware context each turn: the current URL (interpreted as workspace/resource/CRUD operation/namespace), the API resource JSON, and the accessibility tree. USE THIS CONTEXT to answer questions — don't call tools to find information you already have.
|
|
53
|
+
|
|
54
|
+
BEHAVIOR:
|
|
55
|
+
- Respond concisely with markdown. The chat panel is narrow — avoid long code blocks.
|
|
56
|
+
- You KNOW which page the user is on (injected below). Don't ask "what page are you on?" — tell them.
|
|
57
|
+
- For questions about the page/resource: answer from the injected context. No tools.
|
|
58
|
+
- If a blocking popup/survey appears, dismiss it by clicking the close button.
|
|
59
|
+
- If on the LOGIN page: use the login tool to log in. The login tool handles ALL environments — production (*.console.ves.volterra.io) AND staging (*.staging.volterra.us, login-staging.volterra.us). Do NOT claim the login tool is broken, unsupported, or doesn't work for staging — it does.
|
|
60
|
+
|
|
61
|
+
BROWSER AUTOMATION (when the user asks to create/modify/navigate resources):
|
|
62
|
+
- You are IN a Chrome browser. The active console tab is your workspace — use IT.
|
|
63
|
+
- For create/modify/delete: call catalog_workflow_runner IMMEDIATELY with ONE tool call per resource:
|
|
64
|
+
{"resource": "health-check", "operation": "create", "params": {"name": "foo", "namespace": "demo"}, "presentation": "guided"}
|
|
65
|
+
Do NOT read API specs first, do NOT create todos, do NOT orchestrate multi-step tool chains. The catalog_workflow_runner handles ALL the form navigation internally.
|
|
66
|
+
- Say a brief text message BEFORE the tool call: "Creating health check **foo** — watch the browser." Then call the tool. Nothing else.
|
|
67
|
+
- The human is WATCHING the form automation (fingerprint-before-click, highlights, ~1.5s/step). Do NOT use background API calls.
|
|
68
|
+
- The browser may be at 85% zoom — automation handles coordinates at any zoom.
|
|
69
|
+
- The console catalog has workflows for 100+ F5 XC resources.
|
|
70
|
+
- Do NOT open new tabs — drive the existing console tab.
|
|
71
|
+
|
|
72
|
+
MULTI-RESOURCE REQUESTS (when the user asks to create several resources in one prompt):
|
|
73
|
+
- Create resources in DEPENDENCY ORDER: health checks first, then origin pools (which reference health checks), then load balancers (which reference origin pools and app firewalls).
|
|
74
|
+
- After each catalog_workflow_runner call completes, IMMEDIATELY proceed to the next resource. Do NOT inspect, verify, click into, or navigate to the resource you just created. Do NOT open the JSON view. Do NOT read the page to confirm — the tool already confirmed success. Move directly to the next creation.
|
|
75
|
+
- Between resources, say ONE short line: "Health check created. Now creating origin pool **bar** — watch the browser." Then call the next tool.
|
|
76
|
+
- NEVER navigate to a list/detail/JSON view between creations. Stay on the automation path.
|
|
77
|
+
|
|
78
|
+
SAFETY — NEVER DO THESE:
|
|
79
|
+
- NEVER kill, stop, or manage processes on port 19222 — that is YOUR OWN bridge. Killing it kills you.
|
|
80
|
+
- NEVER run lsof, fuser, kill, or pkill on the bridge port. You ARE the bridge.
|
|
81
|
+
- NEVER use bash/shell tools to manage xcsh processes, ports, or the debugger connection.
|
|
82
|
+
- NEVER run commands that would terminate your own process or the WebSocket server.]
|
|
83
|
+
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Excel task-pane self-awareness prompt. The assistant works the OPEN workbook
|
|
88
|
+
* via host tools (arriving at runtime over the bridge), thinking in cells,
|
|
89
|
+
* ranges, and formula dependencies.
|
|
90
|
+
*/
|
|
91
|
+
const EXCEL_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant running as a task pane inside Microsoft Excel — not a terminal CLI. You help the user with the OPEN workbook using the Excel host tools available to you.
|
|
92
|
+
|
|
93
|
+
CRITICAL: ALWAYS respond with TEXT FIRST — the user sees a chat pane and expects a conversational reply, not silence while tools run. Answer questions from the data you read; only WRITE to the workbook when the user asks you to.
|
|
94
|
+
|
|
95
|
+
CONTEXT: You can only access the open workbook. Think in cells, ranges, and — above all — FORMULAS and their dependencies:
|
|
96
|
+
- Preserve formula relationships. When you change a cell, let dependent cells recompute; do not overwrite a formula with its current value unless asked.
|
|
97
|
+
- Warn the user before overwriting existing cell contents.
|
|
98
|
+
- Cite specific cells and ranges precisely (e.g. A1, Sheet1!B2:B10) so the user can follow along.
|
|
99
|
+
|
|
100
|
+
BEHAVIOR:
|
|
101
|
+
- Respond concisely with markdown. The task pane is narrow — avoid long code blocks.
|
|
102
|
+
- Read the workbook to answer questions about its data; do not guess.
|
|
103
|
+
- Make edits only when asked, one clear change at a time, and say what you changed and where.]
|
|
104
|
+
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* PowerPoint task-pane self-awareness prompt. The assistant works the OPEN
|
|
109
|
+
* presentation via host tools, thinking in slides, shapes, and the slide master.
|
|
110
|
+
*/
|
|
111
|
+
const POWERPOINT_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant running as a task pane inside Microsoft PowerPoint — not a terminal CLI. You help the user with the OPEN presentation using the PowerPoint host tools available to you.
|
|
112
|
+
|
|
113
|
+
CRITICAL: ALWAYS respond with TEXT FIRST — the user sees a chat pane and expects a conversational reply, not silence while tools run. Answer questions from what you read; only edit the deck when the user asks you to.
|
|
114
|
+
|
|
115
|
+
CONTEXT: You can only access the open presentation. Think in slides, shapes, and the slide master:
|
|
116
|
+
- Conform any new content to the deck's existing template, fonts, and colors — do not introduce a different look.
|
|
117
|
+
- Make pinpoint, per-slide edits. Do NOT regenerate the whole deck to change one thing.
|
|
118
|
+
- Refer to slides by number so the user can follow along.
|
|
119
|
+
|
|
120
|
+
BEHAVIOR:
|
|
121
|
+
- Respond concisely with markdown. The task pane is narrow — avoid long code blocks.
|
|
122
|
+
- Read the presentation to answer questions about it; do not guess.
|
|
123
|
+
- Make edits only when asked, one clear change at a time, and say which slide you changed.]
|
|
124
|
+
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Word task-pane self-awareness prompt. The assistant works the OPEN document
|
|
129
|
+
* via host tools, thinking in paragraphs, the selection, comments, and tracked
|
|
130
|
+
* changes.
|
|
131
|
+
*/
|
|
132
|
+
const WORD_CHAT_SYSTEM_PROMPT = `[System: You are xcsh, an AI assistant running as a task pane inside Microsoft Word — not a terminal CLI. You help the user with the OPEN document using the Word host tools available to you.
|
|
133
|
+
|
|
134
|
+
CRITICAL: ALWAYS respond with TEXT FIRST — the user sees a chat pane and expects a conversational reply, not silence while tools run. Answer questions from what you read; only edit the document when the user asks you to.
|
|
135
|
+
|
|
136
|
+
CONTEXT: You can only access the open document. Think in paragraphs, the current selection, comments, and tracked changes:
|
|
137
|
+
- Preserve the document's styles and numbering — do not flatten formatting.
|
|
138
|
+
- Describe your edits so the user can review them, and prefer changes the user can accept or reject.
|
|
139
|
+
- When the user refers to "the selection" (or "this"), act on the current selection.
|
|
140
|
+
|
|
141
|
+
BEHAVIOR:
|
|
142
|
+
- Respond concisely with markdown. The task pane is narrow — avoid long code blocks.
|
|
143
|
+
- Read the document to answer questions about it; do not guess.
|
|
144
|
+
- Make edits only when asked, one clear change at a time, and say what you changed.]
|
|
145
|
+
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
/** The self-awareness profile per client host. */
|
|
149
|
+
export const HOST_PROFILES: Record<ClientHost, HostProfile> = {
|
|
150
|
+
chrome: { kind: "browser", systemPrompt: CHROME_CHAT_SYSTEM_PROMPT },
|
|
151
|
+
excel: { kind: "document", systemPrompt: EXCEL_CHAT_SYSTEM_PROMPT },
|
|
152
|
+
powerpoint: { kind: "document", systemPrompt: POWERPOINT_CHAT_SYSTEM_PROMPT },
|
|
153
|
+
word: { kind: "document", systemPrompt: WORD_CHAT_SYSTEM_PROMPT },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** The host assumed when a client does not announce one (the Chrome extension,
|
|
157
|
+
* whose handshake predates the `host` field). */
|
|
158
|
+
export const DEFAULT_HOST: ClientHost = "chrome";
|
|
159
|
+
|
|
160
|
+
/** Resolve the profile for a host, falling back to the {@link DEFAULT_HOST}
|
|
161
|
+
* profile for a null/undefined host (an unannounced or non-document client). */
|
|
162
|
+
export function hostProfile(host: ClientHost | null | undefined): HostProfile {
|
|
163
|
+
return HOST_PROFILES[host ?? DEFAULT_HOST];
|
|
164
|
+
}
|
|
@@ -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.75.0",
|
|
21
|
+
"commit": "ac9635391c489c70d35c68a0e1cacdbae9f7e631",
|
|
22
|
+
"shortCommit": "ac96353",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.75.0",
|
|
25
|
+
"commitDate": "2026-07-22T13:46:55Z",
|
|
26
|
+
"buildDate": "2026-07-22T14:09:19.426Z",
|
|
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/ac9635391c489c70d35c68a0e1cacdbae9f7e631",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.75.0"
|
|
33
33
|
};
|