@zackbart/connecta 0.41.0 → 0.42.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.
- package/CHANGELOG.md +61 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +2 -1
- package/dist/activity.js +5 -5
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-routes.js +31 -5
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/static-oauth.js +26 -18
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +292 -24
- package/dist/catalog-freshness.js +12 -0
- package/dist/catalog-service.js +373 -70
- package/dist/catalog.js +147 -58
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +43 -17
- package/dist/config.js +26 -10
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +62 -5
- package/dist/connectors/remote-mcp.js +10 -1
- package/dist/describe-config.d.ts +3 -2
- package/dist/discovery-health.js +26 -11
- package/dist/errors.d.ts +52 -3
- package/dist/errors.js +86 -3
- package/dist/execute.js +476 -200
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +2 -2
- package/dist/executors/quickjs-child.js +13 -4
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +36 -19
- package/dist/executors/quickjs.js +23 -6
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +7 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +9 -3
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -0
- package/dist/invocation.js +98 -34
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +259 -60
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +166 -51
- package/dist/node.js +3 -1
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +2 -1
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +1 -1
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +0 -13
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -0
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +7 -2
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +1 -0
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +2 -0
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +11 -5
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/index.js +2 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +2 -2
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +1 -0
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +2 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +127 -27
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +13 -4
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +55 -26
- package/dist/routes/shared.d.ts +6 -0
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/request-scope.js +36 -1
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skills.js +100 -33
- package/dist/storage/keys.js +9 -6
- package/dist/storage/sql-native.js +1 -1
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +31 -0
- package/dist/usage-guide.js +14 -13
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +28 -8
- package/documentation/architecture.md +104 -42
- package/documentation/auth.md +46 -19
- package/documentation/code-mode.md +378 -116
- package/documentation/deploying.md +10 -9
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +655 -153
- package/documentation/operating.md +5 -3
- package/documentation/providers.md +18 -6
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +1 -1
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/native-recovery.js
CHANGED
|
@@ -20,11 +20,14 @@ export function nativeRecovery(result, wireName) {
|
|
|
20
20
|
: { operator: "Check connector authentication in deployment configuration." };
|
|
21
21
|
}
|
|
22
22
|
else if (action?.tool === "call_tool" || action?.tool === "call_destructive_tool") {
|
|
23
|
+
const purpose = error.code === "destructive_tool_requires_approval"
|
|
24
|
+
? "Nothing was sent. Refresh the host's tool list, then call this tool only if the write is still wanted; the host asks for approval."
|
|
25
|
+
: (action.purpose ?? "Retry the original native call.");
|
|
23
26
|
error.nextAction = wireName
|
|
24
27
|
? {
|
|
25
28
|
tool: wireName,
|
|
26
29
|
arguments: action.arguments?.args ?? {},
|
|
27
|
-
purpose: `${
|
|
30
|
+
purpose: `${purpose}${action.arguments?.argsRedacted ? " Resend the original arguments; sensitive fields were omitted." : ""}`,
|
|
28
31
|
}
|
|
29
32
|
: { host: "search", purpose: action.purpose };
|
|
30
33
|
}
|
package/dist/native-tools.js
CHANGED
|
@@ -12,6 +12,7 @@ import { withAuthorizationHandoff } from "./authorization-handoff.js";
|
|
|
12
12
|
import { classifyCallError } from "./errors.js";
|
|
13
13
|
import { nativeRecovery } from "./native-recovery.js";
|
|
14
14
|
import { CatalogService } from "./catalog-service.js";
|
|
15
|
+
import { catalogStaleness } from "./catalog-freshness.js";
|
|
15
16
|
import { jsonResult } from "./meta-tools.js";
|
|
16
17
|
import { failureRecord, logFailure } from "./operator-record.js";
|
|
17
18
|
/**
|
|
@@ -24,8 +25,19 @@ export const NATIVE_NAME_MAX = 48;
|
|
|
24
25
|
const MAX_NATIVE_TOOLS = 1_000;
|
|
25
26
|
/** Native `tools/list` cache hint. Shorter than the meta surface's hour: the list follows downstream catalogs. */
|
|
26
27
|
export const NATIVE_TOOLS_LIST_TTL_MS = 300_000;
|
|
28
|
+
/** The hint when a listing omits a connector or lists one stale: about when that connector is next asked. */
|
|
29
|
+
export const NATIVE_DEGRADED_TOOLS_LIST_TTL_MS = 60_000;
|
|
27
30
|
const HASH_CHARS = 6;
|
|
28
31
|
const SEPARATOR = "__";
|
|
32
|
+
/**
|
|
33
|
+
* What every write's name carries. It sits before the tool, because Eve displays
|
|
34
|
+
* a name's last `__` segment and a suffix there would show every write as "write".
|
|
35
|
+
*/
|
|
36
|
+
const WRITE_SEGMENT = `${SEPARATOR}WRITE${SEPARATOR}`;
|
|
37
|
+
/** How every hashed read ends, and so what no write's name may end in. See {@link nativeToolName}. */
|
|
38
|
+
const READ_HASH_TAIL = /_[0-9a-f]{6}$/;
|
|
39
|
+
/** A hashed write keeps this much of its connector, leaving the rest of the budget to its tool. */
|
|
40
|
+
const WRITE_CONNECTOR_HEAD = 24;
|
|
29
41
|
/** No `__`, no leading `_`: the last `__` of a plain name is then always its separator. */
|
|
30
42
|
const PLAIN_TOOL = /^[A-Za-z0-9-](?:[A-Za-z0-9-]|_(?!_))*$/;
|
|
31
43
|
const NATIVE_NAME = new RegExp(`^[A-Za-z][A-Za-z0-9_-]{0,${NATIVE_NAME_MAX - 1}}$`);
|
|
@@ -49,28 +61,82 @@ const shortHashNative = /* @__PURE__ */ registerAuth(shortHash, function (text)
|
|
|
49
61
|
function connectorPart(connectorId) {
|
|
50
62
|
return /^[a-z]/.test(connectorId) ? connectorId : `C${connectorId}`;
|
|
51
63
|
}
|
|
64
|
+
/** The classification a wire name is bound to. A name never changes what it is bound to. */
|
|
65
|
+
function boundClassification(name) {
|
|
66
|
+
return name.includes(WRITE_SEGMENT) && !READ_HASH_TAIL.test(name) ? "write" : "read";
|
|
67
|
+
}
|
|
52
68
|
/**
|
|
53
|
-
* A pure function of the connector id
|
|
54
|
-
* stable across catalog refreshes
|
|
55
|
-
*
|
|
56
|
-
* original pair.
|
|
69
|
+
* A pure function of the connector id, downstream tool name and classification,
|
|
70
|
+
* so it is stable across catalog refreshes until the classification changes. A
|
|
71
|
+
* read is `<connector>__<tool>` when that is exact, legal and short, otherwise its
|
|
72
|
+
* head plus `_` and a six-hex SHA-256 of the original pair. A write is
|
|
73
|
+
* `<connector>__WRITE__<tool>` under the same conditions and when that does not end
|
|
74
|
+
* like a hashed read, otherwise a connector head, the marker, a tool head, `_w` and
|
|
75
|
+
* the same hash.
|
|
76
|
+
*
|
|
77
|
+
* No read's name is ever a write's, including a name a host cached before writes
|
|
78
|
+
* had their own, because a read's name has not changed. A read can carry the marker
|
|
79
|
+
* only when hashed: connector ids have no uppercase, so `WRITE` and the `__` after
|
|
80
|
+
* it would fall inside the tool, and a plain tool has no `__`. Every hashed read
|
|
81
|
+
* ends in `_` and six hex digits. Every write carries the marker and never ends so.
|
|
57
82
|
*/
|
|
58
|
-
export function nativeToolName(connectorId, toolName) {
|
|
59
|
-
return runEdge(nativeToolNameNative(connectorId, toolName));
|
|
83
|
+
export function nativeToolName(connectorId, toolName, classification) {
|
|
84
|
+
return runEdge(nativeToolNameNative(connectorId, toolName, classification));
|
|
60
85
|
}
|
|
61
|
-
const nativeToolNameNative = /* @__PURE__ */ registerAuth(nativeToolName, function (connectorId, toolName) {
|
|
86
|
+
const nativeToolNameNative = /* @__PURE__ */ registerAuth(nativeToolName, function (connectorId, toolName, classification) {
|
|
62
87
|
return authFlow(function* () {
|
|
88
|
+
const connector = connectorPart(connectorId);
|
|
63
89
|
const tool = toolName.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
64
|
-
const
|
|
65
|
-
|
|
90
|
+
const write = classification === "write";
|
|
91
|
+
const plain = `${connector}${write ? WRITE_SEGMENT : SEPARATOR}${tool}`;
|
|
92
|
+
if (tool === toolName &&
|
|
93
|
+
PLAIN_TOOL.test(tool) &&
|
|
94
|
+
plain.length <= NATIVE_NAME_MAX &&
|
|
95
|
+
!(write && READ_HASH_TAIL.test(plain)))
|
|
66
96
|
return plain;
|
|
67
|
-
const
|
|
68
|
-
|
|
97
|
+
const hash = yield* upwardStep(authCall(shortHash, [`${connectorId}.${toolName}`]));
|
|
98
|
+
if (write) {
|
|
99
|
+
const owner = connector.slice(0, WRITE_CONNECTOR_HEAD);
|
|
100
|
+
const suffix = `_w${hash}`;
|
|
101
|
+
const room = NATIVE_NAME_MAX - suffix.length - owner.length - WRITE_SEGMENT.length;
|
|
102
|
+
return `${owner}${WRITE_SEGMENT}${tool.slice(0, room).replace(/_+$/, "")}${suffix}`;
|
|
103
|
+
}
|
|
104
|
+
return `${plain.slice(0, NATIVE_NAME_MAX - HASH_CHARS - 1).replace(/_+$/, "")}_${hash}`;
|
|
69
105
|
});
|
|
70
106
|
});
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Whether any tool of a connector could hold a name: every shape in
|
|
109
|
+
* {@link nativeToolName}, including truncated connector prefixes.
|
|
110
|
+
*/
|
|
111
|
+
function couldClaim(connector, name) {
|
|
112
|
+
const part = connectorPart(connector.id);
|
|
113
|
+
const prefix = `${part}${SEPARATOR}`;
|
|
114
|
+
// A hashed name can truncate even the connector prefix, and strips trailing
|
|
115
|
+
// underscores from its head. Such a name is exactly `${head}_` + six hex digits.
|
|
116
|
+
const head = prefix.slice(0, NATIVE_NAME_MAX - HASH_CHARS - 1).replace(/_+$/, "");
|
|
117
|
+
return (name.startsWith(prefix) ||
|
|
118
|
+
// A hashed write keeps only the head of a long connector id.
|
|
119
|
+
name.startsWith(`${part.slice(0, WRITE_CONNECTOR_HEAD)}${WRITE_SEGMENT}`) ||
|
|
120
|
+
(name.length === head.length + 1 + HASH_CHARS &&
|
|
121
|
+
name.startsWith(`${head}_`) &&
|
|
122
|
+
/^[0-9a-f]+$/.test(name.slice(-HASH_CHARS))));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The name recovery should give: what the refusing verdict selects, because a
|
|
126
|
+
* catalog can change between resolving a name and invoking it.
|
|
127
|
+
*/
|
|
128
|
+
function recoveryName(result, called, names) {
|
|
129
|
+
const error = result?.structuredContent?.error;
|
|
130
|
+
const route = error?.code === "destructive_tool_requires_approval" ? "call_destructive_tool" : error?.nextAction?.tool;
|
|
131
|
+
return route === "call_destructive_tool" ? names.write : route === "call_tool" ? names.read : called;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read or write, from the served classification only. A stale listing cannot
|
|
135
|
+
* vouch for a read: the connector may have reclassified the tool since, so it
|
|
136
|
+
* fails closed (INV-1). The call's own verdict comes from a live catalog.
|
|
137
|
+
*/
|
|
138
|
+
function annotationsFor(tool, stale) {
|
|
139
|
+
return tool.classification === "read" && !stale
|
|
74
140
|
? { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
75
141
|
: { readOnlyHint: false, destructiveHint: true, openWorldHint: true };
|
|
76
142
|
}
|
|
@@ -160,10 +226,13 @@ export class NativeSurface {
|
|
|
160
226
|
/**
|
|
161
227
|
* Every listable native tool. A connector whose catalog fails is omitted
|
|
162
228
|
* whole, never partially, with an operator warning; two tools claiming one
|
|
163
|
-
* name are both omitted and stay reachable by address in execute_code.
|
|
229
|
+
* name are both omitted and stay reachable by address in execute_code. A tool
|
|
230
|
+
* claims its read and write names alike, whichever it is now, so a collision
|
|
231
|
+
* does not come and go with a classification and a name cached under the other
|
|
232
|
+
* classification still finds its tool.
|
|
164
233
|
*/
|
|
165
|
-
claims(connectors = this.registry.listConnectors()) {
|
|
166
|
-
return runEdge(authOwnMethod(NativeSurface.prototype, this, "claims", [connectors]));
|
|
234
|
+
claims(connectors = this.registry.listConnectors(), allowStale = false) {
|
|
235
|
+
return runEdge(authOwnMethod(NativeSurface.prototype, this, "claims", [connectors, allowStale]));
|
|
167
236
|
}
|
|
168
237
|
list() {
|
|
169
238
|
return runEdge(authOwnMethod(NativeSurface.prototype, this, "list", []));
|
|
@@ -176,7 +245,11 @@ export class NativeSurface {
|
|
|
176
245
|
resolve(name) {
|
|
177
246
|
return runEdge(authOwnMethod(NativeSurface.prototype, this, "resolve", [name]));
|
|
178
247
|
}
|
|
179
|
-
/**
|
|
248
|
+
/**
|
|
249
|
+
* A native call: the existing route the name is bound to, never one chosen from
|
|
250
|
+
* the current verdict. A read name takes the read route, which refuses a current
|
|
251
|
+
* write before dispatch, so a host's replay of a cached read cannot reach a write.
|
|
252
|
+
*/
|
|
180
253
|
call(name, args, request) {
|
|
181
254
|
return runEdge(authOwnMethod(NativeSurface.prototype, this, "call", [name, args, request]));
|
|
182
255
|
}
|
|
@@ -185,11 +258,25 @@ export class NativeSurface {
|
|
|
185
258
|
...(entry?.tool.title ? { title: entry.tool.title } : {}),
|
|
186
259
|
...(entry ? { description: describe(entry, this.options.executeCode, guideName) } : {}),
|
|
187
260
|
inputSchema: passThroughSchema(entry?.tool.inputSchema),
|
|
188
|
-
...(entry ? { annotations: annotationsFor(entry.tool) } : {}),
|
|
189
|
-
...(entry
|
|
261
|
+
...(entry ? { annotations: annotationsFor(entry.tool, entry.stale !== undefined) } : {}),
|
|
262
|
+
...(entry
|
|
263
|
+
? {
|
|
264
|
+
_meta: {
|
|
265
|
+
"dev.connecta/address": `${entry.connector.id}.${entry.tool.name}`,
|
|
266
|
+
...(entry.stale
|
|
267
|
+
? {
|
|
268
|
+
"dev.connecta/stale": {
|
|
269
|
+
code: entry.stale.code,
|
|
270
|
+
ageMs: entry.stale.ageMs,
|
|
271
|
+
...(entry.stale.retryAfterMs === undefined ? {} : { retryAfterMs: entry.stale.retryAfterMs }),
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
: {}),
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
: {}),
|
|
190
278
|
}, authMethod((args, request) => Effect.gen({ self: this }, function* () {
|
|
191
|
-
|
|
192
|
-
return nativeRecovery(result, name);
|
|
279
|
+
return yield* authInvoke(this, "call", [name, args ?? {}, request]);
|
|
193
280
|
})));
|
|
194
281
|
}
|
|
195
282
|
/**
|
|
@@ -266,14 +353,15 @@ export class NativeSurface {
|
|
|
266
353
|
error: yield* upwardStep(authCall(withAuthorizationHandoff, [self.options.requestScope, error])),
|
|
267
354
|
});
|
|
268
355
|
result.isError = true;
|
|
269
|
-
return result;
|
|
356
|
+
return nativeRecovery(result, name);
|
|
270
357
|
}
|
|
271
358
|
const address = `${target.entry.connector.id}.${target.entry.tool.name}`;
|
|
272
|
-
|
|
273
|
-
|
|
359
|
+
const result = yield* upwardStep(authInvoke(self.options, "route", [
|
|
360
|
+
boundClassification(name) === "read" ? "call_tool" : "call_destructive_tool",
|
|
274
361
|
{ address, args },
|
|
275
362
|
request,
|
|
276
363
|
]));
|
|
364
|
+
return nativeRecovery(result, recoveryName(result, name, target.entry.names));
|
|
277
365
|
})).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
278
366
|
}, NativeSurface.prototype.call);
|
|
279
367
|
registerAuthMethod(NativeSurface.prototype, "resolve", function (name) {
|
|
@@ -282,19 +370,20 @@ export class NativeSurface {
|
|
|
282
370
|
requestScope: this.options.requestScope,
|
|
283
371
|
signal: this.options.requestSignal,
|
|
284
372
|
}, () => authFlow(function* () {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// A
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
373
|
+
const possible = (...names) => self.registry.listConnectors().filter((connector) => names.some((held) => couldClaim(connector, held)));
|
|
374
|
+
const prefixed = possible(name);
|
|
375
|
+
let { loaded, byName } = yield* upwardStep(authInvoke(self, "claims", [prefixed]));
|
|
376
|
+
let found = byName.get(name);
|
|
377
|
+
if (found) {
|
|
378
|
+
// A tool that loses either name loses both, as listing omits it, so the
|
|
379
|
+
// other name's claimants decide this one too. One step is enough: only a
|
|
380
|
+
// tool sharing one of these two names can take them from this tool.
|
|
381
|
+
const every = possible(found.names.read, found.names.write);
|
|
382
|
+
if (every.length > prefixed.length) {
|
|
383
|
+
({ loaded, byName } = yield* upwardStep(authInvoke(self, "claims", [every])));
|
|
384
|
+
found = byName.get(name);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
298
387
|
if (found === null)
|
|
299
388
|
return { kind: "ambiguous" };
|
|
300
389
|
// An unavailable possible claimant cannot prove uniqueness.
|
|
@@ -310,8 +399,10 @@ export class NativeSurface {
|
|
|
310
399
|
requestScope: this.options.requestScope,
|
|
311
400
|
signal: this.options.requestSignal,
|
|
312
401
|
}, () => authFlow(function* () {
|
|
313
|
-
const { loaded, byName } = yield* upwardStep(authInvoke(self, "claims", []));
|
|
402
|
+
const { loaded, byName } = yield* upwardStep(authInvoke(self, "claims", [undefined, true]));
|
|
314
403
|
for (const entry of loaded) {
|
|
404
|
+
if ("tools" in entry && catalogStaleness(entry.tools))
|
|
405
|
+
self.options.staleCatalogs.push(entry.connector.id);
|
|
315
406
|
// Validate the whole connector before registering any of its tools.
|
|
316
407
|
const tool = "tools" in entry
|
|
317
408
|
? entry.tools.find((tool) => tool.inputSchema !== undefined && !nativeInputSchema(tool.inputSchema))
|
|
@@ -321,32 +412,56 @@ export class NativeSurface {
|
|
|
321
412
|
self.options.catalogErrors.push(entry.connector.id);
|
|
322
413
|
logFailure(self.options.logger, "native catalog unavailable", failureRecord({ connector: entry.connector.id, ...(tool ? { tool } : {}) }, "error" in entry ? entry.error : undefined));
|
|
323
414
|
}
|
|
324
|
-
|
|
415
|
+
if (self.options.catalogErrors.length || self.options.staleCatalogs.length)
|
|
416
|
+
self.options.onDegradedListing?.();
|
|
417
|
+
return [...byName].flatMap(([name, entry]) => entry !== null &&
|
|
418
|
+
entry.name === name &&
|
|
325
419
|
!self.options.catalogErrors.includes(entry.connector.id) &&
|
|
326
|
-
(!(self.options.readOnly || entry.connector.readOnly) || entry.tool.classification === "read")
|
|
420
|
+
(!(self.options.readOnly || entry.connector.readOnly) || entry.tool.classification === "read")
|
|
421
|
+
? [entry]
|
|
422
|
+
: []);
|
|
327
423
|
})).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
328
424
|
}, NativeSurface.prototype.list);
|
|
329
|
-
registerAuthMethod(NativeSurface.prototype, "claims", function (connectors) {
|
|
425
|
+
registerAuthMethod(NativeSurface.prototype, "claims", function (connectors, allowStale = false) {
|
|
330
426
|
const self = this;
|
|
331
427
|
return duringViewEffect({
|
|
332
428
|
requestScope: this.options.requestScope,
|
|
333
429
|
signal: this.options.requestSignal,
|
|
334
430
|
}, () => authFlow(function* () {
|
|
335
|
-
const loaded = yield* upwardStep(authInvoke(self.catalog, "listCatalogs", [true, connectors ?? self.registry.listConnectors()]));
|
|
336
|
-
const
|
|
431
|
+
const loaded = yield* upwardStep(authInvoke(self.catalog, "listCatalogs", [true, connectors ?? self.registry.listConnectors(), allowStale]));
|
|
432
|
+
const claimed = [];
|
|
433
|
+
const claimants = new Map();
|
|
337
434
|
for (const entry of loaded) {
|
|
338
435
|
if (!("tools" in entry))
|
|
339
436
|
continue;
|
|
340
437
|
for (const tool of entry.tools) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
438
|
+
const read = yield* upwardStep(authCall(nativeToolName, [entry.connector.id, tool.name, "read"]));
|
|
439
|
+
const write = yield* upwardStep(authCall(nativeToolName, [entry.connector.id, tool.name, "write"]));
|
|
440
|
+
const name = tool.classification === "read" ? read : write;
|
|
441
|
+
const stale = catalogStaleness(entry.tools);
|
|
442
|
+
claimed.push({
|
|
443
|
+
names: [read, write],
|
|
444
|
+
claim: {
|
|
445
|
+
name,
|
|
446
|
+
names: { read, write },
|
|
447
|
+
connector: entry.connector,
|
|
448
|
+
tool,
|
|
449
|
+
...(stale ? { stale } : {}),
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
for (const held of [read, write])
|
|
453
|
+
claimants.set(held, (claimants.get(held) ?? 0) + 1);
|
|
348
454
|
}
|
|
349
455
|
}
|
|
456
|
+
const byName = new Map();
|
|
457
|
+
for (const { names, claim } of claimed) {
|
|
458
|
+
// A tool that loses either name loses both: it is omitted, not half listed.
|
|
459
|
+
const collides = names.some((name) => claimants.get(name) > 1);
|
|
460
|
+
if (collides)
|
|
461
|
+
logFailure(self.options.logger, "native name collision", failureRecord({ connector: claim.connector.id, tool: claim.tool }));
|
|
462
|
+
for (const name of names)
|
|
463
|
+
byName.set(name, collides ? null : claim);
|
|
464
|
+
}
|
|
350
465
|
return { loaded, byName };
|
|
351
466
|
})).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
352
467
|
}, NativeSurface.prototype.claims);
|
package/dist/node.js
CHANGED
|
@@ -114,6 +114,8 @@ export function listen(connecta, portOrOptions) {
|
|
|
114
114
|
const activeResponses = new Map();
|
|
115
115
|
let shuttingDown = false;
|
|
116
116
|
const server = createServer((req, res) => {
|
|
117
|
+
// Before the body: a client's timer has been running through its upload too.
|
|
118
|
+
const requestArrivedAt = Date.now();
|
|
117
119
|
awaitingRequest.delete(req.socket);
|
|
118
120
|
const socket = req.socket;
|
|
119
121
|
activeResponses.set(socket, (activeResponses.get(socket) ?? 0) + 1);
|
|
@@ -196,7 +198,7 @@ export function listen(connecta, portOrOptions) {
|
|
|
196
198
|
res.end("Payload Too Large");
|
|
197
199
|
return;
|
|
198
200
|
}
|
|
199
|
-
const response = await connecta.fetch(request, undefined, ctx);
|
|
201
|
+
const response = await connecta.fetch(request, undefined, { ...ctx, requestArrivedAt });
|
|
200
202
|
await writeResponse(res, response);
|
|
201
203
|
}
|
|
202
204
|
catch (error) {
|
package/dist/oauth-handoff.js
CHANGED
|
@@ -4,6 +4,9 @@ import { authCall, authInvoke, authFlow, authStorage, registerAuth } from "./aut
|
|
|
4
4
|
import { runEdge } from "./runtime/run.js";
|
|
5
5
|
import { oauthConnectKeys } from "./storage/keys.js";
|
|
6
6
|
const HANDOFF_TTL_MS = 15 * 60_000;
|
|
7
|
+
// A flow can issue a link until its ten-minute retry state ends, and that link lives
|
|
8
|
+
// fifteen more, so a reset's record outlives every link that could repeat it.
|
|
9
|
+
const FLOW_RESET_EXPIRY = { ttlSeconds: 30 * 60 };
|
|
7
10
|
const INTERACTIVE_OAUTH_REQUIRED = "An interactive provider (Clerk or Cloudflare Access) is required to connect OAuth connectors.";
|
|
8
11
|
// Length-prefixed fields avoid JSON keys and escaping without reserving any
|
|
9
12
|
// character in an identity. This is an opaque wire encoding, not encryption.
|
|
@@ -16,7 +19,7 @@ function encodeHandoff(h) {
|
|
|
16
19
|
h.expiresAt.toString(36),
|
|
17
20
|
h.nonce,
|
|
18
21
|
h.force ? "1" : "0",
|
|
19
|
-
h.
|
|
22
|
+
h.flow ?? "",
|
|
20
23
|
]
|
|
21
24
|
.map((field) => `${field.length}:${field}`)
|
|
22
25
|
.join("");
|
|
@@ -36,7 +39,7 @@ function decodeHandoff(value) {
|
|
|
36
39
|
fields.push(value.slice(offset, offset + length));
|
|
37
40
|
offset += length;
|
|
38
41
|
}
|
|
39
|
-
const [connector, owner, principal, origin, expiry, nonce, force,
|
|
42
|
+
const [connector, owner, principal, origin, expiry, nonce, force, flow] = fields;
|
|
40
43
|
if (fields.length !== 8 || offset !== value.length || !/^[0-9a-z]+$/.test(expiry) || !/^[01]$/.test(force))
|
|
41
44
|
throw new Error("Invalid handoff fields");
|
|
42
45
|
return {
|
|
@@ -47,7 +50,7 @@ function decodeHandoff(value) {
|
|
|
47
50
|
expiresAt: parseInt(expiry, 36),
|
|
48
51
|
nonce: nonce,
|
|
49
52
|
force: force === "1",
|
|
50
|
-
...(
|
|
53
|
+
...(flow ? { flow } : {}),
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
function encodeSegment(value) {
|
|
@@ -89,10 +92,10 @@ const oauthConnectUrlNative = /* @__PURE__ */ registerAuth(oauthConnectUrl, func
|
|
|
89
92
|
]))).url;
|
|
90
93
|
});
|
|
91
94
|
});
|
|
92
|
-
export function oauthConnectLink(opts, baseUrl, connectorId, principal, force = false, opaque = false,
|
|
93
|
-
return runEdge(oauthConnectLinkNative(opts, baseUrl, connectorId, principal, force, opaque,
|
|
95
|
+
export function oauthConnectLink(opts, baseUrl, connectorId, principal, force = false, opaque = false, flow) {
|
|
96
|
+
return runEdge(oauthConnectLinkNative(opts, baseUrl, connectorId, principal, force, opaque, flow));
|
|
94
97
|
}
|
|
95
|
-
const oauthConnectLinkNative = /* @__PURE__ */ registerAuth(oauthConnectLink, function (opts, baseUrl, connectorId, principal, force = false, opaque = false,
|
|
98
|
+
const oauthConnectLinkNative = /* @__PURE__ */ registerAuth(oauthConnectLink, function (opts, baseUrl, connectorId, principal, force = false, opaque = false, flow) {
|
|
96
99
|
return authFlow(function* () {
|
|
97
100
|
const unavailable = oauthConnectUnavailable(opts);
|
|
98
101
|
if (unavailable)
|
|
@@ -108,7 +111,7 @@ const oauthConnectLinkNative = /* @__PURE__ */ registerAuth(oauthConnectLink, fu
|
|
|
108
111
|
expiresAt: Date.now() + HANDOFF_TTL_MS,
|
|
109
112
|
nonce: crypto.randomUUID(),
|
|
110
113
|
force,
|
|
111
|
-
...(
|
|
114
|
+
...(flow ? { flow } : {}),
|
|
112
115
|
};
|
|
113
116
|
const vault = opts.config.vault;
|
|
114
117
|
if (opaque && (!vault.seal || !vault.open))
|
|
@@ -126,32 +129,14 @@ const oauthConnectLinkNative = /* @__PURE__ */ registerAuth(oauthConnectLink, fu
|
|
|
126
129
|
});
|
|
127
130
|
});
|
|
128
131
|
/** A claim precedes OAuth reset; only a completed start can prove recovery. */
|
|
129
|
-
export function
|
|
130
|
-
return runEdge(
|
|
132
|
+
export function oauthConnectLinkStarted(opts, baseUrl, id, nonce) {
|
|
133
|
+
return runEdge(oauthConnectLinkStartedNative(opts, baseUrl, id, nonce));
|
|
131
134
|
}
|
|
132
|
-
const
|
|
135
|
+
const oauthConnectLinkStartedNative = /* @__PURE__ */ registerAuth(oauthConnectLinkStarted, function (opts, baseUrl, id, nonce) {
|
|
133
136
|
return authFlow(function* () {
|
|
134
|
-
|
|
137
|
+
return ((yield* upwardStep(authStorage(opts.registry.contextFor(id, baseUrl).storage, "get", [
|
|
135
138
|
oauthConnectKeys.used(nonce),
|
|
136
|
-
]));
|
|
137
|
-
return stage === "used" ? "claimed" : stage === "started" || stage === "failed" ? stage : undefined;
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
/** Make outstanding flow links single-use refusals before a completion check. */
|
|
141
|
-
export function closeOAuthConnectLinks(opts, baseUrl, id, nonces) {
|
|
142
|
-
return runEdge(closeOAuthConnectLinksNative(opts, baseUrl, id, nonces));
|
|
143
|
-
}
|
|
144
|
-
const closeOAuthConnectLinksNative = /* @__PURE__ */ registerAuth(closeOAuthConnectLinks, function (opts, baseUrl, id, nonces) {
|
|
145
|
-
return authFlow(function* () {
|
|
146
|
-
const storage = opts.registry.contextFor(id, baseUrl).storage;
|
|
147
|
-
for (const nonce of nonces) {
|
|
148
|
-
yield* upwardStep(authStorage(storage, "compareAndSet", [
|
|
149
|
-
oauthConnectKeys.used(nonce),
|
|
150
|
-
null,
|
|
151
|
-
"closed",
|
|
152
|
-
{ ttlSeconds: HANDOFF_TTL_MS / 1000 },
|
|
153
|
-
]));
|
|
154
|
-
}
|
|
139
|
+
]))) === "started");
|
|
155
140
|
});
|
|
156
141
|
});
|
|
157
142
|
/** Consuming a round prevents replay from creating sibling retry lineages. */
|
|
@@ -206,6 +191,9 @@ const verifyOAuthHandoffNative = /* @__PURE__ */ registerAuth(verifyOAuthHandoff
|
|
|
206
191
|
atob(payload.slice(3)),
|
|
207
192
|
]))
|
|
208
193
|
: atob(payload));
|
|
194
|
+
// A retired predecessor binding: nothing here can honor it, so it must not be ignored.
|
|
195
|
+
if ("after" in h)
|
|
196
|
+
return null;
|
|
209
197
|
}
|
|
210
198
|
const connector = opts.registry.getConnector(connectorId);
|
|
211
199
|
if (!connector ||
|
|
@@ -221,8 +209,8 @@ const verifyOAuthHandoffNative = /* @__PURE__ */ registerAuth(verifyOAuthHandoff
|
|
|
221
209
|
!h.nonce ||
|
|
222
210
|
typeof h.force !== "boolean")
|
|
223
211
|
return null;
|
|
224
|
-
if (h.
|
|
225
|
-
(typeof h.
|
|
212
|
+
if (h.flow !== undefined &&
|
|
213
|
+
(typeof h.flow !== "string" || !/^[a-f0-9-]{36}$/.test(h.flow) || h.flow === h.nonce || !h.force))
|
|
226
214
|
return null;
|
|
227
215
|
if (yield* upwardStep(authStorage(opts.registry.contextFor(connectorId, baseUrl).storage, "get", [
|
|
228
216
|
oauthConnectKeys.used(h.nonce),
|
|
@@ -235,21 +223,34 @@ const verifyOAuthHandoffNative = /* @__PURE__ */ registerAuth(verifyOAuthHandoff
|
|
|
235
223
|
}
|
|
236
224
|
});
|
|
237
225
|
});
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Claim only after browser identity and permissions pass, before starting OAuth.
|
|
228
|
+
* A forced flow resets once, whichever of its links arrives first. A later visitor
|
|
229
|
+
* continues that attempt, waits on it without spending its link, or, once it has
|
|
230
|
+
* failed, takes over the reset.
|
|
231
|
+
*/
|
|
232
|
+
export function claimOAuthConnectStart(opts, handoff) {
|
|
233
|
+
return runEdge(claimOAuthConnectStartNative(opts, handoff));
|
|
241
234
|
}
|
|
242
|
-
const
|
|
235
|
+
const claimOAuthConnectStartNative = /* @__PURE__ */ registerAuth(claimOAuthConnectStart, function (opts, handoff) {
|
|
243
236
|
return authFlow(function* () {
|
|
244
237
|
if (handoff.expiresAt <= Date.now())
|
|
245
|
-
return
|
|
238
|
+
return "refused";
|
|
246
239
|
const storage = opts.registry.contextFor(handoff.connector, handoff.origin).storage;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return
|
|
240
|
+
const reset = handoff.force ? oauthConnectKeys.reset(handoff.flow ?? handoff.nonce) : undefined;
|
|
241
|
+
const prior = reset ? yield* upwardStep(authStorage(storage, "get", [reset])) : "started";
|
|
242
|
+
if (prior === "used")
|
|
243
|
+
return "pending";
|
|
244
|
+
const claiming = prior !== "started";
|
|
245
|
+
if (claiming &&
|
|
246
|
+
!(yield* upwardStep(authStorage(storage, "compareAndSet", [reset, prior, "used", FLOW_RESET_EXPIRY]))))
|
|
247
|
+
return "pending";
|
|
251
248
|
const expiry = { ttlSeconds: Math.max(1, Math.ceil((handoff.expiresAt - Date.now()) / 1000)) };
|
|
252
|
-
|
|
249
|
+
if (yield* upwardStep(authStorage(storage, "compareAndSet", [oauthConnectKeys.used(handoff.nonce), null, "used", expiry])))
|
|
250
|
+
return claiming ? "reset" : "continue";
|
|
251
|
+
if (claiming)
|
|
252
|
+
yield* upwardStep(authStorage(storage, "compareAndSet", [reset, "used", prior, FLOW_RESET_EXPIRY]));
|
|
253
|
+
return "refused";
|
|
253
254
|
});
|
|
254
255
|
});
|
|
255
256
|
/** Terminal marker only after the start and every tracked reset have settled. */
|
|
@@ -258,9 +259,18 @@ export function finishOAuthConnectStart(opts, handoff, outcome) {
|
|
|
258
259
|
}
|
|
259
260
|
const finishOAuthConnectStartNative = /* @__PURE__ */ registerAuth(finishOAuthConnectStart, function (opts, handoff, outcome) {
|
|
260
261
|
return authFlow(function* () {
|
|
262
|
+
const storage = opts.registry.contextFor(handoff.connector, handoff.origin).storage;
|
|
263
|
+
// Settled first, and whatever the link's expiry: a finished link proves its flow's
|
|
264
|
+
// reset finished, and only the visitor who claimed a reset can find it still claimed.
|
|
265
|
+
if (handoff.force)
|
|
266
|
+
yield* upwardStep(authStorage(storage, "compareAndSet", [
|
|
267
|
+
oauthConnectKeys.reset(handoff.flow ?? handoff.nonce),
|
|
268
|
+
"used",
|
|
269
|
+
outcome,
|
|
270
|
+
FLOW_RESET_EXPIRY,
|
|
271
|
+
]));
|
|
261
272
|
if (handoff.expiresAt <= Date.now())
|
|
262
273
|
return false;
|
|
263
|
-
const storage = opts.registry.contextFor(handoff.connector, handoff.origin).storage;
|
|
264
274
|
const expiry = { ttlSeconds: Math.max(1, Math.ceil((handoff.expiresAt - Date.now()) / 1000)) };
|
|
265
275
|
return yield* upwardStep(authStorage(storage, "compareAndSet", [oauthConnectKeys.used(handoff.nonce), "used", outcome, expiry]));
|
|
266
276
|
});
|
|
@@ -5,7 +5,7 @@ import { ExecutorAdmissionError } from "./executor-admission.js";
|
|
|
5
5
|
import { DiscoveryPolicyError } from "./catalog-service.js";
|
|
6
6
|
import { classificationCode, classifyCallError } from "./errors.js";
|
|
7
7
|
import { usesCallerIdentity } from "./connector-caller.js";
|
|
8
|
-
export function operationActivity(context, registry, name, work, counts) {
|
|
8
|
+
export function operationActivity(context, registry, name, work, counts, source) {
|
|
9
9
|
if (!context?.recordOperation)
|
|
10
10
|
return work;
|
|
11
11
|
return Effect.suspend(() => {
|
|
@@ -53,6 +53,7 @@ export function operationActivity(context, registry, name, work, counts) {
|
|
|
53
53
|
personal,
|
|
54
54
|
...(counts ? { hostCalls: counts() } : {}),
|
|
55
55
|
},
|
|
56
|
+
...(source ? { source } : {}),
|
|
56
57
|
durationMs: performance.now() - started,
|
|
57
58
|
resultBytes,
|
|
58
59
|
outcome: !failed
|
|
@@ -193,6 +193,12 @@ export function registerConnectorVocabulary(connector, declarations) {
|
|
|
193
193
|
if (entries.length)
|
|
194
194
|
connectors.set(connector, new Map(entries));
|
|
195
195
|
}
|
|
196
|
+
/** Give a connector that delegates to an api() connector the vocabulary its hatch registered there. */
|
|
197
|
+
export function carryConnectorVocabulary(source, target) {
|
|
198
|
+
const registered = connectors.get(source);
|
|
199
|
+
if (registered)
|
|
200
|
+
connectors.set(target, registered);
|
|
201
|
+
}
|
|
196
202
|
/**
|
|
197
203
|
* Read tools whose operation vocabulary matches every query word, and the
|
|
198
204
|
* fully determined reads among those operations, each through a visible read
|
|
@@ -118,7 +118,7 @@ export declare function failureRecord(subject: FailureSubject, failure?: unknown
|
|
|
118
118
|
* The events an operator log reports failures as. Closed: a new one is a
|
|
119
119
|
* reviewed line here, never a string assembled at the call site.
|
|
120
120
|
*/
|
|
121
|
-
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";
|
|
121
|
+
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 chunk cleanup failed" | "catalog fence failed" | "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";
|
|
122
122
|
/**
|
|
123
123
|
* Log one failure as `[connecta] <event>` and its record. A record
|
|
124
124
|
* `failureRecord` did not build is replaced, whatever its shape, by a fixed
|