@zackbart/connecta 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +3 -2
  3. package/bin/connecta.mjs +4 -8
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +6 -4
  6. package/dist/agent-routes.js +231 -0
  7. package/dist/auth/downstream-oauth/consent.js +519 -0
  8. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  9. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  10. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  11. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  12. package/dist/auth/downstream-oauth.js +66 -2382
  13. package/dist/auth/native.js +37 -0
  14. package/dist/auth/oauth-grant.js +13 -0
  15. package/dist/auth/static-oauth.js +153 -131
  16. package/dist/auth-elicitation.js +7 -1
  17. package/dist/authorization-handoff.js +11 -5
  18. package/dist/catalog-cache.js +219 -59
  19. package/dist/catalog-policy.js +18 -0
  20. package/dist/catalog-service.js +284 -47
  21. package/dist/catalog.js +18 -2
  22. package/dist/config.d.ts +16 -11
  23. package/dist/config.js +9 -6
  24. package/dist/connector-caller.d.ts +1 -1
  25. package/dist/connector-health.js +59 -0
  26. package/dist/connector-status.js +117 -0
  27. package/dist/connectors/api-connector.d.ts +7 -2
  28. package/dist/connectors/api-connector.js +7 -4
  29. package/dist/connectors/negotiation-cache.js +22 -4
  30. package/dist/connectors/remote-mcp/call.js +164 -0
  31. package/dist/connectors/remote-mcp/catalog.js +278 -0
  32. package/dist/connectors/remote-mcp/config.js +263 -0
  33. package/dist/connectors/remote-mcp/connection.js +682 -0
  34. package/dist/connectors/remote-mcp/errors.js +488 -0
  35. package/dist/connectors/remote-mcp/oauth.js +473 -0
  36. package/dist/connectors/remote-mcp/resources.js +473 -0
  37. package/dist/connectors/remote-mcp/runtime.js +1 -0
  38. package/dist/connectors/remote-mcp/transport.js +555 -0
  39. package/dist/connectors/remote-mcp.js +62 -2863
  40. package/dist/credential-contract.d.ts +23 -0
  41. package/dist/credential-reads.js +17 -0
  42. package/dist/credential-verification.js +53 -0
  43. package/dist/credentials.d.ts +7 -0
  44. package/dist/credentials.js +60 -25
  45. package/dist/describe-config.d.ts +12 -2
  46. package/dist/describe-config.js +2 -0
  47. package/dist/discovery-health.js +199 -0
  48. package/dist/downstream-input.js +2 -0
  49. package/dist/errors.d.ts +55 -17
  50. package/dist/errors.js +95 -41
  51. package/dist/execute.js +152 -114
  52. package/dist/executor-result.js +3 -1
  53. package/dist/guest-functions.js +83 -0
  54. package/dist/guest-runtime.js +60 -19
  55. package/dist/guest-types.d.ts +19 -5
  56. package/dist/http/argument-problems.js +35 -0
  57. package/dist/http/errors.js +5 -4
  58. package/dist/http/fields.js +192 -13
  59. package/dist/http/hatch-response.js +73 -0
  60. package/dist/http/hatch.js +88 -23
  61. package/dist/http/oauth.js +3 -2
  62. package/dist/http/provider.js +32 -4
  63. package/dist/http/safety.js +8 -0
  64. package/dist/http/tools.js +44 -23
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +9 -2
  67. package/dist/invocation.js +26 -8
  68. package/dist/membership.d.ts +13 -5
  69. package/dist/membership.js +34 -2
  70. package/dist/meta-tools.js +54 -31
  71. package/dist/native-recovery.js +4 -16
  72. package/dist/native-tools.js +5 -2
  73. package/dist/oauth-sealing.js +10 -0
  74. package/dist/operation-vocabulary.js +135 -13
  75. package/dist/operator-record.d.ts +2 -2
  76. package/dist/operator-record.js +1 -0
  77. package/dist/operator-ui/contract.d.ts +10 -0
  78. package/dist/operator-ui/generated.js +2 -2
  79. package/dist/operator-ui/model.d.ts +22 -1
  80. package/dist/provider-call-facts.js +6 -2
  81. package/dist/providers/_shared/rest/operation-index.js +48 -50
  82. package/dist/providers/_shared/rest/value-safety.js +80 -0
  83. package/dist/providers/breeze/api.js +7 -1
  84. package/dist/providers/breeze/skill.generated.js +1 -1
  85. package/dist/providers/breeze/tools.js +4 -3
  86. package/dist/providers/ccb/api.js +2 -1
  87. package/dist/providers/ccb/index.js +5 -0
  88. package/dist/providers/ccb/skill.generated.js +1 -1
  89. package/dist/providers/cloudflare/api.js +8 -1
  90. package/dist/providers/cloudflare/skill.generated.js +1 -1
  91. package/dist/providers/docs/lib/schemas.js +24 -22
  92. package/dist/providers/docs/safety.js +2 -0
  93. package/dist/providers/docs/skill.generated.js +1 -1
  94. package/dist/providers/docs/tools.js +2 -4
  95. package/dist/providers/drive/api.js +1 -0
  96. package/dist/providers/drive/lib/contracts.js +8 -5
  97. package/dist/providers/drive/skill.generated.js +1 -1
  98. package/dist/providers/drive/tools.js +11 -16
  99. package/dist/providers/forms/api.js +1 -0
  100. package/dist/providers/forms/skill.generated.js +1 -1
  101. package/dist/providers/forms/tools.js +10 -7
  102. package/dist/providers/github/api.js +1 -0
  103. package/dist/providers/github/lib/discovery.js +1 -1
  104. package/dist/providers/github/skill.generated.js +1 -1
  105. package/dist/providers/github/tools.js +61 -17
  106. package/dist/providers/gmail/lib/schemas.js +60 -50
  107. package/dist/providers/gmail/skill.generated.js +1 -1
  108. package/dist/providers/gmail/tools.js +7 -5
  109. package/dist/providers/granola/index.js +2 -0
  110. package/dist/providers/granola/skill.generated.js +1 -1
  111. package/dist/providers/overflow/api.js +7 -1
  112. package/dist/providers/overflow/skill.generated.js +1 -1
  113. package/dist/providers/planning-center/api.js +3 -1
  114. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  115. package/dist/providers/planning-center/skill.generated.js +1 -1
  116. package/dist/providers/quickbooks/safety.js +80 -39
  117. package/dist/providers/quickbooks/skill.generated.js +1 -1
  118. package/dist/providers/sheets/api.js +1 -0
  119. package/dist/providers/sheets/skill.generated.js +1 -1
  120. package/dist/providers/sheets/tools.js +13 -14
  121. package/dist/providers/slides/skill.generated.js +1 -1
  122. package/dist/providers/slides/tools.js +15 -13
  123. package/dist/providers/stripe/skill.generated.js +1 -1
  124. package/dist/providers/tithely/api.js +3 -1
  125. package/dist/providers/tithely/skill.generated.js +1 -1
  126. package/dist/providers/vercel/api.js +1 -1
  127. package/dist/providers/vercel/skill.generated.js +1 -1
  128. package/dist/providers/vercel/tools.js +4 -3
  129. package/dist/registry.d.ts +10 -4
  130. package/dist/registry.js +41 -13
  131. package/dist/routes/credentials.js +129 -60
  132. package/dist/routes/dispatch.js +5 -1
  133. package/dist/routes/mcp.js +30 -14
  134. package/dist/routes/operator.js +2 -2
  135. package/dist/routes/shared.d.ts +6 -1
  136. package/dist/routes/shared.js +7 -1
  137. package/dist/routes/ui-config.js +2 -2
  138. package/dist/routes/ui-data.js +126 -20
  139. package/dist/routes/ui.js +16 -0
  140. package/dist/runtime/call.js +7 -2
  141. package/dist/runtime/native-storage.js +12 -0
  142. package/dist/runtime/request-reads.js +207 -0
  143. package/dist/runtime/request-scope.js +16 -0
  144. package/dist/runtime/storage.js +12 -0
  145. package/dist/skills.js +37 -20
  146. package/dist/storage/keys.js +53 -3
  147. package/dist/storage/memory.js +8 -2
  148. package/dist/storage/sql-native.js +53 -1
  149. package/dist/storage/sql.js +12 -2
  150. package/dist/storage-digest.js +32 -0
  151. package/dist/timeout.js +8 -0
  152. package/dist/types.d.ts +33 -0
  153. package/dist/usage-guide.js +16 -35
  154. package/dist/validate.d.ts +3 -0
  155. package/dist/validate.js +220 -158
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/dist/write-outcome.js +0 -3
  159. package/documentation/README.md +13 -13
  160. package/documentation/architecture.md +385 -20
  161. package/documentation/auth.md +233 -64
  162. package/documentation/code-mode.md +122 -39
  163. package/documentation/deploying.md +6 -3
  164. package/documentation/integrating.md +16 -2
  165. package/documentation/meta-tools.md +240 -49
  166. package/documentation/migration-0.33.md +5 -1
  167. package/documentation/migration-0.34.md +3 -1
  168. package/documentation/operator-tests.md +1 -1
  169. package/documentation/operator-ui.md +105 -12
  170. package/documentation/pool-membership-upgrade.md +44 -0
  171. package/documentation/providers.md +105 -0
  172. package/examples/worker/AGENTS.md +10 -10
  173. package/examples/worker/README.md +4 -3
  174. package/examples/worker/src/connecta.config.ts +1 -0
  175. package/guides/connecting-agents.md +2 -1
  176. package/guides/how-it-works.md +4 -2
  177. package/package.json +1 -1
  178. package/templates/node/AGENTS.md +13 -12
  179. package/templates/node/package.json +1 -1
  180. package/templates/node/src/connecta.config.ts +1 -0
package/dist/skills.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { META_ROUTES, routeNames } from "./agent-routes.js";
1
2
  import { bytesToHex, utf8Length } from "./bytes.js";
2
3
  import { duringViewEffect } from "./runtime/call.js";
3
4
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
@@ -9,15 +10,17 @@ import { Effect } from "effect";
9
10
  import { runEdge, withDeadlineEffect } from "./runtime/run.js";
10
11
  import { closeScopeOnExit } from "./runtime/connector-scope.js";
11
12
  import { sentSecretsForRequest } from "./sent-secrets.js";
12
- const ROUTE = "Choose a route before discovery. One known-address read uses call_tool; one known-address write uses call_destructive_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.";
13
- const RECOVERY = 'When an auth error includes a handoff, give it to the user or operator. Use authorize_connector for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Missing guidance/repair only: skills tool "usage" or connecta.skill("usage").';
13
+ // The meta instructions and built-in guides use the meta names: built-in guides
14
+ // are byte-identical across deployments, and the eval code arm replaces usage.
15
+ const ROUTE = `Choose a route before discovery. One known-address read uses ${META_ROUTES.read}; one known-address write uses ${META_ROUTES.write}. Unknown-address read-only work starts with ${META_ROUTES.program} to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.`;
16
+ const RECOVERY = `When an auth error includes a handoff, give it to the user or operator. Use ${META_ROUTES.authorize} for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Missing guidance/repair only: skills tool "usage" or connecta.skill("usage").`;
14
17
  /**
15
18
  * The always-loaded MCP `instructions` string. An endpoint is writable or
16
19
  * read-only: a writable one lets programs and call_destructive_tool write,
17
20
  * and the client owns approval; a read-only one omits write tools from search
18
21
  * and describe and refuses every write call. Guides may still name them.
19
22
  */
20
- export const CONNECTA_INSTRUCTIONS = `${ROUTE} On a writable endpoint, programs and call_destructive_tool may write. On a read-only endpoint, search omits write tools and every write call is refused; guides may still name them. The execute_code description says which this endpoint is. Never repeat a write to recover its output. ${RECOVERY}`;
23
+ export const CONNECTA_INSTRUCTIONS = `${ROUTE} On a writable endpoint, programs and ${META_ROUTES.write} may write. On a read-only endpoint, search omits write tools and every write call is refused; guides may still name them. The ${META_ROUTES.program} description says which this endpoint is. Never repeat a write to recover its output. ${RECOVERY}`;
21
24
  /**
22
25
  * The instructions one endpoint serves: {@link CONNECTA_INSTRUCTIONS}, then
23
26
  * the admitted pool's `description` after a fixed lead-in. Plain `/mcp` and a
@@ -31,7 +34,8 @@ export function endpointInstructions(description, native) {
31
34
  }
32
35
  /** Instructions for pools whose host searches the listed native tools. */
33
36
  export function nativeInstructions(executeCode) {
34
- return `Each tool here is one operation; annotations classify reads/writes. Call known tools directly; ${executeCode ? "combine work in execute_code using the program addresses in tool descriptions." : "find tools with your host's search."} Read-only endpoints omit and refuse writes. Never repeat a write for output. Give auth handoffs to the user/operator. Follow nextAction to page with ${executeCode ? "connecta.result in execute_code" : "read_result"}. skills lists guidance.`;
37
+ const routes = routeNames(executeCode ? "native-code" : "native");
38
+ return `Each tool here is one operation; annotations classify reads/writes. Call known tools directly; ${executeCode ? `combine work in ${routes.program} using the program addresses in tool descriptions.` : "find tools with your host's search."} Read-only endpoints omit and refuse writes. Never repeat a write for output. Give auth handoffs to the user/operator. Tools missing? Call authorize_connector; after repair, reconnect to refresh your list. Follow nextAction to page with ${executeCode ? `connecta.result in ${routes.program}` : "read_result"}. ${routes.skills} lists guidance.`;
35
39
  }
36
40
  /** True when at least one of `connectors` carries a usage guide. */
37
41
  export function hasConnectorGuides(connectors) {
@@ -46,36 +50,37 @@ export function hasConnectorGuides(connectors) {
46
50
  function usageSkill() {
47
51
  return USAGE_SKILL;
48
52
  }
53
+ const INVESTIGATE_DESCRIPTION = "Plan an investigation that spans several services; resolve scope, identities, and capability limits before querying, then return the answer with its evidence.";
49
54
  const INVESTIGATE_SKILL = `---
50
55
  name: investigate
51
- description: Plan purchase verification, experiment checks, and customer or deployment investigations across services; resolve scope and capability limits before querying.
56
+ description: ${INVESTIGATE_DESCRIPTION}
52
57
  ---
53
58
 
54
59
  # Investigate across services
55
60
 
56
61
  ## Plan the investigation
57
62
 
58
- Start from the user's question and the evidence that would answer it. Select the app, account, and environment from connector titles, purposes, and relevant guides before looking up records. Reuse verified ids within the task; never carry an id across connectors just because its name matches.
63
+ Start from the user's question and the evidence that would answer it. Select the account, environment, and time window from connector titles, purposes, and relevant guides before looking up records. Reuse verified ids within the task; never carry an id across connectors just because its name matches.
59
64
 
60
- - Purchase verification: resolve the same customer and environment across payment, subscription access, and analytics. Check each separately; a recorded payment does not prove access, and a missing analytics event does not prove payment failure.
61
- - Experiment checks: confirm the project, experiment, time window, and exposure population before comparing outcomes. Return the requested comparison and any missing evidence; do not expand into an unrelated analytics audit.
62
- - Customer or deployment investigations: locate the exact customer or deployment first, then follow only the records needed to explain the reported symptom. Use provider links or ids so the answer can be checked.
65
+ - Resolve the same subject in each service separately. A record in one service does not prove the matching state in another, and a missing record in one does not prove failure elsewhere.
66
+ - Confirm the scope a comparison needs (which records, which window, which population) before comparing. Return the requested comparison and any missing evidence; do not expand into an unrelated audit.
67
+ - Locate the exact record first, then follow only the records needed to explain the reported symptom. Use provider links or ids so the answer can be checked.
63
68
 
64
69
  Establish capability limits early. A partial search is not proof that a tool is absent, and an unavailable catalog is not an empty dataset. Try a scoped search for the missing operation, inspect its guide when relevant, and distinguish unsupported work from missing data or authorization. If the required evidence is unavailable, return what was verified and the specific gap instead of approximating a different question through repeated calls.
65
70
 
66
- A host transport error that requires reconnecting this MCP server cannot be repaired by a downstream tool. Reconnect in the host; do not repeatedly call \`authorize_connector\` through the failed connection.
71
+ A host transport error that requires reconnecting this MCP server cannot be repaired by a downstream tool. Reconnect in the host; do not repeatedly call \`${META_ROUTES.authorize}\` through the failed connection.
67
72
 
68
- Return the answer first, then the evidence and any unresolved gap. Include the app/environment, time window, and source ids or links needed to check it. Separate observed facts from inferences. Stop when the requested evidence is sufficient.
73
+ Return the answer first, then the evidence and any unresolved gap. Include the account or environment, time window, and source ids or links needed to check it. Separate observed facts from inferences. Stop when the requested evidence is sufficient.
69
74
  `;
70
75
  const AVAILABLE_SKILLS = [
71
76
  {
72
77
  name: "usage",
73
- description: "How to route work between one execute_code program and Connecta's explicit call, authorization, and result tools.",
78
+ description: `How to route work between one ${META_ROUTES.program} program and Connecta's explicit call, authorization, and result tools.`,
74
79
  content: usageSkill,
75
80
  },
76
81
  {
77
82
  name: "investigate",
78
- description: "Plan purchase verification, experiment checks, and customer or deployment investigations across services; resolve scope and capability limits before querying.",
83
+ description: INVESTIGATE_DESCRIPTION,
79
84
  content: () => INVESTIGATE_SKILL,
80
85
  },
81
86
  ];
@@ -84,7 +89,8 @@ const AVAILABLE_SKILLS = [
84
89
  * are bare identifiers and never contain ":", so `connector:<id>` cannot
85
90
  * collide with one — not even when a connector's id is literally "usage".
86
91
  * Canonical skill URIs and the prefixed compatibility alias reach connector
87
- * guides. A bare connector id never resolves, so nothing shadows silently.
92
+ * guides. A bare connector id reaches its guide too, unless it is a built-in
93
+ * skill name: built-ins always win, so nothing shadows them.
88
94
  */
89
95
  const CONNECTOR_SKILL_PREFIX = "connector:";
90
96
  /** The skill name that fetches `connector`'s guide. */
@@ -369,6 +375,13 @@ const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (
369
375
  return [builtIns[0], ...guides, ...builtIns.slice(1)];
370
376
  });
371
377
  });
378
+ /** `granola` names connector granola's guide; built-in names and URIs never do. */
379
+ function bareConnectorGuide(records, name) {
380
+ if (name.includes(":") || AVAILABLE_SKILLS.some((skill) => skill.name === name))
381
+ return undefined;
382
+ const alias = connectorSkillName(name);
383
+ return records.find((record) => record.connector !== undefined && record.aliases.includes(alias));
384
+ }
372
385
  /** Keep the downstream path tree, so relative supporting-file references still resolve. */
373
386
  export function downstreamSkillUri(connectorId, uri) {
374
387
  if (uri.length > 32_768)
@@ -525,12 +538,14 @@ export class SkillsRegistry {
525
538
  const connector = this.registry.getConnector(id);
526
539
  if (connector)
527
540
  this.options.onRead?.(connector, "local");
528
- const message = name.startsWith(CONNECTOR_SKILL_PREFIX)
529
- ? connector
541
+ // Skill names resolve a bare id with a guide before reaching here; a
542
+ // resource URI read does not.
543
+ const message = connector
544
+ ? connectorGuide(connector) === undefined
530
545
  ? `Connector "${boundedEchoText(id)}" has no usage guide.`
531
- : `Unknown connector "${boundedEchoText(id)}".`
532
- : connector
533
- ? `Unknown skill "${boundedEchoText(name)}". Connector guides are fetched as "${boundedEchoText(connectorSkillName(name))}".`
546
+ : `Connector "${boundedEchoText(id)}" has its guide at "${boundedEchoText(connectorSkillName(id))}".`
547
+ : name.startsWith(CONNECTOR_SKILL_PREFIX)
548
+ ? `Unknown connector "${boundedEchoText(id)}".`
534
549
  : `Unknown skill "${boundedEchoText(name)}".`;
535
550
  return new ConnectorCallError("not_found", `${message} Available skills: ${available}.`);
536
551
  }
@@ -675,7 +690,9 @@ export class SkillsRegistry {
675
690
  registerAuthMethod(SkillsRegistry.prototype, "lookup", function (uri) {
676
691
  const self = this;
677
692
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
678
- const local = (yield* upwardStep(self.local)).find((record) => record.entry.uri === uri || record.aliases.includes(uri));
693
+ const records = yield* upwardStep(self.local);
694
+ const local = records.find((record) => record.entry.uri === uri || record.aliases.includes(uri)) ??
695
+ bareConnectorGuide(records, uri);
679
696
  if (local) {
680
697
  const record = yield* upwardStep(authCall(withManifest, [local]));
681
698
  self.assertSafe([record]);
@@ -153,7 +153,33 @@ export const catalogKeys = {
153
153
  manifest: (connectorId) => validateStorageKey(`catalog:${connectorId}`),
154
154
  chunk: (connectorId, revision, index) => validateStorageKey(`catalog:${connectorId}:chunk:${revision}:${index}`),
155
155
  };
156
- /** SDK catalog response cache, complete manifests published after their chunks. */
156
+ /** How long a connector-health record outlives its retry time, fencing the next probe's claim. */
157
+ export const CONNECTOR_HEALTH_LINGER_MS = 60_000;
158
+ /**
159
+ * Bounded connector health by connector id and a digest of the owner
160
+ * partition and described configuration. `discovery` holds the latest failed
161
+ * discovery's typed code, observation time, retry time and the catalog
162
+ * generation it saw; `status` the operator page's latest status observation
163
+ * (state, problem code, tool count, observation time, generation). Values
164
+ * never carry downstream text, tools or credentials.
165
+ */
166
+ export const connectorHealthKeys = {
167
+ family: {
168
+ name: "connector-health",
169
+ scope: "root",
170
+ prefixes: ["connector-health:v1:"],
171
+ version: { number: 1, in: "key" },
172
+ codec: jsonCodec,
173
+ ttl: { kind: "configured", by: "discovery: retry time plus 60 seconds; status: 120 seconds" },
174
+ durable: false,
175
+ },
176
+ discovery: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:discovery`),
177
+ status: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:status`),
178
+ };
179
+ /**
180
+ * SDK catalog response cache, complete manifests published after their chunks.
181
+ * Secret-bearing inputs reach these keys only through `secretDigest`.
182
+ */
157
183
  export const responseCacheKeys = {
158
184
  family: {
159
185
  name: "response-cache",
@@ -339,8 +365,31 @@ export const oauthConnectKeys = {
339
365
  used: (nonce) => validateStorageKey(`oauth:connect-used:${nonce}`),
340
366
  retry: (nonce) => validateStorageKey(`oauth:request-used:${nonce}`),
341
367
  };
342
- /** A bounded, credential-partitioned downstream protocol verdict. */
368
+ /**
369
+ * The last verification of a connector's operator-managed credential:
370
+ * `verification:v1`, holding an outcome class, its time and the vault
371
+ * revision of the save it observed. Payload-free and long-lived; a lost
372
+ * record only makes the operator page say "not verified".
373
+ */
374
+ export const verificationKeys = {
375
+ family: {
376
+ name: "verification",
377
+ scope: "connector",
378
+ prefixes: ["verification:v1"],
379
+ version: { number: 1, in: "key" },
380
+ codec: jsonCodec,
381
+ ttl: { kind: "durable" },
382
+ durable: false,
383
+ },
384
+ record: "verification:v1",
385
+ };
386
+ /** A bounded, credential-partitioned downstream protocol verdict, keyed through `secretDigest`. */
343
387
  export const NEGOTIATION_TTL_SECONDS = 300;
388
+ /**
389
+ * A legacy verdict lives longer: a server that upgrades still answers the
390
+ * legacy handshake, and one that stops answering it is renegotiated at once.
391
+ */
392
+ export const LEGACY_NEGOTIATION_TTL_SECONDS = 6 * 60 * 60;
344
393
  export const negotiationKeys = {
345
394
  family: {
346
395
  name: "negotiation",
@@ -348,7 +397,8 @@ export const negotiationKeys = {
348
397
  prefixes: ["negotiation:v1:"],
349
398
  version: { number: 1, in: "key" },
350
399
  codec: jsonCodec,
351
- ttl: { kind: "fixed", seconds: NEGOTIATION_TTL_SECONDS },
400
+ // The longest verdict lifetime; a modern verdict expires sooner.
401
+ ttl: { kind: "fixed", seconds: LEGACY_NEGOTIATION_TTL_SECONDS },
352
402
  durable: false,
353
403
  },
354
404
  verdict: (partitionDigest) => validateStorageKey(`negotiation:v1:${partitionDigest}`),
@@ -1,4 +1,5 @@
1
1
  import { validateStorageKey } from "./keys.js";
2
+ import { noteStorageWrite, observeStorageWrites } from "../runtime/request-reads.js";
2
3
  /** In-memory KV store with expiry. The default for dev and Node. */
3
4
  export function memoryStorage() {
4
5
  const map = new Map();
@@ -40,7 +41,9 @@ export function memoryStorage() {
40
41
  ...(exp !== undefined ? { exp } : {}),
41
42
  });
42
43
  };
43
- return {
44
+ // Each write settles synchronously, so one note covers it (request-reads.ts).
45
+ const root = {};
46
+ return observeStorageWrites({
44
47
  capabilities: { absoluteExpiry: true },
45
48
  describe: () => ({ kind: "memory" }),
46
49
  async get(key) {
@@ -49,10 +52,12 @@ export function memoryStorage() {
49
52
  },
50
53
  async set(key, value, opts) {
51
54
  validateStorageKey(key);
55
+ noteStorageWrite(root, key);
52
56
  write(key, value, opts);
53
57
  },
54
58
  async delete(key) {
55
59
  validateStorageKey(key);
60
+ noteStorageWrite(root, key);
56
61
  map.delete(key);
57
62
  },
58
63
  async list(prefix) {
@@ -68,6 +73,7 @@ export function memoryStorage() {
68
73
  // runs to completion before any other call on this map can start.
69
74
  async compareAndSet(key, expected, next, opts) {
70
75
  validateStorageKey(key);
76
+ noteStorageWrite(root, key);
71
77
  if ((fresh(key)?.value ?? null) !== expected)
72
78
  return false;
73
79
  if (next === null)
@@ -76,5 +82,5 @@ export function memoryStorage() {
76
82
  write(key, next, opts);
77
83
  return true;
78
84
  },
79
- };
85
+ }, { root });
80
86
  }
@@ -27,7 +27,7 @@ import { Effect } from "effect";
27
27
  import { activityBehaviorFacts, activityPackageVersion, activityClientFact, InvalidActivityCursorError, } from "../activity.js";
28
28
  import { assertKnownOptions, ConfigError, keys, optionsOf } from "../config-schema.js";
29
29
  import { agentFrictionForCode } from "../activity-friction.js";
30
- import { validateStorageKey } from "./keys.js";
30
+ import { accessTokenKeys, validateStorageKey } from "./keys.js";
31
31
  const sql = (text, ...params) => ({
32
32
  sql: text,
33
33
  params,
@@ -61,6 +61,8 @@ function schemaOnce(driver, ddl) {
61
61
  });
62
62
  }
63
63
  // --- key-value ----------------------------------------------------------
64
+ /** Keys one `getMany` statement binds. */
65
+ const GET_MANY_KEYS = 64;
64
66
  /** Expired rows one write removes on its way. */
65
67
  const SWEEP_ROWS = 16;
66
68
  function ttlMillis(ttlSeconds) {
@@ -105,6 +107,56 @@ export function nativeSqlStorage(driver) {
105
107
  return yield* current(key);
106
108
  });
107
109
  },
110
+ accessToken(hash) {
111
+ return Effect.gen(function* () {
112
+ const key = accessTokenKeys.lookup(hash);
113
+ yield* ensure();
114
+ // The links select candidate rows only. Admission still parses every
115
+ // raw value and addresses it by its physical key, so JSON extraction
116
+ // differences (e.g. duplicate fields) cannot substitute another slot.
117
+ // json_valid guards malformed stored JSON; expiry uses the DB clock.
118
+ const rows = yield* driver.all(sql(`WITH lookup AS (
119
+ SELECT key, value FROM connecta_kv WHERE key = ? AND ${live}
120
+ ), record AS (
121
+ SELECT key, value FROM connecta_kv WHERE key = ? ||
122
+ (SELECT CASE WHEN json_valid(value) THEN json_extract(value, '$.id') END FROM lookup)
123
+ AND ${live}
124
+ ), slot AS (
125
+ SELECT key, value FROM connecta_kv WHERE key = ? ||
126
+ (SELECT CASE WHEN json_valid(value) THEN json_extract(value, '$.agent') END FROM record)
127
+ AND ${live}
128
+ )
129
+ SELECT key, ${textColumn("value")} FROM lookup
130
+ UNION ALL SELECT key, ${textColumn("value")} FROM record
131
+ UNION ALL SELECT key, ${textColumn("value")} FROM slot`, key, accessTokenKeys.recordPrefix, accessTokenKeys.agent("")));
132
+ return new Map(rows.map((row) => [row.key, textOf(row.value, row.value_bytes, "Stored access token metadata")]));
133
+ });
134
+ },
135
+ getMany(keys) {
136
+ return Effect.gen(function* () {
137
+ for (const key of keys)
138
+ validateStorageKey(key);
139
+ if (keys.length === 0)
140
+ return [];
141
+ yield* ensure();
142
+ // One statement for every key: a chunked value costs one round trip.
143
+ // Rows answer by position, never by the key text a driver returns.
144
+ // Bound parameters stay under D1's per-statement limit of 100.
145
+ const values = keys.map(() => null);
146
+ for (let offset = 0; offset < keys.length; offset += GET_MANY_KEYS) {
147
+ const batch = keys.slice(offset, offset + GET_MANY_KEYS);
148
+ const rows = yield* driver.all({
149
+ sql: `WITH wanted (position, key) AS (VALUES ${batch.map((_, i) => `(${offset + i}, ?)`).join(", ")})
150
+ SELECT wanted.position AS position, ${textColumn("value")}
151
+ FROM wanted JOIN connecta_kv ON connecta_kv.key = wanted.key AND ${live}`,
152
+ params: batch,
153
+ });
154
+ for (const row of rows)
155
+ values[row.position] = textOf(row.value, row.value_bytes, `the stored value of ${JSON.stringify(keys[row.position])}`);
156
+ }
157
+ return values;
158
+ });
159
+ },
108
160
  set(key, value, opts) {
109
161
  return Effect.gen(function* () {
110
162
  validateStorageKey(key);
@@ -1,11 +1,20 @@
1
1
  import { runEdge } from "../runtime/run.js";
2
2
  import { registerNativeStorage, registerNativeActivity } from "../runtime/native-storage.js";
3
+ import { observedWrite, observeStorageWrites } from "../runtime/request-reads.js";
3
4
  import { nativeSqlDriver } from "./sql-driver.js";
4
5
  import { nativeSqlStorage, nativeSqlActivityStore } from "./sql-native.js";
5
6
  /** `KVStorage` over one `connecta_kv` table. */
6
7
  export function sqlStorage(driver, kind) {
7
- const native = nativeSqlStorage(nativeSqlDriver(driver));
8
- return registerNativeStorage({
8
+ const store = nativeSqlStorage(nativeSqlDriver(driver));
9
+ // Request read memos see every write begin and settle (request-reads.ts).
10
+ const root = {};
11
+ const native = {
12
+ ...store,
13
+ set: (key, value, options) => observedWrite(root, key, store.set(key, value, options)),
14
+ delete: (key) => observedWrite(root, key, store.delete(key)),
15
+ compareAndSet: (key, expected, next, options) => observedWrite(root, key, store.compareAndSet(key, expected, next, options)),
16
+ };
17
+ const edge = registerNativeStorage({
9
18
  capabilities: { absoluteExpiry: true },
10
19
  describe: () => ({ kind }),
11
20
  get: (key) => runEdge(native.get(key)),
@@ -14,6 +23,7 @@ export function sqlStorage(driver, kind) {
14
23
  list: (prefix) => runEdge(native.list(prefix)),
15
24
  compareAndSet: (key, expected, next, options) => runEdge(native.compareAndSet(key, expected, next, options)),
16
25
  }, native);
26
+ return observeStorageWrites(edge, { root });
17
27
  }
18
28
  /**
19
29
  * One append-only row per completed downstream call. Rows carry no arguments,
@@ -0,0 +1,32 @@
1
+ import { Context } from "effect";
2
+ import { bytesToHex } from "./bytes.js";
3
+ import { serviceFor, setViewService } from "./runtime/call.js";
4
+ class StorageDigestKey extends Context.Service()("connecta/operation/StorageDigestKey") {
5
+ }
6
+ const importHmac = (raw) => crypto.subtle.importKey("raw", new Uint8Array(raw), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
7
+ // Drawn synchronously on first use, so concurrent first digests share one key.
8
+ let processBytes;
9
+ let processKey;
10
+ const fallbackKey = () => (processKey ??= importHmac((processBytes ??= crypto.getRandomValues(new Uint8Array(32)))));
11
+ const vaultKeys = new WeakMap();
12
+ /** Bind the vault's storage digest key to a registry-built context, when it has one. */
13
+ export function attachStorageDigestKey(ctx, vault) {
14
+ const derive = vault?.storageDigestKey;
15
+ if (typeof derive !== "function")
16
+ return;
17
+ let key = vaultKeys.get(vault);
18
+ if (!key) {
19
+ let pending;
20
+ key = () => (pending ??= derive.call(vault));
21
+ vaultKeys.set(vault, key);
22
+ }
23
+ setViewService(ctx, StorageDigestKey, key);
24
+ }
25
+ /**
26
+ * `hmac:<hex>` of a secret-bearing value, for a stored key only. Stable for one
27
+ * deployment key, so equal inputs still address the same entry.
28
+ */
29
+ export async function secretDigest(ctx, value) {
30
+ const key = await (serviceFor(ctx, StorageDigestKey) ?? fallbackKey)();
31
+ return `hmac:${bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value))))}`;
32
+ }
package/dist/timeout.js CHANGED
@@ -9,6 +9,14 @@ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
9
9
  * to trip only on a pathological hang, not a realistically slow probe.
10
10
  */
11
11
  export const DEFAULT_PROBE_TIMEOUT_MS = CONFIG_DEFAULTS.discovery.probeTimeoutMs;
12
+ /**
13
+ * Each connector's probe deadline in an unscoped search, or
14
+ * `discovery.probeTimeoutMs` when that is shorter. Scoped search, describe
15
+ * and calls keep the full probe deadline.
16
+ */
17
+ export const UNSCOPED_PROBE_TIMEOUT_MS = 5_000;
18
+ /** One unscoped search's whole discovery fan-out ends within this many ms. */
19
+ export const UNSCOPED_DISCOVERY_BUDGET_MS = 9_000;
12
20
  /** A finite, positive integer number of milliseconds, or undefined. */
13
21
  export function normalizeTimeoutMs(value) {
14
22
  if (value === undefined || !Number.isFinite(value) || !(value > 0)) {
package/dist/types.d.ts CHANGED
@@ -193,6 +193,14 @@ export interface ConnectorCredentialConfig {
193
193
  */
194
194
  fields?: ConnectorCredentialFieldConfig[];
195
195
  }
196
+ /**
197
+ * Why a credential test did not pass, as a typed fact with no downstream text.
198
+ * `rejected`: the service refused the credential. `scope`: it accepted the
199
+ * credential but refused a permission the connection needs. `unreachable`: the
200
+ * service could not be reached or was unavailable. `timeout`: it did not
201
+ * answer within the test's deadline. `failed`: anything else.
202
+ */
203
+ export type CredentialFailureClass = "rejected" | "scope" | "unreachable" | "timeout" | "failed";
196
204
  export interface CredentialTestResult {
197
205
  ok: boolean;
198
206
  /**
@@ -201,6 +209,25 @@ export interface CredentialTestResult {
201
209
  * `ok`, and the log records only that a test failed.
202
210
  */
203
211
  message?: string;
212
+ /**
213
+ * Why a failed test failed. Optional: an unclassified failure is `failed`.
214
+ * Ignored when `ok` is true.
215
+ */
216
+ failure?: CredentialFailureClass;
217
+ }
218
+ /**
219
+ * How agent-facing text names Connecta's routes on the surface serving an
220
+ * operation: `call_tool`, `call_destructive_tool`, `search_tools`,
221
+ * `execute_code`, `authorize_connector` and `skills` on the six-tool surface,
222
+ * and other wording where those tools are not listed.
223
+ */
224
+ export interface AgentRouteNames {
225
+ readonly search: string;
226
+ readonly read: string;
227
+ readonly write: string;
228
+ readonly program: string;
229
+ readonly authorize: string;
230
+ readonly skills: string;
204
231
  }
205
232
  export interface ConnectorContext {
206
233
  /** Explicit downstream consent initiation, never set by status/catalog/calls. */
@@ -236,6 +263,12 @@ export interface ConnectorContext {
236
263
  defer?: (promise: Promise<unknown>) => void;
237
264
  /** Requested connector-operation deadline in milliseconds. */
238
265
  timeoutMs?: number;
266
+ /**
267
+ * Route names for text this operation writes for the agent, such as "call
268
+ * authorize_connector" in an auth error. Connecta sets it on every context
269
+ * it builds; build such text from it rather than spelling a tool name.
270
+ */
271
+ routes?: AgentRouteNames;
239
272
  }
240
273
  type ConnectorStatusState = "ok" | "per_caller" | "auth_required" | "credential_required" | "error";
241
274
  /** The last agent-facing catalog read this runtime served for one connector. */
@@ -1,43 +1,36 @@
1
+ import { META_ROUTES } from "./agent-routes.js";
2
+ import { UNCOUNTED_GUEST_FUNCTIONS } from "./guest-functions.js";
1
3
  /** Self-contained content for the usage skill; independent of skill registration. */
2
4
  export const USAGE_SKILL = `---
3
5
  name: usage
4
- description: How to route work between one execute_code program and Connecta's explicit call, authorization, and result tools.
6
+ description: How to route work between one ${META_ROUTES.program} program and Connecta's explicit call, authorization, and result tools.
5
7
  ---
6
8
 
7
9
  # Connecta usage
8
10
 
9
11
  ## Choose the route
10
12
 
11
- | Work | Tool |
12
- | --- | --- |
13
- | One known-address read | call_tool |
14
- | One known-address write | call_destructive_tool |
15
- | Discovery, multiple or dependent calls, paging, reduction | execute_code |
16
-
17
- Read one account with call_tool: \`{ "address": "crm.get_account", "args": { "id": "acct_42" } }\`.
18
- Discover and call together when schemas suffice. search_tools covers catalog
19
- inspection. On a read-only endpoint, as execute_code's description says, search
20
- omits writes (guides may name them) and each fails read_only_endpoint; tell the
21
- user. Unannotated tools fail closed to writes.
22
- For a requested write, make the call through the write route; the host may ask
23
- the user to confirm it. Connecta classifies and routes writes; the host owns approval.
13
+ Route as the server instructions and ${META_ROUTES.program}'s first sentence say.
14
+ Read one account with ${META_ROUTES.read}: \`{ "address": "crm.get_account", "args": { "id": "acct_42" } }\`.
15
+ On a read-only endpoint search omits writes (guides may name them) and each
16
+ fails read_only_endpoint; tell the user. Unannotated tools fail closed to writes.
17
+ Connecta classifies and routes a requested write; the host owns approval.
24
18
 
25
19
  ## The global and the envelopes
26
20
 
27
21
  Pass JavaScript \`async () => { ... }\`, no arguments. Do not shadow \`connecta\`.
28
22
  No imports, require, filesystem, fetch, or timers.
29
23
 
30
- Use \`connecta.call(address, args, { timeoutMs })\` or
31
- \`connecta.call({ address, args, timeoutMs })\`; both return \`{ data, format }\`.
32
- \`format: "json"\` carries JSON; \`format: "text"\` carries text. Parse text only
33
- when documented as JSON. Verify collection roots.
24
+ \`connecta.call(address, args, { timeoutMs })\` and
25
+ \`connecta.call({ address, args, timeoutMs })\` return \`{ data, format }\`.
26
+ Parse \`format: "text"\` only when documented as JSON. Verify collection roots.
34
27
 
35
28
  \`connecta.read("resource://id/"+encodeURIComponent(uri))\` returns \`{ contents }\` for advertised URIs.
36
29
 
37
- Search/describe return \`{ tools }\`, never an array. Search also returns
38
- \`catalogErrors\`, pagination, and possible \`absence\`; check failures before
39
- selecting a match. JSON schemas are the program default. Compact schemas have
40
- \`schemaFormat: "text"\`; do not read their .properties. Select the exact tool and
30
+ Search/describe return \`{ tools }\`, never an array. Check search's
31
+ \`catalogErrors\` and \`absence\` before selecting a match. Search defaults to a
32
+ TypeScript \`signature\`; text schemas have \`schemaFormat: "text"\` and no .properties.
33
+ A \`readyCalls\` entry is a complete read: \`connecta.call(entry.address, entry.args)\`. Select the exact tool and
41
34
  account/environment, inspect its schema, and supply its required keys and types.
42
35
 
43
36
  Discover an unfamiliar read and reduce its result:
@@ -79,7 +72,7 @@ async () => {
79
72
 
80
73
  Host-call budget exhaustion ends the run with budget_exceeded. Catch and
81
74
  allSettled cannot recover it or return partial output. Bound batches before
82
- starting them; every guest call except emit spends host calls.
75
+ starting them; every guest call except ${UNCOUNTED_GUEST_FUNCTIONS} spends host calls.
83
76
  Split remaining reads. Never replay writes for their output.
84
77
 
85
78
  ## Per-connector guides
@@ -134,15 +127,3 @@ base64 and PNG/JPEG/GIF/WebP MIME. Emit works without await, delivers on success
134
127
  and obeys byte/block budgets; text shares the return cap. Return reduced JSON.
135
128
 
136
129
  `;
137
- /** Kept in sync with GuestApi by the declaration test. These are type notation, not runnable JS. */
138
- export const GUEST_API_DECLARATION = `search(args?: CatalogSearchArgs): Promise<CatalogSearchResult>;
139
- describe(args?: CatalogDescribeArgs): Promise<{ tools: CatalogDescription[] }>;
140
- call(address: string, args?: unknown, options?: { timeoutMs?: number }): Promise<GuestResult>;
141
- call(request: { address: string; args?: unknown; timeoutMs?: number }): Promise<GuestResult>;
142
- read(uri: string): Promise<GuestResourceResult>;
143
- result(
144
- id: string | GuestResultHandle,
145
- options?: { page?: number; offset?: number; maxBytes?: number },
146
- ): Promise<GuestResultPage>;
147
- skill(name: string): Promise<{ name: string; text: string; format: "text" }>;
148
- emit(block: GuestNativeBlock | GuestResult): PromiseLike<void>;`;
@@ -1,4 +1,5 @@
1
1
  import { ConnectorCallError } from "./errors.js";
2
+ import type { ArgumentValidationDetails } from "./errors.js";
2
3
  import { type FailureSubject } from "./operator-record.js";
3
4
  import type { JsonSchema, Logger } from "./types.js";
4
5
  export interface ValidateToolInputOptions {
@@ -31,6 +32,8 @@ export interface CompileValidatorOptions {
31
32
  }
32
33
  /** Optional in-process properties have the same absence semantics as JSON input. Never mutate the caller. */
33
34
  export declare function normalizeToolInput<A>(value: A): A;
35
+ /** One example synthesized from the published schema alone, checked by its validator; never caller values. */
36
+ export declare function schemaExample(schema: JsonSchema): Pick<ArgumentValidationDetails, "example" | "exampleUnavailable">;
34
37
  /**
35
38
  * Validate call arguments against a tool's JSON Schema.
36
39
  *