@zackbart/connecta 0.37.1 → 0.38.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 (168) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +2 -2
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/auth/clerk-sdk/generated.js +2 -1
  8. package/dist/auth/downstream-oauth.js +19 -153
  9. package/dist/auth/downstream-revocation.js +1 -1
  10. package/dist/auth/oauth-grant.js +11 -0
  11. package/dist/auth/oauth-token-response.js +1 -1
  12. package/dist/auth/vercel-agent.js +2 -1
  13. package/dist/auth-elicitation.js +2 -2
  14. package/dist/branding.d.ts +1 -15
  15. package/dist/branding.js +2 -2
  16. package/dist/bytes.js +42 -0
  17. package/dist/call-admission-context.js +2 -5
  18. package/dist/call-recovery.js +0 -3
  19. package/dist/catalog-fingerprint.js +2 -18
  20. package/dist/catalog-service.js +1 -1
  21. package/dist/config-defaults.d.ts +1 -0
  22. package/dist/config-defaults.js +1 -0
  23. package/dist/config-schema.d.ts +1 -1
  24. package/dist/config-schema.js +6 -2
  25. package/dist/config.d.ts +41 -14
  26. package/dist/config.js +21 -71
  27. package/dist/connectors/api-connector.js +4 -3
  28. package/dist/connectors/guarded-fetch.d.ts +0 -6
  29. package/dist/connectors/guarded-fetch.js +1 -11
  30. package/dist/connectors/remote-mcp.js +10 -43
  31. package/dist/connectors/resource-uri.js +0 -4
  32. package/dist/d1.d.ts +0 -89
  33. package/dist/d1.js +0 -34
  34. package/dist/downstream-input.js +2 -2
  35. package/dist/execute.js +22 -13
  36. package/dist/executor-result.js +2 -1
  37. package/dist/executors/quickjs-runtime.js +2 -25
  38. package/dist/executors/quickjs.js +1 -1
  39. package/dist/guest-runtime.js +5 -3
  40. package/dist/guest-types.d.ts +5 -5
  41. package/dist/http/client.js +1 -1
  42. package/dist/http/errors.js +2 -2
  43. package/dist/http/json-schema.js +1 -1
  44. package/dist/http/node-transport.js +0 -1
  45. package/dist/http/oauth.js +1 -1
  46. package/dist/http/paging.js +6 -9
  47. package/dist/http/provider.js +1 -1
  48. package/dist/http/safety.js +1 -1
  49. package/dist/http/services.js +1 -8
  50. package/dist/http/tools.js +1 -7
  51. package/dist/http/vendor.js +7 -7
  52. package/dist/identity.js +2 -6
  53. package/dist/invocation-auth.js +2 -1
  54. package/dist/meta-tools.js +27 -27
  55. package/dist/native-tools.js +2 -1
  56. package/dist/oauth-handoff.js +2 -2
  57. package/dist/operation-vocabulary.js +1 -1
  58. package/dist/operator-record.js +1 -49
  59. package/dist/operator-ui/contract.d.ts +2 -2
  60. package/dist/operator-ui/generated.js +3 -3
  61. package/dist/program-writes.js +3 -8
  62. package/dist/provider-call.js +0 -7
  63. package/dist/provider.d.ts +1 -1
  64. package/dist/providers/_shared/google/kit.js +1 -6
  65. package/dist/providers/_shared/google/service-account.js +1 -3
  66. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  67. package/dist/providers/_shared/guide.js +3 -0
  68. package/dist/providers/_shared/json.js +12 -0
  69. package/dist/providers/_shared/rest/compact-index.js +1 -1
  70. package/dist/providers/_shared/rest/kit.js +1 -3
  71. package/dist/providers/_shared/rest/operation-index.js +1 -3
  72. package/dist/providers/_shared/rest/value-safety.js +26 -28
  73. package/dist/providers/breeze/api.js +2 -7
  74. package/dist/providers/breeze/index.js +4 -2
  75. package/dist/providers/breeze/tools.js +3 -4
  76. package/dist/providers/ccb/api.js +1 -6
  77. package/dist/providers/ccb/index.js +2 -1
  78. package/dist/providers/ccb/tools.js +1 -6
  79. package/dist/providers/cloudflare/api.js +2 -2
  80. package/dist/providers/cloudflare/index.js +2 -1
  81. package/dist/providers/cloudflare/safety.js +3 -3
  82. package/dist/providers/docs/tools.js +2 -10
  83. package/dist/providers/drive/tools.js +12 -25
  84. package/dist/providers/forms/tools.js +1 -9
  85. package/dist/providers/gmail/tools.js +5 -21
  86. package/dist/providers/overflow/api.js +1 -3
  87. package/dist/providers/overflow/index.js +2 -1
  88. package/dist/providers/overflow/tools.js +3 -7
  89. package/dist/providers/planning-center/api.js +2 -1
  90. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  91. package/dist/providers/planning-center/lib/json-api.js +1 -9
  92. package/dist/providers/planning-center/tools.js +2 -1
  93. package/dist/providers/sheets/tools.js +1 -9
  94. package/dist/providers/slides/tools.js +4 -13
  95. package/dist/providers/stripe/index.js +2 -1
  96. package/dist/providers/stripe/safety.js +2 -2
  97. package/dist/providers/tithely/api.js +2 -7
  98. package/dist/providers/tithely/index.js +3 -2
  99. package/dist/providers/tithely/tools.js +7 -5
  100. package/dist/providers/vercel/api.js +1 -6
  101. package/dist/providers/vercel/lib/pin.js +1 -1
  102. package/dist/providers/vercel/safety.js +1 -1
  103. package/dist/providers/vercel/tools.js +2 -1
  104. package/dist/redaction-marker.js +2 -2
  105. package/dist/registry.d.ts +1 -1
  106. package/dist/registry.js +5 -7
  107. package/dist/request-principal.js +1 -1
  108. package/dist/request-state.js +2 -2
  109. package/dist/routes/access-tokens.js +1 -1
  110. package/dist/routes/activity-disclosure.js +1 -1
  111. package/dist/routes/connect.js +2 -2
  112. package/dist/routes/credentials.js +1 -1
  113. package/dist/routes/mcp.js +4 -3
  114. package/dist/routes/oauth-management.js +1 -1
  115. package/dist/routes/operator.js +4 -3
  116. package/dist/routes/shared.d.ts +8 -8
  117. package/dist/routes/shared.js +9 -9
  118. package/dist/routes/ui-config.js +1 -1
  119. package/dist/routes/ui.js +1 -1
  120. package/dist/runtime/call-admission.js +1 -1
  121. package/dist/runtime/call.js +1 -1
  122. package/dist/runtime/settlement.js +1 -1
  123. package/dist/runtime/shared-read.js +2 -15
  124. package/dist/sent-secrets.d.ts +1 -2
  125. package/dist/sent-secrets.js +3 -8
  126. package/dist/server.js +4 -4
  127. package/dist/skills.js +8 -35
  128. package/dist/sqlite.d.ts +0 -19
  129. package/dist/sqlite.js +2 -83
  130. package/dist/storage/keys.js +16 -125
  131. package/dist/storage/sql-native.js +0 -101
  132. package/dist/storage/sql.d.ts +1 -24
  133. package/dist/storage/sql.js +1 -12
  134. package/dist/tool-error.js +4 -0
  135. package/dist/url-safety.js +0 -4
  136. package/dist/usage-guide.js +2 -2
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/worker.js +1 -1
  140. package/documentation/README.md +6 -42
  141. package/documentation/architecture.md +86 -496
  142. package/documentation/auth.md +12 -25
  143. package/documentation/code-mode.md +90 -66
  144. package/documentation/deploying.md +19 -40
  145. package/documentation/integrating.md +4 -19
  146. package/documentation/meta-tools.md +6 -6
  147. package/documentation/migration-0.33.md +5 -5
  148. package/documentation/migration-0.34.md +2 -4
  149. package/documentation/migration-retired-layouts.md +74 -0
  150. package/documentation/operator-tests.md +19 -119
  151. package/documentation/operator-ui.md +3 -4
  152. package/documentation/provider-auth-migration.md +4 -10
  153. package/documentation/provider-migration-0.29.md +4 -15
  154. package/examples/worker/AGENTS.md +4 -6
  155. package/examples/worker/README.md +20 -115
  156. package/guides/connecting-agents.md +7 -30
  157. package/guides/how-it-works.md +2 -2
  158. package/guides/safety-and-trust.md +4 -1
  159. package/package.json +19 -19
  160. package/templates/node/README.md +9 -26
  161. package/templates/node/package.json +1 -1
  162. package/dist/auth/oauth-v2-migration.js +0 -135
  163. package/dist/provider-call-facts.d.ts +0 -115
  164. package/dist/provider-call.d.ts +0 -112
  165. package/dist/storage/d1-copy.js +0 -255
  166. package/dist/storage/sql-schema.d.ts +0 -6
  167. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  168. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -158,7 +158,7 @@ function credentialRequest(context, connectorId, action) {
158
158
  if (request.method === "DELETE") {
159
159
  // Clearing nothing is refused, as testing nothing is, so the route
160
160
  // agrees with the operator contract, which offers `clear` only once a
161
- // value is stored in the caller's partition (#837). A value that cannot
161
+ // value is stored in the caller's partition. A value that cannot
162
162
  // be read may still be cleared: removing it is how one recovers.
163
163
  const stored = yield* Effect.tryPromise({
164
164
  try: () => vault.metadata(connectorId, owner),
@@ -32,7 +32,7 @@ const deadlineResponses = new WeakSet();
32
32
  /**
33
33
  * The most a `subscriptions/listen` classification reads. A listen is a small
34
34
  * JSON-RPC request; this read happens before request admission, so it is
35
- * bounded by this fixed cap rather than by the host's body limit (#857).
35
+ * bounded by this fixed cap rather than by the host's body limit.
36
36
  */
37
37
  export const MAX_LISTEN_CLASSIFICATION_BYTES = 64 * 1024;
38
38
  /**
@@ -286,7 +286,7 @@ function admitted(controller, signal) {
286
286
  }), (lease) => Effect.sync(() => lease.release()), { interruptible: true });
287
287
  }
288
288
  /**
289
- * 404 for any `?toolkit=` value, kept after the feature's retirement (#178).
289
+ * 404 for any `?toolkit=` value, kept after the feature's retirement.
290
290
  *
291
291
  * Toolkits are gone, but the URLs that named them are not: clients were handed
292
292
  * MCP endpoint URLs with `?toolkit=` baked in, and nothing about upgrading the
@@ -409,6 +409,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
409
409
  credentialHandoffUrl: opts.config.ui?.credentialHandoffUrl(baseUrl),
410
410
  ...(activity ? { activity } : {}),
411
411
  defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
412
+ maxProgramResultChars: opts.config.execute.maxResultChars,
412
413
  probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
413
414
  discoveryConcurrency: opts.config.discovery.concurrency,
414
415
  requestSignal,
@@ -638,7 +639,7 @@ export function createMcpRoute(opts) {
638
639
  }));
639
640
  // A request naming `subscriptions/listen` defers its permit: nothing of
640
641
  // its body is read until auth and endpoint admission pass, and then at
641
- // most MAX_LISTEN_CLASSIFICATION_BYTES (#857). Only a body-confirmed
642
+ // most MAX_LISTEN_CLASSIFICATION_BYTES. Only a body-confirmed
642
643
  // modern listen goes without a permit; trusting the header alone would
643
644
  // let a legacy tools/call bypass the pool, so anything else takes one
644
645
  // after classification. Every other request takes its permit here,
@@ -23,7 +23,7 @@ function oauthManagementRequest(context, connectorId) {
23
23
  const registry = yield* managementRegistry(context, authz);
24
24
  const connector = registry.getConnector(connectorId);
25
25
  const disconnectAuth = connector?.disconnectAuth?.bind(connector);
26
- // Each verb needs only its own hooks (#837): connecting needs the whole
26
+ // Each verb needs only its own hooks: connecting needs the whole
27
27
  // flow, through the callback's state check; disconnecting needs only
28
28
  // disconnectAuth, so a grant provisioned out of band can still be dropped.
29
29
  const connects = Boolean(connector?.startAuth && connector.finishAuth && connector.verifyState);
@@ -1,3 +1,4 @@
1
+ import { bytesToHex } from "../bytes.js";
1
2
  // The Effect plumbing the operator and activity data routes share.
2
3
  //
3
4
  // Each JSON route under /ui/* is one Effect program, run at its Promise edge
@@ -84,7 +85,7 @@ export function operatorViewerKey(context, authz) {
84
85
  ]));
85
86
  const signingKey = key;
86
87
  const digest = yield* attempt(() => crypto.subtle.sign("HMAC", signingKey, identity));
87
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
88
+ return bytesToHex(new Uint8Array(digest));
88
89
  }), {
89
90
  timeoutMs: 500,
90
91
  signal: context.request.signal,
@@ -101,7 +102,7 @@ export function scopeFor(authz, authenticated, connectorIds = "all") {
101
102
  };
102
103
  }
103
104
  /**
104
- * The whole registry, for a maintainer only (#837). Everyone else gets the
105
+ * The whole registry, for a maintainer only. Everyone else gets the
105
106
  * private 404 plain `/mcp` gives them.
106
107
  */
107
108
  export function visibleRegistry({ opts }, authz) {
@@ -111,7 +112,7 @@ export function visibleRegistry({ opts }, authz) {
111
112
  return Effect.succeed(opts.registry.scoped(scopeFor(authz, opts.config.auth.length > 0)));
112
113
  }
113
114
  /**
114
- * The connectors a connector-auth route may resolve (#837): every connector
115
+ * The connectors a connector-auth route may resolve: every connector
115
116
  * for a maintainer, the connectors of their pools for a pool member, and the
116
117
  * private 404 for anyone else. mayManageConnector still decides each action.
117
118
  */
@@ -78,7 +78,7 @@ export declare function authorize(request: Request, baseUrl: string, auth: reado
78
78
  response: Response;
79
79
  }>;
80
80
  /**
81
- * Admission at an agent endpoint, `/agent/<pool>` (#837). Only the built-in
81
+ * Admission at an agent endpoint, `/agent/<pool>`. Only the built-in
82
82
  * agent credential adapters are consulted: Clerk and Access never run, cookies and Clerk
83
83
  * handshake parameters are stripped, and the runtime context, which carries
84
84
  * Access's `ctx.access` even on a bypassed path, is never passed. Anything but
@@ -88,31 +88,31 @@ export declare function authorizeAgent(request: Request, baseUrl: string, auth:
88
88
  /** Human routes reject machine credentials before any verification or storage I/O. */
89
89
  export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, logger?: Logger): Promise<Awaited<ReturnType<typeof authorize>>>;
90
90
  export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
91
- export declare function withSecurityHeaders(response: Response, requestUrl: URL, _path: string): Response;
91
+ export declare function withSecurityHeaders(response: Response, requestUrl: URL): Response;
92
92
  type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
93
93
  ok: true;
94
94
  }>;
95
95
  /**
96
- * Plain `/mcp` and every operator route admit a maintainer (#837). An open
96
+ * Plain `/mcp` and every operator route admit a maintainer. An open
97
97
  * deployment has no inbound auth and so no maintainers: its one anonymous
98
98
  * caller keeps the whole default endpoint, as the startup warning says.
99
99
  */
100
100
  export declare function admitsMaintainer(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
101
101
  /**
102
102
  * The declared pool `/mcp/<name>` serves to this person, or undefined for the
103
- * same 404. An agent is never admitted here, even when listed (#837).
103
+ * same 404. An agent is never admitted here, even when listed.
104
104
  */
105
105
  export declare function memberPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
106
106
  /** The declared pool `/agent/<name>` serves to this agent, or undefined for the same 404. */
107
107
  export declare function agentPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
108
108
  /**
109
- * The connectors of every pool this identity is a member of (#837): what a
109
+ * The connectors of every pool this identity is a member of: what a
110
110
  * pool member may ever manage, and only their personal ones among these.
111
111
  */
112
112
  export declare function memberConnectors(opts: ServerOptions, identity: Readonly<AuthenticatedIdentity>): "all" | ReadonlySet<string>;
113
113
  /**
114
114
  * Whether this identity may start, finish, or remove a connector's auth
115
- * (#837). Only an interactive person ever may; an agent or any other machine
115
+ * Only an interactive person ever may; an agent or any other machine
116
116
  * credential manages nothing. Shared auth is deployment-wide, so it takes a
117
117
  * maintainer. A personal connector takes a stable principal that is a
118
118
  * maintainer, whose plain `/mcp` includes it, or a member of a pool that
@@ -124,10 +124,10 @@ export declare function mayManageConnector(opts: ServerOptions, authz: Authorize
124
124
  id: string;
125
125
  authScope?: "shared" | "personal";
126
126
  }): boolean;
127
- /** Agent token list, issue, rotate, and revoke: an interactive maintainer only (#837). */
127
+ /** Agent token list, issue, rotate, and revoke: an interactive maintainer only. */
128
128
  export declare function managesAgentTokens(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
129
129
  /**
130
- * Activity reads: a maintainer principal, interactive or not (#837). Members
130
+ * Activity reads: a maintainer principal, interactive or not. Members
131
131
  * and agents read none, and an open deployment has no maintainer.
132
132
  */
133
133
  export declare function readsActivity(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
@@ -163,7 +163,7 @@ const authorizeNative = /* @__PURE__ */ registerAuth(authorize, function (reques
163
163
  return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
164
164
  }
165
165
  // Every caller of this function is a human route. Agents reach pools only at
166
- // `/agent/<pool>` (#837), so a workload or `cta_` credential refuses here
166
+ // `/agent/<pool>`, so a workload or `cta_` credential refuses here
167
167
  // without verification, and never falls back to a cookie, session or Access context.
168
168
  if (machineCredential) {
169
169
  return {
@@ -299,7 +299,7 @@ function agentChallenge() {
299
299
  };
300
300
  }
301
301
  /**
302
- * Admission at an agent endpoint, `/agent/<pool>` (#837). Only the built-in
302
+ * Admission at an agent endpoint, `/agent/<pool>`. Only the built-in
303
303
  * agent credential adapters are consulted: Clerk and Access never run, cookies and Clerk
304
304
  * handshake parameters are stripped, and the runtime context, which carries
305
305
  * Access's `ctx.access` even on a bypassed path, is never passed. Anything but
@@ -376,7 +376,7 @@ export function isSameOrigin(request, baseUrl) {
376
376
  return false;
377
377
  }
378
378
  }
379
- export function withSecurityHeaders(response, requestUrl, _path) {
379
+ export function withSecurityHeaders(response, requestUrl) {
380
380
  const headers = response.headers.get("Content-Type")?.toLowerCase().startsWith("text/html")
381
381
  ? htmlSecurityHeaders(response.headers)
382
382
  : new Headers(response.headers);
@@ -392,7 +392,7 @@ export function withSecurityHeaders(response, requestUrl, _path) {
392
392
  });
393
393
  }
394
394
  /**
395
- * Plain `/mcp` and every operator route admit a maintainer (#837). An open
395
+ * Plain `/mcp` and every operator route admit a maintainer. An open
396
396
  * deployment has no inbound auth and so no maintainers: its one anonymous
397
397
  * caller keeps the whole default endpoint, as the startup warning says.
398
398
  */
@@ -401,7 +401,7 @@ export function admitsMaintainer(opts, authz) {
401
401
  }
402
402
  /**
403
403
  * The declared pool `/mcp/<name>` serves to this person, or undefined for the
404
- * same 404. An agent is never admitted here, even when listed (#837).
404
+ * same 404. An agent is never admitted here, even when listed.
405
405
  */
406
406
  export function memberPool(opts, name, identity) {
407
407
  if (identity.agent !== undefined)
@@ -417,7 +417,7 @@ export function agentPool(opts, name, identity) {
417
417
  return pool?.members.some((member) => member.kind === "agent" && isMember(member, identity)) ? pool : undefined;
418
418
  }
419
419
  /**
420
- * The connectors of every pool this identity is a member of (#837): what a
420
+ * The connectors of every pool this identity is a member of: what a
421
421
  * pool member may ever manage, and only their personal ones among these.
422
422
  */
423
423
  export function memberConnectors(opts, identity) {
@@ -435,7 +435,7 @@ export function memberConnectors(opts, identity) {
435
435
  }
436
436
  /**
437
437
  * Whether this identity may start, finish, or remove a connector's auth
438
- * (#837). Only an interactive person ever may; an agent or any other machine
438
+ * Only an interactive person ever may; an agent or any other machine
439
439
  * credential manages nothing. Shared auth is deployment-wide, so it takes a
440
440
  * maintainer. A personal connector takes a stable principal that is a
441
441
  * maintainer, whose plain `/mcp` includes it, or a member of a pool that
@@ -454,12 +454,12 @@ export function mayManageConnector(opts, authz, connector) {
454
454
  const pools = memberConnectors(opts, identity);
455
455
  return pools === "all" || pools.has(connector.id);
456
456
  }
457
- /** Agent token list, issue, rotate, and revoke: an interactive maintainer only (#837). */
457
+ /** Agent token list, issue, rotate, and revoke: an interactive maintainer only. */
458
458
  export function managesAgentTokens(opts, authz) {
459
459
  return (authz.identity.interactive && authz.identity.agent === undefined && isMaintainer(opts.maintainers, authz.identity));
460
460
  }
461
461
  /**
462
- * Activity reads: a maintainer principal, interactive or not (#837). Members
462
+ * Activity reads: a maintainer principal, interactive or not. Members
463
463
  * and agents read none, and an open deployment has no maintainer.
464
464
  */
465
465
  export function readsActivity(opts, authz) {
@@ -359,7 +359,7 @@ function maintainerRead(context, authz) {
359
359
  .map(([id, tools]) => [id, Object.fromEntries(Object.entries(tools).filter(([name]) => validName(name)))]));
360
360
  // A pool owner is a Workspace mailbox address. Only an authenticated
361
361
  // maintainer reads it; an open deployment's anonymous caller, which this
362
- // route admits, gets null, the same as an unset owner (#837).
362
+ // route admits, gets null, the same as an unset owner.
363
363
  const ownersVisible = opts.config.auth.length > 0 && isMaintainer(opts.maintainers, authz.identity);
364
364
  config.pools = config.pools.map((pool) => ({
365
365
  ...pool,
package/dist/routes/ui.js CHANGED
@@ -182,7 +182,7 @@ function summary(context) {
182
182
  const authz = yield* authorized(context);
183
183
  const registry = yield* visibleRegistry(context, authz);
184
184
  const { visible, mayManage, permissions, activityEnabled, credentialManagement } = operatorView(context, authz, registry);
185
- // Only maintainers reach here (#837); setup commands are offered for every
185
+ // Only maintainers reach here; setup commands are offered for every
186
186
  // declared pool, since they administer them all.
187
187
  const pools = [...opts.pools.keys()];
188
188
  return privateJson({
@@ -7,7 +7,7 @@
7
7
  // block: the one place outside its instance methods that may read its private
8
8
  // bookkeeping, and one that emits nothing into the `.d.ts`.
9
9
  //
10
- // The two controllers stay separate on purpose (#453): request admission
10
+ // The two controllers stay separate on purpose: request admission
11
11
  // bounds executor work, this bounds calls to one downstream connector, and a
12
12
  // shared queue between them was refused.
13
13
  import { Effect } from "effect";
@@ -106,7 +106,7 @@ function ended() {
106
106
  return new ConnectorCallError("connector_call_failed", "This call has ended; a capability kept from it can no longer be used.", { retryable: false });
107
107
  }
108
108
  /** Build one call's services. Invocation calls this in the scoped attempt that owns the permit (B.1). */
109
- export function openCall(inputs) {
109
+ function openCall(inputs) {
110
110
  let open = true;
111
111
  const parentSignal = inputs.parent ? Context.get(inputs.parent.services, Signal) : undefined;
112
112
  const parents = [inputs.signal, parentSignal, inputs.requestSignal].filter((signal) => signal !== undefined);
@@ -10,7 +10,7 @@
10
10
  // it was created.") when the Worker's compatibility date is 2025-06-16 or
11
11
  // later, in releases since September 2026. The refusal throws out of the call
12
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
13
+ // the waiter never resumes. Awaiting a Promise around that wait is no
14
14
  // help: the fiber waiting on the Deferred inside the Promise is resumed in the
15
15
  // completing request just the same.
16
16
  //
@@ -1,18 +1,5 @@
1
- // One read that several readers in one request wait on, each on its own terms.
2
- //
3
- // Discovery and the registry both coalesce a connector's catalog read inside
4
- // one request, so a search, a describe, and the call after them cost one
5
- // downstream listing. The read used to run under whichever reader asked
6
- // first: its signal was the read's signal, so a short-deadline call that
7
- // happened to start the read failed a search that joined it with the call's
8
- // timeout, and a cancelled search cancelled a read others were still waiting
9
- // on (#571).
10
- //
11
- // So the read owns its signal. Each reader waits under its own cancellation,
12
- // and the read is cancelled only when no reader is left to want it. Nothing
13
- // here is shared across requests: a SharedRead lives in one request's map and
14
- // is waited on only by that request's fibers, which is why a plain count of
15
- // readers is enough and no edge is needed between them.
1
+ // Request-local catalog reads own their signal. Each reader waits under its own
2
+ // cancellation; the read is cancelled only when no reader remains (INV-7).
16
3
  import { Deferred, Effect, Exit } from "effect";
17
4
  import { forkRequest } from "./run.js";
18
5
  import { requestOwner } from "./request-scope.js";
@@ -224,7 +224,6 @@ export declare function agentOutputOperations<T extends Record<string, (...args:
224
224
  /** Slot reads cover custom handlers too, including keys put in query strings. A field named
225
225
  * as an identifier (an application or client ID, a username, a public key) is matched whole. */
226
226
  export declare function registerCredentialValues(secrets: SentSecrets, values: import("./types.js").ConnectorCredentialValues | null): void;
227
- export declare function trackCredentialReads(ctx: ConnectorContext): void;
228
227
  /** A connector's result or error as it enters: every connector's credentials and the
229
228
  * request's private arguments, under payload rules. */
230
229
  export declare function redactSentSecrets<T>(ctx: ConnectorContext, value: T): T;
@@ -235,7 +234,7 @@ export declare function requestSentSecrets(ctx: ConnectorContext): SentSecrets;
235
234
  * extracted from. Redact it whole first: a cut through an echoed form can leave a remnant
236
235
  * shorter than any fragment the output boundary matches, and a normalized number or
237
236
  * extracted substring may no longer hold the form at all. */
238
- export declare function redactBeforeCut<T>(ctx: ConnectorContext, value: T): T;
237
+ export declare const redactBeforeCut: typeof redactSentSecrets;
239
238
  /** Base64, or base64url with `url`, of bytes or UTF-8 text Connecta returns to an agent: a
240
239
  * download, an attachment, a REST body, a page cursor. Every such encoding goes through here,
241
240
  * since no later pass reads through an encoding the output does not declare: the content is
@@ -5,7 +5,7 @@ import { Effect } from "effect";
5
5
  import { classificationCode, ConnectorCallError } from "./errors.js";
6
6
  import { credentialUrlViews } from "./credential-url.js";
7
7
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
8
- import { viewSecrets, wrapViewCredentials } from "./runtime/call.js";
8
+ import { viewSecrets } from "./runtime/call.js";
9
9
  import { visitPrivateCallArguments } from "./argument-redaction.js";
10
10
  import { keptErrorName, redactedCopyOf } from "./redaction-identity.js";
11
11
  import { REDACTED } from "./redaction-marker.js";
@@ -688,7 +688,7 @@ function headerSpace(text, at) {
688
688
  }
689
689
  /**
690
690
  * The echo's spans with the value of every sensitive header line (`headerLine`'s names) added,
691
- * read on the text before the echo is replaced (#703). An echoed private value can take part of a
691
+ * read on the text before the echo is replaced. An echoed private value can take part of a
692
692
  * header's name, its `:`, or a credential prefix such as `Bearer `, so the line cannot be read
693
693
  * after replacement; read here, the whole value goes, every cookie pair and any short tail with
694
694
  * it. A line that is not a sensitive header keeps its text. One forward pass; nothing is kept.
@@ -2512,9 +2512,6 @@ export function registerCredentialValues(secrets, values) {
2512
2512
  secrets.add(value);
2513
2513
  }
2514
2514
  }
2515
- export function trackCredentialReads(ctx) {
2516
- wrapViewCredentials(ctx);
2517
- }
2518
2515
  /** A connector's result or error as it enters: every connector's credentials and the
2519
2516
  * request's private arguments, under payload rules. */
2520
2517
  export function redactSentSecrets(ctx, value) {
@@ -2530,9 +2527,7 @@ export function requestSentSecrets(ctx) {
2530
2527
  * extracted from. Redact it whole first: a cut through an echoed form can leave a remnant
2531
2528
  * shorter than any fragment the output boundary matches, and a normalized number or
2532
2529
  * extracted substring may no longer hold the form at all. */
2533
- export function redactBeforeCut(ctx, value) {
2534
- return requestSentSecrets(ctx).redact(value);
2535
- }
2530
+ export const redactBeforeCut = redactSentSecrets;
2536
2531
  /** Base64, or base64url with `url`, of bytes or UTF-8 text Connecta returns to an agent: a
2537
2532
  * download, an attachment, a REST body, a page cursor. Every such encoding goes through here,
2538
2533
  * since no later pass reads through an encoding the output does not declare: the content is
package/dist/server.js CHANGED
@@ -35,7 +35,7 @@ export function createFetchHandler(opts) {
35
35
  // Which sandbox, not how it is tuned: `connecta doctor` reports the
36
36
  // executor it just exercised rather than assuming one, and a
37
37
  // deployment whose executor identifies as nothing omits the key
38
- // instead of inviting a guess (#368).
38
+ // instead of inviting a guess.
39
39
  ...(opts.executorName !== undefined ? { executor: { name: opts.executorName } } : {}),
40
40
  admission: {
41
41
  policy: "global-fifo",
@@ -64,7 +64,7 @@ export function createFetchHandler(opts) {
64
64
  const defer = runtimeContext ? runtimeContext.waitUntil.bind(runtimeContext) : undefined;
65
65
  const originRefusal = routeMcp.rejectOrigin(request);
66
66
  if (originRefusal) {
67
- return Effect.succeed(withSecurityHeaders(originRefusal, url, path));
67
+ return Effect.succeed(withSecurityHeaders(originRefusal, url));
68
68
  }
69
69
  // Container and orchestrator probes reach /health over plain HTTP on
70
70
  // loopback, where no proxy has set X-Forwarded-Proto. Redirecting them to
@@ -87,7 +87,7 @@ export function createFetchHandler(opts) {
87
87
  return Effect.succeed(withSecurityHeaders(new Response(null, {
88
88
  status: 308,
89
89
  headers: { Location: target.toString() },
90
- }), url, path));
90
+ }), url));
91
91
  }
92
92
  const context = {
93
93
  request,
@@ -98,7 +98,7 @@ export function createFetchHandler(opts) {
98
98
  defer,
99
99
  runtimeContext,
100
100
  };
101
- return Effect.map(route(context), (response) => withSecurityHeaders(response, url, path));
101
+ return Effect.map(route(context), (response) => withSecurityHeaders(response, url));
102
102
  });
103
103
  return function fetch(request, runtimeContext) {
104
104
  return runEdge(serve(request, runtimeContext), { signal: request.signal });
package/dist/skills.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bytesToHex, utf8Length } from "./bytes.js";
1
2
  import { duringViewEffect } from "./runtime/call.js";
2
3
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
3
4
  import { upwardStep } from "./upward.js";
@@ -9,7 +10,7 @@ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
9
10
  import { closeScopeOnExit } from "./runtime/connector-scope.js";
10
11
  import { sentSecretsForRequest } from "./sent-secrets.js";
11
12
  const ROUTE = "Choose a route before discovery. One known-address read uses call_tool; one known-address write uses call_destructive_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.";
12
- const RECOVERY = 'When an auth error includes a handoff, give it to the user or operator. Use authorize_connector for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Fetch skills({ name: "usage" }) only for missing guidance or program repair.';
13
+ const RECOVERY = 'When an auth error includes a handoff, give it to the user or operator. Use authorize_connector for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Missing guidance/repair only: skills tool "usage" or connecta.skill("usage").';
13
14
  /**
14
15
  * The always-loaded MCP `instructions` string. An endpoint is writable or
15
16
  * read-only: a writable one lets programs and call_destructive_tool write,
@@ -328,25 +329,6 @@ const MAX_SKILL_CATALOG_BYTES = 8 * 1024 * 1024;
328
329
  const MAX_SKILLS = 1_024;
329
330
  const encoder = new TextEncoder();
330
331
  const PRIVATE = { resultType: "complete", ttlMs: 0, cacheScope: "private" };
331
- /** Bound metadata before cloning it, without another payload-sized byte array. */
332
- function jsonBytes(value) {
333
- const json = JSON.stringify(value);
334
- let bytes = 0;
335
- for (let i = 0; i < json.length; i++) {
336
- const code = json.charCodeAt(i);
337
- if (code < 0x80)
338
- bytes++;
339
- else if (code < 0x800)
340
- bytes += 2;
341
- else if (code >= 0xd800 && code <= 0xdbff && json.charCodeAt(i + 1) >= 0xdc00 && json.charCodeAt(i + 1) <= 0xdfff) {
342
- bytes += 4;
343
- i++;
344
- }
345
- else
346
- bytes += 3;
347
- }
348
- return bytes;
349
- }
350
332
  function localRecord(name, uri, description, content, aliases) {
351
333
  return { entry: { uri, frontmatter: { name, description }, resources: "dynamic" }, content, aliases };
352
334
  }
@@ -369,12 +351,7 @@ const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (
369
351
  (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), (connector) => authFlow(function* () {
370
352
  const name = validSkillName(connector.id)
371
353
  ? connector.id
372
- : `connector-${[
373
- ...new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)]))),
374
- ]
375
- .map((byte) => byte.toString(16).padStart(2, "0"))
376
- .join("")
377
- .slice(0, 48)}`;
354
+ : `connector-${bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)])))).slice(0, 48)}`;
378
355
  const description = connectorGuideSummary(connector);
379
356
  const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
380
357
  const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${connectorGuide(connector)}`;
@@ -474,11 +451,7 @@ const withManifestNative = /* @__PURE__ */ registerAuth(withManifest, function (
474
451
  const bytes = encoder.encode(record.content);
475
452
  if (bytes.length > MAX_SKILL_BYTES)
476
453
  throw new ConnectorCallError("unavailable", "Local skill exceeds the byte bound.");
477
- const digest = [
478
- ...new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", bytes]))),
479
- ]
480
- .map((byte) => byte.toString(16).padStart(2, "0"))
481
- .join("");
454
+ const digest = bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", bytes]))));
482
455
  return {
483
456
  ...record,
484
457
  entry: {
@@ -723,7 +696,7 @@ export class SkillsRegistry {
723
696
  const records = yield* upwardStep(Effect.forEach(yield* upwardStep(self.local), (record) => authCall(withManifest, [record]), {
724
697
  concurrency: "unbounded",
725
698
  }));
726
- let catalogBytes = jsonBytes(records.map((record) => record.entry));
699
+ let catalogBytes = utf8Length(JSON.stringify(records.map((record) => record.entry)));
727
700
  // Bounded sequential connector reads avoid unbounded fan-out and finish
728
701
  // cleanup before a later connector's failure can end the whole listing.
729
702
  for (const connector of self.registry.listConnectors()) {
@@ -736,7 +709,7 @@ export class SkillsRegistry {
736
709
  if (!Array.isArray(entries) || records.length + entries.length > MAX_SKILLS)
737
710
  throw new ConnectorCallError("unavailable", "Downstream skills listing exceeds its bound.");
738
711
  for (const original of entries) {
739
- if (catalogBytes + jsonBytes(original) + 1 > MAX_SKILL_CATALOG_BYTES)
712
+ if (catalogBytes + utf8Length(JSON.stringify(original)) + 1 > MAX_SKILL_CATALOG_BYTES)
740
713
  throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte bound.");
741
714
  const entry = structuredClone(original);
742
715
  validateEntry(entry);
@@ -758,14 +731,14 @@ export class SkillsRegistry {
758
731
  if (records.some((record) => record.entry.uri === uri))
759
732
  throw new ConnectorCallError("unavailable", "Downstream skills listing contains duplicate entries.");
760
733
  const mapped = { ...entry, uri, resources };
761
- catalogBytes += jsonBytes(mapped) + 1;
734
+ catalogBytes += utf8Length(JSON.stringify(mapped)) + 1;
762
735
  if (catalogBytes > MAX_SKILL_CATALOG_BYTES)
763
736
  throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte bound.");
764
737
  records.push({ entry: mapped, aliases: [], connector, files });
765
738
  }
766
739
  }
767
740
  if (records.length > MAX_SKILLS ||
768
- jsonBytes(records.map((record) => record.entry)) > MAX_SKILL_CATALOG_BYTES) {
741
+ utf8Length(JSON.stringify(records.map((record) => record.entry))) > MAX_SKILL_CATALOG_BYTES) {
769
742
  throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte or entry bound.");
770
743
  }
771
744
  // Keep the old investigation guide behind every connector and downstream
package/dist/sqlite.d.ts CHANGED
@@ -27,22 +27,3 @@ export declare function sqliteStorage(database: SqliteDatabase): KVStorage;
27
27
  * older than `retentionDays`.
28
28
  */
29
29
  export declare function sqliteActivityStore(database: SqliteDatabase, options?: SqliteActivityOptions): ActivityStore;
30
- export interface StateFileImport {
31
- /** Entries copied into the database. */
32
- imported: number;
33
- /** Entries left alone because the database already held their key. */
34
- kept: number;
35
- /** Entries whose TTL had already passed. */
36
- expired: number;
37
- }
38
- /**
39
- * One-shot migration from the 0.28 `fileStorage` JSON state file: copy every
40
- * live entry, with its expiry, into the database's key-value table. Run it
41
- * once, with the old deployment stopped, before the first start on SQLite.
42
- *
43
- * Keys the database already holds are kept, so a second run copies nothing
44
- * twice and never overwrites state written since. The file is read, never
45
- * changed; delete it once the deployment is verified. A file that is not a
46
- * state file throws before anything is written.
47
- */
48
- export declare function importStateFile(database: SqliteDatabase, statePath: string, now?: number): StateFileImport;
package/dist/sqlite.js CHANGED
@@ -4,12 +4,11 @@
4
4
  // install. `sqliteStorage` is the deployment's `KVStorage` and
5
5
  // `sqliteActivityStore` its activity history; both create their tables on
6
6
  // first use. Node-only: nothing reachable from the root entry imports this.
7
- import { chmodSync, closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
7
+ import { chmodSync, closeSync, mkdirSync, openSync } from "node:fs";
8
8
  import { dirname, resolve } from "node:path";
9
9
  import { DatabaseSync } from "node:sqlite";
10
10
  import { effectSqlDriver, sqlDriverEdge, sqlSync } from "./storage/sql-driver.js";
11
- import { validateStorageKey } from "./storage/keys.js";
12
- import { KV_SCHEMA, sqlActivityStore, sqlStorage, } from "./storage/sql.js";
11
+ import { sqlActivityStore, sqlStorage, } from "./storage/sql.js";
13
12
  /** Milliseconds a write waits for another process's write lock. */
14
13
  const BUSY_TIMEOUT_MS = 5_000;
15
14
  /**
@@ -100,83 +99,3 @@ export function sqliteStorage(database) {
100
99
  export function sqliteActivityStore(database, options) {
101
100
  return sqlActivityStore(sqliteDriver(database), "sqlite", options);
102
101
  }
103
- /**
104
- * One-shot migration from the 0.28 `fileStorage` JSON state file: copy every
105
- * live entry, with its expiry, into the database's key-value table. Run it
106
- * once, with the old deployment stopped, before the first start on SQLite.
107
- *
108
- * Keys the database already holds are kept, so a second run copies nothing
109
- * twice and never overwrites state written since. The file is read, never
110
- * changed; delete it once the deployment is verified. A file that is not a
111
- * state file throws before anything is written.
112
- */
113
- export function importStateFile(database, statePath, now = Date.now()) {
114
- const text = readFileSync(statePath, "utf8");
115
- // The file holds stored state (tokens, sealed credentials), and these
116
- // errors reach the operator's terminal, so they name the file and an entry's
117
- // position, never a key or the parser's account, which quotes the text.
118
- let loaded;
119
- try {
120
- loaded = JSON.parse(text);
121
- }
122
- catch {
123
- throw new TypeError(`${statePath} is not a connecta state file: it is not valid JSON`);
124
- }
125
- if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) {
126
- throw new TypeError(`${statePath} is not a connecta state file`);
127
- }
128
- const entries = Object.entries(loaded);
129
- for (const [index, [key, entry]] of entries.entries()) {
130
- validateStorageKey(key);
131
- const valid = entry !== null &&
132
- typeof entry === "object" &&
133
- typeof entry.value === "string" &&
134
- (!("exp" in entry) || Number.isFinite(entry.exp));
135
- if (!valid) {
136
- throw new TypeError(`${statePath} is not a connecta state file: entry ${index + 1} has no string value`);
137
- }
138
- }
139
- let db;
140
- let transaction = false;
141
- let step = "open the SQLite database";
142
- const result = { imported: 0, kept: 0, expired: 0 };
143
- try {
144
- db = typeof database === "string" ? openSqlite(database) : database;
145
- step = "begin the import transaction";
146
- db.exec("BEGIN IMMEDIATE");
147
- transaction = true;
148
- step = "prepare the key-value table";
149
- for (const statement of KV_SCHEMA)
150
- db.exec(statement);
151
- const insert = db.prepare(`INSERT INTO connecta_kv (key, value, expires_at_ms) VALUES (?, ?, ?)
152
- ON CONFLICT (key) DO NOTHING`);
153
- for (const [index, [key, entry]] of entries.entries()) {
154
- step = `import entry ${index + 1} into the key-value table`;
155
- const { value, exp } = entry;
156
- if (exp !== undefined && exp <= now) {
157
- result.expired += 1;
158
- continue;
159
- }
160
- if (Number(insert.run(key, value, exp === undefined ? null : Math.ceil(exp)).changes) > 0) {
161
- result.imported += 1;
162
- }
163
- else {
164
- result.kept += 1;
165
- }
166
- }
167
- step = "commit the import transaction";
168
- db.exec("COMMIT");
169
- }
170
- catch {
171
- if (transaction) {
172
- try {
173
- db.exec("ROLLBACK");
174
- }
175
- catch {
176
- // Rollback errors can contain stored text too. Report the failed step.
177
- }
178
- }
179
- throw new Error(`State import failed: could not ${step}`);
180
- }
181
- return result;
182
- }