@zackbart/connecta 0.36.1 → 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 (46) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/version.mjs +1 -1
  3. package/dist/auth/downstream-oauth.js +34 -12
  4. package/dist/auth/oauth-authorization-params.js +52 -0
  5. package/dist/auth/static-oauth.js +4 -6
  6. package/dist/auth/stored-observation.js +8 -0
  7. package/dist/call-admission.js +27 -14
  8. package/dist/connectors/api-connector.js +4 -0
  9. package/dist/connectors/option-shapes.js +6 -1
  10. package/dist/connectors/remote-mcp.d.ts +32 -3
  11. package/dist/connectors/remote-mcp.js +152 -21
  12. package/dist/execute.js +5 -5
  13. package/dist/executor-admission.js +27 -13
  14. package/dist/executors/quickjs.js +4 -1
  15. package/dist/google.d.ts +13 -0
  16. package/dist/google.js +42 -0
  17. package/dist/index.d.ts +2 -1
  18. package/dist/invocation.js +6 -7
  19. package/dist/operator-ui/generated.js +2 -2
  20. package/dist/operator-ui/model.d.ts +2 -0
  21. package/dist/providers/_shared/google/service-account.d.ts +1 -1
  22. package/dist/providers/_shared/google/service-account.js +20 -8
  23. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  24. package/dist/request-principal.js +34 -0
  25. package/dist/request-token.d.ts +53 -0
  26. package/dist/request-token.js +1 -0
  27. package/dist/routes/mcp.js +6 -6
  28. package/dist/routes/ui-config.js +38 -9
  29. package/dist/routes/ui-data.js +30 -5
  30. package/dist/routes/ui.js +10 -2
  31. package/dist/runtime/settlement.js +59 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/dist/worker.js +4 -1
  35. package/documentation/README.md +13 -12
  36. package/documentation/architecture.md +91 -26
  37. package/documentation/auth.md +317 -12
  38. package/documentation/code-mode.md +25 -5
  39. package/documentation/integrating.md +18 -4
  40. package/documentation/operator-tests.md +19 -0
  41. package/documentation/operator-ui.md +57 -46
  42. package/examples/worker/README.md +5 -1
  43. package/guides/connecting-agents.md +6 -0
  44. package/guides/safety-and-trust.md +1 -0
  45. package/package.json +5 -1
  46. 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";
@@ -35,7 +35,7 @@ export declare function parseServiceAccount(owner: string, input: unknown): Serv
35
35
  /** One delegated token request: whose account, acting as whom, for what. */
36
36
  export interface DelegatedTokenRequest {
37
37
  account: ServiceAccountKey;
38
- subject: string;
38
+ subject?: string | undefined;
39
39
  scopes: readonly string[];
40
40
  }
41
41
  /**
@@ -1,8 +1,7 @@
1
1
  /**
2
- * A Google service account acting for Workspace users through domain-wide
3
- * delegation: the key, the signed JWT-bearer assertion, and the short-lived
4
- * access tokens it buys. Shared by every Workspace product provider; never its
5
- * own export.
2
+ * A Google service account acting as itself or for Workspace users through
3
+ * domain-wide delegation: the key, signed JWT-bearer assertions and short-lived
4
+ * access tokens. Shared by Workspace providers and the optional /google helper.
6
5
  *
7
6
  * Why not the vault. One key serves every Workspace connector a deployment
8
7
  * declares — Gmail, Drive, Docs — and it is a deployment secret in the same
@@ -300,9 +299,17 @@ function tokenAnswer(status, code) {
300
299
  * Map a refused token request to what fixes it. The codes are RFC 6749's and
301
300
  * Google documents what each means for a delegated assertion.
302
301
  */
303
- function tokenFailure(account, scopes, status, headers, payload) {
302
+ function tokenFailure(account, scopes, status, headers, payload, subject) {
304
303
  const code = asRecord(payload)["error"];
305
304
  const said = tokenAnswer(status, code);
305
+ if (subject === undefined) {
306
+ if (code === "unauthorized_client" || code === "access_denied" || code === "invalid_scope" || status === 403) {
307
+ return new ConnectorCallError("provider_permission_denied", `Google refused a service-account token.${said} Check the requested OAuth scopes and the service account's IAM permissions for the target API and resources.`);
308
+ }
309
+ if (code === "invalid_grant") {
310
+ return resendAdvice(new ConnectorCallError("auth_required", `Google refused the service-account assertion.${said} The key may be deleted or disabled, or this host's clock may be minutes off. Check the key and clock.`), " before retrying");
311
+ }
312
+ }
306
313
  const client = account.clientId
307
314
  ? `client ID ${account.clientId} (${account.clientEmail})`
308
315
  : `the client ID of ${account.clientEmail}`;
@@ -325,7 +332,7 @@ function tokenFailure(account, scopes, status, headers, payload) {
325
332
  if (status === 403) {
326
333
  return new ConnectorCallError("provider_permission_denied", `Google refused domain-wide delegation.${said} Ask a Workspace super admin to review ${client} in the Admin console's Manage Domain Wide Delegation entry and grant exactly these scopes: ${scopes.join(",")}.`);
327
334
  }
328
- return new ConnectorCallError("connector_call_failed", `Google's token endpoint refused the delegated assertion with HTTP ${status}.${said}`, { retryable: false });
335
+ return new ConnectorCallError("connector_call_failed", `Google's token endpoint refused the ${subject === undefined ? "service-account" : "delegated"} assertion with HTTP ${status}.${said}`, { retryable: false });
329
336
  }
330
337
  async function mint(account, subject, scopes, ctx) {
331
338
  const issuedAt = Date.now();
@@ -339,7 +346,7 @@ async function mint(account, subject, scopes, ctx) {
339
346
  const parsed = await response.jsonResult();
340
347
  const payload = "value" in parsed ? parsed.value : undefined;
341
348
  if (!response.ok) {
342
- throw verdict(tokenFailure(account, scopes, response.status, response.headers, payload));
349
+ throw verdict(tokenFailure(account, scopes, response.status, response.headers, payload, subject));
343
350
  }
344
351
  const body = asRecord(payload);
345
352
  const accessToken = body["access_token"];
@@ -383,7 +390,12 @@ function keyIdentity(account) {
383
390
  }
384
391
  async function cacheKey({ account, subject, scopes }) {
385
392
  // Workspace addresses are case-insensitive; scope order is not a new grant.
386
- return [account.clientEmail, await keyIdentity(account), subject.toLowerCase(), [...scopes].sort().join(" ")].join("\n");
393
+ return JSON.stringify([
394
+ account.clientEmail,
395
+ await keyIdentity(account),
396
+ subject?.toLowerCase() ?? null,
397
+ [...new Set(scopes)].sort(),
398
+ ]);
387
399
  }
388
400
  function remember(key, minted) {
389
401
  tokens.delete(key);
@@ -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.1 (+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
+ }
@@ -0,0 +1,53 @@
1
+ import type { ConnectorContext } from "./types.js";
2
+ /** A request-local Bearer resolver with optional cache invalidation on HTTP 401. */
3
+ export interface RequestToken {
4
+ (ctx: ConnectorContext): Promise<string>;
5
+ /** Drop only the rejected token. This hook never authorizes replay. */
6
+ invalidate?: (rejected: string, ctx: ConnectorContext) => Promise<void>;
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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.1";
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.1";
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();
@@ -8,18 +8,19 @@ For a new deployment, start with [Deploying Connecta](./deploying.md).
8
8
 
9
9
  ## Operator tasks
10
10
 
11
- | Task | Self-contained procedure |
12
- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
13
- | Add or remove a connector | [Change the connector roster](./integrating.md#add-or-remove-a-connector) |
14
- | Set up a maintained provider | [Maintained provider setup](./providers.md) |
15
- | Inspect maintained API sources and tools | [API sources and tool inventory](./providers.md#api-sources-and-tool-inventory) |
16
- | Organize an agent's pools and tiers | [Organizing an agent's pools](./auth.md#organizing-an-agents-pools) |
17
- | Add a person (maintainer or pool member) | [Add a person](./auth.md#add-a-person), [member page](./operator-ui.md#use-the-member-page) |
18
- | Connect a shared credential | [Connect a shared credential](./auth.md#connect-a-shared-credential) |
19
- | Issue, rotate, or revoke an agent token | [Manage an agent token](./auth.md#issue-rotate-or-revoke-an-agent-token) |
20
- | Upgrade between versions | [Upgrade an existing deployment](./deploying.md#upgrade-an-existing-deployment), including [migration to 0.33](./migration-0.33.md) |
21
- | Diagnose with `connecta doctor` | [Verify and diagnose](./deploying.md#verify-and-diagnose) |
22
- | Connect a client with tool search | [Native tool surface](./meta-tools.md#native-tool-surface), [Eve agents](./meta-tools.md#eve-agents) |
11
+ | Task | Self-contained procedure |
12
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
13
+ | Add or remove a connector | [Change the connector roster](./integrating.md#add-or-remove-a-connector) |
14
+ | Set up a maintained provider | [Maintained provider setup](./providers.md) |
15
+ | Inspect maintained API sources and tools | [API sources and tool inventory](./providers.md#api-sources-and-tool-inventory) |
16
+ | Organize an agent's pools and tiers | [Organizing an agent's pools](./auth.md#organizing-an-agents-pools) |
17
+ | Add a person (maintainer or pool member) | [Add a person](./auth.md#add-a-person), [member page](./operator-ui.md#use-the-member-page) |
18
+ | Connect Google hosted MCP with a service account | [Google tokens and shared-agent/personal-people setup](./auth.md#google-service-account-tokens-and-hosted-mcp) |
19
+ | Connect a shared credential | [Connect a shared credential](./auth.md#connect-a-shared-credential) |
20
+ | Issue, rotate, or revoke an agent token | [Manage an agent token](./auth.md#issue-rotate-or-revoke-an-agent-token) |
21
+ | Upgrade between versions | [Upgrade an existing deployment](./deploying.md#upgrade-an-existing-deployment), including [migration to 0.33](./migration-0.33.md) |
22
+ | Diagnose with `connecta doctor` | [Verify and diagnose](./deploying.md#verify-and-diagnose) |
23
+ | Connect a client with tool search | [Native tool surface](./meta-tools.md#native-tool-surface), [Eve agents](./meta-tools.md#eve-agents) |
23
24
 
24
25
  For removed hosted presets and the former hosted modes of Stripe, Vercel,
25
26
  Cloudflare, and GitHub, use [Migrate hosted MCP presets](./hosted-mcp-migration.md).