@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +131 -4
  2. package/dist/apps-shell.d.ts +10 -12
  3. package/dist/apps-shell.js +29 -220
  4. package/dist/catalog-service.d.ts +16 -13
  5. package/dist/catalog-service.js +106 -115
  6. package/dist/catalog.js +29 -46
  7. package/dist/connector-scope.js +2 -7
  8. package/dist/connectors/api.d.ts +4 -16
  9. package/dist/connectors/api.js +19 -46
  10. package/dist/connectors/guarded-fetch.d.ts +9 -23
  11. package/dist/connectors/guarded-fetch.js +38 -76
  12. package/dist/connectors/remote-mcp.js +36 -79
  13. package/dist/errors.d.ts +6 -27
  14. package/dist/errors.js +8 -5
  15. package/dist/execute.d.ts +23 -28
  16. package/dist/execute.js +101 -257
  17. package/dist/executor-result.d.ts +1 -0
  18. package/dist/executor-result.js +4 -11
  19. package/dist/executors/quickjs-child.js +1 -3
  20. package/dist/executors/quickjs-runtime.js +1 -3
  21. package/dist/executors/quickjs.js +1 -3
  22. package/dist/index.js +134 -123
  23. package/dist/invocation.d.ts +1 -1
  24. package/dist/invocation.js +113 -183
  25. package/dist/meta-tools.d.ts +15 -29
  26. package/dist/meta-tools.js +41 -582
  27. package/dist/operator-ui/generated.d.ts +2 -2
  28. package/dist/providers/cloudflare.d.ts +2 -18
  29. package/dist/providers/cloudflare.js +1460 -2451
  30. package/dist/providers/linear.d.ts +4 -41
  31. package/dist/providers/linear.js +8 -39
  32. package/dist/providers/mixpanel.d.ts +3 -25
  33. package/dist/providers/mixpanel.js +7 -22
  34. package/dist/providers/notion.d.ts +1 -15
  35. package/dist/providers/notion.js +44 -173
  36. package/dist/providers/revenuecat.d.ts +4 -57
  37. package/dist/providers/revenuecat.js +10 -93
  38. package/dist/providers/stripe.d.ts +1 -12
  39. package/dist/providers/stripe.js +7 -45
  40. package/dist/registry.d.ts +9 -34
  41. package/dist/registry.js +9 -103
  42. package/dist/routes/mcp.js +1 -1
  43. package/dist/routes/oauth.js +3 -3
  44. package/dist/routes/shared.d.ts +15 -15
  45. package/dist/routes/shared.js +1 -3
  46. package/dist/skills.d.ts +1 -1
  47. package/dist/skills.js +5 -5
  48. package/dist/timeout.d.ts +8 -7
  49. package/dist/timeout.js +47 -38
  50. package/dist/types.d.ts +3 -3
  51. package/dist/ui.d.ts +1 -25
  52. package/dist/ui.js +18 -45
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/documentation/call-admission.md +1 -1
  56. package/documentation/cloudflare.md +1 -1
  57. package/documentation/code-mode.md +25 -25
  58. package/documentation/connectors.md +24 -1
  59. package/documentation/linear.md +1 -1
  60. package/documentation/meta-tools.md +4 -30
  61. package/documentation/mixpanel.md +1 -1
  62. package/documentation/notion.md +1 -1
  63. package/documentation/operations.md +29 -25
  64. package/documentation/provider-conventions.md +4 -5
  65. package/documentation/revenuecat.md +1 -1
  66. package/documentation/stripe.md +1 -1
  67. package/documentation/upgrading.md +43 -8
  68. package/ethos.md +75 -121
  69. package/package.json +3 -4
  70. package/templates/node/package.json +1 -1
  71. package/documentation/code-first-exploration.md +0 -292
  72. package/documentation/mcp-2026-07-28.md +0 -46
  73. package/documentation/mcp-ui-design.md +0 -382
  74. package/documentation/program-ui-read-calls.md +0 -213
  75. package/documentation/provider-audit.md +0 -198
  76. 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 HTML argument and, optionally, one read-binding options object";
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/V1 validation. The first argument remains HTML; the only second
172
- * argument is one read-binding manifest. There are no alternate object or MCP
173
- * block forms.
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 && values.length !== 2) {
177
+ if (values.length !== 1) {
244
178
  throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${values.length} arguments`);
245
179
  }
246
- const html = requireUiHtml(values[0]);
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
- if (this.ui) {
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
- acceptUiPayload(payload) {
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
- ...(limits.discoveryConcurrency !== undefined
519
- ? { concurrency: limits.discoveryConcurrency }
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 callAddress = async (address, args, diagnosticOperation = "call") => {
577
- const outcome = await invocation.invoke(String(address), args ?? {}, invocationContext());
578
- limits.diagnostics?.recordCall(diagnosticOperation, outcome);
579
- if (!outcome.ok) {
580
- const failure = new InvocationFailure(outcome.error);
581
- throw failure;
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
- return outcome.value;
584
- };
585
- const callNamespace = async (connectorId, toolAlias, args) => {
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
- * A read binding is admitted while the program still owns the request. The
596
- * shell later calls the ordinary `call_tool`, which repeats this same
597
- * fail-closed check against the then-current catalog; validating here keeps
598
- * a typo or destructive address from producing a view whose controls can
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
- const payload = await validateUiReads(requireUiPayload(values));
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 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
- },
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
- ...(config.discoveryConcurrency !== undefined
794
- ? { discoveryConcurrency: config.discoveryConcurrency }
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
- const result = jsonResult({
830
- error: {
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
- if (diagnostics) {
847
- const result = jsonResult({
848
- error: {
849
- code: "executor_failed",
850
- message: `Executor failed: ${msg(err)}`,
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
- const result = jsonResult({
904
- error: invocationFailure.details,
905
- ...(logs ? { logs } : {}),
906
- ...discardedEmits(emitted),
907
- ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
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
- if (diagnostics) {
914
- const result = jsonResult({
915
- error: {
916
- code: "executor_failed",
917
- message,
918
- retryable: false,
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
- if (diagnostics) {
939
- const response = jsonResult({
940
- error: {
941
- code: "executor_failed",
942
- message,
943
- retryable: false,
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, options?) for one success-only view; return the same initial summary the HTML renders.
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
- ...(ctx.discoveryConcurrency !== undefined
1053
- ? { discoveryConcurrency: ctx.discoveryConcurrency }
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
- ...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
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: declared unconditionally. A host without the Apps
1082
- // extension ignores unknown _meta and sees the ordinary envelope, which
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
  /**
@@ -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 msg(err) {
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
- // The loop shrinks monotonically, so this is unreachable in practice — but an
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 = serializeExecuteValue(value);
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) {