@zackbart/connecta 0.24.2 → 0.24.4
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/AGENTS.md +18 -20
- package/CHANGELOG.md +205 -1
- package/README.md +5 -6
- package/dist/auth/bearer.js +2 -0
- package/dist/auth/downstream-oauth.d.ts +12 -1
- package/dist/auth/downstream-oauth.js +147 -35
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/call-admission.d.ts +4 -0
- package/dist/call-admission.js +26 -0
- package/dist/catalog-drift.js +9 -4
- package/dist/catalog-service.d.ts +2 -0
- package/dist/catalog-service.js +25 -8
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +246 -121
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +21 -3
- package/dist/connectors/guarded-fetch.d.ts +6 -2
- package/dist/connectors/guarded-fetch.js +61 -24
- package/dist/connectors/remote-mcp.js +92 -57
- package/dist/errors.d.ts +28 -3
- package/dist/errors.js +60 -1
- package/dist/execute.js +85 -23
- package/dist/executor-result.js +3 -1
- package/dist/executors/quickjs-child.js +5 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -0
- package/dist/executors/quickjs-runtime.d.ts +1 -1
- package/dist/executors/quickjs-runtime.js +38 -21
- package/dist/executors/quickjs.js +68 -27
- package/dist/index.d.ts +15 -1
- package/dist/index.js +36 -4
- package/dist/invocation.js +134 -93
- package/dist/mcp-result.js +3 -2
- package/dist/meta-tools.js +191 -36
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +22 -2
- package/dist/registry.js +101 -13
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +84 -13
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +1 -0
- package/dist/routes/shared.js +4 -4
- package/dist/server.js +15 -3
- package/dist/skills.js +6 -5
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.js +312 -34
- package/dist/storage/memory.js +12 -1
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/validate.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -165
- package/documentation/auth.md +199 -145
- package/documentation/code-mode.md +433 -316
- package/documentation/meta-tools.md +363 -355
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/documentation/call-admission.md +0 -142
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -389
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -722
- package/documentation/request-admission.md +0 -170
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -201
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -754
- package/documentation/vercel.md +0 -241
package/dist/execute.js
CHANGED
|
@@ -130,7 +130,8 @@ export class EmitCollector {
|
|
|
130
130
|
accept(raw) {
|
|
131
131
|
const block = requireEmittedBlock(raw);
|
|
132
132
|
if (this.blocks.length >= this.maxBlocks) {
|
|
133
|
-
|
|
133
|
+
// M5: distinguish exhausted block slots from the remaining byte budget.
|
|
134
|
+
throw guestFailure("budget_exceeded", `connecta.emit block-count budget exceeded: ${this.maxBlocks} block(s) maximum, 0 blocks remaining; ${this.maxBytes - this.bytes} of ${this.maxBytes} serialized bytes remaining`);
|
|
134
135
|
}
|
|
135
136
|
const size = diagnosticsEncoder.encode(JSON.stringify(block)).byteLength;
|
|
136
137
|
if (this.bytes + size > this.maxBytes) {
|
|
@@ -171,6 +172,48 @@ function guestFailureSecret() {
|
|
|
171
172
|
crypto.getRandomValues(words);
|
|
172
173
|
return Array.from(words, (word) => word.toString(16).padStart(8, "0")).join("");
|
|
173
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* E1/X11: bound the serialized frame, including JSON escapes, before transport.
|
|
177
|
+
* Keep optional recovery whole: clipping an address or echoed args could turn
|
|
178
|
+
* recovery into a different call. Oversized metadata is omitted instead.
|
|
179
|
+
*/
|
|
180
|
+
function boundedGuestFailure(failure) {
|
|
181
|
+
const boundText = (text, maxChars) => {
|
|
182
|
+
if (JSON.stringify(text).length <= maxChars)
|
|
183
|
+
return text;
|
|
184
|
+
let low = 0;
|
|
185
|
+
let high = Math.min(text.length, maxChars);
|
|
186
|
+
while (low < high) {
|
|
187
|
+
const mid = Math.ceil((low + high) / 2);
|
|
188
|
+
if (JSON.stringify(`${text.slice(0, mid)}…`).length <= maxChars)
|
|
189
|
+
low = mid;
|
|
190
|
+
else
|
|
191
|
+
high = mid - 1;
|
|
192
|
+
}
|
|
193
|
+
// Do not manufacture a lone surrogate when clipping a Unicode message.
|
|
194
|
+
if (low > 0 && /[\uD800-\uDBFF]/.test(text[low - 1]))
|
|
195
|
+
low--;
|
|
196
|
+
return `${text.slice(0, low)}…`;
|
|
197
|
+
};
|
|
198
|
+
let details = {
|
|
199
|
+
...failure.details,
|
|
200
|
+
code: boundText(failure.details.code, 128),
|
|
201
|
+
message: boundText(failure.details.message, 2_000),
|
|
202
|
+
};
|
|
203
|
+
// 3,700 plus the prefix and 32-character secret stays below QuickJS's
|
|
204
|
+
// 4,000-character error bound, with room for bridge-added context.
|
|
205
|
+
if (JSON.stringify(details).length > 3_700) {
|
|
206
|
+
details = {
|
|
207
|
+
code: details.code,
|
|
208
|
+
message: details.message,
|
|
209
|
+
retryable: details.retryable,
|
|
210
|
+
...(details.retryAfterMs !== undefined
|
|
211
|
+
? { retryAfterMs: details.retryAfterMs }
|
|
212
|
+
: {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return new InvocationFailure(details);
|
|
216
|
+
}
|
|
174
217
|
function framedGuestFailure(secret, failure) {
|
|
175
218
|
const framed = new InvocationFailure(failure.details);
|
|
176
219
|
framed.message =
|
|
@@ -191,7 +234,8 @@ function guestErrorPrelude(failureSecret) {
|
|
|
191
234
|
function ConnectaError(message, options) {
|
|
192
235
|
let details;
|
|
193
236
|
if (typeof message === "string" && startsWith(message, failurePrefix)) {
|
|
194
|
-
try { details = parse(slice(message, failurePrefix.length)); }
|
|
237
|
+
try { details = parse(slice(message, failurePrefix.length)); }
|
|
238
|
+
catch { message = "Invalid host failure frame."; }
|
|
195
239
|
}
|
|
196
240
|
const error = construct(
|
|
197
241
|
NativeError,
|
|
@@ -238,17 +282,18 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
238
282
|
const hostCallTimeoutMs = Math.max(1, Math.trunc(limits.hostCallTimeoutMs ?? EXECUTE_HOST_CALL_TIMEOUT_MS));
|
|
239
283
|
const failureSecret = guestFailureSecret();
|
|
240
284
|
let hostCalls = 0;
|
|
285
|
+
// L4/M7: discovery and invocation spend the same budget; emit does not.
|
|
286
|
+
const spendHostCall = () => {
|
|
287
|
+
hostCalls++;
|
|
288
|
+
if (hostCalls > maxHostCalls) {
|
|
289
|
+
throw guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
241
292
|
const invocationContext = () => ({
|
|
242
293
|
source: "execute_code",
|
|
243
294
|
timeoutMs: hostCallTimeoutMs,
|
|
244
295
|
...(limits.signal !== undefined ? { requestSignal: limits.signal } : {}),
|
|
245
296
|
unwrapResult: true,
|
|
246
|
-
beforeDispatch: () => {
|
|
247
|
-
hostCalls++;
|
|
248
|
-
if (hostCalls > maxHostCalls) {
|
|
249
|
-
throw guestFailure("budget_exceeded", `execute_code host-call budget exceeded (${maxHostCalls} calls maximum)`);
|
|
250
|
-
}
|
|
251
|
-
},
|
|
252
297
|
});
|
|
253
298
|
/**
|
|
254
299
|
* Discovery policy failures use the same thrown vocabulary as calls and
|
|
@@ -268,6 +313,7 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
268
313
|
const timedCatalog = async (operation, fn) => {
|
|
269
314
|
const started = Date.now();
|
|
270
315
|
try {
|
|
316
|
+
spendHostCall();
|
|
271
317
|
const result = await fn();
|
|
272
318
|
limits.diagnostics?.recordCatalog(operation, Date.now() - started, true, result);
|
|
273
319
|
return result;
|
|
@@ -278,6 +324,7 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
278
324
|
}
|
|
279
325
|
};
|
|
280
326
|
const callAddress = async (address, args) => {
|
|
327
|
+
spendHostCall();
|
|
281
328
|
const outcome = await invocation.invoke(String(address), args ?? {}, invocationContext());
|
|
282
329
|
limits.diagnostics?.recordCall(outcome);
|
|
283
330
|
if (!outcome.ok)
|
|
@@ -296,7 +343,9 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
296
343
|
}
|
|
297
344
|
limits.emitCollector.accept(block);
|
|
298
345
|
},
|
|
299
|
-
|
|
346
|
+
// Not `async`: a synchronous budget refusal must reject the same promise
|
|
347
|
+
// the transport wrapper awaits, or workerd reports an unhandled rejection.
|
|
348
|
+
search: (raw) => timedCatalog("search", () => typedDiscovery(async () => {
|
|
300
349
|
const args = (raw ?? {});
|
|
301
350
|
const result = flatSearchResult(await catalog.search({
|
|
302
351
|
...args,
|
|
@@ -305,7 +354,7 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
305
354
|
boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
|
|
306
355
|
return result;
|
|
307
356
|
})),
|
|
308
|
-
describe:
|
|
357
|
+
describe: (raw) => timedCatalog("describe", () => typedDiscovery(async () => {
|
|
309
358
|
const args = (raw ?? {});
|
|
310
359
|
const result = { tools: await catalog.describe(args) };
|
|
311
360
|
boundedDiscoveryText(result, 'Split the address list or use format: "compact".');
|
|
@@ -320,8 +369,9 @@ export async function buildSandboxProviders(registry, baseUrl, _logger, activity
|
|
|
320
369
|
}
|
|
321
370
|
catch (err) {
|
|
322
371
|
if (err instanceof InvocationFailure) {
|
|
323
|
-
const
|
|
324
|
-
|
|
372
|
+
const failure = boundedGuestFailure(err);
|
|
373
|
+
const framed = framedGuestFailure(failureSecret, failure);
|
|
374
|
+
limits.onInvocationFailure?.(failure);
|
|
325
375
|
throw framed;
|
|
326
376
|
}
|
|
327
377
|
throw err;
|
|
@@ -377,6 +427,8 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
377
427
|
signal: controller.signal,
|
|
378
428
|
onInvocationFailure: (failure) => {
|
|
379
429
|
invocationFailures.push(failure);
|
|
430
|
+
if (invocationFailures.length > 64)
|
|
431
|
+
invocationFailures.shift();
|
|
380
432
|
},
|
|
381
433
|
emitCollector: emitted,
|
|
382
434
|
...(diagnostics ? { diagnostics } : {}),
|
|
@@ -407,6 +459,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
407
459
|
}
|
|
408
460
|
}
|
|
409
461
|
catch (err) {
|
|
462
|
+
const logs = err !== null && typeof err === "object" && "logs" in err
|
|
463
|
+
? executeLogs(err.logs)
|
|
464
|
+
: undefined;
|
|
410
465
|
if (err instanceof ExecutorAdmissionError) {
|
|
411
466
|
if (err.code === "executor_overloaded") {
|
|
412
467
|
logger.warn("[connecta] execute_code admission rejected", {
|
|
@@ -415,6 +470,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
415
470
|
});
|
|
416
471
|
}
|
|
417
472
|
return failureResponse(err.message, {
|
|
473
|
+
logs,
|
|
418
474
|
emitted: err instanceof ExecutorExecutionError ? emitted : undefined,
|
|
419
475
|
diagnostics,
|
|
420
476
|
code: {
|
|
@@ -428,6 +484,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
428
484
|
});
|
|
429
485
|
}
|
|
430
486
|
return failureResponse(`Executor failed: ${msg(err)}`, {
|
|
487
|
+
logs,
|
|
431
488
|
emitted,
|
|
432
489
|
diagnostics,
|
|
433
490
|
code: "executor_failed",
|
|
@@ -440,10 +497,8 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
440
497
|
lease?.release();
|
|
441
498
|
options.signal?.removeEventListener("abort", forwardAbort);
|
|
442
499
|
}
|
|
443
|
-
const logs = outcome.logs
|
|
444
|
-
|
|
445
|
-
: undefined;
|
|
446
|
-
if (outcome.error) {
|
|
500
|
+
const logs = executeLogs(outcome.logs);
|
|
501
|
+
if (outcome.error !== undefined) {
|
|
447
502
|
// Executor bridges necessarily reduce thrown host errors to strings.
|
|
448
503
|
// Match that terminal string back to the request-local typed failure so
|
|
449
504
|
// an unhandled tool failure keeps the same structured contract as
|
|
@@ -457,8 +512,13 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
457
512
|
// rather than losing it to prose.
|
|
458
513
|
let invocationFailure;
|
|
459
514
|
for (const match of [
|
|
460
|
-
(candidate) =>
|
|
461
|
-
|
|
515
|
+
(candidate) => outcome.error !== "" &&
|
|
516
|
+
[candidate.message, guestFailureFrames.get(candidate)].includes(outcome.error),
|
|
517
|
+
(candidate) => [candidate.message, guestFailureFrames.get(candidate)].some((message) =>
|
|
518
|
+
// E6: empty or tiny prose cannot identify a wrapped failure.
|
|
519
|
+
message !== undefined &&
|
|
520
|
+
message.length >= 8 &&
|
|
521
|
+
outcome.error?.includes(message) === true),
|
|
462
522
|
]) {
|
|
463
523
|
for (let i = invocationFailures.length - 1; i >= 0; i--) {
|
|
464
524
|
const candidate = invocationFailures[i];
|
|
@@ -471,10 +531,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
471
531
|
break;
|
|
472
532
|
}
|
|
473
533
|
if (invocationFailure) {
|
|
474
|
-
//
|
|
475
|
-
// framed with bounded caller text (`boundedEchoText`) precisely so
|
|
476
|
-
// this path never needs one. Adding a cap here instead would leave the
|
|
477
|
-
// top-level surfaces, which have the same amplification, uncovered.
|
|
534
|
+
// E1/X11: return the same bounded details the guest received.
|
|
478
535
|
return failureResponse(invocationFailure.details.message, {
|
|
479
536
|
logs,
|
|
480
537
|
emitted,
|
|
@@ -482,7 +539,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
482
539
|
code: invocationFailure.details,
|
|
483
540
|
});
|
|
484
541
|
}
|
|
485
|
-
const message = `Error: ${outcome.error}`;
|
|
542
|
+
const message = `Error: ${outcome.error || "Execution failed without an error message."}`;
|
|
486
543
|
return failureResponse(message, {
|
|
487
544
|
logs,
|
|
488
545
|
emitted,
|
|
@@ -521,6 +578,11 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
521
578
|
return response;
|
|
522
579
|
};
|
|
523
580
|
}
|
|
581
|
+
function executeLogs(value) {
|
|
582
|
+
return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string")
|
|
583
|
+
? truncateExecuteText(value.join("\n"), MAX_EXECUTE_LOG_CHARS)
|
|
584
|
+
: undefined;
|
|
585
|
+
}
|
|
524
586
|
function failureResponse(message, options) {
|
|
525
587
|
const { logs, emitted, diagnostics, code } = options;
|
|
526
588
|
if (diagnostics || typeof code !== "string") {
|
package/dist/executor-result.js
CHANGED
|
@@ -57,7 +57,9 @@ export function prepareExecuteResultForTransport(outcome) {
|
|
|
57
57
|
// characters). Preserve that Executor-level shape; createExecuteTool applies
|
|
58
58
|
// the smaller model-facing 4k presentation cap in the parent.
|
|
59
59
|
const logs = outcome.logs && outcome.logs.length > 0 ? outcome.logs : undefined;
|
|
60
|
-
|
|
60
|
+
// An empty string is still a failure (E5): the parent renders a fixed
|
|
61
|
+
// message for it, but this transport must not turn it into a success.
|
|
62
|
+
if (outcome.error !== undefined) {
|
|
61
63
|
return {
|
|
62
64
|
result: undefined,
|
|
63
65
|
error: outcome.error,
|
|
@@ -59,7 +59,11 @@ async function run(payload) {
|
|
|
59
59
|
activeJobId = payload.id;
|
|
60
60
|
try {
|
|
61
61
|
const providers = payload.providers.map(({ name, prelude }) => provider(name, payload.id, prelude));
|
|
62
|
-
const raw = await executeQuickJs(payload.code, providers, payload.options)
|
|
62
|
+
const raw = await executeQuickJs(payload.code, providers, payload.options, (entry) => send({
|
|
63
|
+
type: "log",
|
|
64
|
+
jobId: payload.id,
|
|
65
|
+
payloadJson: stringifyBounded(entry, "QuickJS log entry"),
|
|
66
|
+
}));
|
|
63
67
|
const prepared = {
|
|
64
68
|
outcome: prepareExecuteResultForTransport(raw),
|
|
65
69
|
...(raw.timedOut ? { timedOut: true } : {}),
|
|
@@ -22,4 +22,4 @@ export interface QuickJsExecutionResult extends ExecuteResult {
|
|
|
22
22
|
* CPU accumulates only while evalCode/executePendingJobs is synchronously
|
|
23
23
|
* driving QuickJS, so a slow downstream does not consume the short CPU budget.
|
|
24
24
|
*/
|
|
25
|
-
export declare function executeQuickJs(code: string, providers: ExecutorProvider[], options: QuickJsRuntimeOptions): Promise<QuickJsExecutionResult>;
|
|
25
|
+
export declare function executeQuickJs(code: string, providers: ExecutorProvider[], options: QuickJsRuntimeOptions, onLog?: (entry: string) => void): Promise<QuickJsExecutionResult>;
|
|
@@ -80,30 +80,43 @@ export function normalizeCode(code) {
|
|
|
80
80
|
* and fail closed there.
|
|
81
81
|
*/
|
|
82
82
|
function setupScript(providers) {
|
|
83
|
+
// X11: raw replies carry an authenticated frame. Capture the bridge and
|
|
84
|
+
// codec before guest code runs, and expose only the decoded provider calls.
|
|
83
85
|
const lines = [
|
|
84
|
-
`
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
`(() => {
|
|
87
|
+
const call = globalThis.__call;
|
|
88
|
+
const log = globalThis.__log;
|
|
89
|
+
const parse = JSON.parse;
|
|
90
|
+
const stringify = JSON.stringify;
|
|
91
|
+
const freeze = Object.freeze;
|
|
92
|
+
const ProxyConstructor = Proxy;
|
|
93
|
+
const create = Object.create;
|
|
94
|
+
delete globalThis.__call;
|
|
95
|
+
delete globalThis.__log;
|
|
96
|
+
globalThis.console = (() => {
|
|
97
|
+
const fmt = (x) => { if (typeof x === "string") return x; try { return stringify(x); } catch { return String(x); } };
|
|
98
|
+
const emit = (...a) => log(a.map(fmt).join(" "));
|
|
99
|
+
return { log: emit, info: emit, warn: emit, error: emit, debug: emit };
|
|
100
|
+
})();
|
|
101
|
+
const invoke = async (ns, fn, args) => {
|
|
102
|
+
const r = parse(await call(ns, fn, stringify(args)));
|
|
103
|
+
if (!r.ok) throw new GuestError(r.error);
|
|
104
|
+
return r.value;
|
|
105
|
+
};
|
|
106
|
+
const namespace = (ns) => freeze(new ProxyConstructor(create(null), {
|
|
107
|
+
get: (_target, key) => typeof key === "string" ? (...args) => invoke(ns, key, args) : undefined
|
|
108
|
+
}));`,
|
|
97
109
|
];
|
|
98
110
|
for (const p of providers) {
|
|
99
111
|
const ns = JSON.stringify(p.name);
|
|
100
|
-
lines.push(`globalThis[${ns}] =
|
|
112
|
+
lines.push(`globalThis[${ns}] = namespace(${ns});`);
|
|
101
113
|
}
|
|
102
114
|
for (const p of providers) {
|
|
103
115
|
if (p.prelude)
|
|
104
116
|
lines.push(p.prelude);
|
|
105
117
|
}
|
|
106
|
-
lines.push(`
|
|
118
|
+
lines.push(`const GuestError = globalThis.Error;
|
|
119
|
+
})();`);
|
|
107
120
|
return lines.join("\n");
|
|
108
121
|
}
|
|
109
122
|
/** True when a dumped error is QuickJS's deadline-interrupt signal. */
|
|
@@ -145,7 +158,7 @@ function waitForHostOrDeadline(waitForSettle, remainingMs) {
|
|
|
145
158
|
});
|
|
146
159
|
});
|
|
147
160
|
}
|
|
148
|
-
function installBridge(ctx, providers, logs) {
|
|
161
|
+
function installBridge(ctx, providers, logs, onLog) {
|
|
149
162
|
const bridge = {
|
|
150
163
|
pending: 0,
|
|
151
164
|
aborted: false,
|
|
@@ -156,13 +169,17 @@ function installBridge(ctx, providers, logs) {
|
|
|
156
169
|
// Keep both the in-memory character budget and the twice-JSON-encoded
|
|
157
170
|
// transport budget. Reserve enough byte budget for whichever truncation
|
|
158
171
|
// marker ends the stream.
|
|
172
|
+
const captureLog = (entry) => {
|
|
173
|
+
logs.push(entry);
|
|
174
|
+
onLog?.(entry);
|
|
175
|
+
};
|
|
159
176
|
let logTotalChars = 0;
|
|
160
177
|
let logTotalTransportBytes = 0;
|
|
161
178
|
let logBudgetSpent = false;
|
|
162
179
|
const logFn = ctx.newFunction("__log", (h) => {
|
|
163
180
|
if (logs.length >= MAX_LOG_ENTRIES) {
|
|
164
181
|
if (logs.length === MAX_LOG_ENTRIES) {
|
|
165
|
-
|
|
182
|
+
captureLog(LOG_ENTRY_LIMIT_MARKER);
|
|
166
183
|
}
|
|
167
184
|
return;
|
|
168
185
|
}
|
|
@@ -178,11 +195,11 @@ function installBridge(ctx, providers, logs) {
|
|
|
178
195
|
if (logTotalChars + entry.length > MAX_LOG_TOTAL_CHARS ||
|
|
179
196
|
logTotalTransportBytes + entryTransportBytes >
|
|
180
197
|
MAX_QUICKJS_LOG_TRANSPORT_BYTES - MAX_LOG_MARKER_TRANSPORT_BYTES) {
|
|
181
|
-
|
|
198
|
+
captureLog(LOG_SIZE_LIMIT_MARKER);
|
|
182
199
|
logBudgetSpent = true;
|
|
183
200
|
return;
|
|
184
201
|
}
|
|
185
|
-
|
|
202
|
+
captureLog(entry);
|
|
186
203
|
logTotalChars += entry.length;
|
|
187
204
|
logTotalTransportBytes += entryTransportBytes;
|
|
188
205
|
});
|
|
@@ -248,7 +265,7 @@ function installBridge(ctx, providers, logs) {
|
|
|
248
265
|
* CPU accumulates only while evalCode/executePendingJobs is synchronously
|
|
249
266
|
* driving QuickJS, so a slow downstream does not consume the short CPU budget.
|
|
250
267
|
*/
|
|
251
|
-
export async function executeQuickJs(code, providers, options) {
|
|
268
|
+
export async function executeQuickJs(code, providers, options, onLog) {
|
|
252
269
|
const { timeoutMs, cpuTimeMs, memoryLimitBytes, maxStackSizeBytes } = options;
|
|
253
270
|
const QuickJS = await getQuickJS();
|
|
254
271
|
const ctx = QuickJS.newContext();
|
|
@@ -282,7 +299,7 @@ export async function executeQuickJs(code, providers, options) {
|
|
|
282
299
|
return wallInterrupted || cpuInterrupted;
|
|
283
300
|
});
|
|
284
301
|
const logs = [];
|
|
285
|
-
const bridge = installBridge(ctx, providers, logs);
|
|
302
|
+
const bridge = installBridge(ctx, providers, logs, onLog);
|
|
286
303
|
const finish = (r) => {
|
|
287
304
|
bridge.aborted = true;
|
|
288
305
|
// Outstanding host calls still hold deferred-promise handles; their
|
|
@@ -7,6 +7,7 @@ import { existsSync } from "node:fs";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { AdmissionController, ExecutorAdmissionError, ExecutorExecutionError, } from "../executor-admission.js";
|
|
9
9
|
import { msg } from "../errors.js";
|
|
10
|
+
import { MAX_EXECUTE_LOG_CHARS } from "../executor-result.js";
|
|
10
11
|
import { hostCallLabel, MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
|
|
11
12
|
export { normalizeCode } from "./quickjs-runtime.js";
|
|
12
13
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -55,9 +56,14 @@ function nonNegativeWhole(value, fallback, name) {
|
|
|
55
56
|
return resolved;
|
|
56
57
|
}
|
|
57
58
|
function errorPayload(error) {
|
|
59
|
+
// X11: a partial authenticated frame exposes its secret instead of decoding.
|
|
60
|
+
// Refuse it whole if a host ever bypasses execute.ts's framing bound.
|
|
61
|
+
const message = error.length > MAX_ERROR_CHARS && error.includes("\u001econnecta-error:")
|
|
62
|
+
? `Host failure exceeded the ${MAX_ERROR_CHARS}-character bridge limit.`
|
|
63
|
+
: error.slice(0, MAX_ERROR_CHARS);
|
|
58
64
|
return JSON.stringify({
|
|
59
65
|
ok: false,
|
|
60
|
-
error:
|
|
66
|
+
error: message,
|
|
61
67
|
});
|
|
62
68
|
}
|
|
63
69
|
function delay(ms, signal) {
|
|
@@ -217,14 +223,13 @@ class QuickJsChildPool {
|
|
|
217
223
|
child.channel?.ref();
|
|
218
224
|
return new Promise((resolve, reject) => {
|
|
219
225
|
const wallTimer = setTimeout(() => {
|
|
220
|
-
this.
|
|
221
|
-
result: undefined,
|
|
222
|
-
error: `QuickJS child exceeded the ${this.runtimeOptions.timeoutMs}ms wall budget and was terminated.`,
|
|
223
|
-
});
|
|
226
|
+
this.rejectActive(slot, new Error(`QuickJS child exceeded the ${this.runtimeOptions.timeoutMs}ms wall budget and was terminated.`));
|
|
224
227
|
this.recycle(slot);
|
|
225
228
|
}, this.runtimeOptions.timeoutMs + CHILD_EXIT_GRACE_MS);
|
|
226
229
|
const active = {
|
|
227
230
|
id,
|
|
231
|
+
logs: [],
|
|
232
|
+
logChars: 0,
|
|
228
233
|
providers: providerMap,
|
|
229
234
|
resolve,
|
|
230
235
|
reject,
|
|
@@ -328,7 +333,7 @@ class QuickJsChildPool {
|
|
|
328
333
|
return;
|
|
329
334
|
this.recordCrash(slot);
|
|
330
335
|
const error = childExitError(`QuickJS child exited unexpectedly (${exitDescription}).`, stderrTail);
|
|
331
|
-
this.
|
|
336
|
+
this.rejectActive(slot, error);
|
|
332
337
|
});
|
|
333
338
|
child.unref();
|
|
334
339
|
child.channel?.unref();
|
|
@@ -350,19 +355,39 @@ class QuickJsChildPool {
|
|
|
350
355
|
return;
|
|
351
356
|
if (typeof message.payloadJson !== "string")
|
|
352
357
|
return;
|
|
358
|
+
if (message.type === "log") {
|
|
359
|
+
if (message.jobId !== active.id)
|
|
360
|
+
return;
|
|
361
|
+
// Keep one extra character so the presentation layer can signal loss.
|
|
362
|
+
// The final reply owns complete successful logs; this prefix survives
|
|
363
|
+
// only when that reply cannot arrive. Bound even a compromised child.
|
|
364
|
+
if (active.logChars >= MAX_EXECUTE_LOG_CHARS + 1)
|
|
365
|
+
return;
|
|
366
|
+
if (serializedBytes(message.payloadJson) > MAX_QUICKJS_IPC_BYTES)
|
|
367
|
+
return;
|
|
368
|
+
try {
|
|
369
|
+
stringifyBounded(message, "QuickJS log IPC envelope");
|
|
370
|
+
const entry = JSON.parse(message.payloadJson);
|
|
371
|
+
if (typeof entry !== "string")
|
|
372
|
+
return;
|
|
373
|
+
const separator = active.logs.length > 0 ? 1 : 0;
|
|
374
|
+
const retained = entry.slice(0, MAX_EXECUTE_LOG_CHARS + 1 - active.logChars - separator);
|
|
375
|
+
active.logs.push(retained);
|
|
376
|
+
active.logChars += separator + retained.length;
|
|
377
|
+
}
|
|
378
|
+
catch { /* Malformed log messages carry no output. */ }
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
353
381
|
if (message.type === "host-call") {
|
|
354
382
|
if (message.jobId !== active.id)
|
|
355
383
|
return;
|
|
356
|
-
await this.handleHostCall(child, active, message);
|
|
384
|
+
await this.handleHostCall(slot, child, active, message);
|
|
357
385
|
return;
|
|
358
386
|
}
|
|
359
387
|
if (message.type !== "result" || message.jobId !== active.id)
|
|
360
388
|
return;
|
|
361
389
|
if (serializedBytes(message.payloadJson) > MAX_QUICKJS_IPC_BYTES) {
|
|
362
|
-
this.
|
|
363
|
-
result: undefined,
|
|
364
|
-
error: "QuickJS execution result exceeded the IPC limit.",
|
|
365
|
-
});
|
|
390
|
+
this.rejectActive(slot, new Error("QuickJS execution result exceeded the IPC limit."));
|
|
366
391
|
this.recycle(slot);
|
|
367
392
|
return;
|
|
368
393
|
}
|
|
@@ -380,14 +405,11 @@ class QuickJsChildPool {
|
|
|
380
405
|
}
|
|
381
406
|
}
|
|
382
407
|
catch (err) {
|
|
383
|
-
this.
|
|
384
|
-
result: undefined,
|
|
385
|
-
error: `QuickJS child returned an invalid result: ${msg(err)}`,
|
|
386
|
-
});
|
|
408
|
+
this.rejectActive(slot, new Error(`QuickJS child returned an invalid result: ${msg(err)}`));
|
|
387
409
|
this.recycle(slot);
|
|
388
410
|
}
|
|
389
411
|
}
|
|
390
|
-
async handleHostCall(child, active, message) {
|
|
412
|
+
async handleHostCall(slot, child, active, message) {
|
|
391
413
|
let payloadJson;
|
|
392
414
|
try {
|
|
393
415
|
if (serializedBytes(message.payloadJson) >
|
|
@@ -422,35 +444,54 @@ class QuickJsChildPool {
|
|
|
422
444
|
catch (err) {
|
|
423
445
|
payloadJson = errorPayload(msg(err));
|
|
424
446
|
}
|
|
425
|
-
if (!child.connected)
|
|
447
|
+
if (!child.connected || slot.active !== active)
|
|
426
448
|
return;
|
|
449
|
+
const response = {
|
|
450
|
+
type: "host-result",
|
|
451
|
+
jobId: message.jobId,
|
|
452
|
+
callId: message.callId,
|
|
453
|
+
payloadJson,
|
|
454
|
+
};
|
|
427
455
|
try {
|
|
428
|
-
const response = {
|
|
429
|
-
type: "host-result",
|
|
430
|
-
jobId: message.jobId,
|
|
431
|
-
callId: message.callId,
|
|
432
|
-
payloadJson,
|
|
433
|
-
};
|
|
434
456
|
stringifyBounded(response, "QuickJS host-result IPC envelope");
|
|
435
|
-
child.send(response);
|
|
436
457
|
}
|
|
437
458
|
catch {
|
|
438
|
-
//
|
|
439
|
-
|
|
459
|
+
// L6/X10: settle the rejected call even if the outer encoding overflows.
|
|
460
|
+
response.payloadJson = errorPayload(`QuickJS host-result IPC envelope could not be serialized within the ${MAX_QUICKJS_IPC_BYTES}-byte IPC limit.`);
|
|
461
|
+
}
|
|
462
|
+
const failedSend = (error) => {
|
|
463
|
+
if (!error || slot.active !== active)
|
|
464
|
+
return;
|
|
465
|
+
this.rejectActive(slot, error);
|
|
466
|
+
this.recycle(slot);
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
stringifyBounded(response, "QuickJS host-result IPC envelope");
|
|
470
|
+
child.send(response, failedSend);
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
failedSend(err instanceof Error ? err : new Error(msg(err)));
|
|
440
474
|
}
|
|
441
475
|
}
|
|
442
476
|
resolveActive(slot, outcome) {
|
|
443
477
|
const active = slot.active;
|
|
444
478
|
if (!active)
|
|
445
479
|
return;
|
|
480
|
+
// Never concatenate the stream with the final reply: those entries are
|
|
481
|
+
// the same logs. A normal reply retains its existing full log contract.
|
|
482
|
+
const resolved = outcome.logs === undefined && active.logs.length > 0
|
|
483
|
+
? { ...outcome, logs: active.logs }
|
|
484
|
+
: outcome;
|
|
446
485
|
this.clearActive(slot, active);
|
|
447
|
-
active.resolve(
|
|
486
|
+
active.resolve(resolved);
|
|
448
487
|
}
|
|
449
488
|
rejectActive(slot, error) {
|
|
450
489
|
const active = slot.active;
|
|
451
490
|
if (!active)
|
|
452
491
|
return;
|
|
453
492
|
this.clearActive(slot, active);
|
|
493
|
+
if (active.logs.length > 0)
|
|
494
|
+
Object.assign(error, { logs: active.logs });
|
|
454
495
|
active.reject(error);
|
|
455
496
|
}
|
|
456
497
|
clearActive(slot, active) {
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,13 @@ export interface ConnectaCallsConfig {
|
|
|
51
51
|
*/
|
|
52
52
|
maxResultBytes?: number;
|
|
53
53
|
}
|
|
54
|
+
/** Runtime-wide bounds for transient direct-call result paging. */
|
|
55
|
+
export interface ConnectaResultsConfig {
|
|
56
|
+
/** Stored bytes, including the paging envelope. Default 8 MiB. Zero disables stashing. */
|
|
57
|
+
maxStashBytes?: number;
|
|
58
|
+
/** Stored or in-flight entries. Default 64. Zero disables stashing. */
|
|
59
|
+
maxStashEntries?: number;
|
|
60
|
+
}
|
|
54
61
|
/** Budgets for execute_code programs: host calls and rich output (`connecta.emit`). */
|
|
55
62
|
export interface ConnectaExecuteConfig {
|
|
56
63
|
/**
|
|
@@ -149,6 +156,11 @@ export interface ConnectaConfig {
|
|
|
149
156
|
* HTTPS URL also redirects matching inbound HTTP requests to HTTPS.
|
|
150
157
|
*/
|
|
151
158
|
publicUrl?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Exact browser MCP origins, or "*". Defaults to publicUrl's origin and
|
|
161
|
+
* HTTP(S) loopback origins at any port. Originless clients are admitted.
|
|
162
|
+
*/
|
|
163
|
+
allowedOrigins?: readonly string[] | "*";
|
|
152
164
|
/** Optional recorder and reader, created by activityHistory() from /activity. */
|
|
153
165
|
activity?: ActivityModule;
|
|
154
166
|
/** Replaceable owner-partitioned credential storage. Omit for config-owned secrets. */
|
|
@@ -159,6 +171,8 @@ export interface ConnectaConfig {
|
|
|
159
171
|
discovery?: ConnectaDiscoveryConfig;
|
|
160
172
|
/** Deployment-wide call deadlines and result paging threshold. */
|
|
161
173
|
calls?: ConnectaCallsConfig;
|
|
174
|
+
/** Runtime-wide transient result stash limits, shared across subjects. */
|
|
175
|
+
results?: ConnectaResultsConfig;
|
|
162
176
|
/** Budgets for the `connecta.emit` rich-output channel in execute_code. */
|
|
163
177
|
execute?: ConnectaExecuteConfig;
|
|
164
178
|
/** Bounded MCP and fallback code-mode admission. */
|
|
@@ -208,5 +222,5 @@ export { CONNECTA_VERSION } from "./version.js";
|
|
|
208
222
|
export type { Registry } from "./registry.js";
|
|
209
223
|
export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
|
|
210
224
|
export type { ApiOptions, ApiTool } from "./connectors/api.js";
|
|
211
|
-
export type { CatalogDriftCounts, CatalogDriftReport, ConnectaBranding, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
|
|
225
|
+
export type { CatalogDriftCounts, CatalogDriftReport, ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
|
|
212
226
|
export type { ActivityActor, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, CatalogDriftActivityEvent, ToolCallActivityEvent, } from "./activity.js";
|