@zackbart/connecta 0.18.3 → 0.20.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 +131 -4
- package/dist/apps-shell.d.ts +10 -12
- package/dist/apps-shell.js +29 -220
- package/dist/catalog-service.d.ts +16 -13
- package/dist/catalog-service.js +106 -115
- package/dist/catalog.js +29 -46
- package/dist/connector-scope.js +2 -7
- package/dist/connectors/api.d.ts +4 -16
- package/dist/connectors/api.js +19 -46
- package/dist/connectors/guarded-fetch.d.ts +9 -23
- package/dist/connectors/guarded-fetch.js +38 -76
- package/dist/connectors/remote-mcp.js +36 -79
- package/dist/errors.d.ts +6 -27
- package/dist/errors.js +8 -5
- package/dist/execute.d.ts +23 -28
- package/dist/execute.js +101 -257
- package/dist/executor-result.d.ts +1 -0
- package/dist/executor-result.js +4 -11
- package/dist/executors/quickjs-child.js +1 -3
- package/dist/executors/quickjs-runtime.js +1 -3
- package/dist/executors/quickjs.js +1 -3
- package/dist/index.js +134 -123
- package/dist/invocation.d.ts +1 -1
- package/dist/invocation.js +113 -183
- package/dist/meta-tools.d.ts +15 -29
- package/dist/meta-tools.js +41 -582
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/providers/cloudflare.d.ts +2 -18
- package/dist/providers/cloudflare.js +1460 -2451
- package/dist/providers/linear.d.ts +4 -41
- package/dist/providers/linear.js +8 -39
- package/dist/providers/mixpanel.d.ts +3 -25
- package/dist/providers/mixpanel.js +7 -22
- package/dist/providers/notion.d.ts +1 -15
- package/dist/providers/notion.js +44 -173
- package/dist/providers/revenuecat.d.ts +4 -57
- package/dist/providers/revenuecat.js +10 -93
- package/dist/providers/stripe.d.ts +1 -12
- package/dist/providers/stripe.js +7 -45
- package/dist/registry.d.ts +9 -34
- package/dist/registry.js +9 -103
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +3 -3
- package/dist/routes/shared.d.ts +15 -15
- package/dist/routes/shared.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -5
- package/dist/timeout.d.ts +8 -7
- package/dist/timeout.js +47 -38
- package/dist/types.d.ts +3 -3
- package/dist/ui.d.ts +1 -25
- package/dist/ui.js +18 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/call-admission.md +1 -1
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +25 -25
- package/documentation/connectors.md +24 -1
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +4 -30
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +1 -1
- package/documentation/operations.md +29 -25
- package/documentation/provider-conventions.md +4 -5
- package/documentation/revenuecat.md +1 -1
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +43 -8
- package/ethos.md +75 -121
- package/package.json +3 -4
- package/templates/node/package.json +1 -1
- package/documentation/code-first-exploration.md +0 -292
- package/documentation/mcp-2026-07-28.md +0 -46
- package/documentation/mcp-ui-design.md +0 -382
- package/documentation/program-ui-read-calls.md +0 -213
- package/documentation/provider-audit.md +0 -198
- package/documentation/rich-output-design.md +0 -211
package/dist/execute.js
CHANGED
|
@@ -4,10 +4,9 @@ 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 { boundedEchoText, classifyCallError } from "./errors.js";
|
|
7
|
+
import { boundedEchoText, classifyCallError, msg } from "./errors.js";
|
|
8
8
|
import { InvocationFailure, InvocationService, } from "./invocation.js";
|
|
9
9
|
import { hasConnectorGuides } from "./skills.js";
|
|
10
|
-
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
11
10
|
/** Keep one model-written program from amplifying into an unbounded fan-out. */
|
|
12
11
|
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
13
12
|
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
@@ -149,11 +148,7 @@ function requireEmittedBlock(raw) {
|
|
|
149
148
|
}
|
|
150
149
|
return raw;
|
|
151
150
|
}
|
|
152
|
-
const UI_SHAPE_HINT = "connecta.ui accepts exactly one
|
|
153
|
-
const MAX_UI_READ_BINDINGS = 32;
|
|
154
|
-
const MAX_UI_VIEW_ARGS = 32;
|
|
155
|
-
const UI_READ_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
156
|
-
const FORBIDDEN_UI_KEY = new Set(["__proto__", "constructor", "prototype"]);
|
|
151
|
+
const UI_SHAPE_HINT = "connecta.ui accepts exactly one argument: a non-empty string of HTML";
|
|
157
152
|
/** What the argument was, named the way the emit validator names a bad field. */
|
|
158
153
|
function describeUiArgument(raw) {
|
|
159
154
|
if (raw === null)
|
|
@@ -168,9 +163,9 @@ function describeUiArgument(raw) {
|
|
|
168
163
|
return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
|
|
169
164
|
}
|
|
170
165
|
/**
|
|
171
|
-
* Strict U1
|
|
172
|
-
*
|
|
173
|
-
* block
|
|
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.
|
|
174
169
|
*/
|
|
175
170
|
function requireUiHtml(raw) {
|
|
176
171
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
@@ -178,80 +173,11 @@ function requireUiHtml(raw) {
|
|
|
178
173
|
}
|
|
179
174
|
return raw;
|
|
180
175
|
}
|
|
181
|
-
function requireRecord(raw, label) {
|
|
182
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
183
|
-
throw guestFailure("invalid_args", `${label} must be an object`);
|
|
184
|
-
}
|
|
185
|
-
return raw;
|
|
186
|
-
}
|
|
187
|
-
function requireExactKeys(value, allowed, label) {
|
|
188
|
-
const extras = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
189
|
-
if (extras.length > 0) {
|
|
190
|
-
throw guestFailure("invalid_args", `${label} carries unsupported field(s) ${extras.map((key) => JSON.stringify(key)).join(", ")}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function requireUiReadKey(raw, label) {
|
|
194
|
-
if (typeof raw !== "string" ||
|
|
195
|
-
raw.length === 0 ||
|
|
196
|
-
raw.length > 128 ||
|
|
197
|
-
FORBIDDEN_UI_KEY.has(raw)) {
|
|
198
|
-
throw guestFailure("invalid_args", `${label} must be a non-empty string of at most 128 characters and cannot be __proto__, constructor, or prototype`);
|
|
199
|
-
}
|
|
200
|
-
return raw;
|
|
201
|
-
}
|
|
202
|
-
function requireUiReads(raw) {
|
|
203
|
-
const record = requireRecord(raw, "connecta.ui options.reads");
|
|
204
|
-
const names = Object.keys(record);
|
|
205
|
-
if (names.length === 0 || names.length > MAX_UI_READ_BINDINGS) {
|
|
206
|
-
throw guestFailure("invalid_args", `connecta.ui options.reads must contain from 1 through ${MAX_UI_READ_BINDINGS} named bindings`);
|
|
207
|
-
}
|
|
208
|
-
const reads = Object.create(null);
|
|
209
|
-
for (const name of names) {
|
|
210
|
-
if (!UI_READ_NAME.test(name) || FORBIDDEN_UI_KEY.has(name)) {
|
|
211
|
-
throw guestFailure("invalid_args", `connecta.ui read binding name ${JSON.stringify(name)} must match ${UI_READ_NAME}`);
|
|
212
|
-
}
|
|
213
|
-
const value = requireRecord(record[name], `connecta.ui read binding ${JSON.stringify(name)}`);
|
|
214
|
-
requireExactKeys(value, ["address", "fixedArgs", "viewArgs"], `connecta.ui read binding ${JSON.stringify(name)}`);
|
|
215
|
-
if (typeof value.address !== "string" || value.address.length === 0) {
|
|
216
|
-
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} address must be a non-empty string`);
|
|
217
|
-
}
|
|
218
|
-
const fixedArgs = value.fixedArgs === undefined
|
|
219
|
-
? {}
|
|
220
|
-
: requireRecord(value.fixedArgs, `connecta.ui read binding ${JSON.stringify(name)} fixedArgs`);
|
|
221
|
-
const rawViewArgs = value.viewArgs ?? [];
|
|
222
|
-
if (!Array.isArray(rawViewArgs) || rawViewArgs.length > MAX_UI_VIEW_ARGS) {
|
|
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`);
|
|
224
|
-
}
|
|
225
|
-
const viewArgs = rawViewArgs.map((key) => requireUiReadKey(key, `connecta.ui read binding ${JSON.stringify(name)} viewArgs entry`));
|
|
226
|
-
if (new Set(viewArgs).size !== viewArgs.length) {
|
|
227
|
-
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} viewArgs must not repeat a key`);
|
|
228
|
-
}
|
|
229
|
-
for (const key of viewArgs) {
|
|
230
|
-
if (Object.prototype.hasOwnProperty.call(fixedArgs, key)) {
|
|
231
|
-
throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} view argument ${JSON.stringify(key)} cannot override a fixed argument`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
reads[name] = {
|
|
235
|
-
address: value.address,
|
|
236
|
-
fixedArgs,
|
|
237
|
-
viewArgs,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
return reads;
|
|
241
|
-
}
|
|
242
176
|
function requireUiPayload(values) {
|
|
243
|
-
if (values.length !== 1
|
|
177
|
+
if (values.length !== 1) {
|
|
244
178
|
throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${values.length} arguments`);
|
|
245
179
|
}
|
|
246
|
-
|
|
247
|
-
if (values.length === 1)
|
|
248
|
-
return { html };
|
|
249
|
-
const options = requireRecord(values[1], "connecta.ui options");
|
|
250
|
-
requireExactKeys(options, ["reads"], "connecta.ui options");
|
|
251
|
-
if (!Object.prototype.hasOwnProperty.call(options, "reads")) {
|
|
252
|
-
throw guestFailure("invalid_args", "connecta.ui options must contain reads");
|
|
253
|
-
}
|
|
254
|
-
return { html, reads: requireUiReads(options.reads) };
|
|
180
|
+
return { html: requireUiHtml(values[0]) };
|
|
255
181
|
}
|
|
256
182
|
/**
|
|
257
183
|
* Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
|
|
@@ -306,15 +232,15 @@ export class EmitCollector {
|
|
|
306
232
|
* complaint about its type would send the author to fix the wrong thing.
|
|
307
233
|
*/
|
|
308
234
|
acceptUi(...values) {
|
|
309
|
-
|
|
310
|
-
throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
|
|
311
|
-
}
|
|
235
|
+
this.assertUiVacant();
|
|
312
236
|
this.acceptUiPayload(requireUiPayload(values));
|
|
313
237
|
}
|
|
314
|
-
|
|
238
|
+
assertUiVacant() {
|
|
315
239
|
if (this.ui) {
|
|
316
240
|
throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
|
|
317
241
|
}
|
|
242
|
+
}
|
|
243
|
+
acceptUiPayload(payload) {
|
|
318
244
|
let serialized;
|
|
319
245
|
try {
|
|
320
246
|
serialized = JSON.stringify(payload);
|
|
@@ -429,9 +355,6 @@ const SANDBOX_RESERVED_NAMES = new Set([
|
|
|
429
355
|
"__stringifyForCodemode",
|
|
430
356
|
"__parseForCodemode",
|
|
431
357
|
]);
|
|
432
|
-
function msg(err) {
|
|
433
|
-
return err instanceof Error ? err.message : String(err);
|
|
434
|
-
}
|
|
435
358
|
function guestFailure(code, message, retryable = false) {
|
|
436
359
|
return new InvocationFailure({ code, message, retryable });
|
|
437
360
|
}
|
|
@@ -515,13 +438,9 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
515
438
|
requestScope,
|
|
516
439
|
// A program that just missed an address cannot call search_tools.
|
|
517
440
|
searchRoute: "connecta.search",
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
...(limits.probeTimeoutMs !== undefined
|
|
522
|
-
? { probeTimeoutMs: limits.probeTimeoutMs }
|
|
523
|
-
: {}),
|
|
524
|
-
...(limits.defer !== undefined ? { defer: limits.defer } : {}),
|
|
441
|
+
concurrency: limits.discoveryConcurrency,
|
|
442
|
+
probeTimeoutMs: limits.probeTimeoutMs,
|
|
443
|
+
defer: limits.defer,
|
|
525
444
|
});
|
|
526
445
|
const invocation = new InvocationService(registry, catalog, activity);
|
|
527
446
|
const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
|
|
@@ -573,53 +492,30 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
573
492
|
throw err;
|
|
574
493
|
}
|
|
575
494
|
};
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
495
|
+
const timedCatalog = async (operation, fn) => {
|
|
496
|
+
const started = Date.now();
|
|
497
|
+
try {
|
|
498
|
+
const result = await fn();
|
|
499
|
+
limits.diagnostics?.recordCatalog(operation, Date.now() - started, true, result);
|
|
500
|
+
return result;
|
|
582
501
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const outcome = await invocation.invokeToolAlias(String(connectorId), String(toolAlias), sanitizeIdentifier, args ?? {}, invocationContext());
|
|
587
|
-
limits.diagnostics?.recordCall("call", outcome);
|
|
588
|
-
if (!outcome.ok) {
|
|
589
|
-
const failure = new InvocationFailure(outcome.error);
|
|
590
|
-
throw failure;
|
|
502
|
+
catch (err) {
|
|
503
|
+
limits.diagnostics?.recordCatalog(operation, Date.now() - started, false);
|
|
504
|
+
throw err;
|
|
591
505
|
}
|
|
506
|
+
};
|
|
507
|
+
const called = async (operation, invoke) => {
|
|
508
|
+
const outcome = await invoke();
|
|
509
|
+
limits.diagnostics?.recordCall(operation, outcome);
|
|
510
|
+
if (!outcome.ok)
|
|
511
|
+
throw new InvocationFailure(outcome.error);
|
|
592
512
|
return outcome.value;
|
|
593
513
|
};
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
* never work, while validation at use keeps a stale view from retaining old
|
|
600
|
-
* authority.
|
|
601
|
-
*/
|
|
602
|
-
const validateUiReads = async (payload) => {
|
|
603
|
-
if (!payload.reads)
|
|
604
|
-
return payload;
|
|
605
|
-
const reads = Object.create(null);
|
|
606
|
-
for (const [name, binding] of Object.entries(payload.reads)) {
|
|
607
|
-
const resolution = await catalog.resolveTool(binding.address, limits.signal !== undefined ? { signal: limits.signal } : {});
|
|
608
|
-
if (!resolution.ok) {
|
|
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
|
-
});
|
|
613
|
-
}
|
|
614
|
-
if (!isExplicitlyReadOnly(resolution.resolved.definition)) {
|
|
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`);
|
|
616
|
-
}
|
|
617
|
-
reads[name] = {
|
|
618
|
-
...binding,
|
|
619
|
-
address: `${resolution.resolved.connector.id}.${resolution.resolved.toolName}`,
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
return { html: payload.html, reads };
|
|
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()));
|
|
623
519
|
};
|
|
624
520
|
const fns = {
|
|
625
521
|
__callNamespace: callNamespace,
|
|
@@ -642,8 +538,7 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
642
538
|
if (!limits.emitCollector) {
|
|
643
539
|
throw guestFailure("unavailable", "connecta.ui is unavailable: no emission collector was configured for this execution", true);
|
|
644
540
|
}
|
|
645
|
-
|
|
646
|
-
limits.emitCollector.acceptUiPayload(payload);
|
|
541
|
+
limits.emitCollector.acceptUi(...values);
|
|
647
542
|
},
|
|
648
543
|
batch: async (calls) => {
|
|
649
544
|
const started = Date.now();
|
|
@@ -684,43 +579,21 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
|
|
|
684
579
|
throw err;
|
|
685
580
|
}
|
|
686
581
|
},
|
|
687
|
-
search: async (raw) => {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
},
|
|
582
|
+
search: async (raw) => timedCatalog("search", () => typedDiscovery(async () => {
|
|
583
|
+
const args = (raw ?? {});
|
|
584
|
+
const result = flatSearchResult(await catalog.search({
|
|
585
|
+
...args,
|
|
586
|
+
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
587
|
+
}));
|
|
588
|
+
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
589
|
+
return result;
|
|
590
|
+
})),
|
|
591
|
+
describe: async (raw) => timedCatalog("describe", () => typedDiscovery(async () => {
|
|
592
|
+
const args = (raw ?? {});
|
|
593
|
+
const result = { tools: await catalog.describe(args) };
|
|
594
|
+
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
595
|
+
return result;
|
|
596
|
+
})),
|
|
724
597
|
};
|
|
725
598
|
const transportedFns = Object.fromEntries(Object.entries(fns).map(([name, fn]) => [
|
|
726
599
|
name,
|
|
@@ -790,13 +663,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
790
663
|
},
|
|
791
664
|
emitCollector: emitted,
|
|
792
665
|
...(diagnostics ? { diagnostics } : {}),
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
...(config.probeTimeoutMs !== undefined
|
|
797
|
-
? { probeTimeoutMs: config.probeTimeoutMs }
|
|
798
|
-
: {}),
|
|
799
|
-
...(config.defer !== undefined ? { defer: config.defer } : {}),
|
|
666
|
+
discoveryConcurrency: config.discoveryConcurrency,
|
|
667
|
+
probeTimeoutMs: config.probeTimeoutMs,
|
|
668
|
+
defer: config.defer,
|
|
800
669
|
});
|
|
801
670
|
}
|
|
802
671
|
finally {
|
|
@@ -826,8 +695,10 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
826
695
|
retryAfterMs: err.retryAfterMs,
|
|
827
696
|
});
|
|
828
697
|
}
|
|
829
|
-
|
|
830
|
-
|
|
698
|
+
return failureResponse(err.message, {
|
|
699
|
+
emitted: err instanceof ExecutorExecutionError ? emitted : undefined,
|
|
700
|
+
diagnostics,
|
|
701
|
+
code: {
|
|
831
702
|
code: err.code,
|
|
832
703
|
message: err.message,
|
|
833
704
|
retryable: err.retryable,
|
|
@@ -835,28 +706,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
835
706
|
? { retryAfterMs: err.retryAfterMs }
|
|
836
707
|
: {}),
|
|
837
708
|
},
|
|
838
|
-
...(err instanceof ExecutorExecutionError
|
|
839
|
-
? discardedEmits(emitted)
|
|
840
|
-
: {}),
|
|
841
|
-
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
842
709
|
});
|
|
843
|
-
result.isError = true;
|
|
844
|
-
return result;
|
|
845
710
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
retryable: false,
|
|
852
|
-
},
|
|
853
|
-
...discardedEmits(emitted),
|
|
854
|
-
diagnostics: diagnostics.finish(),
|
|
855
|
-
});
|
|
856
|
-
result.isError = true;
|
|
857
|
-
return result;
|
|
858
|
-
}
|
|
859
|
-
return errorResult(`Executor failed: ${msg(err)}${discardedEmitsText(emitted)}`);
|
|
711
|
+
return failureResponse(`Executor failed: ${msg(err)}`, {
|
|
712
|
+
emitted,
|
|
713
|
+
diagnostics,
|
|
714
|
+
code: "executor_failed",
|
|
715
|
+
});
|
|
860
716
|
}
|
|
861
717
|
finally {
|
|
862
718
|
// A sandbox timeout or early return must also release any outstanding
|
|
@@ -900,31 +756,20 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
900
756
|
// framed with bounded caller text (`boundedEchoText`) precisely so
|
|
901
757
|
// this path never needs one. Adding a cap here instead would leave the
|
|
902
758
|
// top-level surfaces, which have the same amplification, uncovered.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
759
|
+
return failureResponse(invocationFailure.details.message, {
|
|
760
|
+
logs,
|
|
761
|
+
emitted,
|
|
762
|
+
diagnostics,
|
|
763
|
+
code: invocationFailure.details,
|
|
908
764
|
});
|
|
909
|
-
result.isError = true;
|
|
910
|
-
return result;
|
|
911
765
|
}
|
|
912
766
|
const message = `Error: ${outcome.error}`;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
},
|
|
920
|
-
...(logs ? { logs } : {}),
|
|
921
|
-
...discardedEmits(emitted),
|
|
922
|
-
diagnostics: diagnostics.finish(),
|
|
923
|
-
});
|
|
924
|
-
result.isError = true;
|
|
925
|
-
return result;
|
|
926
|
-
}
|
|
927
|
-
return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`);
|
|
767
|
+
return failureResponse(message, {
|
|
768
|
+
logs,
|
|
769
|
+
emitted,
|
|
770
|
+
diagnostics,
|
|
771
|
+
code: "executor_failed",
|
|
772
|
+
});
|
|
928
773
|
}
|
|
929
774
|
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
930
775
|
// value) makes JSON.stringify throw — keep that inside the structured
|
|
@@ -935,21 +780,12 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
935
780
|
}
|
|
936
781
|
catch (err) {
|
|
937
782
|
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
},
|
|
945
|
-
...(logs ? { logs } : {}),
|
|
946
|
-
...discardedEmits(emitted),
|
|
947
|
-
diagnostics: diagnostics.finish(),
|
|
948
|
-
});
|
|
949
|
-
response.isError = true;
|
|
950
|
-
return response;
|
|
951
|
-
}
|
|
952
|
-
return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${discardedEmitsText(emitted)}`);
|
|
783
|
+
return failureResponse(message, {
|
|
784
|
+
logs,
|
|
785
|
+
emitted,
|
|
786
|
+
diagnostics,
|
|
787
|
+
code: "executor_failed",
|
|
788
|
+
});
|
|
953
789
|
}
|
|
954
790
|
const response = jsonResult({
|
|
955
791
|
result,
|
|
@@ -976,6 +812,22 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
976
812
|
return response;
|
|
977
813
|
};
|
|
978
814
|
}
|
|
815
|
+
function failureResponse(message, options) {
|
|
816
|
+
const { logs, emitted, diagnostics, code } = options;
|
|
817
|
+
if (diagnostics || typeof code !== "string") {
|
|
818
|
+
const result = jsonResult({
|
|
819
|
+
error: typeof code === "string"
|
|
820
|
+
? { code, message, retryable: false }
|
|
821
|
+
: code,
|
|
822
|
+
...(logs ? { logs } : {}),
|
|
823
|
+
...(emitted ? discardedEmits(emitted) : {}),
|
|
824
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
825
|
+
});
|
|
826
|
+
result.isError = true;
|
|
827
|
+
return result;
|
|
828
|
+
}
|
|
829
|
+
return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}${emitted ? discardedEmitsText(emitted) : ""}`);
|
|
830
|
+
}
|
|
979
831
|
/**
|
|
980
832
|
* M4 and U3: a failed program delivers no blocks and no view, but each
|
|
981
833
|
* discard is visible — and one failure can discard both.
|
|
@@ -1035,7 +887,7 @@ Write one plain-JavaScript async arrow function. Use only:
|
|
|
1035
887
|
- <connectorId>.<toolName>(args) for a sanitized shortcut, or connecta.call(address, args) for a canonical address.
|
|
1036
888
|
- connecta.search(args), connecta.describe(args), and connecta.batch(calls) for discovery and independent read-only calls.
|
|
1037
889
|
- 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
|
|
890
|
+
- connecta.ui(html) for one display-only, success-only view; return the same summary the HTML renders.
|
|
1039
891
|
- console.log(...) — captured.
|
|
1040
892
|
|
|
1041
893
|
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.`;
|
|
@@ -1049,15 +901,11 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1049
901
|
};
|
|
1050
902
|
const connectors = registry.listConnectors();
|
|
1051
903
|
const handler = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
: {}),
|
|
1055
|
-
...(ctx.probeTimeoutMs !== undefined
|
|
1056
|
-
? { probeTimeoutMs: ctx.probeTimeoutMs }
|
|
1057
|
-
: {}),
|
|
904
|
+
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
905
|
+
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1058
906
|
maxEmittedBytes: emitBudgets.maxBytes,
|
|
1059
907
|
maxEmittedBlocks: emitBudgets.maxBlocks,
|
|
1060
|
-
|
|
908
|
+
defer: ctx.defer,
|
|
1061
909
|
});
|
|
1062
910
|
server.registerTool("execute_code", {
|
|
1063
911
|
description: executeDescription(emitBudgets, hasConnectorGuides(connectors), connectors),
|
|
@@ -1078,12 +926,8 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1078
926
|
destructiveHint: false,
|
|
1079
927
|
openWorldHint: true,
|
|
1080
928
|
},
|
|
1081
|
-
// U5 and U10
|
|
1082
|
-
//
|
|
1083
|
-
// is the text fallback the spec mandates — and a stateless aggregator
|
|
1084
|
-
// has nowhere dependable to hold a negotiation check anyway. The
|
|
1085
|
-
// explicit visibility keeps hosts from being told the view may call
|
|
1086
|
-
// execute_code; the default ["model","app"] would say exactly that.
|
|
929
|
+
// U5 and U10 are specified in documentation/code-mode.md; explicit model
|
|
930
|
+
// visibility prevents hosts from offering execute_code to the view.
|
|
1087
931
|
_meta: {
|
|
1088
932
|
ui: {
|
|
1089
933
|
resourceUri: PROGRAM_UI_RESOURCE_URI,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExecuteResult } from "./types.js";
|
|
2
2
|
export declare const MAX_EXECUTE_LOG_CHARS = 4000;
|
|
3
|
+
export declare function serializeResultText(value: unknown): string;
|
|
3
4
|
export declare function guardExecuteResultValue(value: unknown): unknown;
|
|
4
5
|
export declare function truncateExecuteText(text: string, max: number): string;
|
|
5
6
|
/**
|
package/dist/executor-result.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { msg } from "./errors.js";
|
|
1
2
|
/** ~6k tokens. Sandbox code should filter data down before returning. */
|
|
2
3
|
const MAX_EXECUTE_RESULT_CHARS = 24_000;
|
|
3
4
|
export const MAX_EXECUTE_LOG_CHARS = 4_000;
|
|
4
|
-
function
|
|
5
|
-
return err instanceof Error ? err.message : String(err);
|
|
6
|
-
}
|
|
7
|
-
function serializeExecuteValue(value) {
|
|
5
|
+
export function serializeResultText(value) {
|
|
8
6
|
const serialized = JSON.stringify(value);
|
|
9
7
|
return serialized === undefined ? String(value) : serialized;
|
|
10
8
|
}
|
|
@@ -37,15 +35,10 @@ function truncationEnvelope(text) {
|
|
|
37
35
|
// the overshoot ratio (minus a step) strictly shrinks the budget.
|
|
38
36
|
budget = Math.max(0, Math.floor(budget * (MAX_EXECUTE_RESULT_CHARS / size)) - 8);
|
|
39
37
|
}
|
|
40
|
-
|
|
41
|
-
// unchecked slice is exactly how a "bounded" envelope stops being bounded.
|
|
42
|
-
const clamped = { ...base, preview: text.slice(0, Math.max(0, budget)) };
|
|
43
|
-
return JSON.stringify(clamped).length <= MAX_EXECUTE_RESULT_CHARS
|
|
44
|
-
? clamped
|
|
45
|
-
: { ...base, preview: "" };
|
|
38
|
+
return { ...base, preview: text.slice(0, budget) };
|
|
46
39
|
}
|
|
47
40
|
export function guardExecuteResultValue(value) {
|
|
48
|
-
const text =
|
|
41
|
+
const text = serializeResultText(value);
|
|
49
42
|
if (text.length <= MAX_EXECUTE_RESULT_CHARS)
|
|
50
43
|
return value;
|
|
51
44
|
return truncationEnvelope(text);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { prepareExecuteResultForTransport } from "../executor-result.js";
|
|
2
|
+
import { msg } from "../errors.js";
|
|
2
3
|
import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
|
|
3
4
|
import { executeQuickJs, prepareQuickJs } from "./quickjs-runtime.js";
|
|
4
5
|
let activeJobId;
|
|
5
6
|
let nextCallId = 1;
|
|
6
7
|
const pending = new Map();
|
|
7
|
-
function msg(err) {
|
|
8
|
-
return err instanceof Error ? err.message : String(err);
|
|
9
|
-
}
|
|
10
8
|
function send(message) {
|
|
11
9
|
if (!process.send)
|
|
12
10
|
throw new Error("QuickJS child IPC channel is unavailable.");
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// continuations from a host-side loop. Values cross the boundary as JSON, so
|
|
9
9
|
// provider args/results must be JSON-serializable.
|
|
10
10
|
import { getQuickJS, } from "quickjs-emscripten";
|
|
11
|
+
import { msg } from "../errors.js";
|
|
11
12
|
import { hostCallLabel, MAX_QUICKJS_LOG_TRANSPORT_BYTES, serializedBytes, } from "./quickjs-protocol.js";
|
|
12
13
|
/** Load and compile the shared QuickJS WASM module before a run budget starts. */
|
|
13
14
|
export async function prepareQuickJs() {
|
|
@@ -29,9 +30,6 @@ const MAX_LOG_MARKER_TRANSPORT_BYTES = Math.max(logTransportBytes(LOG_ENTRY_LIMI
|
|
|
29
30
|
// This still lets guest code reduce data more than ten times larger than
|
|
30
31
|
// connecta's final response budget.
|
|
31
32
|
const MAX_HOST_RESULT_BYTES = 256 * 1024;
|
|
32
|
-
function msg(err) {
|
|
33
|
-
return err instanceof Error ? err.message : String(err);
|
|
34
|
-
}
|
|
35
33
|
function logTransportBytes(entry) {
|
|
36
34
|
// The log is encoded into ExecutionPayload, then that payloadJson string is
|
|
37
35
|
// encoded into ChildToParentMessage. Measure the units the IPC cap sees.
|
|
@@ -6,6 +6,7 @@ import { fork } from "node:child_process";
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { AdmissionController, ExecutorAdmissionError, ExecutorExecutionError, } from "../executor-admission.js";
|
|
9
|
+
import { msg } from "../errors.js";
|
|
9
10
|
import { hostCallLabel, MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
|
|
10
11
|
export { normalizeCode } from "./quickjs-runtime.js";
|
|
11
12
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -19,9 +20,6 @@ const CHILD_EXIT_GRACE_MS = 250;
|
|
|
19
20
|
const CHILD_STARTUP_TIMEOUT_MS = 10_000;
|
|
20
21
|
const MAX_CHILD_STDERR_BYTES = 8 * 1024;
|
|
21
22
|
const MAX_ERROR_CHARS = 4_000;
|
|
22
|
-
function msg(err) {
|
|
23
|
-
return err instanceof Error ? err.message : String(err);
|
|
24
|
-
}
|
|
25
23
|
function retainStderrTail(current, chunk) {
|
|
26
24
|
const incoming = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
27
25
|
if (incoming.length >= MAX_CHILD_STDERR_BYTES) {
|