@f5-sales-demo/xcsh 19.64.2 → 19.65.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 +6 -3
- package/src/browser/capabilities.json +5 -2
- package/src/browser/chat-conformance.json +82 -1
- package/src/browser/chat-handler.ts +90 -0
- package/src/browser/chat-protocol.ts +60 -9
- package/src/browser/extension-bridge.ts +1 -0
- package/src/host-tools/guards.ts +30 -0
- package/src/host-tools/host-tools.ts +0 -18
- package/src/host-tools/index.ts +2 -6
- package/src/host-tools/types.ts +4 -1
- 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.65.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.65.0",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.65.0",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.65.0",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.65.0",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.65.0",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.65.0",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.65.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.9.0",
|
|
30
30
|
"multiPortDiscovery": true,
|
|
31
31
|
"protocol": "tool_request/result",
|
|
32
32
|
"tools": [
|
|
@@ -832,7 +832,10 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
|
|
|
832
832
|
"host_tool_call",
|
|
833
833
|
"host_tool_update",
|
|
834
834
|
"host_tool_result",
|
|
835
|
-
"host_tool_cancel"
|
|
835
|
+
"host_tool_cancel",
|
|
836
|
+
"configure",
|
|
837
|
+
"configure_ack",
|
|
838
|
+
"configure_error"
|
|
836
839
|
],
|
|
837
840
|
"description": "User ↔ xcsh chat over the bridge. The extension side panel sends chat_request (with mode and page-context snapshot); xcsh streams chat_delta tokens then a terminal chat_done (with reference links) or chat_error. Chat ids are prefixed \"c-\". Tool calls during a turn use the normal tool_request flow. chat_stop halts a streaming response. chat_tool_notice is emitted by the EXTENSION (the service worker) to the panel as a best-effort UI signal when a tool runs during a turn — it is NOT sent by xcsh; xcsh must not produce it to avoid double-rendering in the panel.",
|
|
838
841
|
"promptHints": {
|
|
@@ -852,6 +855,6 @@ export const EXTENSION_CAPABILITIES: ExtensionCapabilities = {
|
|
|
852
855
|
}
|
|
853
856
|
};
|
|
854
857
|
|
|
855
|
-
export const EXTENSION_CONTRACT_VERSION = "1.
|
|
858
|
+
export const EXTENSION_CONTRACT_VERSION = "1.9.0";
|
|
856
859
|
|
|
857
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"];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.1.0",
|
|
3
|
-
"contractVersion": "1.
|
|
3
|
+
"contractVersion": "1.9.0",
|
|
4
4
|
"multiPortDiscovery": true,
|
|
5
5
|
"protocol": "tool_request/result",
|
|
6
6
|
"tools": [
|
|
@@ -740,7 +740,10 @@
|
|
|
740
740
|
"host_tool_call",
|
|
741
741
|
"host_tool_update",
|
|
742
742
|
"host_tool_result",
|
|
743
|
-
"host_tool_cancel"
|
|
743
|
+
"host_tool_cancel",
|
|
744
|
+
"configure",
|
|
745
|
+
"configure_ack",
|
|
746
|
+
"configure_error"
|
|
744
747
|
],
|
|
745
748
|
"description": "User ↔ xcsh chat over the bridge. The extension side panel sends chat_request (with mode and page-context snapshot); xcsh streams chat_delta tokens then a terminal chat_done (with reference links) or chat_error. Chat ids are prefixed \"c-\". Tool calls during a turn use the normal tool_request flow. chat_stop halts a streaming response. chat_tool_notice is emitted by the EXTENSION (the service worker) to the panel as a best-effort UI signal when a tool runs during a turn — it is NOT sent by xcsh; xcsh must not produce it to avoid double-rendering in the panel.",
|
|
746
749
|
"promptHints": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "1.
|
|
2
|
+
"contractVersion": "1.9.0",
|
|
3
3
|
"schemas": {
|
|
4
4
|
"chat_request": {
|
|
5
5
|
"type": "object",
|
|
@@ -356,6 +356,52 @@
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
},
|
|
359
|
+
"configure": {
|
|
360
|
+
"type": "object",
|
|
361
|
+
"required": ["type", "token"],
|
|
362
|
+
"properties": {
|
|
363
|
+
"type": {
|
|
364
|
+
"const": "configure",
|
|
365
|
+
"type": "string"
|
|
366
|
+
},
|
|
367
|
+
"baseUrl": {
|
|
368
|
+
"type": "string"
|
|
369
|
+
},
|
|
370
|
+
"token": {
|
|
371
|
+
"type": "string",
|
|
372
|
+
"minLength": 1
|
|
373
|
+
},
|
|
374
|
+
"model": {
|
|
375
|
+
"type": "string"
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
"configure_ack": {
|
|
380
|
+
"type": "object",
|
|
381
|
+
"required": ["type", "model"],
|
|
382
|
+
"properties": {
|
|
383
|
+
"type": {
|
|
384
|
+
"const": "configure_ack",
|
|
385
|
+
"type": "string"
|
|
386
|
+
},
|
|
387
|
+
"model": {
|
|
388
|
+
"type": "string"
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
"configure_error": {
|
|
393
|
+
"type": "object",
|
|
394
|
+
"required": ["type", "error"],
|
|
395
|
+
"properties": {
|
|
396
|
+
"type": {
|
|
397
|
+
"const": "configure_error",
|
|
398
|
+
"type": "string"
|
|
399
|
+
},
|
|
400
|
+
"error": {
|
|
401
|
+
"type": "string"
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
},
|
|
359
405
|
"host_tool_call": {
|
|
360
406
|
"type": "object",
|
|
361
407
|
"required": ["type", "id", "toolCallId", "toolName", "arguments"],
|
|
@@ -713,6 +759,24 @@
|
|
|
713
759
|
"type": "set_host_tools_error",
|
|
714
760
|
"error": "Host tool \"office_read_range\" must provide a non-empty description"
|
|
715
761
|
},
|
|
762
|
+
"configure": {
|
|
763
|
+
"type": "configure",
|
|
764
|
+
"baseUrl": "https://f5ai.pd.f5net.com/anthropic",
|
|
765
|
+
"token": "sk-example-token-123",
|
|
766
|
+
"model": "claude-opus-4-8"
|
|
767
|
+
},
|
|
768
|
+
"configure_no_baseUrl": {
|
|
769
|
+
"type": "configure",
|
|
770
|
+
"token": "sk-example-token-123"
|
|
771
|
+
},
|
|
772
|
+
"configure_ack": {
|
|
773
|
+
"type": "configure_ack",
|
|
774
|
+
"model": "claude-opus-4-8"
|
|
775
|
+
},
|
|
776
|
+
"configure_error": {
|
|
777
|
+
"type": "configure_error",
|
|
778
|
+
"error": "No model anthropic/claude-opus-4-8 available"
|
|
779
|
+
},
|
|
716
780
|
"host_tool_call": {
|
|
717
781
|
"type": "host_tool_call",
|
|
718
782
|
"id": "7295551234567890",
|
|
@@ -933,6 +997,23 @@
|
|
|
933
997
|
"content": "nope"
|
|
934
998
|
}
|
|
935
999
|
}
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
"schema": "configure",
|
|
1003
|
+
"why": "token missing",
|
|
1004
|
+
"value": {
|
|
1005
|
+
"type": "configure",
|
|
1006
|
+
"baseUrl": "https://f5ai.pd.f5net.com/anthropic",
|
|
1007
|
+
"model": "claude-opus-4-8"
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
"schema": "configure",
|
|
1012
|
+
"why": "token empty",
|
|
1013
|
+
"value": {
|
|
1014
|
+
"type": "configure",
|
|
1015
|
+
"token": ""
|
|
1016
|
+
}
|
|
936
1017
|
}
|
|
937
1018
|
]
|
|
938
1019
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@f5-sales-demo/pi-ai";
|
|
2
|
+
import { DEFAULT_MODEL_ROLE } from "../config/settings-schema";
|
|
2
3
|
import {
|
|
3
4
|
isRpcHostToolResult,
|
|
4
5
|
isRpcHostToolUpdate,
|
|
@@ -14,11 +15,15 @@ import {
|
|
|
14
15
|
type ChatKeepalive,
|
|
15
16
|
type ChatReference,
|
|
16
17
|
type ChatRequest,
|
|
18
|
+
type Configure,
|
|
19
|
+
type ConfigureAck,
|
|
20
|
+
type ConfigureError,
|
|
17
21
|
type HostToolResult,
|
|
18
22
|
type HostToolUpdate,
|
|
19
23
|
type InteractionMode,
|
|
20
24
|
isChatRequest,
|
|
21
25
|
isChatStop,
|
|
26
|
+
isConfigure,
|
|
22
27
|
isSetHostTools,
|
|
23
28
|
type PageContextSnapshot,
|
|
24
29
|
type SetHostTools,
|
|
@@ -92,6 +97,9 @@ export class ChatHandler {
|
|
|
92
97
|
// Host-tool channel (#2046): register client tools, then route the client's
|
|
93
98
|
// result/update frames back to the correlated pending call in the bridge.
|
|
94
99
|
else if (isSetHostTools(msg)) this.#handleSetHostTools(msg as unknown as SetHostTools);
|
|
100
|
+
// Provider configuration channel (#2095): swap the LLM provider/model in
|
|
101
|
+
// session memory at runtime (never persisted), then ack or nack.
|
|
102
|
+
else if (isConfigure(msg)) this.#handleConfigure(msg as unknown as Configure);
|
|
95
103
|
else if (isRpcHostToolResult(msg)) this.#hostToolBridge.handleResult(msg as unknown as HostToolResult);
|
|
96
104
|
else if (isRpcHostToolUpdate(msg)) this.#hostToolBridge.handleUpdate(msg as unknown as HostToolUpdate);
|
|
97
105
|
});
|
|
@@ -287,6 +295,70 @@ export class ChatHandler {
|
|
|
287
295
|
}
|
|
288
296
|
}
|
|
289
297
|
|
|
298
|
+
/** Configure the LLM provider at runtime from a bridge client (#2095), then ack
|
|
299
|
+
* with the selected model so the client can await it before its first prompt.
|
|
300
|
+
* SESSION/RUNTIME MEMORY ONLY — the token is never written to models.yml or the
|
|
301
|
+
* SQLite credential store. Mirrors #handleSetHostTools's try/ack-or-nack shape;
|
|
302
|
+
* never throws out of the handler (a nack keeps a waiting client from hanging).
|
|
303
|
+
*
|
|
304
|
+
* The baked F5 gateway registers its models under the "anthropic" provider
|
|
305
|
+
* (DEFAULT_MODEL_ROLE = "anthropic/claude-opus-4-8"), so that is the provider we
|
|
306
|
+
* (re)configure here. */
|
|
307
|
+
async #handleConfigure(msg: Configure): Promise<void> {
|
|
308
|
+
try {
|
|
309
|
+
const registry = this.#session.modelRegistry;
|
|
310
|
+
const [provider, defaultModelId] = DEFAULT_MODEL_ROLE.split("/");
|
|
311
|
+
|
|
312
|
+
if (msg.baseUrl) {
|
|
313
|
+
// SSRF guard: only an `https:` gateway URL may be dialed. Validate BEFORE
|
|
314
|
+
// registerProvider so a bad URL becomes a configure_error nack (never a
|
|
315
|
+
// silently-ignored frame that hangs the client). We deliberately do NOT
|
|
316
|
+
// block loopback/RFC-1918 targets: an operator-chosen INTERNAL gateway is
|
|
317
|
+
// the whole point (the F5 LiteLLM gateway, and the claude-office CORS proxy
|
|
318
|
+
// at https://127-0-0-1.local-ip.sh:8443 are legitimate targets).
|
|
319
|
+
//
|
|
320
|
+
// Accepted residual (user-requested tradeoff): the operator points xcsh at
|
|
321
|
+
// THEIR OWN gateway with THEIR OWN token over a loopback-only, TLS,
|
|
322
|
+
// Origin-checked bridge (extension-bridge `isAllowedBridgeOrigin`), https is
|
|
323
|
+
// enforced here, and the token is session-only (never persisted to disk).
|
|
324
|
+
const baseUrl = requireHttpsUrl(msg.baseUrl);
|
|
325
|
+
|
|
326
|
+
// baseUrl + apiKey, no models[] → sets the in-memory runtime API key AND
|
|
327
|
+
// overrides the existing provider models' baseUrl/headers (reusing their
|
|
328
|
+
// metadata). Nothing is persisted to disk.
|
|
329
|
+
registry.registerProvider(
|
|
330
|
+
provider,
|
|
331
|
+
{
|
|
332
|
+
baseUrl,
|
|
333
|
+
apiKey: msg.token,
|
|
334
|
+
headers: { "anthropic-beta": "context-1m-2025-08-07" },
|
|
335
|
+
},
|
|
336
|
+
"office-configure",
|
|
337
|
+
);
|
|
338
|
+
} else {
|
|
339
|
+
// Key-only: reuse the baked F5 gateway; set just the non-persistent runtime key.
|
|
340
|
+
registry.authStorage.setRuntimeApiKey(provider, msg.token);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const modelId = msg.model ?? this.#session.model?.id ?? defaultModelId;
|
|
344
|
+
const model = registry.find(provider, modelId);
|
|
345
|
+
if (!model) {
|
|
346
|
+
throw new Error(`No model ${provider}/${modelId} available`);
|
|
347
|
+
}
|
|
348
|
+
// setModel validates the API key and throws if missing → becomes configure_error.
|
|
349
|
+
await this.#session.setModel(model);
|
|
350
|
+
|
|
351
|
+
this.#server.send({ type: "configure_ack", model: model.id } satisfies ConfigureAck);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
// Nack instead of throwing (set_host_tools parity): a client awaiting
|
|
354
|
+
// configure_ack would otherwise hang on a bad frame or missing key.
|
|
355
|
+
this.#server.send({
|
|
356
|
+
type: "configure_error",
|
|
357
|
+
error: err instanceof Error ? err.message : String(err),
|
|
358
|
+
} satisfies ConfigureError);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
290
362
|
#handleChatStop(stop: { id: string }): void {
|
|
291
363
|
const chat = this.#activeChats.get(stop.id);
|
|
292
364
|
if (!chat) return;
|
|
@@ -322,6 +394,24 @@ export class ChatHandler {
|
|
|
322
394
|
}
|
|
323
395
|
}
|
|
324
396
|
|
|
397
|
+
/** SSRF guard for the `configure` frame's optional gateway `baseUrl`: the URL must
|
|
398
|
+
* parse and use `https:`. Returns the ORIGINAL string unchanged on success (no
|
|
399
|
+
* normalization — the operator's exact gateway path is preserved); throws (→ becomes
|
|
400
|
+
* a `configure_error` nack) for a malformed or non-https URL. Loopback/private hosts
|
|
401
|
+
* are intentionally NOT blocked — the target is an operator-chosen internal gateway. */
|
|
402
|
+
export function requireHttpsUrl(raw: string): string {
|
|
403
|
+
let parsed: URL;
|
|
404
|
+
try {
|
|
405
|
+
parsed = new URL(raw);
|
|
406
|
+
} catch {
|
|
407
|
+
throw new Error(`configure baseUrl is not a valid URL: ${raw}`);
|
|
408
|
+
}
|
|
409
|
+
if (parsed.protocol !== "https:") {
|
|
410
|
+
throw new Error(`configure baseUrl must use https (got "${parsed.protocol}")`);
|
|
411
|
+
}
|
|
412
|
+
return raw;
|
|
413
|
+
}
|
|
414
|
+
|
|
325
415
|
/** Best-effort classification of an upstream/provider error message into a
|
|
326
416
|
* ChatErrorReason so the panel can pick a distinct, actionable message. Returns
|
|
327
417
|
* undefined for an unclassified error (the panel then shows the raw error text).
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* Contract source of truth: capabilities.json v1.2.0.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
// Import the guards + wire types from the PURE leaf modules (not the `../host-tools`
|
|
7
|
+
// barrel), so this browser-safe contract can be consumed by a lib.dom (React)
|
|
8
|
+
// TypeScript program without pulling the RpcHostToolBridge's theme/tool-proxy graph.
|
|
9
|
+
import { isRpcHostToolResult, isRpcHostToolUpdate } from "../host-tools/guards";
|
|
10
|
+
import type {
|
|
11
|
+
RpcHostToolCallRequest,
|
|
12
|
+
RpcHostToolCancelRequest,
|
|
13
|
+
RpcHostToolDefinition,
|
|
14
|
+
RpcHostToolResult,
|
|
15
|
+
RpcHostToolUpdate,
|
|
16
|
+
} from "../host-tools/types";
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
// Page context snapshot (auto-attached by extension to every chat_request)
|
|
@@ -176,6 +178,43 @@ export interface SetHostToolsError {
|
|
|
176
178
|
error: string;
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Provider configuration channel (contract 1.9.0)
|
|
183
|
+
//
|
|
184
|
+
// Lets a bridge client (the Chrome extension, the office-xcsh add-in) configure
|
|
185
|
+
// xcsh's LLM provider at runtime — after the socket is connected — without
|
|
186
|
+
// restarting the worker and WITHOUT persisting the token to disk. xcsh stays the
|
|
187
|
+
// intelligence engine; this only swaps the provider credentials/model in session
|
|
188
|
+
// memory. Single config in-flight, so — like `set_host_tools` — there is no `id`
|
|
189
|
+
// correlation field. Mirrors the set_host_tools ack/nack shape exactly.
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/** Inbound: the client configures the LLM provider. `token` is required and
|
|
193
|
+
* non-empty. `baseUrl` (optional) is an Anthropic-compatible gateway base; when
|
|
194
|
+
* omitted, the baked F5 gateway is reused and only the runtime API key is set.
|
|
195
|
+
* `model` (optional) selects the model id; when omitted, the session default is
|
|
196
|
+
* kept. The token lives in session/runtime memory only — never written to disk. */
|
|
197
|
+
export interface Configure {
|
|
198
|
+
type: "configure";
|
|
199
|
+
baseUrl?: string;
|
|
200
|
+
token: string;
|
|
201
|
+
model?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Outbound: acks a `configure` with the model id actually selected, so the
|
|
205
|
+
* client can await configuration before its first prompt. */
|
|
206
|
+
export interface ConfigureAck {
|
|
207
|
+
type: "configure_ack";
|
|
208
|
+
model: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Outbound: nacks a `configure` that failed (bad frame, unknown model, missing
|
|
212
|
+
* API key). Emitted instead of the ack so a client awaiting it never hangs. */
|
|
213
|
+
export interface ConfigureError {
|
|
214
|
+
type: "configure_error";
|
|
215
|
+
error: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
179
218
|
// ---------------------------------------------------------------------------
|
|
180
219
|
// Validators
|
|
181
220
|
// ---------------------------------------------------------------------------
|
|
@@ -202,6 +241,18 @@ export function isSetHostTools(msg: Record<string, unknown>): boolean {
|
|
|
202
241
|
return msg.type === "set_host_tools" && Array.isArray(msg.tools);
|
|
203
242
|
}
|
|
204
243
|
|
|
244
|
+
/** True for a well-formed `configure` frame: a non-empty string `token` is required;
|
|
245
|
+
* `baseUrl`/`model`, when present, must be strings. */
|
|
246
|
+
export function isConfigure(msg: Record<string, unknown>): boolean {
|
|
247
|
+
return (
|
|
248
|
+
msg.type === "configure" &&
|
|
249
|
+
typeof msg.token === "string" &&
|
|
250
|
+
msg.token.length > 0 &&
|
|
251
|
+
(msg.baseUrl === undefined || typeof msg.baseUrl === "string") &&
|
|
252
|
+
(msg.model === undefined || typeof msg.model === "string")
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
205
256
|
/** Delegates to the neutral guard, which requires `result.content` to be an array. */
|
|
206
257
|
export function isHostToolResult(msg: Record<string, unknown>): boolean {
|
|
207
258
|
return isRpcHostToolResult(msg);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-neutral host-tool frame guards.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a leaf module — depending ONLY on the wire types (`./types`) and
|
|
5
|
+
* pi-agent-core's `AgentToolResult` *type* — so browser-safe consumers (e.g.
|
|
6
|
+
* `../browser/chat-protocol`) can import the guards WITHOUT loading the
|
|
7
|
+
* `RpcHostToolBridge` in `./host-tools`, which pulls the theme + tool-proxy
|
|
8
|
+
* runtime graph. That graph is fine for the agent runtime but drags node-coupled
|
|
9
|
+
* modules into a browser (lib.dom) TypeScript program.
|
|
10
|
+
*/
|
|
11
|
+
import type { AgentToolResult } from "@f5-sales-demo/pi-agent-core/types";
|
|
12
|
+
import type { RpcHostToolResult, RpcHostToolUpdate } from "./types";
|
|
13
|
+
|
|
14
|
+
function isAgentToolResult(value: unknown): value is AgentToolResult<unknown> {
|
|
15
|
+
if (!value || typeof value !== "object") return false;
|
|
16
|
+
const content = (value as { content?: unknown }).content;
|
|
17
|
+
return Array.isArray(content);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isRpcHostToolResult(value: unknown): value is RpcHostToolResult {
|
|
21
|
+
if (!value || typeof value !== "object") return false;
|
|
22
|
+
const frame = value as { type?: unknown; id?: unknown; result?: unknown };
|
|
23
|
+
return frame.type === "host_tool_result" && typeof frame.id === "string" && isAgentToolResult(frame.result);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isRpcHostToolUpdate(value: unknown): value is RpcHostToolUpdate {
|
|
27
|
+
if (!value || typeof value !== "object") return false;
|
|
28
|
+
const frame = value as { type?: unknown; id?: unknown; partialResult?: unknown };
|
|
29
|
+
return frame.type === "host_tool_update" && typeof frame.id === "string" && isAgentToolResult(frame.partialResult);
|
|
30
|
+
}
|
|
@@ -19,24 +19,6 @@ type PendingHostToolCall = {
|
|
|
19
19
|
onUpdate?: AgentToolUpdateCallback<unknown>;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
function isAgentToolResult(value: unknown): value is AgentToolResult<unknown> {
|
|
23
|
-
if (!value || typeof value !== "object") return false;
|
|
24
|
-
const content = (value as { content?: unknown }).content;
|
|
25
|
-
return Array.isArray(content);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isRpcHostToolResult(value: unknown): value is RpcHostToolResult {
|
|
29
|
-
if (!value || typeof value !== "object") return false;
|
|
30
|
-
const frame = value as { type?: unknown; id?: unknown; result?: unknown };
|
|
31
|
-
return frame.type === "host_tool_result" && typeof frame.id === "string" && isAgentToolResult(frame.result);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function isRpcHostToolUpdate(value: unknown): value is RpcHostToolUpdate {
|
|
35
|
-
if (!value || typeof value !== "object") return false;
|
|
36
|
-
const frame = value as { type?: unknown; id?: unknown; partialResult?: unknown };
|
|
37
|
-
return frame.type === "host_tool_update" && typeof frame.id === "string" && isAgentToolResult(frame.partialResult);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
22
|
/**
|
|
41
23
|
* Validate + normalize incoming host-tool definitions (trim names/labels/descriptions,
|
|
42
24
|
* enforce non-empty name/description and a JSON-Schema `parameters` object). Shared by
|
package/src/host-tools/index.ts
CHANGED
|
@@ -7,12 +7,8 @@
|
|
|
7
7
|
* transport specifics — a driver supplies only an `output: (frame) => void`
|
|
8
8
|
* sink — so every transport reuses them verbatim.
|
|
9
9
|
*/
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
isRpcHostToolUpdate,
|
|
13
|
-
normalizeHostToolDefinitions,
|
|
14
|
-
RpcHostToolBridge,
|
|
15
|
-
} from "./host-tools";
|
|
10
|
+
export { isRpcHostToolResult, isRpcHostToolUpdate } from "./guards";
|
|
11
|
+
export { normalizeHostToolDefinitions, RpcHostToolBridge } from "./host-tools";
|
|
16
12
|
export type {
|
|
17
13
|
RpcHostToolCallRequest,
|
|
18
14
|
RpcHostToolCancelRequest,
|
package/src/host-tools/types.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* back. They are shared verbatim across transports (stdio RPC and the WS chat
|
|
7
7
|
* bridge), so they live outside of any single transport driver.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
// Narrow subpath (not the barrel) so browser-safe consumers of these wire types
|
|
10
|
+
// don't transitively pull pi-agent-core's runtime graph (agent → pi-utils), which
|
|
11
|
+
// is not lib.dom-safe. AgentToolResult is a pure type.
|
|
12
|
+
import type { AgentToolResult } from "@f5-sales-demo/pi-agent-core/types";
|
|
10
13
|
|
|
11
14
|
export interface RpcHostToolDefinition {
|
|
12
15
|
name: string;
|
|
@@ -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.65.0",
|
|
21
|
+
"commit": "342e21815d71783c1c2b1ffe233bc0078fbcc44a",
|
|
22
|
+
"shortCommit": "342e218",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.65.0",
|
|
25
|
+
"commitDate": "2026-07-20T18:44:32Z",
|
|
26
|
+
"buildDate": "2026-07-20T19:07:31.313Z",
|
|
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/342e21815d71783c1c2b1ffe233bc0078fbcc44a",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.65.0"
|
|
33
33
|
};
|