@zackbart/connecta 0.22.3 → 0.24.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/AGENTS.md +5 -0
- package/CHANGELOG.md +82 -0
- package/README.md +19 -11
- package/dist/activity-friction.d.ts +3 -0
- package/dist/activity-friction.js +19 -0
- package/dist/activity.d.ts +11 -2
- package/dist/activity.js +15 -19
- package/dist/auth/downstream-oauth.d.ts +2 -1
- package/dist/auth/downstream-oauth.js +10 -1
- package/dist/branding.d.ts +67 -0
- package/dist/branding.js +176 -0
- package/dist/catalog-service.d.ts +1 -6
- package/dist/catalog-service.js +3 -53
- package/dist/connectors/remote-mcp.js +3 -5
- package/dist/credential-contract.d.ts +24 -0
- package/dist/credential-contract.js +1 -0
- package/dist/credential-rules.d.ts +85 -0
- package/dist/credential-rules.js +107 -0
- package/dist/credentials.d.ts +4 -100
- package/dist/credentials.js +3 -107
- package/dist/errors.d.ts +1 -1
- package/dist/execute.d.ts +5 -52
- package/dist/execute.js +40 -347
- package/dist/executors/quickjs-protocol.d.ts +0 -7
- package/dist/executors/quickjs-protocol.js +2 -10
- package/dist/executors/quickjs.js +1 -1
- package/dist/index.d.ts +23 -56
- package/dist/index.js +30 -58
- package/dist/invocation.d.ts +0 -33
- package/dist/invocation.js +56 -124
- package/dist/meta-tools.d.ts +7 -6
- package/dist/meta-tools.js +14 -21
- package/dist/module-contracts.d.ts +19 -0
- package/dist/module-contracts.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +6 -3
- package/dist/operator-ui/view.d.ts +2 -18
- package/dist/operator-ui/view.js +3 -20
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +8 -6
- package/dist/routes/activity.js +1 -1
- package/dist/routes/credentials.js +5 -2
- package/dist/routes/mcp.js +7 -51
- package/dist/routes/oauth-management.d.ts +2 -0
- package/dist/routes/oauth-management.js +108 -0
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +21 -121
- package/dist/routes/shared.d.ts +19 -17
- package/dist/routes/shared.js +48 -44
- package/dist/routes/ui.js +36 -33
- package/dist/server.d.ts +1 -2
- package/dist/server.js +7 -45
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +55 -19
- package/dist/types.d.ts +3 -15
- package/dist/ui.d.ts +15 -70
- package/dist/ui.js +176 -317
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +31 -26
- package/documentation/auth.md +65 -111
- package/documentation/call-admission.md +10 -11
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +65 -260
- package/documentation/connectors.md +8 -6
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +37 -18
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +2 -2
- package/documentation/operations.md +19 -23
- package/documentation/operator-ui.md +82 -104
- package/documentation/optional-modules-upgrade.md +243 -0
- package/documentation/provider-conventions.md +7 -5
- package/documentation/revenuecat.md +1 -1
- package/documentation/storage-and-credentials.md +59 -40
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +83 -8
- package/ethos.md +36 -43
- package/examples/worker/AGENTS.md +3 -1
- package/examples/worker/README.md +68 -84
- package/examples/worker/src/d1-activity.ts +1 -1
- package/examples/worker/src/index.ts +11 -6
- package/package.json +18 -2
- package/templates/node/AGENTS.md +8 -6
- package/templates/node/README.md +56 -67
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +1 -1
- package/templates/node/src/index.ts +11 -12
- package/dist/access-tokens.d.ts +0 -31
- package/dist/access-tokens.js +0 -236
- package/dist/apps-shell.d.ts +0 -37
- package/dist/apps-shell.js +0 -174
- package/dist/routes/access-tokens.d.ts +0 -6
- package/dist/routes/access-tokens.js +0 -83
package/dist/execute.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { type ToolResult } from "./meta-tools.js";
|
|
|
5
5
|
import { InvocationFailure } from "./invocation.js";
|
|
6
6
|
import type { RegistryView } from "./registry.js";
|
|
7
7
|
import type { Executor, ExecutorProvider, Logger } from "./types.js";
|
|
8
|
-
export declare const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
9
8
|
/** Complete entries plus an exact omission count, all inside this byte cap. */
|
|
10
9
|
export declare const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
11
10
|
/**
|
|
@@ -17,7 +16,7 @@ export declare const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
|
17
16
|
*/
|
|
18
17
|
export declare const EXECUTE_MAX_EMITTED_BYTES = 4000000;
|
|
19
18
|
export declare const EXECUTE_MAX_EMITTED_BLOCKS = 32;
|
|
20
|
-
type ExecuteDiagnosticOperation = "search" | "describe" | "call"
|
|
19
|
+
type ExecuteDiagnosticOperation = "search" | "describe" | "call";
|
|
21
20
|
interface ExecuteOperationDiagnostics {
|
|
22
21
|
operation: ExecuteDiagnosticOperation;
|
|
23
22
|
count: number;
|
|
@@ -26,7 +25,6 @@ interface ExecuteOperationDiagnostics {
|
|
|
26
25
|
resultBytes: number;
|
|
27
26
|
catalogMs: number;
|
|
28
27
|
connectorMs: number;
|
|
29
|
-
calls?: number;
|
|
30
28
|
}
|
|
31
29
|
declare class ExecuteDiagnostics {
|
|
32
30
|
private readonly started;
|
|
@@ -35,19 +33,12 @@ declare class ExecuteDiagnostics {
|
|
|
35
33
|
setupMs: number;
|
|
36
34
|
executorWallMs: number;
|
|
37
35
|
private emitted?;
|
|
38
|
-
private ui?;
|
|
39
36
|
/** Numbers only, per R8 — and only once something was emitted, so a
|
|
40
37
|
* non-emitting run's diagnostics stay byte-for-byte what they were. */
|
|
41
38
|
recordEmitted(count: number, bytes: number): void;
|
|
42
|
-
/**
|
|
43
|
-
* U9: the UI payload gets its own aggregate — one number, the payload's
|
|
44
|
-
* serialized size. Folding it into `emitted` would desync that aggregate's
|
|
45
|
-
* pair, which reports the bytes a specific block count cost.
|
|
46
|
-
*/
|
|
47
|
-
recordUi(bytes: number): void;
|
|
48
39
|
private stats;
|
|
49
40
|
recordCatalog(operation: "search" | "describe", durationMs: number, ok: boolean, result?: unknown): void;
|
|
50
|
-
recordCall(
|
|
41
|
+
recordCall(outcome: {
|
|
51
42
|
ok: boolean;
|
|
52
43
|
durationMs: number;
|
|
53
44
|
timing: {
|
|
@@ -56,7 +47,6 @@ declare class ExecuteDiagnostics {
|
|
|
56
47
|
};
|
|
57
48
|
value?: unknown;
|
|
58
49
|
}): void;
|
|
59
|
-
recordBatch(durationMs: number, ok: boolean, calls: number, result?: unknown): void;
|
|
60
50
|
finish(): {
|
|
61
51
|
timing: {
|
|
62
52
|
totalMs: number;
|
|
@@ -71,7 +61,6 @@ declare class ExecuteDiagnostics {
|
|
|
71
61
|
count: number;
|
|
72
62
|
bytes: number;
|
|
73
63
|
};
|
|
74
|
-
ui?: number;
|
|
75
64
|
};
|
|
76
65
|
}
|
|
77
66
|
/**
|
|
@@ -91,56 +80,20 @@ export type EmittedBlock = {
|
|
|
91
80
|
data: string;
|
|
92
81
|
mimeType: string;
|
|
93
82
|
};
|
|
94
|
-
interface UiPayload {
|
|
95
|
-
html: string;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
|
|
99
|
-
* loudly at the crossing call — nothing is partially accepted and prior blocks
|
|
100
|
-
* are unaffected — so a program learns it is over budget while it can still
|
|
101
|
-
* choose differently (M5, U4). Accepted output never rides `ExecuteResult`; the
|
|
102
|
-
* handler that owns this collector delivers it on the final tool result.
|
|
103
|
-
*
|
|
104
|
-
* The two channels share the byte aggregate and nothing else: a UI payload is
|
|
105
|
-
* not a block, so it spends no block count, and at most one is ever accepted.
|
|
106
|
-
*/
|
|
107
83
|
export declare class EmitCollector {
|
|
108
84
|
private readonly maxBytes;
|
|
109
85
|
private readonly maxBlocks;
|
|
110
86
|
private readonly diagnostics?;
|
|
111
87
|
readonly blocks: EmittedBlock[];
|
|
112
|
-
/** The shared transport aggregate: emitted blocks plus the UI payload. */
|
|
113
88
|
bytes: number;
|
|
114
|
-
/** The one accepted UI payload (U2), delivered in result `_meta` on success. */
|
|
115
|
-
ui?: UiPayload;
|
|
116
|
-
/** What the blocks alone cost, so the `emitted` aggregate stays a true pair. */
|
|
117
|
-
private blockBytes;
|
|
118
89
|
constructor(maxBytes: number, maxBlocks: number, diagnostics?: ExecuteDiagnostics | undefined);
|
|
119
90
|
accept(raw: unknown): void;
|
|
120
|
-
/**
|
|
121
|
-
* U2 and U4: one payload per run, measured as the serialized bytes of
|
|
122
|
-
* `{ html }` against the same aggregate emit spends. A second call throws
|
|
123
|
-
* naming the constraint rather than replacing the first — one tool result
|
|
124
|
-
* renders one view, and last-wins would silently discard a payload the
|
|
125
|
-
* program deliberately supplied.
|
|
126
|
-
*
|
|
127
|
-
* Multiplicity is checked before shape, so the second call is told what it
|
|
128
|
-
* actually broke. A program whose second payload is also malformed has one
|
|
129
|
-
* problem worth naming — that there is a second payload at all — and a
|
|
130
|
-
* complaint about its type would send the author to fix the wrong thing.
|
|
131
|
-
*/
|
|
132
|
-
acceptUi(...values: unknown[]): void;
|
|
133
|
-
private assertUiVacant;
|
|
134
|
-
private acceptUiPayload;
|
|
135
91
|
}
|
|
136
|
-
/** Convert a connector/tool name into a valid JS identifier. */
|
|
137
|
-
export declare function sanitizeIdentifier(name: string): string;
|
|
138
92
|
/**
|
|
139
|
-
* Expose one
|
|
140
|
-
*
|
|
141
|
-
* calls that namespace or explicitly asks search/describe.
|
|
93
|
+
* Expose one host provider and typed guest errors. Catalogs load only when
|
|
94
|
+
* the program calls a tool or asks search/describe.
|
|
142
95
|
*/
|
|
143
|
-
export declare function buildSandboxProviders(registry: RegistryView, baseUrl: string,
|
|
96
|
+
export declare function buildSandboxProviders(registry: RegistryView, baseUrl: string, _logger: Logger, activity?: ActivityRequestContext, limits?: {
|
|
144
97
|
signal?: AbortSignal | undefined;
|
|
145
98
|
maxHostCalls?: number | undefined;
|
|
146
99
|
hostCallTimeoutMs?: number | undefined;
|
package/dist/execute.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { PROGRAM_UI_META_KEY, PROGRAM_UI_RESOURCE_URI, } from "./apps-shell.js";
|
|
3
2
|
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
|
|
4
3
|
import { errorResult, jsonResult } from "./meta-tools.js";
|
|
5
4
|
import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
|
|
6
5
|
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
|
|
7
|
-
import { boundedEchoText,
|
|
6
|
+
import { boundedEchoText, msg } from "./errors.js";
|
|
8
7
|
import { InvocationFailure, InvocationService, } from "./invocation.js";
|
|
9
8
|
import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, } from "./skills.js";
|
|
10
9
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
11
10
|
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
12
|
-
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
13
11
|
const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
|
|
14
12
|
/** Complete entries plus an exact omission count, all inside this byte cap. */
|
|
15
13
|
export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
@@ -30,21 +28,12 @@ class ExecuteDiagnostics {
|
|
|
30
28
|
setupMs = 0;
|
|
31
29
|
executorWallMs = 0;
|
|
32
30
|
emitted;
|
|
33
|
-
ui;
|
|
34
31
|
/** Numbers only, per R8 — and only once something was emitted, so a
|
|
35
32
|
* non-emitting run's diagnostics stay byte-for-byte what they were. */
|
|
36
33
|
recordEmitted(count, bytes) {
|
|
37
34
|
if (count > 0)
|
|
38
35
|
this.emitted = { count, bytes };
|
|
39
36
|
}
|
|
40
|
-
/**
|
|
41
|
-
* U9: the UI payload gets its own aggregate — one number, the payload's
|
|
42
|
-
* serialized size. Folding it into `emitted` would desync that aggregate's
|
|
43
|
-
* pair, which reports the bytes a specific block count cost.
|
|
44
|
-
*/
|
|
45
|
-
recordUi(bytes) {
|
|
46
|
-
this.ui = bytes;
|
|
47
|
-
}
|
|
48
37
|
stats(operation) {
|
|
49
38
|
let stats = this.operations.get(operation);
|
|
50
39
|
if (!stats) {
|
|
@@ -70,36 +59,16 @@ class ExecuteDiagnostics {
|
|
|
70
59
|
if (ok)
|
|
71
60
|
stats.resultBytes += serializedDiagnosticBytes(result);
|
|
72
61
|
}
|
|
73
|
-
recordCall(
|
|
74
|
-
const stats = this.stats(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
stats.resultBytes += serializedDiagnosticBytes(outcome.value);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
stats.calls = (stats.calls ?? 0) + 1;
|
|
83
|
-
}
|
|
62
|
+
recordCall(outcome) {
|
|
63
|
+
const stats = this.stats("call");
|
|
64
|
+
stats.count++;
|
|
65
|
+
if (outcome.ok)
|
|
66
|
+
stats.resultBytes += serializedDiagnosticBytes(outcome.value);
|
|
84
67
|
stats.failures += outcome.ok ? 0 : 1;
|
|
85
|
-
|
|
86
|
-
stats.durationMs += outcome.durationMs;
|
|
68
|
+
stats.durationMs += outcome.durationMs;
|
|
87
69
|
stats.catalogMs += outcome.timing.catalogMs;
|
|
88
70
|
stats.connectorMs += outcome.timing.connectorMs;
|
|
89
71
|
}
|
|
90
|
-
recordBatch(durationMs, ok, calls, result) {
|
|
91
|
-
const stats = this.stats("batch");
|
|
92
|
-
stats.count++;
|
|
93
|
-
// Calls normally accrue while each child runs. Invalid batch input never
|
|
94
|
-
// starts children, so retain the attempted cardinality here.
|
|
95
|
-
if (!ok)
|
|
96
|
-
stats.calls = Math.max(stats.calls ?? 0, calls);
|
|
97
|
-
stats.durationMs += durationMs;
|
|
98
|
-
if (!ok)
|
|
99
|
-
stats.failures++;
|
|
100
|
-
else
|
|
101
|
-
stats.resultBytes += serializedDiagnosticBytes(result);
|
|
102
|
-
}
|
|
103
72
|
finish() {
|
|
104
73
|
const operations = [...this.operations.values()];
|
|
105
74
|
return {
|
|
@@ -113,7 +82,6 @@ class ExecuteDiagnostics {
|
|
|
113
82
|
},
|
|
114
83
|
operations,
|
|
115
84
|
...(this.emitted ? { emitted: this.emitted } : {}),
|
|
116
|
-
...(this.ui !== undefined ? { ui: this.ui } : {}),
|
|
117
85
|
};
|
|
118
86
|
}
|
|
119
87
|
}
|
|
@@ -148,58 +116,12 @@ function requireEmittedBlock(raw) {
|
|
|
148
116
|
}
|
|
149
117
|
return raw;
|
|
150
118
|
}
|
|
151
|
-
const UI_SHAPE_HINT = "connecta.ui accepts exactly one argument: a non-empty string of HTML";
|
|
152
|
-
/** What the argument was, named the way the emit validator names a bad field. */
|
|
153
|
-
function describeUiArgument(raw) {
|
|
154
|
-
if (raw === null)
|
|
155
|
-
return "null";
|
|
156
|
-
if (raw === undefined)
|
|
157
|
-
return "undefined";
|
|
158
|
-
if (Array.isArray(raw))
|
|
159
|
-
return "an array";
|
|
160
|
-
if (typeof raw === "string")
|
|
161
|
-
return "an empty string";
|
|
162
|
-
const kind = typeof raw;
|
|
163
|
-
return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Strict U1 validation. There is no options parameter and no sugar form, for
|
|
167
|
-
* M1's reason: sugar is how a one-shape contract grows hair. An options bag or
|
|
168
|
-
* an MCP block object is just a non-string, and fails as one.
|
|
169
|
-
*/
|
|
170
|
-
function requireUiHtml(raw) {
|
|
171
|
-
if (typeof raw !== "string" || raw.length === 0) {
|
|
172
|
-
throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${describeUiArgument(raw)}`);
|
|
173
|
-
}
|
|
174
|
-
return raw;
|
|
175
|
-
}
|
|
176
|
-
function requireUiPayload(values) {
|
|
177
|
-
if (values.length !== 1) {
|
|
178
|
-
throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${values.length} arguments`);
|
|
179
|
-
}
|
|
180
|
-
return { html: requireUiHtml(values[0]) };
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
|
|
184
|
-
* loudly at the crossing call — nothing is partially accepted and prior blocks
|
|
185
|
-
* are unaffected — so a program learns it is over budget while it can still
|
|
186
|
-
* choose differently (M5, U4). Accepted output never rides `ExecuteResult`; the
|
|
187
|
-
* handler that owns this collector delivers it on the final tool result.
|
|
188
|
-
*
|
|
189
|
-
* The two channels share the byte aggregate and nothing else: a UI payload is
|
|
190
|
-
* not a block, so it spends no block count, and at most one is ever accepted.
|
|
191
|
-
*/
|
|
192
119
|
export class EmitCollector {
|
|
193
120
|
maxBytes;
|
|
194
121
|
maxBlocks;
|
|
195
122
|
diagnostics;
|
|
196
123
|
blocks = [];
|
|
197
|
-
/** The shared transport aggregate: emitted blocks plus the UI payload. */
|
|
198
124
|
bytes = 0;
|
|
199
|
-
/** The one accepted UI payload (U2), delivered in result `_meta` on success. */
|
|
200
|
-
ui;
|
|
201
|
-
/** What the blocks alone cost, so the `emitted` aggregate stays a true pair. */
|
|
202
|
-
blockBytes = 0;
|
|
203
125
|
constructor(maxBytes, maxBlocks, diagnostics) {
|
|
204
126
|
this.maxBytes = maxBytes;
|
|
205
127
|
this.maxBlocks = maxBlocks;
|
|
@@ -216,45 +138,7 @@ export class EmitCollector {
|
|
|
216
138
|
}
|
|
217
139
|
this.blocks.push(block);
|
|
218
140
|
this.bytes += size;
|
|
219
|
-
this.
|
|
220
|
-
this.diagnostics?.recordEmitted(this.blocks.length, this.blockBytes);
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* U2 and U4: one payload per run, measured as the serialized bytes of
|
|
224
|
-
* `{ html }` against the same aggregate emit spends. A second call throws
|
|
225
|
-
* naming the constraint rather than replacing the first — one tool result
|
|
226
|
-
* renders one view, and last-wins would silently discard a payload the
|
|
227
|
-
* program deliberately supplied.
|
|
228
|
-
*
|
|
229
|
-
* Multiplicity is checked before shape, so the second call is told what it
|
|
230
|
-
* actually broke. A program whose second payload is also malformed has one
|
|
231
|
-
* problem worth naming — that there is a second payload at all — and a
|
|
232
|
-
* complaint about its type would send the author to fix the wrong thing.
|
|
233
|
-
*/
|
|
234
|
-
acceptUi(...values) {
|
|
235
|
-
this.assertUiVacant();
|
|
236
|
-
this.acceptUiPayload(requireUiPayload(values));
|
|
237
|
-
}
|
|
238
|
-
assertUiVacant() {
|
|
239
|
-
if (this.ui) {
|
|
240
|
-
throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
acceptUiPayload(payload) {
|
|
244
|
-
let serialized;
|
|
245
|
-
try {
|
|
246
|
-
serialized = JSON.stringify(payload);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
throw guestFailure("invalid_args", "connecta.ui payload must be JSON-serializable");
|
|
250
|
-
}
|
|
251
|
-
const size = diagnosticsEncoder.encode(serialized).byteLength;
|
|
252
|
-
if (this.bytes + size > this.maxBytes) {
|
|
253
|
-
throw guestFailure("budget_exceeded", `connecta.ui byte budget exceeded: payload is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
|
|
254
|
-
}
|
|
255
|
-
this.ui = payload;
|
|
256
|
-
this.bytes += size;
|
|
257
|
-
this.diagnostics?.recordUi(size);
|
|
141
|
+
this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
|
|
258
142
|
}
|
|
259
143
|
}
|
|
260
144
|
/** A configured emit budget must be a finite number >= 1; anything else falls back. */
|
|
@@ -276,85 +160,6 @@ function serializedDiagnosticBytes(value) {
|
|
|
276
160
|
return 0;
|
|
277
161
|
}
|
|
278
162
|
}
|
|
279
|
-
// deno-fmt-ignore
|
|
280
|
-
const RESERVED = new Set([
|
|
281
|
-
"break",
|
|
282
|
-
"case",
|
|
283
|
-
"catch",
|
|
284
|
-
"class",
|
|
285
|
-
"const",
|
|
286
|
-
"continue",
|
|
287
|
-
"debugger",
|
|
288
|
-
"default",
|
|
289
|
-
"delete",
|
|
290
|
-
"do",
|
|
291
|
-
"else",
|
|
292
|
-
"enum",
|
|
293
|
-
"export",
|
|
294
|
-
"extends",
|
|
295
|
-
"false",
|
|
296
|
-
"finally",
|
|
297
|
-
"for",
|
|
298
|
-
"function",
|
|
299
|
-
"if",
|
|
300
|
-
"import",
|
|
301
|
-
"in",
|
|
302
|
-
"instanceof",
|
|
303
|
-
"let",
|
|
304
|
-
"new",
|
|
305
|
-
"null",
|
|
306
|
-
"return",
|
|
307
|
-
"static",
|
|
308
|
-
"super",
|
|
309
|
-
"switch",
|
|
310
|
-
"this",
|
|
311
|
-
"throw",
|
|
312
|
-
"true",
|
|
313
|
-
"try",
|
|
314
|
-
"typeof",
|
|
315
|
-
"var",
|
|
316
|
-
"void",
|
|
317
|
-
"while",
|
|
318
|
-
"with",
|
|
319
|
-
"yield",
|
|
320
|
-
"await",
|
|
321
|
-
"async",
|
|
322
|
-
]);
|
|
323
|
-
/** Convert a connector/tool name into a valid JS identifier. */
|
|
324
|
-
export function sanitizeIdentifier(name) {
|
|
325
|
-
let id = name.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
326
|
-
if (/^[0-9]/.test(id))
|
|
327
|
-
id = `_${id}`;
|
|
328
|
-
if (RESERVED.has(id))
|
|
329
|
-
id = `${id}_`;
|
|
330
|
-
return id;
|
|
331
|
-
}
|
|
332
|
-
const SANDBOX_RESERVED_NAMES = new Set([
|
|
333
|
-
"connecta",
|
|
334
|
-
"console",
|
|
335
|
-
"arguments",
|
|
336
|
-
"result",
|
|
337
|
-
"undefined",
|
|
338
|
-
"setTimeout",
|
|
339
|
-
"Promise",
|
|
340
|
-
"Error",
|
|
341
|
-
"WorkerEntrypoint",
|
|
342
|
-
"CodeExecutor",
|
|
343
|
-
"__invoke",
|
|
344
|
-
"__namespace",
|
|
345
|
-
"__call",
|
|
346
|
-
"__log",
|
|
347
|
-
"__dispatchers",
|
|
348
|
-
"__connectors",
|
|
349
|
-
"__logs",
|
|
350
|
-
"__CODEMODE_BINARY_TAG",
|
|
351
|
-
"__bytesToBase64",
|
|
352
|
-
"__base64ToBytes",
|
|
353
|
-
"__encodeCodemodeValue",
|
|
354
|
-
"__decodeCodemodeValue",
|
|
355
|
-
"__stringifyForCodemode",
|
|
356
|
-
"__parseForCodemode",
|
|
357
|
-
]);
|
|
358
163
|
function guestFailure(code, message, retryable = false) {
|
|
359
164
|
return new InvocationFailure({ code, message, retryable });
|
|
360
165
|
}
|
|
@@ -373,15 +178,8 @@ function framedGuestFailure(secret, failure) {
|
|
|
373
178
|
guestFailureFrames.set(failure, framed.message);
|
|
374
179
|
return framed;
|
|
375
180
|
}
|
|
376
|
-
/**
|
|
377
|
-
|
|
378
|
-
* shortcuts. The random frame is hidden in this closure and Error is locked:
|
|
379
|
-
* connector prose and model code can neither collide with nor forge its frame.
|
|
380
|
-
*/
|
|
381
|
-
function lazyNamespacePrelude(connectors, failureSecret) {
|
|
382
|
-
const declarations = connectors
|
|
383
|
-
.map(({ id, namespace }) => `globalThis[${JSON.stringify(namespace)}] = __makeConnectaNamespace(${JSON.stringify(id)});`)
|
|
384
|
-
.join("\n");
|
|
181
|
+
/** Rebuild host failures as guest errors without exposing the private frame. */
|
|
182
|
+
function guestErrorPrelude(failureSecret) {
|
|
385
183
|
return `((failurePrefix) => {
|
|
386
184
|
const NativeError = globalThis.Error;
|
|
387
185
|
const startsWith = Function.prototype.call.bind(String.prototype.startsWith);
|
|
@@ -417,20 +215,13 @@ function lazyNamespacePrelude(connectors, failureSecret) {
|
|
|
417
215
|
writable: false,
|
|
418
216
|
configurable: false
|
|
419
217
|
});
|
|
420
|
-
const __makeConnectaNamespace = (connectorId) => Object.freeze(new Proxy(Object.create(null), {
|
|
421
|
-
get: (_target, toolName) => typeof toolName === "string"
|
|
422
|
-
? (args) => connecta.__callNamespace(connectorId, toolName, args)
|
|
423
|
-
: undefined
|
|
424
|
-
}));
|
|
425
|
-
${declarations}
|
|
426
218
|
})(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
|
|
427
219
|
}
|
|
428
220
|
/**
|
|
429
|
-
* Expose one
|
|
430
|
-
*
|
|
431
|
-
* calls that namespace or explicitly asks search/describe.
|
|
221
|
+
* Expose one host provider and typed guest errors. Catalogs load only when
|
|
222
|
+
* the program calls a tool or asks search/describe.
|
|
432
223
|
*/
|
|
433
|
-
export async function buildSandboxProviders(registry, baseUrl,
|
|
224
|
+
export async function buildSandboxProviders(registry, baseUrl, _logger, activity, limits = {}) {
|
|
434
225
|
// All host calls made by one execute_code invocation share a downstream
|
|
435
226
|
// connection, while a later invocation receives a fresh request scope.
|
|
436
227
|
const requestScope = {};
|
|
@@ -447,24 +238,6 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
447
238
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
448
239
|
const failureSecret = guestFailureSecret();
|
|
449
240
|
let hostCalls = 0;
|
|
450
|
-
const connectors = registry.listConnectors();
|
|
451
|
-
const namespaces = [];
|
|
452
|
-
const namespaceOwners = new Map();
|
|
453
|
-
for (const connector of connectors) {
|
|
454
|
-
const namespace = sanitizeIdentifier(connector.id);
|
|
455
|
-
const owner = namespaceOwners.get(namespace);
|
|
456
|
-
const problem = SANDBOX_RESERVED_NAMES.has(namespace)
|
|
457
|
-
? `Connector "${connector.id}" sanitizes to reserved execute_code namespace "${namespace}". Rename or exclude the connector before using execute_code.`
|
|
458
|
-
: owner
|
|
459
|
-
? `Connector ids "${owner}" and "${connector.id}" both sanitize to execute_code namespace "${namespace}". Rename or exclude one connector before using execute_code.`
|
|
460
|
-
: undefined;
|
|
461
|
-
if (problem) {
|
|
462
|
-
logger.warn(`[connecta] execute_code: ${problem}`);
|
|
463
|
-
throw new Error(problem);
|
|
464
|
-
}
|
|
465
|
-
namespaceOwners.set(namespace, connector.id);
|
|
466
|
-
namespaces.push({ id: connector.id, namespace });
|
|
467
|
-
}
|
|
468
241
|
const invocationContext = () => ({
|
|
469
242
|
source: "execute_code",
|
|
470
243
|
timeoutMs: hostCallTimeoutMs,
|
|
@@ -504,21 +277,14 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
504
277
|
throw err;
|
|
505
278
|
}
|
|
506
279
|
};
|
|
507
|
-
const
|
|
508
|
-
const outcome = await invoke();
|
|
509
|
-
limits.diagnostics?.recordCall(
|
|
280
|
+
const callAddress = async (address, args) => {
|
|
281
|
+
const outcome = await invocation.invoke(String(address), args ?? {}, invocationContext());
|
|
282
|
+
limits.diagnostics?.recordCall(outcome);
|
|
510
283
|
if (!outcome.ok)
|
|
511
284
|
throw new InvocationFailure(outcome.error);
|
|
512
285
|
return outcome.value;
|
|
513
286
|
};
|
|
514
|
-
const callAddress = async (address, args, diagnosticOperation = "call") => {
|
|
515
|
-
return called(diagnosticOperation, () => invocation.invoke(String(address), args ?? {}, invocationContext()));
|
|
516
|
-
};
|
|
517
|
-
const callNamespace = async (connectorId, toolAlias, args) => {
|
|
518
|
-
return called("call", () => invocation.invokeToolAlias(String(connectorId), String(toolAlias), sanitizeIdentifier, args ?? {}, invocationContext()));
|
|
519
|
-
};
|
|
520
287
|
const fns = {
|
|
521
|
-
__callNamespace: callNamespace,
|
|
522
288
|
call: (address, args) => callAddress(address, args),
|
|
523
289
|
// Emission is a provider function, never an ExecuteResult field —
|
|
524
290
|
// that is what keeps the Executor contract untouched and parity
|
|
@@ -530,55 +296,6 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
530
296
|
}
|
|
531
297
|
limits.emitCollector.accept(block);
|
|
532
298
|
},
|
|
533
|
-
// The rendered-output channel rides the same bridge for the same
|
|
534
|
-
// reason (U7): one more provider fn, no change to ExecuteResult or
|
|
535
|
-
// the Executor contract. Delivery is the handler's job, not the
|
|
536
|
-
// guest's — nothing here becomes addressable.
|
|
537
|
-
ui: async (...values) => {
|
|
538
|
-
if (!limits.emitCollector) {
|
|
539
|
-
throw guestFailure("unavailable", "connecta.ui is unavailable: no emission collector was configured for this execution", true);
|
|
540
|
-
}
|
|
541
|
-
limits.emitCollector.acceptUi(...values);
|
|
542
|
-
},
|
|
543
|
-
batch: async (calls) => {
|
|
544
|
-
const started = Date.now();
|
|
545
|
-
const callCount = Array.isArray(calls) ? calls.length : 0;
|
|
546
|
-
try {
|
|
547
|
-
if (!Array.isArray(calls)) {
|
|
548
|
-
throw guestFailure("invalid_args", "calls must be an array");
|
|
549
|
-
}
|
|
550
|
-
if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
|
|
551
|
-
throw guestFailure("invalid_args", `connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`);
|
|
552
|
-
}
|
|
553
|
-
const result = await Promise.all(calls.map(async (call) => {
|
|
554
|
-
const item = call;
|
|
555
|
-
try {
|
|
556
|
-
return {
|
|
557
|
-
address: String(item.address),
|
|
558
|
-
ok: true,
|
|
559
|
-
data: await callAddress(item.address, item.args, "batch"),
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
catch (err) {
|
|
563
|
-
const details = err instanceof InvocationFailure
|
|
564
|
-
? err.details
|
|
565
|
-
: classifyCallError(err, "batch_call_failed");
|
|
566
|
-
return {
|
|
567
|
-
address: String(item.address),
|
|
568
|
-
ok: false,
|
|
569
|
-
error: details.message,
|
|
570
|
-
errorDetails: details,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
}));
|
|
574
|
-
limits.diagnostics?.recordBatch(Date.now() - started, true, callCount, result);
|
|
575
|
-
return result;
|
|
576
|
-
}
|
|
577
|
-
catch (err) {
|
|
578
|
-
limits.diagnostics?.recordBatch(Date.now() - started, false, callCount);
|
|
579
|
-
throw err;
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
299
|
search: async (raw) => timedCatalog("search", () => typedDiscovery(async () => {
|
|
583
300
|
const args = (raw ?? {});
|
|
584
301
|
const result = flatSearchResult(await catalog.search({
|
|
@@ -614,7 +331,7 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
614
331
|
return [
|
|
615
332
|
{
|
|
616
333
|
name: "connecta",
|
|
617
|
-
prelude:
|
|
334
|
+
prelude: guestErrorPrelude(failureSecret),
|
|
618
335
|
fns: transportedFns,
|
|
619
336
|
},
|
|
620
337
|
];
|
|
@@ -790,19 +507,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
790
507
|
const response = jsonResult({
|
|
791
508
|
result,
|
|
792
509
|
...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
|
|
793
|
-
// U3: the model learns a view rendered without seeing its bytes. Spread
|
|
794
|
-
// conditionally so a program that never called connecta.ui produces
|
|
795
|
-
// today's byte-for-byte response.
|
|
796
|
-
...(emitted.ui ? { ui: true } : {}),
|
|
797
510
|
...(logs ? { logs } : {}),
|
|
798
511
|
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
799
512
|
});
|
|
800
|
-
if (emitted.ui) {
|
|
801
|
-
// _meta is where the Apps spec's best practices put data "not intended
|
|
802
|
-
// for model context", and how shipped hosts behave. The shell reads
|
|
803
|
-
// exactly this key out of the tool result the host delivers to it.
|
|
804
|
-
response._meta = { [PROGRAM_UI_META_KEY]: emitted.ui };
|
|
805
|
-
}
|
|
806
513
|
if (emitted.blocks.length > 0) {
|
|
807
514
|
// Emitted image/audio blocks are valid MCP content that ToolResult's
|
|
808
515
|
// text-only typing does not model — the same acknowledged gap
|
|
@@ -829,42 +536,35 @@ function failureResponse(message, options) {
|
|
|
829
536
|
return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${emitted ? discardedEmitsText(emitted) : ""}`);
|
|
830
537
|
}
|
|
831
538
|
/**
|
|
832
|
-
*
|
|
833
|
-
* discard is visible — and one failure can discard both.
|
|
539
|
+
* A failed program delivers no blocks and reports how many were discarded.
|
|
834
540
|
*/
|
|
835
541
|
function discardedEmits(emitted) {
|
|
836
|
-
return
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
: {}),
|
|
840
|
-
...(emitted.ui ? { uiDiscarded: true } : {}),
|
|
841
|
-
};
|
|
542
|
+
return emitted.blocks.length > 0
|
|
543
|
+
? { emittedDiscarded: emitted.blocks.length }
|
|
544
|
+
: {};
|
|
842
545
|
}
|
|
843
546
|
/** The same visibility for the plain-text error paths. */
|
|
844
547
|
function discardedEmitsText(emitted) {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
: []),
|
|
849
|
-
...(emitted.ui ? ["uiDiscarded: true"] : []),
|
|
850
|
-
];
|
|
851
|
-
return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
|
|
548
|
+
return emitted.blocks.length > 0
|
|
549
|
+
? `\n\nemittedDiscarded: ${emitted.blocks.length}`
|
|
550
|
+
: "";
|
|
852
551
|
}
|
|
853
552
|
function connectorInventory(connectors) {
|
|
854
553
|
const prefix = "Connectors: ";
|
|
855
554
|
if (connectors.length === 0)
|
|
856
555
|
return `${prefix}none.`;
|
|
857
556
|
const entries = connectors.map((connector) => {
|
|
858
|
-
const
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
557
|
+
const address = connector.id;
|
|
558
|
+
const title = connector.title?.replace(/\s+/g, " ").trim();
|
|
559
|
+
const label = title && title !== connector.id
|
|
560
|
+
? `${address}: ${boundedEchoText(title, 45)}`
|
|
561
|
+
: address;
|
|
862
562
|
if (!connectorGuide(connector))
|
|
863
|
-
return
|
|
563
|
+
return label;
|
|
864
564
|
const requirement = connectorGuideRequired(connector)
|
|
865
565
|
? "required guide"
|
|
866
566
|
: "guide";
|
|
867
|
-
return `${
|
|
567
|
+
return `${label} (${requirement} ${connectorSkillName(connector.id)})`;
|
|
868
568
|
});
|
|
869
569
|
const shown = [];
|
|
870
570
|
for (let index = 0; index < entries.length; index++) {
|
|
@@ -885,18 +585,19 @@ function connectorInventory(connectors) {
|
|
|
885
585
|
return `${prefix}${shown.join(", ")}.`;
|
|
886
586
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
887
587
|
}
|
|
888
|
-
const executeDescription = (emitBudgets, connectorGuides, connectors) => `
|
|
588
|
+
const executeDescription = (emitBudgets, connectorGuides, connectors) => `Use the configured services below to answer the task. A known address uses call_tool. Unknown-address and wider read-only work uses one execute_code program for discovery, calls, and reduction. Do not return catalog matches alone. Only readOnlyHint: true tools are available. Limits: ${EXECUTE_MAX_HOST_CALLS} host calls, ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}s/host call.
|
|
889
589
|
|
|
890
590
|
${connectorInventory(connectors)}
|
|
891
591
|
|
|
892
|
-
|
|
893
|
-
-
|
|
894
|
-
- connecta.
|
|
895
|
-
- connecta.
|
|
896
|
-
-
|
|
897
|
-
-
|
|
592
|
+
Read relevant guides using top-level skills, not sandbox code. Write a plain-JavaScript async arrow:
|
|
593
|
+
- connecta.search({ connector, query, safety: "readOnly", includeSchemas: "json" }) returns { tools }. Search each operation separately; choose by connectorTitle and schemas. Use schema.required and .properties to build args, never guessed fields. Compact schemas are text.
|
|
594
|
+
- connecta.describe({ address }) returns { tools } for unclear schemas.
|
|
595
|
+
- connecta.call(address, args) returns the provider value directly.
|
|
596
|
+
- Use Promise.all for independent calls, or Promise.allSettled to retain failures. Check status before reading value; rejected calls and missing fields are unknown, never false or zero.
|
|
597
|
+
- connecta.emit(block): { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }; success-only, ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes.
|
|
598
|
+
- console.log(...) is captured. Return data for the client to render.
|
|
898
599
|
|
|
899
|
-
No portable ambient capabilities. Return JSON
|
|
600
|
+
No portable ambient capabilities. Return reduced JSON. If a provider result has an unfamiliar shape, return a small sample and continue in another call; never guess fields or use the whole text as an id. Top-level skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}; skills({ name: "investigate" }): task planning.`;
|
|
900
601
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
901
602
|
export function registerExecuteTool(server, registry, ctx) {
|
|
902
603
|
// Resolved once so the description and the collector cannot disagree about
|
|
@@ -932,14 +633,6 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
932
633
|
destructiveHint: false,
|
|
933
634
|
openWorldHint: true,
|
|
934
635
|
},
|
|
935
|
-
// U5 and U10 are specified in documentation/code-mode.md; explicit model
|
|
936
|
-
// visibility prevents hosts from offering execute_code to the view.
|
|
937
|
-
_meta: {
|
|
938
|
-
ui: {
|
|
939
|
-
resourceUri: PROGRAM_UI_RESOURCE_URI,
|
|
940
|
-
visibility: ["model"],
|
|
941
|
-
},
|
|
942
|
-
},
|
|
943
636
|
}, async (args, extra) => {
|
|
944
637
|
const controller = new AbortController();
|
|
945
638
|
const signals = [extra.mcpReq.signal, ctx.requestSignal].filter((signal) => signal !== undefined);
|