@zackbart/connecta 0.40.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 +148 -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 +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- 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 +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- 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 +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- 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 +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -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 +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- 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 +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- 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/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- 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 +10 -8
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
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
|
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The root graph calls the optional recorder, never the /activity implementation.
|
|
2
|
+
import { Cause, Effect, Exit } from "effect";
|
|
3
|
+
import { InvocationFailure } from "./invocation-failure.js";
|
|
4
|
+
import { ExecutorAdmissionError } from "./executor-admission.js";
|
|
5
|
+
import { DiscoveryPolicyError } from "./catalog-service.js";
|
|
6
|
+
import { classificationCode, classifyCallError } from "./errors.js";
|
|
7
|
+
import { usesCallerIdentity } from "./connector-caller.js";
|
|
8
|
+
export function operationActivity(context, registry, name, work, counts, source) {
|
|
9
|
+
if (!context?.recordOperation)
|
|
10
|
+
return work;
|
|
11
|
+
return Effect.suspend(() => {
|
|
12
|
+
const started = performance.now();
|
|
13
|
+
const connectors = registry.listConnectors();
|
|
14
|
+
// Human operations stay owner-only, including discovery of private catalogs.
|
|
15
|
+
let personal = Boolean(context.principalActor);
|
|
16
|
+
try {
|
|
17
|
+
personal ||= connectors.some((connector) => connector.authScope === "personal" ||
|
|
18
|
+
!connector.staticTools ||
|
|
19
|
+
usesCallerIdentity(connector) ||
|
|
20
|
+
connector.describe?.().auth?.mode === "request");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
personal = true;
|
|
24
|
+
}
|
|
25
|
+
return work.pipe(Effect.onExit((exit) => Effect.sync(() => {
|
|
26
|
+
const result = Exit.isSuccess(exit)
|
|
27
|
+
? exit.value
|
|
28
|
+
: undefined;
|
|
29
|
+
const failed = Exit.isFailure(exit) || result?.isError === true;
|
|
30
|
+
const interrupted = Exit.isFailure(exit) && exit.cause.reasons.some(Cause.isInterruptReason);
|
|
31
|
+
const failure = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined;
|
|
32
|
+
const errorCode = interrupted
|
|
33
|
+
? "cancelled"
|
|
34
|
+
: classificationCode(Exit.isFailure(exit)
|
|
35
|
+
? failure instanceof InvocationFailure ||
|
|
36
|
+
failure instanceof ExecutorAdmissionError ||
|
|
37
|
+
failure instanceof DiscoveryPolicyError
|
|
38
|
+
? failure.code
|
|
39
|
+
: classifyCallError(failure).code
|
|
40
|
+
: result?.structuredContent?.error?.code);
|
|
41
|
+
try {
|
|
42
|
+
// Result pages already measure their payload. Other operations have no
|
|
43
|
+
// reusable measurement: never serialize discovery or media for telemetry.
|
|
44
|
+
const page = name === "result" && Exit.isSuccess(exit)
|
|
45
|
+
? exit.value
|
|
46
|
+
: undefined;
|
|
47
|
+
const bytes = page?.structuredContent?.bytes ?? page?.bytes;
|
|
48
|
+
const resultBytes = typeof bytes === "number" && Number.isSafeInteger(bytes) && bytes >= 0 ? bytes : undefined;
|
|
49
|
+
context.recordOperation(context, {
|
|
50
|
+
operation: {
|
|
51
|
+
name,
|
|
52
|
+
connectors: connectors.map((connector) => connector.id),
|
|
53
|
+
personal,
|
|
54
|
+
...(counts ? { hostCalls: counts() } : {}),
|
|
55
|
+
},
|
|
56
|
+
...(source ? { source } : {}),
|
|
57
|
+
durationMs: performance.now() - started,
|
|
58
|
+
resultBytes,
|
|
59
|
+
outcome: !failed
|
|
60
|
+
? "success"
|
|
61
|
+
: errorCode === "timeout"
|
|
62
|
+
? "timeout"
|
|
63
|
+
: errorCode === "cancelled" || errorCode === "executor_cancelled"
|
|
64
|
+
? "cancelled"
|
|
65
|
+
: "error",
|
|
66
|
+
errorCode,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* Optional recording cannot replace the operation's result. */
|
|
71
|
+
}
|
|
72
|
+
})));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const OPERATION_NAMES = new Set(["search", "describe", "result", "skill", "program"]);
|
|
76
|
+
export function checkedActivityOperation(value) {
|
|
77
|
+
if (!value || typeof value !== "object")
|
|
78
|
+
return undefined;
|
|
79
|
+
const input = value;
|
|
80
|
+
if (!OPERATION_NAMES.has(input.name) ||
|
|
81
|
+
typeof input.personal !== "boolean" ||
|
|
82
|
+
!Array.isArray(input.connectors) ||
|
|
83
|
+
input.connectors.some((id) => typeof id !== "string" || !/^[a-zA-Z0-9_-]+$/.test(id)))
|
|
84
|
+
return undefined;
|
|
85
|
+
const counts = input.hostCalls;
|
|
86
|
+
if (counts &&
|
|
87
|
+
[counts.attempted, counts.admitted, counts.succeeded, counts.failed].some((n) => typeof n !== "number" || !Number.isSafeInteger(n) || n < 0))
|
|
88
|
+
return undefined;
|
|
89
|
+
return {
|
|
90
|
+
name: input.name,
|
|
91
|
+
connectors: [...new Set(input.connectors)],
|
|
92
|
+
personal: input.personal,
|
|
93
|
+
...(counts
|
|
94
|
+
? {
|
|
95
|
+
hostCalls: {
|
|
96
|
+
attempted: counts.attempted,
|
|
97
|
+
admitted: counts.admitted,
|
|
98
|
+
succeeded: counts.succeeded,
|
|
99
|
+
failed: counts.failed,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -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
|
|
@@ -23,8 +23,15 @@ export declare function labelErrorClass(ctor: {
|
|
|
23
23
|
* name is exactly one of those, else `DOMException`.
|
|
24
24
|
*/
|
|
25
25
|
export declare function errorLabel(failure: unknown): string | undefined;
|
|
26
|
+
interface IdentityLookupFact {
|
|
27
|
+
outcome: "timeout" | "rejected";
|
|
28
|
+
durationMs: number;
|
|
29
|
+
cold: boolean;
|
|
30
|
+
inFlight: number;
|
|
31
|
+
}
|
|
26
32
|
/** Facts connecta derived about a failure where it saw it. */
|
|
27
33
|
export interface FailureFacts {
|
|
34
|
+
identityLookup?: IdentityLookupFact;
|
|
28
35
|
/** Inbound refusal classification attached to its Response, never its body. */
|
|
29
36
|
reason?: AuthDenialReason;
|
|
30
37
|
step?: FailureStep;
|
|
@@ -64,6 +71,7 @@ export interface FailureSubject {
|
|
|
64
71
|
declare const recordBrand: unique symbol;
|
|
65
72
|
/** One failure as an operator may read it. Only `failureRecord` makes one. */
|
|
66
73
|
export interface FailureRecord {
|
|
74
|
+
readonly identityLookup?: IdentityLookupFact;
|
|
67
75
|
readonly [recordBrand]: true;
|
|
68
76
|
readonly reason?: AuthDenialReason;
|
|
69
77
|
readonly accessContext?: boolean;
|
|
@@ -110,7 +118,7 @@ export declare function failureRecord(subject: FailureSubject, failure?: unknown
|
|
|
110
118
|
* The events an operator log reports failures as. Closed: a new one is a
|
|
111
119
|
* reviewed line here, never a string assembled at the call site.
|
|
112
120
|
*/
|
|
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";
|
|
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";
|
|
114
122
|
/**
|
|
115
123
|
* Log one failure as `[connecta] <event>` and its record. A record
|
|
116
124
|
* `failureRecord` did not build is replaced, whatever its shape, by a fixed
|
package/dist/operator-record.js
CHANGED
|
@@ -47,6 +47,7 @@ const SOURCES = new Set([
|
|
|
47
47
|
"call_destructive_tool",
|
|
48
48
|
"batch_call",
|
|
49
49
|
"execute_code",
|
|
50
|
+
"execute_read_code",
|
|
50
51
|
"resume_execution",
|
|
51
52
|
]);
|
|
52
53
|
const MODES = new Set(["continue", "restart"]);
|
|
@@ -165,6 +166,20 @@ export function errorLabel(failure) {
|
|
|
165
166
|
}
|
|
166
167
|
return undefined;
|
|
167
168
|
}
|
|
169
|
+
function identityLookupFact(value) {
|
|
170
|
+
if (!value ||
|
|
171
|
+
(value.outcome !== "timeout" && value.outcome !== "rejected") ||
|
|
172
|
+
count(value.durationMs, 86_400_000) === undefined ||
|
|
173
|
+
typeof value.cold !== "boolean" ||
|
|
174
|
+
count(value.inFlight, 1_000) === undefined)
|
|
175
|
+
return undefined;
|
|
176
|
+
return {
|
|
177
|
+
outcome: value.outcome,
|
|
178
|
+
durationMs: Math.round(value.durationMs),
|
|
179
|
+
cold: value.cold,
|
|
180
|
+
inFlight: Math.round(value.inFlight),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
168
183
|
function origin(value) {
|
|
169
184
|
if (typeof value !== "string")
|
|
170
185
|
return undefined;
|
|
@@ -190,6 +205,7 @@ function checkedFacts(facts) {
|
|
|
190
205
|
return {
|
|
191
206
|
...defined("reason", member(facts.reason, AUTH_DENIAL_REASONS)),
|
|
192
207
|
...defined("step", member(facts.step, STEPS)),
|
|
208
|
+
...defined("identityLookup", identityLookupFact(facts.identityLookup)),
|
|
193
209
|
...defined("origin", origin(facts.origin)),
|
|
194
210
|
...defined("httpStatus", httpStatus(facts.httpStatus)),
|
|
195
211
|
...defined("oauthError", member(facts.oauthError, OAUTH_ERROR_CODES)),
|