@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,3 +1,5 @@
1
+ import { bytesToHex } from "./bytes.js";
2
+ import { routeNames } from "./agent-routes.js";
1
3
  import { duringViewEffect } from "./runtime/call.js";
2
4
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
3
5
  import { Effect } from "effect";
@@ -35,7 +37,7 @@ function shortHash(text) {
35
37
  const shortHashNative = /* @__PURE__ */ registerAuth(shortHash, function (text) {
36
38
  return authFlow(function* () {
37
39
  const digest = new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", enc.encode(text)])));
38
- return [...digest.subarray(0, HASH_CHARS / 2)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
40
+ return bytesToHex(digest.subarray(0, HASH_CHARS / 2));
39
41
  });
40
42
  });
41
43
  /**
@@ -148,6 +150,8 @@ export class NativeSurface {
148
150
  // Listing omits a read-only endpoint's writes, as search does.
149
151
  readOnly: options.readOnly,
150
152
  defer: options.defer,
153
+ // Declared schema descriptions read as this pool serves them.
154
+ routes: routeNames(options.executeCode ? "native-code" : "native"),
151
155
  });
152
156
  }
153
157
  /**
@@ -182,7 +186,7 @@ export class NativeSurface {
182
186
  ...(entry ? { _meta: { "dev.connecta/address": `${entry.connector.id}.${entry.tool.name}` } } : {}),
183
187
  }, authMethod((args, request) => Effect.gen({ self: this }, function* () {
184
188
  const result = yield* authInvoke(this, "call", [name, args ?? {}, request]);
185
- return nativeRecovery(result, this.options.executeCode, name);
189
+ return nativeRecovery(result, name);
186
190
  })));
187
191
  }
188
192
  /**
@@ -1,3 +1,4 @@
1
+ import { decodeBase64url } from "./bytes.js";
1
2
  import { upwardStep } from "./upward.js";
2
3
  import { authCall, authInvoke, authFlow, authStorage, registerAuth } from "./auth/native.js";
3
4
  import { runEdge } from "./runtime/run.js";
@@ -58,8 +59,7 @@ function encodeSegment(value) {
58
59
  function decodeSegment(value) {
59
60
  if (!/^[A-Za-z0-9_-]+$/.test(value))
60
61
  throw new Error("Invalid handoff encoding");
61
- const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
62
- const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
62
+ const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decodeBase64url(value));
63
63
  if (encodeSegment(decoded) !== value)
64
64
  throw new Error("Noncanonical handoff encoding");
65
65
  return decoded;
@@ -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
+ }
@@ -1,8 +1,18 @@
1
1
  // Private, request-free discovery facts. No provider, schema, handler or I/O dependency.
2
2
  import { lexicalSearchContent, lexicalQueryTerms } from "./catalog.js";
3
+ /** Ready calls one vocabulary offers a search: only its top-ranked operation. */
4
+ const MAX_READY_PER_VOCABULARY = 1;
5
+ /**
6
+ * Normalized action words. A summary may supply these; every other query word
7
+ * names an object and must match the operation's resource identity.
8
+ */
9
+ const ACTIONS = new Set(["get", "list", "count", "show", "create", "update", "delete"]);
10
+ /** Path tokens that version an API rather than name a resource. */
11
+ const VERSION = /^v?\d+$/;
3
12
  const tools = new WeakMap();
4
13
  const connectors = new WeakMap();
5
- export const OPERATION_VOCABULARY_BOUNDS = {
14
+ const synonymGroups = new WeakMap();
15
+ const OPERATION_VOCABULARY_BOUNDS = {
6
16
  operations: 8192,
7
17
  rowChars: 1024,
8
18
  chars: 2_097_152,
@@ -28,20 +38,56 @@ const aliases = {
28
38
  modify: "update",
29
39
  remove: "delete",
30
40
  };
31
- const terms = (text) => [
32
- ...new Set(lexicalQueryTerms(lexicalSearchContent(text)).map((word) => aliases[word] ?? token(word))),
33
- ];
41
+ const normal = (word) => aliases[word] ?? token(word);
42
+ const terms = (text) => [...new Set(lexicalQueryTerms(lexicalSearchContent(text)).map(normal))];
43
+ /** Resource words of a path segment or operation id, without version or action words. */
44
+ const identity = (text) => terms(text).filter((word) => !VERSION.test(word) && !ACTIONS.has(word));
45
+ function resourceOf(path, operationId) {
46
+ const segments = path.slice(0, OPERATION_VOCABULARY_BOUNDS.rowChars).split("/").map(identity);
47
+ const names = [...segments, identity(operationId.slice(0, OPERATION_VOCABULARY_BOUNDS.rowChars))].filter((name) => name.length > 0);
48
+ return { names, last: segments.at(-1) ?? [], terms: new Set(names.flat()) };
49
+ }
50
+ /**
51
+ * Attach a maintained provider's lexical synonym groups to its connector. Each
52
+ * group's words are interchangeable query terms for that connector's tool
53
+ * ranking and operation vocabulary; nothing is inferred.
54
+ */
55
+ export function registerSearchSynonyms(connector, groups) {
56
+ synonymGroups.set(connector, groups);
57
+ }
58
+ /** A connector's groups, including those a provider stamp inherits. */
59
+ function groupsOf(connector) {
60
+ let target = connector;
61
+ while (target && !synonymGroups.has(target))
62
+ target = Object.getPrototypeOf(target);
63
+ return target ? synonymGroups.get(target) : undefined;
64
+ }
65
+ /** The other words of every group holding `word`, each word under `normalize`. */
66
+ function synonymsIn(groups, word, normalize = (raw) => raw) {
67
+ return groups.flatMap((group) => {
68
+ const words = group.map(normalize);
69
+ return words.includes(word) ? words.filter((other) => other !== word) : [];
70
+ });
71
+ }
72
+ /** Word to its declared synonyms for a connector's tool ranking, when its provider declares any. */
73
+ export function searchSynonyms(connector) {
74
+ const groups = groupsOf(connector);
75
+ return groups && ((word) => synonymsIn(groups, word));
76
+ }
34
77
  /** Register only construction-authored compact summaries; never whole request/response schemas. */
35
78
  export function registerOperationVocabulary(tool, rows) {
36
79
  let postings;
37
80
  const routes = [];
38
81
  const cache = new Map();
39
- tools.set(tool, (query, visible, safety, readOnly) => {
82
+ tools.set(tool, (query, visible, safety, readOnly, synonyms) => {
83
+ const none = { matched: false, ready: [] };
40
84
  if (query.length > OPERATION_VOCABULARY_BOUNDS.queryChars)
41
- return false;
85
+ return none;
42
86
  const wanted = terms(query);
43
87
  if (!wanted.length || wanted.length > OPERATION_VOCABULARY_BOUNDS.terms)
44
- return false;
88
+ return none;
89
+ // Each wanted word matches through itself or any declared synonym.
90
+ const alternatives = wanted.map((word) => [word, ...(synonyms ? synonymsIn(synonyms, word, normal) : [])]);
45
91
  if (!postings) {
46
92
  postings = new Map();
47
93
  let chars = 0;
@@ -52,7 +98,19 @@ export function registerOperationVocabulary(tool, rows) {
52
98
  break;
53
99
  chars += text.length;
54
100
  const id = routes.length;
55
- routes.push({ route: row.route, write: row.write });
101
+ routes.push({
102
+ route: row.route,
103
+ write: row.write,
104
+ ...(row.ready
105
+ ? {
106
+ ready: {
107
+ path: row.ready.path,
108
+ summary: row.ready.summary.slice(0, OPERATION_VOCABULARY_BOUNDS.rowChars),
109
+ ...resourceOf(row.ready.path, row.ready.operationId),
110
+ },
111
+ }
112
+ : {}),
113
+ });
56
114
  for (const word of terms(text)) {
57
115
  const hits = postings.get(word);
58
116
  if (hits)
@@ -62,17 +120,20 @@ export function registerOperationVocabulary(tool, rows) {
62
120
  }
63
121
  }
64
122
  }
65
- const key = wanted.join(" ");
123
+ const key = alternatives.map((words) => words.join("|")).join(" ");
66
124
  let found = cache.get(key);
67
125
  if (!found) {
68
- const lists = wanted.map((word) => postings.get(word) ?? []).sort((a, b) => a.length - b.length);
126
+ const lists = alternatives
127
+ .map((words) => [...new Set(words.flatMap((word) => postings.get(word) ?? []))])
128
+ .sort((a, b) => a.length - b.length);
69
129
  const rest = lists.slice(1).map((list) => new Set(list));
70
130
  found = lists[0].filter((id) => rest.every((set) => set.has(id)));
71
131
  if (cache.size >= OPERATION_VOCABULARY_BOUNDS.cache)
72
132
  cache.delete(cache.keys().next().value);
73
133
  cache.set(key, found);
74
134
  }
75
- return found.some((id) => {
135
+ // The same endpoint, safety and visibility checks decide both answers.
136
+ const admitted = found.filter((id) => {
76
137
  const row = routes[id];
77
138
  if (row.write && readOnly)
78
139
  return false;
@@ -80,6 +141,47 @@ export function registerOperationVocabulary(tool, rows) {
80
141
  return false;
81
142
  return row.route !== undefined && visible.has(row.route);
82
143
  });
144
+ // A ready call must name what the query asks for. Every object word, or
145
+ // a declared synonym, belongs to a whole resource name whose words the
146
+ // query states in order. A word met only in a summary, or only as part of
147
+ // a compound name, can route a search, never stand in for the resource.
148
+ const objects = alternatives.filter((words) => !ACTIONS.has(words[0]));
149
+ const at = (name, start) => start + name.length <= objects.length && name.every((word, offset) => objects[start + offset].includes(word));
150
+ const covers = (names) => {
151
+ const covered = objects.map(() => false);
152
+ for (const name of names)
153
+ for (let start = 0; start < objects.length; start++)
154
+ if (at(name, start))
155
+ name.forEach((_, offset) => (covered[start + offset] = true));
156
+ return covered.every(Boolean);
157
+ };
158
+ const asked = new Set(alternatives.flat());
159
+ return {
160
+ matched: admitted.length > 0,
161
+ ready: objects.length
162
+ ? admitted
163
+ .flatMap((id) => {
164
+ const { route, write, ready } = routes[id];
165
+ if (!ready || write || !route || !covers(ready.names))
166
+ return [];
167
+ return [
168
+ {
169
+ path: ready.path,
170
+ summary: ready.summary,
171
+ route,
172
+ id,
173
+ // Relevance first: the last segment names the resource asked
174
+ // for, then the fewest resource words the query did not ask for.
175
+ last: Number(objects.some((_, start) => ready.last.length > 0 && at(ready.last, start))),
176
+ extra: [...ready.terms].filter((word) => !asked.has(word) && !ACTIONS.has(word)).length,
177
+ },
178
+ ];
179
+ })
180
+ .sort((a, b) => b.last - a.last || a.extra - b.extra || a.path.length - b.path.length || a.id - b.id)
181
+ .slice(0, MAX_READY_PER_VOCABULARY)
182
+ .map(({ path, summary, route }) => ({ path, summary, route }))
183
+ : [],
184
+ };
83
185
  });
84
186
  }
85
187
  /** Move private facts across the public ApiTool -> Connector construction boundary. */
@@ -91,11 +193,31 @@ export function registerConnectorVocabulary(connector, declarations) {
91
193
  if (entries.length)
92
194
  connectors.set(connector, new Map(entries));
93
195
  }
196
+ /**
197
+ * Read tools whose operation vocabulary matches every query word, and the
198
+ * fully determined reads among those operations, each through a visible read
199
+ * tool of `catalog`: the caller's view, after endpoint, connector, hidden-tool
200
+ * and grant filtering.
201
+ */
94
202
  export function operationVocabularyMatches(connector, catalog, query, safety, readOnly) {
95
203
  const registered = connectors.get(connector);
204
+ const tools = [];
205
+ const ready = [];
96
206
  if (!registered)
97
- return [];
207
+ return { tools, ready };
98
208
  const visible = new Set(catalog.map((tool) => tool.name));
99
- return catalog.filter((tool) => tool.classification === "read" &&
100
- registered.get(tool.name)?.(query, visible, safety, readOnly || connector.readOnly === true));
209
+ const synonyms = groupsOf(connector);
210
+ for (const tool of catalog) {
211
+ const vocabulary = tool.classification === "read" ? registered.get(tool.name) : undefined;
212
+ const found = vocabulary?.(query, visible, safety, readOnly || connector.readOnly === true, synonyms);
213
+ if (!found?.matched)
214
+ continue;
215
+ tools.push(tool);
216
+ for (const { route, ...call } of found.ready) {
217
+ const target = catalog.find((candidate) => candidate.name === route);
218
+ if (target?.classification === "read")
219
+ ready.push({ ...call, tool: target });
220
+ }
221
+ }
222
+ return { tools, ready };
101
223
  }
@@ -8,7 +8,7 @@ type FailureStep = "MCP handshake" | "tools/list" | "tools/call" | "resources/re
8
8
  */
9
9
  export declare const OAUTH_ERROR_CODES: ReadonlySet<string>;
10
10
  /** Inbound denial facts, never identity text or provider diagnostics. */
11
- export type AuthDenialReason = "authentication_abandoned_saturated" | "authentication_busy" | "authentication_failed" | "authentication_provider_missing" | "authentication_unreachable" | "access_context_missing" | "access_context_unavailable" | "access_audience_invalid" | "access_identity_unavailable" | "access_authorization_header_rejected" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "token_identity_mismatch" | "organization_membership_denied" | "organization_lookup_failed" | "organization_lookup_busy" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "not_maintainer" | "pool_not_declared" | "not_pool_member";
11
+ export type AuthDenialReason = "authentication_abandoned_saturated" | "authentication_busy" | "authentication_failed" | "authentication_provider_missing" | "authentication_unreachable" | "access_context_missing" | "access_context_unavailable" | "access_audience_invalid" | "access_identity_unavailable" | "access_authorization_header_rejected" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "token_identity_mismatch" | "organization_membership_denied" | "organization_lookup_failed" | "organization_lookup_busy" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "ambiguous_pool" | "not_maintainer" | "pool_not_declared" | "not_pool_member";
12
12
  /**
13
13
  * Name `ctor`'s instances `label` in records and fixed text. Call once, at
14
14
  * module scope, beside the class. An error whose prototype chain reaches no
@@ -110,7 +110,7 @@ export declare function failureRecord(subject: FailureSubject, failure?: unknown
110
110
  * The events an operator log reports failures as. Closed: a new one is a
111
111
  * reviewed line here, never a string assembled at the call site.
112
112
  */
113
- export type FailureEvent = "Authentication admission refused" | "Cloudflare Access verification unavailable" | "operator_refused" | "Authentication released after its pending work became unreachable" | "negotiation cache read failed" | "negotiation cache write failed" | "pool tool grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP default endpoint request denied" | "MCP pool request denied" | "MCP agent endpoint request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "operator agent slots read failed" | "result paging unavailable" | "native catalog unavailable" | "native name collision" | "native tools/list refused a pool over the 1,000-tool native cap" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
113
+ export type FailureEvent = "Authentication admission refused" | "Cloudflare Access verification unavailable" | "operator_refused" | "Authentication released after its pending work became unreachable" | "negotiation cache read failed" | "negotiation cache write failed" | "discovery health read failed" | "discovery health write failed" | "pool tool grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "credential verification record failed" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP default endpoint request denied" | "MCP pool request denied" | "MCP agent endpoint request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "operator agent slots read failed" | "result paging unavailable" | "native catalog unavailable" | "native name collision" | "native tools/list refused a pool over the 1,000-tool native cap" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
114
114
  /**
115
115
  * Log one failure as `[connecta] <event>` and its record. A record
116
116
  * `failureRecord` did not build is replaced, whatever its shape, by a fixed
@@ -1,52 +1,4 @@
1
- // What an operator may read about a failure: log records and status text.
2
- // Web-API only — no node: imports here.
3
- //
4
- // Inbound identity-provider text never enters operator logs or status, even
5
- // when it fits a grammar. Activity retains its separate typed actor fields.
6
- //
7
- // Threat model. INV-6 here covers records of calls and failures: logs,
8
- // activity rows, status and statusFor, health, doctor output, and any native
9
- // runtime output connecta triggers (workerd's console). Text a downstream
10
- // server, HTTP API, or client authored, and anything derived from it, never
11
- // reaches one as arbitrary text. Self-declared client name/version facts in
12
- // activity use the bounded ASCII grammars in activityClientFact; invalid values are
13
- // absent at recording, SQL write/read, and operator UI boundaries. Client
14
- // capabilities and other metadata are never recorded. Every typed field
15
- // connecta records is checked against a closed set or a grammar. Operator-
16
- // authored code (custom connectors, plugin `status()`, decorators) is trusted
17
- // to follow the contract, and connecta still checks
18
- // the typed fields it records from it; deliberately adversarial operator code
19
- // is out of scope.
20
- //
21
- // Catalog metadata a connector serves is configuration the operator chose to
22
- // load, and agents already see it: a tool name that fits MCP's tool-name
23
- // grammar, a description, and input and output schemas may appear in the
24
- // authenticated operator page's catalog views, and such a name may be
25
- // logged. A name outside the grammar is withheld everywhere operator-facing,
26
- // the page included. Names containing C0, DEL, or C1 are dropped at catalog
27
- // intake; spaces and non-ASCII names remain callable with withheld records.
28
- //
29
- // INV-6 says logs carry no arguments, results, code, or raw downstream error
30
- // text. Filtering errors where they arise kept missing sources: a validator's
31
- // diagnostic, a stream's TypeError, a JSON-RPC error the agent may see. So the
32
- // rule is enforced here, at the sink. A failure reaches an operator only as
33
- // the fields of a `FailureRecord`, which `failureRecord` alone builds: each
34
- // one copied from an explicit list, checked against a constant table or a
35
- // grammar, and never taken from an error's message, cause, stack, data,
36
- // nested errors, or name. `logFailure` writes nothing else, and every status
37
- // message about a failure comes from `describeFailure`
38
- // (test/operator-record-sources.node.test.ts holds src/ to that).
39
- //
40
- // Provenance is by identity, never by shape. Text that merely looks like
41
- // connecta's (an error named like one of its classes, a status sentence
42
- // shaped like a record) carries no weight: a class label comes from the
43
- // prototype chain, a status message from the status object connecta built.
44
- //
45
- // Agent-facing results are a different surface with a different rule: a
46
- // downstream's own answer to a call may reach the agent that made it (see
47
- // documentation/architecture.md, "Errors and records"). Core's sent-secrets
48
- // boundary removes credentials used by the upstream request from those answers, including
49
- // allowed diagnostics, tool results and nested error causes/data (INV-5).
1
+ // Payload-free operator failure records. See documentation/architecture.md, "Errors and records".
50
2
  import { carryProviderFailure } from "./provider-call-facts.js";
51
3
  import { followRedactedCopies, keepErrorName } from "./redaction-identity.js";
52
4
  import { classificationCode, ConnectorCallError, NETWORK_ERROR_CODES, WithheldTextError, } from "./errors.js";
@@ -134,6 +86,7 @@ const AUTH_DENIAL_REASONS = new Set([
134
86
  "email_domain_denied",
135
87
  "gate_denied",
136
88
  "gate_failed",
89
+ "ambiguous_pool",
137
90
  "not_maintainer",
138
91
  "pool_not_declared",
139
92
  "not_pool_member",
@@ -25,8 +25,18 @@ export interface OperatorConnectorOverlay {
25
25
  catalogAgeMs: number | null;
26
26
  /** Latest visible call in the bounded activity window, or null when unknown. */
27
27
  lastCall: OperatorLastCall | null;
28
+ /**
29
+ * Age of the stored status observation this row shows, when it came from
30
+ * storage rather than a live read. Its tools may be empty until a live read.
31
+ */
32
+ observedAgeMs?: number;
33
+ /**
34
+ * Browser-only: when that stored observation was made, in epoch
35
+ * milliseconds, fixed once as the row lands so its age keeps counting.
36
+ */
37
+ observedAt?: number;
28
38
  }
29
- /** Whole connector access, or exact tool grants. `requireReadOnly` is always false since guarded grants were removed (#837). */
39
+ /** Whole connector access, or exact tool grants. `requireReadOnly` is always false since guarded grants were removed. */
30
40
  export interface OperatorGrant {
31
41
  connectorId: string;
32
42
  tools: "all" | Array<{
@@ -197,5 +207,5 @@ export interface OperatorUiContractV2 {
197
207
  viewerKey: string;
198
208
  };
199
209
  }
200
- /** The operator contract `GET /ui/api/config` serves; schema version 1 was removed (#837). */
210
+ /** The operator contract `GET /ui/api/config` serves; schema version 1 was removed. */
201
211
  export type OperatorUiContract = OperatorUiContractV2;