@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
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // A vendor is an `HttpApi`: endpoints with Schemas for path params, query,
4
4
  // and payload, from which requests are encoded and the endpoint manifest is
@@ -13,13 +13,13 @@
13
13
  // empty 200. Every answer, from a declared operation or a hatch's path, takes
14
14
  // one route: bounded read, JSON, closed public classifier facts captured,
15
15
  // the request's sent secrets redacted from
16
- // every value of the whole body before anything can cut or extract from it
17
- // (#911), its keys kept for value safety to match by name, status (with
16
+ // every value of the whole body before anything can cut or extract from it,
17
+ // its keys kept for value safety to match by name, status (with
18
18
  // its operation, so value safety decides what a failure may quote), the
19
19
  // vendor's `result` hook, then the connection's ResultSafety. A provider
20
20
  // cannot reach a body earlier. A Content-Type redaction altered selects the
21
21
  // format the vendor reads as JSON, so no altered type routes a body around
22
- // ResultSafety as text or bytes (#703).
22
+ // ResultSafety as text or bytes.
23
23
  import { Context, Effect } from "effect";
24
24
  import * as HttpClientRequest from "effect/http/HttpClientRequest";
25
25
  import * as HttpApi from "effect/http-api/HttpApi";
@@ -233,12 +233,12 @@ export function connect(vendor, guarded, options) {
233
233
  : defect)));
234
234
  });
235
235
  // Header-derived format decisions see only redacted Content-Type. Names are protocol
236
- // constants `Headers` must accept, so only the values are redacted, together (#703).
236
+ // constants `Headers` must accept, so only the values are redacted, together.
237
237
  const entries = [...reply.headers];
238
238
  const values = secrets.redact(entries.map(([, value]) => value));
239
239
  const headers = new Headers(entries.map(([name], index) => [name, values[index]]));
240
240
  // Whole, before a detail line trims it or a projection extracts from it. Values only:
241
- // result safety and projections match fields by their vendor names (#703).
241
+ // result safety and projections match fields by their vendor names.
242
242
  let classifierFacts;
243
243
  const parsed = reply.bytes !== undefined
244
244
  ? {
@@ -400,7 +400,7 @@ export function connect(vendor, guarded, options) {
400
400
  /**
401
401
  * The Content-Type a format decision reads. One redaction altered reads as
402
402
  * `application/json`: a vendor's text or binary choice for it would hand
403
- * result safety a body it cannot read field by field (#703).
403
+ * result safety a body it cannot read field by field.
404
404
  */
405
405
  function formatType(redacted) {
406
406
  return redactionAltered(redacted) ? "application/json" : redacted.toLowerCase();
package/dist/identity.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { sha256Hex } from "./bytes.js";
1
2
  const IDENTITY_PART_RE = /^[\x21-\x7e]{1,256}$/;
2
- const encoder = new TextEncoder();
3
3
  export function validIdentityReference(value) {
4
4
  return Boolean(value &&
5
5
  typeof value.namespace === "string" &&
@@ -7,11 +7,7 @@ export function validIdentityReference(value) {
7
7
  IDENTITY_PART_RE.test(value.namespace) &&
8
8
  IDENTITY_PART_RE.test(value.id));
9
9
  }
10
- function bytesToHex(bytes) {
11
- return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
12
- }
13
10
  /** Deterministic pseudonymous partition; raw emails and ids do not enter keys. */
14
11
  export async function identityStorageKey(identity) {
15
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(`${identity.namespace}\n${identity.id}`));
16
- return bytesToHex(new Uint8Array(digest));
12
+ return sha256Hex(`${identity.namespace}\n${identity.id}`);
17
13
  }
package/dist/index.d.ts CHANGED
@@ -41,6 +41,6 @@ export type { Registry } from "./registry.js";
41
41
  export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRequestAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
42
42
  export type { AgentRequestPrincipal, HumanRequestPrincipal, PrincipalForwarding, PrincipalRequestToken, RequestPrincipal, RequestPrincipalPolicy, RequestToken, RequestTokenInput, } from "./request-token.js";
43
43
  export type { ApiHandlerContext, ApiOAuthAccess, ApiOAuthClientAuthentication, ApiOAuthConfig, ApiOptions, ApiTool, } from "./connectors/api.js";
44
- export type { ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, ConnectorUsageGuide, ConnectorSkill, ConnectorSkillResourceContents, DescribedEndpoint, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
44
+ export type { ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, AgentRouteNames, ConnectorContext, ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, ConnectorUsageGuide, ConnectorSkill, ConnectorSkillResourceContents, DescribedEndpoint, ConnectorStatus, CredentialFailureClass, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
45
45
  export type { ActivityActor, ActivityCatalogChange, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, ToolCallActivityEvent, } from "./activity.js";
46
46
  export { META_TOOL_NAMES } from "./meta-tool-names.js";
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
+ import { deploymentRoutes } from "./agent-routes.js";
1
2
  import { describeConfigSources } from "./config-value-sources.js";
2
3
  import { credentialTestRule, describeCredentialTestMismatch } from "./credential-rules.js";
3
4
  import { Registry } from "./registry.js";
4
5
  import { parseConnectorAccess, POOL_NAME_RE } from "./connector-access.js";
5
6
  import { assertsAgents } from "./agent-identity.js";
6
7
  import { ConfigError } from "./config-schema.js";
7
- import { memberContext, resolveMaintainers, resolveMembers, } from "./membership.js";
8
+ import { memberContext, peopleInSeveralPools, resolveMaintainers, resolvePoolMembers, } from "./membership.js";
8
9
  import { createFetchHandler } from "./server.js";
9
10
  import { droppedBrandingUrls, droppedThemeTokens, droppedUiAuthUrls } from "./branding.js";
10
11
  import { readConfig } from "./config.js";
@@ -68,7 +69,7 @@ function resolvePools(pools, readOnly, registry, members) {
68
69
  resolved.set(name, Object.freeze({
69
70
  access,
70
71
  readOnly: pool.readOnly ?? readOnly,
71
- members: Object.freeze(resolveMembers(pool.members, path, members)),
72
+ members: Object.freeze(resolvePoolMembers(pool.members, path, members)),
72
73
  ...(pool.owner !== undefined ? { owner: pool.owner.trim() } : {}),
73
74
  ...(pool.description !== undefined ? { description: pool.description.trim() } : {}),
74
75
  ...(pool.surface === "native" ? { native: { executeCode: pool.executeCode ?? true } } : {}),
@@ -185,6 +186,7 @@ export function createConnecta(config) {
185
186
  // Every structural check runs here, before any work: what comes back is the
186
187
  // configuration the deployment runs with, defaults applied once.
187
188
  const { input, resolved } = readConfig(config);
189
+ const routes = deploymentRoutes(config);
188
190
  const { logger } = resolved;
189
191
  const storage = resolved.storage;
190
192
  const registry = new Registry([...resolved.connectors], {
@@ -208,10 +210,14 @@ export function createConnecta(config) {
208
210
  maxResultBytes: resolved.calls.maxResultBytes,
209
211
  results: resolved.results,
210
212
  classification: resolved.classification,
213
+ routes,
211
214
  });
212
215
  const members = memberContext(resolved.auth, resolved.agents);
213
216
  const maintainers = resolveMaintainers(input.maintainers, resolved.auth.some((provider) => !assertsAgents(provider)), members);
214
217
  const pools = resolvePools(resolved.pools, resolved.readOnly, registry, members);
218
+ for (const person of peopleInSeveralPools(pools, maintainers)) {
219
+ logger.warn("[connecta] Plain /mcp refuses a person in several pools", person);
220
+ }
215
221
  warnInsecureConfig(resolved);
216
222
  const requestAdmission = new AdmissionController(resolved.admission.requests);
217
223
  let codeAdmission;
@@ -249,6 +255,7 @@ export function createConnecta(config) {
249
255
  executor,
250
256
  executorName: configuredExecutorName,
251
257
  requestAdmission,
258
+ routes,
252
259
  });
253
260
  let closePromise;
254
261
  return {
@@ -1,3 +1,4 @@
1
+ import { bytesToHex } from "./bytes.js";
1
2
  import { authCall, authFlow, registerAuth } from "./auth/native.js";
2
3
  import { upwardStep } from "./upward.js";
3
4
  import { runEdge } from "./runtime/run.js";
@@ -34,7 +35,7 @@ export async function classificationDigest(definition) {
34
35
  annotations,
35
36
  schema,
36
37
  }));
37
- return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
38
+ return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
38
39
  }
39
40
  catch {
40
41
  // A digest refusal blocks recovery, not an ordinary call.
@@ -1,3 +1,4 @@
1
+ import { routePhrases } from "./agent-routes.js";
1
2
  import { authCall, registerAuthMethod } from "./auth/native.js";
2
3
  import { withAuthorizationHandoffEffect } from "./authorization-handoff.js";
3
4
  import { hasControlCharacters } from "./tool-name.js";
@@ -9,7 +10,7 @@ import { downstreamValue } from "./mcp-result.js";
9
10
  import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
10
11
  import { splitAddress } from "./registry.js";
11
12
  import { causeError, runEdge, withDeadlineEffect } from "./runtime/run.js";
12
- import { validateCatalogToolInput } from "./validate.js";
13
+ import { schemaExample, validateCatalogToolInput } from "./validate.js";
13
14
  import { sentSecretsFor, sentSecretsForRequest } from "./sent-secrets.js";
14
15
  import { recoveryRecord, recordRecoveryFacts } from "./call-recovery.js";
15
16
  import { callFromContext, contextView, viewServices, Credentials } from "./runtime/call.js";
@@ -141,6 +142,7 @@ export class InvocationFailure extends Error {
141
142
  }
142
143
  }
143
144
  keepErrorName(InvocationFailure, "InvocationFailure");
145
+ const withoutExample = ({ example: _example, ...stated }) => stated;
144
146
  /**
145
147
  * Shared downstream invocation engine. Address/catalog resolution is delegated
146
148
  * to the request-local CatalogService; every remaining call semantic lives
@@ -406,6 +408,14 @@ export class InvocationService {
406
408
  return error;
407
409
  return {
408
410
  ...error,
411
+ // An example comes only from the published schema, never a connector's copy of the
412
+ // call, unless the connector states why there is none.
413
+ validation: {
414
+ ...withoutExample(error.validation),
415
+ ...(error.validation.exampleUnavailable === undefined && resolved?.definition.inputSchema
416
+ ? schemaExample(resolved.definition.inputSchema)
417
+ : {}),
418
+ },
409
419
  connector: target.connector.id,
410
420
  operation: `${target.connector.id}.${target.toolName}`,
411
421
  nextAction: this.catalog.searchRecovery({
@@ -552,7 +562,7 @@ export class InvocationService {
552
562
  return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and ${connectorReadOnly ? `connector "${target.connector.id}"` : "this endpoint"} is read-only. Nothing here can send it.`);
553
563
  }
554
564
  if (refusal === "destructive_tool_requires_approval") {
555
- return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
565
+ return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. ${routePhrases(context).writeNeedsApproval}`);
556
566
  }
557
567
  // Remote MCP tools advertise their input schema in the catalog. Validate
558
568
  // against that same request-local definition before admission or provider
@@ -580,7 +590,12 @@ export class InvocationService {
580
590
  return yield* Effect.fail(callSignal.reason);
581
591
  attempts = 1;
582
592
  const call = (permit) => Effect.gen({ self: this }, function* () {
583
- const operationContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
593
+ const operationContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({
594
+ signal: callSignal,
595
+ timeoutMs: context.timeoutMs,
596
+ defer: this.catalog.defer,
597
+ routes: context.routes,
598
+ }));
584
599
  const lease = callFromContext(operationContext, write ? "write" : "read", {
585
600
  admitted: true,
586
601
  // An invocation deadline aborts when its call finishes. A
@@ -604,11 +619,11 @@ export class InvocationService {
604
619
  yield* Effect.gen(function* () {
605
620
  if (target.connector.credential) {
606
621
  if (!connectorContext.credential) {
607
- throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
608
- `authorize_connector({ connector: "${target.connector.id}" }).`);
622
+ throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. " +
623
+ routePhrases(connectorContext).authorizeCall(target.connector.id));
609
624
  }
610
625
  if (!(yield* Context.get(lease.services, Credentials).values)) {
611
- throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. Call authorize_connector.`);
626
+ throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. ${routePhrases(connectorContext).authorizeCall()}`);
612
627
  }
613
628
  }
614
629
  yield* resolveInvocationAuthEffect(connector, callTool, connectorContext);
@@ -22,6 +22,7 @@ export interface ResolvedPool {
22
22
  readOnly: boolean;
23
23
  /** Exactly the pool's `tools`; nothing intersects or widens it. */
24
24
  access: ConnectorAccess;
25
+ /** Non-empty and homogeneous: all people or all agents. */
25
26
  members: readonly Member[];
26
27
  /** Trimmed `owner`: the Workspace address agents in this pool act as. */
27
28
  owner?: string;
@@ -40,11 +41,8 @@ export interface MemberContext {
40
41
  }
41
42
  /** The namespaces bare ids may resolve in, read from the resolved providers. */
42
43
  export declare function memberContext(auth: readonly InboundAuth[], agents: readonly string[]): MemberContext;
43
- /**
44
- * Resolve one list of references, throwing on any that is malformed,
45
- * ambiguous, unknown, or repeated (INV-11).
46
- */
47
- export declare function resolveMembers(value: unknown, path: string, context: MemberContext): Member[];
44
+ /** Resolve a pool's members, which are all people or all agents (INV-11). */
45
+ export declare function resolvePoolMembers(value: unknown, path: string, context: MemberContext): Member[];
48
46
  /**
49
47
  * `maintainers`: people only. Required whenever an inbound provider can admit
50
48
  * a person, which is any provider but the built-in agent-token adapter. With
@@ -53,5 +51,15 @@ export declare function resolveMembers(value: unknown, path: string, context: Me
53
51
  export declare function resolveMaintainers(value: unknown, admitsPeople: boolean, context: MemberContext): readonly IdentityReference[];
54
52
  /** Whether an admitted identity is this member. An agent never matches a person. */
55
53
  export declare function isMember(member: Member, identity: Readonly<AuthenticatedIdentity>): boolean;
54
+ /**
55
+ * Non-maintainer people listed in several pools, in declaration order. Plain
56
+ * `/mcp` routes a person by their single pool, so it refuses each of these.
57
+ * An open deployment admits no people, so it has none.
58
+ */
59
+ export declare function peopleInSeveralPools(pools: ReadonlyMap<string, ResolvedPool>, maintainers: readonly IdentityReference[]): Array<{
60
+ namespace: string;
61
+ id: string;
62
+ pools: string[];
63
+ }>;
56
64
  /** Whether an admitted identity is a listed maintainer. */
57
65
  export declare function isMaintainer(maintainers: readonly IdentityReference[], identity: Readonly<AuthenticatedIdentity>): boolean;
@@ -65,7 +65,7 @@ const memberKey = (member) => member.kind === "agent" ? `agent\0${member.name}`
65
65
  * Resolve one list of references, throwing on any that is malformed,
66
66
  * ambiguous, unknown, or repeated (INV-11).
67
67
  */
68
- export function resolveMembers(value, path, context) {
68
+ function resolveMembers(value, path, context) {
69
69
  if (!Array.isArray(value)) {
70
70
  throw new ConfigError(`${path} must be an array of principal ids, { namespace, id }, or "agent:<name>".`);
71
71
  }
@@ -79,6 +79,14 @@ export function resolveMembers(value, path, context) {
79
79
  return Object.freeze(member);
80
80
  });
81
81
  }
82
+ /** Resolve a pool's members, which are all people or all agents (INV-11). */
83
+ export function resolvePoolMembers(value, path, context) {
84
+ const members = resolveMembers(value, path, context);
85
+ if (!members.every((member) => member.kind === members[0].kind)) {
86
+ throw new ConfigError(`${path} mixes people and agents; list only one kind.`);
87
+ }
88
+ return members;
89
+ }
82
90
  /**
83
91
  * `maintainers`: people only. Required whenever an inbound provider can admit
84
92
  * a person, which is any provider but the built-in agent-token adapter. With
@@ -88,7 +96,7 @@ export function resolveMaintainers(value, admitsPeople, context) {
88
96
  const path = "ConnectaConfig.maintainers";
89
97
  if (value === undefined || (Array.isArray(value) && value.length === 0)) {
90
98
  if (admitsPeople) {
91
- throw new ConfigError(`${path} is required with inbound auth: list the people who may use plain /mcp, administer shared ` +
99
+ throw new ConfigError(`${path} is required with inbound auth: list the people who receive the whole catalog at plain /mcp, administer shared ` +
92
100
  "credentials, manage agent tokens, and read activity.");
93
101
  }
94
102
  return Object.freeze([]);
@@ -115,6 +123,30 @@ export function isMember(member, identity) {
115
123
  principal.namespace === member.namespace &&
116
124
  principal.id === member.id);
117
125
  }
126
+ /**
127
+ * Non-maintainer people listed in several pools, in declaration order. Plain
128
+ * `/mcp` routes a person by their single pool, so it refuses each of these.
129
+ * An open deployment admits no people, so it has none.
130
+ */
131
+ export function peopleInSeveralPools(pools, maintainers) {
132
+ if (maintainers.length === 0)
133
+ return [];
134
+ const people = new Map();
135
+ for (const [name, pool] of pools) {
136
+ for (const member of pool.members) {
137
+ if (member.kind !== "human")
138
+ continue;
139
+ if (maintainers.some((m) => m.namespace === member.namespace && m.id === member.id))
140
+ continue;
141
+ const key = JSON.stringify([member.namespace, member.id]);
142
+ const person = people.get(key) ?? { namespace: member.namespace, id: member.id, pools: [] };
143
+ if (!person.pools.includes(name))
144
+ person.pools.push(name);
145
+ people.set(key, person);
146
+ }
147
+ }
148
+ return [...people.values()].filter((person) => person.pools.length > 1);
149
+ }
118
150
  /** Whether an admitted identity is a listed maintainer. */
119
151
  export function isMaintainer(maintainers, identity) {
120
152
  return maintainers.some((maintainer) => isMember({ kind: "human", ...maintainer }, identity));
@@ -1,3 +1,5 @@
1
+ import { META_ROUTES } from "./agent-routes.js";
2
+ import { toolError } from "./tool-error.js";
1
3
  import { upwardStep } from "./upward.js";
2
4
  import { authCall, authInvoke, authFlow, authMethod, authStorage, registerAuth } from "./auth/native.js";
3
5
  import { registryProgram } from "./runtime/registry.js";
@@ -20,7 +22,7 @@ import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT, SKILLS_OUTPUT } from "./meta-output.js
20
22
  import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
21
23
  import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
22
24
  import { captureDownstreamInput } from "./downstream-input.js";
23
- import { PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
25
+ import { PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
24
26
  export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
25
27
  const enc = new TextEncoder();
26
28
  // Each page is a slice of payload bytes, so a leading U+FEFF is data.
@@ -37,15 +39,7 @@ function errorResult(message) {
37
39
  return { content: [{ type: "text", text: message }], isError: true };
38
40
  }
39
41
  function discoveryErrorResult(error) {
40
- const result = jsonResult({
41
- error: {
42
- code: error.code,
43
- message: error.message,
44
- retryable: false,
45
- },
46
- });
47
- result.isError = true;
48
- return result;
42
+ return toolError({ code: error.code, message: error.message, retryable: false });
49
43
  }
50
44
  const discoveryResult = function (operation, hint) {
51
45
  return authFlow(function* () {
@@ -86,8 +80,8 @@ function isValidResultOffset(value) {
86
80
  * Backwards, never forwards: re-serving a few bytes the caller already has is
87
81
  * recoverable, silently skipping the rest of a character is not. Offsets the
88
82
  * server itself produced (`nextOffset`) are already boundaries and come back
89
- * unchanged, so this only moves an offset a client computed on its own
90
- * (issue #38). An offset at or past `bytes.length` is left alone — there is no
83
+ * unchanged, so this only moves an offset a client computed on its own.
84
+ * An offset at or past `bytes.length` is left alone — there is no
91
85
  * character there to split.
92
86
  */
93
87
  function alignStartToCharBoundary(bytes, offset) {
@@ -149,6 +143,15 @@ function base64Of(bytes) {
149
143
  }
150
144
  return out;
151
145
  }
146
+ /**
147
+ * The page a notice's nextAction asks a program to read: it fits the program's
148
+ * return even if every byte escapes to six JSON characters, with room for the
149
+ * page envelope, and never asks for more than the stash or bridge will serve.
150
+ */
151
+ function programPageBytes(results) {
152
+ const fits = Math.floor(((results.programChars ?? MAX_EXECUTE_RESULT_CHARS) - 1024) / 6);
153
+ return Math.max(4, Math.min(fits, results.cap, PROGRAM_RESULT_PAGE_BYTES));
154
+ }
152
155
  /** Opens a write's notice, ahead of any paging instruction. */
153
156
  const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see its result.";
154
157
  /** UTF-8 cannot represent lone UTF-16 surrogates without changing them. */
@@ -176,7 +179,7 @@ function isWellFormedText(text) {
176
179
  * reported the decoy near the top instead. A write never gets this: repeating
177
180
  * it is the one thing its notice forbids.
178
181
  */
179
- const READ_REDUCE_FIRST = "To find something specific, repeat this read inside execute_code with connecta.call and filter or search the result there, returning only what matters";
182
+ const readReduceFirst = (routes) => `To find something specific, repeat this read inside ${routes.program} with connecta.call and filter or search the result there, returning only what matters`;
180
183
  function pagingHint(results, totalBytes, preview) {
181
184
  if (results.pagingTool === "read_result") {
182
185
  const shown = preview.kind === "prefix"
@@ -188,13 +191,13 @@ function pagingHint(results, totalBytes, preview) {
188
191
  }
189
192
  if (!results.write) {
190
193
  if (results.binding.program)
191
- return "filter/map/slice data inside execute_code and return only what you need; page this completed return with connecta.result using nextAction.";
194
+ return `filter/map/slice data inside ${results.routes.program} and return only what you need; page this completed return with connecta.result using nextAction.`;
192
195
  const [shown, full] = preview.kind === "prefix"
193
196
  ? [`Bytes 0-${preview.bytes} of ${totalBytes} follow.`, "to read it in full, page the rest"]
194
197
  : preview.kind === "text-of-envelope"
195
198
  ? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
196
199
  : [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
197
- return `${shown} ${READ_REDUCE_FIRST}; ${full} with connecta.result using nextAction.`;
200
+ return `${shown} ${readReduceFirst(results.routes)}; ${full} with connecta.result using nextAction.`;
198
201
  }
199
202
  const body = preview.kind === "prefix"
200
203
  ? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with connecta.result using nextAction.`
@@ -229,7 +232,7 @@ const stashResult = function (bytes, results, preview, rawText) {
229
232
  try {
230
233
  // Base64 permits byte-range decoding, and splitting the envelope across
231
234
  // keys keeps a page's storage read proportional to the page instead of to
232
- // the whole result (issue #540). Chunk 0 carries the header; the connecta.result
235
+ // the whole result. Chunk 0 carries the header; the connecta.result
233
236
  // reader below maps a byte offset back to chunk index and base64 quad.
234
237
  const chunkBytes = resultChunkBytes(totalBytes);
235
238
  const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
@@ -261,7 +264,7 @@ const stashResult = function (bytes, results, preview, rawText) {
261
264
  ? `Paging is unavailable. ${WRITE_ALREADY_RAN}`
262
265
  : results.pagingTool
263
266
  ? "Paging is unavailable."
264
- : "Paging is unavailable. Use execute_code to reduce read-only results before returning them.",
267
+ : `Paging is unavailable. Use ${results.routes.program} to reduce read-only results before returning them.`,
265
268
  };
266
269
  }
267
270
  return {
@@ -280,7 +283,7 @@ const stashResult = function (bytes, results, preview, rawText) {
280
283
  // A byte may cost six JSON characters when escaped. Leave room for the
281
284
  // result id, page counters, and continuation metadata in the returned page.
282
285
  arguments: {
283
- code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${Math.floor((MAX_EXECUTE_RESULT_CHARS - 1024) / 6)} })`,
286
+ code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${programPageBytes(results)} })`,
284
287
  },
285
288
  },
286
289
  };
@@ -300,6 +303,8 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
300
303
  const bytes = enc.encode(text);
301
304
  const write = resolved.definition.classification !== "read";
302
305
  const notice = yield* upwardStep(stashResult(bytes, {
306
+ // Program-call pages name only execute_code, listed wherever programs run.
307
+ routes: META_ROUTES,
303
308
  binding: {
304
309
  identity: registry.resultIdentity(),
305
310
  baseUrl,
@@ -325,10 +330,10 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
325
330
  });
326
331
  });
327
332
  /** Program returns share the direct stash, with all host-admitted call dependencies. */
328
- export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite) {
329
- return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite));
333
+ export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
334
+ return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars));
330
335
  }
331
- const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite) {
336
+ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
332
337
  return authFlow(function* () {
333
338
  const serialized = serializeResultText(value);
334
339
  if (serialized.length <= maxChars)
@@ -339,6 +344,8 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
339
344
  const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
340
345
  const results = {
341
346
  secrets,
347
+ // A program return names only execute_code, which every surface running programs lists.
348
+ routes: META_ROUTES,
342
349
  binding: {
343
350
  identity: registry.resultIdentity(),
344
351
  baseUrl,
@@ -347,6 +354,7 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
347
354
  },
348
355
  write: completedWrite,
349
356
  cap,
357
+ ...(programChars !== undefined ? { programChars } : {}),
350
358
  set: authMethod((id, chunks, ttl) => registryProgram(registry, "stashResult", id, chunks, ttl)),
351
359
  warn: () => logFailure(logger, "result paging unavailable", failureRecord({})),
352
360
  };
@@ -453,16 +461,11 @@ function escapedHeadOf(bytes, cap) {
453
461
  * Bound a downstream MCP `content` array by `cap`.
454
462
  *
455
463
  * A lone text block is measured, stashed, previewed, and paged as its text:
456
- * its envelope adds nothing but a wrapper and a second layer of JSON escaping,
457
- * which made a JSON payload's preview unreadable and inflated every page.
464
+ * its envelope adds nothing but a wrapper and a second layer of JSON escaping.
458
465
  *
459
466
  * Anything else is measured as the serialized envelope — the same string that
460
467
  * gets stashed and paged, and the one that counts every block rather than only
461
- * the text ones. Both halves matter (issue #43). Measuring only text blocks
462
- * meant an oversized all-image result scored zero bytes and was returned inline
463
- * unbounded, with no `resultId` to page from; and measuring one string while
464
- * truncating another left `totalBytes` describing something the cap was never
465
- * compared against.
468
+ * the text ones. Measure the same representation that gets truncated and paged.
466
469
  *
467
470
  * Over the cap, what a client gets depends on whether a preview is usable.
468
471
  * Several text blocks preview as their text joined by newlines, while
@@ -543,12 +546,13 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
543
546
  // identity lets remote connectors reuse one downstream client inside that
544
547
  // request without leaking request-bound I/O into the next one.
545
548
  const requestScope = opts.requestScope ?? {};
549
+ const routes = opts.routes ?? META_ROUTES;
546
550
  if (opts.canManageAuth || opts.oauthConnectUrl || opts.credentialHandoffUrl || opts.oauthConnectUnavailable) {
547
551
  bindAuthorizationHandoffs(requestScope, authMethod((connector) => authCall(authorizationHandoff, [
548
552
  registry,
549
553
  baseUrl,
550
554
  requestScope,
551
- opts,
555
+ { ...opts, routes },
552
556
  { connector },
553
557
  ])));
554
558
  }
@@ -569,6 +573,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
569
573
  requestSignal: opts.requestSignal,
570
574
  // searchRoute keeps its top-level default. In-program callers use a
571
575
  // separate CatalogService configured for connecta.search.
576
+ routes,
572
577
  });
573
578
  const invocation = new InvocationService(registry, catalog, opts.activity);
574
579
  /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
@@ -586,6 +591,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
586
591
  activitySource: opts.activitySource,
587
592
  sentSecrets,
588
593
  readOnly: opts.readOnly,
594
+ routes,
589
595
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
590
596
  ...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
591
597
  unwrapResult: call.resultMode === "value",
@@ -614,6 +620,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
614
620
  const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
615
621
  const results = {
616
622
  secrets,
623
+ routes,
617
624
  binding: {
618
625
  identity: registry.resultIdentity(),
619
626
  baseUrl,
@@ -624,6 +631,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
624
631
  write: resolved.definition.classification !== "read",
625
632
  cap,
626
633
  ...(opts.pagingTool ? { pagingTool: opts.pagingTool } : {}),
634
+ ...(opts.maxProgramResultChars !== undefined ? { programChars: opts.maxProgramResultChars } : {}),
627
635
  set: authMethod((id, value, ttlSeconds) => registryProgram(registry, "stashResult", id, value, ttlSeconds)),
628
636
  // The catalog entry, never `toolName`: a record names a tool
629
637
  // only through the grammar check (src/operator-record.ts).
@@ -670,7 +678,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
670
678
  const structuredRecovery = outcome.error.nextAction !== undefined;
671
679
  const recoveryRequired = structuredRecovery ||
672
680
  // Sanitized `unavailable` diagnostics ride the structured shape too;
673
- // the plain-text path would drop them (#539).
681
+ // the plain-text path would drop them.
674
682
  outcome.error.details !== undefined ||
675
683
  // So does a conflict's `current`: where things stand is the retry.
676
684
  outcome.error.current !== undefined ||
@@ -733,7 +741,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
733
741
  content: [
734
742
  {
735
743
  type: "text",
736
- text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
744
+ text: "Available skills. Fetch one by exact name with this tool.\n\n" +
737
745
  listing.map((skill) => `- \`${skill.name}\` — ${skill.description}`).join("\n"),
738
746
  },
739
747
  ],
@@ -837,6 +845,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
837
845
  binding.identity.subject !== identity.subject ||
838
846
  binding.identity.principal !== identity.principal ||
839
847
  binding.identity.endpoint !== identity.endpoint ||
848
+ binding.identity.pool !== identity.pool ||
840
849
  binding.identity.origin !== identity.origin ||
841
850
  (binding.program === undefined
842
851
  ? typeof binding.connector !== "string" || typeof binding.tool !== "string"
@@ -949,7 +958,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
949
958
  // client that computes its own offsets can land inside a multi-byte
950
959
  // character, which would decode as U+FFFD; the offset actually served is
951
960
  // the boundary at or before it, and it is what the response reports back
952
- // as `offset` (issue #38).
961
+ // as `offset`.
953
962
  const offset = start + alignStartToCharBoundary(bytes, requestedOffset - start);
954
963
  // Both sides of `maxBytes` are validated by now — the argument above, the
955
964
  // cap at intake — so `offset + maxBytes` always reaches past `offset`.
@@ -991,11 +1000,15 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
991
1000
  })),
992
1001
  };
993
1002
  }
994
- const SEARCH_DESC = (readOnly) => `${readOnly ? "Use top-level search for catalog inspection; this read-only endpoint omits write tools." : "Use top-level search for catalog inspection or write work before call_destructive_tool."} Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" selects reads; "approvalRequired" selects the fail-closed write complement. The host owns confirmation; these filters grant no authority. Empty query browses. Returns { catalogErrors, tools, total, offset, limit, hasMore }; read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
995
- const CALL_DESC = (readOnly) => `Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. ${readOnly ? "This endpoint is read-only and refuses every write." : "Unannotated or write-capable tools fail closed to call_destructive_tool."} A truncated result carries a connecta.result action.`;
1003
+ // Each description states only its own tool's scope; the full routing rule
1004
+ // lives once in the server instructions. A client that drops instructions
1005
+ // still routes correctly from these scopes plus execute_code's terse lead.
1006
+ // Only the meta surface lists these tools, so they use its names.
1007
+ const SEARCH_DESC = (readOnly) => `${readOnly ? "Catalog inspection; this read-only endpoint omits write tools." : `Catalog inspection, or finding a write before ${META_ROUTES.write}.`} For read work, search inside one ${META_ROUTES.program} program that also calls and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" selects reads; "approvalRequired" selects the fail-closed write complement; neither grants authority. Empty query browses. Read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
1008
+ const CALL_DESC = (readOnly) => `Call one known-address read: a tool explicitly annotated readOnlyHint: true. Other read work: ${META_ROUTES.program}. ${readOnly ? "This endpoint is read-only and refuses every write." : `Unannotated or write-capable tools fail closed to ${META_ROUTES.write}.`} A truncated result carries a connecta.result action.`;
996
1009
  const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
997
1010
  /** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
998
- const CALL_DESTRUCTIVE_READ_ONLY_DESC = "This endpoint is read-only: search and describe omit write tools, and every write call here is refused. Reads still run; prefer call_tool or execute_code for them.";
1011
+ const CALL_DESTRUCTIVE_READ_ONLY_DESC = `This endpoint is read-only: search and describe omit write tools, and every write call here is refused. Reads still run; prefer ${META_ROUTES.read} or ${META_ROUTES.program} for them.`;
999
1012
  const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or required deployment configuration. A configured credential reports presence only: if a call just failed with an authentication or operator-configuration error, give the retained handoff to the operator; otherwise proceed with the call. force=true requests an OAuth restart when the verified user opens the /connect URL; this tool never accepts credentials.";
1000
1013
  const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.";
1001
1014
  /**
@@ -1084,6 +1097,8 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
1084
1097
  */
1085
1098
  export function registerMetaTools(server, registry, ctx) {
1086
1099
  const mt = createMetaTools(registry, ctx.baseUrl, {
1100
+ maxProgramResultChars: ctx.maxProgramResultChars,
1101
+ routes: ctx.routes,
1087
1102
  ...(ctx.native && !ctx.native.executeCode ? { pagingTool: "read_result" } : {}),
1088
1103
  ...(ctx.native ? { activitySource: "native" } : {}),
1089
1104
  downstreamInput: Boolean(ctx.authElicitation),
@@ -1,5 +1,5 @@
1
1
  /** Adapt only Connecta error envelopes; successful downstream payloads stay untouched. */
2
- export function nativeRecovery(result, executeCode, wireName) {
2
+ export function nativeRecovery(result, wireName) {
3
3
  const response = result;
4
4
  const body = response?.structuredContent;
5
5
  if (!response?.isError || !body || typeof body.error !== "object" || !body.error)
@@ -28,22 +28,10 @@ export function nativeRecovery(result, executeCode, wireName) {
28
28
  }
29
29
  : { host: "search", purpose: action.purpose };
30
30
  }
31
- // These are Connecta-authored recovery fields, never arbitrary downstream data.
32
- const rewrite = (text) => text.replace(/authorize_connector|search_tools|call_(?:destructive_)?tool|execute_code/g, (tool) => tool === "authorize_connector"
33
- ? "the returned authorization handoff"
34
- : tool.startsWith("call_")
35
- ? "the original native tool"
36
- : tool === "execute_code" && executeCode
37
- ? tool
38
- : "your host's tool search");
39
- for (const key of ["message", "instructions", "retry"])
40
- if (typeof error[key] === "string")
41
- error[key] = rewrite(error[key]);
31
+ // Recovery text already uses the native wording: native pools build it with
32
+ // native route names. Only the address in retry text becomes the wire name.
42
33
  if (wireName && typeof error.operation === "string" && typeof error.retry === "string")
43
- error.retry = error.retry.replaceAll(rewrite(error.operation), wireName);
44
- if (!executeCode && typeof error.instructions === "string") {
45
- error.instructions = error.instructions.replace(/; connecta\.search[\s\S]*$/, ". Retry the original native call after recovery.");
46
- }
34
+ error.retry = error.retry.replaceAll(error.operation, wireName);
47
35
  return {
48
36
  ...response,
49
37
  structuredContent: { ...body, error },