@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
|
@@ -335,6 +335,8 @@ export function executorName(executor) {
|
|
|
335
335
|
*/
|
|
336
336
|
export function withExecutorAdmission(executor, admission) {
|
|
337
337
|
return {
|
|
338
|
+
...(executor.deadlineMs !== undefined ? { deadlineMs: executor.deadlineMs } : {}),
|
|
339
|
+
...(executor.maxReadBytes !== undefined ? { maxReadBytes: executor.maxReadBytes } : {}),
|
|
338
340
|
async acquire(options = {}) {
|
|
339
341
|
const token = await admission.acquire(options);
|
|
340
342
|
let released = false;
|
|
@@ -12,6 +12,10 @@ export function brandExecutor(executor, lifecycle) {
|
|
|
12
12
|
return executor;
|
|
13
13
|
}
|
|
14
14
|
export function assertExecutor(executor) {
|
|
15
|
+
const { deadlineMs } = executor;
|
|
16
|
+
if (deadlineMs !== undefined && (!Number.isSafeInteger(deadlineMs) || deadlineMs < 1)) {
|
|
17
|
+
throw new Error("ConnectaConfig.executor.deadlineMs must be a positive whole number of milliseconds.");
|
|
18
|
+
}
|
|
15
19
|
const descriptor = Object.getOwnPropertyDescriptor(executor, EXECUTOR_BRAND);
|
|
16
20
|
const brand = descriptor?.value;
|
|
17
21
|
if (typeof executor.execute === "function" &&
|
|
@@ -35,6 +39,8 @@ export function customExecutor(executor, options) {
|
|
|
35
39
|
const name = executorName(executor);
|
|
36
40
|
const wrapper = {
|
|
37
41
|
...(name ? { name } : {}),
|
|
42
|
+
...(executor.deadlineMs !== undefined ? { deadlineMs: executor.deadlineMs } : {}),
|
|
43
|
+
...(executor.maxReadBytes !== undefined ? { maxReadBytes: executor.maxReadBytes } : {}),
|
|
38
44
|
execute: executor.execute.bind(executor),
|
|
39
45
|
...(executor.close ? { close: executor.close.bind(executor) } : {}),
|
|
40
46
|
...(isAdmittingExecutor(executor)
|
package/dist/executor-result.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { META_ROUTES } from "./
|
|
1
|
+
import { META_ROUTES } from "./route-names.js";
|
|
2
2
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
3
|
-
import { msg } from "./errors.js";
|
|
3
|
+
import { msg, tokenPrefix } from "./errors.js";
|
|
4
4
|
/** ~6k tokens. Sandbox code should filter data down before returning. */
|
|
5
5
|
export const MAX_EXECUTE_RESULT_CHARS = CONFIG_DEFAULTS.execute.maxResultChars;
|
|
6
6
|
export const MAX_EXECUTE_LOG_CHARS = 4_000;
|
|
@@ -64,7 +64,7 @@ export function guardExecuteResultValue(value, maxChars = MAX_EXECUTE_RESULT_CHA
|
|
|
64
64
|
export function truncateExecuteText(text, max) {
|
|
65
65
|
if (text.length <= max)
|
|
66
66
|
return text;
|
|
67
|
-
return `${text
|
|
67
|
+
return `${tokenPrefix(text, max)}\n--- TRUNCATED (${text.length} chars total) — filter/map/slice data inside your code and return only what you need ---`;
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Validate serialization before IPC without discarding the program return.
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { prepareExecuteResultForTransport } from "../executor-result.js";
|
|
2
|
-
import { InvocationFailure } from "../invocation.js";
|
|
2
|
+
import { InvocationFailure } from "../invocation-failure.js";
|
|
3
3
|
import { msg } from "../errors.js";
|
|
4
4
|
import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_RESULT_BYTES, QUICKJS_RESULT_CHUNK_CHARS, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
|
|
5
5
|
import { executeQuickJs, prepareQuickJs } from "./quickjs-runtime.js";
|
|
6
6
|
let activeJobId;
|
|
7
7
|
let nextCallId = 1;
|
|
8
8
|
const pending = new Map();
|
|
9
|
+
/** Host calls still open will never hear back; settling them lets their handles go. */
|
|
10
|
+
function settlePending() {
|
|
11
|
+
for (const request of pending.values()) {
|
|
12
|
+
request.reject(new Error("Execution ended before the host call settled."));
|
|
13
|
+
}
|
|
14
|
+
pending.clear();
|
|
15
|
+
}
|
|
9
16
|
function send(message) {
|
|
10
17
|
if (!process.send)
|
|
11
18
|
throw new Error("QuickJS child IPC channel is unavailable.");
|
|
@@ -70,9 +77,14 @@ async function run(payload) {
|
|
|
70
77
|
jobId: payload.id,
|
|
71
78
|
payloadJson: stringifyBounded(entry, "QuickJS log entry"),
|
|
72
79
|
}));
|
|
80
|
+
// Answer only once the runtime is freed, so the parent knows whether this child can take the
|
|
81
|
+
// next run before it could send one. Only the settled stragglers stand in the way.
|
|
82
|
+
settlePending();
|
|
83
|
+
const tornDown = await raw.tornDown;
|
|
73
84
|
const prepared = {
|
|
74
85
|
outcome: prepareExecuteResultForTransport(raw),
|
|
75
86
|
...(raw.timedOut ? { timedOut: true } : {}),
|
|
87
|
+
...(tornDown === false ? { retire: true } : {}),
|
|
76
88
|
};
|
|
77
89
|
let payloadJson;
|
|
78
90
|
try {
|
|
@@ -85,10 +97,7 @@ async function run(payload) {
|
|
|
85
97
|
}
|
|
86
98
|
finally {
|
|
87
99
|
activeJobId = undefined;
|
|
88
|
-
|
|
89
|
-
request.reject(new Error("Execution ended before the host call settled."));
|
|
90
|
-
}
|
|
91
|
-
pending.clear();
|
|
100
|
+
settlePending();
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
process.on("message", (message) => {
|
|
@@ -16,6 +16,8 @@ export declare function normalizeCode(code: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
export interface QuickJsExecutionResult extends ExecuteResult {
|
|
18
18
|
timedOut?: boolean;
|
|
19
|
+
/** Settles once the run's runtime is freed; false when freeing it failed. */
|
|
20
|
+
tornDown?: Promise<boolean>;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Execute one program inside the child. Wall time includes host waits; guest
|
|
@@ -9,8 +9,8 @@ import { utf8Length } from "../bytes.js";
|
|
|
9
9
|
// continuations from a host-side loop. Values cross the boundary as JSON, so
|
|
10
10
|
// provider args/results must be JSON-serializable.
|
|
11
11
|
import { getQuickJS } from "quickjs-emscripten";
|
|
12
|
-
import { InvocationFailure } from "../invocation.js";
|
|
13
|
-
import { msg } from "../errors.js";
|
|
12
|
+
import { InvocationFailure } from "../invocation-failure.js";
|
|
13
|
+
import { msg, tokenPrefix } from "../errors.js";
|
|
14
14
|
import { withoutProgramTerminator } from "../program-source.js";
|
|
15
15
|
import { hostCallLabel, MAX_QUICKJS_LOG_TRANSPORT_BYTES, serializedBytes } from "./quickjs-protocol.js";
|
|
16
16
|
/** Load and compile the shared QuickJS WASM module before a run budget starts. */
|
|
@@ -28,10 +28,8 @@ const MAX_LOG_TOTAL_CHARS = 256_000;
|
|
|
28
28
|
const LOG_ENTRY_LIMIT_MARKER = `[log truncated after ${MAX_LOG_ENTRIES} entries]`;
|
|
29
29
|
const LOG_SIZE_LIMIT_MARKER = "[log truncated: size budget exceeded]";
|
|
30
30
|
const MAX_LOG_MARKER_TRANSPORT_BYTES = Math.max(logTransportBytes(LOG_ENTRY_LIMIT_MARKER), logTransportBytes(LOG_SIZE_LIMIT_MARKER));
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// This still lets guest code reduce data more than ten times larger than
|
|
34
|
-
// connecta's final response budget.
|
|
31
|
+
// Bounds what one host result puts in the guest. This still lets guest code
|
|
32
|
+
// reduce data more than ten times larger than connecta's final response budget.
|
|
35
33
|
const MAX_HOST_RESULT_BYTES = 256 * 1024;
|
|
36
34
|
function logTransportBytes(entry) {
|
|
37
35
|
// The log is encoded into ExecutionPayload, then that payloadJson string is
|
|
@@ -159,7 +157,7 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
159
157
|
// but slicing here keeps only a bounded string alive in `logs`.
|
|
160
158
|
let entry = ctx.getString(h);
|
|
161
159
|
if (entry.length > MAX_LOG_ENTRY_CHARS) {
|
|
162
|
-
entry = `${entry
|
|
160
|
+
entry = `${tokenPrefix(entry, MAX_LOG_ENTRY_CHARS)}…[entry truncated]`;
|
|
163
161
|
}
|
|
164
162
|
const entryTransportBytes = logTransportBytes(entry);
|
|
165
163
|
if (logTotalChars + entry.length > MAX_LOG_TOTAL_CHARS ||
|
|
@@ -222,7 +220,7 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
222
220
|
}
|
|
223
221
|
else {
|
|
224
222
|
const error = ctx.newError();
|
|
225
|
-
const message = ctx.unwrapResult(ctx.evalCode(JSON.stringify(msg(reply.error)
|
|
223
|
+
const message = ctx.unwrapResult(ctx.evalCode(JSON.stringify(tokenPrefix(msg(reply.error), 4_000))));
|
|
226
224
|
ctx.setProp(error, "message", message);
|
|
227
225
|
message.dispose();
|
|
228
226
|
if (reply.error instanceof InvocationFailure) {
|
|
@@ -262,6 +260,13 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
262
260
|
const { timeoutMs, cpuTimeMs, memoryLimitBytes, maxStackSizeBytes } = options;
|
|
263
261
|
const QuickJS = await getQuickJS();
|
|
264
262
|
const ctx = QuickJS.newContext();
|
|
263
|
+
// quickjs-emscripten-core 0.32.0's executePendingJobs reads its context out-pointer through a
|
|
264
|
+
// typed array, which detaches when WASM memory grows during the job pass. It then reads
|
|
265
|
+
// undefined and calls newContext({ contextPointer: undefined }), which creates a context
|
|
266
|
+
// nothing frees, and freeing the runtime aborts. A run has this one context, and that lookup
|
|
267
|
+
// is the only thing here that reaches newContext, so it always means this one.
|
|
268
|
+
// Remove once a release fixes it: justjake/quickjs-emscripten#240, fix in PR #248.
|
|
269
|
+
ctx.runtime.newContext = () => ctx;
|
|
265
270
|
const deadline = Date.now() + timeoutMs;
|
|
266
271
|
const timeoutError = `Execution timed out after ${timeoutMs}ms.`;
|
|
267
272
|
const cpuTimeoutError = `Execution exceeded the ${cpuTimeMs}ms guest CPU budget.`;
|
|
@@ -293,6 +298,16 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
293
298
|
let describeError;
|
|
294
299
|
const logs = [];
|
|
295
300
|
const bridge = installBridge(ctx, providers, logs, onLog);
|
|
301
|
+
// A failed free leaves this child's WASM module unusable, but the answer is already in host memory.
|
|
302
|
+
const dispose = () => {
|
|
303
|
+
try {
|
|
304
|
+
ctx.dispose();
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
296
311
|
const finish = (r) => {
|
|
297
312
|
bridge.aborted = true;
|
|
298
313
|
describeError?.dispose();
|
|
@@ -307,19 +322,17 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
307
322
|
// stragglers are waiting on. Arming before the pending check (and
|
|
308
323
|
// synchronously, before the first await) closes the settle-before-arm
|
|
309
324
|
// window: any settle after this point resolves the promise we await.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
void (async () => {
|
|
325
|
+
const tornDown = bridge.pending === 0
|
|
326
|
+
? Promise.resolve(dispose())
|
|
327
|
+
: (async () => {
|
|
314
328
|
armWake(bridge);
|
|
315
329
|
while (bridge.pending > 0) {
|
|
316
330
|
await bridge.waitForSettle;
|
|
317
331
|
armWake(bridge);
|
|
318
332
|
}
|
|
319
|
-
|
|
333
|
+
return dispose();
|
|
320
334
|
})();
|
|
321
|
-
}
|
|
322
|
-
return logs.length > 0 ? { ...r, logs } : r;
|
|
335
|
+
return { ...r, ...(logs.length > 0 ? { logs } : {}), tornDown };
|
|
323
336
|
};
|
|
324
337
|
const rejected = (error) => {
|
|
325
338
|
// Interrupt/deadline facts are host state, never inferred by inspecting
|
|
@@ -332,7 +345,7 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
332
345
|
let description = { name: "Error", message: "Program threw a value." };
|
|
333
346
|
const type = ctx.typeof(error);
|
|
334
347
|
if (type === "string")
|
|
335
|
-
description = ctx.getString(error)
|
|
348
|
+
description = tokenPrefix(ctx.getString(error), 4000);
|
|
336
349
|
else if (type === "number")
|
|
337
350
|
description = ctx.getNumber(error);
|
|
338
351
|
else if (type === "undefined")
|
|
@@ -347,7 +360,10 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
347
360
|
if (described.error)
|
|
348
361
|
described.error.dispose();
|
|
349
362
|
else {
|
|
350
|
-
|
|
363
|
+
const parsed = JSON.parse(ctx.getString(described.value));
|
|
364
|
+
if (typeof parsed?.name === "string")
|
|
365
|
+
parsed.name = tokenPrefix(parsed.name, 64);
|
|
366
|
+
description = parsed ?? description;
|
|
351
367
|
described.value.dispose();
|
|
352
368
|
}
|
|
353
369
|
if (wallInterrupted || Date.now() >= deadline)
|
|
@@ -364,7 +380,7 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
364
380
|
const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
|
|
365
381
|
return {
|
|
366
382
|
result: undefined,
|
|
367
|
-
error: formatGuestError(description)
|
|
383
|
+
error: tokenPrefix(formatGuestError(description), 4000),
|
|
368
384
|
failure: {
|
|
369
385
|
name,
|
|
370
386
|
...(call ? { call } : {}),
|
|
@@ -410,8 +426,9 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
410
426
|
for (let i = 0; i < prototypes.length; i++) {
|
|
411
427
|
if (prototype === prototypes[i][0]) { name = prototypes[i][1]; break; }
|
|
412
428
|
}
|
|
413
|
-
|
|
414
|
-
|
|
429
|
+
// One unit past the host's cuts, which read it to tell a finished token from an open one.
|
|
430
|
+
return stringify({ __proto__: null, name: text("name", name, 65),
|
|
431
|
+
message: text("message", "Program threw a value.", 4001), stack: text("stack", "", 1000) });
|
|
415
432
|
};
|
|
416
433
|
})()`));
|
|
417
434
|
if (described.error) {
|
|
@@ -17,9 +17,10 @@ import { existsSync } from "node:fs";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { Deferred, Duration, Effect, Exit, Schedule, Scope } from "effect";
|
|
19
19
|
import { AdmissionController, ExecutorAdmissionError, ExecutorExecutionError } from "../executor-admission.js";
|
|
20
|
-
import { msg } from "../errors.js";
|
|
21
|
-
import { InvocationFailure } from "../invocation.js";
|
|
20
|
+
import { msg, tokenPrefix } from "../errors.js";
|
|
21
|
+
import { InvocationFailure } from "../invocation-failure.js";
|
|
22
22
|
import { brandExecutor } from "../executor-contract.js";
|
|
23
|
+
import { PROGRAM_RESULT_READ_BYTES } from "../program-result.js";
|
|
23
24
|
import { MAX_EXECUTE_LOG_CHARS } from "../executor-result.js";
|
|
24
25
|
import { admit } from "../runtime/admission.js";
|
|
25
26
|
import { awaitExecutorLease, executorProgram, provideExecutorProgram } from "../runtime/executor.js";
|
|
@@ -77,7 +78,7 @@ function nonNegativeWhole(value, fallback, name) {
|
|
|
77
78
|
function errorPayload(error) {
|
|
78
79
|
return JSON.stringify({
|
|
79
80
|
ok: false,
|
|
80
|
-
error: msg(error)
|
|
81
|
+
error: tokenPrefix(msg(error), MAX_ERROR_CHARS),
|
|
81
82
|
...(error instanceof InvocationFailure ? { call: error.details } : {}),
|
|
82
83
|
});
|
|
83
84
|
}
|
|
@@ -121,10 +122,12 @@ let providePoolProgram;
|
|
|
121
122
|
class QuickJsChildPool {
|
|
122
123
|
/** What `/health` and `connecta doctor` call this sandbox. */
|
|
123
124
|
name = "QuickJS";
|
|
125
|
+
deadlineMs;
|
|
124
126
|
admission;
|
|
125
127
|
slots;
|
|
126
128
|
available;
|
|
127
129
|
runtimeOptions;
|
|
130
|
+
maxReadBytes;
|
|
128
131
|
// Scope closes still in flight, so close() can wait out every child rather
|
|
129
132
|
// than only the ones it retired itself.
|
|
130
133
|
retiring = new Set();
|
|
@@ -145,6 +148,10 @@ class QuickJsChildPool {
|
|
|
145
148
|
memoryLimitBytes: positiveWhole(options.memoryLimitBytes, DEFAULT_MEMORY_LIMIT_BYTES, "memoryLimitBytes"),
|
|
146
149
|
maxStackSizeBytes: positiveWhole(options.maxStackSizeBytes, DEFAULT_STACK_LIMIT_BYTES, "maxStackSizeBytes"),
|
|
147
150
|
};
|
|
151
|
+
this.deadlineMs = this.runtimeOptions.timeoutMs;
|
|
152
|
+
// Parsing and then using a value costs CPU and heap in proportion to its size,
|
|
153
|
+
// so a tighter budget reads less, and no budget reads past the fixed cap.
|
|
154
|
+
this.maxReadBytes = Math.min(Math.floor((PROGRAM_RESULT_READ_BYTES * this.runtimeOptions.cpuTimeMs) / DEFAULT_CPU_TIME_MS), 2 * 1024 * 1024, Math.floor(this.runtimeOptions.memoryLimitBytes / 16));
|
|
148
155
|
this.slots = Array.from({ length: concurrency }, () => ({
|
|
149
156
|
crashes: [],
|
|
150
157
|
notBefore: 0,
|
|
@@ -475,9 +482,16 @@ class QuickJsChildPool {
|
|
|
475
482
|
if (typeof entry !== "string")
|
|
476
483
|
return;
|
|
477
484
|
const separator = active.logs.length > 0 ? 1 : 0;
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
485
|
+
const room = MAX_EXECUTE_LOG_CHARS + 1 - active.logChars - separator;
|
|
486
|
+
if (entry.length > room) {
|
|
487
|
+
// A cut between tokens can stop short of the bound, so its marker signals the loss.
|
|
488
|
+
active.logs.push(`${tokenPrefix(entry, room)}…[entry truncated]`);
|
|
489
|
+
active.logChars = MAX_EXECUTE_LOG_CHARS + 1;
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
active.logs.push(entry);
|
|
493
|
+
active.logChars += separator + entry.length;
|
|
494
|
+
}
|
|
481
495
|
}
|
|
482
496
|
catch {
|
|
483
497
|
/* Malformed log messages carry no output. */
|
|
@@ -519,7 +533,10 @@ class QuickJsChildPool {
|
|
|
519
533
|
slot.backoff = undefined;
|
|
520
534
|
slot.notBefore = 0;
|
|
521
535
|
this.resolveActive(slot, payload.outcome);
|
|
522
|
-
if (payload.
|
|
536
|
+
if (payload.retire) {
|
|
537
|
+
console.warn("[connecta] QuickJS could not free a finished run's runtime. Its answer stands; replacing the child.");
|
|
538
|
+
}
|
|
539
|
+
if (payload.timedOut || payload.retire) {
|
|
523
540
|
// A timed-out host call may still retain QuickJS handles. A fresh child
|
|
524
541
|
// is cheaper and safer than reusing a context with unknown stragglers.
|
|
525
542
|
// The runtime sets this flag itself: matching error text here would let
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The plain runtime slot keeps the HTTP kit and Node sender off the root
|
|
2
2
|
// and Worker graphs. /node installs its sender when listen() starts serving.
|
|
3
3
|
import { ConnectorCallError } from "./errors.js";
|
|
4
|
+
/** Internal request marker: only downstream-learned URL fetches set this. */
|
|
5
|
+
export const learnedDestination = Symbol("connecta.learnedDestination");
|
|
4
6
|
let owned;
|
|
5
7
|
/** Runtime configuration, never request data. Returns the previous slot for isolated fixtures. */
|
|
6
8
|
export function configureGuardedTransport(transport) {
|
package/dist/guest-functions.js
CHANGED
|
@@ -13,7 +13,7 @@ export const GUEST_FUNCTIONS = [
|
|
|
13
13
|
{
|
|
14
14
|
name: "search",
|
|
15
15
|
budget: "hostCall",
|
|
16
|
-
declaration: "search(args?: { query?: string; connector?: string; limit?: number; offset?: number; includeSchemas?: Schemas }): Promise<{ tools: Tool[]; catalogErrors: { connector: string; code: string; message: string }[]; hasMore: boolean; nextOffset?: number }>;",
|
|
16
|
+
declaration: "search(args?: { query?: string; connector?: string; limit?: number; offset?: number; includeSchemas?: Schemas; fullDescriptions?: boolean }): Promise<{ tools: Tool[]; catalogErrors: { connector: string; code: string; message: string; stale?: true }[]; absence?: { message: string }; readyCalls?: { address: string; args: { path: string } }[]; hasMore: boolean; nextOffset?: number }>;",
|
|
17
17
|
},
|
|
18
18
|
/** Describes exact addresses, one entry per address in order. */
|
|
19
19
|
{
|
|
@@ -34,10 +34,11 @@ export const GUEST_FUNCTIONS = [
|
|
|
34
34
|
budget: "hostCall",
|
|
35
35
|
declaration: "read(uri: string): Promise<{ contents: { uri: string; text?: string; blob?: string }[] }>;",
|
|
36
36
|
},
|
|
37
|
-
/**
|
|
37
|
+
/** Reads a retained result whole, or pages it by page or UTF-8 byte offset. */
|
|
38
38
|
{
|
|
39
39
|
name: "result",
|
|
40
40
|
budget: "hostCall",
|
|
41
|
+
wrapped: true,
|
|
41
42
|
deadline: "result storage",
|
|
42
43
|
declaration: "result(id: string | Paged, options?: { page?: number; offset?: number; maxBytes?: number }): Promise<{ text: string; hasMore: boolean; nextOffset?: number }>;",
|
|
43
44
|
},
|
|
@@ -55,7 +56,7 @@ export const GUEST_FUNCTIONS = [
|
|
|
55
56
|
budget: "emission",
|
|
56
57
|
declaration: "emit(block: Block | Native | Exclude<Result, Paged>): PromiseLike<void>;",
|
|
57
58
|
},
|
|
58
|
-
/** Tells the host a
|
|
59
|
+
/** Tells the host a program read a result field off an unawaited call. */
|
|
59
60
|
{
|
|
60
61
|
name: "unawaited",
|
|
61
62
|
budget: "once",
|
|
@@ -76,7 +77,7 @@ export const GUEST_API_DECLARATION = `declare const connecta: {
|
|
|
76
77
|
${GUEST_FUNCTIONS.flatMap((fn) => ("declaration" in fn ? [fn.declaration] : [])).join("\n")}
|
|
77
78
|
};
|
|
78
79
|
type Schemas = "json" | "compact" | "typescript";
|
|
79
|
-
type Tool = { address: string; description?: string; inputSchema?: unknown; outputSchema?: unknown; signature?: string };
|
|
80
|
+
type Tool = { address: string; name?: string; description?: string; inputSchema?: unknown; outputSchema?: unknown; signature?: string; guideRequired?: true };
|
|
80
81
|
type Paged = { format: "paged"; valueFormat: "json" | "text"; resultId?: string; totalBytes: number; truncated: true; hint: string };
|
|
81
82
|
type Result = ({ format: "json"; data: unknown } | { format: "text"; data: string } | Paged) & { content?: Native[] };
|
|
82
83
|
type Block = { type: "text"; text: string } | { type: "image" | "audio"; data: string; mimeType: string };
|
package/dist/guest-runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GUEST_FUNCTIONS } from "./guest-functions.js";
|
|
2
|
+
import { LEADING_TRIVIA } from "./program-source.js";
|
|
2
3
|
/** Capture the single-use runner before evaluating the guest expression. */
|
|
3
4
|
export function wrapGuestProgram(code) {
|
|
4
5
|
return `async () => globalThis.__connecta_run()(async (connecta) => await (\n${code}\n)())`;
|
|
@@ -109,6 +110,7 @@ export function guestInitializer() {
|
|
|
109
110
|
const promiseCatch = Function.prototype.call.bind(NativePromise.prototype.catch);
|
|
110
111
|
const promiseFinally = Function.prototype.call.bind(NativePromise.prototype.finally);
|
|
111
112
|
const push = Function.prototype.call.bind(Array.prototype.push);
|
|
113
|
+
const join = Function.prototype.call.bind(Array.prototype.join);
|
|
112
114
|
const apply = Function.prototype.call.bind(Function.prototype.apply);
|
|
113
115
|
const NativeError = Error;
|
|
114
116
|
const NativeWeakMap = WeakMap;
|
|
@@ -131,10 +133,35 @@ export function guestInitializer() {
|
|
|
131
133
|
page = await hostResult(value.transfer);
|
|
132
134
|
text += page.text;
|
|
133
135
|
} while (page.hasMore);
|
|
134
|
-
|
|
136
|
+
const whole = { data: value.handle.valueFormat === "json" ? JSON.parse(text) : text,
|
|
135
137
|
format: value.handle.valueFormat };
|
|
138
|
+
// Nothing can name a read's bytes now, so the host may reclaim them.
|
|
139
|
+
if (value.reclaim) try { await hostResult(value.transfer); } catch {}
|
|
140
|
+
return whole;
|
|
136
141
|
} catch { return value.handle; }
|
|
137
142
|
}
|
|
143
|
+
async function result(id, options) {
|
|
144
|
+
if (!options || options.all !== true) {
|
|
145
|
+
const page = await apply(hostResult, undefined, arguments);
|
|
146
|
+
// The transfer cursor is the prelude's: a getter on all must not hand it to the program.
|
|
147
|
+
if (page && page.transfer) delete page.transfer;
|
|
148
|
+
return page;
|
|
149
|
+
}
|
|
150
|
+
let page = await hostResult(id, options);
|
|
151
|
+
const parts = [page.text];
|
|
152
|
+
const transfer = page.transfer;
|
|
153
|
+
while (page.hasMore) {
|
|
154
|
+
page = await hostResult(transfer);
|
|
155
|
+
push(parts, page.text);
|
|
156
|
+
}
|
|
157
|
+
const text = join(parts, "");
|
|
158
|
+
const stored = id ? id.valueFormat : undefined;
|
|
159
|
+
if (stored === "text") return { data: text, format: "text" };
|
|
160
|
+
// A handle's JSON that won't parse throws; only a bare id, which doesn't say, may be text.
|
|
161
|
+
if (stored === "json") try { return { data: JSON.parse(text), format: "json" }; } catch { throw new NativeError("This result can't be parsed whole, because redaction can leave its JSON invalid. Page it with { offset } instead."); }
|
|
162
|
+
try { return { data: JSON.parse(text), format: "json" }; } catch {}
|
|
163
|
+
return { data: text, format: "text" };
|
|
164
|
+
}
|
|
138
165
|
async function call() {
|
|
139
166
|
const reply = await apply(hostCall, undefined, arguments);
|
|
140
167
|
if (!reply || !reply.native) return inline(reply);
|
|
@@ -208,6 +235,7 @@ ${GUEST_NAMESPACE}
|
|
|
208
235
|
if (unawaited) try { await provider.unawaited(); } catch {}
|
|
209
236
|
throw error;
|
|
210
237
|
}
|
|
238
|
+
if (unawaited) try { await provider.unawaited(); } catch {}
|
|
211
239
|
for (let i = 0; i < pending.length; i++) {
|
|
212
240
|
const report = await pending[i].settled;
|
|
213
241
|
if (!report.ok && !pending[i].handled) throw report.error;
|
|
@@ -272,6 +300,14 @@ export function programError(raw, code) {
|
|
|
272
300
|
hint =
|
|
273
301
|
'Use connecta.call(address, args), connecta.search, connecta.describe, connecta.result, and connecta.skill("usage"). Services are addressed by connector.tool, never guest globals.';
|
|
274
302
|
}
|
|
303
|
+
else if (name === "TypeError" &&
|
|
304
|
+
/not a function|not iterable/.test(message) &&
|
|
305
|
+
/\bconnecta\s*\.\s*(?:search|describe|call)\b/.test(code.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, ""))) {
|
|
306
|
+
hint = "search and describe return { tools }; call returns { data, format }.";
|
|
307
|
+
}
|
|
308
|
+
else if (name === "SyntaxError" && !/^(?:async\b|\(|function\b)/.test(code.replace(LEADING_TRIVIA, ""))) {
|
|
309
|
+
hint = "Pass one expression: async () => { ...; return answer; }.";
|
|
310
|
+
}
|
|
275
311
|
return {
|
|
276
312
|
code: "program_error",
|
|
277
313
|
message: `Program ${name}: ${message}`,
|
package/dist/guest-types.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface GuestApi {
|
|
|
15
15
|
timeoutMs?: number;
|
|
16
16
|
}): Promise<GuestResult>;
|
|
17
17
|
read(uri: string): Promise<GuestResourceResult>;
|
|
18
|
+
/** Reads a retained result whole, for one host call; a larger one throws `result_too_large`. */
|
|
19
|
+
result(id: string | GuestResultHandle, options: {
|
|
20
|
+
all: true;
|
|
21
|
+
}): Promise<Exclude<GuestResult, GuestResultHandle>>;
|
|
18
22
|
result(id: string | GuestResultHandle, options?: {
|
|
19
23
|
page?: number;
|
|
20
24
|
offset?: number;
|
|
@@ -138,8 +142,16 @@ interface CatalogSearchFailure extends CatalogFailureDetail {
|
|
|
138
142
|
connector: string;
|
|
139
143
|
nextAction?: Extract<NonNullable<CallErrorDetails["nextAction"]>, {
|
|
140
144
|
tool: "authorize_connector";
|
|
145
|
+
} | {
|
|
146
|
+
url: string;
|
|
147
|
+
} | {
|
|
148
|
+
action: string;
|
|
141
149
|
}>;
|
|
142
150
|
retry?: string;
|
|
151
|
+
/** This connector's tools are listed from its last complete catalog; a call still checks the live one. */
|
|
152
|
+
stale?: true;
|
|
153
|
+
/** With `stale`: milliseconds since the downstream last confirmed that catalog. */
|
|
154
|
+
ageMs?: number;
|
|
143
155
|
}
|
|
144
156
|
interface CatalogSearchPage {
|
|
145
157
|
catalogErrors: CatalogSearchFailure[];
|
|
@@ -196,6 +208,7 @@ interface CatalogDescription {
|
|
|
196
208
|
schemaFormat?: "json" | "text";
|
|
197
209
|
name?: string;
|
|
198
210
|
description?: string;
|
|
211
|
+
descriptionTruncated?: true;
|
|
199
212
|
guide?: string;
|
|
200
213
|
guideSummary?: string;
|
|
201
214
|
guideRequired?: true;
|
|
@@ -207,6 +220,8 @@ interface CatalogDescription {
|
|
|
207
220
|
inputSchemaTruncated?: true;
|
|
208
221
|
outputSchemaTruncated?: true;
|
|
209
222
|
annotations?: ToolDef["annotations"];
|
|
223
|
+
/** In `execute_read_code`: false when this route refuses the tool because it can write. */
|
|
224
|
+
callable?: false;
|
|
210
225
|
error?: string;
|
|
211
226
|
errorDetails?: CatalogDescriptionFailureDetail;
|
|
212
227
|
}
|
|
@@ -214,6 +229,7 @@ type CatalogSearchTool = {
|
|
|
214
229
|
name: string;
|
|
215
230
|
address: string;
|
|
216
231
|
description?: string;
|
|
232
|
+
descriptionTruncated?: true;
|
|
217
233
|
inputSchema?: unknown;
|
|
218
234
|
outputSchema?: unknown;
|
|
219
235
|
signature?: string;
|
|
@@ -226,6 +242,8 @@ type CatalogSearchTool = {
|
|
|
226
242
|
outputKeys?: string[];
|
|
227
243
|
annotations?: ToolDef["annotations"];
|
|
228
244
|
classification?: ToolDef["classification"];
|
|
245
|
+
/** In `execute_read_code`: false when this route refuses the tool because it can write. */
|
|
246
|
+
callable?: false;
|
|
229
247
|
guideRequired?: true;
|
|
230
248
|
guideRequiredReasons?: GuideRequiredReason[];
|
|
231
249
|
};
|
package/dist/http/client.js
CHANGED
|
@@ -30,6 +30,12 @@ import { RedirectRefused, Refusal, RequestRefused, ResendRefused, ResponseTooLar
|
|
|
30
30
|
import { Classification, Dispatches, IdempotencyKey, Ledger, RequestFacts, Secrets, Signal, WriteCapacity, reserveWrite, writeRequestKey, } from "./services.js";
|
|
31
31
|
const decoder = new TextDecoder();
|
|
32
32
|
const responseEntries = new WeakMap();
|
|
33
|
+
// Private request identity for middleware observing a rejected response. Never
|
|
34
|
+
// attach headers or body to the failure or its payload-free ledger entry.
|
|
35
|
+
const failedRequests = new WeakMap();
|
|
36
|
+
export function failedResponseRequest(failure) {
|
|
37
|
+
return failedRequests.get(failure);
|
|
38
|
+
}
|
|
33
39
|
/** The core's physical request, also available to direct HttpClient middleware. */
|
|
34
40
|
export function responseRequest(response) {
|
|
35
41
|
return responseEntries.get(response);
|
|
@@ -88,18 +94,23 @@ export function guardedClient(options) {
|
|
|
88
94
|
confined = HttpClientRequest.setHeader(confined, name, value);
|
|
89
95
|
}
|
|
90
96
|
const secrets = yield* Secrets;
|
|
97
|
+
const identifiers = new Set();
|
|
91
98
|
for (const [name, value] of Object.entries(yield* options.authenticate)) {
|
|
92
99
|
if (confined.headers[name.toLowerCase()] !== undefined) {
|
|
93
100
|
return yield* refuse(`A ${provider} request may not set the ${name.toLowerCase()} header; authentication is connector-owned.`);
|
|
94
101
|
}
|
|
95
102
|
confined = HttpClientRequest.setHeader(confined, name, value);
|
|
96
|
-
|
|
103
|
+
const lower = name.toLowerCase();
|
|
104
|
+
if (!options.publicIdentifierHeaders?.includes(lower))
|
|
105
|
+
secrets.header(value);
|
|
106
|
+
else if (secrets.publicIdentifierHeader(value))
|
|
107
|
+
identifiers.add(lower);
|
|
97
108
|
}
|
|
98
109
|
// Registered as guarded-fetch registers: the final URL, headers, and a
|
|
99
110
|
// text body (a form body's secrets are read from it).
|
|
100
111
|
const text = bodyText(confined);
|
|
101
112
|
secrets.request(wireUrl(confined), {
|
|
102
|
-
headers: Object.fromEntries(Object.entries(confined.headers)),
|
|
113
|
+
headers: Object.fromEntries(Object.entries(confined.headers).filter(([name]) => !identifiers.has(name))),
|
|
103
114
|
...(text === undefined ? {} : { body: text }),
|
|
104
115
|
});
|
|
105
116
|
preparedEntries.set(confined, entry);
|
|
@@ -216,6 +227,7 @@ export function guardedClient(options) {
|
|
|
216
227
|
const refused = (failure) => {
|
|
217
228
|
dispatch.failure = failure;
|
|
218
229
|
responseEntries.set(failure, entry);
|
|
230
|
+
failedRequests.set(failure, request);
|
|
219
231
|
return Effect.fail(failure);
|
|
220
232
|
};
|
|
221
233
|
const response = yield* Effect.tryPromise({
|
package/dist/http/errors.js
CHANGED
|
@@ -88,6 +88,9 @@ export function isHttpFailure(error) {
|
|
|
88
88
|
export function validateVendorErrors(vendor) {
|
|
89
89
|
errorMeanings(vendor);
|
|
90
90
|
captureErrorReviews(vendor);
|
|
91
|
+
const limit = vendor.maxHeaderChars;
|
|
92
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_REVIEWED_HEADER))
|
|
93
|
+
throw new Error(`maxHeaderChars must be a whole number from 1 to ${MAX_REVIEWED_HEADER}.`);
|
|
91
94
|
const proof = vendor.authBeforeExecution;
|
|
92
95
|
if (proof && (typeof proof.evidence !== "string" || !proof.evidence.trim() || typeof proof.matches !== "function")) {
|
|
93
96
|
throw new Error("An authBeforeExecution declaration requires nonempty evidence and a narrow raw-fact predicate.");
|
|
@@ -106,6 +109,7 @@ const TOKEN = /^[A-Za-z0-9_.:[\]-]{1,128}$/;
|
|
|
106
109
|
/** How many entries of a list the detail line sees. Classification sees them all. */
|
|
107
110
|
const DISPLAYED = 8;
|
|
108
111
|
const MAX_HEADER = 128;
|
|
112
|
+
const MAX_REVIEWED_HEADER = 1024;
|
|
109
113
|
const MAX_DEPTH = 32;
|
|
110
114
|
const MAX_WAIT_MS = 86_400_000;
|
|
111
115
|
const errorReviews = new WeakMap();
|
|
@@ -292,11 +296,12 @@ function displayed(facts, vendor) {
|
|
|
292
296
|
Array.isArray(value) ? value.slice(0, display[name] ?? DISPLAYED) : value,
|
|
293
297
|
]));
|
|
294
298
|
}
|
|
295
|
-
function visibleHeaders(headers,
|
|
299
|
+
function visibleHeaders(headers, vendor) {
|
|
296
300
|
const out = Object.create(null);
|
|
297
|
-
|
|
301
|
+
const limit = vendor.maxHeaderChars ?? MAX_HEADER;
|
|
302
|
+
for (const name of vendor.headers ?? []) {
|
|
298
303
|
const value = headers.get(name);
|
|
299
|
-
if (value !== null && value.length <=
|
|
304
|
+
if (value !== null && value.length <= limit && !hasControl(value))
|
|
300
305
|
out[name.toLowerCase()] = value;
|
|
301
306
|
}
|
|
302
307
|
return out;
|
|
@@ -518,7 +523,7 @@ function classificationInput(failure, vendor, withheld) {
|
|
|
518
523
|
facts: { ...redacted, ...(captured?.vendor === vendor ? captured.facts : vocabularyFacts(redacted, vendor)) },
|
|
519
524
|
presence: structuralPresence(failure.body, captureErrorReviews(vendor).presence),
|
|
520
525
|
numbers,
|
|
521
|
-
headers: visibleHeaders(failure.headers, vendor
|
|
526
|
+
headers: visibleHeaders(failure.headers, vendor),
|
|
522
527
|
retryAfterMs: numbers["retryAfterMs"],
|
|
523
528
|
operation: failure.operation,
|
|
524
529
|
withheld,
|
package/dist/http/fields.js
CHANGED
|
@@ -22,8 +22,10 @@ const ANCHORS = {
|
|
|
22
22
|
item: "each item in items",
|
|
23
23
|
data: "the response body in data, each item when the body is a list",
|
|
24
24
|
};
|
|
25
|
+
/** What a result can carry that its schema cannot name: the withheld marker and cuts for size. */
|
|
26
|
+
const RESULT_NOTE = 'It only narrows the result. A withheld value reads "[redacted]"; truncated lists paths cut for size, and the ids to fetch them stay.';
|
|
25
27
|
/** The description of a `fields` argument: its anchor, one valid example, then any tool note. */
|
|
26
|
-
export function fieldsDescription(anchor, example, note =
|
|
28
|
+
export function fieldsDescription(anchor, example, note = RESULT_NOTE) {
|
|
27
29
|
return `Dot paths to keep. Paths start at ${ANCHORS[anchor]}, e.g. ${JSON.stringify([example])}. ${note}`;
|
|
28
30
|
}
|
|
29
31
|
/** The `fields` argument: at most 50 dot paths of 1 to 200 characters. */
|
|
@@ -174,13 +176,15 @@ export function boundWarnings(warnings) {
|
|
|
174
176
|
const omitted = warnings.length - kept.length;
|
|
175
177
|
return omitted ? [...kept, `${omitted} more warning${omitted === 1 ? "" : "s"} omitted.`] : kept;
|
|
176
178
|
}
|
|
179
|
+
/** Enough declared names to repair a path; a wide contract's full list costs more than most replies. */
|
|
180
|
+
const NAMED_CHARS = 160;
|
|
177
181
|
/** Declared names listed in one warning, within its character budget. */
|
|
178
182
|
function named(names) {
|
|
179
183
|
let text = "";
|
|
180
184
|
let shown = 0;
|
|
181
185
|
for (const name of names) {
|
|
182
186
|
const next = text ? `${text}, ${name}` : name;
|
|
183
|
-
if (next.length >
|
|
187
|
+
if (next.length > NAMED_CHARS)
|
|
184
188
|
break;
|
|
185
189
|
text = next;
|
|
186
190
|
shown += 1;
|
|
@@ -201,6 +205,8 @@ export function fieldsMisses(data, fields, declared = []) {
|
|
|
201
205
|
return [];
|
|
202
206
|
const flat = (values) => values.flatMap((value) => (Array.isArray(value) ? flat(value) : [value]));
|
|
203
207
|
const contract = declared.map((path) => segments(path.replace(/^\[\]\.?/, "")));
|
|
208
|
+
// Misses stopping at the same place list its names once.
|
|
209
|
+
const listed = new Map();
|
|
204
210
|
return fields.flatMap((field, index) => {
|
|
205
211
|
const path = segments(field);
|
|
206
212
|
let values = flat([data]);
|
|
@@ -219,9 +225,15 @@ export function fieldsMisses(data, fields, declared = []) {
|
|
|
219
225
|
.filter((parts) => parts.length > depth && prefix.every((part, at) => parts[at] === part))
|
|
220
226
|
.map((parts) => parts[depth])),
|
|
221
227
|
];
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
const opening = `fields/${index} matched nothing after filtering, at segment ${depth + 1}`;
|
|
229
|
+
if (!names.length)
|
|
230
|
+
return [`${opening}; nothing is declared there.`];
|
|
231
|
+
const list = named(names);
|
|
232
|
+
const earlier = listed.get(list);
|
|
233
|
+
if (earlier !== undefined)
|
|
234
|
+
return [`${opening}; declared there: same as fields/${earlier}.`];
|
|
235
|
+
listed.set(list, index);
|
|
236
|
+
return [`${opening}; declared there: ${list}.`];
|
|
225
237
|
}
|
|
226
238
|
return [];
|
|
227
239
|
});
|