@zackbart/connecta 0.28.1 → 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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/argument-redaction.js +230 -0
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +409 -225
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +633 -0
- package/dist/config.js +508 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +194 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -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/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +336 -56
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +146 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +126 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +140 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -14,9 +14,7 @@ export class ExecutorAdmissionError extends Error {
|
|
|
14
14
|
this.code = code;
|
|
15
15
|
this.name = "ExecutorAdmissionError";
|
|
16
16
|
this.retryable = code === "executor_overloaded";
|
|
17
|
-
if (opts.retryAfterMs !== undefined &&
|
|
18
|
-
Number.isFinite(opts.retryAfterMs) &&
|
|
19
|
-
opts.retryAfterMs >= 0) {
|
|
17
|
+
if (opts.retryAfterMs !== undefined && Number.isFinite(opts.retryAfterMs) && opts.retryAfterMs >= 0) {
|
|
20
18
|
this.retryAfterMs = Math.trunc(opts.retryAfterMs);
|
|
21
19
|
}
|
|
22
20
|
}
|
|
@@ -72,9 +70,8 @@ export class AdmissionController {
|
|
|
72
70
|
this.maxQueueSize = nonNegativeWhole(options.maxQueueSize, "maxQueueSize");
|
|
73
71
|
this.queueTimeoutMs = positiveWhole(options.queueTimeoutMs, "queueTimeoutMs");
|
|
74
72
|
this.retryAfterMs = nonNegativeWhole(options.retryAfterMs ?? this.queueTimeoutMs, "retryAfterMs");
|
|
75
|
-
this.maxDurationMs =
|
|
76
|
-
? undefined
|
|
77
|
-
: positiveWhole(options.maxDurationMs, "maxDurationMs");
|
|
73
|
+
this.maxDurationMs =
|
|
74
|
+
options.maxDurationMs === undefined ? undefined : positiveWhole(options.maxDurationMs, "maxDurationMs");
|
|
78
75
|
if (this.maxDurationMs !== undefined && this.maxDurationMs > 2_147_483_647) {
|
|
79
76
|
throw new TypeError("maxDurationMs must be at most 2,147,483,647 milliseconds.");
|
|
80
77
|
}
|
|
@@ -129,9 +126,7 @@ export class AdmissionController {
|
|
|
129
126
|
}
|
|
130
127
|
makeLease(waitMs) {
|
|
131
128
|
const id = Symbol("admission lease");
|
|
132
|
-
const record = this.maxDurationMs !== undefined
|
|
133
|
-
? { expiresAt: Date.now() + this.maxDurationMs }
|
|
134
|
-
: {};
|
|
129
|
+
const record = this.maxDurationMs !== undefined ? { expiresAt: Date.now() + this.maxDurationMs } : {};
|
|
135
130
|
this.leases.set(id, record);
|
|
136
131
|
return {
|
|
137
132
|
waitMs,
|
|
@@ -202,9 +197,7 @@ export class AdmissionController {
|
|
|
202
197
|
}
|
|
203
198
|
// A queued request's own timer reaps the earliest admitted lease. A
|
|
204
199
|
// fallback check covers a briefly empty lease map after a handoff.
|
|
205
|
-
return Number.isFinite(next)
|
|
206
|
-
? Math.max(1, next - Date.now())
|
|
207
|
-
: 250;
|
|
200
|
+
return Number.isFinite(next) ? Math.max(1, next - Date.now()) : 250;
|
|
208
201
|
}
|
|
209
202
|
remove(waiter) {
|
|
210
203
|
const index = this.waiters.indexOf(waiter);
|
|
@@ -233,7 +226,7 @@ export class AdmissionController {
|
|
|
233
226
|
// static block is the one place outside an instance method that may read
|
|
234
227
|
// this bookkeeping while emitting nothing there.
|
|
235
228
|
static {
|
|
236
|
-
provideAdmissionProgram((controller, signal) => Effect.suspend(() => {
|
|
229
|
+
provideAdmissionProgram((controller, signal, wait) => Effect.suspend(() => {
|
|
237
230
|
controller.reapExpired();
|
|
238
231
|
if (controller.closed) {
|
|
239
232
|
controller.closedTotal++;
|
|
@@ -248,6 +241,10 @@ export class AdmissionController {
|
|
|
248
241
|
controller.admittedTotal++;
|
|
249
242
|
return Effect.succeed(controller.makeLease(0));
|
|
250
243
|
}
|
|
244
|
+
if (!wait) {
|
|
245
|
+
controller.rejectedTotal++;
|
|
246
|
+
return Effect.fail(controller.overloaded("No executor slot is free for nested execution. Invoke the operation directly, after this program ends."));
|
|
247
|
+
}
|
|
251
248
|
if (controller.waiters.length >= controller.maxQueueSize) {
|
|
252
249
|
controller.rejectedTotal++;
|
|
253
250
|
return Effect.fail(controller.overloaded("Executor queue is full."));
|
|
@@ -284,8 +281,7 @@ export class AdmissionController {
|
|
|
284
281
|
}
|
|
285
282
|
/** Preserve the one-method Workers seam while recognizing richer Node executors. */
|
|
286
283
|
export function isAdmittingExecutor(executor) {
|
|
287
|
-
return
|
|
288
|
-
typeof executor.acquire === "function");
|
|
284
|
+
return "acquire" in executor && typeof executor.acquire === "function";
|
|
289
285
|
}
|
|
290
286
|
/** Terminal- and JSON-safe upper bound for an executor's self-reported name. */
|
|
291
287
|
const MAX_EXECUTOR_NAME_LENGTH = 40;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AdmittingExecutor, Executor } from "./types.js";
|
|
2
|
+
type ExecutorLifecycle = "leased" | "self-managed";
|
|
3
|
+
export declare function brandExecutor<T extends Executor>(executor: T, lifecycle: ExecutorLifecycle): T;
|
|
4
|
+
export declare function assertExecutor(executor: Executor): void;
|
|
5
|
+
/** Explicitly accept responsibility for a custom sandbox's run resources. */
|
|
6
|
+
export interface CustomExecutorOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The custom executor owns guest termination and resource cleanup, including
|
|
9
|
+
* budget exhaustion, cancellation, and deadlines. Connecta cannot add these
|
|
10
|
+
* guarantees to an executor that exposes only execute().
|
|
11
|
+
*/
|
|
12
|
+
lifecycle: "self-managed";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Opt a custom sandbox into createConnecta without changing the original object.
|
|
16
|
+
* Delegated methods retain their original receiver, including private fields.
|
|
17
|
+
*/
|
|
18
|
+
export declare function customExecutor(executor: AdmittingExecutor, options: CustomExecutorOptions): AdmittingExecutor;
|
|
19
|
+
export declare function customExecutor(executor: Executor, options: CustomExecutorOptions): Executor;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { executorName, isAdmittingExecutor } from "./executor-admission.js";
|
|
2
|
+
// Symbol.for and a data descriptor survive duplicate packages and minification.
|
|
3
|
+
// This is a construction contract, not a security boundary against host config.
|
|
4
|
+
const EXECUTOR_BRAND = Symbol.for("connecta.executor");
|
|
5
|
+
export function brandExecutor(executor, lifecycle) {
|
|
6
|
+
Object.defineProperty(executor, EXECUTOR_BRAND, {
|
|
7
|
+
value: Object.freeze({ version: 1, lifecycle }),
|
|
8
|
+
enumerable: false,
|
|
9
|
+
writable: false,
|
|
10
|
+
configurable: false,
|
|
11
|
+
});
|
|
12
|
+
return executor;
|
|
13
|
+
}
|
|
14
|
+
export function assertExecutor(executor) {
|
|
15
|
+
const descriptor = Object.getOwnPropertyDescriptor(executor, EXECUTOR_BRAND);
|
|
16
|
+
const brand = descriptor?.value;
|
|
17
|
+
if (typeof executor.execute === "function" &&
|
|
18
|
+
descriptor?.enumerable === false &&
|
|
19
|
+
brand?.version === 1 &&
|
|
20
|
+
(brand.lifecycle === "leased" || brand.lifecycle === "self-managed"))
|
|
21
|
+
return;
|
|
22
|
+
throw new Error("ConnectaConfig.executor must declare its lifecycle. Unbranded executors, including " +
|
|
23
|
+
"upstream DynamicWorkerExecutor, are unsupported because request-owned resources " +
|
|
24
|
+
"must be released when a run ends. Use one of these options: " +
|
|
25
|
+
'import { workerExecutor } from "@zackbart/connecta/worker"; ' +
|
|
26
|
+
"executor: workerExecutor({ loader: env.LOADER }); " +
|
|
27
|
+
'import { quickJsExecutor } from "@zackbart/connecta/quickjs"; executor: quickJsExecutor(); ' +
|
|
28
|
+
'or import { customExecutor } from "@zackbart/connecta"; ' +
|
|
29
|
+
'executor: customExecutor(myExecutor, { lifecycle: "self-managed" }).');
|
|
30
|
+
}
|
|
31
|
+
export function customExecutor(executor, options) {
|
|
32
|
+
if (options?.lifecycle !== "self-managed" || typeof executor?.execute !== "function") {
|
|
33
|
+
throw new Error('customExecutor requires an executor and { lifecycle: "self-managed" }.');
|
|
34
|
+
}
|
|
35
|
+
const name = executorName(executor);
|
|
36
|
+
const wrapper = {
|
|
37
|
+
...(name ? { name } : {}),
|
|
38
|
+
execute: executor.execute.bind(executor),
|
|
39
|
+
...(executor.close ? { close: executor.close.bind(executor) } : {}),
|
|
40
|
+
...(isAdmittingExecutor(executor)
|
|
41
|
+
? {
|
|
42
|
+
acquire: executor.acquire.bind(executor),
|
|
43
|
+
...(executor.admissionSnapshot ? { admissionSnapshot: executor.admissionSnapshot.bind(executor) } : {}),
|
|
44
|
+
}
|
|
45
|
+
: {}),
|
|
46
|
+
};
|
|
47
|
+
return brandExecutor(wrapper, "self-managed");
|
|
48
|
+
}
|
package/dist/executor-result.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { msg } from "./errors.js";
|
|
2
2
|
/** ~6k tokens. Sandbox code should filter data down before returning. */
|
|
3
|
-
const MAX_EXECUTE_RESULT_CHARS = 24_000;
|
|
3
|
+
export const MAX_EXECUTE_RESULT_CHARS = 24_000;
|
|
4
4
|
export const MAX_EXECUTE_LOG_CHARS = 4_000;
|
|
5
5
|
export function serializeResultText(value) {
|
|
6
6
|
const serialized = JSON.stringify(value);
|
|
@@ -18,30 +18,45 @@ const TRUNCATION_HINT = "filter/map/slice data inside execute_code and return on
|
|
|
18
18
|
* of what the program returned, and truncation happens exactly once no matter
|
|
19
19
|
* how many hops the value takes.
|
|
20
20
|
*/
|
|
21
|
-
function truncationEnvelope(text) {
|
|
21
|
+
function truncationEnvelope(text, maxChars, totalChars = text.length, hint = TRUNCATION_HINT) {
|
|
22
22
|
const base = {
|
|
23
23
|
truncated: true,
|
|
24
24
|
preview: "",
|
|
25
|
-
totalChars
|
|
26
|
-
hint
|
|
25
|
+
totalChars,
|
|
26
|
+
hint,
|
|
27
27
|
};
|
|
28
|
-
let budget = Math.max(0,
|
|
28
|
+
let budget = Math.max(0, maxChars - JSON.stringify(base).length);
|
|
29
29
|
for (let attempt = 0; attempt < 8 && budget > 0; attempt += 1) {
|
|
30
30
|
const candidate = { ...base, preview: text.slice(0, budget) };
|
|
31
31
|
const size = JSON.stringify(candidate).length;
|
|
32
|
-
if (size <=
|
|
32
|
+
if (size <= maxChars)
|
|
33
33
|
return candidate;
|
|
34
34
|
// Every character costs at least one serialized character, so scaling by
|
|
35
35
|
// the overshoot ratio (minus a step) strictly shrinks the budget.
|
|
36
|
-
budget = Math.max(0, Math.floor(budget * (
|
|
36
|
+
budget = Math.max(0, Math.floor(budget * (maxChars / size)) - 8);
|
|
37
37
|
}
|
|
38
38
|
return { ...base, preview: text.slice(0, budget) };
|
|
39
39
|
}
|
|
40
|
-
export function guardExecuteResultValue(value) {
|
|
40
|
+
export function guardExecuteResultValue(value, maxChars = MAX_EXECUTE_RESULT_CHARS, hintPrefix) {
|
|
41
|
+
const hint = hintPrefix ? `${hintPrefix} ${TRUNCATION_HINT}` : TRUNCATION_HINT;
|
|
41
42
|
const text = serializeResultText(value);
|
|
42
|
-
if (text.length <=
|
|
43
|
+
if (text.length <= maxChars)
|
|
43
44
|
return value;
|
|
44
|
-
|
|
45
|
+
// A custom executor may already have guarded this value at a larger cap.
|
|
46
|
+
// Shrink its preview once more without nesting notices or losing original size.
|
|
47
|
+
if (value !== null && typeof value === "object") {
|
|
48
|
+
const prior = value;
|
|
49
|
+
if (prior.truncated === true &&
|
|
50
|
+
prior.hint === TRUNCATION_HINT &&
|
|
51
|
+
typeof prior.preview === "string" &&
|
|
52
|
+
typeof prior.totalChars === "number" &&
|
|
53
|
+
Number.isSafeInteger(prior.totalChars) &&
|
|
54
|
+
prior.totalChars >= prior.preview.length &&
|
|
55
|
+
Object.keys(value).length === 4) {
|
|
56
|
+
return truncationEnvelope(prior.preview, maxChars, prior.totalChars, hint);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return truncationEnvelope(text, maxChars, text.length, hint);
|
|
45
60
|
}
|
|
46
61
|
export function truncateExecuteText(text, max) {
|
|
47
62
|
if (text.length <= max)
|
|
@@ -49,8 +64,8 @@ export function truncateExecuteText(text, max) {
|
|
|
49
64
|
return `${text.slice(0, max)}\n--- TRUNCATED (${text.length} chars total) — filter/map/slice data inside your code and return only what you need ---`;
|
|
50
65
|
}
|
|
51
66
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
67
|
+
* Validate serialization before IPC without discarding the program return.
|
|
68
|
+
* The host applies the presentation cap after it can stash the full value.
|
|
54
69
|
*/
|
|
55
70
|
export function prepareExecuteResultForTransport(outcome) {
|
|
56
71
|
// QuickJS already bounds captured logs at source (entries + cumulative
|
|
@@ -63,12 +78,14 @@ export function prepareExecuteResultForTransport(outcome) {
|
|
|
63
78
|
return {
|
|
64
79
|
result: undefined,
|
|
65
80
|
error: outcome.error,
|
|
81
|
+
...(outcome.failure ? { failure: outcome.failure } : {}),
|
|
66
82
|
...(logs ? { logs } : {}),
|
|
67
83
|
};
|
|
68
84
|
}
|
|
69
85
|
try {
|
|
86
|
+
serializeResultText(outcome.result);
|
|
70
87
|
return {
|
|
71
|
-
result:
|
|
88
|
+
result: outcome.result,
|
|
72
89
|
...(logs ? { logs } : {}),
|
|
73
90
|
};
|
|
74
91
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { prepareExecuteResultForTransport } from "../executor-result.js";
|
|
2
|
+
import { InvocationFailure } from "../invocation.js";
|
|
2
3
|
import { msg } from "../errors.js";
|
|
3
|
-
import { MAX_QUICKJS_IPC_BYTES, MAX_QUICKJS_HOST_RPC_BYTES, serializedBytes, stringifyBounded, } from "./quickjs-protocol.js";
|
|
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";
|
|
4
5
|
import { executeQuickJs, prepareQuickJs } from "./quickjs-runtime.js";
|
|
5
6
|
let activeJobId;
|
|
6
7
|
let nextCallId = 1;
|
|
@@ -38,9 +39,7 @@ function provider(name, jobId, prelude) {
|
|
|
38
39
|
return result;
|
|
39
40
|
};
|
|
40
41
|
},
|
|
41
|
-
getOwnPropertyDescriptor: (_target, key) => typeof key === "string"
|
|
42
|
-
? { configurable: true, enumerable: false }
|
|
43
|
-
: undefined,
|
|
42
|
+
getOwnPropertyDescriptor: (_target, key) => typeof key === "string" ? { configurable: true, enumerable: false } : undefined,
|
|
44
43
|
});
|
|
45
44
|
return { name, fns: functions, ...(prelude ? { prelude } : {}) };
|
|
46
45
|
}
|
|
@@ -52,6 +51,13 @@ function fixedTransportFailure(message) {
|
|
|
52
51
|
},
|
|
53
52
|
};
|
|
54
53
|
}
|
|
54
|
+
function sendResult(jobId, payloadJson) {
|
|
55
|
+
for (let offset = 0; offset + QUICKJS_RESULT_CHUNK_CHARS < payloadJson.length; offset += QUICKJS_RESULT_CHUNK_CHARS) {
|
|
56
|
+
send({ type: "result-chunk", jobId, payloadJson: payloadJson.slice(offset, offset + QUICKJS_RESULT_CHUNK_CHARS) });
|
|
57
|
+
}
|
|
58
|
+
const tail = Math.floor((payloadJson.length - 1) / QUICKJS_RESULT_CHUNK_CHARS) * QUICKJS_RESULT_CHUNK_CHARS;
|
|
59
|
+
send({ type: "result", jobId, payloadJson: payloadJson.slice(tail) });
|
|
60
|
+
}
|
|
55
61
|
async function run(payload) {
|
|
56
62
|
if (activeJobId !== undefined) {
|
|
57
63
|
throw new Error("QuickJS child received overlapping executions.");
|
|
@@ -70,12 +76,12 @@ async function run(payload) {
|
|
|
70
76
|
};
|
|
71
77
|
let payloadJson;
|
|
72
78
|
try {
|
|
73
|
-
payloadJson = stringifyBounded(prepared, "QuickJS execution result");
|
|
79
|
+
payloadJson = stringifyBounded(prepared, "QuickJS execution result", MAX_QUICKJS_RESULT_BYTES);
|
|
74
80
|
}
|
|
75
81
|
catch (err) {
|
|
76
|
-
payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS execution result exceeded the ${
|
|
82
|
+
payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS execution result exceeded the ${MAX_QUICKJS_RESULT_BYTES}-byte aggregate transport limit: ${msg(err)}`));
|
|
77
83
|
}
|
|
78
|
-
|
|
84
|
+
sendResult(payload.id, payloadJson);
|
|
79
85
|
}
|
|
80
86
|
finally {
|
|
81
87
|
activeJobId = undefined;
|
|
@@ -104,7 +110,7 @@ process.on("message", (message) => {
|
|
|
104
110
|
if (result.ok)
|
|
105
111
|
request.resolve(result.value);
|
|
106
112
|
else
|
|
107
|
-
request.reject(new Error(result.error));
|
|
113
|
+
request.reject(result.call ? new InvocationFailure(result.call) : new Error(result.error));
|
|
108
114
|
}
|
|
109
115
|
catch (err) {
|
|
110
116
|
request.reject(new Error(`Invalid host result: ${msg(err)}`));
|
|
@@ -119,7 +125,7 @@ process.on("message", (message) => {
|
|
|
119
125
|
const payload = JSON.parse(message.payloadJson);
|
|
120
126
|
void run(payload).catch((err) => {
|
|
121
127
|
const payloadJson = JSON.stringify(fixedTransportFailure(`QuickJS child failed: ${msg(err)}`));
|
|
122
|
-
|
|
128
|
+
sendResult(payload.id, payloadJson);
|
|
123
129
|
activeJobId = undefined;
|
|
124
130
|
});
|
|
125
131
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// separate 512 KiB budget measured after both JSON encodings. The final result
|
|
4
|
-
// is shaped to 24k characters; even if every character takes its worst-case
|
|
5
|
-
// seven transport bytes, that leaves ample structural overhead below this hard
|
|
6
|
-
// process.send ceiling.
|
|
1
|
+
// Result messages remain bounded even when a full return travels in chunks.
|
|
2
|
+
// The host's presentation guard stashes the completed value after assembly.
|
|
7
3
|
export const MAX_QUICKJS_IPC_BYTES = 1024 * 1024;
|
|
4
|
+
/** Aggregate result transport bound, independent of per-message IPC and log caps. */
|
|
5
|
+
export const MAX_QUICKJS_RESULT_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
export const QUICKJS_RESULT_CHUNK_CHARS = 65_536;
|
|
8
7
|
export const MAX_QUICKJS_LOG_TRANSPORT_BYTES = 512 * 1024;
|
|
9
8
|
/** Preserve #84's stopgap before a host value enters the child/WASM process. */
|
|
10
9
|
export const MAX_QUICKJS_HOST_RPC_BYTES = 256 * 1024;
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
// running the provider function; `executePendingJobs` drives guest
|
|
8
8
|
// continuations from a host-side loop. Values cross the boundary as JSON, so
|
|
9
9
|
// provider args/results must be JSON-serializable.
|
|
10
|
-
import { getQuickJS
|
|
10
|
+
import { getQuickJS } from "quickjs-emscripten";
|
|
11
|
+
import { InvocationFailure } from "../invocation.js";
|
|
11
12
|
import { msg } from "../errors.js";
|
|
12
13
|
import { withoutProgramTerminator } from "../program-source.js";
|
|
13
|
-
import { hostCallLabel, MAX_QUICKJS_LOG_TRANSPORT_BYTES, serializedBytes
|
|
14
|
+
import { hostCallLabel, MAX_QUICKJS_LOG_TRANSPORT_BYTES, serializedBytes } from "./quickjs-protocol.js";
|
|
14
15
|
/** Load and compile the shared QuickJS WASM module before a run budget starts. */
|
|
15
16
|
export async function prepareQuickJs() {
|
|
16
17
|
await getQuickJS();
|
|
@@ -82,8 +83,8 @@ export function normalizeCode(code) {
|
|
|
82
83
|
* and fail closed there.
|
|
83
84
|
*/
|
|
84
85
|
function setupScript(providers) {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
86
|
+
// Capture the host bridge and codec before guest code runs.
|
|
87
|
+
// Rejections are native guest Errors created and retained by the host.
|
|
87
88
|
const lines = [
|
|
88
89
|
`(() => {
|
|
89
90
|
const call = globalThis.__call;
|
|
@@ -102,7 +103,6 @@ function setupScript(providers) {
|
|
|
102
103
|
})();
|
|
103
104
|
const invoke = async (ns, fn, args) => {
|
|
104
105
|
const r = parse(await call(ns, fn, stringify(args)));
|
|
105
|
-
if (!r.ok) throw new GuestError(r.error);
|
|
106
106
|
return r.value;
|
|
107
107
|
};
|
|
108
108
|
const namespace = (ns) => freeze(new ProxyConstructor(create(null), {
|
|
@@ -117,24 +117,14 @@ function setupScript(providers) {
|
|
|
117
117
|
if (p.prelude)
|
|
118
118
|
lines.push(p.prelude);
|
|
119
119
|
}
|
|
120
|
-
lines.push(`
|
|
121
|
-
})();`);
|
|
120
|
+
lines.push(`})();`);
|
|
122
121
|
return lines.join("\n");
|
|
123
122
|
}
|
|
124
|
-
/** True when a dumped error is QuickJS's deadline-interrupt signal. */
|
|
125
|
-
function isInterrupt(dumped) {
|
|
126
|
-
if (!dumped || typeof dumped !== "object")
|
|
127
|
-
return false;
|
|
128
|
-
const e = dumped;
|
|
129
|
-
return e.name === "InternalError" && e.message === "interrupted";
|
|
130
|
-
}
|
|
131
123
|
function formatGuestError(dumped) {
|
|
132
124
|
if (dumped && typeof dumped === "object") {
|
|
133
125
|
const e = dumped;
|
|
134
126
|
if (e.message != null) {
|
|
135
|
-
return e.name != null && e.name !== "Error"
|
|
136
|
-
? `${String(e.name)}: ${String(e.message)}`
|
|
137
|
-
: String(e.message);
|
|
127
|
+
return e.name != null && e.name !== "Error" ? `${String(e.name)}: ${String(e.message)}` : String(e.message);
|
|
138
128
|
}
|
|
139
129
|
}
|
|
140
130
|
return typeof dumped === "string" ? dumped : JSON.stringify(dumped);
|
|
@@ -166,6 +156,7 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
166
156
|
aborted: false,
|
|
167
157
|
wake: () => { },
|
|
168
158
|
waitForSettle: Promise.resolve(),
|
|
159
|
+
failures: [],
|
|
169
160
|
};
|
|
170
161
|
armWake(bridge);
|
|
171
162
|
// Keep both the in-memory character budget and the twice-JSON-encoded
|
|
@@ -195,8 +186,7 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
195
186
|
}
|
|
196
187
|
const entryTransportBytes = logTransportBytes(entry);
|
|
197
188
|
if (logTotalChars + entry.length > MAX_LOG_TOTAL_CHARS ||
|
|
198
|
-
logTotalTransportBytes + entryTransportBytes >
|
|
199
|
-
MAX_QUICKJS_LOG_TRANSPORT_BYTES - MAX_LOG_MARKER_TRANSPORT_BYTES) {
|
|
189
|
+
logTotalTransportBytes + entryTransportBytes > MAX_QUICKJS_LOG_TRANSPORT_BYTES - MAX_LOG_MARKER_TRANSPORT_BYTES) {
|
|
200
190
|
captureLog(LOG_SIZE_LIMIT_MARKER);
|
|
201
191
|
logBudgetSpent = true;
|
|
202
192
|
return;
|
|
@@ -211,9 +201,7 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
211
201
|
const invoke = async (ns, fn, argsJson) => {
|
|
212
202
|
try {
|
|
213
203
|
const provider = byName.get(ns);
|
|
214
|
-
const f = provider && Object.hasOwn(provider.fns, fn)
|
|
215
|
-
? provider.fns[fn]
|
|
216
|
-
: undefined;
|
|
204
|
+
const f = provider && Object.hasOwn(provider.fns, fn) ? provider.fns[fn] : undefined;
|
|
217
205
|
if (!f)
|
|
218
206
|
throw new Error(`Unknown function ${ns}.${fn}`);
|
|
219
207
|
const args = JSON.parse(argsJson);
|
|
@@ -231,10 +219,10 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
231
219
|
if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
|
|
232
220
|
throw new Error(`Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`);
|
|
233
221
|
}
|
|
234
|
-
return json;
|
|
222
|
+
return { json };
|
|
235
223
|
}
|
|
236
224
|
catch (err) {
|
|
237
|
-
return
|
|
225
|
+
return { error: err };
|
|
238
226
|
}
|
|
239
227
|
};
|
|
240
228
|
const callFn = ctx.newFunction("__call", (nsH, fnH, argsH) => {
|
|
@@ -243,16 +231,42 @@ function installBridge(ctx, providers, logs, onLog) {
|
|
|
243
231
|
const argsJson = ctx.getString(argsH);
|
|
244
232
|
const deferred = ctx.newPromise();
|
|
245
233
|
bridge.pending++;
|
|
246
|
-
void invoke(ns, fn, argsJson).then((
|
|
234
|
+
void invoke(ns, fn, argsJson).then((reply) => {
|
|
247
235
|
bridge.pending--;
|
|
248
236
|
if (bridge.aborted) {
|
|
249
237
|
// Context is (about to be) torn down — settle nothing, free our handles.
|
|
250
238
|
deferred.dispose();
|
|
251
239
|
}
|
|
252
240
|
else {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
241
|
+
if ("json" in reply) {
|
|
242
|
+
const h = ctx.newString(reply.json);
|
|
243
|
+
deferred.resolve(h);
|
|
244
|
+
h.dispose();
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const error = ctx.newError();
|
|
248
|
+
const message = ctx.unwrapResult(ctx.evalCode(JSON.stringify(msg(reply.error).slice(0, 4_000))));
|
|
249
|
+
ctx.setProp(error, "message", message);
|
|
250
|
+
message.dispose();
|
|
251
|
+
if (reply.error instanceof InvocationFailure) {
|
|
252
|
+
const call = reply.error.details;
|
|
253
|
+
// The error identity and typed facts stay in host state. A guest
|
|
254
|
+
// may edit public fields without changing the retained failure.
|
|
255
|
+
bridge.failures.push({ error: error.dup(), call });
|
|
256
|
+
for (const [key, value] of Object.entries({
|
|
257
|
+
code: call.code,
|
|
258
|
+
retryable: call.retryable,
|
|
259
|
+
details: call,
|
|
260
|
+
data: call,
|
|
261
|
+
})) {
|
|
262
|
+
const property = ctx.unwrapResult(ctx.evalCode(`(${JSON.stringify(value)})`));
|
|
263
|
+
ctx.setProp(error, key, property);
|
|
264
|
+
property.dispose();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
deferred.reject(error);
|
|
268
|
+
error.dispose();
|
|
269
|
+
}
|
|
256
270
|
}
|
|
257
271
|
bridge.wake();
|
|
258
272
|
});
|
|
@@ -296,14 +310,18 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
296
310
|
if (!inGuest)
|
|
297
311
|
return false;
|
|
298
312
|
wallInterrupted = Date.now() >= deadline;
|
|
299
|
-
cpuInterrupted =
|
|
300
|
-
cpuUsedMs + (Date.now() - segmentStarted) >= cpuTimeMs;
|
|
313
|
+
cpuInterrupted = cpuUsedMs + (Date.now() - segmentStarted) >= cpuTimeMs;
|
|
301
314
|
return wallInterrupted || cpuInterrupted;
|
|
302
315
|
});
|
|
316
|
+
let describeError;
|
|
303
317
|
const logs = [];
|
|
304
318
|
const bridge = installBridge(ctx, providers, logs, onLog);
|
|
305
319
|
const finish = (r) => {
|
|
306
320
|
bridge.aborted = true;
|
|
321
|
+
describeError?.dispose();
|
|
322
|
+
for (const failure of bridge.failures)
|
|
323
|
+
failure.error.dispose();
|
|
324
|
+
bridge.failures.length = 0;
|
|
307
325
|
// Outstanding host calls still hold deferred-promise handles; their
|
|
308
326
|
// callbacks free them and wake the drain, which disposes the context
|
|
309
327
|
// once the last straggler settles. Re-arm before every wait so the
|
|
@@ -326,32 +344,119 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
326
344
|
}
|
|
327
345
|
return logs.length > 0 ? { ...r, logs } : r;
|
|
328
346
|
};
|
|
329
|
-
const
|
|
347
|
+
const rejected = (error) => {
|
|
348
|
+
// Interrupt/deadline facts are host state, never inferred by inspecting
|
|
349
|
+
// the thrown value. Do not reenter a guest whose budget has ended.
|
|
350
|
+
if (wallInterrupted || Date.now() >= deadline)
|
|
351
|
+
return timedOut();
|
|
352
|
+
if (cpuInterrupted || cpuUsedMs >= cpuTimeMs)
|
|
353
|
+
return { result: undefined, error: cpuTimeoutError };
|
|
354
|
+
const call = bridge.failures.find((failure) => ctx.eq(failure.error, error))?.call;
|
|
355
|
+
let description = { name: "Error", message: "Program threw a value." };
|
|
356
|
+
const type = ctx.typeof(error);
|
|
357
|
+
if (type === "string")
|
|
358
|
+
description = ctx.getString(error).slice(0, 4000);
|
|
359
|
+
else if (type === "number")
|
|
360
|
+
description = ctx.getNumber(error);
|
|
361
|
+
else if (type === "undefined")
|
|
362
|
+
description = "undefined";
|
|
363
|
+
else if (type === "boolean")
|
|
364
|
+
description = ctx.eq(error, ctx.true);
|
|
365
|
+
else if (ctx.eq(error, ctx.null))
|
|
366
|
+
description = null;
|
|
367
|
+
else if (describeError) {
|
|
368
|
+
const describe = describeError;
|
|
369
|
+
const described = runGuest(() => ctx.callFunction(describe, ctx.undefined, error));
|
|
370
|
+
if (described.error)
|
|
371
|
+
described.error.dispose();
|
|
372
|
+
else {
|
|
373
|
+
description = JSON.parse(ctx.getString(described.value)) ?? description;
|
|
374
|
+
described.value.dispose();
|
|
375
|
+
}
|
|
376
|
+
if (wallInterrupted || Date.now() >= deadline)
|
|
377
|
+
return timedOut();
|
|
378
|
+
if (cpuInterrupted || cpuUsedMs >= cpuTimeMs)
|
|
379
|
+
return { result: undefined, error: cpuTimeoutError };
|
|
380
|
+
}
|
|
381
|
+
const name = description && typeof description === "object" && "name" in description && typeof description.name === "string"
|
|
382
|
+
? description.name
|
|
383
|
+
: "Error";
|
|
384
|
+
const stack = description && typeof description === "object" && "stack" in description && typeof description.stack === "string"
|
|
385
|
+
? description.stack
|
|
386
|
+
: "";
|
|
387
|
+
const location = /:(\d+)(?::\d+)?\)?(?:\n|$)/.exec(stack);
|
|
388
|
+
return {
|
|
389
|
+
result: undefined,
|
|
390
|
+
error: formatGuestError(description).slice(0, 4000),
|
|
391
|
+
failure: {
|
|
392
|
+
name,
|
|
393
|
+
...(call ? { call } : {}),
|
|
394
|
+
...(location ? { line: Math.max(1, Number(location[1]) - 1) } : {}),
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
};
|
|
330
398
|
const timedOut = () => ({
|
|
331
399
|
result: undefined,
|
|
332
400
|
error: timeoutError,
|
|
401
|
+
failure: {
|
|
402
|
+
name: "TimeoutError",
|
|
403
|
+
timeout: { elapsedMs: Date.now() - (deadline - timeoutMs), deadlineMs: timeoutMs },
|
|
404
|
+
},
|
|
333
405
|
timedOut: true,
|
|
334
406
|
});
|
|
407
|
+
// Keep this diagnostic function in a host-owned handle. Error.isError
|
|
408
|
+
// checks the native brand without Proxy traps. Own data descriptors of
|
|
409
|
+
// native Errors are safe to read; accessors and arbitrary thrown objects
|
|
410
|
+
// get a fixed description instead of dump's guest serialization/getters.
|
|
411
|
+
const described = runGuest(() => ctx.evalCode(`(() => {
|
|
412
|
+
const isError = Error.isError;
|
|
413
|
+
const descriptor = Object.getOwnPropertyDescriptor;
|
|
414
|
+
const prototypeOf = Object.getPrototypeOf;
|
|
415
|
+
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
|
|
416
|
+
const slice = Function.prototype.call.bind(String.prototype.slice);
|
|
417
|
+
const stringify = JSON.stringify;
|
|
418
|
+
const prototypes = [
|
|
419
|
+
[Error.prototype, "Error"], [TypeError.prototype, "TypeError"],
|
|
420
|
+
[SyntaxError.prototype, "SyntaxError"], [ReferenceError.prototype, "ReferenceError"],
|
|
421
|
+
[RangeError.prototype, "RangeError"], [EvalError.prototype, "EvalError"],
|
|
422
|
+
[URIError.prototype, "URIError"], [AggregateError.prototype, "AggregateError"]
|
|
423
|
+
];
|
|
424
|
+
return (error) => {
|
|
425
|
+
if (!isError(error)) return "null";
|
|
426
|
+
const text = (key, fallback, limit) => {
|
|
427
|
+
const own = descriptor(error, key);
|
|
428
|
+
return own && hasOwn(own, "value") && typeof own.value === "string"
|
|
429
|
+
? slice(own.value, 0, limit) : fallback;
|
|
430
|
+
};
|
|
431
|
+
const prototype = prototypeOf(error);
|
|
432
|
+
let name = "Error";
|
|
433
|
+
for (let i = 0; i < prototypes.length; i++) {
|
|
434
|
+
if (prototype === prototypes[i][0]) { name = prototypes[i][1]; break; }
|
|
435
|
+
}
|
|
436
|
+
return stringify({ __proto__: null, name: text("name", name, 64),
|
|
437
|
+
message: text("message", "Program threw a value.", 4000), stack: text("stack", "", 1000) });
|
|
438
|
+
};
|
|
439
|
+
})()`));
|
|
440
|
+
if (described.error) {
|
|
441
|
+
const rejection = rejected(described.error);
|
|
442
|
+
described.error.dispose();
|
|
443
|
+
return finish(rejection);
|
|
444
|
+
}
|
|
445
|
+
describeError = described.value;
|
|
335
446
|
const setup = runGuest(() => ctx.evalCode(setupScript(providers)));
|
|
336
447
|
if (setup.error) {
|
|
337
|
-
const
|
|
448
|
+
const rejection = rejected(setup.error);
|
|
338
449
|
setup.error.dispose();
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return fail(cpuTimeoutError);
|
|
343
|
-
return fail(`Sandbox setup failed: ${detail}`);
|
|
450
|
+
return finish(rejection.timedOut || cpuInterrupted
|
|
451
|
+
? rejection
|
|
452
|
+
: { ...rejection, error: `Sandbox setup failed: ${rejection.error}` });
|
|
344
453
|
}
|
|
345
454
|
setup.value.dispose();
|
|
346
455
|
const evaluated = runGuest(() => ctx.evalCode(`Promise.resolve((\n${normalizeCode(code)}\n)())`));
|
|
347
456
|
if (evaluated.error) {
|
|
348
|
-
const
|
|
457
|
+
const rejection = rejected(evaluated.error);
|
|
349
458
|
evaluated.error.dispose();
|
|
350
|
-
|
|
351
|
-
return finish(timedOut());
|
|
352
|
-
if (isInterrupt(dumped) && cpuInterrupted)
|
|
353
|
-
return fail(cpuTimeoutError);
|
|
354
|
-
return fail(formatGuestError(dumped));
|
|
459
|
+
return finish(rejection);
|
|
355
460
|
}
|
|
356
461
|
const promiseHandle = evaluated.value;
|
|
357
462
|
// Drive the guest: run microtask jobs, inspect the result promise,
|
|
@@ -364,15 +469,9 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
364
469
|
}
|
|
365
470
|
const jobs = runGuest(() => ctx.runtime.executePendingJobs());
|
|
366
471
|
if (jobs.error) {
|
|
367
|
-
const
|
|
472
|
+
const rejection = rejected(jobs.error);
|
|
368
473
|
jobs.error.dispose();
|
|
369
|
-
|
|
370
|
-
return timedOut();
|
|
371
|
-
}
|
|
372
|
-
if (isInterrupt(dumped) && cpuInterrupted) {
|
|
373
|
-
return { result: undefined, error: cpuTimeoutError };
|
|
374
|
-
}
|
|
375
|
-
return { result: undefined, error: formatGuestError(dumped) };
|
|
474
|
+
return rejection;
|
|
376
475
|
}
|
|
377
476
|
const state = ctx.getPromiseState(promiseHandle);
|
|
378
477
|
if (state.type === "fulfilled") {
|
|
@@ -381,15 +480,9 @@ export async function executeQuickJs(code, providers, options, onLog) {
|
|
|
381
480
|
return { result };
|
|
382
481
|
}
|
|
383
482
|
if (state.type === "rejected") {
|
|
384
|
-
const
|
|
483
|
+
const rejection = rejected(state.error);
|
|
385
484
|
state.error.dispose();
|
|
386
|
-
|
|
387
|
-
return timedOut();
|
|
388
|
-
}
|
|
389
|
-
if (isInterrupt(dumped) && cpuInterrupted) {
|
|
390
|
-
return { result: undefined, error: cpuTimeoutError };
|
|
391
|
-
}
|
|
392
|
-
return { result: undefined, error: formatGuestError(dumped) };
|
|
485
|
+
return rejection;
|
|
393
486
|
}
|
|
394
487
|
if (bridge.pending === 0) {
|
|
395
488
|
return {
|