@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,7 +1,17 @@
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();
14
+ const synonymGroups = new WeakMap();
5
15
  const OPERATION_VOCABULARY_BOUNDS = {
6
16
  operations: 8192,
7
17
  rowChars: 1024,
@@ -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
@@ -86,6 +86,7 @@ const AUTH_DENIAL_REASONS = new Set([
86
86
  "email_domain_denied",
87
87
  "gate_denied",
88
88
  "gate_failed",
89
+ "ambiguous_pool",
89
90
  "not_maintainer",
90
91
  "pool_not_declared",
91
92
  "not_pool_member",
@@ -25,6 +25,16 @@ 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
39
  /** Whole connector access, or exact tool grants. `requireReadOnly` is always false since guarded grants were removed. */
30
40
  export interface OperatorGrant {