@opengeni/runtime 3.2.2 → 3.3.1-canary.38922642121001
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/dist/agent-LJT5NPWT.js +9 -0
- package/dist/assets/codemode-client.json +1 -1
- package/dist/{chunk-HJSIPTFA.js → chunk-4XDJ4TJA.js} +2 -2
- package/dist/{chunk-XQ23VCZ4.js → chunk-6FQ6KEII.js} +3 -3
- package/dist/{chunk-7LJ7KDPI.js → chunk-ERD4ICVB.js} +4 -3
- package/dist/{chunk-YG26XE7B.js → chunk-JYDG2A7H.js} +2 -2
- package/dist/{chunk-RSHQSLEJ.js → chunk-KIL4FGX5.js} +3 -3
- package/dist/{chunk-NWS4HPB7.js → chunk-MIAL5I4O.js} +2 -2
- package/dist/{chunk-7OUG7WBV.js → chunk-N5EB52EH.js} +3 -3
- package/dist/{chunk-Z5U6WE22.js → chunk-PRZPGRIK.js} +2 -2
- package/dist/{chunk-KCPW4K7L.js → chunk-PZQUUBEB.js} +2 -2
- package/dist/{chunk-2AUDDDO2.js → chunk-XJARX6VV.js} +194 -23
- package/dist/chunk-XJARX6VV.js.map +1 -0
- package/dist/{chunk-5WJOPANS.js → chunk-Y5THZH5N.js} +7 -7
- package/dist/{chunk-462CH77G.js → chunk-ZUHGY36R.js} +181 -8
- package/dist/chunk-ZUHGY36R.js.map +1 -0
- package/dist/gmail-rest-mcp.js +8 -8
- package/dist/index.d.ts +3 -0
- package/dist/index.js +14 -12
- package/dist/interaction-tool-json-schema.d.ts +20 -0
- package/dist/{lazy-manifest-AJ6XPJ4F.js → lazy-manifest-BLZ3UZLX.js} +4 -4
- package/dist/{localSources-SSIEH5F5.js → localSources-I6SIQ6CX.js} +6 -6
- package/dist/model-call.js +3 -3
- package/dist/sandbox/browser-control-client.d.ts +10 -0
- package/dist/sandbox/browser-control-http.d.ts +33 -0
- package/dist/sandbox/index.js +7 -7
- package/dist/sandbox/routing/backend-resolver.d.ts +1 -0
- package/dist/sandbox/selfhosted/session.d.ts +19 -0
- package/dist/slack-rest-mcp.js +8 -8
- package/dist/workspace-tool-gateway.js +12 -12
- package/package.json +14 -14
- package/src/bundled_site_skills/opengeni-sites/SKILL.md +46 -12
- package/src/index.ts +39 -1
- package/src/interaction-tool-json-schema.ts +214 -0
- package/src/interaction-tools.ts +6 -6
- package/src/sandbox/browser-control-client.ts +89 -0
- package/src/sandbox/browser-control-http.ts +32 -0
- package/src/sandbox/routing/backend-resolver.ts +2 -0
- package/src/sandbox/selfhosted/session.ts +116 -0
- package/dist/agent-CTPLA3HP.js +0 -9
- package/dist/browser-action-json-schema.d.ts +0 -4
- package/dist/chunk-2AUDDDO2.js.map +0 -1
- package/dist/chunk-462CH77G.js.map +0 -1
- package/src/browser-action-json-schema.ts +0 -10
- /package/dist/{agent-CTPLA3HP.js.map → agent-LJT5NPWT.js.map} +0 -0
- /package/dist/{chunk-HJSIPTFA.js.map → chunk-4XDJ4TJA.js.map} +0 -0
- /package/dist/{chunk-XQ23VCZ4.js.map → chunk-6FQ6KEII.js.map} +0 -0
- /package/dist/{chunk-7LJ7KDPI.js.map → chunk-ERD4ICVB.js.map} +0 -0
- /package/dist/{chunk-YG26XE7B.js.map → chunk-JYDG2A7H.js.map} +0 -0
- /package/dist/{chunk-RSHQSLEJ.js.map → chunk-KIL4FGX5.js.map} +0 -0
- /package/dist/{chunk-NWS4HPB7.js.map → chunk-MIAL5I4O.js.map} +0 -0
- /package/dist/{chunk-7OUG7WBV.js.map → chunk-N5EB52EH.js.map} +0 -0
- /package/dist/{chunk-Z5U6WE22.js.map → chunk-PRZPGRIK.js.map} +0 -0
- /package/dist/{chunk-KCPW4K7L.js.map → chunk-PZQUUBEB.js.map} +0 -0
- /package/dist/{chunk-5WJOPANS.js.map → chunk-Y5THZH5N.js.map} +0 -0
- /package/dist/{lazy-manifest-AJ6XPJ4F.js.map → lazy-manifest-BLZ3UZLX.js.map} +0 -0
- /package/dist/{localSources-SSIEH5F5.js.map → localSources-I6SIQ6CX.js.map} +0 -0
package/src/index.ts
CHANGED
|
@@ -117,6 +117,7 @@ import { createSecretArgumentResolver } from "./secret-argument-resolution";
|
|
|
117
117
|
import {
|
|
118
118
|
approvalIdentifier,
|
|
119
119
|
INTERACTION_REQUEST_HUMAN_MODEL_TOOL_NAME,
|
|
120
|
+
RequestHumanInteractionToolInput,
|
|
120
121
|
CAPABILITY_DESCRIPTORS,
|
|
121
122
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
122
123
|
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
@@ -303,6 +304,7 @@ import {
|
|
|
303
304
|
applyDiff,
|
|
304
305
|
RunContext,
|
|
305
306
|
ToolGuardrailFunctionOutputFactory,
|
|
307
|
+
defineToolInputGuardrail,
|
|
306
308
|
type AgentInputItem,
|
|
307
309
|
type CallModelInputFilter,
|
|
308
310
|
type MCPServer,
|
|
@@ -3881,6 +3883,23 @@ function activeModelToolInvocation(modelName: string): ModelToolInvocation | nul
|
|
|
3881
3883
|
return invocation?.modelName === modelName ? invocation : null;
|
|
3882
3884
|
}
|
|
3883
3885
|
|
|
3886
|
+
/** Why interaction_request_human arguments cannot be recorded as an
|
|
3887
|
+
* intervention, in words the model can act on, or null when they can. */
|
|
3888
|
+
export function interactionRequestArgumentsProblem(serialized: string): string | null {
|
|
3889
|
+
let value: unknown;
|
|
3890
|
+
try {
|
|
3891
|
+
value = JSON.parse(serialized);
|
|
3892
|
+
} catch {
|
|
3893
|
+
return "Invalid interaction_request_human arguments: they are not valid JSON. Nothing was requested; call it again with a JSON object matching its schema.";
|
|
3894
|
+
}
|
|
3895
|
+
const parsed = RequestHumanInteractionToolInput.safeParse(value);
|
|
3896
|
+
if (parsed.success) return null;
|
|
3897
|
+
const issues = parsed.error.issues
|
|
3898
|
+
.slice(0, 5)
|
|
3899
|
+
.map((issue) => `${issue.path.join(".") || "arguments"}: ${issue.message}`);
|
|
3900
|
+
return `Invalid interaction_request_human arguments: ${issues.join("; ")}. Nothing was requested; call it again with corrected arguments.`;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3884
3903
|
/**
|
|
3885
3904
|
* Turn the canonical interaction-request tool into a typed SDK interruption.
|
|
3886
3905
|
* Its MCP/Codemode catalog entry and execution stay unchanged; this projection
|
|
@@ -3903,6 +3922,22 @@ function installInteractionInterventionPolicy(
|
|
|
3903
3922
|
const originalInvoke = tool.invoke.bind(tool);
|
|
3904
3923
|
return {
|
|
3905
3924
|
...tool,
|
|
3925
|
+
// Runs before the approval pause, so invalid arguments come back to
|
|
3926
|
+
// the model as a correctable tool result. Without it the call would
|
|
3927
|
+
// pause first, the intervention could never be recorded, and the
|
|
3928
|
+
// turn would fail.
|
|
3929
|
+
inputGuardrails: [
|
|
3930
|
+
...(tool.inputGuardrails ?? []),
|
|
3931
|
+
defineToolInputGuardrail({
|
|
3932
|
+
name: "validate_interaction_request_human",
|
|
3933
|
+
run: async ({ toolCall }) => {
|
|
3934
|
+
const problem = interactionRequestArgumentsProblem(toolCall.arguments);
|
|
3935
|
+
return problem
|
|
3936
|
+
? ToolGuardrailFunctionOutputFactory.rejectContent(problem)
|
|
3937
|
+
: ToolGuardrailFunctionOutputFactory.allow();
|
|
3938
|
+
},
|
|
3939
|
+
}),
|
|
3940
|
+
],
|
|
3906
3941
|
needsApproval: async (
|
|
3907
3942
|
_runContext: Parameters<typeof originalNeedsApproval>[0],
|
|
3908
3943
|
_parsedInput: Parameters<typeof originalNeedsApproval>[1],
|
|
@@ -8469,7 +8504,10 @@ export class PrefixedMcpServer implements MCPServer {
|
|
|
8469
8504
|
return {
|
|
8470
8505
|
...tool,
|
|
8471
8506
|
name,
|
|
8472
|
-
|
|
8507
|
+
// Name the account only when the turn has several accounts of this
|
|
8508
|
+
// connector to tell apart. With one account the line adds nothing
|
|
8509
|
+
// the model can act on, and it would repeat on every tool.
|
|
8510
|
+
...(this.accountLabel?.account
|
|
8473
8511
|
? {
|
|
8474
8512
|
description:
|
|
8475
8513
|
`Account: ${this.accountLabel.model}. Use only this account; if the intended account for a write is unclear, ask before calling.\n${tool.description ?? ""}`.trimEnd(),
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { AttemptToolJsonSchema } from "@opengeni/contracts";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
type JsonSchemaNode = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/** Keywords that only constrain validation. The tool's Zod schema still
|
|
7
|
+
* enforces them before execution, and rejects with the exact field. */
|
|
8
|
+
const VALIDATION_ONLY_KEYWORDS = new Set(["$schema", "$id", "minLength", "maxLength"]);
|
|
9
|
+
/** Larger numeric bounds are overflow guards, not guidance. */
|
|
10
|
+
const MEANINGFUL_NUMERIC_BOUND = 100_000;
|
|
11
|
+
const MEANINGFUL_MAX_ITEMS = 64;
|
|
12
|
+
/** Longer patterns (UUIDs, control-character guards) are validation noise. */
|
|
13
|
+
const MEANINGFUL_PATTERN_LENGTH = 32;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The JSON Schema an interaction tool shows the model.
|
|
17
|
+
*
|
|
18
|
+
* Model providers expand every `$ref` when they render a tool, so a tagged
|
|
19
|
+
* union such as a browser action, whose locator union repeats in each of its
|
|
20
|
+
* twenty variants, costs several thousand tokens. Each tagged union of objects
|
|
21
|
+
* is shown as one object instead: the tag lists every variant, each field
|
|
22
|
+
* appears once, and one compact line names the fields each variant takes.
|
|
23
|
+
* Validation-only limits are left out. The tool's own Zod schema stays the
|
|
24
|
+
* authority and rejects any invalid combination with the exact field.
|
|
25
|
+
*/
|
|
26
|
+
export function interactionToolInputJsonSchema(schema: z.ZodType): AttemptToolJsonSchema {
|
|
27
|
+
const generated = z.toJSONSchema(schema, { target: "draft-2020-12" }) as JsonSchemaNode;
|
|
28
|
+
return modelFacingSchema(generated) as AttemptToolJsonSchema;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Per-variant field lists of a flattened union, for the enclosing summary. */
|
|
32
|
+
const flattenedVariants = new WeakMap<JsonSchemaNode, Array<[string, string[]]>>();
|
|
33
|
+
|
|
34
|
+
function modelFacingSchema(node: unknown): unknown {
|
|
35
|
+
if (Array.isArray(node)) return node.map(modelFacingSchema);
|
|
36
|
+
if (!isNode(node)) return node;
|
|
37
|
+
const out: JsonSchemaNode = {};
|
|
38
|
+
for (const [key, value] of Object.entries(node)) {
|
|
39
|
+
if (VALIDATION_ONLY_KEYWORDS.has(key)) continue;
|
|
40
|
+
if (key === "additionalProperties" && value === false) continue;
|
|
41
|
+
if (
|
|
42
|
+
(key === "minimum" || key === "maximum") &&
|
|
43
|
+
typeof value === "number" &&
|
|
44
|
+
Math.abs(value) >= MEANINGFUL_NUMERIC_BOUND
|
|
45
|
+
) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (key === "minItems" && typeof value === "number" && value <= 1) continue;
|
|
49
|
+
if (key === "maxItems" && typeof value === "number" && value > MEANINGFUL_MAX_ITEMS) continue;
|
|
50
|
+
if (
|
|
51
|
+
key === "pattern" &&
|
|
52
|
+
typeof value === "string" &&
|
|
53
|
+
value.length > MEANINGFUL_PATTERN_LENGTH
|
|
54
|
+
) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
out[key] = modelFacingSchema(value);
|
|
58
|
+
}
|
|
59
|
+
for (const unionKey of ["oneOf", "anyOf"] as const) {
|
|
60
|
+
const members = out[unionKey];
|
|
61
|
+
if (!Array.isArray(members)) continue;
|
|
62
|
+
const flattened = flattenTaggedUnion(members);
|
|
63
|
+
if (!flattened) continue;
|
|
64
|
+
delete out[unionKey];
|
|
65
|
+
const { description, ...rest } = flattened;
|
|
66
|
+
Object.assign(out, rest);
|
|
67
|
+
const own = typeof out.description === "string" ? out.description : "";
|
|
68
|
+
out.description = own ? `${own} ${String(description)}` : description;
|
|
69
|
+
flattenedVariants.set(out, flattenedVariants.get(flattened) ?? []);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One object for a union whose members are objects sharing a string tag.
|
|
75
|
+
* Null when the union is not such a tagged union. */
|
|
76
|
+
function flattenTaggedUnion(members: unknown[]): JsonSchemaNode | null {
|
|
77
|
+
if (members.length < 2 || !members.every(isObjectSchema)) return null;
|
|
78
|
+
const objects = members as Array<JsonSchemaNode & { properties: Record<string, unknown> }>;
|
|
79
|
+
const tag = ["type", "kind", "operation"].find((candidate) =>
|
|
80
|
+
objects.every((member) => tagValues(member.properties[candidate]) !== null),
|
|
81
|
+
);
|
|
82
|
+
if (!tag) return null;
|
|
83
|
+
|
|
84
|
+
const variants: Array<[string, string[]]> = [];
|
|
85
|
+
const tagValuesInOrder: string[] = [];
|
|
86
|
+
const properties = new Map<string, unknown[]>();
|
|
87
|
+
for (const member of objects) {
|
|
88
|
+
const nested = flattenedVariants.get(member);
|
|
89
|
+
const required = new Set(Array.isArray(member.required) ? (member.required as string[]) : []);
|
|
90
|
+
if (nested) {
|
|
91
|
+
variants.push(...nested);
|
|
92
|
+
} else {
|
|
93
|
+
const fields = Object.keys(member.properties)
|
|
94
|
+
.filter((field) => field !== tag)
|
|
95
|
+
.map((field) => (required.has(field) ? field : `${field}?`));
|
|
96
|
+
for (const value of tagValues(member.properties[tag])!) variants.push([value, fields]);
|
|
97
|
+
}
|
|
98
|
+
for (const value of tagValues(member.properties[tag])!) {
|
|
99
|
+
if (!tagValuesInOrder.includes(value)) tagValuesInOrder.push(value);
|
|
100
|
+
}
|
|
101
|
+
for (const [field, schema] of Object.entries(member.properties)) {
|
|
102
|
+
if (field === tag) continue;
|
|
103
|
+
const seen = properties.get(field) ?? [];
|
|
104
|
+
if (!seen.some((existing) => sameSchema(existing, schema))) seen.push(schema);
|
|
105
|
+
properties.set(field, seen);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const merged: Record<string, unknown> = {
|
|
110
|
+
[tag]: { type: "string", enum: tagValuesInOrder },
|
|
111
|
+
};
|
|
112
|
+
for (const [field, schemas] of properties) {
|
|
113
|
+
merged[field] = schemas.length === 1 ? schemas[0] : mergeAlternatives(schemas);
|
|
114
|
+
}
|
|
115
|
+
const summary = variants
|
|
116
|
+
.map(([value, fields]) => (fields.length > 0 ? `${value}: ${fields.join(", ")}` : value))
|
|
117
|
+
.join("; ");
|
|
118
|
+
const flattened: JsonSchemaNode = {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: merged,
|
|
121
|
+
required: [tag],
|
|
122
|
+
description: `Fields by ${tag} (? = optional): ${summary}.`,
|
|
123
|
+
};
|
|
124
|
+
flattenedVariants.set(flattened, variants);
|
|
125
|
+
return flattened;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeAlternatives(schemas: unknown[]): unknown {
|
|
129
|
+
if (schemas.every((schema) => tagValues(schema) !== null)) {
|
|
130
|
+
return { type: "string", enum: [...new Set(schemas.flatMap((schema) => tagValues(schema)!))] };
|
|
131
|
+
}
|
|
132
|
+
return { anyOf: schemas };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function tagValues(schema: unknown): string[] | null {
|
|
136
|
+
if (!isNode(schema)) return null;
|
|
137
|
+
if (typeof schema.const === "string") return [schema.const];
|
|
138
|
+
if (
|
|
139
|
+
Array.isArray(schema.enum) &&
|
|
140
|
+
schema.enum.length > 0 &&
|
|
141
|
+
schema.enum.every((value) => typeof value === "string")
|
|
142
|
+
) {
|
|
143
|
+
return schema.enum as string[];
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isObjectSchema(schema: unknown): boolean {
|
|
149
|
+
return isNode(schema) && schema.type === "object" && isNode(schema.properties);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sameSchema(left: unknown, right: unknown): boolean {
|
|
153
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isNode(value: unknown): value is JsonSchemaNode {
|
|
157
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const MAX_REPORTED_ISSUES = 4;
|
|
161
|
+
const MAX_ISSUE_MESSAGE_CHARS = 240;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Name exactly what is wrong with invalid tool arguments. A union failure
|
|
165
|
+
* reports the variant the arguments chose (the one whose tag matched), so a
|
|
166
|
+
* click without a locator role says `action.locator.role`, not only "invalid".
|
|
167
|
+
*/
|
|
168
|
+
export function describeInvalidToolArguments(error: z.ZodError): string {
|
|
169
|
+
const issues = leafIssues(error.issues, []).slice(0, MAX_REPORTED_ISSUES);
|
|
170
|
+
const described = issues.map(({ path, message }) => {
|
|
171
|
+
const text = message.replace(/^Invalid input: /u, "").slice(0, MAX_ISSUE_MESSAGE_CHARS);
|
|
172
|
+
return path.length > 0 ? `${path.join(".")}: ${text}` : text;
|
|
173
|
+
});
|
|
174
|
+
return `Interaction tool arguments are invalid: ${described.join("; ")}.`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
type LeafIssue = { path: PropertyKey[]; message: string };
|
|
178
|
+
|
|
179
|
+
function leafIssues(issues: readonly z.core.$ZodIssue[], base: PropertyKey[]): LeafIssue[] {
|
|
180
|
+
const leaves: LeafIssue[] = [];
|
|
181
|
+
for (const issue of issues) {
|
|
182
|
+
const path = [...base, ...issue.path];
|
|
183
|
+
if (issue.code === "invalid_union" && issue.errors.length > 0) {
|
|
184
|
+
leaves.push(...leafIssues(chosenUnionBranch(issue.errors), path));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (issue.code === "unrecognized_keys") {
|
|
188
|
+
leaves.push({ path, message: `unexpected field ${issue.keys.join(", ")}` });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
leaves.push({ path, message: issue.message });
|
|
192
|
+
}
|
|
193
|
+
return leaves;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The union branch the arguments meant: one whose tag matched, else the
|
|
197
|
+
* branch that reports the unknown tag, else the one with the fewest issues. */
|
|
198
|
+
function chosenUnionBranch(branches: z.core.$ZodIssue[][]): z.core.$ZodIssue[] {
|
|
199
|
+
const tagMismatch = (issue: z.core.$ZodIssue): boolean =>
|
|
200
|
+
(issue.code === "invalid_union" && issue.errors.length === 0) ||
|
|
201
|
+
(issue.code === "invalid_value" &&
|
|
202
|
+
["type", "kind", "operation"].includes(String(issue.path.at(-1))));
|
|
203
|
+
const matching = branches.filter((branch) => !branch.some(tagMismatch));
|
|
204
|
+
if (matching.length > 0) {
|
|
205
|
+
return matching.reduce((best, branch) => (branch.length < best.length ? branch : best));
|
|
206
|
+
}
|
|
207
|
+
const unknownTag = branches.find((branch) =>
|
|
208
|
+
branch.some((issue) => issue.code === "invalid_union" && issue.errors.length === 0),
|
|
209
|
+
);
|
|
210
|
+
return (
|
|
211
|
+
unknownTag?.filter((issue) => issue.code === "invalid_union") ??
|
|
212
|
+
branches.reduce((best, branch) => (branch.length < best.length ? branch : best))
|
|
213
|
+
);
|
|
214
|
+
}
|
package/src/interaction-tools.ts
CHANGED
|
@@ -90,7 +90,10 @@ import {
|
|
|
90
90
|
type InteractionTransport,
|
|
91
91
|
} from "@opengeni/sdk";
|
|
92
92
|
import { z } from "zod";
|
|
93
|
-
import {
|
|
93
|
+
import {
|
|
94
|
+
describeInvalidToolArguments,
|
|
95
|
+
interactionToolInputJsonSchema,
|
|
96
|
+
} from "./interaction-tool-json-schema";
|
|
94
97
|
import { guardedMcpFetch } from "./mcp-network";
|
|
95
98
|
|
|
96
99
|
export const INTERACTION_ATTEMPT_TOOL_NAMES = FIRST_PARTY_IN_PROCESS_TOOL_NAMES;
|
|
@@ -786,10 +789,7 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
786
789
|
codemodePath: options.codemodePath,
|
|
787
790
|
title: options.title,
|
|
788
791
|
description: options.description,
|
|
789
|
-
inputSchema:
|
|
790
|
-
options.name === "browser_act"
|
|
791
|
-
? browserActionInputJsonSchema(options.input)
|
|
792
|
-
: jsonSchema(options.input),
|
|
792
|
+
inputSchema: interactionToolInputJsonSchema(options.input),
|
|
793
793
|
outputSchema: jsonSchema(options.output),
|
|
794
794
|
annotations: {
|
|
795
795
|
title: options.title,
|
|
@@ -2603,7 +2603,7 @@ async function safeInteractionExecution<TInput extends z.ZodType, TOutput extend
|
|
|
2603
2603
|
return result;
|
|
2604
2604
|
} catch (error) {
|
|
2605
2605
|
if (error instanceof z.ZodError) {
|
|
2606
|
-
return interactionErrorResult("invalid_arguments",
|
|
2606
|
+
return interactionErrorResult("invalid_arguments", describeInvalidToolArguments(error));
|
|
2607
2607
|
}
|
|
2608
2608
|
// A failed read is still a useful tool result. Preserve the API's public
|
|
2609
2609
|
// explanation; mutations and uncertain outcomes retain their failure path.
|
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
} from "./browser-control-server";
|
|
88
88
|
import { parseExecResponseBanner } from "./exec-banner";
|
|
89
89
|
import { controllerStreamRequest, parseControllerStreamResponse } from "./browser-control-stream";
|
|
90
|
+
import type { BrowserControlHttpCall, BrowserControlHttpOutcome } from "./browser-control-http";
|
|
90
91
|
import {
|
|
91
92
|
buildStreamUrl,
|
|
92
93
|
exposedPortAllowsHostFetch,
|
|
@@ -177,6 +178,8 @@ export type BrowserControlPlacementSession = {
|
|
|
177
178
|
ensureBrowserControl?: (
|
|
178
179
|
request: BrowserControlEnsureRequest,
|
|
179
180
|
) => Promise<BrowserControlEnsureResponse>;
|
|
181
|
+
/** Agent-native controller transport, addressed by authority scope. */
|
|
182
|
+
browserControlHttp?: (request: BrowserControlHttpCall) => Promise<BrowserControlHttpOutcome>;
|
|
180
183
|
openBrowserFrames?: (
|
|
181
184
|
request: BrowserFramesOpenRequest,
|
|
182
185
|
) => Promise<{ channel: StreamChannel; endpoint: ExposedPortEndpoint }>;
|
|
@@ -1176,6 +1179,19 @@ export class BrowserControlClient {
|
|
|
1176
1179
|
);
|
|
1177
1180
|
}
|
|
1178
1181
|
|
|
1182
|
+
const native = await this.requestNative(input, BROWSER_CONTROL_MAX_JSON_BYTES);
|
|
1183
|
+
if (native) {
|
|
1184
|
+
let text: string;
|
|
1185
|
+
try {
|
|
1186
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(native.body);
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
throw new BrowserControlProtocolError("browser controller response is not UTF-8", {
|
|
1189
|
+
cause: error,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
return parseEnvelope(text, native.status);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1179
1195
|
if (
|
|
1180
1196
|
this.session.execWithInput &&
|
|
1181
1197
|
(input.body === undefined || Buffer.byteLength(JSON.stringify(input.body)) <= 32 * 1024)
|
|
@@ -1357,6 +1373,18 @@ export class BrowserControlClient {
|
|
|
1357
1373
|
);
|
|
1358
1374
|
}
|
|
1359
1375
|
|
|
1376
|
+
const nativeFrame = await this.requestNative(
|
|
1377
|
+
input,
|
|
1378
|
+
input.surface === "browser" ? 24 * 1024 * 1024 : COMPUTER_SCREENSHOT_MAX_BYTES,
|
|
1379
|
+
);
|
|
1380
|
+
if (nativeFrame) {
|
|
1381
|
+
if (nativeFrame.status < 200 || nativeFrame.status >= 300) {
|
|
1382
|
+
parseEnvelope(new TextDecoder().decode(nativeFrame.body), nativeFrame.status);
|
|
1383
|
+
throw new BrowserControlProtocolError("browser controller image response is invalid");
|
|
1384
|
+
}
|
|
1385
|
+
return controllerImageFrame(nativeFrame.body, nativeFrame.headers, input);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1360
1388
|
if (this.session.execWithInput) {
|
|
1361
1389
|
try {
|
|
1362
1390
|
const response = await this.requestStream(
|
|
@@ -1512,8 +1540,69 @@ export class BrowserControlClient {
|
|
|
1512
1540
|
}
|
|
1513
1541
|
|
|
1514
1542
|
private async controllerPort(): Promise<number> {
|
|
1543
|
+
if (!this.nativeAuthority || !this.session.ensureBrowserControl) return this.port;
|
|
1544
|
+
return await this.ensureControllerPort(false);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/** The agent-native transport: one in-process loopback request on the
|
|
1548
|
+
* Connected Machine, addressed by authority scope, with no process started.
|
|
1549
|
+
* Null when the placement does not serve it; the caller then keeps its
|
|
1550
|
+
* command transport. A controller that is not running receives nothing, so
|
|
1551
|
+
* it is ensured and the request sent once more whatever its method. */
|
|
1552
|
+
private async requestNative(
|
|
1553
|
+
input: { method: string; path: string; token: string; body?: unknown; timeoutMs?: number },
|
|
1554
|
+
maxResponseBytes: number,
|
|
1555
|
+
retry = true,
|
|
1556
|
+
): Promise<{ status: number; headers: Map<string, string>; body: Uint8Array } | null> {
|
|
1557
|
+
const authority = this.nativeAuthority;
|
|
1558
|
+
if (!authority || !this.session.browserControlHttp || !this.session.ensureBrowserControl) {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
const method = input.method;
|
|
1562
|
+
if (method !== "GET" && method !== "POST" && method !== "PUT" && method !== "DELETE") {
|
|
1563
|
+
throw new BrowserControlProtocolError("browser controller request method is invalid");
|
|
1564
|
+
}
|
|
1565
|
+
// Validates the path exactly as the command transport does.
|
|
1566
|
+
localControllerUrl(this.port, input.path);
|
|
1567
|
+
const body =
|
|
1568
|
+
input.body === undefined ? undefined : new TextEncoder().encode(JSON.stringify(input.body));
|
|
1569
|
+
if (body !== undefined && body.byteLength > BROWSER_CONTROL_MAX_JSON_BYTES) {
|
|
1570
|
+
throw new RangeError("browser controller request body is too large");
|
|
1571
|
+
}
|
|
1572
|
+
let outcome: BrowserControlHttpOutcome;
|
|
1573
|
+
try {
|
|
1574
|
+
outcome = await this.session.browserControlHttp({
|
|
1575
|
+
scopeId: authority.scopeId,
|
|
1576
|
+
scopeGeneration: authority.scopeGeneration,
|
|
1577
|
+
method,
|
|
1578
|
+
path: input.path,
|
|
1579
|
+
bearerToken: requireToken(input.token, "browser controller token"),
|
|
1580
|
+
...(body === undefined ? {} : { body }),
|
|
1581
|
+
timeoutMs: boundedTimeout(input.timeoutMs ?? this.timeoutMs),
|
|
1582
|
+
maxResponseBytes,
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
if (retry && method === "GET") {
|
|
1586
|
+
await this.ensureControllerPort(true);
|
|
1587
|
+
return await this.requestNative(input, maxResponseBytes, false);
|
|
1588
|
+
}
|
|
1589
|
+
throw new BrowserControlTransportError("browser controller request transport failed", {
|
|
1590
|
+
cause: error,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
if (outcome.kind === "unsupported") return null;
|
|
1594
|
+
if (outcome.kind === "controller_absent") {
|
|
1595
|
+
if (!retry) throw new BrowserControlTransportError("browser controller is not running");
|
|
1596
|
+
await this.ensureControllerPort(true);
|
|
1597
|
+
return await this.requestNative(input, maxResponseBytes, false);
|
|
1598
|
+
}
|
|
1599
|
+
return outcome;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
private async ensureControllerPort(replace: boolean): Promise<number> {
|
|
1515
1603
|
if (!this.nativeAuthority || !this.session.ensureBrowserControl) return this.port;
|
|
1516
1604
|
const key = nativeControllerKey(this.nativeAuthority);
|
|
1605
|
+
if (replace) nativeControllerPorts.delete(key);
|
|
1517
1606
|
const cached = nativeControllerPorts.get(key);
|
|
1518
1607
|
if (cached !== undefined) return cached;
|
|
1519
1608
|
const ensured = await this.session.ensureBrowserControl({
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** One request to an agent-owned browser/computer controller, addressed by its
|
|
2
|
+
* exact authority scope rather than a local port. */
|
|
3
|
+
export type BrowserControlHttpCall = {
|
|
4
|
+
scopeId: string;
|
|
5
|
+
scopeGeneration: string;
|
|
6
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
7
|
+
/** Absolute controller path with optional query. */
|
|
8
|
+
path: string;
|
|
9
|
+
bearerToken: string;
|
|
10
|
+
/** JSON request body bytes, when the request has a body. */
|
|
11
|
+
body?: Uint8Array;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
maxResponseBytes: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** What a placement transport did with one controller request. */
|
|
17
|
+
export type BrowserControlHttpOutcome =
|
|
18
|
+
/** The placement cannot serve it natively; nothing was sent. */
|
|
19
|
+
| { kind: "unsupported" }
|
|
20
|
+
/** No controller for that scope and generation accepted it; nothing was
|
|
21
|
+
* delivered, so the caller may ensure the controller and send again. */
|
|
22
|
+
| { kind: "controller_absent" }
|
|
23
|
+
| {
|
|
24
|
+
kind: "response";
|
|
25
|
+
status: number;
|
|
26
|
+
/** Lowercase header names. */
|
|
27
|
+
headers: Map<string, string>;
|
|
28
|
+
body: Uint8Array;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Extra time beyond the request's own deadline for the reply to arrive. */
|
|
32
|
+
export const BROWSER_CONTROL_HTTP_REPLY_GRACE_MS = 5_000;
|
|
@@ -67,6 +67,7 @@ export interface SelfhostedConnectionBinding {
|
|
|
67
67
|
operationResourcePolicySupported: boolean;
|
|
68
68
|
operationCpuQuotaSupported: boolean;
|
|
69
69
|
transactionalFsWriteSupported?: boolean;
|
|
70
|
+
browserControlHttpSupported?: boolean;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export interface ActiveBackendResolverDeps {
|
|
@@ -406,6 +407,7 @@ export function makeActiveBackendResolver(
|
|
|
406
407
|
operationResourcePolicySupported: connection.operationResourcePolicySupported,
|
|
407
408
|
operationCpuQuotaSupported: connection.operationCpuQuotaSupported,
|
|
408
409
|
transactionalFsWriteSupported: connection.transactionalFsWriteSupported === true,
|
|
410
|
+
browserControlHttpSupported: connection.browserControlHttpSupported === true,
|
|
409
411
|
// The routing proxy caches this session by sandbox+epoch, but command
|
|
410
412
|
// policy and runner authority are mutable. Re-read one coherent DB
|
|
411
413
|
// snapshot only when a new exec/Git is admitted.
|