@zackbart/connecta 0.29.0 → 0.30.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 +67 -0
- package/README.md +123 -134
- package/bin/connecta.mjs +13 -2
- package/bin/version.mjs +1 -1
- package/dist/argument-redaction.js +230 -0
- package/dist/auth-elicitation.js +14 -9
- package/dist/authorization-handoff.js +111 -0
- package/dist/catalog-cache.js +2 -2
- package/dist/catalog-service.js +103 -63
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -7
- package/dist/connectors/guarded-fetch.js +3 -3
- package/dist/connectors/negotiation-cache.js +1 -1
- package/dist/downstream-input-context.js +9 -0
- package/dist/downstream-input.js +13 -11
- package/dist/errors.d.ts +36 -12
- package/dist/errors.js +19 -9
- package/dist/execute.js +375 -43
- package/dist/executor-result.js +11 -9
- package/dist/executors/quickjs-child.js +12 -5
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +6 -1
- package/dist/executors/quickjs.js +20 -4
- package/dist/guest-runtime.js +52 -2
- package/dist/guest-types.d.ts +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/invocation.js +40 -14
- package/dist/meta-output.js +0 -15
- package/dist/meta-tools.js +166 -95
- package/dist/oauth-handoff.js +88 -11
- package/dist/program-result.js +5 -0
- package/dist/program-writes.js +5 -1
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/registry.d.ts +8 -6
- package/dist/registry.js +134 -61
- package/dist/resource-read.js +4 -1
- package/dist/routes/mcp.js +11 -2
- package/dist/sent-secrets.d.ts +54 -13
- package/dist/sent-secrets.js +770 -231
- package/dist/skills.js +26 -16
- package/dist/sqlite.js +21 -6
- package/dist/storage/keys.js +20 -5
- package/dist/storage/memory.js +9 -4
- package/dist/storage/sql.js +18 -9
- package/dist/types.d.ts +17 -3
- package/dist/usage-guide.js +49 -40
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +2 -1
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +121 -89
- package/documentation/auth.md +138 -5
- package/documentation/code-mode.md +224 -107
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +135 -30
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +4 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +5 -0
- package/examples/worker/README.md +7 -0
- package/package.json +7 -1
- package/templates/node/README.md +7 -0
- package/templates/node/package.json +1 -1
package/dist/execute.js
CHANGED
|
@@ -5,8 +5,9 @@ import { Cause, Deferred, Duration, Effect, Exit } from "effect";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { advertisedSchema } from "./advertised-schema.js";
|
|
7
7
|
import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
|
|
8
|
-
import { createMetaTools, jsonResult } from "./meta-tools.js";
|
|
9
|
-
import {
|
|
8
|
+
import { createMetaTools, guardProgramReturn, jsonResult, pageProgramResult, } from "./meta-tools.js";
|
|
9
|
+
import { PROGRAM_RESULT_INLINE_BYTES, PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES, } from "./program-result.js";
|
|
10
|
+
import { MAX_EXECUTE_LOG_CHARS, MAX_EXECUTE_RESULT_CHARS, truncateExecuteText } from "./executor-result.js";
|
|
10
11
|
import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor } from "./executor-admission.js";
|
|
11
12
|
import { boundedEchoText, ConnectorCallError, msg } from "./errors.js";
|
|
12
13
|
import { DEFAULT_MAX_WRITES, ProgramWrites, writeStateOf } from "./program-writes.js";
|
|
@@ -118,6 +119,10 @@ class ExecuteDiagnostics {
|
|
|
118
119
|
};
|
|
119
120
|
}
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Guest-authored blocks exclude pointers (INV-3). Native call blocks are
|
|
124
|
+
* retained on the host and forwarded only through request-local references.
|
|
125
|
+
*/
|
|
121
126
|
const EMIT_SHAPE_HINT = '{ type: "text", text } or { type: "image" | "audio", data (base64), mimeType }';
|
|
122
127
|
/**
|
|
123
128
|
* Strict M1 validation: required fields present and string-valued, nothing
|
|
@@ -175,26 +180,142 @@ export class EmitCollector {
|
|
|
175
180
|
this.diagnostics = diagnostics;
|
|
176
181
|
}
|
|
177
182
|
accept(raw) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
this.acceptNative([requireEmittedBlock(raw)]);
|
|
184
|
+
}
|
|
185
|
+
/** Only the host's retained downstream result may enter this path. */
|
|
186
|
+
acceptNative(blocks) {
|
|
187
|
+
let bytes = this.bytes;
|
|
188
|
+
let textChars = this.textChars;
|
|
189
|
+
let count = this.blocks.length;
|
|
190
|
+
for (const block of blocks) {
|
|
191
|
+
if (count >= this.maxBlocks) {
|
|
192
|
+
throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 blocks remaining; ${this.maxBytes - bytes} of ${this.maxBytes} serialized bytes remaining`);
|
|
193
|
+
}
|
|
194
|
+
const serialized = JSON.stringify(block);
|
|
195
|
+
const size = diagnosticsEncoder.encode(serialized).byteLength;
|
|
196
|
+
if (bytes + size > this.maxBytes) {
|
|
197
|
+
throw guestFailure("budget_exceeded", `connecta.emit byte budget exceeded: block is ${size} serialized bytes with ${this.maxBytes - bytes} of ${this.maxBytes} remaining`);
|
|
198
|
+
}
|
|
199
|
+
textChars += block.type === "text" ? serialized.length : 0;
|
|
200
|
+
// Embedded resource text shares the result cap too; binary blobs use
|
|
201
|
+
// the transport budget, just like images and audio.
|
|
202
|
+
if (block.type === "resource" && "text" in block.resource)
|
|
203
|
+
textChars += serialized.length;
|
|
204
|
+
if (textChars > MAX_EXECUTE_RESULT_CHARS - 512) {
|
|
205
|
+
throw guestFailure("result_too_large", "Emitted text shares the 24,000-character program result limit. Reduce the text before emitting it.");
|
|
206
|
+
}
|
|
207
|
+
count++;
|
|
208
|
+
bytes += size;
|
|
191
209
|
}
|
|
192
|
-
this.textChars
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
210
|
+
this.textChars = textChars;
|
|
211
|
+
this.bytes = bytes;
|
|
212
|
+
this.blocks.push(...blocks);
|
|
195
213
|
this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
|
|
196
214
|
}
|
|
197
215
|
}
|
|
216
|
+
/** Bound both JSON size and object amplification before retaining native blocks. */
|
|
217
|
+
class NativePresentationLimit extends Error {
|
|
218
|
+
}
|
|
219
|
+
function boundedNativeValue(value, maxBytes, copy) {
|
|
220
|
+
let bytes = 0;
|
|
221
|
+
let nodes = 0;
|
|
222
|
+
// Empty objects cost only two JSON bytes but much more heap. Derive the
|
|
223
|
+
// allocation ceiling from the transport budget, with a fixed stack ceiling.
|
|
224
|
+
const maxNodes = Math.floor(Math.min(maxBytes, PROGRAM_RESULT_INLINE_BYTES) / 32);
|
|
225
|
+
const active = new Set();
|
|
226
|
+
const charge = (size) => {
|
|
227
|
+
bytes += size;
|
|
228
|
+
if (bytes > maxBytes)
|
|
229
|
+
throw new NativePresentationLimit();
|
|
230
|
+
};
|
|
231
|
+
const string = (text) => {
|
|
232
|
+
// Count JSON escapes and UTF-8 directly, without allocating a potentially
|
|
233
|
+
// six-times-larger escaped string or byte buffer just to reject it.
|
|
234
|
+
if (text.length > maxBytes - bytes)
|
|
235
|
+
throw new NativePresentationLimit();
|
|
236
|
+
charge(2);
|
|
237
|
+
for (let i = 0; i < text.length; i++) {
|
|
238
|
+
const unit = text.charCodeAt(i);
|
|
239
|
+
if (unit === 34 || unit === 92 || unit === 8 || unit === 9 || unit === 10 || unit === 12 || unit === 13)
|
|
240
|
+
charge(2);
|
|
241
|
+
else if (unit < 32)
|
|
242
|
+
charge(6);
|
|
243
|
+
else if (unit < 128)
|
|
244
|
+
charge(1);
|
|
245
|
+
else if (unit < 2048)
|
|
246
|
+
charge(2);
|
|
247
|
+
else if (unit >= 0xd800 &&
|
|
248
|
+
unit <= 0xdbff &&
|
|
249
|
+
text.charCodeAt(i + 1) >= 0xdc00 &&
|
|
250
|
+
text.charCodeAt(i + 1) <= 0xdfff) {
|
|
251
|
+
charge(4);
|
|
252
|
+
i++;
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
charge(unit >= 0xd800 && unit <= 0xdfff ? 6 : 3);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const visit = (item, depth) => {
|
|
259
|
+
if (++nodes > maxNodes || depth > 64)
|
|
260
|
+
throw new NativePresentationLimit();
|
|
261
|
+
if (typeof item === "string") {
|
|
262
|
+
string(item);
|
|
263
|
+
return item;
|
|
264
|
+
}
|
|
265
|
+
if (item === null || typeof item === "boolean" || typeof item === "number") {
|
|
266
|
+
charge(JSON.stringify(item).length);
|
|
267
|
+
return item;
|
|
268
|
+
}
|
|
269
|
+
if (typeof item !== "object" || active.has(item))
|
|
270
|
+
throw new NativePresentationLimit();
|
|
271
|
+
const array = Array.isArray(item);
|
|
272
|
+
if (![array ? Array.prototype : Object.prototype, null].includes(Object.getPrototypeOf(item)))
|
|
273
|
+
throw new NativePresentationLimit();
|
|
274
|
+
const jsonHook = Object.getOwnPropertyDescriptor(item, "toJSON");
|
|
275
|
+
if (jsonHook && (!("value" in jsonHook) || typeof jsonHook.value === "function"))
|
|
276
|
+
throw new NativePresentationLimit();
|
|
277
|
+
active.add(item);
|
|
278
|
+
charge(2);
|
|
279
|
+
if (array) {
|
|
280
|
+
if (item.length > maxNodes - nodes)
|
|
281
|
+
throw new NativePresentationLimit();
|
|
282
|
+
const result = copy ? [] : undefined;
|
|
283
|
+
for (let i = 0; i < item.length; i++) {
|
|
284
|
+
if (i)
|
|
285
|
+
charge(1);
|
|
286
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, String(i));
|
|
287
|
+
if (descriptor && !("value" in descriptor))
|
|
288
|
+
throw new NativePresentationLimit();
|
|
289
|
+
const child = visit(descriptor?.value ?? null, depth + 1);
|
|
290
|
+
result?.push(child);
|
|
291
|
+
}
|
|
292
|
+
active.delete(item);
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
const result = copy ? {} : undefined;
|
|
296
|
+
let first = true;
|
|
297
|
+
for (const key in item) {
|
|
298
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
299
|
+
if (!descriptor?.enumerable)
|
|
300
|
+
continue;
|
|
301
|
+
if (!("value" in descriptor))
|
|
302
|
+
throw new NativePresentationLimit();
|
|
303
|
+
if (descriptor.value === undefined)
|
|
304
|
+
continue;
|
|
305
|
+
if (!first)
|
|
306
|
+
charge(1);
|
|
307
|
+
first = false;
|
|
308
|
+
string(key);
|
|
309
|
+
charge(1);
|
|
310
|
+
const child = visit(descriptor.value, depth + 1);
|
|
311
|
+
if (result)
|
|
312
|
+
Object.defineProperty(result, key, { value: child, enumerable: true, writable: true, configurable: true });
|
|
313
|
+
}
|
|
314
|
+
active.delete(item);
|
|
315
|
+
return result;
|
|
316
|
+
};
|
|
317
|
+
return { value: visit(value, 0), bytes };
|
|
318
|
+
}
|
|
198
319
|
/** A positive whole-number budget, or the default when the value is unusable. */
|
|
199
320
|
function resolveBudget(value, fallback) {
|
|
200
321
|
return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.trunc(value) : fallback;
|
|
@@ -320,6 +441,20 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
320
441
|
stopped.catch(() => { });
|
|
321
442
|
const maxWrites = resolveBudget(limits.maxWrites, DEFAULT_MAX_WRITES);
|
|
322
443
|
let writes = 0;
|
|
444
|
+
// These are transfer cursors only. All result bytes live in the existing stash.
|
|
445
|
+
const transfers = new Map();
|
|
446
|
+
const nativeResults = new Map();
|
|
447
|
+
const transportResult = (value) => {
|
|
448
|
+
if (value.format === "paged") {
|
|
449
|
+
const handle = value;
|
|
450
|
+
if (handle.resultId && handle.totalBytes <= PROGRAM_RESULT_INLINE_BYTES) {
|
|
451
|
+
const transfer = `transfer:${crypto.randomUUID()}`;
|
|
452
|
+
transfers.set(transfer, { id: handle.resultId, offset: 0 });
|
|
453
|
+
return { transfer, handle };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return value;
|
|
457
|
+
};
|
|
323
458
|
/** Budget and account for writes admitted by the pool trust decision. */
|
|
324
459
|
const invocationContext = (sending, timeoutMs) => ({
|
|
325
460
|
source: "execute_code",
|
|
@@ -329,6 +464,86 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
329
464
|
...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
|
|
330
465
|
unwrapResult: true,
|
|
331
466
|
trust: limits.trust,
|
|
467
|
+
processResult: async (value, resolved, secrets, format, raw) => {
|
|
468
|
+
const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets, limits.trust, requestScope, wireBytes);
|
|
469
|
+
const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
|
|
470
|
+
? raw
|
|
471
|
+
: undefined;
|
|
472
|
+
const dataKey = envelope
|
|
473
|
+
? "toolResult" in envelope
|
|
474
|
+
? "toolResult"
|
|
475
|
+
: envelope.structuredContent !== undefined
|
|
476
|
+
? "structuredContent"
|
|
477
|
+
: undefined
|
|
478
|
+
: undefined;
|
|
479
|
+
// Preserve #788 before even inspecting presentation. Paging serializes
|
|
480
|
+
// the structured value synchronously; no native metadata is visited.
|
|
481
|
+
if (dataKey !== undefined &&
|
|
482
|
+
diagnosticsEncoder.encode(JSON.stringify(value)).byteLength > PROGRAM_RESULT_INLINE_BYTES) {
|
|
483
|
+
return page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
|
|
484
|
+
}
|
|
485
|
+
const content = envelope?.content;
|
|
486
|
+
if (!Array.isArray(content) ||
|
|
487
|
+
!content.some((block) => block && ["image", "audio", "resource", "resource_link"].includes(block.type))) {
|
|
488
|
+
return transportResult(await page(value, format));
|
|
489
|
+
}
|
|
490
|
+
const unavailablePresentation = () => ({
|
|
491
|
+
format: "paged",
|
|
492
|
+
valueFormat: "json",
|
|
493
|
+
totalBytes: 0,
|
|
494
|
+
truncated: true,
|
|
495
|
+
hint: "Paging is unavailable. Native presentation exceeds byte, node or depth limits, or is not acyclic JSON. Reduce the downstream presentation before forwarding it.",
|
|
496
|
+
});
|
|
497
|
+
let snapshot;
|
|
498
|
+
let retained;
|
|
499
|
+
try {
|
|
500
|
+
// Oversized but bounded envelopes may still use the existing stash.
|
|
501
|
+
// Measure without copying them; only inline native blocks are retained.
|
|
502
|
+
const measured = boundedNativeValue(envelope, Math.max(EXECUTE_MAX_EMITTED_BYTES, limits.emitCollector?.maxBytes ?? 0), false);
|
|
503
|
+
if (measured.bytes <= PROGRAM_RESULT_INLINE_BYTES) {
|
|
504
|
+
retained = boundedNativeValue(content, PROGRAM_RESULT_INLINE_BYTES, true).value;
|
|
505
|
+
snapshot = { ...envelope, content: retained };
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
snapshot = envelope;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (!(error instanceof NativePresentationLimit))
|
|
513
|
+
throw error;
|
|
514
|
+
const result = dataKey === undefined ? undefined : transportResult(await page(value, format));
|
|
515
|
+
const notice = unavailablePresentation();
|
|
516
|
+
return result ? { value: result, native: { value: notice } } : notice;
|
|
517
|
+
}
|
|
518
|
+
// Snapshot blocks before either stash operation yields. Start both
|
|
519
|
+
// serializations now so an oversized presentation's pages also own the
|
|
520
|
+
// original bytes even if the structured-data stash pauses.
|
|
521
|
+
const resultPending = dataKey === undefined ? undefined : page(value, format, PROGRAM_RESULT_WIRE_BYTES - 1024);
|
|
522
|
+
const nativePending = page(snapshot, "json");
|
|
523
|
+
const result = await resultPending;
|
|
524
|
+
const native = await nativePending;
|
|
525
|
+
let valueResult;
|
|
526
|
+
if (native.format === "paged") {
|
|
527
|
+
valueResult = result ? transportResult(result) : undefined;
|
|
528
|
+
if (!native.resultId || native.totalBytes > PROGRAM_RESULT_INLINE_BYTES) {
|
|
529
|
+
return valueResult ? { value: valueResult, native: { value: native } } : native;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Redaction can shrink an oversized original below the inline cap. It
|
|
533
|
+
// still has no retained snapshot: keep its handle, never mint a ref to
|
|
534
|
+
// absent or connector-owned blocks.
|
|
535
|
+
if (!retained) {
|
|
536
|
+
const notice = native.format === "paged" ? native : unavailablePresentation();
|
|
537
|
+
const valueResult = result ? transportResult(result) : undefined;
|
|
538
|
+
return valueResult ? { value: valueResult, native: { value: notice } } : notice;
|
|
539
|
+
}
|
|
540
|
+
const ref = crypto.randomUUID();
|
|
541
|
+
nativeResults.set(ref, retained);
|
|
542
|
+
return {
|
|
543
|
+
...(valueResult ? { value: valueResult } : {}),
|
|
544
|
+
native: { ref, value: transportResult(native), dataKey },
|
|
545
|
+
};
|
|
546
|
+
},
|
|
332
547
|
beforeWrite: (target) => Effect.sync(() => {
|
|
333
548
|
if (programWrites?.isClosed) {
|
|
334
549
|
return {
|
|
@@ -391,15 +606,36 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
391
606
|
hostCallTimeoutMs;
|
|
392
607
|
const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
|
|
393
608
|
// A trusted-pool write settles here — unknown if the call never returned an
|
|
394
|
-
// outcome.
|
|
609
|
+
// outcome. Credentials are filtered from arguments; submitted private values are not.
|
|
395
610
|
const sending = {};
|
|
396
611
|
const outcome = yield* invocation
|
|
397
|
-
.pipeline(address, sentSecrets.
|
|
612
|
+
.pipeline(address, sentSecrets.redactCredentials(args ?? {}), invocationContext(sending, timeoutMs))
|
|
398
613
|
.pipe(Effect.onExit((exit) => Effect.sync(() => sending.settle?.(Exit.isSuccess(exit) ? writeStateOf(exit.value) : "unknown"))));
|
|
614
|
+
if (outcome.resolved) {
|
|
615
|
+
const resolved = outcome.resolved;
|
|
616
|
+
limits.resultDependencies?.push({
|
|
617
|
+
connector: resolved.connector.id,
|
|
618
|
+
tool: resolved.definition.name,
|
|
619
|
+
classification: outcome.dispatched && resolved.definition.classification !== "read" ? "write" : "read",
|
|
620
|
+
// A refusal exposes catalog metadata, not a completed write's result.
|
|
621
|
+
...(!outcome.dispatched ? { access: "catalog" } : {}),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// Resolution failures can expose a catalog error or connector metadata
|
|
626
|
+
// without ever finding a tool. Such bytes depend on connector visibility,
|
|
627
|
+
// not on the existence of the requested (possibly unknown) tool.
|
|
628
|
+
const target = registry.resolveAddress(address);
|
|
629
|
+
if (target)
|
|
630
|
+
catalogDependency(target.connector.id);
|
|
631
|
+
if (!outcome.ok)
|
|
632
|
+
for (const connector of outcome.error.configuredConnectors ?? [])
|
|
633
|
+
catalogDependency(connector);
|
|
634
|
+
}
|
|
399
635
|
diagnostics?.recordCall(outcome);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
636
|
+
if (!outcome.ok)
|
|
637
|
+
return yield* Effect.fail(new InvocationFailure(outcome.error));
|
|
638
|
+
return outcome.value;
|
|
403
639
|
});
|
|
404
640
|
// Discovery gets the same treatment from here (L2): once the run has
|
|
405
641
|
// ended no search or describe starts, and one still in flight fails
|
|
@@ -419,24 +655,58 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
419
655
|
? Effect.fail(cancelled())
|
|
420
656
|
: Effect.raceAllFirst([reading, fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled))])).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
421
657
|
});
|
|
422
|
-
const meta = createMetaTools(registry, baseUrl, {
|
|
658
|
+
const meta = createMetaTools(registry, baseUrl, {
|
|
659
|
+
trust: limits.trust,
|
|
660
|
+
requestScope,
|
|
661
|
+
onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
|
|
662
|
+
});
|
|
663
|
+
const catalogDependency = (connector, tool) => {
|
|
664
|
+
if (registry.getConnector(connector))
|
|
665
|
+
limits.resultDependencies?.push({
|
|
666
|
+
connector,
|
|
667
|
+
...(tool ? { tool } : {}),
|
|
668
|
+
classification: "read",
|
|
669
|
+
access: "catalog",
|
|
670
|
+
});
|
|
671
|
+
};
|
|
672
|
+
const catalogAddressDependency = (address) => {
|
|
673
|
+
const resolved = registry.resolveAddress(address);
|
|
674
|
+
if (resolved)
|
|
675
|
+
catalogDependency(resolved.connector.id, resolved.toolName);
|
|
676
|
+
};
|
|
423
677
|
const operations = {
|
|
424
678
|
call,
|
|
425
679
|
result: (id, options, signal) => Effect.tryPromise({
|
|
426
680
|
try: async () => {
|
|
681
|
+
const transfer = typeof id === "string" ? transfers.get(id) : undefined;
|
|
682
|
+
const transferId = id;
|
|
683
|
+
if (transfer) {
|
|
684
|
+
id = transfer.id;
|
|
685
|
+
options = { offset: transfer.offset, maxBytes: PROGRAM_RESULT_PAGE_BYTES };
|
|
686
|
+
}
|
|
687
|
+
else if (id !== null && typeof id === "object" && !Array.isArray(id)) {
|
|
688
|
+
id = id.resultId;
|
|
689
|
+
}
|
|
427
690
|
if (typeof id !== "string" ||
|
|
428
691
|
(!options && options !== undefined) ||
|
|
429
692
|
(options !== undefined && (typeof options !== "object" || Array.isArray(options)))) {
|
|
430
|
-
throw guestFailure("invalid_args", "Use connecta.result(
|
|
693
|
+
throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }).");
|
|
431
694
|
}
|
|
432
|
-
const args = (options ?? {});
|
|
433
|
-
if (Object.keys(args).some((key) => !["offset", "maxBytes"].includes(key)) ||
|
|
695
|
+
const args = { ...(options ?? {}) };
|
|
696
|
+
if (Object.keys(args).some((key) => !["page", "offset", "maxBytes"].includes(key)) ||
|
|
697
|
+
(args.page !== undefined &&
|
|
698
|
+
(typeof args.page !== "number" ||
|
|
699
|
+
!Number.isSafeInteger(args.page) ||
|
|
700
|
+
args.page < 0 ||
|
|
701
|
+
args.offset !== undefined)) ||
|
|
434
702
|
(args.offset !== undefined &&
|
|
435
703
|
(typeof args.offset !== "number" || !Number.isSafeInteger(args.offset) || args.offset < 0)) ||
|
|
436
704
|
(args.maxBytes !== undefined &&
|
|
437
705
|
(typeof args.maxBytes !== "number" || !Number.isSafeInteger(args.maxBytes) || args.maxBytes < 1))) {
|
|
438
|
-
throw guestFailure("invalid_args", "Use connecta.result(
|
|
706
|
+
throw guestFailure("invalid_args", "Use connecta.result(handleOrId, { page?, offset?, maxBytes? }) with whole page/offset >= 0 and maxBytes >= 1; page and offset are mutually exclusive.");
|
|
439
707
|
}
|
|
708
|
+
// A page must fit every shipped bridge even when the connector cap is raised.
|
|
709
|
+
args.maxBytes = Math.min(args.maxBytes ?? PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_PAGE_BYTES);
|
|
440
710
|
const page = await meta.readResult({ id, ...args }, { signal: signal });
|
|
441
711
|
if (page.isError)
|
|
442
712
|
throw new InvocationFailure((page.structuredContent?.error ?? {
|
|
@@ -444,6 +714,12 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
444
714
|
message: page.content[0]?.text ?? "Result unavailable.",
|
|
445
715
|
retryable: false,
|
|
446
716
|
}));
|
|
717
|
+
if (transfer) {
|
|
718
|
+
if (page.structuredContent?.hasMore)
|
|
719
|
+
transfer.offset = page.structuredContent.nextOffset;
|
|
720
|
+
else
|
|
721
|
+
transfers.delete(transferId);
|
|
722
|
+
}
|
|
447
723
|
return page.structuredContent;
|
|
448
724
|
},
|
|
449
725
|
catch: (err) => err,
|
|
@@ -455,6 +731,11 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
455
731
|
const skills = new SkillsRegistry(registry, baseUrl, {
|
|
456
732
|
requestScope,
|
|
457
733
|
requestSignal: utilitySignal ?? hostAccessSignal,
|
|
734
|
+
onRead: (connector, source) => limits.resultDependencies?.push({
|
|
735
|
+
connector: connector.id,
|
|
736
|
+
classification: "read",
|
|
737
|
+
...(source === "local" ? { access: "catalog" } : {}),
|
|
738
|
+
}),
|
|
458
739
|
probeTimeoutMs: limits.probeTimeoutMs,
|
|
459
740
|
defer: limits.defer,
|
|
460
741
|
});
|
|
@@ -470,6 +751,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
470
751
|
requestScope,
|
|
471
752
|
sentSecrets,
|
|
472
753
|
timeoutMs: hostCallTimeoutMs,
|
|
754
|
+
onResolved: (connector) => limits.resultDependencies?.push({ connector: connector.id, classification: "read" }),
|
|
473
755
|
onConnectorTime: (elapsed) => {
|
|
474
756
|
connectorMs += elapsed;
|
|
475
757
|
},
|
|
@@ -485,7 +767,26 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
485
767
|
if (!limits.emitCollector) {
|
|
486
768
|
throw guestFailure("unavailable", "connecta.emit is unavailable: no emission collector was configured for this execution", true);
|
|
487
769
|
}
|
|
488
|
-
|
|
770
|
+
if (block !== null && typeof block === "object" && "contentResult" in block) {
|
|
771
|
+
throw guestFailure("result_too_large", "Native content is not inline; page result.contentResult with connecta.result and reduce it before emitting.");
|
|
772
|
+
}
|
|
773
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block) && "ref" in block) {
|
|
774
|
+
const reference = block;
|
|
775
|
+
const native = typeof reference.ref === "string" ? nativeResults.get(reference.ref) : undefined;
|
|
776
|
+
if (!native ||
|
|
777
|
+
Object.keys(reference).some((key) => !["ref", "index"].includes(key)) ||
|
|
778
|
+
(reference.index !== undefined &&
|
|
779
|
+
(typeof reference.index !== "number" ||
|
|
780
|
+
!Number.isSafeInteger(reference.index) ||
|
|
781
|
+
reference.index < 0 ||
|
|
782
|
+
reference.index >= native.length))) {
|
|
783
|
+
throw guestFailure("invalid_args", "connecta.emit requires a native result from this program's connecta.call.");
|
|
784
|
+
}
|
|
785
|
+
const blocks = reference.index === undefined ? native : [native[reference.index]];
|
|
786
|
+
limits.emitCollector.acceptNative(sentSecrets.redact(blocks));
|
|
787
|
+
}
|
|
788
|
+
else
|
|
789
|
+
limits.emitCollector.accept(sentSecrets.redact(block));
|
|
489
790
|
},
|
|
490
791
|
catch: (err) => err,
|
|
491
792
|
}),
|
|
@@ -497,12 +798,35 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
497
798
|
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
498
799
|
}));
|
|
499
800
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
801
|
+
for (const tool of result.tools)
|
|
802
|
+
catalogAddressDependency(tool.address);
|
|
803
|
+
for (const error of result.catalogErrors)
|
|
804
|
+
catalogDependency(error.connector);
|
|
805
|
+
for (const connector of result.absence?.configuredConnectors ?? [])
|
|
806
|
+
catalogDependency(connector);
|
|
807
|
+
for (const connector of result.queryAnalysis?.configuredConnectors ?? [])
|
|
808
|
+
catalogDependency(connector);
|
|
809
|
+
if (result.queryAnalysis?.connectorScope)
|
|
810
|
+
catalogDependency(result.queryAnalysis.connectorScope);
|
|
500
811
|
return result;
|
|
501
812
|
}),
|
|
502
813
|
describe: (raw) => discovery("describe", async () => {
|
|
503
814
|
const args = (raw ?? {});
|
|
504
815
|
const result = { tools: await catalog.describe({ ...args, format: args.format ?? "json" }) };
|
|
505
816
|
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
817
|
+
for (const tool of result.tools) {
|
|
818
|
+
if (!tool.error)
|
|
819
|
+
catalogAddressDependency(tool.address);
|
|
820
|
+
else {
|
|
821
|
+
const resolved = registry.resolveAddress(tool.address);
|
|
822
|
+
if (resolved)
|
|
823
|
+
catalogDependency(resolved.connector.id);
|
|
824
|
+
for (const connector of tool.errorDetails?.configuredConnectors ?? [])
|
|
825
|
+
catalogDependency(connector);
|
|
826
|
+
for (const suggestion of tool.errorDetails?.suggestions ?? [])
|
|
827
|
+
catalogAddressDependency(suggestion);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
506
830
|
return result;
|
|
507
831
|
}),
|
|
508
832
|
};
|
|
@@ -511,7 +835,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
511
835
|
const typedFailure = (err) => Effect.suspend(() => {
|
|
512
836
|
if (!(err instanceof InvocationFailure))
|
|
513
837
|
return Effect.fail(err);
|
|
514
|
-
const failure = boundedGuestFailure(sentSecrets
|
|
838
|
+
const failure = boundedGuestFailure(redactAgentOutput(sentSecrets, err));
|
|
515
839
|
return Effect.fail(failure);
|
|
516
840
|
});
|
|
517
841
|
return {
|
|
@@ -534,7 +858,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
534
858
|
}
|
|
535
859
|
// L4/M7: spend on entry, before even invalid arguments are checked;
|
|
536
860
|
// emit has a separate budget and never spends this one.
|
|
537
|
-
|
|
861
|
+
// Transparent transfer pages consume the already-admitted call, not guest host calls.
|
|
862
|
+
const counted = name !== "emit" && !(name === "result" && typeof args[0] === "string" && transfers.has(args[0]));
|
|
538
863
|
if (counted && ++hostCalls.attempted > maxHostCalls) {
|
|
539
864
|
hostCalls.failed++;
|
|
540
865
|
budgetFailure = new HostCallBudgetExceeded(hostCalls, maxHostCalls);
|
|
@@ -703,6 +1028,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
703
1028
|
const emitted = new EmitCollector(resolveBudget(config.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES), resolveBudget(config.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS), diagnostics);
|
|
704
1029
|
const hostCalls = { attempted: 0, admitted: 0, succeeded: 0, failed: 0 };
|
|
705
1030
|
const programWrites = new ProgramWrites();
|
|
1031
|
+
const resultDependencies = [];
|
|
706
1032
|
const dispatchController = new AbortController();
|
|
707
1033
|
const terminal = Deferred.makeUnsafe();
|
|
708
1034
|
let budgetFailure;
|
|
@@ -754,6 +1080,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
754
1080
|
trust: config.trust,
|
|
755
1081
|
maxWrites: config.maxWrites,
|
|
756
1082
|
programWrites,
|
|
1083
|
+
resultDependencies,
|
|
757
1084
|
dispatchController,
|
|
758
1085
|
}, requestScope)));
|
|
759
1086
|
if (signal.aborted) {
|
|
@@ -770,11 +1097,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
770
1097
|
}, awaitExecutor(() => (admitted
|
|
771
1098
|
? admitted.execute(wrapGuestProgram(program), [provider])
|
|
772
1099
|
: executor.execute(wrapGuestProgram(program), [provider])).then((outcome) => {
|
|
773
|
-
executorLogs = sentSecrets.redact(outcome?.logs);
|
|
1100
|
+
executorLogs = sentSecrets.redact(outcome?.logs, "prose");
|
|
774
1101
|
return sentSecrets.redact(outcome);
|
|
775
1102
|
}, (err) => {
|
|
776
1103
|
if (err !== null && typeof err === "object" && "logs" in err) {
|
|
777
|
-
executorLogs = sentSecrets.redact(err.logs);
|
|
1104
|
+
executorLogs = sentSecrets.redact(err.logs, "prose");
|
|
778
1105
|
}
|
|
779
1106
|
throw sentSecrets.redact(err);
|
|
780
1107
|
}), signal, { watchdog, terminal: Deferred.await(terminal) }).pipe(Effect.ensuring(Effect.suspend(() => {
|
|
@@ -783,13 +1110,18 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
783
1110
|
return programWrites.drain();
|
|
784
1111
|
}))));
|
|
785
1112
|
});
|
|
786
|
-
const reported = {
|
|
1113
|
+
const reported = {
|
|
1114
|
+
emitted,
|
|
1115
|
+
diagnostics,
|
|
1116
|
+
program,
|
|
1117
|
+
guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets, config.trust, logger, maxChars, programWrites.hasSucceeded),
|
|
1118
|
+
};
|
|
787
1119
|
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
788
1120
|
// Releasing a QuickJS lease ends its child and rejects execute() with
|
|
789
1121
|
// the retained log prefix. Give that report one timer turn, just as
|
|
790
1122
|
// cancellation does in awaitExecutor; never wait on a Worker isolate.
|
|
791
1123
|
budgetFailure ? Effect.sleep(Duration.millis(1)).pipe(Effect.as(exit)) : Effect.succeed(exit)));
|
|
792
|
-
return Effect.map(Effect.
|
|
1124
|
+
return Effect.map(Effect.flatMap(exited, (exit) => {
|
|
793
1125
|
// A synchronous fire-and-forget burst can also settle its executor in
|
|
794
1126
|
// this turn. The host's terminal refusal always wins over that value.
|
|
795
1127
|
if (budgetFailure instanceof HostCallBudgetExceeded) {
|
|
@@ -799,12 +1131,12 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
799
1131
|
emitted,
|
|
800
1132
|
diagnostics,
|
|
801
1133
|
});
|
|
802
|
-
return failed;
|
|
1134
|
+
return Effect.succeed(failed);
|
|
803
1135
|
}
|
|
804
1136
|
if (Exit.isFailure(exit)) {
|
|
805
|
-
return failedRun(sentSecrets
|
|
1137
|
+
return Effect.succeed(failedRun(redactAgentOutput(sentSecrets, Cause.squash(exit.cause)), logger, reported));
|
|
806
1138
|
}
|
|
807
|
-
return finishedRun(sentSecrets
|
|
1139
|
+
return Effect.promise(() => finishedRun(redactAgentOutput(sentSecrets, exit.value), reported));
|
|
808
1140
|
}), (unfinished) => {
|
|
809
1141
|
// Every run response passes through write accounting.
|
|
810
1142
|
const response = programWrites.finish(unfinished);
|
|
@@ -874,7 +1206,7 @@ function failedRun(err, logger, { emitted, diagnostics, program }) {
|
|
|
874
1206
|
});
|
|
875
1207
|
}
|
|
876
1208
|
/** The response for an ExecuteResult: the program's error or its value. */
|
|
877
|
-
function finishedRun(outcome, { emitted, diagnostics, program }) {
|
|
1209
|
+
async function finishedRun(outcome, { emitted, diagnostics, program, guardResult }) {
|
|
878
1210
|
if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) {
|
|
879
1211
|
return failureResponse("Executor failed: expected an ExecuteResult object.", {
|
|
880
1212
|
emitted,
|
|
@@ -925,7 +1257,7 @@ function finishedRun(outcome, { emitted, diagnostics, program }) {
|
|
|
925
1257
|
// error path so captured logs survive instead of a raw SDK 500.
|
|
926
1258
|
let result;
|
|
927
1259
|
try {
|
|
928
|
-
result =
|
|
1260
|
+
result = await guardResult(outcome.result, MAX_EXECUTE_RESULT_CHARS - emitted.textChars);
|
|
929
1261
|
}
|
|
930
1262
|
catch (err) {
|
|
931
1263
|
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
@@ -1007,15 +1339,15 @@ function connectorInventory(connectors) {
|
|
|
1007
1339
|
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, trust) => `One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. ${trust === "trusted" ? "Trusted pool: programs may read and write; the host approves the program as a write." : "Read-only pool: programs read; writes use call_destructive_tool."} Limits: ${hostLimits.maxHostCalls} host calls, ${maxWrites} writes, ${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
|
|
1008
1340
|
${connectorInventory(connectors)}
|
|
1009
1341
|
|
|
1010
|
-
Use async () => { ... }, no arguments
|
|
1342
|
+
Use async () => { ... }, no arguments. No portable ambient capabilities. API:
|
|
1011
1343
|
\`\`\`ts
|
|
1012
1344
|
declare const connecta: {
|
|
1013
1345
|
${GUEST_API_DECLARATION}
|
|
1014
1346
|
};
|
|
1015
1347
|
\`\`\`
|
|
1016
|
-
|
|
1348
|
+
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.
|
|
1017
1349
|
|
|
1018
|
-
|
|
1350
|
+
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. skills({ name: "investigate" }): workflow; skills({ name: "usage" }): repair${connectorGuides ? ", guide handling" : ""}.`;
|
|
1019
1351
|
// Module scope, like the other five meta-tool inputs: its JSON Schema is
|
|
1020
1352
|
// derived once per process. Budgets and connectors vary by deployment and
|
|
1021
1353
|
// view, so they live in the description, never here.
|