@f5-sales-demo/xcsh 21.0.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 +8 -8
- package/src/browser/capabilities.generated.ts +21 -1
- package/src/browser/chat-handler.ts +13 -9
- package/src/browser/extension-contract.ts +62 -0
- package/src/cli/args.ts +98 -9
- package/src/cli/flag-spec.ts +11 -4
- package/src/cli/plugin-cli.ts +11 -0
- package/src/cli/sandbox-check.ts +35 -6
- package/src/commands/launch.ts +1 -3
- package/src/config/model-registry.ts +2 -0
- package/src/discovery/helpers.ts +18 -4
- package/src/exec/bash-executor.ts +1 -0
- package/src/internal-urls/build-info-runtime.ts +1 -1
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/console-resolve.ts +6 -5
- package/src/internal-urls/extension-tools.generated.ts +4 -0
- package/src/internal-urls/xcsh-protocol.ts +6 -5
- package/src/main.ts +84 -42
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/model-selector.ts +5 -1
- package/src/prompts/internal-urls/containment.md +5 -3
- package/src/prompts/tools/bash.md +3 -2
- package/src/sandbox/command-operands.ts +82 -5
- package/src/sandbox/containment.ts +32 -13
- package/src/sandbox/enforce.ts +12 -5
- package/src/sandbox/session-fence.ts +1 -0
- package/src/sdk.ts +30 -32
- package/src/session/agent-session.ts +35 -13
- package/src/system-prompt.ts +24 -20
- package/src/thinking.ts +5 -2
- package/src/tools/bash.ts +26 -19
- package/src/tools/catalog-workflow-runner.ts +6 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "21.
|
|
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.
|
|
65
|
-
"@f5-sales-demo/pi-ai": "21.
|
|
66
|
-
"@f5-sales-demo/pi-natives": "21.
|
|
67
|
-
"@f5-sales-demo/pi-resource-management": "21.
|
|
68
|
-
"@f5-sales-demo/pi-tui": "21.
|
|
69
|
-
"@f5-sales-demo/pi-utils": "21.
|
|
70
|
-
"@f5-sales-demo/xcsh-stats": "21.
|
|
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:
|
|
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(
|
|
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
|
+
}
|
package/src/cli/args.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
|
|
18
18
|
export type Mode = "text" | "json" | "rpc" | "acp";
|
|
19
19
|
|
|
20
|
+
export type ExtensionFlagRegistry = ReadonlyMap<string, { type: "boolean" | "string" }>;
|
|
21
|
+
|
|
20
22
|
export interface Args {
|
|
21
23
|
cwd?: string;
|
|
22
24
|
allowHome?: boolean;
|
|
@@ -224,7 +226,97 @@ function isValueToken(token: string | undefined): token is string {
|
|
|
224
226
|
return token !== undefined && !token.startsWith("-") && !token.startsWith("@");
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
export
|
|
229
|
+
export interface LaunchBootstrapArgs {
|
|
230
|
+
allowHome?: boolean;
|
|
231
|
+
cwd?: string;
|
|
232
|
+
noSandbox?: boolean;
|
|
233
|
+
allowPath: string[];
|
|
234
|
+
noMemories?: boolean;
|
|
235
|
+
hooks: string[];
|
|
236
|
+
extensions: string[];
|
|
237
|
+
noExtensions?: boolean;
|
|
238
|
+
pluginDirs: string[];
|
|
239
|
+
preExtensionExit: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const BOOTSTRAP_VALUE_FLAGS = new Set(["allow-path", "hook", "extension", "plugin-dir"]);
|
|
243
|
+
const PRE_EXTENSION_EXITS = new Set(["version", "list-models", "export"]);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Read only the built-in controls needed to load extensions.
|
|
247
|
+
*
|
|
248
|
+
* This is deliberately not an argument parse: it never classifies positional input, files, or
|
|
249
|
+
* extension flags. It only follows the known built-in grammar far enough to avoid mistaking a
|
|
250
|
+
* built-in value for a discovery control. The authoritative parse happens after this scan.
|
|
251
|
+
*/
|
|
252
|
+
export function scanLaunchBootstrapArgs(args: readonly string[]): LaunchBootstrapArgs {
|
|
253
|
+
const result: LaunchBootstrapArgs = {
|
|
254
|
+
allowPath: [],
|
|
255
|
+
hooks: [],
|
|
256
|
+
extensions: [],
|
|
257
|
+
pluginDirs: [],
|
|
258
|
+
preExtensionExit: false,
|
|
259
|
+
};
|
|
260
|
+
const tokens = normalizeFlagTokens(args);
|
|
261
|
+
|
|
262
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
263
|
+
const token = tokens[i];
|
|
264
|
+
if (token === "--") break;
|
|
265
|
+
if (!token.startsWith("-") || token === "-") continue;
|
|
266
|
+
|
|
267
|
+
const name = token.startsWith("--") ? token.slice(2) : flagNameForChar(token.slice(1));
|
|
268
|
+
if (name === undefined) continue;
|
|
269
|
+
const spec = flagSpec(name);
|
|
270
|
+
if (!spec) continue;
|
|
271
|
+
|
|
272
|
+
let value: string | true = true;
|
|
273
|
+
if (spec.arity === "optional-value") {
|
|
274
|
+
if (isValueToken(tokens[i + 1])) value = tokens[++i];
|
|
275
|
+
} else if (takesValue(spec)) {
|
|
276
|
+
if (tokens[i + 1] === undefined) continue;
|
|
277
|
+
value = tokens[++i];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (PRE_EXTENSION_EXITS.has(name)) result.preExtensionExit = true;
|
|
281
|
+
if (name === "allow-home") result.allowHome = true;
|
|
282
|
+
if (name === "no-sandbox") result.noSandbox = true;
|
|
283
|
+
if (name === "no-memories") result.noMemories = true;
|
|
284
|
+
if (name === "no-extensions") result.noExtensions = true;
|
|
285
|
+
if (BOOTSTRAP_VALUE_FLAGS.has(name) && value !== true) {
|
|
286
|
+
if (name === "allow-path") result.allowPath.push(value);
|
|
287
|
+
if (name === "hook") result.hooks.push(value);
|
|
288
|
+
if (name === "extension") result.extensions.push(value);
|
|
289
|
+
if (name === "plugin-dir") result.pluginDirs.push(value);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface ResolvedLaunchArgs {
|
|
297
|
+
bootstrap: LaunchBootstrapArgs;
|
|
298
|
+
parsed: Args;
|
|
299
|
+
extensionFlags?: ExtensionFlagRegistry;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resolve extension registrations before the one parse whose result drives launch behavior.
|
|
304
|
+
* Version/model-list/export retain their pre-extension fast path.
|
|
305
|
+
*/
|
|
306
|
+
export async function resolveLaunchArgs(
|
|
307
|
+
args: readonly string[],
|
|
308
|
+
loadExtensionFlags: (bootstrap: LaunchBootstrapArgs) => Promise<ExtensionFlagRegistry>,
|
|
309
|
+
): Promise<ResolvedLaunchArgs> {
|
|
310
|
+
const bootstrap = scanLaunchBootstrapArgs(args);
|
|
311
|
+
const extensionFlags = bootstrap.preExtensionExit ? undefined : await loadExtensionFlags(bootstrap);
|
|
312
|
+
return {
|
|
313
|
+
bootstrap,
|
|
314
|
+
parsed: parseArgs([...args], extensionFlags),
|
|
315
|
+
...(extensionFlags ? { extensionFlags } : {}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function parseArgs(args: string[], extensionFlags?: ExtensionFlagRegistry): Args {
|
|
228
320
|
const result: Args = {
|
|
229
321
|
messages: [],
|
|
230
322
|
fileArgs: [],
|
|
@@ -282,23 +374,20 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
|
|
282
374
|
continue;
|
|
283
375
|
}
|
|
284
376
|
|
|
285
|
-
// Extension
|
|
377
|
+
// Extension registrations are loaded before this authoritative parse.
|
|
286
378
|
const extFlag = name === undefined ? undefined : extensionFlags?.get(name);
|
|
287
379
|
if (extFlag && name !== undefined) {
|
|
288
380
|
if (extFlag.type === "boolean") {
|
|
289
|
-
|
|
381
|
+
const inlineValue = token.startsWith("--") ? token.split("=", 2)[1] : undefined;
|
|
382
|
+
result.unknownFlags.set(name, inlineValue === undefined ? true : inlineValue === "true");
|
|
290
383
|
} else if (i + 1 < tokens.length) {
|
|
291
384
|
result.unknownFlags.set(name, tokens[++i]);
|
|
292
385
|
}
|
|
293
386
|
continue;
|
|
294
387
|
}
|
|
295
388
|
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
// the next token silently discards the user's prompt whenever the flag turns out to be
|
|
299
|
-
// boolean (`xcsh -p --verbose "do work"`). Leaving it means a string extension flag's value
|
|
300
|
-
// still reaches `messages`, which is the pre-existing behaviour and the lesser harm — the
|
|
301
|
-
// real fix is to load extensions before the first parse, which is out of scope here.
|
|
389
|
+
// Do not consume a following token for a genuine unknown flag: it may be prompt content.
|
|
390
|
+
// Registered extension flags never reach this path because launch discovers them first.
|
|
302
391
|
result.unrecognizedFlags.push({ token, name: name ?? token.replace(/^-+/, "") });
|
|
303
392
|
}
|
|
304
393
|
|
package/src/cli/flag-spec.ts
CHANGED
|
@@ -175,20 +175,23 @@ export function validateInlineFlagSyntax(args: readonly string[], extensionFlags
|
|
|
175
175
|
if (!inline) continue;
|
|
176
176
|
|
|
177
177
|
const spec = flagSpec(inline.name);
|
|
178
|
-
|
|
179
|
-
if (isBoolean) {
|
|
178
|
+
if (spec?.arity === "boolean") {
|
|
180
179
|
throw new CliUsageError(`--${inline.name} is a boolean flag and does not take a value`);
|
|
181
180
|
}
|
|
181
|
+
if (extensionFlags?.get(inline.name)?.type === "boolean" && inline.value !== "true" && inline.value !== "false") {
|
|
182
|
+
throw new CliUsageError(`--${inline.name} is a boolean flag and expects true or false`);
|
|
183
|
+
}
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
/**
|
|
186
188
|
* Rewrite `--name=value` into `["--name", "value"]` for every flag that takes a value.
|
|
187
189
|
*
|
|
188
|
-
* A boolean flag with `=` is an error rather than a guess: accepting `--no-sandbox=true` invites
|
|
190
|
+
* A built-in boolean flag with `=` is an error rather than a guess: accepting `--no-sandbox=true` invites
|
|
189
191
|
* `--no-sandbox=false`, which the parser has no way to express, and quietly reading it as "on" would
|
|
190
192
|
* be exactly the class of bug #2469 reports. Unknown names are left intact so the unknown-flag path
|
|
191
193
|
* can report the token as the user wrote it.
|
|
194
|
+
* Extension boolean flags accept explicit true or false values through their runtime map.
|
|
192
195
|
*
|
|
193
196
|
* Short forms are untouched: no shell convention makes `-p=x` mean `-p x`.
|
|
194
197
|
*/
|
|
@@ -223,7 +226,11 @@ export function normalizeFlagTokens(args: readonly string[], extensionFlags?: Ex
|
|
|
223
226
|
|
|
224
227
|
const extension = extensionFlags?.get(name);
|
|
225
228
|
if (extension) {
|
|
226
|
-
|
|
229
|
+
if (extension.type === "boolean") {
|
|
230
|
+
normalized.push(arg);
|
|
231
|
+
} else {
|
|
232
|
+
normalized.push(`--${name}`, value);
|
|
233
|
+
}
|
|
227
234
|
continue;
|
|
228
235
|
}
|
|
229
236
|
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -425,6 +425,17 @@ async function handleInstall(
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
if (target.type === "marketplace") {
|
|
428
|
+
if (flags.dryRun) {
|
|
429
|
+
const preview = {
|
|
430
|
+
action: "install",
|
|
431
|
+
target: `${target.name}@${target.marketplace}`,
|
|
432
|
+
scope: flags.scope ?? "user",
|
|
433
|
+
dryRun: true,
|
|
434
|
+
};
|
|
435
|
+
if (flags.json) console.log(JSON.stringify(preview, null, 2));
|
|
436
|
+
else console.log(chalk.dim(`[dry-run] Would install ${preview.target} (${preview.scope})`));
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
428
439
|
try {
|
|
429
440
|
const entry = await mktMgr.installPlugin(target.name, target.marketplace, {
|
|
430
441
|
force: flags.force,
|
package/src/cli/sandbox-check.ts
CHANGED
|
@@ -6,10 +6,17 @@ import { executeShell, fencePermits } from "@f5-sales-demo/pi-natives";
|
|
|
6
6
|
import { isEnoent } from "@f5-sales-demo/pi-utils";
|
|
7
7
|
import { Settings } from "../config/settings";
|
|
8
8
|
import { fenceForNative } from "../exec/bash-executor";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
buildContainmentFence,
|
|
11
|
+
type ContainmentFence,
|
|
12
|
+
containmentStatus,
|
|
13
|
+
fenceVerdict,
|
|
14
|
+
seatbeltFenceVerdict,
|
|
15
|
+
} from "../sandbox/containment";
|
|
10
16
|
import { evaluateToolCall } from "../sandbox/enforce";
|
|
11
17
|
import {
|
|
12
18
|
SANDBOX_CHECK_NAMED_SIBLING_ENV,
|
|
19
|
+
SANDBOX_CHECK_NAMED_SIBLING_EXPECTATION_ENV,
|
|
13
20
|
SANDBOX_OPERATOR_HOME_ENV,
|
|
14
21
|
SANDBOX_SESSION_ROOT_ENV,
|
|
15
22
|
sandboxCheckSiblingRoot,
|
|
@@ -74,6 +81,13 @@ function sanitizeDetail(value: string, redactions: readonly Redaction[]): string
|
|
|
74
81
|
return sanitized.length > 500 ? `${sanitized.slice(0, 497)}...` : sanitized;
|
|
75
82
|
}
|
|
76
83
|
|
|
84
|
+
function inheritedNamedSiblingDenied(value: string | undefined): boolean | undefined {
|
|
85
|
+
if (value === undefined) return undefined;
|
|
86
|
+
if (value === "denied") return true;
|
|
87
|
+
if (value === "allowed") return false;
|
|
88
|
+
throw new Error(`invalid ${SANDBOX_CHECK_NAMED_SIBLING_EXPECTATION_ENV}: expected "allowed" or "denied"`);
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
function errnoFromOutput(output: string): string {
|
|
78
92
|
if (/operation not permitted/iu.test(output)) return "EPERM";
|
|
79
93
|
if (/permission denied/iu.test(output)) return "EACCES";
|
|
@@ -210,6 +224,10 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
210
224
|
const inheritedHome = process.env[SANDBOX_OPERATOR_HOME_ENV];
|
|
211
225
|
const inheritedSibling = process.env[SANDBOX_CHECK_NAMED_SIBLING_ENV];
|
|
212
226
|
const inheritedProfile = inheritedWorkspace !== undefined;
|
|
227
|
+
const inheritedSiblingDenied =
|
|
228
|
+
inheritedProfile && inheritedSibling !== undefined
|
|
229
|
+
? inheritedNamedSiblingDenied(process.env[SANDBOX_CHECK_NAMED_SIBLING_EXPECTATION_ENV])
|
|
230
|
+
: undefined;
|
|
213
231
|
const workspaceInput = inheritedWorkspace ?? process.cwd();
|
|
214
232
|
const homeInput = inheritedHome ?? os.homedir();
|
|
215
233
|
redactions.push([workspaceInput, "<workspace>"], [homeInput, "<operator-home>"]);
|
|
@@ -462,7 +480,7 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
462
480
|
);
|
|
463
481
|
});
|
|
464
482
|
|
|
465
|
-
await check("named sibling
|
|
483
|
+
await check("named sibling follows the active boundary", async () => {
|
|
466
484
|
const displayPath = "<session-parent>/<synthetic-sibling>";
|
|
467
485
|
let liveSibling = inheritedSibling;
|
|
468
486
|
if (liveSibling === undefined) {
|
|
@@ -480,13 +498,24 @@ export async function runSandboxCheck(options: SandboxCheckOptions = {}): Promis
|
|
|
480
498
|
return exceptionOutcome("create named sibling fixture", displayPath, error, redactions);
|
|
481
499
|
}
|
|
482
500
|
}
|
|
501
|
+
const seatbeltDeniesSibling =
|
|
502
|
+
inheritedSiblingDenied ??
|
|
503
|
+
(backend.backend === "seatbelt" && seatbeltFenceVerdict(liveFence, liveSibling, "read") === "deny");
|
|
483
504
|
const result = await shellProbe(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
undefined,
|
|
505
|
+
`cd ${quote(liveSibling)} && test "$(cat named.txt)" = sibling`,
|
|
506
|
+
liveWorkspace,
|
|
507
|
+
inheritedProfile ? undefined : liveFence,
|
|
487
508
|
abortController.signal,
|
|
488
509
|
);
|
|
489
|
-
return shellOutcome(
|
|
510
|
+
return shellOutcome(
|
|
511
|
+
result,
|
|
512
|
+
!seatbeltDeniesSibling,
|
|
513
|
+
seatbeltDeniesSibling
|
|
514
|
+
? "Seatbelt must deny a named sibling outside the workspace"
|
|
515
|
+
: "live profile must allow a named sibling read",
|
|
516
|
+
displayPath,
|
|
517
|
+
redactions,
|
|
518
|
+
);
|
|
490
519
|
});
|
|
491
520
|
|
|
492
521
|
if (backend.osEnforced) {
|
package/src/commands/launch.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { APP_NAME } from "@f5-sales-demo/pi-utils";
|
|
6
6
|
import { Args, Command } from "@f5-sales-demo/pi-utils/cli";
|
|
7
|
-
import { parseArgs } from "../cli/args";
|
|
8
7
|
import { buildCliFlags } from "../cli/flag-spec";
|
|
9
8
|
import { runRootCommand } from "../main";
|
|
10
9
|
|
|
@@ -36,7 +35,6 @@ export default class Index extends Command {
|
|
|
36
35
|
static strict = false;
|
|
37
36
|
|
|
38
37
|
async run(): Promise<void> {
|
|
39
|
-
|
|
40
|
-
await runRootCommand(parsed, this.argv);
|
|
38
|
+
await runRootCommand(this.argv);
|
|
41
39
|
}
|
|
42
40
|
}
|
|
@@ -187,6 +187,8 @@ const ThinkingControlModeSchema = Type.Union([
|
|
|
187
187
|
const ModelThinkingSchema = Type.Object({
|
|
188
188
|
minLevel: EffortSchema,
|
|
189
189
|
maxLevel: EffortSchema,
|
|
190
|
+
defaultLevel: Type.Optional(EffortSchema),
|
|
191
|
+
canDisable: Type.Optional(Type.Boolean()),
|
|
190
192
|
mode: ThinkingControlModeSchema,
|
|
191
193
|
});
|
|
192
194
|
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -843,12 +843,13 @@ export async function listXcshPluginRoots(
|
|
|
843
843
|
roots.push(...projectRoots, ...deduped);
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
-
// Merge --plugin-dir roots (highest precedence) on every fresh load
|
|
846
|
+
// Merge --plugin-dir roots (highest precedence) on every fresh load. Local roots use
|
|
847
|
+
// an artificial marketplace ID, so identity must be matched by manifest plugin name;
|
|
848
|
+
// comparing full IDs would leave the installed copy active beside the candidate.
|
|
847
849
|
if (injectedPluginDirRoots.length > 0) {
|
|
848
|
-
const
|
|
849
|
-
const filtered = roots.filter(r => !injectedIds.has(r.id));
|
|
850
|
+
const merged = prioritizeInjectedPluginRoots(roots, injectedPluginDirRoots);
|
|
850
851
|
roots.length = 0;
|
|
851
|
-
roots.push(...
|
|
852
|
+
roots.push(...merged);
|
|
852
853
|
}
|
|
853
854
|
|
|
854
855
|
const result = { roots, warnings };
|
|
@@ -856,6 +857,19 @@ export async function listXcshPluginRoots(
|
|
|
856
857
|
return result;
|
|
857
858
|
}
|
|
858
859
|
|
|
860
|
+
export function prioritizeInjectedPluginRoots(
|
|
861
|
+
installed: XcshPluginRoot[],
|
|
862
|
+
injected: XcshPluginRoot[],
|
|
863
|
+
): XcshPluginRoot[] {
|
|
864
|
+
const seen = new Set<string>();
|
|
865
|
+
const winners = injected.filter(root => {
|
|
866
|
+
if (seen.has(root.plugin)) return false;
|
|
867
|
+
seen.add(root.plugin);
|
|
868
|
+
return true;
|
|
869
|
+
});
|
|
870
|
+
return [...winners, ...installed.filter(root => !seen.has(root.plugin))];
|
|
871
|
+
}
|
|
872
|
+
|
|
859
873
|
export interface XcshPluginSummary {
|
|
860
874
|
/** Registry id (root.plugin) — the key the `xcsh://plugin/<id>` resolver matches on. */
|
|
861
875
|
id: string;
|
|
@@ -24,6 +24,7 @@ export function fenceForNative(fence: ContainmentFence | undefined) {
|
|
|
24
24
|
allowReadOnly: [...fence.allowReadOnly],
|
|
25
25
|
allowWriteOnly: [...fence.allowWriteOnly],
|
|
26
26
|
deny: [...fence.deny],
|
|
27
|
+
denyOnSeatbelt: [...fence.denyOnSeatbelt],
|
|
27
28
|
denyEnumerate: [...fence.denyEnumerate],
|
|
28
29
|
};
|
|
29
30
|
}
|
|
@@ -171,7 +171,7 @@ function renderContainment(containment: ContainmentStatus | null): string {
|
|
|
171
171
|
if (!containment) return "";
|
|
172
172
|
// `landlock` is derived rather than another field on the status, because the template needs a
|
|
173
173
|
// boolean and Handlebars cannot compare strings. It gates the Linux-only costs — unlistable split
|
|
174
|
-
// directories
|
|
174
|
+
// directories and no setuid — which are true of that backend and no other.
|
|
175
175
|
return prompt.render(containmentTemplate, {
|
|
176
176
|
containment: { ...containment, landlock: containment.backend === "landlock" },
|
|
177
177
|
});
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "21.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "21.2.0",
|
|
21
|
+
"commit": "d217f62f68fae54fe18d88c7d90a8f7dc4703c0d",
|
|
22
|
+
"shortCommit": "d217f62",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v21.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.
|
|
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
|
};
|
|
@@ -121,10 +121,10 @@ function renderResource(resource: string, catalog: ConsoleCatalogData, fieldMeta
|
|
|
121
121
|
const doc = (parseYaml(raw) ?? {}) as Record<string, unknown>;
|
|
122
122
|
const console_ = (doc.console ?? {}) as Record<string, unknown>;
|
|
123
123
|
const lines = [`# ${(doc.label as string | undefined) ?? key}`, ""];
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
const routePattern = typeof console_.route_pattern === "string" ? console_.route_pattern : undefined;
|
|
125
|
+
if (routePattern) {
|
|
126
|
+
const routePrefix = typeof console_.route_prefix === "string" ? console_.route_prefix : undefined;
|
|
127
|
+
const fullRoute = routePrefix ? `${routePrefix}${routePattern}` : routePattern;
|
|
128
128
|
lines.push(`**Route:** \`${fullRoute}\``, "");
|
|
129
129
|
}
|
|
130
130
|
if (Array.isArray(console_.menu_path)) lines.push(`**Menu:** ${(console_.menu_path as string[]).join(" › ")}`, "");
|
|
@@ -155,8 +155,9 @@ function renderWorkflow(resource: string, operation: string, catalog: ConsoleCat
|
|
|
155
155
|
for (const s of steps) {
|
|
156
156
|
const sel = s.selector ? ` \`${s.selector}\`` : "";
|
|
157
157
|
const val = s.value != null ? ` = ${JSON.stringify(s.value)}` : "";
|
|
158
|
+
const action = typeof s.action === "string" ? s.action : "unknown";
|
|
158
159
|
lines.push(
|
|
159
|
-
`1. **${
|
|
160
|
+
`1. **${action}**${sel}${val} — ${(s.description as string | undefined) ?? (s.id as string | undefined) ?? ""}`,
|
|
160
161
|
);
|
|
161
162
|
}
|
|
162
163
|
return `${lines.join("\n")}\n`;
|
|
@@ -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";
|