@zackbart/connecta 0.22.3 → 0.24.0

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