@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
@@ -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,13 +10,13 @@ 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";
16
17
  import { chargeCallBudget } from "./runtime/call-admission.js";
17
18
  import { callEntry } from "./runtime/call-entry.js";
18
- import { forgetProviderFailure, isKnownProviderFailure, isSettledWrite, keyProtectsCall, knownCallFailure, onlyAuxiliaryRequests, knownProviderFailure, openLogicalCall, } from "./provider-call.js";
19
+ import { forgetProviderFailure, isKnownProviderFailure, isSettledWrite, keyProtectsCall, knownCallFailure, refusalRequest, onlyAuxiliaryRequests, knownProviderFailure, openLogicalCall, } from "./provider-call.js";
19
20
  import { classifyWriteOutcome, settleWrite, settleWriteResult } from "./write-outcome.js";
20
21
  import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuthEffect, } from "./invocation-auth.js";
21
22
  import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.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
@@ -286,10 +288,13 @@ export class InvocationService {
286
288
  }
287
289
  if (mayHaveWritten &&
288
290
  (error.code === "auth_required" || error.code === "downstream_oauth_required") &&
289
- !logicalCall?.sent.some((request) => request.dispatched)) {
291
+ !logicalCall?.sent.some((request) => request.dispatched) &&
292
+ !(answered && refusalRequest(error)?.dispatched === false)) {
290
293
  // An empty resource ledger cannot settle this auth failure. Keep
291
294
  // actual resource-refusal provenance, but withdraw an empty ledger's
292
295
  // known mark before composing advice and counting program writes.
296
+ // A boundary-owned physical pre-dispatch refusal is positive proof;
297
+ // its private provenance cannot be supplied by error fields.
293
298
  answered = false;
294
299
  forgetProviderFailure(error);
295
300
  }
@@ -406,6 +411,14 @@ export class InvocationService {
406
411
  return error;
407
412
  return {
408
413
  ...error,
414
+ // An example comes only from the published schema, never a connector's copy of the
415
+ // call, unless the connector states why there is none.
416
+ validation: {
417
+ ...withoutExample(error.validation),
418
+ ...(error.validation.exampleUnavailable === undefined && resolved?.definition.inputSchema
419
+ ? schemaExample(resolved.definition.inputSchema)
420
+ : {}),
421
+ },
409
422
  connector: target.connector.id,
410
423
  operation: `${target.connector.id}.${target.toolName}`,
411
424
  nextAction: this.catalog.searchRecovery({
@@ -552,7 +565,7 @@ export class InvocationService {
552
565
  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
566
  }
554
567
  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.`);
568
+ return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. ${routePhrases(context).writeNeedsApproval}`);
556
569
  }
557
570
  // Remote MCP tools advertise their input schema in the catalog. Validate
558
571
  // against that same request-local definition before admission or provider
@@ -580,7 +593,12 @@ export class InvocationService {
580
593
  return yield* Effect.fail(callSignal.reason);
581
594
  attempts = 1;
582
595
  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 }));
596
+ const operationContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({
597
+ signal: callSignal,
598
+ timeoutMs: context.timeoutMs,
599
+ defer: this.catalog.defer,
600
+ routes: context.routes,
601
+ }));
584
602
  const lease = callFromContext(operationContext, write ? "write" : "read", {
585
603
  admitted: true,
586
604
  // An invocation deadline aborts when its call finishes. A
@@ -604,11 +622,11 @@ export class InvocationService {
604
622
  yield* Effect.gen(function* () {
605
623
  if (target.connector.credential) {
606
624
  if (!connectorContext.credential) {
607
- throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. Call " +
608
- `authorize_connector({ connector: "${target.connector.id}" }).`);
625
+ throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. " +
626
+ routePhrases(connectorContext).authorizeCall(target.connector.id));
609
627
  }
610
628
  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.`);
629
+ throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. ${routePhrases(connectorContext).authorizeCall()}`);
612
630
  }
613
631
  }
614
632
  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,4 @@
1
+ import { META_ROUTES } from "./agent-routes.js";
1
2
  import { toolError } from "./tool-error.js";
2
3
  import { upwardStep } from "./upward.js";
3
4
  import { authCall, authInvoke, authFlow, authMethod, authStorage, registerAuth } from "./auth/native.js";
@@ -178,7 +179,7 @@ function isWellFormedText(text) {
178
179
  * reported the decoy near the top instead. A write never gets this: repeating
179
180
  * it is the one thing its notice forbids.
180
181
  */
181
- 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`;
182
183
  function pagingHint(results, totalBytes, preview) {
183
184
  if (results.pagingTool === "read_result") {
184
185
  const shown = preview.kind === "prefix"
@@ -190,13 +191,13 @@ function pagingHint(results, totalBytes, preview) {
190
191
  }
191
192
  if (!results.write) {
192
193
  if (results.binding.program)
193
- 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.`;
194
195
  const [shown, full] = preview.kind === "prefix"
195
196
  ? [`Bytes 0-${preview.bytes} of ${totalBytes} follow.`, "to read it in full, page the rest"]
196
197
  : preview.kind === "text-of-envelope"
197
198
  ? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
198
199
  : [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
199
- return `${shown} ${READ_REDUCE_FIRST}; ${full} with connecta.result using nextAction.`;
200
+ return `${shown} ${readReduceFirst(results.routes)}; ${full} with connecta.result using nextAction.`;
200
201
  }
201
202
  const body = preview.kind === "prefix"
202
203
  ? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with connecta.result using nextAction.`
@@ -263,7 +264,7 @@ const stashResult = function (bytes, results, preview, rawText) {
263
264
  ? `Paging is unavailable. ${WRITE_ALREADY_RAN}`
264
265
  : results.pagingTool
265
266
  ? "Paging is unavailable."
266
- : "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.`,
267
268
  };
268
269
  }
269
270
  return {
@@ -302,6 +303,8 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
302
303
  const bytes = enc.encode(text);
303
304
  const write = resolved.definition.classification !== "read";
304
305
  const notice = yield* upwardStep(stashResult(bytes, {
306
+ // Program-call pages name only execute_code, listed wherever programs run.
307
+ routes: META_ROUTES,
305
308
  binding: {
306
309
  identity: registry.resultIdentity(),
307
310
  baseUrl,
@@ -341,6 +344,8 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
341
344
  const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
342
345
  const results = {
343
346
  secrets,
347
+ // A program return names only execute_code, which every surface running programs lists.
348
+ routes: META_ROUTES,
344
349
  binding: {
345
350
  identity: registry.resultIdentity(),
346
351
  baseUrl,
@@ -541,12 +546,13 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
541
546
  // identity lets remote connectors reuse one downstream client inside that
542
547
  // request without leaking request-bound I/O into the next one.
543
548
  const requestScope = opts.requestScope ?? {};
549
+ const routes = opts.routes ?? META_ROUTES;
544
550
  if (opts.canManageAuth || opts.oauthConnectUrl || opts.credentialHandoffUrl || opts.oauthConnectUnavailable) {
545
551
  bindAuthorizationHandoffs(requestScope, authMethod((connector) => authCall(authorizationHandoff, [
546
552
  registry,
547
553
  baseUrl,
548
554
  requestScope,
549
- opts,
555
+ { ...opts, routes },
550
556
  { connector },
551
557
  ])));
552
558
  }
@@ -567,6 +573,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
567
573
  requestSignal: opts.requestSignal,
568
574
  // searchRoute keeps its top-level default. In-program callers use a
569
575
  // separate CatalogService configured for connecta.search.
576
+ routes,
570
577
  });
571
578
  const invocation = new InvocationService(registry, catalog, opts.activity);
572
579
  /** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
@@ -584,6 +591,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
584
591
  activitySource: opts.activitySource,
585
592
  sentSecrets,
586
593
  readOnly: opts.readOnly,
594
+ routes,
587
595
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
588
596
  ...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
589
597
  unwrapResult: call.resultMode === "value",
@@ -612,6 +620,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
612
620
  const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
613
621
  const results = {
614
622
  secrets,
623
+ routes,
615
624
  binding: {
616
625
  identity: registry.resultIdentity(),
617
626
  baseUrl,
@@ -836,6 +845,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
836
845
  binding.identity.subject !== identity.subject ||
837
846
  binding.identity.principal !== identity.principal ||
838
847
  binding.identity.endpoint !== identity.endpoint ||
848
+ binding.identity.pool !== identity.pool ||
839
849
  binding.identity.origin !== identity.origin ||
840
850
  (binding.program === undefined
841
851
  ? typeof binding.connector !== "string" || typeof binding.tool !== "string"
@@ -990,11 +1000,15 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
990
1000
  })),
991
1001
  };
992
1002
  }
993
- 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".`;
994
- 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.`;
995
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.";
996
1010
  /** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
997
- 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.`;
998
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.";
999
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.";
1000
1014
  /**
@@ -1084,6 +1098,7 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
1084
1098
  export function registerMetaTools(server, registry, ctx) {
1085
1099
  const mt = createMetaTools(registry, ctx.baseUrl, {
1086
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),
@@ -1115,6 +1130,37 @@ export function registerMetaTools(server, registry, ctx) {
1115
1130
  bindRequest(request);
1116
1131
  return yield* upwardStep(authInvoke(mt, "skills", [args]));
1117
1132
  })));
1133
+ server.registerTool("authorize_connector", {
1134
+ description: ctx.native
1135
+ ? AUTHORIZE_DESC +
1136
+ " Use when a connector’s tools are missing. Granted: " +
1137
+ registry
1138
+ .listConnectors()
1139
+ .map((connector) => connector.id)
1140
+ .join(", ") +
1141
+ "."
1142
+ : AUTHORIZE_DESC,
1143
+ inputSchema: AUTHORIZE_INPUT,
1144
+ outputSchema: AUTHORIZE_OUTPUT,
1145
+ // Starts (or with force, resets) a downstream OAuth flow — it changes
1146
+ // stored connector auth state, so it is deliberately not read-only.
1147
+ annotations: {
1148
+ readOnlyHint: false,
1149
+ destructiveHint: false,
1150
+ openWorldHint: true,
1151
+ },
1152
+ }, authMethod((args, request) => authFlow(function* () {
1153
+ bindRequest(request);
1154
+ const run = authMethod(() => authInvoke(mt, "authorizeConnector", [args]));
1155
+ return yield* upwardStep(ctx.authElicitation
1156
+ ? authInvoke(ctx.authElicitation, "run", [
1157
+ "authorize_connector",
1158
+ args,
1159
+ request,
1160
+ run,
1161
+ ])
1162
+ : authCall(run, []));
1163
+ })));
1118
1164
  if (ctx.native) {
1119
1165
  if (!ctx.native.executeCode) {
1120
1166
  server.registerTool("read_result", { description: READ_RESULT_DESC, inputSchema: READ_RESULT_INPUT, annotations: READ_ONLY_LOCAL }, authMethod((args, request) => authFlow(function* () {
@@ -1186,28 +1232,5 @@ export function registerMetaTools(server, registry, ctx) {
1186
1232
  ])
1187
1233
  : authInvoke(mt, "callDestructiveTool", [call]));
1188
1234
  })));
1189
- server.registerTool("authorize_connector", {
1190
- description: AUTHORIZE_DESC,
1191
- inputSchema: AUTHORIZE_INPUT,
1192
- outputSchema: AUTHORIZE_OUTPUT,
1193
- // Starts (or with force, resets) a downstream OAuth flow — it changes
1194
- // stored connector auth state, so it is deliberately not read-only.
1195
- annotations: {
1196
- readOnlyHint: false,
1197
- destructiveHint: false,
1198
- openWorldHint: true,
1199
- },
1200
- }, authMethod((args, request) => authFlow(function* () {
1201
- bindRequest(request);
1202
- const run = authMethod(() => authInvoke(mt, "authorizeConnector", [args]));
1203
- return yield* upwardStep(ctx.authElicitation
1204
- ? authInvoke(ctx.authElicitation, "run", [
1205
- "authorize_connector",
1206
- args,
1207
- request,
1208
- run,
1209
- ])
1210
- : authCall(run, []));
1211
- })));
1212
1235
  return undefined;
1213
1236
  }
@@ -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 },
@@ -1,4 +1,5 @@
1
1
  import { bytesToHex } from "./bytes.js";
2
+ import { routeNames } from "./agent-routes.js";
2
3
  import { duringViewEffect } from "./runtime/call.js";
3
4
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
4
5
  import { Effect } from "effect";
@@ -28,7 +29,7 @@ const SEPARATOR = "__";
28
29
  const PLAIN_TOOL = /^[A-Za-z0-9-](?:[A-Za-z0-9-]|_(?!_))*$/;
29
30
  const NATIVE_NAME = new RegExp(`^[A-Za-z][A-Za-z0-9_-]{0,${NATIVE_NAME_MAX - 1}}$`);
30
31
  /** Connecta's own tools on a native endpoint. Their names never contain `__`. */
31
- const NATIVE_OWN_TOOLS = ["execute_code", "skills", "read_result"];
32
+ const NATIVE_OWN_TOOLS = ["execute_code", "skills", "read_result", "authorize_connector"];
32
33
  const enc = new TextEncoder();
33
34
  function shortHash(text) {
34
35
  return runEdge(shortHashNative(text));
@@ -149,6 +150,8 @@ export class NativeSurface {
149
150
  // Listing omits a read-only endpoint's writes, as search does.
150
151
  readOnly: options.readOnly,
151
152
  defer: options.defer,
153
+ // Declared schema descriptions read as this pool serves them.
154
+ routes: routeNames(options.executeCode ? "native-code" : "native"),
152
155
  });
153
156
  }
154
157
  /**
@@ -183,7 +186,7 @@ export class NativeSurface {
183
186
  ...(entry ? { _meta: { "dev.connecta/address": `${entry.connector.id}.${entry.tool.name}` } } : {}),
184
187
  }, authMethod((args, request) => Effect.gen({ self: this }, function* () {
185
188
  const result = yield* authInvoke(this, "call", [name, args ?? {}, request]);
186
- return nativeRecovery(result, this.options.executeCode, name);
189
+ return nativeRecovery(result, name);
187
190
  })));
188
191
  }
189
192
  /**
@@ -46,3 +46,13 @@ export async function deriveRequestStateKey(raw) {
46
46
  info: new TextEncoder().encode("auth-elicitation"),
47
47
  }, material, 256));
48
48
  }
49
+ /** A non-extractable HMAC key for secret-bearing stored cache keys; nothing is stored. */
50
+ export async function deriveStorageDigestKey(raw) {
51
+ const material = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveKey"]);
52
+ return crypto.subtle.deriveKey({
53
+ name: "HKDF",
54
+ hash: "SHA-256",
55
+ salt: new TextEncoder().encode("connecta:storage-digest:v1"),
56
+ info: new TextEncoder().encode("cache-keys"),
57
+ }, material, { name: "HMAC", hash: "SHA-256", length: 256 }, false, ["sign"]);
58
+ }