@zackbart/connecta 0.36.2 → 0.37.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/version.mjs +1 -1
  3. package/dist/auth/downstream-oauth.js +31 -12
  4. package/dist/auth/static-oauth.js +3 -2
  5. package/dist/auth/stored-observation.js +8 -0
  6. package/dist/call-admission.js +27 -14
  7. package/dist/connectors/api-connector.js +4 -0
  8. package/dist/connectors/option-shapes.js +5 -1
  9. package/dist/connectors/remote-mcp.d.ts +29 -3
  10. package/dist/connectors/remote-mcp.js +142 -22
  11. package/dist/execute.js +5 -5
  12. package/dist/executor-admission.js +27 -13
  13. package/dist/executors/quickjs.js +4 -1
  14. package/dist/index.d.ts +2 -1
  15. package/dist/invocation.js +6 -7
  16. package/dist/operator-ui/generated.js +2 -2
  17. package/dist/operator-ui/model.d.ts +2 -0
  18. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  19. package/dist/request-principal.js +34 -0
  20. package/dist/request-token.d.ts +46 -0
  21. package/dist/routes/mcp.js +6 -6
  22. package/dist/routes/ui-config.js +38 -9
  23. package/dist/routes/ui-data.js +30 -5
  24. package/dist/routes/ui.js +10 -2
  25. package/dist/runtime/settlement.js +59 -0
  26. package/dist/version.d.ts +1 -1
  27. package/dist/version.js +1 -1
  28. package/dist/worker.js +4 -1
  29. package/documentation/architecture.md +68 -25
  30. package/documentation/auth.md +129 -11
  31. package/documentation/code-mode.md +25 -5
  32. package/documentation/integrating.md +18 -4
  33. package/documentation/operator-tests.md +19 -0
  34. package/documentation/operator-ui.md +57 -46
  35. package/examples/worker/README.md +5 -1
  36. package/guides/connecting-agents.md +6 -0
  37. package/guides/safety-and-trust.md +1 -0
  38. package/package.json +1 -1
  39. package/templates/node/package.json +1 -1
@@ -57,6 +57,8 @@ interface UiCredential {
57
57
  notice?: string;
58
58
  }
59
59
  export interface UiConnector {
60
+ /** Complete catalog and status from this detail request, when supported. */
61
+ overlay?: OperatorConnectorOverlay;
60
62
  id: string;
61
63
  /** Downstream authentication owner. Older payload fixtures omit it as shared. */
62
64
  authScope?: "shared" | "personal";
@@ -55,4 +55,4 @@ export declare const PLANNING_CENTER_MAX_RESPONSE_BYTES: number;
55
55
  */
56
56
  export declare const PLANNING_CENTER_ADMISSION: ConnectorCallAdmissionPolicy;
57
57
  /** Planning Center refuses a request without a descriptive one (HTTP 403). */
58
- export declare const DEFAULT_USER_AGENT = "connecta/0.36.2 (+https://github.com/zackbart/connecta)";
58
+ export declare const DEFAULT_USER_AGENT = "connecta/0.37.0 (+https://github.com/zackbart/connecta)";
@@ -0,0 +1,34 @@
1
+ // The admitted principal a request-auth resolver may receive (#1103).
2
+ //
3
+ // Only the caller core attached from the admitting authorization counts
4
+ // (`callerOf`, see connector-caller.ts). No argument, program or header can
5
+ // name it, and a context no request admitted has none. Nothing here
6
+ // invents a delegated identity: a caller that is not an admitted person, or a
7
+ // declared agent the deployment explicitly accepts, is refused before the
8
+ // resolver runs.
9
+ import { isAgentName } from "./agent-identity.js";
10
+ import { callerOf } from "./connector-caller.js";
11
+ import { validIdentityReference } from "./identity.js";
12
+ /**
13
+ * The principal this operation's admitted caller is. Anything else is refused
14
+ * through `refuse`, the connector's own `auth_required` error.
15
+ */
16
+ export function requestPrincipalFor(policy, ctx, refuse) {
17
+ const caller = callerOf(ctx);
18
+ // An operator check, a context no request admitted, or an open deployment's anonymous caller.
19
+ if (!caller?.authenticated)
20
+ throw refuse("acts as the admitted caller, and this operation has none");
21
+ const { identity } = caller;
22
+ if (identity.agent !== undefined) {
23
+ if (policy !== "human-or-agent")
24
+ throw refuse("refuses agents unless deployment code accepts them");
25
+ if (!isAgentName(identity.agent) || identity.principal !== undefined || !caller.pool)
26
+ throw refuse("acts as the admitted caller, and refuses this one");
27
+ return Object.freeze({ kind: "agent", name: identity.agent, pool: caller.pool });
28
+ }
29
+ const principal = identity.principal;
30
+ if (!validIdentityReference(principal))
31
+ throw refuse("acts as the admitted caller, and refuses this one");
32
+ // Fixed key order: `JSON.stringify` of the frozen result is its partition key.
33
+ return Object.freeze({ kind: "human", namespace: principal.namespace, id: principal.id });
34
+ }
@@ -5,3 +5,49 @@ export interface RequestToken {
5
5
  /** Drop only the rejected token. This hook never authorizes replay. */
6
6
  invalidate?: (rejected: string, ctx: ConnectorContext) => Promise<void>;
7
7
  }
8
+ /**
9
+ * A person inbound auth admitted, as the `{ namespace, id }` a member
10
+ * reference resolves to. Never taken from arguments, programs or headers.
11
+ */
12
+ export interface HumanRequestPrincipal {
13
+ readonly kind: "human";
14
+ readonly namespace: string;
15
+ readonly id: string;
16
+ }
17
+ /** A declared agent and the pool it was admitted through. */
18
+ export interface AgentRequestPrincipal {
19
+ readonly kind: "agent";
20
+ readonly name: string;
21
+ readonly pool: string;
22
+ }
23
+ /** The admitted caller a principal-forwarding request resolver receives. */
24
+ export type RequestPrincipal = HumanRequestPrincipal | AgentRequestPrincipal;
25
+ /**
26
+ * Which admitted callers a principal-forwarding request resolver accepts.
27
+ * `"human"` refuses agents; `"human-or-agent"` also accepts declared agents.
28
+ */
29
+ export type RequestPrincipalPolicy = "human" | "human-or-agent";
30
+ /**
31
+ * Opt-in principal forwarding for request auth: the resolver receives the
32
+ * admitted caller, and every other caller is refused before it runs.
33
+ */
34
+ export type PrincipalForwarding = {
35
+ accept: "human";
36
+ token: PrincipalRequestToken<HumanRequestPrincipal>;
37
+ } | {
38
+ accept: "human-or-agent";
39
+ token: PrincipalRequestToken;
40
+ };
41
+ /** Per-resolution input for a principal-forwarding resolver. */
42
+ export interface RequestTokenInput<P extends RequestPrincipal = RequestPrincipal> {
43
+ readonly principal: P;
44
+ }
45
+ /**
46
+ * A request-local Bearer resolver that receives the admitted principal.
47
+ * Return the same token for the same principal while it is valid.
48
+ */
49
+ export interface PrincipalRequestToken<P extends RequestPrincipal = RequestPrincipal> {
50
+ (ctx: ConnectorContext, input: RequestTokenInput<P>): Promise<string>;
51
+ /** Drop only the rejected token for this principal. This hook never authorizes replay. */
52
+ invalidate?: (rejected: string, ctx: ConnectorContext, input: RequestTokenInput<P>) => Promise<void>;
53
+ }
@@ -267,12 +267,12 @@ function scopedToBody(handler, signal) {
267
267
  /**
268
268
  * The request's admission permit, owned by its Scope.
269
269
  *
270
- * Admission goes through the controller's Promise rather than its Effect
271
- * program, on purpose. The pool is shared by every request
272
- * in the isolate, and a queued request is handed its permit from inside the
273
- * request whose body just ended. A fiber resumed there would authorize and
274
- * serve this request in that one's I/O context, which workerd refuses;
275
- * awaiting `acquire()`'s promise resumes it in its own.
270
+ * The pool is shared by every request in the isolate, and a queued request is
271
+ * handed its permit from inside the request whose body just ended. The queue
272
+ * wakes it through a Promise its own request created
273
+ * (src/runtime/settlement.ts), and this fiber awaits `acquire()`'s Promise, so
274
+ * it authorizes and serves this request in its own I/O context, never that
275
+ * one's, which workerd refuses.
276
276
  *
277
277
  * The wait stays interruptible, and a permit granted just as it was
278
278
  * interrupted still arrives, later, to be released on arrival.
@@ -1,3 +1,4 @@
1
+ import { storedAuthObservation } from "../auth/stored-observation.js";
1
2
  import { agentDescriptions } from "../agent-description.js";
2
3
  import { readAgentAdmission } from "../agent-observation.js";
3
4
  // `GET /ui/api/config`: operator contract v2 (#837, decisions/0006), the
@@ -25,6 +26,8 @@ import { workspaceSubjectOf } from "../workspace-subject.js";
25
26
  import { authorized, refuse, scopeFor, serveOperator } from "./operator.js";
26
27
  import { isMaintainer, isMember } from "../membership.js";
27
28
  import { admitsMaintainer, mayManageConnector, privateJson, readsActivity } from "./shared.js";
29
+ const cachedMaintainers = new WeakSet();
30
+ const cachedMode = (context) => context.url.searchParams.get("live") === "cached";
28
31
  const attempt = (operation) => Effect.tryPromise({ try: operation, catch: (error) => error });
29
32
  const validName = (name) => recordedToolName({ name }) === name;
30
33
  const classified = (tool) => tool.classification === "read" ? "read" : "write";
@@ -65,7 +68,7 @@ function probe(context, registry, connector, scope, work, fallback) {
65
68
  */
66
69
  function bounded(context, read, fallback) {
67
70
  return withDeadlineEffect(() => read(), {
68
- timeoutMs: context.opts.config.discovery.probeTimeoutMs,
71
+ timeoutMs: cachedMaintainers.has(context) ? 500 : context.opts.config.discovery.probeTimeoutMs,
69
72
  signal: context.request.signal,
70
73
  timeoutError: new Error("Operator config read timed out."),
71
74
  }).pipe(Effect.catchDefect((defect) => Effect.fail(defect)), Effect.catch(fallback));
@@ -141,7 +144,7 @@ const isTimestamp = (value) => /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(v
141
144
  * declared agent's `lastSeenAt` from the rows this maintainer may see. Agent
142
145
  * rows are recorded under the access-token directory with the agent's name.
143
146
  */
144
- function scanActivity(context, authz, registry, rows, agents) {
147
+ export function scanOperatorActivity(context, authz, registry, rows, agents) {
145
148
  const activity = context.opts.config.activity;
146
149
  if (!activity?.store.list)
147
150
  return Effect.succeed("unconfigured");
@@ -232,6 +235,12 @@ function sharedSource(context, connector, row) {
232
235
  // Only a connector asking to be authorized has no grant; any other
233
236
  // state, a credential prompt or an error included, has one. A grant
234
237
  // carries no saved-at time of its own.
238
+ if (cachedMode(context)) {
239
+ const observe = storedAuthObservation(connector);
240
+ return observe
241
+ ? bounded(context, () => attempt(() => observe(opts.registry.contextFor(connector.id, context.baseUrl, {}))).pipe(Effect.map((ok) => (ok ? { kind: "shared", source: "oauth", savedAt: null } : MISSING))), () => Effect.succeed(MISSING))
242
+ : Effect.succeed(MISSING);
243
+ }
235
244
  return Effect.succeed(row?.status === "auth_required" ? MISSING : { kind: "shared", source: "oauth", savedAt: null });
236
245
  }
237
246
  if (described?.auth?.mode === "none" && described.source.kind === "remote-mcp") {
@@ -258,6 +267,13 @@ function connectedPeople(context, connector, people) {
258
267
  ? yield* bounded(context, () => attempt(() => vault.metadata(connector.id, principalKey)).pipe(Effect.map((metadata) => storedCredentialShape(credential, metadata?.fields ?? null).state === "valid")), () => Effect.succeed(false))
259
268
  : false;
260
269
  }
270
+ else if (cachedMode(context)) {
271
+ const registry = opts.registry.scoped({ connectorIds: [connector.id], principalKey });
272
+ const observe = storedAuthObservation(connector);
273
+ ok = observe
274
+ ? yield* bounded(context, () => attempt(() => observe(registry.contextFor(connector.id, baseUrl, {}))), () => Effect.succeed(false))
275
+ : false;
276
+ }
261
277
  else {
262
278
  const registry = opts.registry.scoped({ connectorIds: [connector.id], principalKey });
263
279
  const scope = {};
@@ -354,11 +370,14 @@ function maintainerRead(context, authz) {
354
370
  for (const person of [...opts.maintainers, ...[...opts.pools.values()].flatMap(humansOf)])
355
371
  people.set(referenceKey(person), person);
356
372
  const requestActivity = operatorActivityContext(context, authz);
373
+ const cached = cachedMode(context);
357
374
  const [rows, label, slots] = yield* Effect.all([
358
- Effect.forEach(visible, (c) => observe(context, registry, c, requestActivity), {
359
- concurrency: resolveDiscoveryConcurrency(opts.config.discovery.concurrency),
360
- }),
361
- humanLabels(context, authz, [...people.values()]),
375
+ cached
376
+ ? Effect.succeed([])
377
+ : Effect.forEach(visible, (c) => observe(context, registry, c, requestActivity), {
378
+ concurrency: resolveDiscoveryConcurrency(opts.config.discovery.concurrency),
379
+ }),
380
+ bounded(context, () => humanLabels(context, authz, [...people.values()]), () => Effect.succeed(() => null)),
362
381
  // Every declared agent's slot, exactly as GET /ui/access-tokens lists
363
382
  // it. Without them the agents list cannot be whole, so a failed read
364
383
  // fails the view rather than dropping agents from it.
@@ -399,7 +418,7 @@ function maintainerRead(context, authz) {
399
418
  token: tokenSlots.get(name) ?? null,
400
419
  lastSeenAt: null,
401
420
  }));
402
- const activity = yield* scanActivity(context, authz, registry, rows, agents);
421
+ const activity = yield* scanOperatorActivity(context, authz, registry, rows, agents);
403
422
  let admissionsVisible = readsActivity(opts, authz) && activity !== "forbidden";
404
423
  // A configured read gate still applies when the history store has no list.
405
424
  if (admissionsVisible && activity === "unconfigured" && opts.config.activity?.readGate) {
@@ -572,8 +591,18 @@ function memberRead(context, authz, pools) {
572
591
  function configRead(context) {
573
592
  return Effect.gen(function* () {
574
593
  const authz = yield* authorized(context);
575
- if (admitsMaintainer(context.opts, authz))
576
- return yield* maintainerRead(context, authz);
594
+ if (admitsMaintainer(context.opts, authz)) {
595
+ if (!cachedMode(context))
596
+ return yield* maintainerRead(context, authz);
597
+ cachedMaintainers.add(context);
598
+ // The entire metadata build stays below the browser admission deadline,
599
+ // even for a store that ignores cancellation or an unusually large roster.
600
+ return yield* withDeadlineEffect(() => maintainerRead(context, authz), {
601
+ timeoutMs: 5000,
602
+ signal: context.request.signal,
603
+ timeoutError: new Error("Operator metadata timed out."),
604
+ }).pipe(Effect.catch(() => refuse("operator metadata could not be read", 503)));
605
+ }
577
606
  const pools = memberPools(context, authz);
578
607
  if (pools.length > 0)
579
608
  return yield* memberRead(context, authz, pools);
@@ -1,3 +1,4 @@
1
+ import { describedTools } from "../described.js";
1
2
  // The Effect core of the operator payload: `buildUiData` in src/ui.ts is its
2
3
  // Promise face, and the `/ui/connectors/<id>` route yields it directly.
3
4
  //
@@ -12,7 +13,7 @@ import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentia
12
13
  import { failureRecord, failureStatus, logFailure, recordedToolName, statusFailure } from "../operator-record.js";
13
14
  import { closeScope } from "../runtime/connector-scope.js";
14
15
  import { withDeadlineEffect } from "../runtime/run.js";
15
- import { uiProblemFor, uiToolSafety } from "../ui.js";
16
+ import { uiProblemFor } from "../ui.js";
16
17
  import { CONNECTA_VERSION } from "../version.js";
17
18
  const DETAIL_TIMEOUT_MS = 30_000;
18
19
  const DETAIL_TIMED_OUT = "Connection details timed out. Retry this connection.";
@@ -63,11 +64,13 @@ export function uiData(registry, baseUrl, options) {
63
64
  return yield* attempt(async () => (await registry.getTools(c.id, baseUrl, requestScope, {
64
65
  signal,
65
66
  ...(options.defer ? { defer: options.defer } : {}),
66
- })).map((t) => ({
67
+ }))
68
+ .flatMap((t) => describedTools([t]))
69
+ .map((t) => ({
70
+ ...t,
67
71
  name: recordedToolName(t),
68
72
  address: `${c.id}.${recordedToolName(t)}`,
69
- ...(t.description ? { description: t.description } : {}),
70
- safety: uiToolSafety(t),
73
+ classification: t.classification === "read" ? "read" : "write",
71
74
  }))).pipe(Effect.map((tools) => ({ status, tools, catalogFailed: false })),
72
75
  // The registry owns the failed catalog observation; the page only
73
76
  // needs to know there was one.
@@ -169,7 +172,21 @@ export function uiData(registry, baseUrl, options) {
169
172
  status: status.state,
170
173
  ...(problem ? { problem } : {}),
171
174
  toolCount: tools.length,
172
- tools,
175
+ tools: tools.map((tool) => ({
176
+ name: tool.name,
177
+ address: tool.address,
178
+ ...(tool.description ? { description: tool.description } : {}),
179
+ safety: tool.classification === "read" ? "runs_in_programs" : "needs_approval",
180
+ })),
181
+ overlay: {
182
+ id: c.id,
183
+ status: status.state,
184
+ tools,
185
+ ...(problem ? { problem } : {}),
186
+ ...(status.registrationPath ? { auth: { registrationPath: status.registrationPath } } : {}),
187
+ catalogAgeMs: registry.catalogAgeMs(c.id),
188
+ lastCall: null,
189
+ },
173
190
  // Counts only, and only when a refresh in this runtime produced them.
174
191
  // `Registry.withCatalogObservations` rebuilt the report through
175
192
  ...(status.catalogAccess ? { catalogAccess: status.catalogAccess } : {}),
@@ -193,6 +210,14 @@ export function uiData(registry, baseUrl, options) {
193
210
  oauth: Boolean(c.startAuth && c.disconnectAuth),
194
211
  toolCount: 0,
195
212
  tools: [],
213
+ overlay: {
214
+ id: c.id,
215
+ status: "error",
216
+ problem: "connector_unavailable",
217
+ tools: [],
218
+ catalogAgeMs: registry.catalogAgeMs(c.id),
219
+ lastCall: null,
220
+ },
196
221
  };
197
222
  });
198
223
  const row = (c) => Effect.suspend(() => {
package/dist/routes/ui.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { withDeadlineEffect } from "../runtime/run.js";
1
2
  import { operatorAsset } from "../operator-ui/assets.js";
2
3
  import { operatorActivityContext } from "./activity-context.js";
3
4
  import { isSafeHttpsUrl } from "../branding.js";
@@ -10,7 +11,7 @@ import { CONNECTA_FAVICON_ICO } from "../favicon.js";
10
11
  import { CONNECTA_FAVICON_SVG, operatorPageForPath, renderUiHtml } from "../ui.js";
11
12
  import { authorized, refuse, scopeFor, serveOperator, visibleRegistry, } from "./operator.js";
12
13
  import { managesAgentTokens, mayManageConnector, privateJson, readsActivity } from "./shared.js";
13
- import { routeUiConfig } from "./ui-config.js";
14
+ import { routeUiConfig, scanOperatorActivity } from "./ui-config.js";
14
15
  import { uiData } from "./ui-data.js";
15
16
  /**
16
17
  * Headers that make an operator-supplied favicon body inert on this origin.
@@ -160,7 +161,14 @@ function connectorDetail(context, id) {
160
161
  timeoutMs: opts.config.discovery.probeTimeoutMs,
161
162
  signal: request.signal,
162
163
  });
163
- return privateJson({ ...data.connectors[0], permissions: permissions(connector) });
164
+ const detail = data.connectors[0];
165
+ if (detail.overlay)
166
+ yield* withDeadlineEffect(() => scanOperatorActivity(context, authz, registry, [detail.overlay], []), {
167
+ timeoutMs: 500,
168
+ signal: request.signal,
169
+ timeoutError: new Error("Activity observation timed out."),
170
+ }).pipe(Effect.catch(() => Effect.succeed("unavailable")));
171
+ return privateJson({ ...detail, permissions: permissions(connector) });
164
172
  });
165
173
  }
166
174
  /** The summary: every visible connector as "loading", with no probe at all. */
@@ -0,0 +1,59 @@
1
+ // One outcome that a fiber in one request waits on while another request may
2
+ // deliver it: a queued admission permit, a shared token refresh.
3
+ //
4
+ // Effect resumes a waiting fiber synchronously, inside whatever completes the
5
+ // Deferred it awaits, and runs it in the async context it suspended in: the
6
+ // fiber captured a `node:async_hooks` AsyncResource when it suspended and
7
+ // resumes through `runInAsyncScope`. On Workers that context belongs to the
8
+ // waiter's request. workerd refuses to enter it from another request ("Cannot
9
+ // call this AsyncLocalStorage bound function outside of the request in which
10
+ // it was created.") when the Worker's compatibility date is 2025-06-16 or
11
+ // later, in releases since September 2026. The refusal throws out of the call
12
+ // that completed the Deferred, failing the request that released a slot, and
13
+ // the waiter never resumes (#1106). Awaiting a Promise around that wait is no
14
+ // help: the fiber waiting on the Deferred inside the Promise is resumed in the
15
+ // completing request just the same.
16
+ //
17
+ // So nothing here resumes a fiber. `settle` records the outcome and resolves,
18
+ // for each waiter, a Promise that the waiter's own request created. workerd
19
+ // runs a Promise's reactions in the request that created it, whichever
20
+ // request resolves it; Node runs them one microtask later in the waiter's
21
+ // async context. The waiter resumes there. The record itself is synchronous,
22
+ // so a queue's arbitration reads the outcome at once, and a waiter that leaves
23
+ // before it wakes can still give back what it was handed.
24
+ import { Effect } from "effect";
25
+ export class Settlement {
26
+ exit;
27
+ wakers = new Set();
28
+ /** The recorded outcome; undefined until `settle` runs. */
29
+ get outcome() {
30
+ return this.exit;
31
+ }
32
+ /** Record the first outcome and wake each waiter in its own request. */
33
+ settle(exit) {
34
+ if (this.exit)
35
+ return false;
36
+ this.exit = exit;
37
+ const wakers = [...this.wakers];
38
+ this.wakers.clear();
39
+ for (const wake of wakers)
40
+ wake();
41
+ return true;
42
+ }
43
+ /** The outcome, waited for by a fiber that resumes in its own request. */
44
+ wait() {
45
+ return Effect.suspend(() => this.exit ??
46
+ Effect.callback((resume) => {
47
+ let wake;
48
+ // Created here, by the waiting fiber, so it belongs to the waiting
49
+ // request whichever request resolves it.
50
+ void new Promise((resolve) => {
51
+ wake = resolve;
52
+ }).then(() => resume(this.exit));
53
+ this.wakers.add(wake);
54
+ return Effect.sync(() => {
55
+ this.wakers.delete(wake);
56
+ });
57
+ }));
58
+ }
59
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const CONNECTA_VERSION = "0.36.2";
1
+ export declare const CONNECTA_VERSION = "0.37.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.36.2";
3
+ export const CONNECTA_VERSION = "0.37.0";
package/dist/worker.js CHANGED
@@ -45,10 +45,13 @@ function routeGuestImports(source) {
45
45
  }
46
46
  /** Loader-only upstream sandbox whose lease disposes handles at run end. */
47
47
  export function workerExecutor(options) {
48
+ // One pool per executor, so per isolate. A queued run may wait out one
49
+ // default guest deadline: programs spend it awaiting host calls, and a
50
+ // five-second wait refused nearly every run queued behind one (#1106).
48
51
  const admission = new AdmissionController(options.admission ?? {
49
52
  concurrency: 2,
50
53
  maxQueueSize: 8,
51
- queueTimeoutMs: 5_000,
54
+ queueTimeoutMs: 60_000,
52
55
  retryAfterMs: 1_000,
53
56
  });
54
57
  const active = new Set();
@@ -1753,6 +1753,20 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1753
1753
  Native transport parity, OAuth and write-provenance suites retain their
1754
1754
  workerd coverage. The shared throwaway RSA key in `test/fixtures/rsa.ts` is
1755
1755
  used by write-provenance and unknown-write-advice fixtures.
1756
+ - `test/executor-requests.test.ts` runs overlapping `execute_code` programs
1757
+ as separate workerd requests through the real Worker Loader executor, with
1758
+ real TCP host calls that read their own request's signal. Queued programs
1759
+ and the programs whose release admitted them return their own answers; queue
1760
+ refusals keep the retryable shape. Its fixture applies workerd's request
1761
+ binding of async-context frames, which the pinned runtime predates, through a
1762
+ signal each request owns, so it refuses another request's entry with or
1763
+ without an async-context token, as workerd does; pointing
1764
+ `MINIFLARE_WORKERD_PATH` at a newer workerd runs the native check.
1765
+ `test/executor-native.test.ts` shows that a slot granted just before
1766
+ `close()` never starts a program on the native Worker Loader.
1767
+ `test/request-context.node.test.ts` pins the same wake order and async context
1768
+ on Node for executor and call admission and QuickJS host calls, and gives back
1769
+ grants that shutdown overtook.
1756
1770
  - The request, catalog, auxiliary-call and renewal lifetime
1757
1771
  suites, plus `test/auth-admission-binding.test.ts`, exercise cancellation,
1758
1772
  held native work and garbage collection. These test actual request ownership;
@@ -1786,7 +1800,16 @@ in-memory token caching and exact-token invalidation, with an absent subject
1786
1800
  for service accounts acting as themselves. Workspace providers keep supplying
1787
1801
  subjects and retain their delegated error guidance. `src/request-token.ts`
1788
1802
  owns the callable resolver contract and optional HTTP-401 invalidation hook;
1789
- `remoteMcp()` invokes it without replaying the failed operation.
1803
+ `remoteMcp()` invokes it without replaying the failed operation. With
1804
+ `auth.principal`, `src/request-principal.ts` reads the admitted caller only
1805
+ through `callerOf` (see `src/connector-caller.ts`), refuses callers the policy
1806
+ does not accept before resolving, and gives `remoteMcp()` a principal key that
1807
+ joins the credential digest, so no client crosses principals. A custom `fetch`
1808
+ replaces global fetch and the owned write sender in that connector's transport,
1809
+ behind the same redirect wrapper and a same-origin guard.
1810
+ `test/remote-mcp-principal-forwarding.test.ts` checks admission, forging,
1811
+ client replacement, transport confinement and secrecy on Node, and a real
1812
+ service binding on workerd.
1790
1813
  `test/google-service-account-token.test.ts` exercises signing/cache behavior on
1791
1814
  Node and workerd; `test/google-service-account-remote.node.test.ts` checks
1792
1815
  real-socket token/MCP requests, invalidation and one physical attempt per write.
@@ -2026,11 +2049,13 @@ existing execute pipeline, using its request identity and call capabilities.
2026
2049
  The executor seam creates no second request or call owner.
2027
2050
 
2028
2051
  Both built-ins acquire their permits through native admission and return
2029
- execute-once leases with synchronous, idempotent release. A queued standalone
2030
- execution crosses `awaitExecutorLease`'s Promise handoff before host setup, so
2031
- workerd resumes in the waiting request. `execute_code` retains its public
2032
- `acquire()` handoff for that same reason. A cancelled queued handoff withdraws
2033
- its waiter, and a late custom acquisition releases its abandoned lease.
2052
+ execute-once leases with synchronous, idempotent release. Admission wakes a
2053
+ queued waiter through a Settlement, so it resumes in its own request
2054
+ ([across requests](#across-requests)). A queued standalone execution also
2055
+ crosses `awaitExecutorLease`'s Promise handoff before host setup, and
2056
+ `execute_code` retains its public `acquire()` handoff. A cancelled queued
2057
+ handoff withdraws its waiter, and a late custom acquisition releases its
2058
+ abandoned lease.
2034
2059
 
2035
2060
  The Worker SDK prepares source and dispatchers against an inert loader. The
2036
2061
  native host then acquires the actual Loader handle and RPC entrypoint with
@@ -2046,7 +2071,9 @@ Both built-ins normalize provider leaf returns with `Promise.resolve`, retaining
2046
2071
  direct JavaScript callers' synchronous value tolerance and original host
2047
2072
  rejections. The published provider-function signature remains Promise-shaped.
2048
2073
 
2049
- QuickJS owns host-call fibers in each active run's Scope. Completion,
2074
+ QuickJS owns host-call fibers in each active run's Scope, started in the
2075
+ async context the run captured: a warm child's IPC events arrive in the
2076
+ context of the request that spawned it. Completion,
2050
2077
  interruption, cancellation, recycling and shutdown revoke provider lookup and
2051
2078
  interrupt those fibers; late SDK results and stale IPC cannot reply to another
2052
2079
  run. Releasing a lease during readiness or crash backoff also cancels the old
@@ -2231,24 +2258,40 @@ request's abort handler also releases them idempotently if workerd discards the
2231
2258
  pending continuation on disconnect. Membership cache
2232
2259
  hits precede these bounds; see [organization admission](./auth.md#one-clerk-instance-one-organization-per-deployment).
2233
2260
 
2234
- Asynchronous work shared across requests meets only through a Deferred that the owning
2235
- request completes, never through a fiber that outlives its request. Two
2236
- workerd rules follow, and both have bitten.
2237
-
2238
- **Waiting on a shared Deferred is its own edge.** Deferred resumes a waiting
2239
- fiber synchronously, inside the call that completed it, which on Workers means
2240
- inside the completing request's I/O context. A waiter that goes on to touch its
2241
- own body, transport, or storage there fails with "Cannot perform I/O on behalf
2242
- of a different request", or hangs until its deadline. So the wait runs through
2243
- `runEdge`, and the waiter does its own I/O after awaiting that promise, which
2244
- workerd resumes in the waiter's request as it does any promise resolved from
2245
- another. The same holds one level out for every queue: a queued call, a queued
2246
- `execute_code`, and a queued `/mcp` request are each handed their permit from
2247
- inside the request that released one, so each awaits the controller's
2248
- Promise-shaped `acquire` rather than yielding its Effect program, and does its
2249
- own work only once that promise resolves. Effect's `Semaphore` is not used
2250
- across requests for the same reason, and because a same-tick newcomer can take
2251
- a released permit ahead of a waiter that queued earlier.
2261
+ Asynchronous work shared across requests meets only through a `Settlement`
2262
+ (`src/runtime/settlement.ts`) that one request settles, never through a fiber
2263
+ that outlives its request or a Deferred another request awaits. Two workerd
2264
+ rules follow, and both have bitten.
2265
+
2266
+ **No request resumes another request's fiber.** Deferred resumes a waiting
2267
+ fiber synchronously, inside the call that completed it, and Effect re-enters
2268
+ the async context the fiber suspended in through the `AsyncResource` it
2269
+ captured. On Workers that context belongs to the waiter's request. Since
2270
+ workerd's September 2026 releases, a Worker with compatibility date 2025-06-16
2271
+ or later (or `bind_asynclocalstorage_snapshot_to_request`) refuses that entry
2272
+ from any other request: "Cannot call this AsyncLocalStorage bound function
2273
+ outside of the request in which it was created." The throw escapes the
2274
+ completing call, so the request that released an admission slot failed with
2275
+ `executor_failed`, and the stranded waiter later timed out (#1106). Older
2276
+ runtimes ran the waiter in the completing request instead, where its own I/O
2277
+ failed with "Cannot perform I/O on behalf of a different request". Awaiting a
2278
+ Promise around the wait fixes neither: the fiber waiting inside that Promise is
2279
+ the one resumed. A Settlement records its outcome synchronously, for queue
2280
+ arbitration and late-grant cleanup, and wakes each waiter by resolving a
2281
+ Promise the waiter's own request created. workerd runs that Promise's reactions
2282
+ in the request that created it, and Node runs them a microtask later in the
2283
+ waiter's async context. That wake is a turn late, so whatever a waiter acts on
2284
+ it rechecks there: an admission queue gives back a grant its controller closed
2285
+ in between, while a grant that beat the waiter's own abort stays the waiter's to
2286
+ release, and a refresh waiter rechecks its own signal before using a flight's
2287
+ answer. Executor admission, `/mcp` request
2288
+ admission, connector call admission and OAuth refresh flights all queue through
2289
+ Settlements; Effect's `Semaphore` is not used across requests for the same
2290
+ reason, and because a same-tick newcomer can take a released permit ahead of a
2291
+ waiter that queued earlier. Events from shared native objects follow the same
2292
+ rule: a warm QuickJS child's IPC messages arrive in the async context of the
2293
+ request that spawned it, so host calls dispatch inside the running program's
2294
+ captured context.
2252
2295
 
2253
2296
  **Never read another request's signal.** On workerd, reading `aborted` or
2254
2297
  `reason`, or calling `abort()`, on a signal that belongs to another request