@zackbart/connecta 0.37.1 → 0.39.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 (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  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/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -1,4 +1,6 @@
1
- // Tithe.ly's giving API (v1) as a kit vendor (decision 0010): the operations,
1
+ import { Routes, routePhrases } from "../../agent-routes.js";
2
+ import { asRecord, asArray } from "../_shared/json.js";
3
+ // Tithe.ly's giving API (v1) as a kit vendor: the operations,
2
4
  // what its statuses mean, its credential, its in-band refusal, the probe, and
3
5
  // the value-safety review. Facts from the per-operation reference at
4
6
  // https://docs.tithe.ly/reference/introduction, each marked where it is
@@ -388,9 +390,10 @@ function keyPairProblem(values) {
388
390
  /** Operator-held Basic keys, resolved per request; a mismatched pair is refused before anything leaves. */
389
391
  export const authenticate = Effect.gen(function* () {
390
392
  const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
393
+ const routes = yield* Routes;
391
394
  const pair = keyPairProblem(values);
392
395
  if (typeof pair === "string") {
393
- return yield* failed(new ConnectorCallError("auth_required", `${pair} Call authorize_connector for recovery options; an operator adds the keys on this connection in the operator UI.`));
396
+ return yield* failed(new ConnectorCallError("auth_required", `${pair} ${routePhrases(routes).authorizeFor("recovery options")}; an operator adds the keys on this connection in the operator UI.`));
394
397
  }
395
398
  return { Authorization: `Basic ${btoa(`${pair.publicKey}:${pair.privateKey}`)}` };
396
399
  });
@@ -509,12 +512,6 @@ export function tithelySafety(sensitiveFields) {
509
512
  }
510
513
  // --- Shared readers ------------------------------------------------------------------
511
514
  const failed = (error) => Effect.fail(new Refusal({ error }));
512
- export function asRecord(value) {
513
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
514
- }
515
- export function asArray(value) {
516
- return Array.isArray(value) ? value : [];
517
- }
518
515
  export function text(value) {
519
516
  return typeof value === "string" && value !== "" ? value : undefined;
520
517
  }
@@ -1,6 +1,7 @@
1
+ import { readOnlyNotice } from "../_shared/guide.js";
1
2
  import { skill } from "./skill.generated.js";
2
3
  /**
3
- * Tithe.ly's giving API (v1) on the provider kit (decision 0010). No SDK
4
+ * Tithe.ly's giving API (v1) on the provider kit. No SDK
4
5
  * exists to depend on, and none would be welcome: this is a payments API, and
5
6
  * every tool was chosen and written, not generated (decision record 0001:
6
7
  * hand-authored tools). The vendor lives in `api.ts` and the tools in
@@ -67,7 +68,7 @@ function usageGuide(options) {
67
68
  sensitiveFields(options) === "return"
68
69
  ? "This connection returns organizations' payout bank and legal-contact details through tithely_api_read; the named reads still omit them."
69
70
  : "This connection withholds organizations' payout bank and legal-contact details from every result.",
70
- ...(options.readOnly === true ? ["This connection is read-only: every write is refused."] : []),
71
+ ...readOnlyNotice(options.readOnly === true),
71
72
  ...(options.defaultPageSize !== undefined
72
73
  ? [`List tools return ${options.defaultPageSize} rows by default on this connection.`]
73
74
  : []),
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## Giving data is pastoral\n\nDonor names, emails, addresses, and gift amounts are pastoral data. Reduce\ninside `execute_code`: page, filter, and total there, and return the\naggregate (sums by fund, month, or donor count), not donor rows, unless the\ntask needs named people.\n\n## Ids and joins\n\n- Ids carry their kind: `org_` organization, `user_` donor account, `ch_`\n charge, `rc_` recurring gift, `pm_` payment method. Read them from the list\n tools; never guess one.\n- Tithe.ly lists charges and recurring gifts only for one organization or one\n donor, so pass `organizationId` or `accountId`.\n- A charge carries its donor's name and email, its organization's name, and\n its payment method, so most joins need no second read.\n- `list_accounts` covers every account these keys can see; `scope: \"api\"`\n narrows it to accounts created through this API.\n- The named reads project Tithe.ly's records. A field a projection omits, such\n as an account's phone and postal address, comes back from `tithely_api_read`\n on that record's path.\n\n## Money, dates and funds\n\n- A fund is a Tithe.ly giving type: `giving_type` in Tithe.ly's records,\n `fund` in projections.\n- Projections give amounts as integer `…Cents` and dates as ISO 8601 UTC\n `…At`. Tithe.ly's own records, from `tithely_api_read`, keep string cents\n and Unix seconds.\n- `depositedAt` may be `\"pending\"`. There is no deposits resource: net amount\n and fees live on each charge.\n- A charge a recurring gift produced has `recurring: true`; a one-time gift\n has `recurring: false`.\n\n## Writes\n\n- Every write moves money or changes a donor's records. Bodies are form\n fields in integer cents and Unix seconds.\n- `POST /refunds/{charge_id}` refunds a whole charge.\n `DELETE /recurring/{recurring_id}` stops future charges and refunds nothing.\n- Card and bank tokens, which `POST /charge-once` and `POST /payment-methods`\n need, come only from Tithely.js in a donor's browser. An agent cannot\n create one.\n- Tithe.ly reports most refusals as HTTP 200 with `status: \"fail\"` and a\n reason but no code. They arrive as `connector_call_failed`; read the reason\n before deciding what to do next.\n\n## Not available\n\nOnly Tithe.ly's API v1 is reachable; its undocumented v2 is not.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_accounts`, `list_charges`, `list_organizations`, `list_payment_methods`, `list_recurring_charges`, `tithely_api_details`, `tithely_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 99). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `tithely_api_search` finds this API's operations and reads no account data, `tithely_api_details` shows one operation's contract, and `tithely_api_read` calls a reviewed read. `tithely_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## Giving data is pastoral\n\nDonor names, emails, addresses, and gift amounts are pastoral data. Reduce\ninside `execute_code`: page, filter, and total there, and return the\naggregate (sums by fund, month, or donor count), not donor rows, unless the\ntask needs named people.\n\n## Ids and joins\n\n- Ids carry their kind: `org_` organization, `user_` donor account, `ch_`\n charge, `rc_` recurring gift, `pm_` payment method. Read them from the list\n tools; never guess one.\n- Tithe.ly lists charges and recurring gifts only for one organization or one\n donor, so pass `organizationId` or `accountId`.\n- A charge carries its donor's name and email, its organization's name, and\n its payment method, so most joins need no second read.\n- `list_accounts` covers every account these keys can see; `scope: \"api\"`\n narrows it to accounts created through this API.\n- The named reads project Tithe.ly's records. A field a projection omits, such\n as an account's phone and postal address, comes back from `tithely_api_read`\n on that record's path.\n\n## Money, dates and funds\n\n- A fund is a Tithe.ly giving type: `giving_type` in Tithe.ly's records,\n `fund` in projections.\n- Projections give amounts as integer `…Cents` and dates as ISO 8601 UTC\n `…At`. Tithe.ly's own records, from `tithely_api_read`, keep string cents\n and Unix seconds.\n- `depositedAt` may be `\"pending\"`. There is no deposits resource: net amount\n and fees live on each charge.\n- A charge a recurring gift produced has `recurring: true`; a one-time gift\n has `recurring: false`.\n\n## Writes\n\n- Every write moves money or changes a donor's records. Bodies are form\n fields in integer cents and Unix seconds.\n- `POST /refunds/{charge_id}` refunds a whole charge.\n `DELETE /recurring/{recurring_id}` stops future charges and refunds nothing.\n- Card and bank tokens, which `POST /charge-once` and `POST /payment-methods`\n need, come only from Tithely.js in a donor's browser. An agent cannot\n create one.\n- Tithe.ly reports most refusals as HTTP 200 with `status: \"fail\"` and a\n reason but no code. They arrive as `connector_call_failed`; read the reason\n before deciding what to do next.\n\n## Not available\n\nOnly Tithe.ly's API v1 is reachable; its undocumented v2 is not.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_accounts`, `list_charges`, `list_organizations`, `list_payment_methods`, `list_recurring_charges`, `tithely_api_details`, `tithely_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 99). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `tithely_api_search` finds this API's operations and reads no account data, `tithely_api_details` shows one operation's contract, and `tithely_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `tithely_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Account instructions"
5
5
  };
@@ -1,8 +1,10 @@
1
- // Tithe.ly's curated tools on the provider kit (decision 0010) and their
1
+ import { compact as compactDefined } from "../_shared/json.js";
2
+ import { asArray, asRecord } from "../_shared/json.js";
3
+ // Tithe.ly's curated tools on the provider kit and their
2
4
  // projections. Decision 0011's step 2: standard lists ({ items, page } with
3
5
  // limit, cursor and fields), fields on every named read, and no `raw`: what a
4
6
  // projection drops stays reachable through the standard hatch, which the kit
5
- // builds from `api.ts`'s index and verdicts (decision 0014). Each named read
7
+ // builds from `api.ts`'s index and verdicts. Each named read
6
8
  // earns its place against the hatch with paging Tithe.ly cannot report
7
9
  // (lookahead), units (cents, ISO dates), joins (donor, organization and
8
10
  // payment method inline) or reconciliation of a known id after a write.
@@ -13,7 +15,7 @@ import { Refusal } from "../../http/errors.js";
13
15
  import { Integer } from "../../http/json-schema.js";
14
16
  import { lookahead } from "../../http/paging.js";
15
17
  import { get as getTool, list } from "../../http/tools.js";
16
- import { asArray, asRecord, entity, getCharge, getRecurring, listAccounts, listAllAccounts, listCharges, listOrganizations, listPaymentMethods, listRecurring, MAX_PAGE_SIZE, PAYOUT_CURRENCY, text, } from "./api.js";
18
+ import { entity, getCharge, getRecurring, listAccounts, listAllAccounts, listCharges, listOrganizations, listPaymentMethods, listRecurring, MAX_PAGE_SIZE, PAYOUT_CURRENCY, text, } from "./api.js";
17
19
  // --- Schemas ---------------------------------------------------------------------
18
20
  const Id = Schema.String.check(Schema.isMinLength(1));
19
21
  const Text = Schema.optionalKey(Schema.String);
@@ -267,7 +269,7 @@ function seconds(value, name) {
267
269
  return Effect.succeed(Math.floor(parsed / 1000));
268
270
  }
269
271
  function compact(value) {
270
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
272
+ return compactDefined(value);
271
273
  }
272
274
  /** Integer cents from Tithe.ly's string-or-number amounts. */
273
275
  function cents(value) {
@@ -325,7 +327,7 @@ function projectOrganization(value) {
325
327
  return compact({
326
328
  organizationId: text(organization["organization_id"]),
327
329
  name: text(organization["name"]),
328
- // The giving widget id embeds Tithe.ly's giving form; the step-2 eval showed agents need it (decision 0014).
330
+ // The giving widget id embeds Tithe.ly's giving form; the step-2 eval showed agents need it.
329
331
  widgetId: text(organization["widget_id"]),
330
332
  ownerAccountId: text(organization["account_id"]),
331
333
  website: text(organization["website"]),
@@ -1,3 +1,4 @@
1
+ import { isRecord } from "../_shared/json.js";
1
2
  // Vercel's pinned contract, authentication, failures and value-safety boundary.
2
3
  import { Effect, Schema } from "effect";
3
4
  import { ConnectorCallError } from "../../errors.js";
@@ -24,9 +25,6 @@ const READ_POSTS = [
24
25
  ["POST", "/v1/registrar/domains/search", "Searches domain availability and pricing; buys nothing."],
25
26
  ["POST", "/v8/artifacts", "Reads Remote Cache metadata for a list of artifact hashes; uploads nothing."],
26
27
  ];
27
- export function isRecord(value) {
28
- return typeof value === "object" && value !== null && !Array.isArray(value);
29
- }
30
28
  export function record(value) {
31
29
  return isRecord(value) ? value : {};
32
30
  }
@@ -36,9 +34,6 @@ export function text(value) {
36
34
  export function invalid(message) {
37
35
  throw new ConnectorCallError("invalid_args", message);
38
36
  }
39
- export function compact(value) {
40
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
41
- }
42
37
  function truthy(value) {
43
38
  return value === true || value === 1 || (typeof value === "string" && /^(?:1|true)$/i.test(value.trim()));
44
39
  }
@@ -461,7 +456,7 @@ export function transformGuide(guide, options) {
461
456
  const base = Object.values(VERCEL_VERDICTS).filter((verdict) => verdict === "refuse").length;
462
457
  const total = vercelIndex.ops.filter((op) => VERCEL_VERDICTS[`${op.method} ${op.path}`] === "refuse" ||
463
458
  PIN_DISCOVERY_REFUSALS[`${op.method} ${op.path}`] !== undefined).length;
464
- const sentence = `${base} operations are refused by design; use \`vercel_api_details\` with the method and path to inspect the original operation summary and its separate refusal.`;
459
+ const sentence = `${base} operations are refused by design; use \`vercel_api_details\` with the method and path to see each summary and refusal,`;
465
460
  const lines = guide.content.split("\n");
466
461
  const targets = lines.flatMap((line, position) => line.startsWith("- **Hatch:** `vercel_api_search` ") && line.includes(sentence) ? [position] : []);
467
462
  const position = targets[0];
@@ -4,7 +4,7 @@ import { ConnectorCallError } from "../../../errors.js";
4
4
  import { Refusal } from "../../../http/errors.js";
5
5
  import { Budget } from "../../../http/services.js";
6
6
  import { checked } from "../../_shared/rest/kit.js";
7
- export const MAX_PIN_LOOKUPS = 8;
7
+ const MAX_PIN_LOOKUPS = 8;
8
8
  export const PIN_WRITE_STEPS = Object.freeze({
9
9
  write: "once",
10
10
  lookup1: "once",
@@ -214,7 +214,7 @@ const absent = {
214
214
  //
215
215
  // Residual risk: a secret a person typed into a free-text
216
216
  // name, description, log line, or identifier is out of scope.
217
- // Embedded URL verdict (#812): reviewed paths + global signing parameters.
217
+ // Embedded URL verdict: reviewed paths + global signing parameters.
218
218
  // Evidence: pinned index/candidates sha256:5a702fd2dc76 identify deployment
219
219
  // reads/creation/listing, build events and runtime logs; the operation/path
220
220
  // list below scans errorMessage, deployments.errorMessage, payload.text/text
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## Projects, deployments and domains\n\nResolve a team by slug, a project by name, and deployments by the observed\nproject id. Team ids start with `team_`, project ids with `prj_`, and deployment\nids with `dpl_`. A deployment hostname and a deployment id identify the same\ndeployment; an alias can later point to a different deployment.\n\nSome project paths accept a project name, but a deployment list's projectId\nfilter requires the observed `prj_` id. Resolve the name before listing its\ndeployments; an empty result for a guessed name does not prove absence.\nComplete each identifier lookup before starting the reads that depend on it.\n\n<!-- when: unpinned -->\nThe configured team is a routing default, not a restriction. Explicit teamId\nor slug selects another team the token can reach; teamId: null selects personal\nscope. Vercel token permissions determine access. Prefer an explicit team when\njoining resources across several calls.\n<!-- end -->\n\n<!-- when: pinned -->\nThe deployment confines this connection to one team. A supplied team slug must\nbelong to that team; personal scope and foreign teams are refused. Project and\ndeployment references require ownership reads. Global/account and transfer\noperations without a verifiable team boundary remain unavailable. A team list\nreturns only the pinned team.\n<!-- end -->\n\nRead deployment state before its build events. Runtime logs diagnose requests\nafter a deployment built; build logs explain a failed build. Narrow time windows\nbefore raising collection limits. Generic reads refuse live follow streams;\nuse the bounded build/runtime readers. Deployment timestamps are milliseconds.\n\nProject domains and deployment aliases are different joins: a project domain\nbelongs to the project, while an alias identifies its assigned deployment.\nRead a pending domain's verification instructions before changing DNS.\n\n## Environment changes\n\nResolve environment-variable ids using metadata, including target, branch and\ncustom environment. A key can have distinct production and preview values.\nExisting values cannot be retrieved. Supply new values only when the task asks\nfor a change; confirm the returned id and metadata.\n\nEnvironment changes apply to future deployments. A new deployment is a separate\nwrite. Promoting an existing deployment does not rebuild it or update embedded\nenvironment values. After an uncertain update, reconcile observable metadata\nsuch as id, targets and comment; reading metadata cannot verify a secret value.\n\n## Explicit files and credentials\n\nDeployment files can reference previously uploaded digests. File uploads accept\nexplicit bytes and never read a path on the agent's machine. Decrypted-value,\ncredential-minting, protection-bypass-link, signing and domain move-out operations\nare refused; use Vercel's dashboard for those workflows.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_teams`, `vercel_api_details`, `vercel_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `vercel_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 20, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `vercel_api_search` finds this API's operations and reads no account data, `vercel_api_details` shows one operation's contract, and `vercel_api_read` calls a reviewed read. `vercel_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 29 operations are refused by design; use `vercel_api_details` with the method and path to inspect the original operation summary and its separate refusal. Use details for refused methods outside the read/write call schemas; those methods remain unavailable.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## Projects, deployments and domains\n\nResolve a team by slug, a project by name, and deployments by the observed\nproject id. Team ids start with `team_`, project ids with `prj_`, and deployment\nids with `dpl_`. A deployment hostname and a deployment id identify the same\ndeployment; an alias can later point to a different deployment.\n\nSome project paths accept a project name, but a deployment list's projectId\nfilter requires the observed `prj_` id. Resolve the name before listing its\ndeployments; an empty result for a guessed name does not prove absence.\nComplete each identifier lookup before starting the reads that depend on it.\n\n<!-- when: unpinned -->\nThe configured team is a routing default, not a restriction. Explicit teamId\nor slug selects another team the token can reach; teamId: null selects personal\nscope. Vercel token permissions determine access. Prefer an explicit team when\njoining resources across several calls.\n<!-- end -->\n\n<!-- when: pinned -->\nThe deployment confines this connection to one team. A supplied team slug must\nbelong to that team; personal scope and foreign teams are refused. Project and\ndeployment references require ownership reads. Global/account and transfer\noperations without a verifiable team boundary remain unavailable. A team list\nreturns only the pinned team.\n<!-- end -->\n\nRead deployment state before its build events. Runtime logs diagnose requests\nafter a deployment built; build logs explain a failed build. Narrow time windows\nbefore raising collection limits. Generic reads refuse live follow streams;\nuse the bounded build/runtime readers. Deployment timestamps are milliseconds.\n\nProject domains and deployment aliases are different joins: a project domain\nbelongs to the project, while an alias identifies its assigned deployment.\nRead a pending domain's verification instructions before changing DNS.\n\n## Environment changes\n\nResolve environment-variable ids using metadata, including target, branch and\ncustom environment. A key can have distinct production and preview values.\nExisting values cannot be retrieved. Supply new values only when the task asks\nfor a change; confirm the returned id and metadata.\n\nEnvironment changes apply to future deployments. A new deployment is a separate\nwrite. Promoting an existing deployment does not rebuild it or update embedded\nenvironment values. After an uncertain update, reconcile observable metadata\nsuch as id, targets and comment; reading metadata cannot verify a secret value.\n\n## Explicit files and credentials\n\nDeployment files can reference previously uploaded digests. File uploads accept\nexplicit bytes and never read a path on the agent's machine. Decrypted-value,\ncredential-minting, protection-bypass-link, signing and domain move-out operations\nare refused; use Vercel's dashboard for those workflows.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_teams`, `vercel_api_details`, `vercel_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `vercel_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 20, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `vercel_api_search` finds this API's operations and reads no account data, `vercel_api_details` shows one operation's contract, and `vercel_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `vercel_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 29 operations are refused by design; use `vercel_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Account instructions"
5
5
  };
@@ -1,15 +1,16 @@
1
+ import { compact, isRecord } from "../_shared/json.js";
1
2
  // Curated Vercel metadata and bounded logs through kit-owned call services.
2
3
  import { Effect, Schema } from "effect";
3
4
  import { ConnectorCallError } from "../../errors.js";
4
5
  import { Refusal } from "../../http/errors.js";
5
6
  import { read, write, list } from "../../http/tools.js";
6
- import { fieldsProblem, selectFields } from "../../http/fields.js";
7
+ import { fieldsContract, selectFields } from "../../http/fields.js";
7
8
  import { vercelPage } from "../../http/hatch-paging.js";
8
9
  import { schema, typedParams } from "../_shared/rest/kit.js";
9
10
  import { refuseWithheldUpload } from "../_shared/rest/value-safety.js";
10
11
  import { rowStop } from "./lib/logs.js";
11
12
  import { checked } from "../_shared/rest/kit.js";
12
- import { admit, compact, defaultTeam, isRecord, invalid, named, queryOf, queryPairs, record, reviewedCode, restCall, text, vercelIndex, refuse, checkServer, } from "./api.js";
13
+ import { admit, defaultTeam, invalid, named, queryOf, queryPairs, record, reviewedCode, restCall, text, vercelIndex, refuse, checkServer, } from "./api.js";
13
14
  import { pinOptions, readPinnedTeam, PIN_WRITE_STEPS } from "./lib/pin.js";
14
15
  const MAX_LOG_BYTES = 1024 * 1024;
15
16
  const MAX_LOG_TEXT = 4_000;
@@ -65,6 +66,7 @@ const ENV_SCHEMA = {
65
66
  },
66
67
  required: ["id", "key", "type"],
67
68
  };
69
+ const ENV_FIELDS = fieldsContract("item", ENV_SCHEMA, "Values cannot be restored.");
68
70
  /** Deliberately omits `value`, even though redaction already removed it. */
69
71
  function envMetadata(value) {
70
72
  const variable = record(value);
@@ -157,7 +159,7 @@ export function vercelTools({ connection, options, }) {
157
159
  type: "array",
158
160
  maxItems: 50,
159
161
  items: { type: "string", minLength: 1, maxLength: 200 },
160
- description: "Dot paths to keep in each metadata item; values cannot be restored.",
162
+ description: ENV_FIELDS.description,
161
163
  },
162
164
  }, ["projectId"]),
163
165
  outputSchema: {
@@ -170,7 +172,7 @@ export function vercelTools({ connection, options, }) {
170
172
  },
171
173
  run: (args) => Effect.gen(function* () {
172
174
  const fields = args["fields"];
173
- const problem = fieldsProblem(fields, new Set(Object.keys(ENV_SCHEMA.properties)), "an environment metadata item");
175
+ const problem = ENV_FIELDS.problem(fields);
174
176
  if (problem)
175
177
  throw problem;
176
178
  const { data } = yield* send("GET", `/v10/projects/${segment(args["projectId"])}/env`, {
@@ -7,7 +7,7 @@ export const REDACTED = "[redacted]";
7
7
  * secrets from every value of a vendor body before result safety reads it, so a rule that decides
8
8
  * on a value (a type discriminator, a record's label or type, a URL's scheme or parameter names,
9
9
  * a Content-Type) can find the placeholder where the vendor wrote what the rule matches.
10
- * Such a decision takes its most protective outcome (#703).
10
+ * Such a decision takes its most protective outcome.
11
11
  */
12
12
  export function redactionAltered(value) {
13
13
  return typeof value === "string" && value.includes(REDACTED);
@@ -92,7 +92,7 @@ function credentialUnit(code) {
92
92
  }
93
93
  /**
94
94
  * The text right after the placeholder at `at` that may be the rest of a credential whose public
95
- * prefix redaction took (#703): up to the first unit `ends` accepts, reading later placeholders
95
+ * prefix redaction took: up to the first unit `ends` accepts, reading later placeholders
96
96
  * whole so none is split. Redaction can take `sk_live_`, `whsec_`, `Bearer `, or a signing
97
97
  * parameter's name and `=`, leaving what follows; with that prefix known, any remainder rebuilds
98
98
  * the credential, however short or encoded, so `readable` holds whenever it carries any base64 or
@@ -71,8 +71,10 @@ export interface RegistryOptions {
71
71
  maxStashBytes?: number;
72
72
  maxStashEntries?: number;
73
73
  } | undefined;
74
+ /** Route names every context carries unless a call names its own; meta by default. */
75
+ routes?: import("./types.js").AgentRouteNames | undefined;
74
76
  }
75
- export type ConnectorOperationOptions = Pick<ConnectorContext, "signal" | "timeoutMs" | "defer">;
77
+ export type ConnectorOperationOptions = Pick<ConnectorContext, "signal" | "timeoutMs" | "defer" | "routes">;
76
78
  /**
77
79
  * The registry surface a per-connection MCP server consumes: every meta-tool
78
80
  * (`src/meta-tools.ts`) and the `execute_code` sandbox bridge (`src/execute.ts`)
@@ -84,6 +86,7 @@ export type ConnectorOperationOptions = Pick<ConnectorContext, "signal" | "timeo
84
86
  */
85
87
  /** Host-admitted identity and endpoint bindings for result paging. */
86
88
  export interface ResultIdentity {
89
+ pool: string | null;
87
90
  subject: string | null;
88
91
  principal: string | null;
89
92
  endpoint: string;
@@ -124,8 +127,11 @@ export interface RegistryView {
124
127
  recheckResultAccess(dependencies: readonly ProgramResultDependency[], classification: "read" | "write", signal?: AbortSignal): Promise<boolean>;
125
128
  /** Reserve deployment-wide capacity before writing a paging envelope's chunks. */
126
129
  stashResult(id: string, chunks: readonly string[], ttlSeconds: number): Promise<boolean>;
127
- /** Local declared-vs-stored credential mismatch, with no downstream I/O. */
128
- credentialDriftFor(id: string): Promise<string | undefined>;
130
+ /**
131
+ * Local declared-vs-stored credential mismatch, with no downstream I/O. With
132
+ * `requestScope`, the read joins that request's read memo.
133
+ */
134
+ credentialDriftFor(id: string, requestScope?: object): Promise<string | undefined>;
129
135
  /** Value-free shape learned from successful calls, never a provider declaration. */
130
136
  observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
131
137
  /** Passively learn one successful unwrapped result; failures stay isolated. */
@@ -240,7 +246,7 @@ export declare class Registry implements RegistryView {
240
246
  *
241
247
  * `chunks[n]` lands on `resultKeys.chunk(id, n)` inside `partition` — the
242
248
  * layout connecta.result reads back, so a page fetches only the chunks it covers
243
- * instead of the whole stored result (issue #540). Chunking is a read-cost
249
+ * instead of the whole stored result. Chunking is a read-cost
244
250
  * decision, not a capacity one: however many keys an envelope occupies, it
245
251
  * is one stash entry charged its total ASCII length.
246
252
  *
@@ -287,7 +293,7 @@ export declare class Registry implements RegistryView {
287
293
  describeStaticTools(id: string): ToolDef[] | undefined;
288
294
  private validateClassification;
289
295
  getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions, scope?: RegistryScope): Promise<ToolDef[]>;
290
- credentialDriftFor(id: string): Promise<string | undefined>;
296
+ credentialDriftFor(id: string, requestScope?: object): Promise<string | undefined>;
291
297
  /** Age of a complete cache entry; reads never refresh it. */
292
298
  catalogAgeMs(id: string): number | null;
293
299
  /** Pure lookup shared by status probes and operator catalog refreshes. */
package/dist/registry.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { wrapViewCredentials } from "./runtime/call.js";
2
+ import { META_ROUTES } from "./agent-routes.js";
3
+ import { sha256Hex } from "./bytes.js";
1
4
  import { catalogRevision } from "./catalog-revision.js";
2
5
  import { authCall, authInvoke } from "./auth/native.js";
3
6
  import { Clock, Duration, Effect, Random } from "effect";
@@ -18,12 +21,16 @@ import { boundedStatus, failureRecord, failureStatus, logFailure, ownStatus } fr
18
21
  import { ObservedOutputSchemas } from "./result-shapes.js";
19
22
  import { ContinuationBinding, OperationCredentials, ContinuationStorage, operationView, setViewService, duringViewEffect, } from "./runtime/call.js";
20
23
  import { hasNativeStorage, registerNativeStorage } from "./runtime/native-storage.js";
24
+ import { observeStorageWrites } from "./runtime/request-reads.js";
25
+ import { attachStorageDigestKey } from "./storage-digest.js";
26
+ import { requestReads } from "./runtime/request-scope.js";
27
+ import { credentialReads } from "./credential-reads.js";
21
28
  import { provideRegistryPrograms, registryEdgeProgram, registryProgram } from "./runtime/registry.js";
22
29
  import { causeError, runEdge } from "./runtime/run.js";
23
30
  import { Storage } from "./runtime/services.js";
24
31
  import { SharedRead } from "./runtime/shared-read.js";
25
- import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet, storageList, } from "./runtime/storage.js";
26
- import { redactCatalog, sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
32
+ import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageGetMany, storageSet, storageList, } from "./runtime/storage.js";
33
+ import { redactCatalog, sentSecretsFor, sentSecretsForRequest } from "./sent-secrets.js";
27
34
  import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary } from "./skills.js";
28
35
  import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashExpiryKeys, stashLedgerKeys, } from "./storage/keys.js";
29
36
  import { assertStaticToolNames, hasControlCharacters } from "./tool-name.js";
@@ -103,12 +110,14 @@ function namespaced(storage, prefix) {
103
110
  list: async (keyPrefix) => (await storage.list(prefix + keyPrefix)).map((key) => key.slice(prefix.length)),
104
111
  compareAndSet: (k, expected, next, o) => storage.compareAndSet(prefix + k, expected, next, o),
105
112
  };
113
+ observeStorageWrites(edge, { storage, prefix });
106
114
  return hasNativeStorage(storage)
107
115
  ? registerNativeStorage(edge, {
108
116
  // Select against the underlying current methods at dispatch. A
109
117
  // namespace created during a decoration regains the native path when
110
118
  // that decoration is restored, without capturing stale capabilities.
111
119
  get: (key) => storageGet(prefix + key).pipe(Effect.provideService(Storage, storage)),
120
+ getMany: (keys) => storageGetMany(keys.map((key) => prefix + key)).pipe(Effect.provideService(Storage, storage)),
112
121
  set: (key, value, options) => storageSet(prefix + key, value, options).pipe(Effect.provideService(Storage, storage)),
113
122
  delete: (key) => storageDelete(prefix + key).pipe(Effect.provideService(Storage, storage)),
114
123
  list: (keyPrefix) => storageList(prefix + keyPrefix).pipe(Effect.provideService(Storage, storage), Effect.map((keys) => keys.map((key) => key.slice(prefix.length)))),
@@ -232,10 +241,6 @@ function swapStashLedger(plan) {
232
241
  }).pipe(Effect.catch(() => Effect.succeed({ status: "exhausted" })));
233
242
  });
234
243
  }
235
- async function sha256Hex(value) {
236
- const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
237
- return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
238
- }
239
244
  /**
240
245
  * Holds the connector set, resolves addresses, and coalesces tool listings
241
246
  * within one request. Remote MCP clients own persisted response caching. Connector failures are isolated: a broken
@@ -461,8 +466,12 @@ export class Registry {
461
466
  let credentialProgram;
462
467
  if (this.opts.credentialVault && credentialConfig) {
463
468
  const vault = this.opts.credentialVault;
469
+ // The credential a call sends: always read fresh. It records what it
470
+ // saw in the request's memo, so a later status or detail read in the
471
+ // same request is never older than what was sent.
472
+ const fresh = credentialReads(vault, requestReads(requestScope), true);
464
473
  credentialProgram = Effect.tryPromise({
465
- try: () => vault.getAll(id, this.opts.credentialOwner),
474
+ try: () => fresh.getAll(id, this.opts.credentialOwner),
466
475
  catch: (error) => error,
467
476
  }).pipe(Effect.flatMap((values) => {
468
477
  const shape = storedCredentialShape(credentialConfig, values);
@@ -487,6 +496,7 @@ export class Registry {
487
496
  ...(this.opts.oauthClientName !== undefined ? { oauthClientName: this.opts.oauthClientName } : {}),
488
497
  ...(credentialAccess ? { credential: credentialAccess } : {}),
489
498
  requestScope,
499
+ routes: this.opts.routes ?? META_ROUTES,
490
500
  ...callOptions,
491
501
  }, scope?.caller);
492
502
  setViewService(context, OperationCredentials, credentialProgram);
@@ -502,7 +512,11 @@ export class Registry {
502
512
  storage: this.opts.catalogStorage ?? this.opts.storage,
503
513
  partition: this.catalogPartition(scope),
504
514
  credentialRevision: () => catalogRevision(this.opts.credentialVault),
505
- sharedPartition: JSON.stringify([baseUrl, this.opts.publicUrl ?? null, scope?.caller?.pool ?? null]),
515
+ sharedPartition: JSON.stringify([
516
+ baseUrl,
517
+ this.opts.publicUrl ?? null,
518
+ (scope?.caller ?? scope?.operatorCaller)?.pool ?? null,
519
+ ]),
506
520
  defaultTtlMs: (this.opts.toolCacheTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds) * 1000,
507
521
  minTtlMs: (this.opts.catalogMinTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMinTtlSeconds) * 1000,
508
522
  maxTtlMs: (this.opts.catalogMaxTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMaxTtlSeconds) * 1000,
@@ -517,8 +531,9 @@ export class Registry {
517
531
  }
518
532
  : {}),
519
533
  });
534
+ attachStorageDigestKey(context, this.opts.credentialVault);
520
535
  sentSecretsFor(context);
521
- trackCredentialReads(context);
536
+ wrapViewCredentials(context);
522
537
  attachOAuthPartition(context, this.oauthPartition);
523
538
  // Downstream OAuth state is sealed under the vault key, bound to this
524
539
  // connector and owner. The sealer rides beside the context, not on it.
@@ -581,7 +596,7 @@ export class Registry {
581
596
  *
582
597
  * `chunks[n]` lands on `resultKeys.chunk(id, n)` inside `partition` — the
583
598
  * layout connecta.result reads back, so a page fetches only the chunks it covers
584
- * instead of the whole stored result (issue #540). Chunking is a read-cost
599
+ * instead of the whole stored result. Chunking is a read-cost
585
600
  * decision, not a capacity one: however many keys an envelope occupies, it
586
601
  * is one stash entry charged its total ASCII length.
587
602
  *
@@ -603,7 +618,7 @@ export class Registry {
603
618
  * connector's namespace. Backs connecta.result.
604
619
  */
605
620
  resultIdentity() {
606
- return { subject: null, principal: null, endpoint: "/mcp", origin: null };
621
+ return { pool: null, subject: null, principal: null, endpoint: "/mcp", origin: null };
607
622
  }
608
623
  recheckResultAccess(_dependencies, _classification, _signal) {
609
624
  return Promise.resolve(true);
@@ -765,8 +780,8 @@ export class Registry {
765
780
  getTools(id, baseUrl, requestScope, callOptions = {}, scope) {
766
781
  return runEdge(registryEdgeProgram(Registry.prototype, this, "getTools", id, baseUrl, requestScope, callOptions, scope));
767
782
  }
768
- credentialDriftFor(id) {
769
- return runEdge(registryEdgeProgram(Registry.prototype, this, "credentialDriftFor", id));
783
+ credentialDriftFor(id, requestScope) {
784
+ return runEdge(registryEdgeProgram(Registry.prototype, this, "credentialDriftFor", id, requestScope));
770
785
  }
771
786
  /** Age of a complete cache entry; reads never refresh it. */
772
787
  catalogAgeMs(id) {
@@ -946,13 +961,16 @@ export class Registry {
946
961
  return yield* load.join(signal);
947
962
  }));
948
963
  }),
949
- credentialDriftFor: (self, id) => Effect.gen(function* () {
964
+ credentialDriftFor: (self, id, requestScope) => Effect.gen(function* () {
950
965
  const credential = self.connectors.get(id)?.credential;
951
966
  const vault = self.opts.credentialVault;
952
967
  if (!credential || !vault)
953
968
  return undefined;
969
+ // Memoized per request: drift only labels a status or detail view.
970
+ // A dispatch reads its credential fresh (contextFor).
971
+ const reader = requestScope ? credentialReads(vault, requestReads(requestScope)) : vault;
954
972
  return yield* Effect.tryPromise({
955
- try: () => vault.getAll(id, self.opts.credentialOwner),
973
+ try: () => reader.getAll(id, self.opts.credentialOwner),
956
974
  catch: (error) => error,
957
975
  }).pipe(Effect.map((values) => {
958
976
  const shape = storedCredentialShape(credential, values);
@@ -970,8 +988,13 @@ export class Registry {
970
988
  const withObservations = (status) => self.withCatalogObservations(id, baseUrl, status, scope);
971
989
  return duringViewEffect(ctx, () => Effect.gen(function* () {
972
990
  if (connector.credential) {
991
+ // Memoized per request: this read only picks the status shown.
992
+ // The probe that follows reads the credential it sends fresh.
993
+ const vault = self.opts.credentialVault;
973
994
  const values = yield* Effect.tryPromise({
974
- try: () => self.opts.credentialVault?.getAll(id, self.opts.credentialOwner) ?? Promise.resolve(null),
995
+ try: () => vault
996
+ ? credentialReads(vault, requestReads(requestScope)).getAll(id, self.opts.credentialOwner)
997
+ : Promise.resolve(null),
975
998
  catch: (error) => error,
976
999
  });
977
1000
  const shape = storedCredentialShape(connector.credential, values);
@@ -1082,6 +1105,7 @@ class ScopedRegistryView {
1082
1105
  return {
1083
1106
  subject: this.scope.subjectKey ?? null,
1084
1107
  principal: this.scope.principalKey ?? null,
1108
+ pool: this.scope.caller?.pool ?? null,
1085
1109
  endpoint: this.scope.endpoint ?? "/mcp",
1086
1110
  origin: this.scope.origin ?? null,
1087
1111
  };
@@ -1092,8 +1116,8 @@ class ScopedRegistryView {
1092
1116
  resultsStorage() {
1093
1117
  return this.scope.subjectKey ? this.root.scopedStorage(this.scope.subjectKey) : this.root.resultsStorage();
1094
1118
  }
1095
- credentialDriftFor(id) {
1096
- return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "credentialDriftFor", id));
1119
+ credentialDriftFor(id, requestScope) {
1120
+ return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "credentialDriftFor", id, requestScope));
1097
1121
  }
1098
1122
  observedOutputSchema(connectorId, definition) {
1099
1123
  return this.registryFor(connectorId)?.observedOutputSchema(connectorId, definition);
@@ -1149,9 +1173,11 @@ class ScopedRegistryView {
1149
1173
  }
1150
1174
  return carryCatalogFreshness(tools, visible);
1151
1175
  }),
1152
- credentialDriftFor: (self, id) => Effect.suspend(() => {
1176
+ credentialDriftFor: (self, id, requestScope) => Effect.suspend(() => {
1153
1177
  const registry = self.registryFor(id);
1154
- return registry ? registryProgram(registry, "credentialDriftFor", id) : Effect.succeed(undefined);
1178
+ return registry
1179
+ ? registryProgram(registry, "credentialDriftFor", id, requestScope)
1180
+ : Effect.succeed(undefined);
1155
1181
  }),
1156
1182
  statusFor: (self, ...args) => Effect.suspend(() => {
1157
1183
  const registry = self.registryFor(args[0]);
@@ -1,4 +1,4 @@
1
- // The admitted principal a request-auth resolver may receive (#1103).
1
+ // The admitted principal a request-auth resolver may receive.
2
2
  //
3
3
  // Only the caller core attached from the admitting authorization counts
4
4
  // (`callerOf`, see connector-caller.ts). No argument, program or header can
@@ -1,3 +1,4 @@
1
+ import { sha256Hex } from "./bytes.js";
1
2
  import { ProtocolError, INVALID_PARAMS } from "@modelcontextprotocol/server";
2
3
  export const REQUEST_STATE_TTL_MS = 10 * 60_000;
3
4
  export const MAX_INPUT_ROUNDS = 3;
@@ -16,6 +17,5 @@ export function canonicalState(value) {
16
17
  : item);
17
18
  }
18
19
  export async function requestDigest(tool, args) {
19
- const bytes = new TextEncoder().encode(canonicalState({ tool, args }));
20
- return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
20
+ return sha256Hex(canonicalState({ tool, args }));
21
21
  }
@@ -67,7 +67,7 @@ export async function routeAccessTokens(context, manager) {
67
67
  const admin = await authorizeUiIdentity(request, baseUrl, opts.config.auth, "access token management", context.runtimeContext, opts.config.logger);
68
68
  if (!admin.ok)
69
69
  return admin.response;
70
- // Interactive maintainers alone manage agent tokens (#837); anyone else
70
+ // Interactive maintainers alone manage agent tokens; anyone else
71
71
  // gets the same private 404 every operator route gives them.
72
72
  if (!managesAgentTokens(opts, admin) || !admin.identity.principal) {
73
73
  return privateJson({ error: "not found" }, { status: 404 });
@@ -6,7 +6,7 @@ function activityToolVisible(access, event) {
6
6
  }
7
7
  /**
8
8
  * One disclosure rule for the timeline and the UI's last-call overlay. Only
9
- * maintainers read activity (#837), and only after the optional
9
+ * maintainers read activity, and only after the optional
10
10
  * read gate. They may see other principals' shared-connector history,
11
11
  * including request IDs, client name/version, and actor kind/id/namespace, on
12
12
  * plain /mcp and in every declared pool, within the tools that pool declares.
@@ -88,7 +88,7 @@ async function connect(context) {
88
88
  if (!authz.ok) {
89
89
  // Access sign-in is enforced at the edge. Clerk needs its own sign-in page.
90
90
  // The sign-in page returns to this link, which then sends a maintainer on
91
- // to the operator UI. Pool members cannot use that UI yet (#837), so
91
+ // to the operator UI. Pool members cannot use that UI yet, so
92
92
  // signing in there would strand them.
93
93
  if (authz.response.status === 401 &&
94
94
  authorizationCredential(request).kind === "absent" &&
@@ -107,7 +107,7 @@ async function connect(context) {
107
107
  if (authz.principalKey !== handoff.principal || !mayManageConnector(opts, authz, connector)) {
108
108
  return refuse("Sign in as the user who requested this connection and has permission to manage it.");
109
109
  }
110
- // The operator UI serves maintainers only until member views ship (#837).
110
+ // The operator UI serves maintainers only until member views ship.
111
111
  // A member's OAuth link starts here directly, as it does without a UI.
112
112
  const viaUi = Boolean(opts.config.ui) && admitsMaintainer(opts, authz);
113
113
  if (!connector.startAuth) {