@zackbart/connecta 0.37.1 → 0.38.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 +31 -0
- package/README.md +2 -2
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth.js +19 -153
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/oauth-grant.js +11 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +2 -2
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-service.js +1 -1
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +41 -14
- package/dist/config.js +21 -71
- package/dist/connectors/api-connector.js +4 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/remote-mcp.js +10 -43
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/downstream-input.js +2 -2
- package/dist/execute.js +22 -13
- package/dist/executor-result.js +2 -1
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-runtime.js +5 -3
- package/dist/guest-types.d.ts +5 -5
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +2 -2
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +1 -1
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +1 -1
- package/dist/http/safety.js +1 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +1 -7
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/invocation-auth.js +2 -1
- package/dist/meta-tools.js +27 -27
- package/dist/native-tools.js +2 -1
- package/dist/oauth-handoff.js +2 -2
- package/dist/operation-vocabulary.js +1 -1
- package/dist/operator-record.js +1 -49
- package/dist/operator-ui/contract.d.ts +2 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +1 -3
- package/dist/providers/_shared/rest/value-safety.js +26 -28
- package/dist/providers/breeze/api.js +2 -7
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/tools.js +3 -4
- package/dist/providers/ccb/api.js +1 -6
- package/dist/providers/ccb/index.js +2 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/docs/tools.js +2 -10
- package/dist/providers/drive/tools.js +12 -25
- package/dist/providers/forms/tools.js +1 -9
- package/dist/providers/gmail/tools.js +5 -21
- package/dist/providers/overflow/api.js +1 -3
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +2 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/sheets/tools.js +1 -9
- package/dist/providers/slides/tools.js +4 -13
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/tithely/api.js +2 -7
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +1 -6
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/tools.js +2 -1
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +5 -7
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +1 -1
- package/dist/routes/mcp.js +4 -3
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +4 -3
- package/dist/routes/shared.d.ts +8 -8
- package/dist/routes/shared.js +9 -9
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +1 -1
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +8 -35
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +16 -125
- package/dist/storage/sql-native.js +0 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +1 -12
- package/dist/tool-error.js +4 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/documentation/README.md +6 -42
- package/documentation/architecture.md +86 -496
- package/documentation/auth.md +12 -25
- package/documentation/code-mode.md +90 -66
- package/documentation/deploying.md +19 -40
- package/documentation/integrating.md +4 -19
- package/documentation/meta-tools.md +6 -6
- package/documentation/migration-0.33.md +5 -5
- package/documentation/migration-0.34.md +2 -4
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +19 -119
- package/documentation/operator-ui.md +3 -4
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/examples/worker/AGENTS.md +4 -6
- package/examples/worker/README.md +20 -115
- package/guides/connecting-agents.md +7 -30
- package/guides/how-it-works.md +2 -2
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/d1.js
CHANGED
|
@@ -3,12 +3,8 @@
|
|
|
3
3
|
// One D1 database holds everything: `d1Storage(db)` is the deployment's
|
|
4
4
|
// `KVStorage`, and `d1ActivityStore(db)` its activity history. Both create
|
|
5
5
|
// their tables on first use, so binding the database is the whole setup.
|
|
6
|
-
// `copyKvToD1` moves a 0.28 deployment's Workers KV state into it once.
|
|
7
|
-
import { runEdge } from "./runtime/run.js";
|
|
8
6
|
import { d1Driver } from "./storage/d1-driver.js";
|
|
9
|
-
import { copyKvToD1Effect, markKvToD1LiveEffect } from "./storage/d1-copy.js";
|
|
10
7
|
import { sqlActivityStore, sqlStorage } from "./storage/sql.js";
|
|
11
|
-
import { keepErrorName } from "./redaction-identity.js";
|
|
12
8
|
/**
|
|
13
9
|
* `KVStorage` over a D1 database, with the atomic `compareAndSet` every
|
|
14
10
|
* subsystem relies on. The `connecta_kv` table is created on first use; a
|
|
@@ -25,33 +21,3 @@ export function d1Storage(db) {
|
|
|
25
21
|
export function d1ActivityStore(db, options) {
|
|
26
22
|
return sqlActivityStore(d1Driver(db), "d1", options);
|
|
27
23
|
}
|
|
28
|
-
/**
|
|
29
|
-
* A copy that stopped. The message is fixed wording that names a family at
|
|
30
|
-
* most, never a key or value, and the binding's own error is not attached:
|
|
31
|
-
* KV and D1 errors can quote either. Entries before `cursor` are copied;
|
|
32
|
-
* rerunning from it is safe while the maintenance window still holds.
|
|
33
|
-
*/
|
|
34
|
-
export class KvToD1CopyError extends Error {
|
|
35
|
-
cursor;
|
|
36
|
-
name = "KvToD1CopyError";
|
|
37
|
-
constructor(message,
|
|
38
|
-
/** Resume point; absent means start over, which is also safe. */
|
|
39
|
-
cursor) {
|
|
40
|
-
super(message);
|
|
41
|
-
this.cursor = cursor;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
keepErrorName(KvToD1CopyError, "KvToD1CopyError");
|
|
45
|
-
/** Mark cutover before reopening traffic. The marker stays in D1 permanently. */
|
|
46
|
-
export function markKvToD1Live(db, source) {
|
|
47
|
-
return runEdge(markKvToD1LiveEffect(db, source));
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Copy live KV entries verbatim while all writers and traffic are stopped.
|
|
51
|
-
* A live D1 row is kept unless its family is explicitly allowed to overwrite.
|
|
52
|
-
* `verify` reads hashes and absolute expiries without changing copied entries.
|
|
53
|
-
* Results contain counts only. Resume tokens are source-bound atomic claims.
|
|
54
|
-
*/
|
|
55
|
-
export function copyKvToD1(kv, db, options) {
|
|
56
|
-
return runEdge(copyKvToD1Effect(kv, db, options));
|
|
57
|
-
}
|
package/dist/downstream-input.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toolError } from "./tool-error.js";
|
|
1
2
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
2
3
|
import { Effect } from "effect";
|
|
3
4
|
import { upwardStep } from "./upward.js";
|
|
@@ -129,8 +130,7 @@ function failure(code, message, write = false) {
|
|
|
129
130
|
writeOutcome: "unknown",
|
|
130
131
|
}
|
|
131
132
|
: { code, message, retryable: false };
|
|
132
|
-
|
|
133
|
-
return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
|
|
133
|
+
return toolError(error, { ok: false });
|
|
134
134
|
}
|
|
135
135
|
/** MCP-only continuation coordination. No downstream prompt enters operator records. */
|
|
136
136
|
export class DownstreamElicitation {
|
package/dist/execute.js
CHANGED
|
@@ -175,13 +175,15 @@ export class EmitCollector {
|
|
|
175
175
|
maxBytes;
|
|
176
176
|
maxBlocks;
|
|
177
177
|
diagnostics;
|
|
178
|
+
maxResultChars;
|
|
178
179
|
blocks = [];
|
|
179
180
|
bytes = 0;
|
|
180
181
|
textChars = 0;
|
|
181
|
-
constructor(maxBytes, maxBlocks, diagnostics) {
|
|
182
|
+
constructor(maxBytes, maxBlocks, diagnostics, maxResultChars = MAX_EXECUTE_RESULT_CHARS) {
|
|
182
183
|
this.maxBytes = maxBytes;
|
|
183
184
|
this.maxBlocks = maxBlocks;
|
|
184
185
|
this.diagnostics = diagnostics;
|
|
186
|
+
this.maxResultChars = maxResultChars;
|
|
185
187
|
}
|
|
186
188
|
accept(raw) {
|
|
187
189
|
this.acceptNative([requireEmittedBlock(raw)]);
|
|
@@ -205,8 +207,8 @@ export class EmitCollector {
|
|
|
205
207
|
// the transport budget, just like images and audio.
|
|
206
208
|
if (block.type === "resource" && "text" in block.resource)
|
|
207
209
|
textChars += serialized.length;
|
|
208
|
-
if (textChars >
|
|
209
|
-
throw guestFailure("result_too_large",
|
|
210
|
+
if (textChars > this.maxResultChars - 512) {
|
|
211
|
+
throw guestFailure("result_too_large", `Emitted text shares the ${this.maxResultChars.toLocaleString("en-US")}-character program result limit. Reduce the text before emitting it.`);
|
|
210
212
|
}
|
|
211
213
|
count++;
|
|
212
214
|
bytes += size;
|
|
@@ -750,8 +752,10 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
750
752
|
}),
|
|
751
753
|
skill: (name, _options, utilitySignal) => Effect.tryPromise({
|
|
752
754
|
try: async () => {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
+
// Accept the top-level skills tool's { name } shape too: models copy it into programs.
|
|
756
|
+
const skillName = name && typeof name === "object" ? name.name : name;
|
|
757
|
+
if (typeof skillName !== "string")
|
|
758
|
+
throw guestFailure("invalid_args", 'Use connecta.skill("usage") or connecta.skill("connector:<id>") with an exact skill name; the skills tool lists names.');
|
|
755
759
|
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
756
760
|
requestScope,
|
|
757
761
|
requestSignal: utilitySignal ?? hostAccessSignal,
|
|
@@ -763,7 +767,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
763
767
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
764
768
|
defer: limits.defer,
|
|
765
769
|
});
|
|
766
|
-
return { name, format: "text", text: await skills.text(
|
|
770
|
+
return { name: skillName, format: "text", text: await skills.text(skillName) };
|
|
767
771
|
},
|
|
768
772
|
catch: (err) => err instanceof ConnectorCallError ? guestFailure(err.code, err.message, err.code === "unavailable") : err,
|
|
769
773
|
}),
|
|
@@ -1044,10 +1048,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1044
1048
|
logger,
|
|
1045
1049
|
};
|
|
1046
1050
|
const hostCallTimeoutMs = resolveBudget(config.hostCallTimeoutMs, EXECUTE_HOST_CALL_TIMEOUT_MS);
|
|
1051
|
+
const maxResultChars = resolveBudget(config.maxResultChars, MAX_EXECUTE_RESULT_CHARS);
|
|
1047
1052
|
const play = (program, diagnosticsRequested, callerSignal, requestScope, hostCalls, programWrites) => Effect.suspend(() => {
|
|
1048
1053
|
const sentSecrets = sentSecretsForRequest(requestScope);
|
|
1049
1054
|
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
1050
|
-
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
|
|
1055
|
+
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics, maxResultChars);
|
|
1051
1056
|
const resultDependencies = [];
|
|
1052
1057
|
const dispatchController = new AbortController();
|
|
1053
1058
|
const terminal = Deferred.makeUnsafe();
|
|
@@ -1137,15 +1142,17 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1137
1142
|
emitted,
|
|
1138
1143
|
diagnostics,
|
|
1139
1144
|
program,
|
|
1140
|
-
|
|
1145
|
+
// Emitted text shares the return cap; a follow-up page run gets all of it.
|
|
1146
|
+
guardResult: (value) => authCall(guardProgramReturn, [
|
|
1141
1147
|
value,
|
|
1142
1148
|
registry,
|
|
1143
1149
|
baseUrl,
|
|
1144
1150
|
[...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()],
|
|
1145
1151
|
sentSecrets,
|
|
1146
1152
|
logger,
|
|
1147
|
-
|
|
1153
|
+
maxResultChars - emitted.textChars,
|
|
1148
1154
|
programWrites.hasSucceeded,
|
|
1155
|
+
maxResultChars,
|
|
1149
1156
|
]),
|
|
1150
1157
|
};
|
|
1151
1158
|
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
@@ -1301,7 +1308,7 @@ function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
|
|
|
1301
1308
|
// error path so captured logs survive instead of a raw SDK 500.
|
|
1302
1309
|
let result;
|
|
1303
1310
|
try {
|
|
1304
|
-
result = yield* guardResult(outcome.result
|
|
1311
|
+
result = yield* guardResult(outcome.result);
|
|
1305
1312
|
}
|
|
1306
1313
|
catch (err) {
|
|
1307
1314
|
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
@@ -1381,7 +1388,7 @@ function connectorInventory(connectors) {
|
|
|
1381
1388
|
return `${prefix}${shown.join(", ")}.`;
|
|
1382
1389
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
1383
1390
|
}
|
|
1384
|
-
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : readOnly ? "One known read: call_tool. Everything else: execute_code. Read-only endpoint: search and describe omit write tools, and every write call is refused." : "One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. Writable endpoint: programs may read and write; the client approves."} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
|
|
1391
|
+
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly, native = false) => `${native ? `Call tools directly; combine work here using their program addresses. ${readOnly ? "Read-only endpoint: writes are omitted and refused." : "Writable endpoint: programs may read and write; the client approves."}` : readOnly ? "One known read: call_tool. Everything else: execute_code. Read-only endpoint: search and describe omit write tools, and every write call is refused." : "One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. Writable endpoint: programs may read and write; the client approves."} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.callTimeoutMs === hostLimits.hostCallTimeoutMs ? `${hostLimits.callTimeoutMs / 1_000}s/host call` : `${hostLimits.callTimeoutMs / 1_000}s/call (${hostLimits.hostCallTimeoutMs / 1_000}s result/skill/read)`}.
|
|
1385
1392
|
${connectorInventory(connectors)}
|
|
1386
1393
|
|
|
1387
1394
|
Use async () => { ... }, no arguments. No portable ambient capabilities. API:
|
|
@@ -1392,7 +1399,7 @@ ${GUEST_API_DECLARATION}
|
|
|
1392
1399
|
\`\`\`
|
|
1393
1400
|
Search: {tools}, catalogErrors. JSON schemas default; compact text. Calls: {data, format:"json"|"text"}; >1 MiB: paged handle. Oversized returns: resultId/nextAction. connecta.result pages; UTF-8 byte offsets. allSettled keeps typed errors; host budget_exceeded ends the run.
|
|
1394
1401
|
|
|
1395
|
-
Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks. Authored: text/base64 image/audio; no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap.
|
|
1402
|
+
Never repeat writes for output. emit(result) or emit(block) from result.content forwards MCP blocks. Authored: text/base64 image/audio; no await. Caps: ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes. Text shares result cap. connecta.skill("usage"): repair${connectorGuides ? ", guide handling" : ""}; connecta.skill("investigate"): workflow.`;
|
|
1396
1403
|
// Module scope, like the other five meta-tool inputs: its JSON Schema is
|
|
1397
1404
|
// derived once per process. Budgets and connectors vary by deployment and
|
|
1398
1405
|
// view, so they live in the description, never here.
|
|
@@ -1430,12 +1437,14 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1430
1437
|
defer: ctx.defer,
|
|
1431
1438
|
readOnly: ctx.readOnly,
|
|
1432
1439
|
maxWrites,
|
|
1440
|
+
maxResultChars: ctx.maxResultChars,
|
|
1433
1441
|
requestScope: ctx.requestScope,
|
|
1434
1442
|
});
|
|
1435
1443
|
server.registerTool("execute_code", {
|
|
1436
1444
|
description: executeDescription(emitBudgets, {
|
|
1437
1445
|
...hostLimits,
|
|
1438
|
-
|
|
1446
|
+
// connecta.call falls back to calls.defaultTimeoutMs; result, skill and read always use hostCallTimeoutMs.
|
|
1447
|
+
callTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1439
1448
|
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true, ctx.native === true),
|
|
1440
1449
|
inputSchema: EXECUTE_INPUT,
|
|
1441
1450
|
outputSchema: EXECUTE_OUTPUT,
|
package/dist/executor-result.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
1
2
|
import { msg } from "./errors.js";
|
|
2
3
|
/** ~6k tokens. Sandbox code should filter data down before returning. */
|
|
3
|
-
export const MAX_EXECUTE_RESULT_CHARS =
|
|
4
|
+
export const MAX_EXECUTE_RESULT_CHARS = CONFIG_DEFAULTS.execute.maxResultChars;
|
|
4
5
|
export const MAX_EXECUTE_LOG_CHARS = 4_000;
|
|
5
6
|
export function serializeResultText(value) {
|
|
6
7
|
const serialized = JSON.stringify(value);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { utf8Length } from "../bytes.js";
|
|
1
2
|
// QuickJS-in-WebAssembly runtime used inside the disposable child process.
|
|
2
3
|
//
|
|
3
4
|
// Model-written code runs inside a QuickJS interpreter compiled to WASM:
|
|
@@ -37,30 +38,6 @@ function logTransportBytes(entry) {
|
|
|
37
38
|
// encoded into ChildToParentMessage. Measure the units the IPC cap sees.
|
|
38
39
|
return serializedBytes(JSON.stringify(JSON.stringify(entry)));
|
|
39
40
|
}
|
|
40
|
-
function exceedsUtf8ByteLimit(value, limit) {
|
|
41
|
-
let bytes = 0;
|
|
42
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
43
|
-
const code = value.charCodeAt(index);
|
|
44
|
-
if (code <= 0x7f)
|
|
45
|
-
bytes += 1;
|
|
46
|
-
else if (code <= 0x7ff)
|
|
47
|
-
bytes += 2;
|
|
48
|
-
else if (code >= 0xd800 &&
|
|
49
|
-
code <= 0xdbff &&
|
|
50
|
-
index + 1 < value.length &&
|
|
51
|
-
value.charCodeAt(index + 1) >= 0xdc00 &&
|
|
52
|
-
value.charCodeAt(index + 1) <= 0xdfff) {
|
|
53
|
-
bytes += 4;
|
|
54
|
-
index += 1;
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
bytes += 3;
|
|
58
|
-
}
|
|
59
|
-
if (bytes > limit)
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
41
|
/** Normalize model output into an async-arrow expression: strip markdown fences, wrap bare bodies. */
|
|
65
42
|
export function normalizeCode(code) {
|
|
66
43
|
let c = code.trim();
|
|
@@ -216,7 +193,7 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
216
193
|
catch (err) {
|
|
217
194
|
throw new Error(`Host result from ${label} could not be serialized: ${msg(err)}`);
|
|
218
195
|
}
|
|
219
|
-
if (
|
|
196
|
+
if (utf8Length(json, MAX_HOST_RESULT_BYTES) > MAX_HOST_RESULT_BYTES) {
|
|
220
197
|
throw new Error(`Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`);
|
|
221
198
|
}
|
|
222
199
|
return { json };
|
|
@@ -119,7 +119,7 @@ function terminate(child) {
|
|
|
119
119
|
}
|
|
120
120
|
let providePoolProgram;
|
|
121
121
|
class QuickJsChildPool {
|
|
122
|
-
/** What `/health` and `connecta doctor` call this sandbox
|
|
122
|
+
/** What `/health` and `connecta doctor` call this sandbox. */
|
|
123
123
|
name = "QuickJS";
|
|
124
124
|
admission;
|
|
125
125
|
slots;
|
package/dist/guest-runtime.js
CHANGED
|
@@ -160,6 +160,8 @@ export function guestInitializer() {
|
|
|
160
160
|
read: provider.read,
|
|
161
161
|
result: provider.result,
|
|
162
162
|
skill: provider.skill,
|
|
163
|
+
// Undeclared alias: models write connecta.skills, the top-level tool's name.
|
|
164
|
+
skills: provider.skill,
|
|
163
165
|
emit(block) {
|
|
164
166
|
const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
|
|
165
167
|
return trackEmission(emit(ref || block));
|
|
@@ -224,10 +226,10 @@ export function programError(raw, code) {
|
|
|
224
226
|
/(?:const|let|var|function|\()\s*connecta\b/.test(code)) {
|
|
225
227
|
hint = "Do not shadow connecta. Use the host-provided connecta global.";
|
|
226
228
|
}
|
|
227
|
-
else if (/callTool|\bskills\b|connecta\.
|
|
228
|
-
(["TypeError", "ReferenceError"].includes(name) && /connecta\.
|
|
229
|
+
else if (/callTool|\bskills\b|connecta\.guide/.test(message) ||
|
|
230
|
+
(["TypeError", "ReferenceError"].includes(name) && /connecta\.guide\b/.test(code))) {
|
|
229
231
|
hint =
|
|
230
|
-
|
|
232
|
+
'Use connecta.call(address, args), connecta.search, connecta.describe, connecta.result, and connecta.skill("usage"). Services are addressed by connector.tool, never guest globals.';
|
|
231
233
|
}
|
|
232
234
|
return {
|
|
233
235
|
code: "program_error",
|
package/dist/guest-types.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export type GuestBlock = {
|
|
|
103
103
|
mimeType: string;
|
|
104
104
|
};
|
|
105
105
|
type SchemaFormat = "json" | "compact" | "typescript";
|
|
106
|
-
|
|
106
|
+
interface CatalogSearchArgs {
|
|
107
107
|
query?: string;
|
|
108
108
|
connector?: string;
|
|
109
109
|
safety?: "readOnly" | "approvalRequired" | "all";
|
|
@@ -113,7 +113,7 @@ export interface CatalogSearchArgs {
|
|
|
113
113
|
includeSchemas?: SchemaFormat;
|
|
114
114
|
includeSchemaKeys?: boolean;
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
interface CatalogDescribeArgs {
|
|
117
117
|
address?: unknown;
|
|
118
118
|
addresses?: unknown;
|
|
119
119
|
format?: SchemaFormat;
|
|
@@ -140,7 +140,7 @@ interface CatalogSearchFailure extends CatalogFailureDetail {
|
|
|
140
140
|
}>;
|
|
141
141
|
retry?: string;
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
interface CatalogSearchPage {
|
|
144
144
|
catalogErrors: CatalogSearchFailure[];
|
|
145
145
|
absence?: {
|
|
146
146
|
service: string;
|
|
@@ -170,14 +170,14 @@ export interface CatalogSearchPage {
|
|
|
170
170
|
guidance?: string;
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
type CatalogSearchResult = CatalogSearchPage & {
|
|
174
174
|
tools: Array<CatalogSearchTool & {
|
|
175
175
|
connectorTitle?: string;
|
|
176
176
|
guide?: string;
|
|
177
177
|
guideSummary?: string;
|
|
178
178
|
}>;
|
|
179
179
|
};
|
|
180
|
-
|
|
180
|
+
interface CatalogDescription {
|
|
181
181
|
address: string;
|
|
182
182
|
schemaFormat?: "json" | "text";
|
|
183
183
|
name?: string;
|
package/dist/http/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { confinementBase, REDIRECT_STATUSES, BODILESS_STATUSES } from "../http-policy.js";
|
|
2
2
|
import { guardedTransport, sendGuarded } from "../guarded-transport.js";
|
|
3
|
-
// Guarded HTTP transport
|
|
3
|
+
// Guarded HTTP transport. Unreachable from src/index.ts.
|
|
4
4
|
//
|
|
5
5
|
// The guarded transport as an Effect `HttpClient`, with the behaviour of
|
|
6
6
|
// src/connectors/guarded-fetch.ts: requests confined beneath one base URL,
|
package/dist/http/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Internal HTTP failure contract
|
|
1
|
+
// Internal HTTP failure contract. Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
3
|
// Every way a kit request can fail, as a tagged error, and the one mapping
|
|
4
4
|
// from those to the wire `ConnectorCallError`. A provider supplies meaning
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// refusing status counts only for the request that received it. Advice to
|
|
27
27
|
// send a call again is a segment of its own (`resendAdvice`), never part of
|
|
28
28
|
// the message, so output composes it only where the outcome allows.
|
|
29
|
-
// Q8 policy is decided once by the core's `settleWrite
|
|
29
|
+
// Q8 policy is decided once by the core's `settleWrite`, from the
|
|
30
30
|
// ledger entries this kit forwards its matched proof and renewals to. A
|
|
31
31
|
// write answered 401 stays unknown; only renewal removes its handoff, and
|
|
32
32
|
// only a reviewed authentication-before-execution declaration proves refusal.
|
package/dist/http/json-schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider kit
|
|
1
|
+
// Provider kit. Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
3
|
// One Schema is a tool's type, its argument decoder, and its catalog JSON
|
|
4
4
|
// Schema. Effect's generator is correct but spells some things differently
|
package/dist/http/oauth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Refresh-once middleware
|
|
1
|
+
// Refresh-once middleware. Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
3
|
// Refresh-once for a connection that authenticates with a token it can
|
|
4
4
|
// renew: a downstream OAuth grant, a service-account assertion, a machine
|
package/dist/http/paging.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { bytesToHex, decodeBase64url } from "../bytes.js";
|
|
2
|
+
// Provider kit. Unreachable from src/index.ts.
|
|
2
3
|
//
|
|
3
4
|
// List tools take one page at a time. Lookahead detects an extra row; bound
|
|
4
5
|
// cursors authenticate the tool, arguments, connection and credential owner.
|
|
@@ -52,7 +53,7 @@ async function signingKey(storage) {
|
|
|
52
53
|
return cached;
|
|
53
54
|
let raw = await storage.get(continuationKeys.signingKey);
|
|
54
55
|
if (raw === null) {
|
|
55
|
-
const candidate =
|
|
56
|
+
const candidate = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
56
57
|
raw = (await storage.compareAndSet(continuationKeys.signingKey, null, candidate))
|
|
57
58
|
? candidate
|
|
58
59
|
: await storage.get(continuationKeys.signingKey);
|
|
@@ -92,7 +93,7 @@ export const boundCursor = {
|
|
|
92
93
|
const expected = yield* digest(tool, args);
|
|
93
94
|
let parsed;
|
|
94
95
|
try {
|
|
95
|
-
parsed = JSON.parse(new TextDecoder().decode(
|
|
96
|
+
parsed = JSON.parse(new TextDecoder().decode(decodeBase64url(token)));
|
|
96
97
|
}
|
|
97
98
|
catch {
|
|
98
99
|
parsed = undefined;
|
|
@@ -108,7 +109,7 @@ export const boundCursor = {
|
|
|
108
109
|
if (token !== base64url(JSON.stringify({ v: record.v, d: record.d, c: record.c, m: record.m })))
|
|
109
110
|
return yield* Effect.fail(new Refusal({ error: new ConnectorCallError("invalid_args", STRAY_CURSOR) }));
|
|
110
111
|
const key = yield* keyForCall;
|
|
111
|
-
const mac =
|
|
112
|
+
const mac = decodeBase64url(record.m);
|
|
112
113
|
const binding = yield* ContinuationBinding;
|
|
113
114
|
const claims = new TextEncoder().encode(canonicalJson([record.v, binding.connection, binding.principal, record.d, record.c]));
|
|
114
115
|
const valid = yield* Effect.promise(() => crypto.subtle.verify("HMAC", key, mac, claims));
|
|
@@ -125,7 +126,7 @@ function digest(tool, args) {
|
|
|
125
126
|
.sort()
|
|
126
127
|
.map((key) => [key, args[key]]);
|
|
127
128
|
const bytes = new TextEncoder().encode(canonicalJson([tool, shape]));
|
|
128
|
-
return Effect.promise(() => crypto.subtle.digest("SHA-256", bytes)).pipe(Effect.map((hash) =>
|
|
129
|
+
return Effect.promise(() => crypto.subtle.digest("SHA-256", bytes)).pipe(Effect.map((hash) => bytesToHex(new Uint8Array(hash).slice(0, 16))));
|
|
129
130
|
}
|
|
130
131
|
function base64url(text) {
|
|
131
132
|
return base64urlBytes(new TextEncoder().encode(text));
|
|
@@ -136,7 +137,3 @@ function base64urlBytes(bytes) {
|
|
|
136
137
|
binary += String.fromCharCode(byte);
|
|
137
138
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
138
139
|
}
|
|
139
|
-
function fromBase64url(token) {
|
|
140
|
-
const binary = atob(token.replace(/-/g, "+").replace(/_/g, "/"));
|
|
141
|
-
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
142
|
-
}
|
package/dist/http/provider.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Internal provider kit
|
|
1
|
+
// Internal provider kit. Unreachable from src/index.ts.
|
|
2
2
|
import { Context, Effect, Option } from "effect";
|
|
3
3
|
import { api } from "../connectors/api.js";
|
|
4
4
|
import { OAUTH_CHALLENGE_MAX_BYTES } from "../auth/renewable-grant.js";
|
package/dist/http/safety.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider kit
|
|
1
|
+
// Provider kit. Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
3
|
// The one value-safety boundary of a kit connection. `connect` requires it,
|
|
4
4
|
// and `Send` hands a provider nothing it has not passed: a named tool, a
|
package/dist/http/services.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Context, Effect } from "effect";
|
|
|
3
3
|
import { dispatchProviderRequest, providerRequest } from "../provider-call.js";
|
|
4
4
|
import { callClassification } from "../call-classification.js";
|
|
5
5
|
import { fromSignal } from "../runtime/run.js";
|
|
6
|
-
import { callFromContext, ContinuationBinding, ContinuationStorage,
|
|
6
|
+
import { callFromContext, ContinuationBinding, ContinuationStorage, serviceFor, Signal } from "../runtime/call.js";
|
|
7
7
|
import { RequestRefused, ResendRefused } from "./errors.js";
|
|
8
8
|
export { writeRequestKey } from "./write-request.js";
|
|
9
9
|
export { Caller, Credentials, Secrets, RequestSecrets, Budget, Recovery, Signal, Classification, Ledger, } from "../runtime/call.js";
|
|
@@ -70,10 +70,3 @@ export function fromConnectorContext(ctx, fallback) {
|
|
|
70
70
|
services: lease.services.pipe(Context.add(ContinuationStorage, serviceFor(ctx, ContinuationStorage) ?? ctx.storage), Context.add(ContinuationBinding, serviceFor(ctx, ContinuationBinding) ?? { connection: "", principal: null }), Context.add(Dispatches, []), Context.add(WriteCapacity, new Map([["write", new Map([[undefined, undefined]])]]))),
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
export function openCall(inputs) {
|
|
74
|
-
const lease = openCoreCall(inputs);
|
|
75
|
-
return {
|
|
76
|
-
...lease,
|
|
77
|
-
services: lease.services.pipe(Context.add(Dispatches, []), Context.add(WriteCapacity, new Map([["write", new Map([[undefined, undefined]])]]))),
|
|
78
|
-
};
|
|
79
|
-
}
|
package/dist/http/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider kit
|
|
1
|
+
// Provider kit. Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
3
|
// Tool builders. A tool is a name, a description, an input Schema, an output
|
|
4
4
|
// Schema, and a program; the builder renders both Schemas as catalog JSON
|
|
@@ -24,7 +24,6 @@ import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
|
|
|
24
24
|
import { catalogSchema, Integer } from "./json-schema.js";
|
|
25
25
|
import { boundCursor, Cursor, StandardPage } from "./paging.js";
|
|
26
26
|
import { Classification, Dispatches, IdempotencyKey, Recovery, ReadTool, WriteCapacity, writeRequestKey, fromConnectorContext, } from "./services.js";
|
|
27
|
-
const inputs = new WeakMap();
|
|
28
27
|
const keyed = new WeakSet();
|
|
29
28
|
/** Whether a kit write builder declared an idempotency key for this tool. */
|
|
30
29
|
export function takesIdempotencyKey(tool) {
|
|
@@ -62,10 +61,6 @@ function originalOrder(decoded, original) {
|
|
|
62
61
|
};
|
|
63
62
|
return copy(decoded, original);
|
|
64
63
|
}
|
|
65
|
-
/** The input Schema a kit tool decodes with, for the catalog/decoder agreement check. */
|
|
66
|
-
export function kitInput(tool) {
|
|
67
|
-
return inputs.get(tool);
|
|
68
|
-
}
|
|
69
64
|
/** A Struct's own property names, for `fields`. */
|
|
70
65
|
function propertyNames(schema, what) {
|
|
71
66
|
const fields = schema.fields;
|
|
@@ -350,7 +345,6 @@ rendered) {
|
|
|
350
345
|
})));
|
|
351
346
|
});
|
|
352
347
|
registerToolEntry(tool, (args, ctx) => program(args, ctx, inLogicalCall(ctx)));
|
|
353
|
-
inputs.set(tool, spec.input);
|
|
354
348
|
if (idempotency)
|
|
355
349
|
keyed.add(tool);
|
|
356
350
|
return tool;
|
package/dist/http/vendor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider kit
|
|
1
|
+
// Provider kit. Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
3
|
// A vendor is an `HttpApi`: endpoints with Schemas for path params, query,
|
|
4
4
|
// and payload, from which requests are encoded and the endpoint manifest is
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
// empty 200. Every answer, from a declared operation or a hatch's path, takes
|
|
14
14
|
// one route: bounded read, JSON, closed public classifier facts captured,
|
|
15
15
|
// the request's sent secrets redacted from
|
|
16
|
-
// every value of the whole body before anything can cut or extract from it
|
|
17
|
-
//
|
|
16
|
+
// every value of the whole body before anything can cut or extract from it,
|
|
17
|
+
// its keys kept for value safety to match by name, status (with
|
|
18
18
|
// its operation, so value safety decides what a failure may quote), the
|
|
19
19
|
// vendor's `result` hook, then the connection's ResultSafety. A provider
|
|
20
20
|
// cannot reach a body earlier. A Content-Type redaction altered selects the
|
|
21
21
|
// format the vendor reads as JSON, so no altered type routes a body around
|
|
22
|
-
// ResultSafety as text or bytes
|
|
22
|
+
// ResultSafety as text or bytes.
|
|
23
23
|
import { Context, Effect } from "effect";
|
|
24
24
|
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
25
25
|
import * as HttpApi from "effect/http-api/HttpApi";
|
|
@@ -233,12 +233,12 @@ export function connect(vendor, guarded, options) {
|
|
|
233
233
|
: defect)));
|
|
234
234
|
});
|
|
235
235
|
// Header-derived format decisions see only redacted Content-Type. Names are protocol
|
|
236
|
-
// constants `Headers` must accept, so only the values are redacted, together
|
|
236
|
+
// constants `Headers` must accept, so only the values are redacted, together.
|
|
237
237
|
const entries = [...reply.headers];
|
|
238
238
|
const values = secrets.redact(entries.map(([, value]) => value));
|
|
239
239
|
const headers = new Headers(entries.map(([name], index) => [name, values[index]]));
|
|
240
240
|
// Whole, before a detail line trims it or a projection extracts from it. Values only:
|
|
241
|
-
// result safety and projections match fields by their vendor names
|
|
241
|
+
// result safety and projections match fields by their vendor names.
|
|
242
242
|
let classifierFacts;
|
|
243
243
|
const parsed = reply.bytes !== undefined
|
|
244
244
|
? {
|
|
@@ -400,7 +400,7 @@ export function connect(vendor, guarded, options) {
|
|
|
400
400
|
/**
|
|
401
401
|
* The Content-Type a format decision reads. One redaction altered reads as
|
|
402
402
|
* `application/json`: a vendor's text or binary choice for it would hand
|
|
403
|
-
* result safety a body it cannot read field by field
|
|
403
|
+
* result safety a body it cannot read field by field.
|
|
404
404
|
*/
|
|
405
405
|
function formatType(redacted) {
|
|
406
406
|
return redactionAltered(redacted) ? "application/json" : redacted.toLowerCase();
|
package/dist/identity.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { sha256Hex } from "./bytes.js";
|
|
1
2
|
const IDENTITY_PART_RE = /^[\x21-\x7e]{1,256}$/;
|
|
2
|
-
const encoder = new TextEncoder();
|
|
3
3
|
export function validIdentityReference(value) {
|
|
4
4
|
return Boolean(value &&
|
|
5
5
|
typeof value.namespace === "string" &&
|
|
@@ -7,11 +7,7 @@ export function validIdentityReference(value) {
|
|
|
7
7
|
IDENTITY_PART_RE.test(value.namespace) &&
|
|
8
8
|
IDENTITY_PART_RE.test(value.id));
|
|
9
9
|
}
|
|
10
|
-
function bytesToHex(bytes) {
|
|
11
|
-
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
12
|
-
}
|
|
13
10
|
/** Deterministic pseudonymous partition; raw emails and ids do not enter keys. */
|
|
14
11
|
export async function identityStorageKey(identity) {
|
|
15
|
-
|
|
16
|
-
return bytesToHex(new Uint8Array(digest));
|
|
12
|
+
return sha256Hex(`${identity.namespace}\n${identity.id}`);
|
|
17
13
|
}
|
package/dist/invocation-auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bytesToHex } from "./bytes.js";
|
|
1
2
|
import { authCall, authFlow, registerAuth } from "./auth/native.js";
|
|
2
3
|
import { upwardStep } from "./upward.js";
|
|
3
4
|
import { runEdge } from "./runtime/run.js";
|
|
@@ -34,7 +35,7 @@ export async function classificationDigest(definition) {
|
|
|
34
35
|
annotations,
|
|
35
36
|
schema,
|
|
36
37
|
}));
|
|
37
|
-
return
|
|
38
|
+
return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
|
|
38
39
|
}
|
|
39
40
|
catch {
|
|
40
41
|
// A digest refusal blocks recovery, not an ordinary call.
|