@zackbart/connecta 0.38.0 → 0.40.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 (180) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +3 -2
  3. package/bin/connecta.mjs +4 -8
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +6 -4
  6. package/dist/agent-routes.js +231 -0
  7. package/dist/auth/downstream-oauth/consent.js +519 -0
  8. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  9. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  10. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  11. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  12. package/dist/auth/downstream-oauth.js +66 -2382
  13. package/dist/auth/native.js +37 -0
  14. package/dist/auth/oauth-grant.js +13 -0
  15. package/dist/auth/static-oauth.js +153 -131
  16. package/dist/auth-elicitation.js +7 -1
  17. package/dist/authorization-handoff.js +11 -5
  18. package/dist/catalog-cache.js +219 -59
  19. package/dist/catalog-policy.js +18 -0
  20. package/dist/catalog-service.js +284 -47
  21. package/dist/catalog.js +18 -2
  22. package/dist/config.d.ts +16 -11
  23. package/dist/config.js +9 -6
  24. package/dist/connector-caller.d.ts +1 -1
  25. package/dist/connector-health.js +59 -0
  26. package/dist/connector-status.js +117 -0
  27. package/dist/connectors/api-connector.d.ts +7 -2
  28. package/dist/connectors/api-connector.js +7 -4
  29. package/dist/connectors/negotiation-cache.js +22 -4
  30. package/dist/connectors/remote-mcp/call.js +164 -0
  31. package/dist/connectors/remote-mcp/catalog.js +278 -0
  32. package/dist/connectors/remote-mcp/config.js +263 -0
  33. package/dist/connectors/remote-mcp/connection.js +682 -0
  34. package/dist/connectors/remote-mcp/errors.js +488 -0
  35. package/dist/connectors/remote-mcp/oauth.js +473 -0
  36. package/dist/connectors/remote-mcp/resources.js +473 -0
  37. package/dist/connectors/remote-mcp/runtime.js +1 -0
  38. package/dist/connectors/remote-mcp/transport.js +555 -0
  39. package/dist/connectors/remote-mcp.js +62 -2863
  40. package/dist/credential-contract.d.ts +23 -0
  41. package/dist/credential-reads.js +17 -0
  42. package/dist/credential-verification.js +53 -0
  43. package/dist/credentials.d.ts +7 -0
  44. package/dist/credentials.js +60 -25
  45. package/dist/describe-config.d.ts +12 -2
  46. package/dist/describe-config.js +2 -0
  47. package/dist/discovery-health.js +199 -0
  48. package/dist/downstream-input.js +2 -0
  49. package/dist/errors.d.ts +55 -17
  50. package/dist/errors.js +95 -41
  51. package/dist/execute.js +152 -114
  52. package/dist/executor-result.js +3 -1
  53. package/dist/guest-functions.js +83 -0
  54. package/dist/guest-runtime.js +60 -19
  55. package/dist/guest-types.d.ts +19 -5
  56. package/dist/http/argument-problems.js +35 -0
  57. package/dist/http/errors.js +5 -4
  58. package/dist/http/fields.js +192 -13
  59. package/dist/http/hatch-response.js +73 -0
  60. package/dist/http/hatch.js +88 -23
  61. package/dist/http/oauth.js +3 -2
  62. package/dist/http/provider.js +32 -4
  63. package/dist/http/safety.js +8 -0
  64. package/dist/http/tools.js +44 -23
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +9 -2
  67. package/dist/invocation.js +26 -8
  68. package/dist/membership.d.ts +13 -5
  69. package/dist/membership.js +34 -2
  70. package/dist/meta-tools.js +54 -31
  71. package/dist/native-recovery.js +4 -16
  72. package/dist/native-tools.js +5 -2
  73. package/dist/oauth-sealing.js +10 -0
  74. package/dist/operation-vocabulary.js +135 -13
  75. package/dist/operator-record.d.ts +2 -2
  76. package/dist/operator-record.js +1 -0
  77. package/dist/operator-ui/contract.d.ts +10 -0
  78. package/dist/operator-ui/generated.js +2 -2
  79. package/dist/operator-ui/model.d.ts +22 -1
  80. package/dist/provider-call-facts.js +6 -2
  81. package/dist/providers/_shared/rest/operation-index.js +48 -50
  82. package/dist/providers/_shared/rest/value-safety.js +80 -0
  83. package/dist/providers/breeze/api.js +7 -1
  84. package/dist/providers/breeze/skill.generated.js +1 -1
  85. package/dist/providers/breeze/tools.js +4 -3
  86. package/dist/providers/ccb/api.js +2 -1
  87. package/dist/providers/ccb/index.js +5 -0
  88. package/dist/providers/ccb/skill.generated.js +1 -1
  89. package/dist/providers/cloudflare/api.js +8 -1
  90. package/dist/providers/cloudflare/skill.generated.js +1 -1
  91. package/dist/providers/docs/lib/schemas.js +24 -22
  92. package/dist/providers/docs/safety.js +2 -0
  93. package/dist/providers/docs/skill.generated.js +1 -1
  94. package/dist/providers/docs/tools.js +2 -4
  95. package/dist/providers/drive/api.js +1 -0
  96. package/dist/providers/drive/lib/contracts.js +8 -5
  97. package/dist/providers/drive/skill.generated.js +1 -1
  98. package/dist/providers/drive/tools.js +11 -16
  99. package/dist/providers/forms/api.js +1 -0
  100. package/dist/providers/forms/skill.generated.js +1 -1
  101. package/dist/providers/forms/tools.js +10 -7
  102. package/dist/providers/github/api.js +1 -0
  103. package/dist/providers/github/lib/discovery.js +1 -1
  104. package/dist/providers/github/skill.generated.js +1 -1
  105. package/dist/providers/github/tools.js +61 -17
  106. package/dist/providers/gmail/lib/schemas.js +60 -50
  107. package/dist/providers/gmail/skill.generated.js +1 -1
  108. package/dist/providers/gmail/tools.js +7 -5
  109. package/dist/providers/granola/index.js +2 -0
  110. package/dist/providers/granola/skill.generated.js +1 -1
  111. package/dist/providers/overflow/api.js +7 -1
  112. package/dist/providers/overflow/skill.generated.js +1 -1
  113. package/dist/providers/planning-center/api.js +3 -1
  114. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  115. package/dist/providers/planning-center/skill.generated.js +1 -1
  116. package/dist/providers/quickbooks/safety.js +80 -39
  117. package/dist/providers/quickbooks/skill.generated.js +1 -1
  118. package/dist/providers/sheets/api.js +1 -0
  119. package/dist/providers/sheets/skill.generated.js +1 -1
  120. package/dist/providers/sheets/tools.js +13 -14
  121. package/dist/providers/slides/skill.generated.js +1 -1
  122. package/dist/providers/slides/tools.js +15 -13
  123. package/dist/providers/stripe/skill.generated.js +1 -1
  124. package/dist/providers/tithely/api.js +3 -1
  125. package/dist/providers/tithely/skill.generated.js +1 -1
  126. package/dist/providers/vercel/api.js +1 -1
  127. package/dist/providers/vercel/skill.generated.js +1 -1
  128. package/dist/providers/vercel/tools.js +4 -3
  129. package/dist/registry.d.ts +10 -4
  130. package/dist/registry.js +41 -13
  131. package/dist/routes/credentials.js +129 -60
  132. package/dist/routes/dispatch.js +5 -1
  133. package/dist/routes/mcp.js +30 -14
  134. package/dist/routes/operator.js +2 -2
  135. package/dist/routes/shared.d.ts +6 -1
  136. package/dist/routes/shared.js +7 -1
  137. package/dist/routes/ui-config.js +2 -2
  138. package/dist/routes/ui-data.js +126 -20
  139. package/dist/routes/ui.js +16 -0
  140. package/dist/runtime/call.js +7 -2
  141. package/dist/runtime/native-storage.js +12 -0
  142. package/dist/runtime/request-reads.js +207 -0
  143. package/dist/runtime/request-scope.js +16 -0
  144. package/dist/runtime/storage.js +12 -0
  145. package/dist/skills.js +37 -20
  146. package/dist/storage/keys.js +53 -3
  147. package/dist/storage/memory.js +8 -2
  148. package/dist/storage/sql-native.js +53 -1
  149. package/dist/storage/sql.js +12 -2
  150. package/dist/storage-digest.js +32 -0
  151. package/dist/timeout.js +8 -0
  152. package/dist/types.d.ts +33 -0
  153. package/dist/usage-guide.js +16 -35
  154. package/dist/validate.d.ts +3 -0
  155. package/dist/validate.js +220 -158
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/dist/write-outcome.js +0 -3
  159. package/documentation/README.md +13 -13
  160. package/documentation/architecture.md +385 -20
  161. package/documentation/auth.md +233 -64
  162. package/documentation/code-mode.md +122 -39
  163. package/documentation/deploying.md +6 -3
  164. package/documentation/integrating.md +16 -2
  165. package/documentation/meta-tools.md +240 -49
  166. package/documentation/migration-0.33.md +5 -1
  167. package/documentation/migration-0.34.md +3 -1
  168. package/documentation/operator-tests.md +1 -1
  169. package/documentation/operator-ui.md +105 -12
  170. package/documentation/pool-membership-upgrade.md +44 -0
  171. package/documentation/providers.md +105 -0
  172. package/examples/worker/AGENTS.md +10 -10
  173. package/examples/worker/README.md +4 -3
  174. package/examples/worker/src/connecta.config.ts +1 -0
  175. package/guides/connecting-agents.md +2 -1
  176. package/guides/how-it-works.md +4 -2
  177. package/package.json +1 -1
  178. package/templates/node/AGENTS.md +13 -12
  179. package/templates/node/package.json +1 -1
  180. package/templates/node/src/connecta.config.ts +1 -0
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,
@@ -425,6 +429,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
425
429
  concurrency: limits.discoveryConcurrency,
426
430
  probeTimeoutMs: limits.probeTimeoutMs,
427
431
  defer: limits.defer,
432
+ routes: limits.routes,
428
433
  });
429
434
  const invocation = new InvocationService(registry, catalog, activity);
430
435
  const maxHostCalls = Math.max(1, Math.trunc(limits.maxHostCalls ?? EXECUTE_MAX_HOST_CALLS));
@@ -468,6 +473,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
468
473
  source: "execute_code",
469
474
  sentSecrets,
470
475
  timeoutMs,
476
+ ...(limits.routes ? { routes: limits.routes } : {}),
471
477
  ...(signal !== undefined ? { requestSignal: signal } : {}),
472
478
  ...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
473
479
  unwrapResult: true,
@@ -698,6 +704,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
698
704
  catalogDependency(resolved.connector.id, resolved.toolName);
699
705
  };
700
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
+ },
701
712
  call,
702
713
  result: (id, options, signal, onStage) => Effect.tryPromise({
703
714
  try: async () => {
@@ -822,7 +833,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
822
833
  const args = (raw ?? {});
823
834
  const result = flatSearchResult(await catalog.search({
824
835
  ...args,
825
- includeSchemas: args.includeSchemas ?? "json",
836
+ includeSchemas: args.includeSchemas ?? "typescript",
826
837
  includeSchemaKeys: args.includeSchemaKeys !== false,
827
838
  }, catalogDependency));
828
839
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
@@ -856,90 +867,102 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
856
867
  const failure = boundedGuestFailure(redactAgentOutput(sentSecrets, err));
857
868
  return Effect.fail(failure);
858
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
+ };
859
878
  return {
860
879
  name: "connecta",
861
880
  // Trusted host code the program runs after and cannot undo.
862
881
  prelude: guestPrelude(),
863
- fns: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
864
- name,
865
- (...args) => {
866
- if (budgetFailure)
867
- return stopped;
868
- // Invalid emits still allocate guest promises and typed rejections.
869
- // Bound those attempts without consuming the invocation budget or discarding identities.
870
- if (name === "emit" && ++emitAttempts > maxEmitAttempts) {
871
- budgetFailure = guestFailure("budget_exceeded", `execute_code emission-attempt budget exceeded (${maxEmitAttempts} attempts maximum).`);
872
- programWrites?.close();
873
- limits.dispatchController?.abort();
874
- limits.onHostCallBudgetExceeded?.(budgetFailure);
875
- return stopped;
876
- }
877
- // L4/M7: spend on entry, before even invalid arguments are checked;
878
- // emit has a separate budget and never spends this one.
879
- // Transparent transfer pages consume the already-admitted call, not guest host calls.
880
- const counted = name !== "emit" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
881
- if (counted && ++hostCalls.attempted > maxHostCalls) {
882
- hostCalls.failed++;
883
- budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
884
- programWrites?.close();
885
- limits.dispatchController?.abort();
886
- limits.onHostCallBudgetExceeded?.(budgetFailure);
887
- return stopped;
888
- }
889
- if (counted)
890
- hostCalls.admitted++;
891
- const started = Date.now();
892
- const utility = name === "result" || name === "skill";
893
- let stage = name === "result" ? "result storage" : "skill lookup";
894
- const invoke = Effect.suspend(() => operation(...args));
895
- const guarded = utility
896
- ? withDeadlineEffect((utilitySignal) => Effect.suspend(() => operation(args[0], args[1], utilitySignal, (current) => {
897
- stage = current;
898
- })), {
899
- timeoutMs: hostCallTimeoutMs,
900
- ...(hostAccessSignal ? { signal: hostAccessSignal } : {}),
901
- timeoutError: guestFailure("timeout", `connecta.${name} timed out.`),
902
- })
903
- : invoke;
904
- const settled = runEdge(guarded.pipe(Effect.catch((error) => {
905
- if (utility && hostAccessSignal?.aborted)
906
- return typedFailure(guestFailure("cancelled", `connecta.${name} was cancelled because the run ended.`));
907
- if (utility && error instanceof InvocationFailure && error.code === "timeout") {
908
- const elapsedMs = Date.now() - started;
909
- const message = `Operation "connecta.${name}" timed out during ${stage} after ${elapsedMs}ms (effective deadline ${hostCallTimeoutMs}ms).`;
910
- return typedFailure(new InvocationFailure({
911
- code: "timeout",
912
- message,
913
- retryable: true,
914
- details: {
915
- operation: `connecta.${name}`,
916
- stage,
917
- elapsedMs,
918
- deadlineMs: hostCallTimeoutMs,
919
- },
920
- }));
921
- }
922
- return typedFailure(error);
923
- }))).then((value) => {
924
- if (counted)
925
- hostCalls.succeeded++;
926
- return budgetFailure ? stopped : redactAgentOutput(sentSecrets, value);
927
- }, (err) => {
928
- if (counted)
929
- 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) => {
930
891
  if (budgetFailure)
931
892
  return stopped;
932
- throw redactAgentOutput(sentSecrets, err);
933
- });
934
- // The executor owns this promise, and a program may abandon a call
935
- // that the run's end then cancels before anyone has awaited it.
936
- // The rejection is still there for whoever does; it is just not an
937
- // unhandled rejection of the host's, which workerd reports as soon
938
- // as a microtask passes without a handler.
939
- settled.catch(() => { });
940
- return settled;
941
- },
942
- ])),
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
+ })),
943
966
  };
944
967
  }
945
968
  /**
@@ -1054,6 +1077,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1054
1077
  const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
1055
1078
  const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
1056
1079
  const resultDependencies = [];
1080
+ const unawaited = { read: false };
1057
1081
  const dispatchController = new AbortController();
1058
1082
  const terminal = Deferred.makeUnsafe();
1059
1083
  let budgetFailure;
@@ -1104,9 +1128,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1104
1128
  defer: config.defer,
1105
1129
  readOnly: config.readOnly,
1106
1130
  maxWrites: config.maxWrites,
1131
+ routes: config.routes,
1107
1132
  programWrites,
1108
1133
  resultDependencies,
1109
1134
  dispatchController,
1135
+ unawaited,
1110
1136
  }, requestScope)));
1111
1137
  if (signal.aborted) {
1112
1138
  return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
@@ -1142,6 +1168,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
1142
1168
  emitted,
1143
1169
  diagnostics,
1144
1170
  program,
1171
+ unawaited,
1145
1172
  // Emitted text shares the return cap; a follow-up page run gets all of it.
1146
1173
  guardResult: (value) => authCall(guardProgramReturn, [
1147
1174
  value,
@@ -1256,7 +1283,7 @@ function failedRun(err, logger, { emitted, diagnostics, program }) {
1256
1283
  });
1257
1284
  }
1258
1285
  /** The response for an ExecuteResult: the program's error or its value. */
1259
- function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
1286
+ function finishedRun(outcome, { emitted, diagnostics, program, unawaited, guardResult }) {
1260
1287
  return authFlow(function* () {
1261
1288
  if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
1262
1289
  return failureResponse("Executor failed: expected an ExecuteResult object.", {
@@ -1300,6 +1327,7 @@ function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
1300
1327
  name: outcome.failure?.name ?? /^(SyntaxError|ReferenceError|TypeError|RangeError):/.exec(outcome.error)?.[1],
1301
1328
  message: outcome.error || "Execution failed without an error message.",
1302
1329
  ...(outcome.failure?.line !== undefined ? { line: outcome.failure.line - 1 } : {}),
1330
+ unawaited: unawaited.read,
1303
1331
  }, program),
1304
1332
  });
1305
1333
  }
@@ -1356,50 +1384,59 @@ function failureResponse(message, options) {
1356
1384
  function discardedEmits(emitted) {
1357
1385
  return emitted.blocks.length > 0 ? { emittedDiscarded: emitted.blocks.length } : {};
1358
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
+ */
1359
1402
  function connectorInventory(connectors) {
1360
1403
  const prefix = "Connectors: ";
1361
1404
  if (connectors.length === 0)
1362
1405
  return `${prefix}none.`;
1363
- const entries = connectors.map((connector) => {
1364
- const address = connector.id;
1406
+ const fits = (line) => boundedEchoText(line, CONNECTOR_INVENTORY_MAX_BYTES) === line;
1407
+ const entry = (connector, detail) => {
1408
+ const notes = [];
1365
1409
  const title = connector.title?.replace(/\s+/g, " ").trim();
1366
- const label = title && title !== connector.id ? `${address}: ${boundedEchoText(title, 45)}` : address;
1367
- if (!connectorGuide(connector))
1368
- return label;
1369
- const requirement = connectorGuideRequired(connector) ? "required guide" : "guide";
1370
- return `${label} (${requirement} ${connectorSkillName(connector.id)})`;
1371
- });
1372
- const shown = [];
1373
- for (let index = 0; index < entries.length; index++) {
1374
- const entry = entries[index];
1375
- if (entry === undefined)
1376
- break;
1377
- const candidate = [...shown, entry].join(", ");
1378
- const omitted = entries.length - index - 1;
1379
- const suffix = omitted > 0 ? `; +${omitted} more.` : ".";
1380
- const serialized = prefix + candidate + suffix;
1381
- if (boundedEchoText(serialized, CONNECTOR_INVENTORY_MAX_BYTES) !== serialized) {
1382
- 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}`);
1383
1416
  }
1384
- 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;
1385
1423
  }
1386
- const omitted = entries.length - shown.length;
1387
- if (omitted === 0)
1388
- return `${prefix}${shown.join(", ")}.`;
1389
- 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.`;
1390
1429
  }
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)`}.
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)`}.
1392
1431
  ${connectorInventory(connectors)}
1393
1432
 
1394
1433
  Use async () => { ... }, no arguments. No portable ambient capabilities. API:
1395
1434
  \`\`\`ts
1396
- declare const connecta: {
1397
1435
  ${GUEST_API_DECLARATION}
1398
- };
1399
1436
  \`\`\`
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.
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.
1401
1438
 
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.`;
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" : ""}.`;
1403
1440
  // Module scope, like the other five meta-tool inputs: its JSON Schema is
1404
1441
  // derived once per process. Budgets and connectors vary by deployment and
1405
1442
  // view, so they live in the description, never here.
@@ -1439,13 +1476,14 @@ export function registerExecuteTool(server, registry, ctx) {
1439
1476
  maxWrites,
1440
1477
  maxResultChars: ctx.maxResultChars,
1441
1478
  requestScope: ctx.requestScope,
1479
+ routes: ctx.routes,
1442
1480
  });
1443
1481
  server.registerTool("execute_code", {
1444
1482
  description: executeDescription(emitBudgets, {
1445
1483
  ...hostLimits,
1446
1484
  // connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
1447
1485
  callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
1448
- }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true),
1486
+ }, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true, ctx.routes),
1449
1487
  inputSchema: EXECUTE_INPUT,
1450
1488
  outputSchema: EXECUTE_OUTPUT,
1451
1489
  // The client sees execute_code as a write exactly when the endpoint writes.
@@ -1480,6 +1518,6 @@ export function registerExecuteTool(server, registry, ctx) {
1480
1518
  const result = yield* ctx.authElicitation
1481
1519
  ? authInvoke(ctx.authElicitation, "run", ["execute_code", args, extra, run])
1482
1520
  : authCall(run, []);
1483
- return ctx.native ? nativeRecovery(result, true) : result;
1521
+ return ctx.native ? nativeRecovery(result) : result;
1484
1522
  })));
1485
1523
  }
@@ -1,3 +1,4 @@
1
+ import { META_ROUTES } from "./agent-routes.js";
1
2
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
2
3
  import { msg } from "./errors.js";
3
4
  /** ~6k tokens. Sandbox code should filter data down before returning. */
@@ -7,7 +8,8 @@ export function serializeResultText(value) {
7
8
  const serialized = JSON.stringify(value);
8
9
  return serialized === undefined ? String(value) : serialized;
9
10
  }
10
- 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`;
11
13
  /**
12
14
  * Shape the over-cap notice so the **serialized envelope** fits the same cap
13
15
  * the raw value missed. Escaping matters: a preview sliced to the cap is JSON
@@ -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 };`;
@@ -1,3 +1,4 @@
1
+ import { GUEST_FUNCTIONS } from "./guest-functions.js";
1
2
  /** Capture the single-use runner before evaluating the guest expression. */
2
3
  export function wrapGuestProgram(code) {
3
4
  return `async () => globalThis.__connecta_run()(async (connecta) => await (\n${code}\n)())`;
@@ -62,6 +63,34 @@ export function guestPromiseInitializer() {
62
63
  Object.freeze(NativePromise.prototype);
63
64
  })()`;
64
65
  }
66
+ /**
67
+ * The frozen namespace, joined from the registry. A `wrapped` or `emission`
68
+ * function uses the initializer's local of the same name; an alias shares one
69
+ * function object. Only guest source enters this string.
70
+ */
71
+ const GUEST_NAMESPACE = (() => {
72
+ const before = [];
73
+ const entries = ["__proto__: null"];
74
+ for (const fn of GUEST_FUNCTIONS) {
75
+ if (fn.budget === "once")
76
+ continue;
77
+ if (fn.budget === "emission") {
78
+ entries.push(`${fn.name}(block) {
79
+ const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
80
+ return trackEmission(${fn.name}(ref || block));
81
+ }`);
82
+ continue;
83
+ }
84
+ const value = `hosted(${"wrapped" in fn ? fn.name : `provider.${fn.name}`})`;
85
+ if (!("alias" in fn)) {
86
+ entries.push(`${fn.name}: ${value}`);
87
+ continue;
88
+ }
89
+ before.push(` const ${fn.name} = ${value};\n`);
90
+ entries.push(fn.name, `// Undeclared alias: ${fn.alias.note}\n ${fn.alias.name}: ${fn.name}`);
91
+ }
92
+ return `${before.join("")} const namespace = freeze({\n ${entries.join(",\n ")}\n });`;
93
+ })();
65
94
  /** Capture a private initializer before a Worker evaluates its guest module. */
66
95
  export function guestInitializer() {
67
96
  return `(() => {
@@ -152,21 +181,17 @@ export function guestInitializer() {
152
181
  }
153
182
  });
154
183
  }
155
- const namespace = freeze({
156
- __proto__: null,
157
- search: provider.search,
158
- describe: provider.describe,
159
- call,
160
- read: provider.read,
161
- result: provider.result,
162
- skill: provider.skill,
163
- // Undeclared alias: models write connecta.skills, the top-level tool's name.
164
- skills: provider.skill,
165
- emit(block) {
166
- const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
167
- return trackEmission(emit(ref || block));
168
- }
169
- });
184
+ // Result fields read off an unawaited host call flag the run, still undefined.
185
+ let unawaited = false;
186
+ const fields = {};
187
+ for (const key of ["data", "format", "content", "tools", "catalogErrors", "total", "text", "resultId", "hasMore", "nextOffset"])
188
+ fields[key] = { get() { unawaited = true; } };
189
+ const hosted = (fn) => function () {
190
+ const promise = apply(fn, undefined, arguments);
191
+ if (promise && typeof promise === "object") defineProperties(promise, fields);
192
+ return promise;
193
+ };
194
+ ${GUEST_NAMESPACE}
170
195
  globalThis.connecta = namespace;
171
196
  defineProperties(globalThis, {
172
197
  Error: { value: NativeError, writable: false, configurable: false },
@@ -176,7 +201,13 @@ export function guestInitializer() {
176
201
  return async (program) => {
177
202
  if (used) throw new NativeError("Guest runner initialization is single-use.");
178
203
  used = true;
179
- const result = await program(namespace);
204
+ let result;
205
+ try {
206
+ result = await program(namespace);
207
+ } catch (error) {
208
+ if (unawaited) try { await provider.unawaited(); } catch {}
209
+ throw error;
210
+ }
180
211
  for (let i = 0; i < pending.length; i++) {
181
212
  const report = await pending[i].settled;
182
213
  if (!report.ok && !pending[i].handled) throw report.error;
@@ -200,8 +231,15 @@ const ERROR_NAMES = new Set([
200
231
  ]);
201
232
  /** Fixed repair guidance, used only in agent-facing results. */
202
233
  export function programError(raw, code) {
203
- const message = typeof raw.message === "string" ? raw.message : "Program failed.";
204
- const name = typeof raw.name === "string" && ERROR_NAMES.has(raw.name) ? raw.name : "Error";
234
+ const text = typeof raw.message === "string" ? raw.message : "Program failed.";
235
+ // Executors may report "TypeError: …" as the message itself; name it once.
236
+ const prefix = /^([A-Za-z]*Error): /.exec(text)?.[1];
237
+ const name = typeof raw.name === "string" && ERROR_NAMES.has(raw.name)
238
+ ? raw.name
239
+ : prefix && ERROR_NAMES.has(prefix)
240
+ ? prefix
241
+ : "Error";
242
+ const message = prefix === name ? text.slice(name.length + 2) : text;
205
243
  const stack = typeof raw.stack === "string" ? raw.stack : "";
206
244
  // Compare QuickJS/V8 locations to the wrapper baseline after the source.
207
245
  const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
@@ -217,7 +255,10 @@ export function programError(raw, code) {
217
255
  ? Math.max(1, Number(location[1]) - Number(baseline[1]) + code.split("\n").length + 1)
218
256
  : null;
219
257
  let hint = "Use plain JavaScript in one async () => { ... } expression and the connecta global.";
220
- if (/\brequire\b|\b[Ii]mports?\b|\bfs\b|filesystem|node:/.test(message) ||
258
+ if (raw.unawaited === true) {
259
+ hint = "connecta.* returns a Promise; await it: (await connecta.call(address, args)).data.";
260
+ }
261
+ else if (/\brequire\b|\b[Ii]mports?\b|\bfs\b|filesystem|node:/.test(message) ||
221
262
  (name === "SyntaxError" && /\bimport\b/.test(code))) {
222
263
  hint =
223
264
  "Imports, require, and filesystem access are outside the guest API. Use connecta.call to access configured services.";