@zackbart/connecta 0.24.4 → 0.26.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/AGENTS.md +43 -12
- package/CHANGELOG.md +524 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/advertised-schema.js +53 -0
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +449 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +194 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +152 -106
- package/dist/catalog.js +297 -38
- package/dist/concurrency.js +24 -23
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +330 -240
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +745 -282
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +145 -63
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +255 -212
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +281 -26
- package/dist/invocation.js +371 -286
- package/dist/meta-tools.js +213 -94
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +103 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +128 -3
- package/dist/program-source.js +321 -0
- package/dist/providers/notion.js +2 -2
- package/dist/providers/revenuecat.js +32 -5
- package/dist/providers/stripe.d.ts +3 -2
- package/dist/providers/vercel.js +1 -1
- package/dist/registry.d.ts +43 -7
- package/dist/registry.js +434 -288
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +74 -154
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +196 -163
- package/dist/routes/mcp.js +228 -112
- package/dist/routes/oauth-management.js +73 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +83 -0
- package/dist/routes/shared.d.ts +25 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +211 -0
- package/dist/routes/ui.js +76 -35
- package/dist/run-journal.js +347 -0
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +137 -89
- package/dist/skills.js +28 -9
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +39 -43
- package/dist/tool-safety.d.ts +27 -1
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +45 -7
- package/dist/ui.d.ts +25 -4
- package/dist/ui.js +77 -167
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +367 -16
- package/documentation/auth.md +163 -2
- package/documentation/code-mode.md +274 -56
- package/documentation/meta-tools.md +200 -44
- package/ethos.md +46 -47
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +224 -14
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +42 -7
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +21 -5
- package/package.json +16 -4
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -96
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -188
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame every copyable fix prompt shares: the operator page's failure
|
|
3
|
+
* notices and the OAuth callback page both hand an operator text to paste into
|
|
4
|
+
* a coding agent working in the deployment's repository.
|
|
5
|
+
*
|
|
6
|
+
* A prompt is assembled from fixed catalogue text and, at most, a configured
|
|
7
|
+
* connector id. It has no parameter an error message, a downstream body, a
|
|
8
|
+
* token, or a URL could arrive through — which is the whole guarantee: the
|
|
9
|
+
* text leaves the page on a clipboard, into a tool the deployment does not
|
|
10
|
+
* control, so it must be something the deployment would be content to publish.
|
|
11
|
+
* The agent finds the underlying cause where it belongs, in the deployment's
|
|
12
|
+
* own logs and a local reproduction.
|
|
13
|
+
*/
|
|
14
|
+
const CONNECTOR_ID_RE = /^[a-z0-9_-]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* Render one prompt. A connector id is the only variable part, and only when
|
|
17
|
+
* it has the shape configuration enforces; anything else is dropped rather
|
|
18
|
+
* than escaped, since a value that is not a connector id is not config.
|
|
19
|
+
*/
|
|
20
|
+
export function renderFixPrompt(spec, connectorId) {
|
|
21
|
+
const id = connectorId !== undefined && CONNECTOR_ID_RE.test(connectorId)
|
|
22
|
+
? connectorId
|
|
23
|
+
: undefined;
|
|
24
|
+
return [
|
|
25
|
+
"Diagnose and fix a problem in this connecta deployment (the @zackbart/connecta package). " +
|
|
26
|
+
"It is configured as code: connectors, credential slots, OAuth clients, pools, and inbound auth " +
|
|
27
|
+
"are declared in the deployment's source and environment, so the fix belongs there and not in the operator page.",
|
|
28
|
+
`Problem: ${spec.problem}` + (id ? `\nConnector id: ${id}` : ""),
|
|
29
|
+
`Where to look:\n${spec.steps.map((step) => `- ${step}`).join("\n")}`,
|
|
30
|
+
"This prompt deliberately carries no error text, tokens, or URLs. Find the underlying cause in the deployment's " +
|
|
31
|
+
"own logs (lines prefixed [connecta]) or by reproducing the failure locally. Never put a secret in source, " +
|
|
32
|
+
"a log line, or your reply: secrets belong in the deployment's environment or its credential vault.",
|
|
33
|
+
"Make the smallest change that fixes it, then verify it by redeploying and refreshing the connection on the " +
|
|
34
|
+
"operator Connections page. If the fix needs something you cannot do yourself — a provider console setting, " +
|
|
35
|
+
"a secret only the operator holds — name that exact step instead.",
|
|
36
|
+
].join("\n\n");
|
|
37
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { CredentialVault } from "./credential-contract.js";
|
|
2
2
|
import { Registry } from "./registry.js";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type { ConnectorGrant } from "./connector-access.js";
|
|
4
|
+
import type { ActivityModule, ArtifactsModule, OperatorSurface } from "./module-contracts.js";
|
|
5
|
+
export type { ActivityModule, ArtifactsModule, OperatorSurface, } from "./module-contracts.js";
|
|
5
6
|
export type { CredentialVault, CredentialMetadata } from "./credential-contract.js";
|
|
6
7
|
import type { AuthenticatedIdentity, Connector, Executor, IdentityReference, InboundAuth, KVStorage, Logger } from "./types.js";
|
|
7
8
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
@@ -47,7 +48,7 @@ export interface ConnectaCallsConfig {
|
|
|
47
48
|
/**
|
|
48
49
|
* Max inline result size (bytes) before truncation and `get_result` paging.
|
|
49
50
|
* Must be a finite whole number >= 1; invalid values warn and fall back to
|
|
50
|
-
*
|
|
51
|
+
* 24_000. Connectors may override it individually.
|
|
51
52
|
*/
|
|
52
53
|
maxResultBytes?: number;
|
|
53
54
|
}
|
|
@@ -81,6 +82,55 @@ export interface ConnectaExecuteConfig {
|
|
|
81
82
|
* values fall back to the default.
|
|
82
83
|
*/
|
|
83
84
|
hostCallTimeoutMs?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Hard ceiling on one execution, in milliseconds, enforced outside the
|
|
87
|
+
* sandbox. Default 120_000, above both executors' own deadlines (QuickJS
|
|
88
|
+
* 30_000, the Dynamic Worker 60_000), so it only ends a run whose executor
|
|
89
|
+
* never settled; that run fails as unresponsive and its admission slot is
|
|
90
|
+
* released. Keep it above any raised executor deadline. Invalid values
|
|
91
|
+
* fall back to the default.
|
|
92
|
+
*/
|
|
93
|
+
watchdogMs?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Let programs write. A call to a tool that is not explicitly read-only
|
|
96
|
+
* pauses the run before anything is sent and returns the exact write with
|
|
97
|
+
* a token; `resume_execution` repeating it is the approval, and the
|
|
98
|
+
* program replays from a journal to send it and continue. Needs `storage`
|
|
99
|
+
* with `compareAndSet`, because two resumes of one pause must send its
|
|
100
|
+
* writes at most once. Omitted, it is on exactly when the storage has one
|
|
101
|
+
* (a single startup warning says when it is not); `true` over storage
|
|
102
|
+
* without one refuses to construct; `false` keeps E4's refusal.
|
|
103
|
+
* `resume_execution` is listed either way.
|
|
104
|
+
*/
|
|
105
|
+
resumableWrites?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Consequential calls one run may send, on top of the host-call budget
|
|
108
|
+
* every call already spends. Default 10. A replayed write spends it again,
|
|
109
|
+
* so it bounds the run, not each play. Invalid values fall back to the
|
|
110
|
+
* default.
|
|
111
|
+
*/
|
|
112
|
+
maxWrites?: number;
|
|
113
|
+
/**
|
|
114
|
+
* How long a paused run can be resumed, in seconds from its first pause.
|
|
115
|
+
* Default 1_800. A later pause in the same run keeps the first deadline, so
|
|
116
|
+
* a run never replays reads older than this. Invalid values fall back to
|
|
117
|
+
* the default.
|
|
118
|
+
*/
|
|
119
|
+
pausedRunTtlSeconds?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Which writes a program may make without pausing for approval. Keys are
|
|
122
|
+
* connector ids (all of that connector's tools) or `connector.tool`
|
|
123
|
+
* addresses (that tool); values are `"never"` (never ask) or `"ask"`. The
|
|
124
|
+
* most specific key wins, and `"ask"` switches off a connector's own
|
|
125
|
+
* default. An exempt write still spends the write budget, is recorded in
|
|
126
|
+
* activity, and is journaled like any other; it is never read-only
|
|
127
|
+
* anywhere else — discovery still lists it as approval-required and
|
|
128
|
+
* `call_tool` still refuses it. Works with resumable writes off too. An
|
|
129
|
+
* unknown connector id, or an `api()` address its tools do not include,
|
|
130
|
+
* refuses to construct; a remote connector's tool names load later, so an
|
|
131
|
+
* address naming one it never serves simply never matches.
|
|
132
|
+
*/
|
|
133
|
+
approval?: Readonly<Record<string, "never" | "ask">>;
|
|
84
134
|
}
|
|
85
135
|
export interface AdmissionPoolConfig {
|
|
86
136
|
/** Simultaneous work admitted to this pool. */
|
|
@@ -92,6 +142,10 @@ export interface AdmissionPoolConfig {
|
|
|
92
142
|
/** Retry hint returned with overload failures, in milliseconds. */
|
|
93
143
|
retryAfterMs?: number;
|
|
94
144
|
}
|
|
145
|
+
export interface RequestAdmissionConfig extends AdmissionPoolConfig {
|
|
146
|
+
/** Hard lifetime of an admitted /mcp request. Default 300,000 ms. */
|
|
147
|
+
maxDurationMs?: number;
|
|
148
|
+
}
|
|
95
149
|
/**
|
|
96
150
|
* Runtime-portable server-memory boundaries. `/health` and operator routes do
|
|
97
151
|
* not consume these permits, so they remain responsive during MCP saturation.
|
|
@@ -101,7 +155,7 @@ export interface ConnectaAdmissionConfig {
|
|
|
101
155
|
* The `/mcp` request boundary. Defaults to 16 active, 32 queued, and a
|
|
102
156
|
* 5-second maximum wait.
|
|
103
157
|
*/
|
|
104
|
-
requests?:
|
|
158
|
+
requests?: RequestAdmissionConfig;
|
|
105
159
|
/**
|
|
106
160
|
* Fallback pool for an `executor` that does not implement its own `acquire`.
|
|
107
161
|
* Defaults to 2 active, 8 queued, and a 5-second maximum wait. Bounded
|
|
@@ -115,10 +169,13 @@ export interface ConnectaIdentityConfig {
|
|
|
115
169
|
/**
|
|
116
170
|
* What this admitted identity may discover and call: `"all"`, or a list
|
|
117
171
|
* whose entries are connector ids (the whole connector) and `connector.tool`
|
|
118
|
-
* addresses (that tool only)
|
|
119
|
-
*
|
|
172
|
+
* addresses (that tool only), or `{ tool: "connector.tool",
|
|
173
|
+
* requireReadOnly: true }` for an exact tool only while its loaded catalog
|
|
174
|
+
* explicitly classifies it read-only. Grants are additive: an unrestricted
|
|
175
|
+
* connector or address grant wins over the guarded form. An address naming
|
|
176
|
+
* a tool the catalog lacks is unreachable and warned once, never widened.
|
|
120
177
|
*/
|
|
121
|
-
connectorAccess?(identity: Readonly<AuthenticatedIdentity>): "all" | readonly
|
|
178
|
+
connectorAccess?(identity: Readonly<AuthenticatedIdentity>): "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>;
|
|
122
179
|
/** Global payload-free activity reads. Defaults to interactive humans. */
|
|
123
180
|
activityAccess?(principal: Readonly<IdentityReference>): boolean | Promise<boolean>;
|
|
124
181
|
/** Shared credential and OAuth administration. Defaults to none. */
|
|
@@ -167,6 +224,14 @@ export interface ConnectaConfig {
|
|
|
167
224
|
vault?: CredentialVault;
|
|
168
225
|
/** Optional connection UI, created by operatorUi() from /ui. */
|
|
169
226
|
ui?: OperatorSurface;
|
|
227
|
+
/**
|
|
228
|
+
* Optional team pages over stored data, created by artifacts() from
|
|
229
|
+
* /artifacts. Adds the built-in `artifacts` connector; needs `publicUrl`,
|
|
230
|
+
* because the links it hands out are shared.
|
|
231
|
+
*/
|
|
232
|
+
artifacts?: ArtifactsModule;
|
|
233
|
+
/** Optional dedicated HTTPS origin that serves only artifact pages and their library. */
|
|
234
|
+
artifactOrigin?: string;
|
|
170
235
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
171
236
|
discovery?: ConnectaDiscoveryConfig;
|
|
172
237
|
/** Deployment-wide call deadlines and result paging threshold. */
|
package/dist/index.js
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
import { credentialTestRule, describeCredentialTestMismatch, } from "./credential-rules.js";
|
|
2
2
|
import { Registry } from "./registry.js";
|
|
3
|
-
import { parseConnectorAccess, POOL_NAME_RE } from "./connector-access.js";
|
|
3
|
+
import { intersectAccess, parseConnectorAccess, POOL_NAME_RE } from "./connector-access.js";
|
|
4
4
|
import { createFetchHandler } from "./server.js";
|
|
5
|
+
import { createProgramRunner } from "./execute.js";
|
|
5
6
|
import { droppedBrandingUrls, droppedThemeTokens, droppedUiAuthUrls, } from "./branding.js";
|
|
6
7
|
import { memoryStorage } from "./storage/memory.js";
|
|
7
8
|
import { CONNECTA_VERSION } from "./version.js";
|
|
8
9
|
import { AdmissionController, executorName, isAdmittingExecutor, withExecutorAdmission, } from "./executor-admission.js";
|
|
10
|
+
import { DEFAULT_MAX_WRITES, DEFAULT_PAUSED_RUN_TTL_SECONDS, } from "./resumable.js";
|
|
11
|
+
import { disposeEdgeRuntime } from "./runtime/run.js";
|
|
12
|
+
import { NO_EXEMPTIONS } from "./tool-safety.js";
|
|
13
|
+
import { createCoreRuntime, resolveLogger } from "./runtime/services.js";
|
|
9
14
|
const REQUEST_ADMISSION_DEFAULTS = {
|
|
10
15
|
concurrency: 16,
|
|
11
16
|
maxQueueSize: 32,
|
|
12
17
|
queueTimeoutMs: 5_000,
|
|
13
18
|
retryAfterMs: 1_000,
|
|
14
19
|
};
|
|
20
|
+
const DEFAULT_REQUEST_MAX_DURATION_MS = 300_000;
|
|
15
21
|
const CODE_ADMISSION_DEFAULTS = {
|
|
16
22
|
concurrency: 2,
|
|
17
23
|
maxQueueSize: 8,
|
|
18
24
|
queueTimeoutMs: 5_000,
|
|
19
25
|
retryAfterMs: 1_000,
|
|
20
26
|
};
|
|
21
|
-
function admissionController(options, defaults) {
|
|
27
|
+
function admissionController(options, defaults, maxDurationMs) {
|
|
22
28
|
return new AdmissionController({
|
|
23
29
|
concurrency: options?.concurrency ?? defaults.concurrency,
|
|
24
30
|
maxQueueSize: options?.maxQueueSize ?? defaults.maxQueueSize,
|
|
25
31
|
queueTimeoutMs: options?.queueTimeoutMs ?? defaults.queueTimeoutMs,
|
|
26
32
|
retryAfterMs: options?.retryAfterMs ?? defaults.retryAfterMs,
|
|
33
|
+
...(maxDurationMs !== undefined ? { maxDurationMs } : {}),
|
|
27
34
|
});
|
|
28
35
|
}
|
|
29
|
-
function defaultLogger() {
|
|
30
|
-
return {
|
|
31
|
-
debug: (...a) => console.debug("[connecta]", ...a),
|
|
32
|
-
info: (...a) => console.info("[connecta]", ...a),
|
|
33
|
-
warn: (...a) => console.warn("[connecta]", ...a),
|
|
34
|
-
error: (...a) => console.error("[connecta]", ...a),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
36
|
/** Bearer providers are checked before Clerk (per spec). */
|
|
38
37
|
function normalizeAuth(auth) {
|
|
39
38
|
const list = auth ? (Array.isArray(auth) ? auth : [auth]) : [];
|
|
@@ -48,6 +47,10 @@ const admissionPoolSchema = {
|
|
|
48
47
|
queueTimeoutMs: null,
|
|
49
48
|
retryAfterMs: null,
|
|
50
49
|
};
|
|
50
|
+
const requestAdmissionSchema = {
|
|
51
|
+
...admissionPoolSchema,
|
|
52
|
+
maxDurationMs: null,
|
|
53
|
+
};
|
|
51
54
|
const CONFIG_SCHEMA = {
|
|
52
55
|
connectors: null,
|
|
53
56
|
auth: null,
|
|
@@ -64,6 +67,8 @@ const CONFIG_SCHEMA = {
|
|
|
64
67
|
activity: null,
|
|
65
68
|
vault: null,
|
|
66
69
|
ui: null,
|
|
70
|
+
artifacts: null,
|
|
71
|
+
artifactOrigin: null,
|
|
67
72
|
discovery: {
|
|
68
73
|
concurrency: null,
|
|
69
74
|
catalogTtlSeconds: null,
|
|
@@ -84,9 +89,14 @@ const CONFIG_SCHEMA = {
|
|
|
84
89
|
maxEmittedBlocks: null,
|
|
85
90
|
maxHostCalls: null,
|
|
86
91
|
hostCallTimeoutMs: null,
|
|
92
|
+
watchdogMs: null,
|
|
93
|
+
resumableWrites: null,
|
|
94
|
+
maxWrites: null,
|
|
95
|
+
pausedRunTtlSeconds: null,
|
|
96
|
+
approval: null,
|
|
87
97
|
},
|
|
88
98
|
admission: {
|
|
89
|
-
requests:
|
|
99
|
+
requests: requestAdmissionSchema,
|
|
90
100
|
code: admissionPoolSchema,
|
|
91
101
|
},
|
|
92
102
|
logger: null,
|
|
@@ -151,7 +161,7 @@ function assertKnownConfig(config) {
|
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
|
-
if (config.vault && ["get", "getAll", "set", "setAll", "metadata", "delete"].some(key => typeof config.vault[key] !== "function"))
|
|
164
|
+
if (config.vault && (["get", "getAll", "set", "setAll", "metadata", "delete"].some(key => typeof config.vault[key] !== "function") || ["seal", "open"].some(key => !["undefined", "function"].includes(typeof config.vault[key]))))
|
|
155
165
|
throw new Error("ConnectaConfig.vault must implement CredentialVault");
|
|
156
166
|
if (config.ui && (typeof config.ui.handle !== "function" || typeof config.ui.credentialHandoffUrl !== "function" || !Array.isArray(config.ui.reservedPaths)))
|
|
157
167
|
throw new Error("ConnectaConfig.ui must be created with operatorUi(...)");
|
|
@@ -162,6 +172,47 @@ function assertKnownConfig(config) {
|
|
|
162
172
|
typeof activity.recordTool !== "function")) {
|
|
163
173
|
throw new Error("ConnectaConfig.activity must be created with activityHistory(...)");
|
|
164
174
|
}
|
|
175
|
+
const artifacts = config.artifacts;
|
|
176
|
+
if (artifacts !== undefined) {
|
|
177
|
+
const connector = artifacts?.connector;
|
|
178
|
+
if (typeof artifacts !== "object" ||
|
|
179
|
+
artifacts === null ||
|
|
180
|
+
!connector ||
|
|
181
|
+
connector.id !== "artifacts" ||
|
|
182
|
+
typeof connector.callTool !== "function" ||
|
|
183
|
+
typeof artifacts.handle !== "function") {
|
|
184
|
+
throw new Error("ConnectaConfig.artifacts must be created with artifacts(...) from @zackbart/connecta/artifacts");
|
|
185
|
+
}
|
|
186
|
+
if (config.connectors?.some((candidate) => candidate?.id === "artifacts")) {
|
|
187
|
+
throw new Error('Connector id "artifacts" is reserved by the artifacts module; rename ' +
|
|
188
|
+
"your connector or drop the artifacts option");
|
|
189
|
+
}
|
|
190
|
+
if (!config.publicUrl) {
|
|
191
|
+
throw new Error("ConnectaConfig.artifacts needs publicUrl: artifact links are shared " +
|
|
192
|
+
"with teammates, so they must name the deployment's own origin, never " +
|
|
193
|
+
"one taken from a request's Host header");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (config.artifactOrigin !== undefined) {
|
|
197
|
+
if (!config.artifacts || !config.ui || !config.publicUrl) {
|
|
198
|
+
throw new Error("ConnectaConfig.artifactOrigin needs artifacts, ui, and publicUrl");
|
|
199
|
+
}
|
|
200
|
+
if (typeof config.artifactOrigin !== "string") {
|
|
201
|
+
throw new Error("ConnectaConfig.artifactOrigin must be an HTTPS origin");
|
|
202
|
+
}
|
|
203
|
+
let origin;
|
|
204
|
+
try {
|
|
205
|
+
origin = new URL(config.artifactOrigin);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
throw new Error("ConnectaConfig.artifactOrigin must be an HTTPS origin");
|
|
209
|
+
}
|
|
210
|
+
if (origin.protocol !== "https:" ||
|
|
211
|
+
origin.toString() !== `${origin.origin}/` ||
|
|
212
|
+
origin.origin === new URL(config.publicUrl).origin) {
|
|
213
|
+
throw new Error("ConnectaConfig.artifactOrigin must be a distinct HTTPS origin");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
165
216
|
}
|
|
166
217
|
/**
|
|
167
218
|
* Validate declared pools against the connector set. Everything checkable at
|
|
@@ -224,6 +275,92 @@ function resolvePools(pools, registry) {
|
|
|
224
275
|
}
|
|
225
276
|
return resolved;
|
|
226
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Validate `execute.approval` against the connector set, like the pools: a
|
|
280
|
+
* malformed key or value, an unknown connector, and an `api()` address its
|
|
281
|
+
* tools do not include all throw. Remote catalogs load lazily, so an address
|
|
282
|
+
* on one is kept and simply never matches a tool it does not serve.
|
|
283
|
+
*/
|
|
284
|
+
function resolveApprovalPolicy(approval, registry) {
|
|
285
|
+
for (const connector of registry.listConnectors()) {
|
|
286
|
+
if (connector.approval !== undefined && connector.approval !== "never") {
|
|
287
|
+
throw new Error(`Connector "${connector.id}": approval must be "never" or omitted`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (approval === undefined)
|
|
291
|
+
return NO_EXEMPTIONS;
|
|
292
|
+
if (!approval || typeof approval !== "object" || Array.isArray(approval)) {
|
|
293
|
+
throw new Error("ConnectaConfig.execute.approval must be an object keyed by connector ids and connector.tool addresses");
|
|
294
|
+
}
|
|
295
|
+
const connectors = new Map();
|
|
296
|
+
const tools = new Map();
|
|
297
|
+
for (const [key, value] of Object.entries(approval)) {
|
|
298
|
+
const where = `ConnectaConfig.execute.approval[${JSON.stringify(key)}]`;
|
|
299
|
+
if (value !== "never" && value !== "ask") {
|
|
300
|
+
throw new Error(`${where} must be "never" or "ask"`);
|
|
301
|
+
}
|
|
302
|
+
const dot = key.indexOf(".");
|
|
303
|
+
const id = dot === -1 ? key : key.slice(0, dot);
|
|
304
|
+
const tool = dot === -1 ? undefined : key.slice(dot + 1);
|
|
305
|
+
if (!id || tool === "") {
|
|
306
|
+
throw new Error(`${where}: keys are connector ids or connector.tool addresses`);
|
|
307
|
+
}
|
|
308
|
+
const connector = registry.getConnector(id);
|
|
309
|
+
if (!connector)
|
|
310
|
+
throw new Error(`${where}: unknown connector "${id}"`);
|
|
311
|
+
if (tool === undefined) {
|
|
312
|
+
connectors.set(id, value);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (connector.staticTools &&
|
|
316
|
+
!connector.staticTools.some((candidate) => candidate.name === tool)) {
|
|
317
|
+
throw new Error(`${where}: connector "${id}" has no tool "${tool}"`);
|
|
318
|
+
}
|
|
319
|
+
tools.set(key, value);
|
|
320
|
+
}
|
|
321
|
+
return { connectors, tools };
|
|
322
|
+
}
|
|
323
|
+
/** A positive whole number, or the default when the value is unusable. */
|
|
324
|
+
function positiveWhole(value, fallback) {
|
|
325
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1
|
|
326
|
+
? Math.trunc(value)
|
|
327
|
+
: fallback;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Resumable writes' deployment settings, or undefined when they are off.
|
|
331
|
+
* Omitted, they are on exactly when the storage can claim atomically, and a
|
|
332
|
+
* deployment left without them hears so once. Asked for over storage that
|
|
333
|
+
* cannot claim, they throw: at-most-once delivery of an approved write rests
|
|
334
|
+
* on exactly one resume winning the claim, and a read-then-write stand-in
|
|
335
|
+
* would let two win.
|
|
336
|
+
*/
|
|
337
|
+
function resolveResumableWrites(execute, storage, logger) {
|
|
338
|
+
const enabled = execute?.resumableWrites;
|
|
339
|
+
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
340
|
+
throw new Error("ConnectaConfig.execute.resumableWrites must be a boolean");
|
|
341
|
+
}
|
|
342
|
+
if (enabled === false)
|
|
343
|
+
return undefined;
|
|
344
|
+
if (enabled === undefined && typeof storage.compareAndSet !== "function") {
|
|
345
|
+
logger.warn("[connecta] resumable writes are off: the configured storage has no " +
|
|
346
|
+
"compareAndSet, so programs refuse writes (E4) and resume_execution " +
|
|
347
|
+
"answers resumable_writes_unavailable. Use storage with an atomic " +
|
|
348
|
+
"compareAndSet (the Worker example's D1 store, fileStorage, " +
|
|
349
|
+
"memoryStorage) to let programs pause for approval, or set " +
|
|
350
|
+
"execute.resumableWrites: false to keep this and silence the warning.");
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
if (typeof storage.compareAndSet !== "function") {
|
|
354
|
+
throw new Error("ConnectaConfig.execute.resumableWrites needs storage with compareAndSet: " +
|
|
355
|
+
"two resumes of one paused run must send its writes at most once, which " +
|
|
356
|
+
"takes an atomic claim. memoryStorage() and fileStorage() provide it, as " +
|
|
357
|
+
"does the Worker example's D1 store; Cloudflare KV cannot.");
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
maxWrites: positiveWhole(execute?.maxWrites, DEFAULT_MAX_WRITES),
|
|
361
|
+
ttlSeconds: positiveWhole(execute?.pausedRunTtlSeconds, DEFAULT_PAUSED_RUN_TTL_SECONDS),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
227
364
|
/**
|
|
228
365
|
* One-time construction warnings for deployment shapes that run fine but are
|
|
229
366
|
* usually unintended. Warning-only — never throws and never changes behavior;
|
|
@@ -236,7 +373,7 @@ function warnInsecureConfig(config, inboundAuth, logger) {
|
|
|
236
373
|
// Static API headers can carry secrets without declaring credential hooks.
|
|
237
374
|
// Any configured connector warrants the open-deployment warning.
|
|
238
375
|
if (inboundAuth.length === 0 &&
|
|
239
|
-
config.connectors.length > 0) {
|
|
376
|
+
(config.connectors.length > 0 || config.artifacts)) {
|
|
240
377
|
logger.warn("[connecta] running with no inbound authentication: any caller can " +
|
|
241
378
|
"invoke every shared connector. " +
|
|
242
379
|
(hasCredentialConnector || oauthConnectors.length > 0
|
|
@@ -244,6 +381,18 @@ function warnInsecureConfig(config, inboundAuth, logger) {
|
|
|
244
381
|
: "") +
|
|
245
382
|
"Configure `auth` (for example bearerToken(...) or Clerk) to gate access.");
|
|
246
383
|
}
|
|
384
|
+
// Artifact pages mount inside the operator UI and open only for an
|
|
385
|
+
// authenticated viewer; either missing leaves the connector with no page.
|
|
386
|
+
if (config.artifacts && !config.ui) {
|
|
387
|
+
logger.warn("[connecta] artifacts is configured without ui: agents can publish and " +
|
|
388
|
+
"read artifacts, but there is no viewer, so their links answer 404. " +
|
|
389
|
+
"Add ui: operatorUi() to serve artifact pages.");
|
|
390
|
+
}
|
|
391
|
+
else if (config.artifacts && inboundAuth.length === 0) {
|
|
392
|
+
logger.warn("[connecta] artifacts is configured with no inbound authentication: " +
|
|
393
|
+
"artifact pages refuse every request, because there is no team to " +
|
|
394
|
+
"show them to. Configure `auth` to open them.");
|
|
395
|
+
}
|
|
247
396
|
// Unset publicUrl with OAuth connectors: the downstream redirect_uri is
|
|
248
397
|
// derived per-request from the attacker-influenced inbound Host header.
|
|
249
398
|
if (oauthConnectors.length > 0 && !config.publicUrl) {
|
|
@@ -296,18 +445,31 @@ function warnInsecureConfig(config, inboundAuth, logger) {
|
|
|
296
445
|
}
|
|
297
446
|
// A credential test hook that cannot test the declared credential shape.
|
|
298
447
|
// The shape picks the hook (see `credentialTestRule`) and the other one is
|
|
299
|
-
// never substituted, so the
|
|
300
|
-
//
|
|
301
|
-
//
|
|
448
|
+
// never substituted, so the connection offers no Test action in the operator
|
|
449
|
+
// UI and the route answers 400. Without this line the only way to discover
|
|
450
|
+
// the mistake is to click a button that isn't there.
|
|
302
451
|
for (const connector of config.connectors) {
|
|
303
452
|
const { mismatch } = credentialTestRule(connector);
|
|
304
453
|
if (!mismatch)
|
|
305
454
|
continue;
|
|
306
455
|
logger.warn(`[connecta] connector "${connector.id}" cannot test its credential: ` +
|
|
307
|
-
`${describeCredentialTestMismatch(mismatch)}.
|
|
456
|
+
`${describeCredentialTestMismatch(mismatch)}. The connection in the operator UI offers no Test ` +
|
|
308
457
|
`action and POST /ui/credentials/${connector.id}/test answers 400 ` +
|
|
309
458
|
"until the matching hook is implemented.");
|
|
310
459
|
}
|
|
460
|
+
// A vault written before seal/open existed encrypts credentials but cannot
|
|
461
|
+
// seal downstream OAuth state, so tokens stay plaintext at rest beside them.
|
|
462
|
+
const vault = config.vault;
|
|
463
|
+
if (vault && (typeof vault.seal !== "function" || typeof vault.open !== "function")) {
|
|
464
|
+
for (const connector of config.connectors) {
|
|
465
|
+
if (!connector.startAuth)
|
|
466
|
+
continue;
|
|
467
|
+
logger.warn(`[connecta] connector "${connector.id}" stores its downstream OAuth ` +
|
|
468
|
+
"tokens, client registration, and PKCE verifier as plaintext: the " +
|
|
469
|
+
"configured vault implements no `seal`/`open`. Use " +
|
|
470
|
+
"encryptedCredentialVault(...) or add both methods to seal them.");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
311
473
|
// OAuth connectors whose callback cannot perform a state/CSRF check. The
|
|
312
474
|
// public route refuses every callback for these connectors rather than hand
|
|
313
475
|
// an unverified code to finishAuth, so this warning explains why auth cannot
|
|
@@ -331,9 +493,7 @@ export function createConnecta(config) {
|
|
|
331
493
|
'"@cloudflare/codemode" on Workers.');
|
|
332
494
|
}
|
|
333
495
|
const storage = config.storage ?? memoryStorage();
|
|
334
|
-
const logger = config.logger
|
|
335
|
-
? { debug() { }, info() { }, warn() { }, error() { } }
|
|
336
|
-
: config.logger ?? defaultLogger();
|
|
496
|
+
const logger = resolveLogger(config.logger);
|
|
337
497
|
const credentialVault = config.vault;
|
|
338
498
|
const configuredAuth = normalizeAuth(config.auth);
|
|
339
499
|
const serverInfo = {
|
|
@@ -341,7 +501,12 @@ export function createConnecta(config) {
|
|
|
341
501
|
name: config.serverInfo?.name ?? "connecta",
|
|
342
502
|
version: config.serverInfo?.version ?? CONNECTA_VERSION,
|
|
343
503
|
};
|
|
344
|
-
|
|
504
|
+
// The artifacts module contributes one prebuilt connector, appended like any
|
|
505
|
+
// configured one: same catalog, invocation, admission, and activity.
|
|
506
|
+
const connectors = config.artifacts
|
|
507
|
+
? [...config.connectors, config.artifacts.connector]
|
|
508
|
+
: config.connectors;
|
|
509
|
+
const registry = new Registry(connectors, {
|
|
345
510
|
storage,
|
|
346
511
|
logger,
|
|
347
512
|
credentialVault,
|
|
@@ -364,8 +529,12 @@ export function createConnecta(config) {
|
|
|
364
529
|
});
|
|
365
530
|
const inboundAuth = configuredAuth;
|
|
366
531
|
const pools = resolvePools(config.pools, registry);
|
|
532
|
+
const resumable = resolveResumableWrites(config.execute, storage, logger);
|
|
533
|
+
const approval = resolveApprovalPolicy(config.execute?.approval, registry);
|
|
534
|
+
// Exempt writes need a budget whether or not programs can pause.
|
|
535
|
+
const maxWrites = positiveWhole(config.execute?.maxWrites, DEFAULT_MAX_WRITES);
|
|
367
536
|
warnInsecureConfig(config, inboundAuth, logger);
|
|
368
|
-
const requestAdmission = admissionController(config.admission?.requests, REQUEST_ADMISSION_DEFAULTS);
|
|
537
|
+
const requestAdmission = admissionController(config.admission?.requests, REQUEST_ADMISSION_DEFAULTS, config.admission?.requests?.maxDurationMs ?? DEFAULT_REQUEST_MAX_DURATION_MS);
|
|
369
538
|
const configuredCodeAdmission = admissionController(config.admission?.code, CODE_ADMISSION_DEFAULTS);
|
|
370
539
|
let codeAdmission;
|
|
371
540
|
let executor = config.executor;
|
|
@@ -381,17 +550,91 @@ export function createConnecta(config) {
|
|
|
381
550
|
"implements acquire() and owns its admission pool; configure that " +
|
|
382
551
|
"executor's concurrency and queue options instead.");
|
|
383
552
|
}
|
|
553
|
+
if (config.artifacts?.bindRefresh) {
|
|
554
|
+
const sharedIds = registry.listConnectors()
|
|
555
|
+
.filter((connector) => connector.id !== "artifacts" && connector.authScope !== "personal")
|
|
556
|
+
.map((connector) => connector.id);
|
|
557
|
+
const refreshConfig = {
|
|
558
|
+
discoveryConcurrency: config.discovery?.concurrency,
|
|
559
|
+
probeTimeoutMs: config.discovery?.probeTimeoutMs,
|
|
560
|
+
maxEmittedBytes: config.execute?.maxEmittedBytes,
|
|
561
|
+
maxEmittedBlocks: config.execute?.maxEmittedBlocks,
|
|
562
|
+
maxHostCalls: config.execute?.maxHostCalls,
|
|
563
|
+
hostCallTimeoutMs: config.execute?.hostCallTimeoutMs,
|
|
564
|
+
watchdogMs: config.execute?.watchdogMs,
|
|
565
|
+
failOnInvocationFailure: true,
|
|
566
|
+
// A connector's own `approval: "never"` still applies when policy maps
|
|
567
|
+
// are empty. Override every shared connector explicitly for refresh.
|
|
568
|
+
approval: {
|
|
569
|
+
connectors: new Map(sharedIds.map((id) => [id, "ask"])),
|
|
570
|
+
tools: new Map(),
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
const refreshRunner = createProgramRunner(registry.scoped({ connectorIds: [] }), config.publicUrl, executor, logger, undefined, refreshConfig);
|
|
574
|
+
config.artifacts.bindRefresh({
|
|
575
|
+
claimMs: refreshRunner.claimMs,
|
|
576
|
+
execute: async (program, owner, signal) => {
|
|
577
|
+
if (!owner)
|
|
578
|
+
throw new Error("Refresh owner is missing; reconfigure this program.");
|
|
579
|
+
let access = parseConnectorAccess(config.identity?.connectorAccess
|
|
580
|
+
? await config.identity.connectorAccess(owner.identity) : "all", { allowReadOnly: true });
|
|
581
|
+
if (owner.pool) {
|
|
582
|
+
const pool = pools.get(owner.pool);
|
|
583
|
+
if (!pool || await pool.grant(owner.identity) !== true) {
|
|
584
|
+
throw new Error("Refresh owner's pool grant is no longer available.");
|
|
585
|
+
}
|
|
586
|
+
access = intersectAccess(access, pool.access);
|
|
587
|
+
}
|
|
588
|
+
if (access.connectorIds !== "all" && !access.connectorIds.includes("artifacts")) {
|
|
589
|
+
throw new Error("Refresh owner no longer has artifact publishing access.");
|
|
590
|
+
}
|
|
591
|
+
if (access.toolAccess?.get("artifacts") &&
|
|
592
|
+
!access.toolAccess.get("artifacts").has("set_refresh")) {
|
|
593
|
+
throw new Error("Refresh owner no longer has refresh configuration access.");
|
|
594
|
+
}
|
|
595
|
+
if (access.guardedToolAccess?.get("artifacts")?.has("set_refresh")) {
|
|
596
|
+
throw new Error("A read-only guarded grant cannot configure refresh.");
|
|
597
|
+
}
|
|
598
|
+
access = intersectAccess(access, { connectorIds: sharedIds });
|
|
599
|
+
if (signal.aborted)
|
|
600
|
+
throw new Error("Refresh deadline expired before execution.");
|
|
601
|
+
const view = registry.scoped({ connectorIds: access.connectorIds,
|
|
602
|
+
...(access.toolAccess ? { toolAccess: access.toolAccess } : {}),
|
|
603
|
+
...(access.guardedToolAccess ? { guardedToolAccess: access.guardedToolAccess } : {}) });
|
|
604
|
+
const runner = createProgramRunner(view, config.publicUrl, executor, logger, undefined, refreshConfig);
|
|
605
|
+
return runner.execute({ code: program }, { signal });
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// Every structural check has passed, so this is the configuration the
|
|
610
|
+
// deployment runs with. Creating the runtime builds nothing — a Worker may
|
|
611
|
+
// construct its Connecta at global scope, where no fiber may start.
|
|
612
|
+
const runtime = createCoreRuntime(registry, {
|
|
613
|
+
storage,
|
|
614
|
+
logger,
|
|
615
|
+
vault: credentialVault,
|
|
616
|
+
activity: config.activity,
|
|
617
|
+
config: {
|
|
618
|
+
config,
|
|
619
|
+
serverInfo,
|
|
620
|
+
auth: inboundAuth,
|
|
621
|
+
pools,
|
|
622
|
+
executorName: configuredExecutorName,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
384
625
|
const handler = createFetchHandler({
|
|
385
626
|
registry,
|
|
386
627
|
auth: inboundAuth,
|
|
387
628
|
identity: config.identity,
|
|
388
629
|
pools,
|
|
389
630
|
publicUrl: config.publicUrl,
|
|
631
|
+
artifactOrigin: config.artifactOrigin,
|
|
390
632
|
allowedOrigins: config.allowedOrigins,
|
|
391
633
|
serverInfo,
|
|
392
634
|
logger,
|
|
393
635
|
activity: config.activity?.store,
|
|
394
636
|
activityModule: config.activity,
|
|
637
|
+
artifactsModule: config.artifacts,
|
|
395
638
|
activityReadGate: config.activity?.readGate,
|
|
396
639
|
activityDeploymentId: config.activity?.deploymentId,
|
|
397
640
|
executor,
|
|
@@ -404,6 +647,10 @@ export function createConnecta(config) {
|
|
|
404
647
|
maxEmittedBlocks: config.execute?.maxEmittedBlocks,
|
|
405
648
|
maxHostCalls: config.execute?.maxHostCalls,
|
|
406
649
|
hostCallTimeoutMs: config.execute?.hostCallTimeoutMs,
|
|
650
|
+
watchdogMs: config.execute?.watchdogMs,
|
|
651
|
+
resumable,
|
|
652
|
+
approval,
|
|
653
|
+
maxWrites,
|
|
407
654
|
credentialVault,
|
|
408
655
|
ui: config.ui,
|
|
409
656
|
deploymentInfo: config.deploymentInfo,
|
|
@@ -417,10 +664,18 @@ export function createConnecta(config) {
|
|
|
417
664
|
registry,
|
|
418
665
|
close: async () => {
|
|
419
666
|
closePromise ??= Promise.resolve().then(async () => {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
667
|
+
try {
|
|
668
|
+
requestAdmission.close();
|
|
669
|
+
codeAdmission?.close();
|
|
670
|
+
registry.closeCallAdmission();
|
|
671
|
+
await config.executor?.close?.();
|
|
672
|
+
}
|
|
673
|
+
finally {
|
|
674
|
+
// Last, and whether or not the executor closed cleanly. A fiber
|
|
675
|
+
// already running keeps the services it was given; only a run
|
|
676
|
+
// started after this is refused.
|
|
677
|
+
await disposeEdgeRuntime(runtime);
|
|
678
|
+
}
|
|
424
679
|
});
|
|
425
680
|
await closePromise;
|
|
426
681
|
},
|