@zackbart/connecta 0.37.1 → 0.39.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 (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  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/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/execute.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { META_ROUTES, routeNames } from "./agent-routes.js";
1
2
  import { authCall, authFlow, authInvoke, authMethod } from "./auth/native.js";
2
3
  import { nativeRecovery } from "./native-recovery.js";
3
- import { GUEST_API_DECLARATION } from "./usage-guide.js";
4
+ import { GUEST_API_DECLARATION, GUEST_FUNCTIONS, UNCOUNTED_GUEST_FUNCTIONS, } from "./guest-functions.js";
4
5
  import { bindMcpClient } from "./mcp-client-context.js";
5
6
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
6
7
  import { Cause, Deferred, Duration, Effect, Exit } from "effect";
@@ -37,8 +38,11 @@ const EXECUTE_MAX_CODE_BYTES = 64 * 1024;
37
38
  * never settles at all.
38
39
  */
39
40
  const EXECUTE_WATCHDOG_MS = CONFIG_DEFAULTS.execute.watchdogMs;
40
- /** Complete entries plus an exact omission count, all inside this byte cap. */
41
- export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
41
+ /**
42
+ * Room to name every connector of a typical deployment. Past it, complete
43
+ * entries plus an exact omission count stay inside this byte cap.
44
+ */
45
+ export const CONNECTOR_INVENTORY_MAX_BYTES = 1_024;
42
46
  /**
43
47
  * Default budgets for `connecta.emit`. The byte budget is a transport bound,
44
48
  * not a context bound — emitted image/audio blocks reach the model as media,
@@ -175,13 +179,15 @@ export class EmitCollector {
175
179
  maxBytes;
176
180
  maxBlocks;
177
181
  diagnostics;
182
+ maxResultChars;
178
183
  blocks = [];
179
184
  bytes = 0;
180
185
  textChars = 0;
181
- constructor(maxBytes, maxBlocks, diagnostics) {
186
+ constructor(maxBytes, maxBlocks, diagnostics, maxResultChars = MAX_EXECUTE_RESULT_CHARS) {
182
187
  this.maxBytes = maxBytes;
183
188
  this.maxBlocks = maxBlocks;
184
189
  this.diagnostics = diagnostics;
190
+ this.maxResultChars = maxResultChars;
185
191
  }
186
192
  accept(raw) {
187
193
  this.acceptNative([requireEmittedBlock(raw)]);
@@ -205,8 +211,8 @@ export class EmitCollector {
205
211
  // the transport budget, just like images and audio.
206
212
  if (block.type === "resource" && "text" in block.resource)
207
213
  textChars += serialized.length;
208
- if (textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
209
- throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
214
+ if (textChars > this.maxResultChars - 512) {
215
+ throw guestFailure("result_too_large", `Emitted text shares the ${this.maxResultChars.toLocaleString("en-US")}-character program result limit. Reduce the text before emitting it.`);
210
216
  }
211
217
  count++;
212
218
  bytes += size;
@@ -423,6 +429,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
423
429
  concurrency: limits.discoveryConcurrency,
424
430
  probeTimeoutMs: limits.probeTimeoutMs,
425
431
  defer: limits.defer,
432
+ routes: limits.routes,
426
433
  });
427
434
  const invocation = new InvocationService(registry, catalog, activity);
428
435
  const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
@@ -466,6 +473,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
466
473
  source: "execute_code",
467
474
  sentSecrets,
468
475
  timeoutMs,
476
+ ...(limits.routes ? { routes: limits.routes } : {}),
469
477
  ...(signal !== undefined ? { requestSignal: signal } : {}),
470
478
  ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
471
479
  unwrapResult: true,
@@ -696,6 +704,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
696
704
  catalogDependency(resolved.connector.id, resolved.toolName);
697
705
  };
698
706
  const operations = {
707
+ // Uncounted: a failed program read a result field off an unawaited host call.
708
+ unawaited: () => {
709
+ if (limits.unawaited)
710
+ limits.unawaited.read = true;
711
+ },
699
712
  call,
700
713
  result: (id, options, signal, onStage) => Effect.tryPromise({
701
714
  try: async () => {
@@ -750,8 +763,10 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
750
763
  }),
751
764
  skill: (name, _options, utilitySignal) => Effect.tryPromise({
752
765
  try: async () => {
753
- if (typeof name !== "string")
754
- throw guestFailure("invalid_args", "Use connecta.skill(name) with an exact skill name.");
766
+ // Accept the top-level skills tool's { name } shape too: models copy it into programs.
767
+ const skillName = name && typeof name === "object" ? name.name : name;
768
+ if (typeof skillName !== "string")
769
+ throw guestFailure("invalid_args", 'Use connecta.skill("usage") or connecta.skill("connector:<id>") with an exact skill name; the skills tool lists names.');
755
770
  const skills = new SkillsRegistry(registry, baseUrl, {
756
771
  requestScope,
757
772
  requestSignal: utilitySignal ?? hostAccessSignal,
@@ -763,7 +778,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
763
778
  probeTimeoutMs: limits.probeTimeoutMs,
764
779
  defer: limits.defer,
765
780
  });
766
- return { name, format: "text", text: await skills.text(name) };
781
+ return { name: skillName, format: "text", text: await skills.text(skillName) };
767
782
  },
768
783
  catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
769
784
  }),
@@ -818,7 +833,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
818
833
  const args = (raw ?? {});
819
834
  const result = flatSearchResult(await catalog.search({
820
835
  ...args,
821
- includeSchemas: args.includeSchemas ?? "json",
836
+ includeSchemas: args.includeSchemas ?? "typescript",
822
837
  includeSchemaKeys: args.includeSchemaKeys !== false,
823
838
  }, catalogDependency));
824
839
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
@@ -852,90 +867,102 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
852
867
  const failure = boundedGuestFailure(redactAgentOutput(sentSecrets, err));
853
868
  return Effect.fail(failure);
854
869
  });
870
+ /** End the whole run: later bridge calls, including emit, lose host access. */
871
+ const endRun = (failure) => {
872
+ budgetFailure = failure;
873
+ programWrites?.close();
874
+ limits.dispatchController?.abort();
875
+ limits.onHostCallBudgetExceeded?.(failure);
876
+ return stopped;
877
+ };
855
878
  return {
856
879
  name: "connecta",
857
880
  // Trusted host code the program runs after and cannot undo.
858
881
  prelude: guestPrelude(),
859
- fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
860
- name,
861
- (...args) => {
862
- if (budgetFailure)
863
- return stopped;
864
- // Invalid emits still allocate guest promises and typed rejections.
865
- // Bound those attempts without consuming the invocation budget or discarding identities.
866
- if (name === "emit" && ++emitAttempts > maxEmitAttempts) {
867
- budgetFailure = guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`);
868
- programWrites?.close();
869
- limits.dispatchController?.abort();
870
- limits.onHostCallBudgetExceeded?.(budgetFailure);
871
- return stopped;
872
- }
873
- // L4/M7: spend on entry, before even invalid arguments are checked;
874
- // emit has a separate budget and never spends this one.
875
- // Transparent transfer pages consume the already-admitted call, not guest host calls.
876
- const counted = name !== "emit" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
877
- if (counted && ++hostCalls.attempted > maxHostCalls) {
878
- hostCalls.failed++;
879
- budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
880
- programWrites?.close();
881
- limits.dispatchController?.abort();
882
- limits.onHostCallBudgetExceeded?.(budgetFailure);
883
- return stopped;
884
- }
885
- if (counted)
886
- hostCalls.admitted++;
887
- const started = Date.now();
888
- const utility = name === "result" || name === "skill";
889
- let stage = name === "result" ? "result storage" : "skill lookup";
890
- const invoke = Effect.suspend(() => operation(...args));
891
- const guarded = utility
892
- ? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
893
- stage = current;
894
- })), {
895
- timeoutMs: hostCallTimeoutMs,
896
- ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
897
- timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
898
- })
899
- : invoke;
900
- const settled = runEdge(guarded.pipe(Effect.catch((error) => {
901
- if (utility && hostAccessSignal?.aborted)
902
- return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
903
- if (utility && error instanceof InvocationFailure && error.code === "timeout") {
904
- const elapsedMs = Date.now() - started;
905
- const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
906
- return typedFailure(new InvocationFailure({
907
- code: "timeout",
908
- message,
909
- retryable: true,
910
- details: {
911
- operation: `connecta.${name}`,
912
- stage,
913
- elapsedMs,
914
- deadlineMs: hostCallTimeoutMs,
915
- },
916
- }));
917
- }
918
- return typedFailure(error);
919
- }))).then((value) => {
920
- if (counted)
921
- hostCalls.succeeded++;
922
- return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
923
- }, (err) => {
924
- if (counted)
925
- hostCalls.failed++;
882
+ // One bridge function per registry entry, admitted by its budget class.
883
+ fns: Object.fromEntries(GUEST_FUNCTIONS.map((fn) => {
884
+ const { name, budget } = fn;
885
+ const operation = operations[name];
886
+ const deadline = "deadline" in fn ? fn.deadline : undefined;
887
+ let notices = 0;
888
+ return [
889
+ name,
890
+ (...args) => {
926
891
  if (budgetFailure)
927
892
  return stopped;
928
- throw redactAgentOutput(sentSecrets, err);
929
- });
930
- // The executor owns this promise, and a program may abandon a call
931
- // that the run's end then cancels before anyone has awaited it.
932
- // The rejection is still there for whoever does; it is just not an
933
- // unhandled rejection of the host's, which workerd reports as soon
934
- // as a microtask passes without a handler.
935
- settled.catch(() => { });
936
- return settled;
937
- },
938
- ])),
893
+ // Uncounted: one per run; any further call through the bridge ends the run.
894
+ if (budget === "once") {
895
+ if (++notices > 1)
896
+ return endRun(guestFailure("budget_exceeded", `execute_code ${fn.label} is limited to one per run.`));
897
+ operation();
898
+ return Promise.resolve();
899
+ }
900
+ // Invalid emits still allocate guest promises and typed rejections.
901
+ // Bound those attempts without consuming the invocation budget or discarding identities.
902
+ if (budget === "emission" && ++emitAttempts > maxEmitAttempts)
903
+ return endRun(guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`));
904
+ // L4/M7: spend on entry, before even invalid arguments are checked;
905
+ // emission has a separate budget and never spends this one.
906
+ // Transparent transfer pages consume the already-admitted call, not guest host calls.
907
+ const counted = budget === "hostCall" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
908
+ if (counted && ++hostCalls.attempted > maxHostCalls) {
909
+ hostCalls.failed++;
910
+ return endRun(new HostCallBudgetExceeded(hostCalls, maxHostCalls));
911
+ }
912
+ if (counted)
913
+ hostCalls.admitted++;
914
+ const started = Date.now();
915
+ let stage = deadline ?? "";
916
+ const invoke = Effect.suspend(() => operation(...args));
917
+ const guarded = deadline
918
+ ? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
919
+ stage = current;
920
+ })), {
921
+ timeoutMs: hostCallTimeoutMs,
922
+ ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
923
+ timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
924
+ })
925
+ : invoke;
926
+ const settled = runEdge(guarded.pipe(Effect.catch((error) => {
927
+ if (deadline && hostAccessSignal?.aborted)
928
+ return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
929
+ if (deadline && error instanceof InvocationFailure && error.code === "timeout") {
930
+ const elapsedMs = Date.now() - started;
931
+ const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
932
+ return typedFailure(new InvocationFailure({
933
+ code: "timeout",
934
+ message,
935
+ retryable: true,
936
+ details: {
937
+ operation: `connecta.${name}`,
938
+ stage,
939
+ elapsedMs,
940
+ deadlineMs: hostCallTimeoutMs,
941
+ },
942
+ }));
943
+ }
944
+ return typedFailure(error);
945
+ }))).then((value) => {
946
+ if (counted)
947
+ hostCalls.succeeded++;
948
+ return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
949
+ }, (err) => {
950
+ if (counted)
951
+ hostCalls.failed++;
952
+ if (budgetFailure)
953
+ return stopped;
954
+ throw redactAgentOutput(sentSecrets, err);
955
+ });
956
+ // The executor owns this promise, and a program may abandon a call
957
+ // that the run's end then cancels before anyone has awaited it.
958
+ // The rejection is still there for whoever does; it is just not an
959
+ // unhandled rejection of the host's, which workerd reports as soon
960
+ // as a microtask passes without a handler.
961
+ settled.catch(() => { });
962
+ return settled;
963
+ },
964
+ ];
965
+ })),
939
966
  };
940
967
  }
941
968
  /**
@@ -1044,11 +1071,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1044
1071
  logger,
1045
1072
  };
1046
1073
  const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
1074
+ const maxResultChars = resolveBudget(config.maxResultChars, MAX_EXECUTE_RESULT_CHARS);
1047
1075
  const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites) => Effect.suspend(() => {
1048
1076
  const sentSecrets = sentSecretsForRequest(requestScope);
1049
1077
  const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
1050
- const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
1078
+ const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
1051
1079
  const resultDependencies = [];
1080
+ const unawaited = { read: false };
1052
1081
  const dispatchController = new AbortController();
1053
1082
  const terminal = Deferred.makeUnsafe();
1054
1083
  let budgetFailure;
@@ -1099,9 +1128,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1099
1128
  defer: config.defer,
1100
1129
  readOnly: config.readOnly,
1101
1130
  maxWrites: config.maxWrites,
1131
+ routes: config.routes,
1102
1132
  programWrites,
1103
1133
  resultDependencies,
1104
1134
  dispatchController,
1135
+ unawaited,
1105
1136
  }, requestScope)));
1106
1137
  if (signal.aborted) {
1107
1138
  return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
@@ -1137,15 +1168,18 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1137
1168
  emitted,
1138
1169
  diagnostics,
1139
1170
  program,
1140
- guardResult: (value, maxChars) => authCall(guardProgramReturn, [
1171
+ unawaited,
1172
+ // Emitted text shares the return cap; a follow-up page run gets all of it.
1173
+ guardResult: (value) => authCall(guardProgramReturn, [
1141
1174
  value,
1142
1175
  registry,
1143
1176
  baseUrl,
1144
1177
  [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()],
1145
1178
  sentSecrets,
1146
1179
  logger,
1147
- maxChars,
1180
+ maxResultChars - emitted.textChars,
1148
1181
  programWrites.hasSucceeded,
1182
+ maxResultChars,
1149
1183
  ]),
1150
1184
  };
1151
1185
  const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
@@ -1249,7 +1283,7 @@ function failedRun(err, logger, { emitted, diagnostics, program }) {
1249
1283
  });
1250
1284
  }
1251
1285
  /** The response for an ExecuteResult: the program's error or its value. */
1252
- function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
1286
+ function finishedRun(outcome, { emitted, diagnostics, program, unawaited, guardResult }) {
1253
1287
  return authFlow(function* () {
1254
1288
  if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
1255
1289
  return failureResponse("Executor failed: expected an ExecuteResult object.", {
@@ -1293,6 +1327,7 @@ function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
1293
1327
  name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
1294
1328
  message: outcome.error || "Execution failed without an error message.",
1295
1329
  ...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
1330
+ unawaited: unawaited.read,
1296
1331
  }, program),
1297
1332
  });
1298
1333
  }
@@ -1301,7 +1336,7 @@ function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
1301
1336
  // error path so captured logs survive instead of a raw SDK 500.
1302
1337
  let result;
1303
1338
  try {
1304
- result = yield* guardResult(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
1339
+ result = yield* guardResult(outcome.result);
1305
1340
  }
1306
1341
  catch (err) {
1307
1342
  const message = `Error: result is not JSON-serializable: ${msg(err)}`;
@@ -1349,50 +1384,59 @@ function failureResponse(message, options) {
1349
1384
  function discardedEmits(emitted) {
1350
1385
  return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
1351
1386
  }
1387
+ const inventoryWords = (text) => text
1388
+ .toLowerCase()
1389
+ .split(/[^\p{L}\p{N}]+/u)
1390
+ .filter(Boolean);
1391
+ /** A title that only respells the id ("Google Calendar" for google_calendar) adds nothing. */
1392
+ function titleAddsInformation(id, title) {
1393
+ const words = inventoryWords(title);
1394
+ const idWords = inventoryWords(id);
1395
+ return words.join("") !== idWords.join("") && words.some((word) => !idWords.includes(word));
1396
+ }
1397
+ /**
1398
+ * Every configured connector, from configuration alone. Detail degrades before
1399
+ * names do: titles go first, then optional guide pointers, and only past the
1400
+ * byte cap do whole entries give way to an exact omission count.
1401
+ */
1352
1402
  function connectorInventory(connectors) {
1353
1403
  const prefix = "Connectors: ";
1354
1404
  if (connectors.length === 0)
1355
1405
  return `${prefix}none.`;
1356
- const entries = connectors.map((connector) => {
1357
- const address = connector.id;
1406
+ const fits = (line) => boundedEchoText(line, CONNECTOR_INVENTORY_MAX_BYTES) === line;
1407
+ const entry = (connector, detail) => {
1408
+ const notes = [];
1358
1409
  const title = connector.title?.replace(/\s+/g, " ").trim();
1359
- const label = title && title !== connector.id ? `${address}: ${boundedEchoText(title, 45)}` : address;
1360
- if (!connectorGuide(connector))
1361
- return label;
1362
- const requirement = connectorGuideRequired(connector) ? "required guide" : "guide";
1363
- return `${label} (${requirement} ${connectorSkillName(connector.id)})`;
1364
- });
1365
- const shown = [];
1366
- for (let index = 0; index < entries.length; index++) {
1367
- const entry = entries[index];
1368
- if (entry === undefined)
1369
- break;
1370
- const candidate = [...shown, entry].join(", ");
1371
- const omitted = entries.length - index - 1;
1372
- const suffix = omitted > 0 ? `; +${omitted} more.` : ".";
1373
- const serialized = prefix + candidate + suffix;
1374
- if (boundedEchoText(serialized, CONNECTOR_INVENTORY_MAX_BYTES) !== serialized) {
1375
- break;
1410
+ if (detail >= 2 && title && titleAddsInformation(connector.id, title))
1411
+ notes.push(boundedEchoText(title, 45));
1412
+ const required = connectorGuideRequired(connector);
1413
+ if (connectorGuide(connector) && (required || detail >= 1)) {
1414
+ const fetch = `skills({name:"${connectorSkillName(connector.id)}"})`;
1415
+ notes.push(required ? `read first: ${fetch}` : `guide: ${fetch}`);
1376
1416
  }
1377
- shown.push(entry);
1417
+ return notes.length > 0 ? `${connector.id} (${notes.join("; ")})` : connector.id;
1418
+ };
1419
+ for (const detail of [2, 1, 0]) {
1420
+ const line = `${prefix}${connectors.map((connector) => entry(connector, detail)).join(", ")}.`;
1421
+ if (fits(line))
1422
+ return line;
1378
1423
  }
1379
- const omitted = entries.length - shown.length;
1380
- if (omitted === 0)
1381
- return `${prefix}${shown.join(", ")}.`;
1382
- return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
1424
+ const entries = connectors.map((connector) => entry(connector, 0));
1425
+ let shown = 0;
1426
+ while (fits(`${prefix}${entries.slice(0, shown + 1).join(", ")}; +${entries.length - shown - 1} more.`))
1427
+ shown++;
1428
+ return `${prefix}${entries.slice(0, shown).join(", ")}${shown > 0 ? "; " : ""}+${entries.length - shown} more.`;
1383
1429
  }
1384
- 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.
1430
+ const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false, routes = META_ROUTES) => `${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."}` : `${routes === routeNames("code") ? `Every operation uses ${routes.program}.` : readOnly ? `One known read: ${routes.read}. Everything else: ${routes.program}.` : `One known read: ${routes.read}. One known write: ${routes.write}. Everything else: ${routes.program}.`} ${readOnly ? "Read-only endpoint: search and describe omit write tools, and every write call is refused." : "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)`}.
1385
1431
  ${connectorInventory(connectors)}
1386
1432
 
1387
1433
  Use async () => { ... }, no arguments. No portable ambient capabilities. API:
1388
1434
  \`\`\`ts
1389
- declare const connecta: {
1390
1435
  ${GUEST_API_DECLARATION}
1391
- };
1392
1436
  \`\`\`
1393
- 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.
1437
+ search and describe return objects, not arrays. Search shows a TypeScript signature by default; describe, JSON Schema. A call result over 1 MiB returns a paged handle; read it with connecta.result(handle, { offset }), which pages by UTF-8 byte offset. Every function except ${UNCOUNTED_GUEST_FUNCTIONS} spends a host call. Promise.allSettled keeps per-call errors, but exceeding the host-call limit ends the program. When paging is available, an oversized program return carries resultId and nextAction.
1394
1438
 
1395
- 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" : ""}.`;
1439
+ Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks unchanged; an authored Block's image/audio data is base64. emit needs no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. connecta.skill("usage"): repair${connectorGuides ? ", guide handling" : ""}.`;
1396
1440
  // Module scope, like the other five meta-tool inputs: its JSON Schema is
1397
1441
  // derived once per process. Budgets and connectors vary by deployment and
1398
1442
  // view, so they live in the description, never here.
@@ -1430,13 +1474,16 @@ export function registerExecuteTool(server, registry, ctx) {
1430
1474
  defer: ctx.defer,
1431
1475
  readOnly: ctx.readOnly,
1432
1476
  maxWrites,
1477
+ maxResultChars: ctx.maxResultChars,
1433
1478
  requestScope: ctx.requestScope,
1479
+ routes: ctx.routes,
1434
1480
  });
1435
1481
  server.registerTool("execute_code", {
1436
1482
  description: executeDescription(emitBudgets, {
1437
1483
  ...hostLimits,
1438
- hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1439
- }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true),
1484
+ // connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
1485
+ callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1486
+ }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes),
1440
1487
  inputSchema: EXECUTE_INPUT,
1441
1488
  outputSchema: EXECUTE_OUTPUT,
1442
1489
  // The client sees execute_code as a write exactly when the endpoint writes.
@@ -1471,6 +1518,6 @@ export function registerExecuteTool(server, registry, ctx) {
1471
1518
  const result = yield* ctx.authElicitation
1472
1519
  ? authInvoke(ctx.authElicitation, "run", ["execute_code", args, extra, run])
1473
1520
  : authCall(run, []);
1474
- return ctx.native ? nativeRecovery(result, true) : result;
1521
+ return ctx.native ? nativeRecovery(result) : result;
1475
1522
  })));
1476
1523
  }
@@ -1,12 +1,15 @@
1
+ import { META_ROUTES } from "./agent-routes.js";
2
+ import { CONFIG_DEFAULTS } from "./config-defaults.js";
1
3
  import { msg } from "./errors.js";
2
4
  /** ~6k tokens. Sandbox code should filter data down before returning. */
3
- export const MAX_EXECUTE_RESULT_CHARS = 24_000;
5
+ export const MAX_EXECUTE_RESULT_CHARS = CONFIG_DEFAULTS.execute.maxResultChars;
4
6
  export const MAX_EXECUTE_LOG_CHARS = 4_000;
5
7
  export function serializeResultText(value) {
6
8
  const serialized = JSON.stringify(value);
7
9
  return serialized === undefined ? String(value) : serialized;
8
10
  }
9
- const TRUNCATION_HINT = "filter/map/slice data inside execute_code and return only what you need";
11
+ // Only program returns carry it, and every surface running programs lists execute_code.
12
+ const TRUNCATION_HINT = `filter/map/slice data inside ${META_ROUTES.program} and return only what you need`;
10
13
  /**
11
14
  * Shape the over-cap notice so the **serialized envelope** fits the same cap
12
15
  * the raw value missed. Escaping matters: a preview sliced to the cap is JSON
@@ -1,3 +1,4 @@
1
+ import { utf8Length } from "../bytes.js";
1
2
  // QuickJS-in-WebAssembly runtime used inside the disposable child process.
2
3
  //
3
4
  // Model-written code runs inside a QuickJS interpreter compiled to WASM:
@@ -37,30 +38,6 @@ function logTransportBytes(entry) {
37
38
  // encoded into ChildToParentMessage. Measure the units the IPC cap sees.
38
39
  return serializedBytes(JSON.stringify(JSON.stringify(entry)));
39
40
  }
40
- function exceedsUtf8ByteLimit(value, limit) {
41
- let bytes = 0;
42
- for (let index = 0; index < value.length; index += 1) {
43
- const code = value.charCodeAt(index);
44
- if (code <= 0x7f)
45
- bytes += 1;
46
- else if (code <= 0x7ff)
47
- bytes += 2;
48
- else if (code >= 0xd800 &&
49
- code <= 0xdbff &&
50
- index + 1 < value.length &&
51
- value.charCodeAt(index + 1) >= 0xdc00 &&
52
- value.charCodeAt(index + 1) <= 0xdfff) {
53
- bytes += 4;
54
- index += 1;
55
- }
56
- else {
57
- bytes += 3;
58
- }
59
- if (bytes > limit)
60
- return true;
61
- }
62
- return false;
63
- }
64
41
  /** Normalize model output into an async-arrow expression: strip markdown fences, wrap bare bodies. */
65
42
  export function normalizeCode(code) {
66
43
  let c = code.trim();
@@ -216,7 +193,7 @@ function installBridge(ctx, providers, logs, onLog) {
216
193
  catch (err) {
217
194
  throw new Error(`Host result from ${label} could not be serialized: ${msg(err)}`);
218
195
  }
219
- if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
196
+ if (utf8Length(json, MAX_HOST_RESULT_BYTES) > MAX_HOST_RESULT_BYTES) {
220
197
  throw new Error(`Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`);
221
198
  }
222
199
  return { json };
@@ -119,7 +119,7 @@ function terminate(child) {
119
119
  }
120
120
  let providePoolProgram;
121
121
  class QuickJsChildPool {
122
- /** What `/health` and `connecta doctor` call this sandbox (#368). */
122
+ /** What `/health` and `connecta doctor` call this sandbox. */
123
123
  name = "QuickJS";
124
124
  admission;
125
125
  slots;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Every `connecta.<name>` function a guest program can reach, in one list.
3
+ *
4
+ * Guest-safe data only: no host implementation, no imports. The guest runtime
5
+ * builds its namespace from it, execute.ts admits each host call by its budget
6
+ * class and binds it by name to an implementation typed against this list, and
7
+ * the execute_code description's compact declaration is joined from it.
8
+ * `GuestApi` in guest-types.ts stays the published type; the package-surface
9
+ * test compiles the joined declaration against it.
10
+ */
11
+ export const GUEST_FUNCTIONS = [
12
+ /** Searches the catalog and returns one flat page of `{ tools }`. */
13
+ {
14
+ name: "search",
15
+ budget: "hostCall",
16
+ declaration: "search(args?: { query?: string; connector?: string; limit?: number; offset?: number; includeSchemas?: Schemas }): Promise<{ tools: Tool[]; catalogErrors: { connector: string; code: string; message: string }[]; hasMore: boolean; nextOffset?: number }>;",
17
+ },
18
+ /** Describes exact addresses, one entry per address in order. */
19
+ {
20
+ name: "describe",
21
+ budget: "hostCall",
22
+ declaration: "describe(args?: { address?: string; addresses?: string[]; format?: Schemas }): Promise<{ tools: (Tool & { error?: string })[] }>;",
23
+ },
24
+ /** Calls one tool and inlines transfer pages and native content. */
25
+ {
26
+ name: "call",
27
+ budget: "hostCall",
28
+ wrapped: true,
29
+ declaration: "call(address: string, args?: unknown, options?: { timeoutMs?: number }): Promise<Result>;",
30
+ },
31
+ /** Reads one advertised resource URI. */
32
+ {
33
+ name: "read",
34
+ budget: "hostCall",
35
+ declaration: "read(uri: string): Promise<{ contents: { uri: string; text?: string; blob?: string }[] }>;",
36
+ },
37
+ /** Pages a retained result by page or UTF-8 byte offset. */
38
+ {
39
+ name: "result",
40
+ budget: "hostCall",
41
+ deadline: "result storage",
42
+ declaration: "result(id: string | Paged, options?: { page?: number; offset?: number; maxBytes?: number }): Promise<{ text: string; hasMore: boolean; nextOffset?: number }>;",
43
+ },
44
+ /** Fetches a skill's text by exact name. */
45
+ {
46
+ name: "skill",
47
+ budget: "hostCall",
48
+ deadline: "skill lookup",
49
+ alias: { name: "skills", note: "models write connecta.skills, the top-level tool's name." },
50
+ declaration: "skill(name: string): Promise<{ text: string }>;",
51
+ },
52
+ /** Forwards a content block to the client; needs no await. */
53
+ {
54
+ name: "emit",
55
+ budget: "emission",
56
+ declaration: "emit(block: Block | Native | Exclude<Result, Paged>): PromiseLike<void>;",
57
+ },
58
+ /** Tells the host a failed program read a result field off an unawaited call. */
59
+ {
60
+ name: "unawaited",
61
+ budget: "once",
62
+ label: "missing-await notice",
63
+ },
64
+ ];
65
+ /** Public guest functions that spend no host call, for the agent-facing text. */
66
+ export const UNCOUNTED_GUEST_FUNCTIONS = GUEST_FUNCTIONS.filter((fn) => fn.budget === "emission")
67
+ .map((fn) => fn.name)
68
+ .join(" and ");
69
+ /**
70
+ * The guest API as the execute_code description shows it: compact shapes of
71
+ * GuestApi in src/guest-types.ts, checked against it by the package-surface
72
+ * declaration test. Arguments match what GuestApi accepts in both directions,
73
+ * results are supertypes of its results, and no field is invented. Type notation, not runnable JS.
74
+ */
75
+ export const GUEST_API_DECLARATION = `declare const connecta: {
76
+ ${GUEST_FUNCTIONS.flatMap((fn) => ("declaration" in fn ? [fn.declaration] : [])).join("\n")}
77
+ };
78
+ type Schemas = "json" | "compact" | "typescript";
79
+ type Tool = { address: string; description?: string; inputSchema?: unknown; outputSchema?: unknown; signature?: string };
80
+ type Paged = { format: "paged"; valueFormat: "json" | "text"; resultId?: string; totalBytes: number; truncated: true; hint: string };
81
+ type Result = ({ format: "json"; data: unknown } | { format: "text"; data: string } | Paged) & { content?: Native[] };
82
+ type Block = { type: "text"; text: string } | { type: "image" | "audio"; data: string; mimeType: string };
83
+ type Native = Block | { type: "resource"; resource: { uri: string } & ({ text: string } | { blob: string }) } | { type: "resource_link"; uri: string; name: string };`;