@zackbart/connecta 0.37.0 → 0.38.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 (183) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +2 -2
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/auth/clerk-sdk/generated.js +2 -1
  8. package/dist/auth/cloudflare-access.d.ts +3 -1
  9. package/dist/auth/cloudflare-access.js +22 -11
  10. package/dist/auth/downstream-oauth.js +195 -270
  11. package/dist/auth/downstream-revocation.js +1 -1
  12. package/dist/auth/oauth-grant.js +11 -0
  13. package/dist/auth/oauth-token-response.js +1 -1
  14. package/dist/auth/vercel-agent.js +2 -1
  15. package/dist/auth-elicitation.js +2 -2
  16. package/dist/branding.d.ts +1 -15
  17. package/dist/branding.js +2 -2
  18. package/dist/bytes.js +42 -0
  19. package/dist/call-admission-context.js +2 -5
  20. package/dist/call-admission.js +5 -0
  21. package/dist/call-recovery.js +0 -3
  22. package/dist/catalog-cache.js +26 -7
  23. package/dist/catalog-fingerprint.js +2 -18
  24. package/dist/catalog-revision.js +9 -0
  25. package/dist/catalog-service.js +51 -20
  26. package/dist/config-defaults.d.ts +1 -0
  27. package/dist/config-defaults.js +1 -0
  28. package/dist/config-schema.d.ts +1 -1
  29. package/dist/config-schema.js +6 -2
  30. package/dist/config.d.ts +41 -14
  31. package/dist/config.js +21 -71
  32. package/dist/connector-caller.js +1 -1
  33. package/dist/connectors/api-connector.js +4 -3
  34. package/dist/connectors/guarded-fetch.d.ts +0 -6
  35. package/dist/connectors/guarded-fetch.js +1 -11
  36. package/dist/connectors/remote-mcp.js +93 -69
  37. package/dist/connectors/resource-uri.js +0 -4
  38. package/dist/credentials.js +3 -0
  39. package/dist/d1.d.ts +0 -89
  40. package/dist/d1.js +0 -34
  41. package/dist/downstream-input.js +2 -2
  42. package/dist/execute.js +161 -116
  43. package/dist/executor-result.js +2 -1
  44. package/dist/executors/quickjs-runtime.js +2 -25
  45. package/dist/executors/quickjs.js +1 -1
  46. package/dist/guest-runtime.js +5 -3
  47. package/dist/guest-types.d.ts +5 -5
  48. package/dist/http/client.js +1 -1
  49. package/dist/http/errors.js +2 -2
  50. package/dist/http/json-schema.js +1 -1
  51. package/dist/http/node-transport.js +0 -1
  52. package/dist/http/oauth.js +1 -1
  53. package/dist/http/paging.js +6 -9
  54. package/dist/http/provider.js +1 -1
  55. package/dist/http/safety.js +1 -1
  56. package/dist/http/services.js +1 -8
  57. package/dist/http/tools.js +1 -7
  58. package/dist/http/vendor.js +7 -7
  59. package/dist/identity.js +2 -6
  60. package/dist/index.js +1 -2
  61. package/dist/invocation-auth.js +2 -1
  62. package/dist/invocation.js +8 -2
  63. package/dist/meta-tools.js +27 -27
  64. package/dist/native-tools.js +2 -1
  65. package/dist/oauth-handoff.js +2 -2
  66. package/dist/operation-vocabulary.js +1 -1
  67. package/dist/operator-record.d.ts +8 -2
  68. package/dist/operator-record.js +18 -50
  69. package/dist/operator-ui/contract.d.ts +4 -2
  70. package/dist/operator-ui/generated.js +3 -3
  71. package/dist/operator-ui/model.d.ts +7 -2
  72. package/dist/program-writes.js +3 -8
  73. package/dist/provider-call.js +0 -7
  74. package/dist/provider.d.ts +1 -1
  75. package/dist/providers/_shared/google/kit.js +1 -6
  76. package/dist/providers/_shared/google/service-account.js +1 -3
  77. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  78. package/dist/providers/_shared/guide.js +3 -0
  79. package/dist/providers/_shared/json.js +12 -0
  80. package/dist/providers/_shared/rest/compact-index.js +1 -1
  81. package/dist/providers/_shared/rest/kit.js +1 -3
  82. package/dist/providers/_shared/rest/operation-index.js +1 -3
  83. package/dist/providers/_shared/rest/value-safety.js +26 -28
  84. package/dist/providers/breeze/api.js +2 -7
  85. package/dist/providers/breeze/index.js +4 -2
  86. package/dist/providers/breeze/tools.js +3 -4
  87. package/dist/providers/ccb/api.js +1 -6
  88. package/dist/providers/ccb/index.js +2 -1
  89. package/dist/providers/ccb/tools.js +1 -6
  90. package/dist/providers/cloudflare/api.js +2 -2
  91. package/dist/providers/cloudflare/index.js +2 -1
  92. package/dist/providers/cloudflare/safety.js +3 -3
  93. package/dist/providers/docs/tools.js +2 -10
  94. package/dist/providers/drive/tools.js +12 -25
  95. package/dist/providers/forms/tools.js +1 -9
  96. package/dist/providers/gmail/tools.js +5 -21
  97. package/dist/providers/overflow/api.js +1 -3
  98. package/dist/providers/overflow/index.js +2 -1
  99. package/dist/providers/overflow/tools.js +3 -7
  100. package/dist/providers/planning-center/api.js +2 -1
  101. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  102. package/dist/providers/planning-center/lib/json-api.js +1 -9
  103. package/dist/providers/planning-center/tools.js +2 -1
  104. package/dist/providers/sheets/tools.js +1 -9
  105. package/dist/providers/slides/tools.js +4 -13
  106. package/dist/providers/stripe/index.js +2 -1
  107. package/dist/providers/stripe/safety.js +2 -2
  108. package/dist/providers/tithely/api.js +2 -7
  109. package/dist/providers/tithely/index.js +3 -2
  110. package/dist/providers/tithely/tools.js +7 -5
  111. package/dist/providers/vercel/api.js +1 -6
  112. package/dist/providers/vercel/lib/pin.js +1 -1
  113. package/dist/providers/vercel/safety.js +1 -1
  114. package/dist/providers/vercel/tools.js +2 -1
  115. package/dist/redaction-marker.js +2 -2
  116. package/dist/registry.d.ts +4 -1
  117. package/dist/registry.js +35 -22
  118. package/dist/request-principal.js +15 -2
  119. package/dist/request-state.js +2 -2
  120. package/dist/routes/access-tokens.js +1 -1
  121. package/dist/routes/activity-disclosure.js +1 -1
  122. package/dist/routes/connect.js +2 -2
  123. package/dist/routes/credentials.js +1 -1
  124. package/dist/routes/dispatch.js +10 -0
  125. package/dist/routes/mcp.js +23 -21
  126. package/dist/routes/oauth-management.js +1 -1
  127. package/dist/routes/operator.js +53 -7
  128. package/dist/routes/shared.d.ts +8 -8
  129. package/dist/routes/shared.js +22 -15
  130. package/dist/routes/ui-config.js +6 -6
  131. package/dist/routes/ui-data.js +2 -2
  132. package/dist/routes/ui.js +9 -4
  133. package/dist/runtime/call-admission.js +1 -1
  134. package/dist/runtime/call.js +1 -1
  135. package/dist/runtime/settlement.js +1 -1
  136. package/dist/runtime/shared-read.js +2 -15
  137. package/dist/sent-secrets.d.ts +14 -6
  138. package/dist/sent-secrets.js +231 -85
  139. package/dist/server.js +4 -4
  140. package/dist/skills.js +8 -35
  141. package/dist/sqlite.d.ts +0 -19
  142. package/dist/sqlite.js +2 -83
  143. package/dist/storage/keys.js +29 -124
  144. package/dist/storage/sql-native.js +14 -111
  145. package/dist/storage/sql.d.ts +1 -24
  146. package/dist/storage/sql.js +1 -12
  147. package/dist/tool-error.js +4 -0
  148. package/dist/types.d.ts +4 -3
  149. package/dist/ui.js +3 -0
  150. package/dist/url-safety.js +0 -4
  151. package/dist/usage-guide.js +2 -2
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/dist/worker.js +1 -1
  155. package/documentation/README.md +6 -42
  156. package/documentation/architecture.md +190 -507
  157. package/documentation/auth.md +172 -58
  158. package/documentation/code-mode.md +98 -66
  159. package/documentation/deploying.md +19 -40
  160. package/documentation/integrating.md +8 -19
  161. package/documentation/meta-tools.md +23 -6
  162. package/documentation/migration-0.33.md +5 -5
  163. package/documentation/migration-0.34.md +2 -4
  164. package/documentation/migration-retired-layouts.md +74 -0
  165. package/documentation/operator-tests.md +19 -119
  166. package/documentation/operator-ui.md +46 -8
  167. package/documentation/provider-auth-migration.md +4 -10
  168. package/documentation/provider-migration-0.29.md +4 -15
  169. package/examples/worker/AGENTS.md +4 -6
  170. package/examples/worker/README.md +20 -115
  171. package/guides/connecting-agents.md +7 -30
  172. package/guides/how-it-works.md +2 -2
  173. package/guides/safety-and-trust.md +4 -1
  174. package/package.json +19 -19
  175. package/templates/node/README.md +9 -26
  176. package/templates/node/package.json +1 -1
  177. package/dist/auth/oauth-v2-migration.js +0 -135
  178. package/dist/provider-call-facts.d.ts +0 -115
  179. package/dist/provider-call.d.ts +0 -112
  180. package/dist/storage/d1-copy.js +0 -255
  181. package/dist/storage/sql-schema.d.ts +0 -6
  182. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  183. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/d1.d.ts CHANGED
@@ -38,93 +38,4 @@ export declare function d1Storage(db: D1DatabaseBinding): KVStorage;
38
38
  * write prunes a bounded batch of rows older than `retentionDays`.
39
39
  */
40
40
  export declare function d1ActivityStore(db: D1DatabaseBinding, options?: D1ActivityOptions): ActivityStore;
41
- /**
42
- * The part of a Workers KV binding `copyKvToD1` reads. Structural, like
43
- * `D1DatabaseBinding`; a Worker's `KVNamespace` satisfies it.
44
- */
45
- export interface KVNamespaceBinding {
46
- list(options?: {
47
- limit?: number;
48
- cursor?: string | null;
49
- }): Promise<{
50
- keys: readonly {
51
- name: string;
52
- expiration?: number;
53
- }[];
54
- list_complete: boolean;
55
- cursor?: string;
56
- }>;
57
- get(key: string, type: "text"): Promise<string | null>;
58
- }
59
- export interface KvToD1CopyOptions {
60
- /** The source KV namespace id. Resume tokens are bound to this identity. */
61
- source: string;
62
- /** Resume against the same source and database within seven days. */
63
- cursor?: string;
64
- /** Explicit families whose conflicting D1 rows the operator judged stale. */
65
- overwriteFamilies?: readonly string[];
66
- /** Compare value hashes and absolute expiries without copying entries. */
67
- verify?: boolean;
68
- /** Override the cutover guard only after assessing stale KV state. */
69
- allowStale?: boolean;
70
- /** Keys read per call. Default 500; use a smaller budget for large rows. */
71
- maxKeys?: number;
72
- }
73
- /** What a copy did with one family's keys. */
74
- export interface KvToD1Counts {
75
- /** Written where D1 held no live entry. */
76
- copied: number;
77
- /** D1 already held the same value. */
78
- unchanged: number;
79
- /** D1 held a different value or expiry and kept it. */
80
- conflicts: number;
81
- /** D1 held a different value or expiry, replaced under `overwriteFamilies`. */
82
- overwritten: number;
83
- /** Expired or deleted in Workers KV before it could be read. */
84
- expired: number;
85
- /** A key/value/row D1 cannot hold, left in Workers KV. */
86
- invalid: number;
87
- /** Exact value hash and expiry match during a verification pass. */
88
- verified: number;
89
- /** Missing or different value/expiry during a verification pass. */
90
- mismatches: number;
91
- }
92
- export interface KvToD1CopyResult {
93
- /** True once the whole namespace has been read. */
94
- done: boolean;
95
- /**
96
- * Pass back as `options.cursor` to continue; absent when `done`. A random
97
- * token: the Workers KV cursor it stands for stays in D1, because that
98
- * cursor can spell the last key listed.
99
- */
100
- cursor?: string;
101
- /**
102
- * Counts by key family (`src/storage/keys.ts`), plus `connector-owned` and
103
- * `unclassified`. Never a key or a value.
104
- */
105
- families: Record<string, KvToD1Counts>;
106
- }
107
- /**
108
- * A copy that stopped. The message is fixed wording that names a family at
109
- * most, never a key or value, and the binding's own error is not attached:
110
- * KV and D1 errors can quote either. Entries before `cursor` are copied;
111
- * rerunning from it is safe while the maintenance window still holds.
112
- */
113
- export declare class KvToD1CopyError extends Error {
114
- /** Resume point; absent means start over, which is also safe. */
115
- readonly cursor: string | undefined;
116
- readonly name = "KvToD1CopyError";
117
- constructor(message: string,
118
- /** Resume point; absent means start over, which is also safe. */
119
- cursor: string | undefined);
120
- }
121
- /** Mark cutover before reopening traffic. The marker stays in D1 permanently. */
122
- export declare function markKvToD1Live(db: D1DatabaseBinding, source: string): Promise<void>;
123
- /**
124
- * Copy live KV entries verbatim while all writers and traffic are stopped.
125
- * A live D1 row is kept unless its family is explicitly allowed to overwrite.
126
- * `verify` reads hashes and absolute expiries without changing copied entries.
127
- * Results contain counts only. Resume tokens are source-bound atomic claims.
128
- */
129
- export declare function copyKvToD1(kv: KVNamespaceBinding, db: D1DatabaseBinding, options: KvToD1CopyOptions): Promise<KvToD1CopyResult>;
130
41
  export {};
package/dist/d1.js CHANGED
@@ -3,12 +3,8 @@
3
3
  // One D1 database holds everything: `d1Storage(db)` is the deployment's
4
4
  // `KVStorage`, and `d1ActivityStore(db)` its activity history. Both create
5
5
  // their tables on first use, so binding the database is the whole setup.
6
- // `copyKvToD1` moves a 0.28 deployment's Workers KV state into it once.
7
- import { runEdge } from "./runtime/run.js";
8
6
  import { d1Driver } from "./storage/d1-driver.js";
9
- import { copyKvToD1Effect, markKvToD1LiveEffect } from "./storage/d1-copy.js";
10
7
  import { sqlActivityStore, sqlStorage } from "./storage/sql.js";
11
- import { keepErrorName } from "./redaction-identity.js";
12
8
  /**
13
9
  * `KVStorage` over a D1 database, with the atomic `compareAndSet` every
14
10
  * subsystem relies on. The `connecta_kv` table is created on first use; a
@@ -25,33 +21,3 @@ export function d1Storage(db) {
25
21
  export function d1ActivityStore(db, options) {
26
22
  return sqlActivityStore(d1Driver(db), "d1", options);
27
23
  }
28
- /**
29
- * A copy that stopped. The message is fixed wording that names a family at
30
- * most, never a key or value, and the binding's own error is not attached:
31
- * KV and D1 errors can quote either. Entries before `cursor` are copied;
32
- * rerunning from it is safe while the maintenance window still holds.
33
- */
34
- export class KvToD1CopyError extends Error {
35
- cursor;
36
- name = "KvToD1CopyError";
37
- constructor(message,
38
- /** Resume point; absent means start over, which is also safe. */
39
- cursor) {
40
- super(message);
41
- this.cursor = cursor;
42
- }
43
- }
44
- keepErrorName(KvToD1CopyError, "KvToD1CopyError");
45
- /** Mark cutover before reopening traffic. The marker stays in D1 permanently. */
46
- export function markKvToD1Live(db, source) {
47
- return runEdge(markKvToD1LiveEffect(db, source));
48
- }
49
- /**
50
- * Copy live KV entries verbatim while all writers and traffic are stopped.
51
- * A live D1 row is kept unless its family is explicitly allowed to overwrite.
52
- * `verify` reads hashes and absolute expiries without changing copied entries.
53
- * Results contain counts only. Resume tokens are source-bound atomic claims.
54
- */
55
- export function copyKvToD1(kv, db, options) {
56
- return runEdge(copyKvToD1Effect(kv, db, options));
57
- }
@@ -1,3 +1,4 @@
1
+ import { toolError } from "./tool-error.js";
1
2
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
2
3
  import { Effect } from "effect";
3
4
  import { upwardStep } from "./upward.js";
@@ -129,8 +130,7 @@ function failure(code, message, write = false) {
129
130
  writeOutcome: "unknown",
130
131
  }
131
132
  : { code, message, retryable: false };
132
- const structuredContent = { ok: false, error };
133
- return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
133
+ return toolError(error, { ok: false });
134
134
  }
135
135
  /** MCP-only continuation coordination. No downstream prompt enters operator records. */
136
136
  export class DownstreamElicitation {
package/dist/execute.js CHANGED
@@ -1,4 +1,4 @@
1
- import { authCall, authInvoke, authMethod } from "./auth/native.js";
1
+ import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
2
2
  import { nativeRecovery } from "./native-recovery.js";
3
3
  import { GUEST_API_DECLARATION } from "./usage-guide.js";
4
4
  import { bindMcpClient } from "./mcp-client-context.js";
@@ -18,10 +18,11 @@ import { guestPrelude, programError, wrapGuestProgram } from "./guest-runtime.js
18
18
  import { EXECUTE_OUTPUT } from "./meta-output.js";
19
19
  import { normalizeProgramSource } from "./program-source.js";
20
20
  import { readResource } from "./resource-read.js";
21
- import { agentOutputOperations, redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
21
+ import { redactAgentOutput, sentSecretsForRequest } from "./sent-secrets.js";
22
22
  import { normalizeTimeoutMs } from "./timeout.js";
23
23
  import { fromSignal, runEdge, withDeadlineEffect } from "./runtime/run.js";
24
24
  import { closeScope } from "./runtime/connector-scope.js";
25
+ import { RequestScope, requestOwner } from "./runtime/request-scope.js";
25
26
  import { executorProgram } from "./runtime/executor.js";
26
27
  import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, SkillsRegistry, } from "./skills.js";
27
28
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
@@ -174,13 +175,15 @@ export class EmitCollector {
174
175
  maxBytes;
175
176
  maxBlocks;
176
177
  diagnostics;
178
+ maxResultChars;
177
179
  blocks = [];
178
180
  bytes = 0;
179
181
  textChars = 0;
180
- constructor(maxBytes, maxBlocks, diagnostics) {
182
+ constructor(maxBytes, maxBlocks, diagnostics, maxResultChars = MAX_EXECUTE_RESULT_CHARS) {
181
183
  this.maxBytes = maxBytes;
182
184
  this.maxBlocks = maxBlocks;
183
185
  this.diagnostics = diagnostics;
186
+ this.maxResultChars = maxResultChars;
184
187
  }
185
188
  accept(raw) {
186
189
  this.acceptNative([requireEmittedBlock(raw)]);
@@ -204,8 +207,8 @@ export class EmitCollector {
204
207
  // the transport budget, just like images and audio.
205
208
  if (block.type === "resource" && "text" in block.resource)
206
209
  textChars += serialized.length;
207
- if (textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
208
- throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
210
+ if (textChars > this.maxResultChars - 512) {
211
+ throw guestFailure("result_too_large", `Emitted text shares the ${this.maxResultChars.toLocaleString("en-US")}-character program result limit. Reduce the text before emitting it.`);
209
212
  }
210
213
  count++;
211
214
  bytes += size;
@@ -469,8 +472,20 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
469
472
  ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
470
473
  unwrapResult: true,
471
474
  readOnly: limits.readOnly,
472
- processResult: async (value, resolved, secrets, format, raw) => {
473
- const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets, requestScope, wireBytes);
475
+ processResult: authMethod((value, resolved, secrets, format, raw) => Effect.gen(function* () {
476
+ // Dispatch succeeded before presentation began. Cancellation of paging
477
+ // cannot turn this completed write into an unknown or failed write.
478
+ sending.settle?.("ok");
479
+ const page = (data, valueFormat, wireBytes) => authCall(pageProgramResult, [
480
+ registry,
481
+ baseUrl,
482
+ resolved,
483
+ data,
484
+ valueFormat,
485
+ secrets,
486
+ requestScope,
487
+ wireBytes,
488
+ ]);
474
489
  const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
475
490
  ? raw
476
491
  : undefined;
@@ -485,12 +500,12 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
485
500
  // the structured value synchronously; no native metadata is visited.
486
501
  if (dataKey !== undefined &&
487
502
  diagnosticsEncoder.encode(JSON.stringify(value)).byteLength > PROGRAM_RESULT_INLINE_BYTES) {
488
- return page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
503
+ return yield* page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
489
504
  }
490
505
  const content = envelope?.content;
491
506
  if (!Array.isArray(content) ||
492
507
  !content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
493
- return transportResult(await page(value, format));
508
+ return transportResult(yield* page(value, format));
494
509
  }
495
510
  const unavailablePresentation = () => ({
496
511
  format: "paged",
@@ -516,7 +531,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
516
531
  catch (error) {
517
532
  if (!(error instanceof NativePresentationLimit))
518
533
  throw error;
519
- const result = dataKey === undefined ? undefined : transportResult(await page(value, format));
534
+ const result = dataKey === undefined ? undefined : transportResult(yield* page(value, format));
520
535
  const notice = unavailablePresentation();
521
536
  return result ? { value: result, native: { value: notice } } : notice;
522
537
  }
@@ -525,8 +540,9 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
525
540
  // original bytes even if the structured-data stash pauses.
526
541
  const resultPending = dataKey === undefined ? undefined : page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
527
542
  const nativePending = page(snapshot, "json");
528
- const result = await resultPending;
529
- const native = await nativePending;
543
+ const [result, native] = yield* Effect.all([resultPending ?? Effect.succeed(undefined), nativePending], {
544
+ concurrency: "unbounded",
545
+ });
530
546
  let valueResult;
531
547
  if (native.format === "paged") {
532
548
  valueResult = result ? transportResult(result) : undefined;
@@ -548,7 +564,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
548
564
  ...(valueResult ? { value: valueResult } : {}),
549
565
  native: { ref, value: transportResult(native), dataKey },
550
566
  };
551
- },
567
+ })),
552
568
  beforeWrite: (target) => Effect.sync(() => {
553
569
  if (programWrites?.isClosed) {
554
570
  return {
@@ -736,8 +752,10 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
736
752
  }),
737
753
  skill: (name, _options, utilitySignal) => Effect.tryPromise({
738
754
  try: async () => {
739
- if (typeof name !== "string")
740
- throw guestFailure("invalid_args", "Use connecta.skill(name) with an exact skill name.");
755
+ // Accept the top-level skills tool's { name } shape too: models copy it into programs.
756
+ const skillName = name && typeof name === "object" ? name.name : name;
757
+ if (typeof skillName !== "string")
758
+ throw guestFailure("invalid_args", 'Use connecta.skill("usage") or connecta.skill("connector:<id>") with an exact skill name; the skills tool lists names.');
741
759
  const skills = new SkillsRegistry(registry, baseUrl, {
742
760
  requestScope,
743
761
  requestSignal: utilitySignal ?? hostAccessSignal,
@@ -749,7 +767,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
749
767
  probeTimeoutMs: limits.probeTimeoutMs,
750
768
  defer: limits.defer,
751
769
  });
752
- return { name, format: "text", text: await skills.text(name) };
770
+ return { name: skillName, format: "text", text: await skills.text(skillName) };
753
771
  },
754
772
  catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
755
773
  }),
@@ -1030,12 +1048,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1030
1048
  logger,
1031
1049
  };
1032
1050
  const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
1033
- const play = (program, diagnosticsRequested, callerSignal, requestScope) => Effect.suspend(() => {
1051
+ const maxResultChars = resolveBudget(config.maxResultChars, MAX_EXECUTE_RESULT_CHARS);
1052
+ const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites) => Effect.suspend(() => {
1034
1053
  const sentSecrets = sentSecretsForRequest(requestScope);
1035
1054
  const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
1036
- const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
1037
- const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
1038
- const programWrites = new ProgramWrites();
1055
+ const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
1039
1056
  const resultDependencies = [];
1040
1057
  const dispatchController = new AbortController();
1041
1058
  const terminal = Deferred.makeUnsafe();
@@ -1116,21 +1133,34 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1116
1133
  }).pipe(Effect.map((outcome) => sentSecrets.redact(outcome)), Effect.mapError((error) => sentSecrets.redact(error))), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
1117
1134
  programWrites.close();
1118
1135
  dispatchController.abort();
1119
- return programWrites.drain();
1136
+ // Finalizers are masked by default. The request must be able
1137
+ // to stop this wait as well as the invocations being drained.
1138
+ return programWrites.drain().pipe(Effect.interruptible);
1120
1139
  }))));
1121
1140
  });
1122
1141
  const reported = {
1123
1142
  emitted,
1124
1143
  diagnostics,
1125
1144
  program,
1126
- guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets, logger, maxChars, programWrites.hasSucceeded),
1145
+ // Emitted text shares the return cap; a follow-up page run gets all of it.
1146
+ guardResult: (value) => authCall(guardProgramReturn, [
1147
+ value,
1148
+ registry,
1149
+ baseUrl,
1150
+ [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()],
1151
+ sentSecrets,
1152
+ logger,
1153
+ maxResultChars - emitted.textChars,
1154
+ programWrites.hasSucceeded,
1155
+ maxResultChars,
1156
+ ]),
1127
1157
  };
1128
1158
  const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
1129
1159
  // Releasing a QuickJS lease ends its child and rejects execute() with
1130
1160
  // the retained log prefix. Give that report one timer turn, just as
1131
1161
  // cancellation does in awaitExecutor; never wait on a Worker isolate.
1132
1162
  budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
1133
- return Effect.map(Effect.flatMap(exited, (exit) => {
1163
+ return Effect.flatMap(exited, (exit) => {
1134
1164
  // A synchronous fire-and-forget burst can also settle its executor in
1135
1165
  // this turn. The host's terminal refusal always wins over that value.
1136
1166
  if (budgetFailure instanceof HostCallBudgetExceeded) {
@@ -1145,36 +1175,47 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1145
1175
  if (Exit.isFailure(exit)) {
1146
1176
  return Effect.succeed(failedRun(redactAgentOutput(sentSecrets, Cause.squash(exit.cause)), logger, reported));
1147
1177
  }
1148
- return Effect.promise(() => finishedRun(redactAgentOutput(sentSecrets, exit.value), reported));
1149
- }), (unfinished) => {
1150
- // Every run response passes through write accounting.
1151
- const response = programWrites.finish(unfinished);
1152
- // Calls abandoned by the guest are failed when the run cancels them.
1153
- const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
1154
- response.structuredContent = { ...response.structuredContent, hostCalls: counts };
1155
- if (response.content[0]?.type === "text")
1156
- response.content[0].text = JSON.stringify(response.structuredContent);
1157
- return response;
1158
- }).pipe(Effect.map((result) => redactAgentOutput(sentSecrets, result)));
1178
+ // The executor lease is already released, but presenting its return
1179
+ // still belongs to the caller. Compose the registered stash program
1180
+ // here so cancellation interrupts storage waits and their continuations.
1181
+ const cancelled = new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during result processing.");
1182
+ return withDeadlineEffect(() => finishedRun(redactAgentOutput(sentSecrets, exit.value), reported), {
1183
+ ...(callerSignal ? { signal: callerSignal } : {}),
1184
+ timeoutError: cancelled,
1185
+ }).pipe(Effect.catch((error) => Effect.succeed(failedRun(callerSignal?.aborted ? cancelled : error, logger, reported))));
1186
+ });
1159
1187
  });
1160
- return agentOutputOperations((requestScope) => ({
1161
- execute: authMethod(({ code, diagnostics }, options = {}) => {
1188
+ return authMethod(({ code, diagnostics }, options = {}) => Effect.suspend(() => {
1189
+ const requestScope = config.requestScope ?? {};
1190
+ const sentSecrets = sentSecretsForRequest(requestScope);
1191
+ const owner = requestOwner(requestScope);
1192
+ if (!owner.open)
1193
+ return Effect.fail(owner.controller.signal.reason);
1194
+ const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
1195
+ const programWrites = new ProgramWrites();
1196
+ const response = Effect.suspend(() => {
1162
1197
  // A code-unit count above the cap is already too large in UTF-8. Check
1163
1198
  // that first so a huge direct-call string is never encoded in full.
1164
1199
  if (code.length > EXECUTE_MAX_CODE_BYTES ||
1165
1200
  new TextEncoder().encode(code).byteLength > EXECUTE_MAX_CODE_BYTES) {
1166
1201
  const message = `execute_code code exceeds the ${EXECUTE_MAX_CODE_BYTES}-byte UTF-8 limit.`;
1167
- const response = failureResponse(message, { code: { code: "invalid_args", message, retryable: false } });
1168
- response.structuredContent = {
1169
- ...response.structuredContent,
1170
- hostCalls: { attempted: 0, admitted: 0, succeeded: 0, failed: 0 },
1171
- };
1172
- response.content[0].text = JSON.stringify(response.structuredContent);
1173
- return Effect.succeed(response);
1202
+ return Effect.succeed(failureResponse(message, { code: { code: "invalid_args", message, retryable: false } }));
1174
1203
  }
1175
- return Effect.suspend(() => play(normalizeProgramSource(code), diagnostics, options.signal, requestScope));
1176
- }),
1177
- }), config.requestScope).execute;
1204
+ return play(normalizeProgramSource(code), diagnostics, options.signal, requestScope, hostCalls, programWrites);
1205
+ });
1206
+ return response.pipe(Effect.provideService(RequestScope, owner), Effect.map((unfinished) => {
1207
+ // Every response, including intake refusals, crosses accounting and
1208
+ // exactly one envelope check at this operation boundary.
1209
+ const result = redactAgentOutput(sentSecrets, programWrites.finish(unfinished));
1210
+ // Calls abandoned by the guest are failed when the run cancels them.
1211
+ // Connecta attaches its own integers after the envelope check.
1212
+ const counts = { ...hostCalls, failed: hostCalls.attempted - hostCalls.succeeded };
1213
+ result.structuredContent = { ...result.structuredContent, hostCalls: counts };
1214
+ if (result.content[0]?.type === "text")
1215
+ result.content[0].text = JSON.stringify(result.structuredContent);
1216
+ return result;
1217
+ }), Effect.catchDefect(Effect.fail), Effect.mapError((error) => redactAgentOutput(sentSecrets, error)));
1218
+ }));
1178
1219
  }
1179
1220
  /** An execution that never produced an ExecuteResult, as the model sees it. */
1180
1221
  function failedRun(err, logger, { emitted, diagnostics, program }) {
@@ -1215,81 +1256,83 @@ function failedRun(err, logger, { emitted, diagnostics, program }) {
1215
1256
  });
1216
1257
  }
1217
1258
  /** The response for an ExecuteResult: the program's error or its value. */
1218
- async function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
1219
- if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
1220
- return failureResponse("Executor failed: expected an ExecuteResult object.", {
1221
- emitted,
1222
- diagnostics,
1223
- code: "executor_failed",
1224
- });
1225
- }
1226
- const logs = executeLogs(outcome.logs);
1227
- if (outcome.error !== undefined && typeof outcome.error !== "string") {
1228
- return failureResponse("Executor failed: ExecuteResult.error must be a string.", {
1229
- logs,
1230
- emitted,
1231
- diagnostics,
1232
- code: "executor_failed",
1233
- });
1234
- }
1235
- if (outcome.error !== undefined) {
1236
- const failure = outcome.failure?.call;
1237
- if (outcome.failure?.timeout) {
1238
- const { elapsedMs, deadlineMs } = outcome.failure.timeout;
1239
- const message = `Operation "execute_code" timed out during sandbox after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`;
1259
+ function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
1260
+ return authFlow(function* () {
1261
+ if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
1262
+ return failureResponse("Executor failed: expected an ExecuteResult object.", {
1263
+ emitted,
1264
+ diagnostics,
1265
+ code: "executor_failed",
1266
+ });
1267
+ }
1268
+ const logs = executeLogs(outcome.logs);
1269
+ if (outcome.error !== undefined && typeof outcome.error !== "string") {
1270
+ return failureResponse("Executor failed: ExecuteResult.error must be a string.", {
1271
+ logs,
1272
+ emitted,
1273
+ diagnostics,
1274
+ code: "executor_failed",
1275
+ });
1276
+ }
1277
+ if (outcome.error !== undefined) {
1278
+ const failure = outcome.failure?.call;
1279
+ if (outcome.failure?.timeout) {
1280
+ const { elapsedMs, deadlineMs } = outcome.failure.timeout;
1281
+ const message = `Operation "execute_code" timed out during sandbox after ${elapsedMs}ms (effective deadline ${deadlineMs}ms).`;
1282
+ return failureResponse(message, {
1283
+ logs,
1284
+ emitted,
1285
+ diagnostics,
1286
+ code: {
1287
+ code: "timeout",
1288
+ message,
1289
+ retryable: false,
1290
+ details: { operation: "execute_code", stage: "sandbox", elapsedMs, deadlineMs },
1291
+ },
1292
+ });
1293
+ }
1294
+ return failureResponse(failure?.message ?? outcome.error, {
1295
+ logs,
1296
+ emitted,
1297
+ diagnostics,
1298
+ code: failure ??
1299
+ programError({
1300
+ name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
1301
+ message: outcome.error || "Execution failed without an error message.",
1302
+ ...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
1303
+ }, program),
1304
+ });
1305
+ }
1306
+ // A result crossing back as a host BigInt (or otherwise unserializable
1307
+ // value) makes JSON.stringify throw — keep that inside the structured
1308
+ // error path so captured logs survive instead of a raw SDK 500.
1309
+ let result;
1310
+ try {
1311
+ result = yield* guardResult(outcome.result);
1312
+ }
1313
+ catch (err) {
1314
+ const message = `Error: result is not JSON-serializable: ${msg(err)}`;
1240
1315
  return failureResponse(message, {
1241
1316
  logs,
1242
1317
  emitted,
1243
1318
  diagnostics,
1244
- code: {
1245
- code: "timeout",
1246
- message,
1247
- retryable: false,
1248
- details: { operation: "execute_code", stage: "sandbox", elapsedMs, deadlineMs },
1249
- },
1319
+ code: "executor_failed",
1250
1320
  });
1251
1321
  }
1252
- return failureResponse(failure?.message ?? outcome.error, {
1253
- logs,
1254
- emitted,
1255
- diagnostics,
1256
- code: failure ??
1257
- programError({
1258
- name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
1259
- message: outcome.error || "Execution failed without an error message.",
1260
- ...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
1261
- }, program),
1262
- });
1263
- }
1264
- // A result crossing back as a host BigInt (or otherwise unserializable
1265
- // value) makes JSON.stringify throw — keep that inside the structured
1266
- // error path so captured logs survive instead of a raw SDK 500.
1267
- let result;
1268
- try {
1269
- result = await guardResult(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
1270
- }
1271
- catch (err) {
1272
- const message = `Error: result is not JSON-serializable: ${msg(err)}`;
1273
- return failureResponse(message, {
1274
- logs,
1275
- emitted,
1276
- diagnostics,
1277
- code: "executor_failed",
1322
+ const response = jsonResult({
1323
+ result,
1324
+ ...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
1325
+ ...(logs ? { logs } : {}),
1326
+ ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
1278
1327
  });
1279
- }
1280
- const response = jsonResult({
1281
- result,
1282
- ...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
1283
- ...(logs ? { logs } : {}),
1284
- ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
1328
+ if (emitted.blocks.length > 0) {
1329
+ // Emitted image/audio blocks are valid MCP content that ToolResult's
1330
+ // text-only typing does not model — the same acknowledged gap
1331
+ // guardContent lives with for downstream block passthrough.
1332
+ response.content.push(...emitted.blocks);
1333
+ }
1334
+ return response;
1285
1335
  });
1286
- if (emitted.blocks.length > 0) {
1287
- // Emitted image/audio blocks are valid MCP content that ToolResult's
1288
- // text-only typing does not model — the same acknowledged gap
1289
- // guardContent lives with for downstream block passthrough.
1290
- response.content.push(...emitted.blocks);
1291
- }
1292
- return response;
1293
1336
  }
1294
1337
  function executeLogs(value) {
1295
1338
  return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")
@@ -1345,7 +1388,7 @@ function connectorInventory(connectors) {
1345
1388
  return `${prefix}${shown.join(", ")}.`;
1346
1389
  return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
1347
1390
  }
1348
- const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : readOnly ? "One known read: call_tool. Everything else: execute_code. Read-only endpoint: search and describe omit write tools, and every write call is refused." : "One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. Writable endpoint: programs may read and write; the client approves."} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
1391
+ const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : readOnly ? "One known read: call_tool. Everything else: execute_code. Read-only endpoint: search and describe omit write tools, and every write call is refused." : "One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. Writable endpoint: programs may read and write; the client approves."} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}.
1349
1392
  ${connectorInventory(connectors)}
1350
1393
 
1351
1394
  Use async () => { ... }, no arguments. No portable ambient capabilities. API:
@@ -1356,7 +1399,7 @@ ${GUEST_API_DECLARATION}
1356
1399
  \`\`\`
1357
1400
  Search: {tools}, catalogErrors. JSON schemas default; compact text. Calls: {data, format:"json"|"text"}; >1 MiB: paged handle. Oversized returns: resultId/nextAction. connecta.result pages; UTF-8 byte offsets. allSettled keeps typed errors; host budget_exceeded ends the run.
1358
1401
 
1359
- Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks. Authored: text/base64 image/audio; no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
1402
+ Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks. Authored: text/base64 image/audio; no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. connecta.skill("usage"): repair${connectorGuides ? ", guide handling" : ""}; connecta.skill("investigate"): workflow.`;
1360
1403
  // Module scope, like the other five meta-tool inputs: its JSON Schema is
1361
1404
  // derived once per process. Budgets and connectors vary by deployment and
1362
1405
  // view, so they live in the description, never here.
@@ -1394,12 +1437,14 @@ export function registerExecuteTool(server, registry, ctx) {
1394
1437
  defer: ctx.defer,
1395
1438
  readOnly: ctx.readOnly,
1396
1439
  maxWrites,
1440
+ maxResultChars: ctx.maxResultChars,
1397
1441
  requestScope: ctx.requestScope,
1398
1442
  });
1399
1443
  server.registerTool("execute_code", {
1400
1444
  description: executeDescription(emitBudgets, {
1401
1445
  ...hostLimits,
1402
- hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1446
+ // connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
1447
+ callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1403
1448
  }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true),
1404
1449
  inputSchema: EXECUTE_INPUT,
1405
1450
  outputSchema: EXECUTE_OUTPUT,
@@ -1,6 +1,7 @@
1
+ import { CONFIG_DEFAULTS } from "./config-defaults.js";
1
2
  import { msg } from "./errors.js";
2
3
  /** ~6k tokens. Sandbox code should filter data down before returning. */
3
- export const MAX_EXECUTE_RESULT_CHARS = 24_000;
4
+ export const MAX_EXECUTE_RESULT_CHARS = CONFIG_DEFAULTS.execute.maxResultChars;
4
5
  export const MAX_EXECUTE_LOG_CHARS = 4_000;
5
6
  export function serializeResultText(value) {
6
7
  const serialized = JSON.stringify(value);