@zackbart/connecta 0.16.1 → 0.17.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 +102 -0
- package/dist/catalog-service.d.ts +4 -0
- package/dist/catalog-service.js +8 -1
- package/dist/catalog.js +114 -12
- package/dist/errors.d.ts +4 -6
- package/dist/execute.d.ts +5 -0
- package/dist/execute.js +229 -161
- package/dist/invocation.js +3 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.js +46 -14
- 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 +2 -2
- package/dist/providers/stripe.js +13 -11
- package/dist/registry.d.ts +32 -9
- package/dist/registry.js +217 -33
- package/dist/routes/mcp.js +6 -0
- package/dist/skills.d.ts +4 -0
- package/dist/skills.js +157 -18
- 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 +7 -4
- package/documentation/code-mode.md +45 -53
- package/documentation/connector-guides.md +24 -19
- package/documentation/connectors.md +13 -1
- package/documentation/meta-tools.md +26 -17
- package/documentation/mixpanel.md +20 -0
- package/documentation/operations.md +21 -18
- package/documentation/operator-ui.md +12 -2
- package/documentation/provider-audit.md +3 -3
- package/documentation/provider-conventions.md +26 -13
- package/documentation/stripe.md +45 -14
- package/documentation/upgrading.md +28 -4
- package/ethos.md +3 -3
- package/examples/worker/README.md +4 -3
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/dist/execute.js
CHANGED
|
@@ -125,7 +125,7 @@ const EMIT_SHAPE_HINT = '{ type: "text", text } or { type: "image" | "audio", da
|
|
|
125
125
|
*/
|
|
126
126
|
function requireEmittedBlock(raw) {
|
|
127
127
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
128
|
-
throw
|
|
128
|
+
throw guestFailure("invalid_args", `connecta.emit accepts exactly one content block: ${EMIT_SHAPE_HINT}`);
|
|
129
129
|
}
|
|
130
130
|
const block = raw;
|
|
131
131
|
const fields = block.type === "text"
|
|
@@ -134,16 +134,16 @@ function requireEmittedBlock(raw) {
|
|
|
134
134
|
? ["type", "data", "mimeType"]
|
|
135
135
|
: undefined;
|
|
136
136
|
if (!fields) {
|
|
137
|
-
throw
|
|
137
|
+
throw guestFailure("invalid_args", `connecta.emit supports content types "text", "image", and "audio"; got ${JSON.stringify(block.type)}`);
|
|
138
138
|
}
|
|
139
139
|
for (const field of fields) {
|
|
140
140
|
if (typeof block[field] !== "string") {
|
|
141
|
-
throw
|
|
141
|
+
throw guestFailure("invalid_args", `connecta.emit block field "${field}" must be a string: ${EMIT_SHAPE_HINT}`);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
const extra = Object.keys(block).filter((key) => !fields.includes(key));
|
|
145
145
|
if (extra.length > 0) {
|
|
146
|
-
throw
|
|
146
|
+
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
147
|
}
|
|
148
148
|
return raw;
|
|
149
149
|
}
|
|
@@ -172,20 +172,20 @@ function describeUiArgument(raw) {
|
|
|
172
172
|
*/
|
|
173
173
|
function requireUiHtml(raw) {
|
|
174
174
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
175
|
-
throw
|
|
175
|
+
throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${describeUiArgument(raw)}`);
|
|
176
176
|
}
|
|
177
177
|
return raw;
|
|
178
178
|
}
|
|
179
179
|
function requireRecord(raw, label) {
|
|
180
180
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
181
|
-
throw
|
|
181
|
+
throw guestFailure("invalid_args", `${label} must be an object`);
|
|
182
182
|
}
|
|
183
183
|
return raw;
|
|
184
184
|
}
|
|
185
185
|
function requireExactKeys(value, allowed, label) {
|
|
186
186
|
const extras = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
187
187
|
if (extras.length > 0) {
|
|
188
|
-
throw
|
|
188
|
+
throw guestFailure("invalid_args", `${label} carries unsupported field(s) ${extras.map((key) => JSON.stringify(key)).join(", ")}`);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
function requireUiReadKey(raw, label) {
|
|
@@ -193,7 +193,7 @@ function requireUiReadKey(raw, label) {
|
|
|
193
193
|
raw.length === 0 ||
|
|
194
194
|
raw.length > 128 ||
|
|
195
195
|
FORBIDDEN_UI_KEY.has(raw)) {
|
|
196
|
-
throw
|
|
196
|
+
throw guestFailure("invalid_args", `${label} must be a non-empty string of at most 128 characters and cannot be __proto__, constructor, or prototype`);
|
|
197
197
|
}
|
|
198
198
|
return raw;
|
|
199
199
|
}
|
|
@@ -201,32 +201,32 @@ function requireUiReads(raw) {
|
|
|
201
201
|
const record = requireRecord(raw, "connecta.ui options.reads");
|
|
202
202
|
const names = Object.keys(record);
|
|
203
203
|
if (names.length === 0 || names.length > MAX_UI_READ_BINDINGS) {
|
|
204
|
-
throw
|
|
204
|
+
throw guestFailure("invalid_args", `connecta.ui options.reads must contain from 1 through ${MAX_UI_READ_BINDINGS} named bindings`);
|
|
205
205
|
}
|
|
206
206
|
const reads = Object.create(null);
|
|
207
207
|
for (const name of names) {
|
|
208
208
|
if (!UI_READ_NAME.test(name) || FORBIDDEN_UI_KEY.has(name)) {
|
|
209
|
-
throw
|
|
209
|
+
throw guestFailure("invalid_args", `connecta.ui read binding name ${JSON.stringify(name)} must match ${UI_READ_NAME}`);
|
|
210
210
|
}
|
|
211
211
|
const value = requireRecord(record[name], `connecta.ui read binding ${JSON.stringify(name)}`);
|
|
212
212
|
requireExactKeys(value, ["address", "fixedArgs", "viewArgs"], `connecta.ui read binding ${JSON.stringify(name)}`);
|
|
213
213
|
if (typeof value.address !== "string" || value.address.length === 0) {
|
|
214
|
-
throw
|
|
214
|
+
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} address must be a non-empty string`);
|
|
215
215
|
}
|
|
216
216
|
const fixedArgs = value.fixedArgs === undefined
|
|
217
217
|
? {}
|
|
218
218
|
: requireRecord(value.fixedArgs, `connecta.ui read binding ${JSON.stringify(name)} fixedArgs`);
|
|
219
219
|
const rawViewArgs = value.viewArgs ?? [];
|
|
220
220
|
if (!Array.isArray(rawViewArgs) || rawViewArgs.length > MAX_UI_VIEW_ARGS) {
|
|
221
|
-
throw
|
|
221
|
+
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
222
|
}
|
|
223
223
|
const viewArgs = rawViewArgs.map((key) => requireUiReadKey(key, `connecta.ui read binding ${JSON.stringify(name)} viewArgs entry`));
|
|
224
224
|
if (new Set(viewArgs).size !== viewArgs.length) {
|
|
225
|
-
throw
|
|
225
|
+
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} viewArgs must not repeat a key`);
|
|
226
226
|
}
|
|
227
227
|
for (const key of viewArgs) {
|
|
228
228
|
if (Object.prototype.hasOwnProperty.call(fixedArgs, key)) {
|
|
229
|
-
throw
|
|
229
|
+
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} view argument ${JSON.stringify(key)} cannot override a fixed argument`);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
reads[name] = {
|
|
@@ -239,7 +239,7 @@ function requireUiReads(raw) {
|
|
|
239
239
|
}
|
|
240
240
|
function requireUiPayload(values) {
|
|
241
241
|
if (values.length !== 1 && values.length !== 2) {
|
|
242
|
-
throw
|
|
242
|
+
throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${values.length} arguments`);
|
|
243
243
|
}
|
|
244
244
|
const html = requireUiHtml(values[0]);
|
|
245
245
|
if (values.length === 1)
|
|
@@ -247,7 +247,7 @@ function requireUiPayload(values) {
|
|
|
247
247
|
const options = requireRecord(values[1], "connecta.ui options");
|
|
248
248
|
requireExactKeys(options, ["reads"], "connecta.ui options");
|
|
249
249
|
if (!Object.prototype.hasOwnProperty.call(options, "reads")) {
|
|
250
|
-
throw
|
|
250
|
+
throw guestFailure("invalid_args", "connecta.ui options must contain reads");
|
|
251
251
|
}
|
|
252
252
|
return { html, reads: requireUiReads(options.reads) };
|
|
253
253
|
}
|
|
@@ -280,11 +280,11 @@ export class EmitCollector {
|
|
|
280
280
|
accept(raw) {
|
|
281
281
|
const block = requireEmittedBlock(raw);
|
|
282
282
|
if (this.blocks.length >= this.maxBlocks) {
|
|
283
|
-
throw
|
|
283
|
+
throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 remaining`);
|
|
284
284
|
}
|
|
285
285
|
const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
|
|
286
286
|
if (this.bytes + size > this.maxBytes) {
|
|
287
|
-
throw
|
|
287
|
+
throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
|
|
288
288
|
}
|
|
289
289
|
this.blocks.push(block);
|
|
290
290
|
this.bytes += size;
|
|
@@ -305,24 +305,24 @@ export class EmitCollector {
|
|
|
305
305
|
*/
|
|
306
306
|
acceptUi(...values) {
|
|
307
307
|
if (this.ui) {
|
|
308
|
-
throw
|
|
308
|
+
throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
|
|
309
309
|
}
|
|
310
310
|
this.acceptUiPayload(requireUiPayload(values));
|
|
311
311
|
}
|
|
312
312
|
acceptUiPayload(payload) {
|
|
313
313
|
if (this.ui) {
|
|
314
|
-
throw
|
|
314
|
+
throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
|
|
315
315
|
}
|
|
316
316
|
let serialized;
|
|
317
317
|
try {
|
|
318
318
|
serialized = JSON.stringify(payload);
|
|
319
319
|
}
|
|
320
320
|
catch {
|
|
321
|
-
throw
|
|
321
|
+
throw guestFailure("invalid_args", "connecta.ui payload must be JSON-serializable");
|
|
322
322
|
}
|
|
323
323
|
const size = diagnosticsEncoder.encode(serialized).byteLength;
|
|
324
324
|
if (this.bytes + size > this.maxBytes) {
|
|
325
|
-
throw
|
|
325
|
+
throw guestFailure("budget_exceeded", `connecta.ui byte budget exceeded: payload is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
|
|
326
326
|
}
|
|
327
327
|
this.ui = payload;
|
|
328
328
|
this.bytes += size;
|
|
@@ -430,18 +430,75 @@ const SANDBOX_RESERVED_NAMES = new Set([
|
|
|
430
430
|
function msg(err) {
|
|
431
431
|
return err instanceof Error ? err.message : String(err);
|
|
432
432
|
}
|
|
433
|
-
function
|
|
433
|
+
function guestFailure(code, message, retryable = false) {
|
|
434
|
+
return new InvocationFailure({ code, message, retryable });
|
|
435
|
+
}
|
|
436
|
+
const GUEST_FAILURE_FRAME = "\u001econnecta-error:";
|
|
437
|
+
const guestFailureFrames = new WeakMap();
|
|
438
|
+
/** Per-execution secret: guest prose cannot collide with this host frame. */
|
|
439
|
+
function guestFailureSecret() {
|
|
440
|
+
const words = new Uint32Array(4);
|
|
441
|
+
crypto.getRandomValues(words);
|
|
442
|
+
return Array.from(words, (word) => word.toString(16).padStart(8, "0")).join("");
|
|
443
|
+
}
|
|
444
|
+
function framedGuestFailure(secret, failure) {
|
|
445
|
+
const framed = new InvocationFailure(failure.details);
|
|
446
|
+
framed.message =
|
|
447
|
+
`${GUEST_FAILURE_FRAME}${secret}:${JSON.stringify(failure.details)}`;
|
|
448
|
+
guestFailureFrames.set(failure, framed.message);
|
|
449
|
+
return framed;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Rebuilds host failures as guest Error instances, then installs connector
|
|
453
|
+
* shortcuts. The random frame is hidden in this closure and Error is locked:
|
|
454
|
+
* connector prose and model code can neither collide with nor forge its frame.
|
|
455
|
+
*/
|
|
456
|
+
function lazyNamespacePrelude(connectors, failureSecret) {
|
|
434
457
|
const declarations = connectors
|
|
435
458
|
.map(({ id, namespace }) => `globalThis[${JSON.stringify(namespace)}] = __makeConnectaNamespace(${JSON.stringify(id)});`)
|
|
436
459
|
.join("\n");
|
|
437
|
-
return `(() => {
|
|
460
|
+
return `((failurePrefix) => {
|
|
461
|
+
const NativeError = globalThis.Error;
|
|
462
|
+
const startsWith = Function.prototype.call.bind(String.prototype.startsWith);
|
|
463
|
+
const slice = Function.prototype.call.bind(String.prototype.slice);
|
|
464
|
+
const parse = JSON.parse;
|
|
465
|
+
const freeze = Object.freeze;
|
|
466
|
+
const defineProperties = Object.defineProperties;
|
|
467
|
+
const construct = Reflect.construct;
|
|
468
|
+
function ConnectaError(message, options) {
|
|
469
|
+
let details;
|
|
470
|
+
if (typeof message === "string" && startsWith(message, failurePrefix)) {
|
|
471
|
+
try { details = parse(slice(message, failurePrefix.length)); } catch {}
|
|
472
|
+
}
|
|
473
|
+
const error = construct(
|
|
474
|
+
NativeError,
|
|
475
|
+
options === undefined ? [details ? details.message : message] : [details ? details.message : message, options],
|
|
476
|
+
new.target || NativeError
|
|
477
|
+
);
|
|
478
|
+
if (details) {
|
|
479
|
+
freeze(details);
|
|
480
|
+
defineProperties(error, {
|
|
481
|
+
code: { value: details.code, enumerable: true },
|
|
482
|
+
retryable: { value: details.retryable, enumerable: true },
|
|
483
|
+
details: { value: details, enumerable: true }
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return error;
|
|
487
|
+
}
|
|
488
|
+
ConnectaError.prototype = NativeError.prototype;
|
|
489
|
+
Object.setPrototypeOf(ConnectaError, NativeError);
|
|
490
|
+
Object.defineProperty(globalThis, "Error", {
|
|
491
|
+
value: ConnectaError,
|
|
492
|
+
writable: false,
|
|
493
|
+
configurable: false
|
|
494
|
+
});
|
|
438
495
|
const __makeConnectaNamespace = (connectorId) => Object.freeze(new Proxy(Object.create(null), {
|
|
439
496
|
get: (_target, toolName) => typeof toolName === "string"
|
|
440
497
|
? (args) => connecta.__callNamespace(connectorId, toolName, args)
|
|
441
498
|
: undefined
|
|
442
499
|
}));
|
|
443
500
|
${declarations}
|
|
444
|
-
})();`;
|
|
501
|
+
})(${JSON.stringify(`${GUEST_FAILURE_FRAME}${failureSecret}:`)});`;
|
|
445
502
|
}
|
|
446
503
|
/**
|
|
447
504
|
* Expose one fixed host provider plus trusted sandbox setup that creates a
|
|
@@ -462,10 +519,12 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
462
519
|
...(limits.probeTimeoutMs !== undefined
|
|
463
520
|
? { probeTimeoutMs: limits.probeTimeoutMs }
|
|
464
521
|
: {}),
|
|
522
|
+
...(limits.defer !== undefined ? { defer: limits.defer } : {}),
|
|
465
523
|
});
|
|
466
524
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
467
525
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
468
526
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
527
|
+
const failureSecret = guestFailureSecret();
|
|
469
528
|
let hostCalls = 0;
|
|
470
529
|
const connectors = registry.listConnectors();
|
|
471
530
|
const namespaces = [];
|
|
@@ -493,16 +552,13 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
493
552
|
beforeDispatch: () => {
|
|
494
553
|
hostCalls++;
|
|
495
554
|
if (hostCalls > maxHostCalls) {
|
|
496
|
-
throw
|
|
555
|
+
throw guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`);
|
|
497
556
|
}
|
|
498
557
|
},
|
|
499
558
|
});
|
|
500
559
|
/**
|
|
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.
|
|
560
|
+
* Discovery policy failures use the same thrown vocabulary as calls and
|
|
561
|
+
* utilities. The transport below reconstructs their code inside the guest.
|
|
506
562
|
*/
|
|
507
563
|
const typedDiscovery = async (operation) => {
|
|
508
564
|
try {
|
|
@@ -510,11 +566,7 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
510
566
|
}
|
|
511
567
|
catch (err) {
|
|
512
568
|
if (err instanceof DiscoveryPolicyError) {
|
|
513
|
-
|
|
514
|
-
code: err.code,
|
|
515
|
-
message: err.message,
|
|
516
|
-
retryable: false,
|
|
517
|
-
}));
|
|
569
|
+
throw guestFailure(err.code, err.message);
|
|
518
570
|
}
|
|
519
571
|
throw err;
|
|
520
572
|
}
|
|
@@ -524,7 +576,6 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
524
576
|
limits.diagnostics?.recordCall(diagnosticOperation, outcome);
|
|
525
577
|
if (!outcome.ok) {
|
|
526
578
|
const failure = new InvocationFailure(outcome.error);
|
|
527
|
-
limits.onInvocationFailure?.(failure);
|
|
528
579
|
throw failure;
|
|
529
580
|
}
|
|
530
581
|
return outcome.value;
|
|
@@ -534,7 +585,6 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
534
585
|
limits.diagnostics?.recordCall("call", outcome);
|
|
535
586
|
if (!outcome.ok) {
|
|
536
587
|
const failure = new InvocationFailure(outcome.error);
|
|
537
|
-
limits.onInvocationFailure?.(failure);
|
|
538
588
|
throw failure;
|
|
539
589
|
}
|
|
540
590
|
return outcome.value;
|
|
@@ -554,10 +604,13 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
554
604
|
for (const [name, binding] of Object.entries(payload.reads)) {
|
|
555
605
|
const resolution = await catalog.resolveTool(binding.address, limits.signal !== undefined ? { signal: limits.signal } : {});
|
|
556
606
|
if (!resolution.ok) {
|
|
557
|
-
throw new
|
|
607
|
+
throw new InvocationFailure({
|
|
608
|
+
...resolution.error,
|
|
609
|
+
message: `connecta.ui read binding ${JSON.stringify(name)} could not resolve ${JSON.stringify(binding.address)}: ${resolution.error.message}`,
|
|
610
|
+
});
|
|
558
611
|
}
|
|
559
612
|
if (!isExplicitlyReadOnly(resolution.resolved.definition)) {
|
|
560
|
-
throw
|
|
613
|
+
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
614
|
}
|
|
562
615
|
reads[name] = {
|
|
563
616
|
...binding,
|
|
@@ -566,118 +619,128 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
566
619
|
}
|
|
567
620
|
return { html: payload.html, reads };
|
|
568
621
|
};
|
|
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();
|
|
622
|
+
const fns = {
|
|
623
|
+
__callNamespace: callNamespace,
|
|
624
|
+
call: (address, args) => callAddress(address, args),
|
|
625
|
+
// Emission is a provider function, never an ExecuteResult field —
|
|
626
|
+
// that is what keeps the Executor contract untouched and parity
|
|
627
|
+
// structural (M8). It spends no host-call budget (M7); its own
|
|
628
|
+
// budgets live in the collector.
|
|
629
|
+
emit: async (block) => {
|
|
630
|
+
if (!limits.emitCollector) {
|
|
631
|
+
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
632
|
+
}
|
|
633
|
+
limits.emitCollector.accept(block);
|
|
634
|
+
},
|
|
635
|
+
// The rendered-output channel rides the same bridge for the same
|
|
636
|
+
// reason (U7): one more provider fn, no change to ExecuteResult or
|
|
637
|
+
// the Executor contract. Delivery is the handler's job, not the
|
|
638
|
+
// guest's — nothing here becomes addressable.
|
|
639
|
+
ui: async (...values) => {
|
|
640
|
+
if (!limits.emitCollector) {
|
|
641
|
+
throw guestFailure("unavailable", "connecta.ui is unavailable: no emission collector was configured for this execution", true);
|
|
642
|
+
}
|
|
643
|
+
const payload = await validateUiReads(requireUiPayload(values));
|
|
644
|
+
limits.emitCollector.acceptUiPayload(payload);
|
|
645
|
+
},
|
|
646
|
+
batch: async (calls) => {
|
|
647
|
+
const started = Date.now();
|
|
648
|
+
const callCount = Array.isArray(calls) ? calls.length : 0;
|
|
649
|
+
try {
|
|
650
|
+
if (!Array.isArray(calls)) {
|
|
651
|
+
throw guestFailure("invalid_args", "calls must be an array");
|
|
652
|
+
}
|
|
653
|
+
if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
|
|
654
|
+
throw guestFailure("invalid_args", `connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`);
|
|
655
|
+
}
|
|
656
|
+
const result = await Promise.all(calls.map(async (call) => {
|
|
657
|
+
const item = call;
|
|
665
658
|
try {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
});
|
|
672
|
-
limits.diagnostics?.recordCatalog("describe", Date.now() - started, true, result);
|
|
673
|
-
return result;
|
|
659
|
+
return {
|
|
660
|
+
address: String(item.address),
|
|
661
|
+
ok: true,
|
|
662
|
+
data: await callAddress(item.address, item.args, "batch"),
|
|
663
|
+
};
|
|
674
664
|
}
|
|
675
665
|
catch (err) {
|
|
676
|
-
|
|
677
|
-
|
|
666
|
+
const details = err instanceof InvocationFailure
|
|
667
|
+
? err.details
|
|
668
|
+
: classifyCallError(err, "batch_call_failed");
|
|
669
|
+
return {
|
|
670
|
+
address: String(item.address),
|
|
671
|
+
ok: false,
|
|
672
|
+
error: details.message,
|
|
673
|
+
errorDetails: details,
|
|
674
|
+
};
|
|
678
675
|
}
|
|
679
|
-
}
|
|
680
|
-
|
|
676
|
+
}));
|
|
677
|
+
limits.diagnostics?.recordBatch(Date.now() - started, true, callCount, result);
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
limits.diagnostics?.recordBatch(Date.now() - started, false, callCount);
|
|
682
|
+
throw err;
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
search: async (raw) => {
|
|
686
|
+
const started = Date.now();
|
|
687
|
+
try {
|
|
688
|
+
const result = await typedDiscovery(async () => {
|
|
689
|
+
const args = (raw ?? {});
|
|
690
|
+
const result = flatSearchResult(await catalog.search({
|
|
691
|
+
...args,
|
|
692
|
+
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
693
|
+
}));
|
|
694
|
+
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
695
|
+
return result;
|
|
696
|
+
});
|
|
697
|
+
limits.diagnostics?.recordCatalog("search", Date.now() - started, true, result);
|
|
698
|
+
return result;
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
limits.diagnostics?.recordCatalog("search", Date.now() - started, false);
|
|
702
|
+
throw err;
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
describe: async (raw) => {
|
|
706
|
+
const started = Date.now();
|
|
707
|
+
try {
|
|
708
|
+
const result = await typedDiscovery(async () => {
|
|
709
|
+
const args = (raw ?? {});
|
|
710
|
+
const result = { tools: await catalog.describe(args) };
|
|
711
|
+
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
712
|
+
return result;
|
|
713
|
+
});
|
|
714
|
+
limits.diagnostics?.recordCatalog("describe", Date.now() - started, true, result);
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
limits.diagnostics?.recordCatalog("describe", Date.now() - started, false);
|
|
719
|
+
throw err;
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
const transportedFns = Object.fromEntries(Object.entries(fns).map(([name, fn]) => [
|
|
724
|
+
name,
|
|
725
|
+
async (...args) => {
|
|
726
|
+
try {
|
|
727
|
+
return await fn(...args);
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
if (err instanceof InvocationFailure) {
|
|
731
|
+
const framed = framedGuestFailure(failureSecret, err);
|
|
732
|
+
limits.onInvocationFailure?.(err);
|
|
733
|
+
throw framed;
|
|
734
|
+
}
|
|
735
|
+
throw err;
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
]));
|
|
739
|
+
return [
|
|
740
|
+
{
|
|
741
|
+
name: "connecta",
|
|
742
|
+
prelude: lazyNamespacePrelude(namespaces, failureSecret),
|
|
743
|
+
fns: transportedFns,
|
|
681
744
|
},
|
|
682
745
|
];
|
|
683
746
|
}
|
|
@@ -731,6 +794,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
731
794
|
...(config.probeTimeoutMs !== undefined
|
|
732
795
|
? { probeTimeoutMs: config.probeTimeoutMs }
|
|
733
796
|
: {}),
|
|
797
|
+
...(config.defer !== undefined ? { defer: config.defer } : {}),
|
|
734
798
|
});
|
|
735
799
|
}
|
|
736
800
|
finally {
|
|
@@ -816,8 +880,8 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
816
880
|
// rather than losing it to prose.
|
|
817
881
|
let invocationFailure;
|
|
818
882
|
for (const match of [
|
|
819
|
-
(candidate) =>
|
|
820
|
-
(candidate) => outcome.error?.includes(
|
|
883
|
+
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
|
|
884
|
+
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) => message !== undefined && outcome.error?.includes(message) === true),
|
|
821
885
|
]) {
|
|
822
886
|
for (let i = invocationFailures.length - 1; i >= 0; i--) {
|
|
823
887
|
const candidate = invocationFailures[i];
|
|
@@ -932,22 +996,24 @@ function discardedEmitsText(emitted) {
|
|
|
932
996
|
];
|
|
933
997
|
return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
|
|
934
998
|
}
|
|
935
|
-
const executeDescription = (emitBudgets, connectorGuides) => `Choose the route before discovery. Exactly one unknown-address read uses top-level search_tools then call_tool
|
|
999
|
+
const executeDescription = (emitBudgets, connectorGuides) => `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.
|
|
936
1000
|
|
|
937
|
-
Write an async arrow function.
|
|
938
|
-
- Connector globals call <connectorId>.<toolName>(args)
|
|
1001
|
+
Write an async arrow function. Portable programs use no ambient capabilities. Use only:
|
|
1002
|
+
- Connector globals call sanitized <connectorId>.<toolName>(args): non-identifier characters become "_"; prefix a leading digit; suffix a reserved word.
|
|
939
1003
|
- connecta.call(address, args) and connecta.batch(calls) use canonical addresses. Every batch entry is { address, ok: true, data } or { address, ok: false, error, errorDetails: { code, retryable } }; destructure it.
|
|
940
1004
|
- top-level search_tools returns { connectors: [{ id, tools }], total, offset, limit, hasMore }; connecta.search returns { tools, total, offset, limit, hasMore }; connecta.describe returns { tools }.
|
|
941
|
-
- connecta.search(args) loads catalogs and must be followed by selection and calls in this program; set connector to the obvious id to load one, otherwise it loads all. For distinct operations, make separate short searches here.
|
|
942
|
-
- connecta.emit(block) —
|
|
943
|
-
- connecta.ui(html, options?) — one success-only view
|
|
944
|
-
- console.log(...) — captured
|
|
1005
|
+
- connecta.search(args) loads catalogs and must be followed by selection and calls in this program; set connector to the obvious id to load one, otherwise it loads all. For distinct operations, make separate short searches here. Check address, description, requiredInputKeys, truncation, safety, and outputs; never take the first lexical or merely input-compatible match. Select by fit; do not require it to be the only match. Missing outputKeys means inspect outputSchema, not discard the candidate. Every required key needs task or prior-result data; do not prefer zero required keys. Put every requiredInputKey in call args. For dependencies, match an earlier outputKey to the later requiredInputKey. [] means no required keys, not permission to invent args. Describe only a truncated/insufficient compact shape. Reducers use declared outputKeys, never guessed items/results roots. Describe one address or an addresses array. Use safety: "readOnly" to avoid advertising calls this sandbox cannot execute.${connectorGuides ? " guideRequired: true = stop. Describe clears only schema_truncated; otherwise return its exact guide, fetch with top-level skills, then write the informed call." : ""}
|
|
1006
|
+
- 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.
|
|
1007
|
+
- connecta.ui(html, options?) — one success-only view. One arg is display-only; reads use { reads: { name: { address, fixedArgs?, viewArgs? } } } and markup calls connecta.read(name, args). Admission and one budget, not two, apply; a second, over-budget, or invalid call throws catchably; the model reads the return value, not the view; return the initial summary from its variables.
|
|
1008
|
+
- console.log(...) — captured.
|
|
1009
|
+
|
|
1010
|
+
QuickJS blocks imports and has no fetch, process, timers, crypto, or WebSocket. Dynamic Workers require only { loader }; bindings/modules/globalOutbound violate the contract. Then env maps are empty; node:fs/http/https are absent; outbound fetch/WebSocket/node:net/tls are denied; DNS is unresolved. Runtime builtins remain through import() and process.getBuiltinModule(), including node:path and cloudflare:workers; the set can drift. Timers/process/crypto/WebSocket and data: fetch remain. Avoid runtime-only capabilities; QuickJS fails.
|
|
945
1011
|
|
|
946
1012
|
Dependent example (only when the second call requires a value returned by the first): async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const pick = (suffix) => { const match = tools.find((t) => t.address.endsWith(suffix)); if (!match) throw new Error("no tool for " + suffix); return match.address; }; const run = await connecta.call(pick(".get_run"), { runId: 42 }); const logs = await connecta.call(pick(".get_job_logs"), { jobId: run.failedJobId }); return [run, logs]; }
|
|
947
1013
|
|
|
948
|
-
|
|
1014
|
+
A caught Connecta failure keeps its message and exposes code, retryable, and details; batch uses the same fields. Branch on fields, never prose. Never retry retryable: false, or rate_limited immediately; portable code cannot wait. Return JSON; reduce large results before they truncate.
|
|
949
1015
|
|
|
950
|
-
Plain JS
|
|
1016
|
+
Plain JS. Compact schemas are TypeScript-like: write the property names they display; never guess positions or aliases.`;
|
|
951
1017
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
952
1018
|
export function registerExecuteTool(server, registry, ctx) {
|
|
953
1019
|
// Resolved once so the description and the collector cannot disagree about
|
|
@@ -965,6 +1031,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
965
1031
|
: {}),
|
|
966
1032
|
maxEmittedBytes: emitBudgets.maxBytes,
|
|
967
1033
|
maxEmittedBlocks: emitBudgets.maxBlocks,
|
|
1034
|
+
...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
|
|
968
1035
|
});
|
|
969
1036
|
server.registerTool("execute_code", {
|
|
970
1037
|
description: executeDescription(emitBudgets, hasConnectorGuides(registry.listConnectors())),
|
|
@@ -977,8 +1044,9 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
977
1044
|
.optional()
|
|
978
1045
|
.describe("Add request-local, payload-free timing and result-size summaries."),
|
|
979
1046
|
}),
|
|
980
|
-
//
|
|
981
|
-
// executor
|
|
1047
|
+
// This hint describes connector calls, all explicitly read-only. The
|
|
1048
|
+
// supported executor constructions deny outbound access, filesystem,
|
|
1049
|
+
// and deployment config; X5 documents Dynamic runtime modules separately.
|
|
982
1050
|
annotations: {
|
|
983
1051
|
readOnlyHint: true,
|
|
984
1052
|
destructiveHint: false,
|