@zackbart/connecta 0.16.1 → 0.18.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/CHANGELOG.md +180 -0
- package/README.md +4 -0
- package/dist/catalog-service.d.ts +10 -0
- package/dist/catalog-service.js +77 -5
- package/dist/catalog.js +114 -12
- package/dist/errors.d.ts +4 -6
- package/dist/execute.d.ts +7 -0
- package/dist/execute.js +262 -168
- package/dist/invocation.js +3 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.js +55 -23
- package/dist/operator-ui/generated.d.ts +1 -1
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -1
- package/dist/providers/mixpanel.d.ts +3 -5
- package/dist/providers/mixpanel.js +73 -5
- package/dist/providers/stripe.d.ts +25 -24
- package/dist/providers/stripe.js +64 -35
- package/dist/registry.d.ts +32 -9
- package/dist/registry.js +217 -33
- package/dist/routes/mcp.js +6 -0
- package/dist/routes/ui.js +1 -1
- package/dist/skills.d.ts +5 -1
- package/dist/skills.js +206 -30
- package/dist/types.d.ts +14 -2
- package/dist/ui.js +4 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +8 -5
- package/documentation/code-mode.md +68 -68
- package/documentation/connector-guides.md +29 -27
- package/documentation/connectors.md +13 -1
- package/documentation/meta-tools.md +53 -19
- package/documentation/mixpanel.md +20 -0
- package/documentation/notion.md +17 -0
- package/documentation/operations.md +24 -21
- package/documentation/operator-ui.md +12 -2
- package/documentation/provider-audit.md +15 -7
- package/documentation/provider-conventions.md +26 -13
- package/documentation/stripe.md +66 -59
- package/documentation/upgrading.md +46 -4
- package/ethos.md +7 -7
- package/examples/worker/README.md +4 -3
- package/package.json +2 -2
- package/templates/node/README.md +7 -0
- package/templates/node/package.json +5 -2
package/dist/execute.js
CHANGED
|
@@ -4,7 +4,7 @@ import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchR
|
|
|
4
4
|
import { errorResult, jsonResult } from "./meta-tools.js";
|
|
5
5
|
import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
|
|
6
6
|
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
|
|
7
|
-
import { classifyCallError } from "./errors.js";
|
|
7
|
+
import { boundedEchoText, classifyCallError } from "./errors.js";
|
|
8
8
|
import { InvocationFailure, InvocationService, } from "./invocation.js";
|
|
9
9
|
import { hasConnectorGuides } from "./skills.js";
|
|
10
10
|
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
@@ -12,6 +12,8 @@ import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
|
12
12
|
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
13
13
|
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
14
14
|
const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
|
|
15
|
+
/** Complete entries plus an exact omission count, all inside this byte cap. */
|
|
16
|
+
export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
|
|
15
17
|
/**
|
|
16
18
|
* Default budgets for `connecta.emit`. The byte budget is a transport bound,
|
|
17
19
|
* not a context bound — emitted image/audio blocks reach the model as media,
|
|
@@ -125,7 +127,7 @@ const EMIT_SHAPE_HINT = '{ type: "text", text } or { type: "image" | "audio", da
|
|
|
125
127
|
*/
|
|
126
128
|
function requireEmittedBlock(raw) {
|
|
127
129
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
128
|
-
throw
|
|
130
|
+
throw guestFailure("invalid_args", `connecta.emit accepts exactly one content block: ${EMIT_SHAPE_HINT}`);
|
|
129
131
|
}
|
|
130
132
|
const block = raw;
|
|
131
133
|
const fields = block.type === "text"
|
|
@@ -134,16 +136,16 @@ function requireEmittedBlock(raw) {
|
|
|
134
136
|
? ["type", "data", "mimeType"]
|
|
135
137
|
: undefined;
|
|
136
138
|
if (!fields) {
|
|
137
|
-
throw
|
|
139
|
+
throw guestFailure("invalid_args", `connecta.emit supports content types "text", "image", and "audio"; got ${JSON.stringify(block.type)}`);
|
|
138
140
|
}
|
|
139
141
|
for (const field of fields) {
|
|
140
142
|
if (typeof block[field] !== "string") {
|
|
141
|
-
throw
|
|
143
|
+
throw guestFailure("invalid_args", `connecta.emit block field "${field}" must be a string: ${EMIT_SHAPE_HINT}`);
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
const extra = Object.keys(block).filter((key) => !fields.includes(key));
|
|
145
147
|
if (extra.length > 0) {
|
|
146
|
-
throw
|
|
148
|
+
throw guestFailure("invalid_args", `connecta.emit block carries unsupported field(s) ${extra.map((key) => JSON.stringify(key)).join(", ")}; a "${String(block.type)}" block is exactly { ${fields.join(", ")} }`);
|
|
147
149
|
}
|
|
148
150
|
return raw;
|
|
149
151
|
}
|
|
@@ -172,20 +174,20 @@ function describeUiArgument(raw) {
|
|
|
172
174
|
*/
|
|
173
175
|
function requireUiHtml(raw) {
|
|
174
176
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
175
|
-
throw
|
|
177
|
+
throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${describeUiArgument(raw)}`);
|
|
176
178
|
}
|
|
177
179
|
return raw;
|
|
178
180
|
}
|
|
179
181
|
function requireRecord(raw, label) {
|
|
180
182
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
181
|
-
throw
|
|
183
|
+
throw guestFailure("invalid_args", `${label} must be an object`);
|
|
182
184
|
}
|
|
183
185
|
return raw;
|
|
184
186
|
}
|
|
185
187
|
function requireExactKeys(value, allowed, label) {
|
|
186
188
|
const extras = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
187
189
|
if (extras.length > 0) {
|
|
188
|
-
throw
|
|
190
|
+
throw guestFailure("invalid_args", `${label} carries unsupported field(s) ${extras.map((key) => JSON.stringify(key)).join(", ")}`);
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
function requireUiReadKey(raw, label) {
|
|
@@ -193,7 +195,7 @@ function requireUiReadKey(raw, label) {
|
|
|
193
195
|
raw.length === 0 ||
|
|
194
196
|
raw.length > 128 ||
|
|
195
197
|
FORBIDDEN_UI_KEY.has(raw)) {
|
|
196
|
-
throw
|
|
198
|
+
throw guestFailure("invalid_args", `${label} must be a non-empty string of at most 128 characters and cannot be __proto__, constructor, or prototype`);
|
|
197
199
|
}
|
|
198
200
|
return raw;
|
|
199
201
|
}
|
|
@@ -201,32 +203,32 @@ function requireUiReads(raw) {
|
|
|
201
203
|
const record = requireRecord(raw, "connecta.ui options.reads");
|
|
202
204
|
const names = Object.keys(record);
|
|
203
205
|
if (names.length === 0 || names.length > MAX_UI_READ_BINDINGS) {
|
|
204
|
-
throw
|
|
206
|
+
throw guestFailure("invalid_args", `connecta.ui options.reads must contain from 1 through ${MAX_UI_READ_BINDINGS} named bindings`);
|
|
205
207
|
}
|
|
206
208
|
const reads = Object.create(null);
|
|
207
209
|
for (const name of names) {
|
|
208
210
|
if (!UI_READ_NAME.test(name) || FORBIDDEN_UI_KEY.has(name)) {
|
|
209
|
-
throw
|
|
211
|
+
throw guestFailure("invalid_args", `connecta.ui read binding name ${JSON.stringify(name)} must match ${UI_READ_NAME}`);
|
|
210
212
|
}
|
|
211
213
|
const value = requireRecord(record[name], `connecta.ui read binding ${JSON.stringify(name)}`);
|
|
212
214
|
requireExactKeys(value, ["address", "fixedArgs", "viewArgs"], `connecta.ui read binding ${JSON.stringify(name)}`);
|
|
213
215
|
if (typeof value.address !== "string" || value.address.length === 0) {
|
|
214
|
-
throw
|
|
216
|
+
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} address must be a non-empty string`);
|
|
215
217
|
}
|
|
216
218
|
const fixedArgs = value.fixedArgs === undefined
|
|
217
219
|
? {}
|
|
218
220
|
: requireRecord(value.fixedArgs, `connecta.ui read binding ${JSON.stringify(name)} fixedArgs`);
|
|
219
221
|
const rawViewArgs = value.viewArgs ?? [];
|
|
220
222
|
if (!Array.isArray(rawViewArgs) || rawViewArgs.length > MAX_UI_VIEW_ARGS) {
|
|
221
|
-
throw
|
|
223
|
+
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} viewArgs must be an array of at most ${MAX_UI_VIEW_ARGS} strings`);
|
|
222
224
|
}
|
|
223
225
|
const viewArgs = rawViewArgs.map((key) => requireUiReadKey(key, `connecta.ui read binding ${JSON.stringify(name)} viewArgs entry`));
|
|
224
226
|
if (new Set(viewArgs).size !== viewArgs.length) {
|
|
225
|
-
throw
|
|
227
|
+
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} viewArgs must not repeat a key`);
|
|
226
228
|
}
|
|
227
229
|
for (const key of viewArgs) {
|
|
228
230
|
if (Object.prototype.hasOwnProperty.call(fixedArgs, key)) {
|
|
229
|
-
throw
|
|
231
|
+
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} view argument ${JSON.stringify(key)} cannot override a fixed argument`);
|
|
230
232
|
}
|
|
231
233
|
}
|
|
232
234
|
reads[name] = {
|
|
@@ -239,7 +241,7 @@ function requireUiReads(raw) {
|
|
|
239
241
|
}
|
|
240
242
|
function requireUiPayload(values) {
|
|
241
243
|
if (values.length !== 1 && values.length !== 2) {
|
|
242
|
-
throw
|
|
244
|
+
throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${values.length} arguments`);
|
|
243
245
|
}
|
|
244
246
|
const html = requireUiHtml(values[0]);
|
|
245
247
|
if (values.length === 1)
|
|
@@ -247,7 +249,7 @@ function requireUiPayload(values) {
|
|
|
247
249
|
const options = requireRecord(values[1], "connecta.ui options");
|
|
248
250
|
requireExactKeys(options, ["reads"], "connecta.ui options");
|
|
249
251
|
if (!Object.prototype.hasOwnProperty.call(options, "reads")) {
|
|
250
|
-
throw
|
|
252
|
+
throw guestFailure("invalid_args", "connecta.ui options must contain reads");
|
|
251
253
|
}
|
|
252
254
|
return { html, reads: requireUiReads(options.reads) };
|
|
253
255
|
}
|
|
@@ -280,11 +282,11 @@ export class EmitCollector {
|
|
|
280
282
|
accept(raw) {
|
|
281
283
|
const block = requireEmittedBlock(raw);
|
|
282
284
|
if (this.blocks.length >= this.maxBlocks) {
|
|
283
|
-
throw
|
|
285
|
+
throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 remaining`);
|
|
284
286
|
}
|
|
285
287
|
const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
|
|
286
288
|
if (this.bytes + size > this.maxBytes) {
|
|
287
|
-
throw
|
|
289
|
+
throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
|
|
288
290
|
}
|
|
289
291
|
this.blocks.push(block);
|
|
290
292
|
this.bytes += size;
|
|
@@ -305,24 +307,24 @@ export class EmitCollector {
|
|
|
305
307
|
*/
|
|
306
308
|
acceptUi(...values) {
|
|
307
309
|
if (this.ui) {
|
|
308
|
-
throw
|
|
310
|
+
throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
|
|
309
311
|
}
|
|
310
312
|
this.acceptUiPayload(requireUiPayload(values));
|
|
311
313
|
}
|
|
312
314
|
acceptUiPayload(payload) {
|
|
313
315
|
if (this.ui) {
|
|
314
|
-
throw
|
|
316
|
+
throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
|
|
315
317
|
}
|
|
316
318
|
let serialized;
|
|
317
319
|
try {
|
|
318
320
|
serialized = JSON.stringify(payload);
|
|
319
321
|
}
|
|
320
322
|
catch {
|
|
321
|
-
throw
|
|
323
|
+
throw guestFailure("invalid_args", "connecta.ui payload must be JSON-serializable");
|
|
322
324
|
}
|
|
323
325
|
const size = diagnosticsEncoder.encode(serialized).byteLength;
|
|
324
326
|
if (this.bytes + size > this.maxBytes) {
|
|
325
|
-
throw
|
|
327
|
+
throw guestFailure("budget_exceeded", `connecta.ui byte budget exceeded: payload is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
|
|
326
328
|
}
|
|
327
329
|
this.ui = payload;
|
|
328
330
|
this.bytes += size;
|
|
@@ -430,18 +432,75 @@ const SANDBOX_RESERVED_NAMES = new Set([
|
|
|
430
432
|
function msg(err) {
|
|
431
433
|
return err instanceof Error ? err.message : String(err);
|
|
432
434
|
}
|
|
433
|
-
function
|
|
435
|
+
function guestFailure(code, message, retryable = false) {
|
|
436
|
+
return new InvocationFailure({ code, message, retryable });
|
|
437
|
+
}
|
|
438
|
+
const GUEST_FAILURE_FRAME = "\u001econnecta-error:";
|
|
439
|
+
const guestFailureFrames = new WeakMap();
|
|
440
|
+
/** Per-execution secret: guest prose cannot collide with this host frame. */
|
|
441
|
+
function guestFailureSecret() {
|
|
442
|
+
const words = new Uint32Array(4);
|
|
443
|
+
crypto.getRandomValues(words);
|
|
444
|
+
return Array.from(words, (word) => word.toString(16).padStart(8, "0")).join("");
|
|
445
|
+
}
|
|
446
|
+
function framedGuestFailure(secret, failure) {
|
|
447
|
+
const framed = new InvocationFailure(failure.details);
|
|
448
|
+
framed.message =
|
|
449
|
+
`${GUEST_FAILURE_FRAME}${secret}:${JSON.stringify(failure.details)}`;
|
|
450
|
+
guestFailureFrames.set(failure, framed.message);
|
|
451
|
+
return framed;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Rebuilds host failures as guest Error instances, then installs connector
|
|
455
|
+
* shortcuts. The random frame is hidden in this closure and Error is locked:
|
|
456
|
+
* connector prose and model code can neither collide with nor forge its frame.
|
|
457
|
+
*/
|
|
458
|
+
function lazyNamespacePrelude(connectors, failureSecret) {
|
|
434
459
|
const declarations = connectors
|
|
435
460
|
.map(({ id, namespace }) => `globalThis[${JSON.stringify(namespace)}] = __makeConnectaNamespace(${JSON.stringify(id)});`)
|
|
436
461
|
.join("\n");
|
|
437
|
-
return `(() => {
|
|
462
|
+
return `((failurePrefix) => {
|
|
463
|
+
const NativeError = globalThis.Error;
|
|
464
|
+
const startsWith = Function.prototype.call.bind(String.prototype.startsWith);
|
|
465
|
+
const slice = Function.prototype.call.bind(String.prototype.slice);
|
|
466
|
+
const parse = JSON.parse;
|
|
467
|
+
const freeze = Object.freeze;
|
|
468
|
+
const defineProperties = Object.defineProperties;
|
|
469
|
+
const construct = Reflect.construct;
|
|
470
|
+
function ConnectaError(message, options) {
|
|
471
|
+
let details;
|
|
472
|
+
if (typeof message === "string" && startsWith(message, failurePrefix)) {
|
|
473
|
+
try { details = parse(slice(message, failurePrefix.length)); } catch {}
|
|
474
|
+
}
|
|
475
|
+
const error = construct(
|
|
476
|
+
NativeError,
|
|
477
|
+
options === undefined ? [details ? details.message : message] : [details ? details.message : message, options],
|
|
478
|
+
new.target || NativeError
|
|
479
|
+
);
|
|
480
|
+
if (details) {
|
|
481
|
+
freeze(details);
|
|
482
|
+
defineProperties(error, {
|
|
483
|
+
code: { value: details.code, enumerable: true },
|
|
484
|
+
retryable: { value: details.retryable, enumerable: true },
|
|
485
|
+
details: { value: details, enumerable: true }
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
return error;
|
|
489
|
+
}
|
|
490
|
+
ConnectaError.prototype = NativeError.prototype;
|
|
491
|
+
Object.setPrototypeOf(ConnectaError, NativeError);
|
|
492
|
+
Object.defineProperty(globalThis, "Error", {
|
|
493
|
+
value: ConnectaError,
|
|
494
|
+
writable: false,
|
|
495
|
+
configurable: false
|
|
496
|
+
});
|
|
438
497
|
const __makeConnectaNamespace = (connectorId) => Object.freeze(new Proxy(Object.create(null), {
|
|
439
498
|
get: (_target, toolName) => typeof toolName === "string"
|
|
440
499
|
? (args) => connecta.__callNamespace(connectorId, toolName, args)
|
|
441
500
|
: undefined
|
|
442
501
|
}));
|
|
443
502
|
${declarations}
|
|
444
|
-
})();`;
|
|
503
|
+
})(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
|
|
445
504
|
}
|
|
446
505
|
/**
|
|
447
506
|
* Expose one fixed host provider plus trusted sandbox setup that creates a
|
|
@@ -462,10 +521,12 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
462
521
|
...(limits.probeTimeoutMs !== undefined
|
|
463
522
|
? { probeTimeoutMs: limits.probeTimeoutMs }
|
|
464
523
|
: {}),
|
|
524
|
+
...(limits.defer !== undefined ? { defer: limits.defer } : {}),
|
|
465
525
|
});
|
|
466
526
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
467
527
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
468
528
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
529
|
+
const failureSecret = guestFailureSecret();
|
|
469
530
|
let hostCalls = 0;
|
|
470
531
|
const connectors = registry.listConnectors();
|
|
471
532
|
const namespaces = [];
|
|
@@ -493,16 +554,13 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
493
554
|
beforeDispatch: () => {
|
|
494
555
|
hostCalls++;
|
|
495
556
|
if (hostCalls > maxHostCalls) {
|
|
496
|
-
throw
|
|
557
|
+
throw guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`);
|
|
497
558
|
}
|
|
498
559
|
},
|
|
499
560
|
});
|
|
500
561
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* request-local channel so an unhandled `invalid_args`/`result_too_large`
|
|
504
|
-
* reaches the model with its code instead of as prose. The guest still sees
|
|
505
|
-
* only the message — that is the bridge's limit, not a policy.
|
|
562
|
+
* Discovery policy failures use the same thrown vocabulary as calls and
|
|
563
|
+
* utilities. The transport below reconstructs their code inside the guest.
|
|
506
564
|
*/
|
|
507
565
|
const typedDiscovery = async (operation) => {
|
|
508
566
|
try {
|
|
@@ -510,11 +568,7 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
510
568
|
}
|
|
511
569
|
catch (err) {
|
|
512
570
|
if (err instanceof DiscoveryPolicyError) {
|
|
513
|
-
|
|
514
|
-
code: err.code,
|
|
515
|
-
message: err.message,
|
|
516
|
-
retryable: false,
|
|
517
|
-
}));
|
|
571
|
+
throw guestFailure(err.code, err.message);
|
|
518
572
|
}
|
|
519
573
|
throw err;
|
|
520
574
|
}
|
|
@@ -524,7 +578,6 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
524
578
|
limits.diagnostics?.recordCall(diagnosticOperation, outcome);
|
|
525
579
|
if (!outcome.ok) {
|
|
526
580
|
const failure = new InvocationFailure(outcome.error);
|
|
527
|
-
limits.onInvocationFailure?.(failure);
|
|
528
581
|
throw failure;
|
|
529
582
|
}
|
|
530
583
|
return outcome.value;
|
|
@@ -534,7 +587,6 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
534
587
|
limits.diagnostics?.recordCall("call", outcome);
|
|
535
588
|
if (!outcome.ok) {
|
|
536
589
|
const failure = new InvocationFailure(outcome.error);
|
|
537
|
-
limits.onInvocationFailure?.(failure);
|
|
538
590
|
throw failure;
|
|
539
591
|
}
|
|
540
592
|
return outcome.value;
|
|
@@ -554,10 +606,13 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
554
606
|
for (const [name, binding] of Object.entries(payload.reads)) {
|
|
555
607
|
const resolution = await catalog.resolveTool(binding.address, limits.signal !== undefined ? { signal: limits.signal } : {});
|
|
556
608
|
if (!resolution.ok) {
|
|
557
|
-
throw new
|
|
609
|
+
throw new InvocationFailure({
|
|
610
|
+
...resolution.error,
|
|
611
|
+
message: `connecta.ui read binding ${JSON.stringify(name)} could not resolve ${JSON.stringify(binding.address)}: ${resolution.error.message}`,
|
|
612
|
+
});
|
|
558
613
|
}
|
|
559
614
|
if (!isExplicitlyReadOnly(resolution.resolved.definition)) {
|
|
560
|
-
throw
|
|
615
|
+
throw guestFailure("destructive_tool_requires_approval", `connecta.ui read binding ${JSON.stringify(name)} refuses ${JSON.stringify(binding.address)}: the tool is not explicitly read-only`);
|
|
561
616
|
}
|
|
562
617
|
reads[name] = {
|
|
563
618
|
...binding,
|
|
@@ -566,118 +621,128 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
566
621
|
}
|
|
567
622
|
return { html: payload.html, reads };
|
|
568
623
|
};
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
const result = await Promise.all(calls.map(async (call) => {
|
|
607
|
-
const item = call;
|
|
608
|
-
try {
|
|
609
|
-
return {
|
|
610
|
-
address: String(item.address),
|
|
611
|
-
ok: true,
|
|
612
|
-
data: await callAddress(item.address, item.args, "batch"),
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
catch (err) {
|
|
616
|
-
// The failure shape connecta.batch reports: the message a
|
|
617
|
-
// program can log, plus the typed details it must classify by. A
|
|
618
|
-
// thrown host error crosses the sandbox bridge as a bare
|
|
619
|
-
// message string in every executor, so this is the one place a
|
|
620
|
-
// program can tell a policy refusal from a transient failure.
|
|
621
|
-
const details = err instanceof InvocationFailure
|
|
622
|
-
? err.details
|
|
623
|
-
: classifyCallError(err, "batch_call_failed");
|
|
624
|
-
return {
|
|
625
|
-
address: String(item.address),
|
|
626
|
-
ok: false,
|
|
627
|
-
error: details.message,
|
|
628
|
-
errorDetails: details,
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
}));
|
|
632
|
-
limits.diagnostics?.recordBatch(Date.now() - started, true, callCount, result);
|
|
633
|
-
return result;
|
|
634
|
-
}
|
|
635
|
-
catch (err) {
|
|
636
|
-
limits.diagnostics?.recordBatch(Date.now() - started, false, callCount);
|
|
637
|
-
throw err;
|
|
638
|
-
}
|
|
639
|
-
},
|
|
640
|
-
search: async (raw) => {
|
|
641
|
-
const started = Date.now();
|
|
642
|
-
try {
|
|
643
|
-
const result = await typedDiscovery(async () => {
|
|
644
|
-
const args = (raw ?? {});
|
|
645
|
-
const result = flatSearchResult(await catalog.search({
|
|
646
|
-
...args,
|
|
647
|
-
// Key metadata rides along with schemas by default, since that
|
|
648
|
-
// is the whole point of it in code mode. It stays opt-out
|
|
649
|
-
// because it counts against the same discovery-byte ceiling.
|
|
650
|
-
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
651
|
-
}));
|
|
652
|
-
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
653
|
-
return result;
|
|
654
|
-
});
|
|
655
|
-
limits.diagnostics?.recordCatalog("search", Date.now() - started, true, result);
|
|
656
|
-
return result;
|
|
657
|
-
}
|
|
658
|
-
catch (err) {
|
|
659
|
-
limits.diagnostics?.recordCatalog("search", Date.now() - started, false);
|
|
660
|
-
throw err;
|
|
661
|
-
}
|
|
662
|
-
},
|
|
663
|
-
describe: async (raw) => {
|
|
664
|
-
const started = Date.now();
|
|
624
|
+
const fns = {
|
|
625
|
+
__callNamespace: callNamespace,
|
|
626
|
+
call: (address, args) => callAddress(address, args),
|
|
627
|
+
// Emission is a provider function, never an ExecuteResult field —
|
|
628
|
+
// that is what keeps the Executor contract untouched and parity
|
|
629
|
+
// structural (M8). It spends no host-call budget (M7); its own
|
|
630
|
+
// budgets live in the collector.
|
|
631
|
+
emit: async (block) => {
|
|
632
|
+
if (!limits.emitCollector) {
|
|
633
|
+
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
634
|
+
}
|
|
635
|
+
limits.emitCollector.accept(block);
|
|
636
|
+
},
|
|
637
|
+
// The rendered-output channel rides the same bridge for the same
|
|
638
|
+
// reason (U7): one more provider fn, no change to ExecuteResult or
|
|
639
|
+
// the Executor contract. Delivery is the handler's job, not the
|
|
640
|
+
// guest's — nothing here becomes addressable.
|
|
641
|
+
ui: async (...values) => {
|
|
642
|
+
if (!limits.emitCollector) {
|
|
643
|
+
throw guestFailure("unavailable", "connecta.ui is unavailable: no emission collector was configured for this execution", true);
|
|
644
|
+
}
|
|
645
|
+
const payload = await validateUiReads(requireUiPayload(values));
|
|
646
|
+
limits.emitCollector.acceptUiPayload(payload);
|
|
647
|
+
},
|
|
648
|
+
batch: async (calls) => {
|
|
649
|
+
const started = Date.now();
|
|
650
|
+
const callCount = Array.isArray(calls) ? calls.length : 0;
|
|
651
|
+
try {
|
|
652
|
+
if (!Array.isArray(calls)) {
|
|
653
|
+
throw guestFailure("invalid_args", "calls must be an array");
|
|
654
|
+
}
|
|
655
|
+
if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
|
|
656
|
+
throw guestFailure("invalid_args", `connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`);
|
|
657
|
+
}
|
|
658
|
+
const result = await Promise.all(calls.map(async (call) => {
|
|
659
|
+
const item = call;
|
|
665
660
|
try {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
});
|
|
672
|
-
limits.diagnostics?.recordCatalog("describe", Date.now() - started, true, result);
|
|
673
|
-
return result;
|
|
661
|
+
return {
|
|
662
|
+
address: String(item.address),
|
|
663
|
+
ok: true,
|
|
664
|
+
data: await callAddress(item.address, item.args, "batch"),
|
|
665
|
+
};
|
|
674
666
|
}
|
|
675
667
|
catch (err) {
|
|
676
|
-
|
|
677
|
-
|
|
668
|
+
const details = err instanceof InvocationFailure
|
|
669
|
+
? err.details
|
|
670
|
+
: classifyCallError(err, "batch_call_failed");
|
|
671
|
+
return {
|
|
672
|
+
address: String(item.address),
|
|
673
|
+
ok: false,
|
|
674
|
+
error: details.message,
|
|
675
|
+
errorDetails: details,
|
|
676
|
+
};
|
|
678
677
|
}
|
|
679
|
-
}
|
|
680
|
-
|
|
678
|
+
}));
|
|
679
|
+
limits.diagnostics?.recordBatch(Date.now() - started, true, callCount, result);
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
limits.diagnostics?.recordBatch(Date.now() - started, false, callCount);
|
|
684
|
+
throw err;
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
search: async (raw) => {
|
|
688
|
+
const started = Date.now();
|
|
689
|
+
try {
|
|
690
|
+
const result = await typedDiscovery(async () => {
|
|
691
|
+
const args = (raw ?? {});
|
|
692
|
+
const result = flatSearchResult(await catalog.search({
|
|
693
|
+
...args,
|
|
694
|
+
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
695
|
+
}));
|
|
696
|
+
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
697
|
+
return result;
|
|
698
|
+
});
|
|
699
|
+
limits.diagnostics?.recordCatalog("search", Date.now() - started, true, result);
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
limits.diagnostics?.recordCatalog("search", Date.now() - started, false);
|
|
704
|
+
throw err;
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
describe: async (raw) => {
|
|
708
|
+
const started = Date.now();
|
|
709
|
+
try {
|
|
710
|
+
const result = await typedDiscovery(async () => {
|
|
711
|
+
const args = (raw ?? {});
|
|
712
|
+
const result = { tools: await catalog.describe(args) };
|
|
713
|
+
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
714
|
+
return result;
|
|
715
|
+
});
|
|
716
|
+
limits.diagnostics?.recordCatalog("describe", Date.now() - started, true, result);
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
limits.diagnostics?.recordCatalog("describe", Date.now() - started, false);
|
|
721
|
+
throw err;
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
const transportedFns = Object.fromEntries(Object.entries(fns).map(([name, fn]) => [
|
|
726
|
+
name,
|
|
727
|
+
async (...args) => {
|
|
728
|
+
try {
|
|
729
|
+
return await fn(...args);
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
if (err instanceof InvocationFailure) {
|
|
733
|
+
const framed = framedGuestFailure(failureSecret, err);
|
|
734
|
+
limits.onInvocationFailure?.(err);
|
|
735
|
+
throw framed;
|
|
736
|
+
}
|
|
737
|
+
throw err;
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
]));
|
|
741
|
+
return [
|
|
742
|
+
{
|
|
743
|
+
name: "connecta",
|
|
744
|
+
prelude: lazyNamespacePrelude(namespaces, failureSecret),
|
|
745
|
+
fns: transportedFns,
|
|
681
746
|
},
|
|
682
747
|
];
|
|
683
748
|
}
|
|
@@ -731,6 +796,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
731
796
|
...(config.probeTimeoutMs !== undefined
|
|
732
797
|
? { probeTimeoutMs: config.probeTimeoutMs }
|
|
733
798
|
: {}),
|
|
799
|
+
...(config.defer !== undefined ? { defer: config.defer } : {}),
|
|
734
800
|
});
|
|
735
801
|
}
|
|
736
802
|
finally {
|
|
@@ -816,8 +882,8 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
816
882
|
// rather than losing it to prose.
|
|
817
883
|
let invocationFailure;
|
|
818
884
|
for (const match of [
|
|
819
|
-
(candidate) =>
|
|
820
|
-
(candidate) => outcome.error?.includes(
|
|
885
|
+
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
|
|
886
|
+
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) => message !== undefined && outcome.error?.includes(message) === true),
|
|
821
887
|
]) {
|
|
822
888
|
for (let i = invocationFailures.length - 1; i >= 0; i--) {
|
|
823
889
|
const candidate = invocationFailures[i];
|
|
@@ -932,22 +998,47 @@ function discardedEmitsText(emitted) {
|
|
|
932
998
|
];
|
|
933
999
|
return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
|
|
934
1000
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1001
|
+
function connectorInventory(connectors) {
|
|
1002
|
+
const prefix = "Connectors: ";
|
|
1003
|
+
if (connectors.length === 0)
|
|
1004
|
+
return `${prefix}none.`;
|
|
1005
|
+
const entries = connectors.map((connector) => {
|
|
1006
|
+
const shortcut = sanitizeIdentifier(connector.id);
|
|
1007
|
+
return shortcut === connector.id
|
|
1008
|
+
? connector.id
|
|
1009
|
+
: `${connector.id} (shortcut ${shortcut})`;
|
|
1010
|
+
});
|
|
1011
|
+
const shown = [];
|
|
1012
|
+
for (let index = 0; index < entries.length; index++) {
|
|
1013
|
+
const entry = entries[index];
|
|
1014
|
+
if (entry === undefined)
|
|
1015
|
+
break;
|
|
1016
|
+
const candidate = [...shown, entry].join(", ");
|
|
1017
|
+
const omitted = entries.length - index - 1;
|
|
1018
|
+
const suffix = omitted > 0 ? `; +${omitted} more.` : ".";
|
|
1019
|
+
const serialized = prefix + candidate + suffix;
|
|
1020
|
+
if (boundedEchoText(serialized, CONNECTOR_INVENTORY_MAX_BYTES) !== serialized) {
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
shown.push(entry);
|
|
1024
|
+
}
|
|
1025
|
+
const omitted = entries.length - shown.length;
|
|
1026
|
+
if (omitted === 0)
|
|
1027
|
+
return `${prefix}${shown.join(", ")}.`;
|
|
1028
|
+
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
1029
|
+
}
|
|
1030
|
+
const executeDescription = (emitBudgets, connectorGuides, connectors) => `Choose the route before discovery. Exactly one unknown-address read uses top-level search_tools then call_tool. execute_code is the primary surface for everything wider: make exactly one execute_code call that searches, selects, calls, and reduces. A discovery-only program wastes its round trip: finish here, don't return catalog matches for a later call. Only readOnlyHint: true tools are available. Limits: ${EXECUTE_MAX_HOST_CALLS} host calls per run, ${EXECUTE_MAX_BATCH_CALLS} per batch, ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second host deadline.
|
|
945
1031
|
|
|
946
|
-
|
|
1032
|
+
${connectorInventory(connectors)}
|
|
947
1033
|
|
|
948
|
-
|
|
1034
|
+
Write one plain-JavaScript async arrow function. Use only:
|
|
1035
|
+
- <connectorId>.<toolName>(args) for a sanitized shortcut, or connecta.call(address, args) for a canonical address.
|
|
1036
|
+
- connecta.search(args), connecta.describe(args), and connecta.batch(calls) for discovery and independent read-only calls.
|
|
1037
|
+
- connecta.emit(block) — { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }. Success-only; ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes; invalid/over-budget throws.
|
|
1038
|
+
- connecta.ui(html, options?) for one success-only view; return the same initial summary the HTML renders.
|
|
1039
|
+
- console.log(...) — captured.
|
|
949
1040
|
|
|
950
|
-
|
|
1041
|
+
Programs have no portable ambient capabilities. Return JSON and reduce large results before they truncate. Fetch skills({ name: "usage" }) once for selection rules, exact result shapes, repair, examples, guide handling${connectorGuides ? ", connector-guide rules" : ""}, and runtime differences.`;
|
|
951
1042
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
952
1043
|
export function registerExecuteTool(server, registry, ctx) {
|
|
953
1044
|
// Resolved once so the description and the collector cannot disagree about
|
|
@@ -956,6 +1047,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
956
1047
|
maxBytes: resolveEmitBudget(ctx.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
|
|
957
1048
|
maxBlocks: resolveEmitBudget(ctx.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS),
|
|
958
1049
|
};
|
|
1050
|
+
const connectors = registry.listConnectors();
|
|
959
1051
|
const handler = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
|
|
960
1052
|
...(ctx.discoveryConcurrency !== undefined
|
|
961
1053
|
? { discoveryConcurrency: ctx.discoveryConcurrency }
|
|
@@ -965,20 +1057,22 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
965
1057
|
: {}),
|
|
966
1058
|
maxEmittedBytes: emitBudgets.maxBytes,
|
|
967
1059
|
maxEmittedBlocks: emitBudgets.maxBlocks,
|
|
1060
|
+
...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
|
|
968
1061
|
});
|
|
969
1062
|
server.registerTool("execute_code", {
|
|
970
|
-
description: executeDescription(emitBudgets, hasConnectorGuides(
|
|
1063
|
+
description: executeDescription(emitBudgets, hasConnectorGuides(connectors), connectors),
|
|
971
1064
|
inputSchema: z.object({
|
|
972
1065
|
code: z
|
|
973
1066
|
.string()
|
|
974
|
-
.describe("One complete JavaScript async arrow function
|
|
1067
|
+
.describe("One complete JavaScript async arrow function that discovers, calls, and returns the reduced answer."),
|
|
975
1068
|
diagnostics: z
|
|
976
1069
|
.boolean()
|
|
977
1070
|
.optional()
|
|
978
1071
|
.describe("Add request-local, payload-free timing and result-size summaries."),
|
|
979
1072
|
}),
|
|
980
|
-
//
|
|
981
|
-
// executor
|
|
1073
|
+
// This hint describes connector calls, all explicitly read-only. The
|
|
1074
|
+
// supported executor constructions deny outbound access, filesystem,
|
|
1075
|
+
// and deployment config; X5 documents Dynamic runtime modules separately.
|
|
982
1076
|
annotations: {
|
|
983
1077
|
readOnlyHint: true,
|
|
984
1078
|
destructiveHint: false,
|