@zackbart/connecta 0.31.0 → 0.33.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 (169) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +30 -20
  3. package/bin/connecta.mjs +25 -6
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.d.ts +167 -23
  6. package/dist/access-tokens.js +676 -140
  7. package/dist/agent-identity.js +47 -0
  8. package/dist/auth/cloudflare-access.js +13 -5
  9. package/dist/auth-elicitation.js +1 -1
  10. package/dist/call-admission-context.js +15 -0
  11. package/dist/call-admission.js +30 -43
  12. package/dist/call-recovery.js +6 -2
  13. package/dist/catalog-service.js +7 -2
  14. package/dist/config-value-sources.js +14 -9
  15. package/dist/config.d.ts +58 -78
  16. package/dist/config.js +143 -47
  17. package/dist/connector-access.d.ts +4 -33
  18. package/dist/connector-access.js +15 -99
  19. package/dist/connector-caller.d.ts +7 -1
  20. package/dist/connector-caller.js +3 -2
  21. package/dist/connectors/api-connector.d.ts +27 -4
  22. package/dist/connectors/api-connector.js +2 -0
  23. package/dist/connectors/guarded-fetch.d.ts +9 -0
  24. package/dist/connectors/guarded-fetch.js +7 -0
  25. package/dist/connectors/remote-mcp.js +2 -0
  26. package/dist/describe-config.d.ts +32 -7
  27. package/dist/describe-config.js +9 -18
  28. package/dist/errors.d.ts +1 -1
  29. package/dist/errors.js +1 -0
  30. package/dist/execute.js +16 -14
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +12 -7
  34. package/dist/membership.d.ts +51 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/meta-tools.js +17 -19
  37. package/dist/module-contracts.d.ts +23 -6
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -2
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/contract.d.ts +151 -29
  43. package/dist/operator-ui/endpoints.js +192 -0
  44. package/dist/operator-ui/fix-prompts.js +4 -4
  45. package/dist/operator-ui/generated.js +5 -4
  46. package/dist/operator-ui/member-page.js +112 -0
  47. package/dist/operator-ui/model.d.ts +15 -7
  48. package/dist/operator-ui/view.d.ts +84 -19
  49. package/dist/operator-ui/view.js +83 -21
  50. package/dist/page-styles.js +1 -1
  51. package/dist/program-writes.js +2 -2
  52. package/dist/provider.d.ts +63 -11
  53. package/dist/provider.js +46 -7
  54. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  55. package/dist/providers/_shared/google/workspace.js +31 -9
  56. package/dist/providers/_shared/rest/tools.js +2 -2
  57. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  58. package/dist/providers/_shared/rest/value-safety.js +699 -7
  59. package/dist/providers/breeze/skill.generated.js +2 -2
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +3 -3
  62. package/dist/providers/cloudflare/rest.js +25 -3
  63. package/dist/providers/cloudflare/skill.generated.js +2 -2
  64. package/dist/providers/cloudflare/value-safety.js +32 -0
  65. package/dist/providers/docs/index.d.ts +1 -1
  66. package/dist/providers/docs/index.js +5 -5
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.d.ts +1 -1
  69. package/dist/providers/drive/index.js +4 -4
  70. package/dist/providers/drive/skill.generated.js +2 -2
  71. package/dist/providers/forms/index.d.ts +1 -1
  72. package/dist/providers/forms/index.js +5 -5
  73. package/dist/providers/forms/skill.generated.js +2 -2
  74. package/dist/providers/github/skill.generated.js +2 -2
  75. package/dist/providers/gmail/index.d.ts +1 -1
  76. package/dist/providers/gmail/index.js +279 -26
  77. package/dist/providers/gmail/skill.generated.js +2 -2
  78. package/dist/providers/linear/index.js +1 -1
  79. package/dist/providers/mixpanel/index.d.ts +27 -12
  80. package/dist/providers/mixpanel/index.js +58 -6
  81. package/dist/providers/mixpanel/operations.js +20 -0
  82. package/dist/providers/mixpanel/rest.js +603 -0
  83. package/dist/providers/mixpanel/skill.generated.js +3 -2
  84. package/dist/providers/mixpanel/value-safety.js +16 -0
  85. package/dist/providers/notion/index.d.ts +7 -0
  86. package/dist/providers/notion/index.js +116 -11
  87. package/dist/providers/notion/rest.js +183 -24
  88. package/dist/providers/notion/skill.generated.js +4 -4
  89. package/dist/providers/notion/value-safety.js +30 -5
  90. package/dist/providers/overflow/index.js +1 -1
  91. package/dist/providers/planning-center/index.d.ts +27 -0
  92. package/dist/providers/planning-center/index.js +287 -25
  93. package/dist/providers/planning-center/skill.generated.js +24 -4
  94. package/dist/providers/sheets/index.d.ts +4 -1
  95. package/dist/providers/sheets/index.js +9 -6
  96. package/dist/providers/sheets/skill.generated.js +2 -2
  97. package/dist/providers/slides/index.d.ts +1 -1
  98. package/dist/providers/slides/index.js +6 -6
  99. package/dist/providers/slides/skill.generated.js +2 -2
  100. package/dist/providers/stripe/rest.js +9 -2
  101. package/dist/providers/stripe/skill.generated.js +2 -2
  102. package/dist/providers/stripe/value-safety.js +22 -0
  103. package/dist/providers/vercel/index.js +2 -2
  104. package/dist/providers/vercel/rest.js +20 -2
  105. package/dist/providers/vercel/skill.generated.js +2 -2
  106. package/dist/providers/vercel/value-safety.js +22 -0
  107. package/dist/providers/wispr-flow/index.d.ts +14 -0
  108. package/dist/providers/wispr-flow/index.js +117 -0
  109. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  110. package/dist/registry.d.ts +2 -3
  111. package/dist/registry.js +8 -9
  112. package/dist/routes/access-tokens.js +82 -75
  113. package/dist/routes/activity-disclosure.js +17 -18
  114. package/dist/routes/activity.js +34 -54
  115. package/dist/routes/actor-labels.js +33 -4
  116. package/dist/routes/agent-path.js +43 -0
  117. package/dist/routes/connect.js +16 -12
  118. package/dist/routes/credentials.js +16 -40
  119. package/dist/routes/mcp.js +155 -85
  120. package/dist/routes/oauth-management.js +11 -5
  121. package/dist/routes/oauth.js +3 -9
  122. package/dist/routes/operator.js +25 -17
  123. package/dist/routes/shared.d.ts +60 -20
  124. package/dist/routes/shared.js +187 -96
  125. package/dist/routes/ui-config.js +437 -97
  126. package/dist/routes/ui-data.js +2 -2
  127. package/dist/routes/ui.js +9 -16
  128. package/dist/runtime/call-admission.js +9 -0
  129. package/dist/server.js +8 -0
  130. package/dist/skills.js +5 -4
  131. package/dist/storage/keys.js +9 -1
  132. package/dist/tool-safety.js +13 -5
  133. package/dist/types.d.ts +12 -1
  134. package/dist/ui.d.ts +2 -2
  135. package/dist/ui.js +17 -15
  136. package/dist/usage-guide.js +3 -3
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/workspace-address.js +9 -0
  140. package/dist/workspace-subject.js +23 -0
  141. package/documentation/README.md +46 -22
  142. package/documentation/architecture.md +378 -58
  143. package/documentation/auth.md +806 -215
  144. package/documentation/code-mode.md +93 -81
  145. package/documentation/deploying.md +128 -26
  146. package/documentation/integrating.md +145 -22
  147. package/documentation/meta-tools.md +58 -46
  148. package/documentation/migration-0.33.md +344 -0
  149. package/documentation/operating.md +14 -9
  150. package/documentation/operator-tests.md +117 -31
  151. package/documentation/operator-ui.md +326 -55
  152. package/documentation/provider-auth-migration.md +42 -27
  153. package/documentation/provider-migration-0.29.md +3 -3
  154. package/examples/worker/AGENTS.md +35 -8
  155. package/examples/worker/README.md +92 -53
  156. package/examples/worker/src/connecta.config.ts +41 -20
  157. package/examples/worker/wrangler.jsonc +3 -0
  158. package/package.json +7 -2
  159. package/templates/node/.env.example +3 -0
  160. package/templates/node/AGENTS.md +33 -10
  161. package/templates/node/Dockerfile +2 -2
  162. package/templates/node/README.md +71 -44
  163. package/templates/node/docker-compose.yml +3 -2
  164. package/templates/node/package.json +4 -3
  165. package/templates/node/src/adopt-token.ts +23 -0
  166. package/templates/node/src/connecta.config.ts +33 -12
  167. package/templates/node/src/rotate-token.ts +17 -0
  168. package/dist/tool-safety.d.ts +0 -9
  169. package/templates/node/src/provision-token.ts +0 -17
@@ -46,7 +46,7 @@ import { skill } from "./skill.generated.js";
46
46
  import { apiConnector as api, defined } from "../../connectors/api-connector.js";
47
47
  import { ConnectorCallError } from "../../errors.js";
48
48
  import { RESULT_BUDGET_BYTES, clampText, jsonBytes } from "../_shared/google/result-size.js";
49
- import { googleOutcomeOf, GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, } from "../_shared/google/workspace.js";
49
+ import { googleOutcomeOf, GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, workspaceConnector, } from "../_shared/google/workspace.js";
50
50
  import { asProviderFactory } from "../../provider.js";
51
51
  /** The Slides API root. Every path beneath it names a presentation by id. */
52
52
  export const SLIDES_API_BASE_URL = "https://slides.googleapis.com/v1";
@@ -2051,7 +2051,7 @@ function tools(client) {
2051
2051
  name: "create_presentation",
2052
2052
  description: "Create an empty Google Slides deck with one title slide in the user's My Drive root. It cannot place the deck in a folder or copy a template.",
2053
2053
  // Additive: a new file changes nothing that existed. Not read-only, so
2054
- // program calls require a trusted pool; other writes use
2054
+ // program calls require a writable endpoint; other writes use
2055
2055
  // call_destructive_tool. The host controls approval.
2056
2056
  annotations: { readOnlyHint: false, destructiveHint: false },
2057
2057
  inputSchema: input({
@@ -2239,7 +2239,7 @@ function tools(client) {
2239
2239
  name: "create_comment",
2240
2240
  description: "Comment on a slide, a notes page, an element, a range of its text, or a table cell, optionally assigning it. Adds a thread; Slides notifies as in the editor.",
2241
2241
  // Additive: a new thread changes nothing that existed. Not read-only,
2242
- // so program calls require a trusted pool; other writes use
2242
+ // so program calls require a writable endpoint; other writes use
2243
2243
  // call_destructive_tool. The host controls approval.
2244
2244
  annotations: { readOnlyHint: false, destructiveHint: false },
2245
2245
  inputSchema: input({
@@ -2594,7 +2594,7 @@ function usageGuide(purpose, instructions) {
2594
2594
  *
2595
2595
  * Reads run in programs. `create_presentation`, `create_slide`,
2596
2596
  * `create_comment`, and `create_comment_reply` are additive writes the host
2597
- * approves through call_destructive_tool or a trusted program;
2597
+ * approves through call_destructive_tool or a program on a writable endpoint;
2598
2598
  * `replace_all_text`, `update_comment_thread`, `update_comment_post`,
2599
2599
  * `delete_comment`, `delete_comment_reply`, and `batch_update_presentation`
2600
2600
  * are destructive.
@@ -2626,7 +2626,7 @@ function slidesConnector(id, options) {
2626
2626
  notFound: "ambiguous",
2627
2627
  connection,
2628
2628
  });
2629
- return api(id, {
2629
+ return workspaceConnector(connection, api(id, {
2630
2630
  ...defined({
2631
2631
  callAdmission: options.callAdmission,
2632
2632
  maxResultBytes: options.maxResultBytes,
@@ -2641,5 +2641,5 @@ function slidesConnector(id, options) {
2641
2641
  required: true,
2642
2642
  },
2643
2643
  tools: tools(client),
2644
- });
2644
+ }));
2645
2645
  }
@@ -1,9 +1,9 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "# Google Slides usage\n\nActs as the signed-in person in Google Slides through Workspace delegation: read, create, and edit the decks they can open.\n\nConnection purpose: \n\n## Whose decks\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the decks that person can open. No argument names an\naccount. A call with none mapped fails `auth_required`; only an operator can\nchange the mapping. A deck the person cannot see and one that does not exist\nfail alike — Google does not say which.\n\n## Finding a deck\n\nSlides cannot list or search decks; that is Google Drive's job. Take the id\nfrom a Drive search or from a URL (`/presentation/d/<id>/`).\n\n## Reading\n\n- `get_presentation` returns each slide's text in reading order (top to\n bottom, then left to right), speaker notes, and alt text for images,\n videos, and charts. Empty placeholders are listed with their id and type;\n lines and other empty shapes are counted in `omittedElements`. `index`\n is 0-based, the numbering `create_slide` takes.\n- Nothing is cut without a way back. Text past `maxCharsPerSlide`, or past\n what one result can carry (about 192 KB), ends with a marker, and its\n element's `textCursor` (the slide's `notesCursor` for notes) is a\n `get_page` cursor that continues from the cut. A slide too crowded to\n list whole gives `elementsNotShown` and an `elementsCursor`.\n- `get_page` reads any page by objectId — a slide, a layout (its\n placeholders' types and indexes, for `placeholderIdMappings`), a master,\n a notes page (`notesPageId`), or the notes master (`notesMasterId`) —\n listing every element, groups opened,\n and continuing long text across pages from `page.nextCursor`.\n- `list_layouts` lists masters and their layouts; `get_presentation`\n previews them, and `layoutsCursor` continues where the preview stops.\n- `raw: true` returns Slides' own JSON: `get_presentation` a page of\n slides, `get_page` the page and its top-level elements. One too large\n for a result is named (`rawNotShown`) or sent in `rawJson` chunks;\n join the chunks' `json` in order and parse.\n- Every cursor is bound to the deck and the revision it was read at. A\n `conflict` on a cursor means the deck changed: start again without one.\n A page may hold fewer rows than `limit`; follow `page.nextCursor`.\n- `get_slide_thumbnail` returns a link, never the image. The link opens as\n this person for about 30 minutes; do not share it.\n\n## Writing\n\n- `create_presentation` and `create_slide` are additive. A new slide is\n empty: fill it with `batch_update_presentation` (`insertText` into its\n placeholders, after reading their ids with `get_presentation`).\n- `replace_all_text` changes every literal match, case-sensitive unless\n `matchCase: false`. Pass `requiredRevisionId` to refuse a deck that\n changed since you read it.\n- `batch_update_presentation` takes Slides' own Request objects and always\n requires the `revisionId` from the read the requests were built on. All\n requests apply or none do, except comment changes (below). Each write\n returns the new `revisionId` for the next one. A `conflict` means the\n deck changed since that read: re-read it, rebuild the requests, and send\n them with the new `revisionId`. Its replies keep every id at every depth\n whole, in every reply; text is cut first, and a large reply's other fields\n are named in `cut`. Ids too many for this tool's 192 KiB budget are all\n returned anyway, without text; past 256 KiB, more than a program can\n receive, call the tool directly and page with `connecta.result`. A batch\n whose ids pass even that returns its comment state and `repliesNotShown`;\n read what it made with `list_comments`, `get_presentation`, or\n `get_page`, and do not send it again.\n- An id or revision too long to copy into a result is left out and flagged\n (`revisionIdNotShown`), never cut. A failure that says the write\n applied means Google answered 2xx: do not repeat it; re-read instead.\n- Nothing here shares, moves, or deletes a deck.\n\n## Comments\n\n- `list_comments` lists the deck's threads, or one page's with\n `pageObjectId` (a speaker-notes comment is on the notes page). Each has\n its `commentId`, `status` (OPEN or RESOLVED), where it is anchored\n (`pageObjectIds`, `objectIds`), the `quote` it was made on, its\n `headPost`, and `replies`. A post's `author.user` (`users/…`) is\n the stable id; `displayName` is only a name. Slides gives no `user`\n for an anonymous or imported post. Text too long for one result continues\n on the next page: a row with `continued` picks up its thread where the\n last stopped, and `repliesOffset` counts the replies before it. Its\n cursor is bound to the threads and their anchors as well as the revision,\n so a comment added or moved between pages is a `conflict` too. Reading\n comments needs comment access; a view-only account is refused.\n- The projection leaves out each post's HTML (`contentHtml`), whether it\n came from a copied deck, and the text and cell ranges an anchor covers.\n `raw: true` returns each thread as Slides sends it, with\n `commentAnchors` added (its anchors, each with its `pageObjectId`),\n in `rawJson` chunks when one is too large.\n- `create_comment` and `create_comment_reply` are additive and return\n every new id whole. `update_comment_thread` resolves, reopens, or\n reassigns a thread (one at a time; a reassignment needs `content`),\n replacing its status or assignee, and is destructive,\n like `update_comment_post`, `delete_comment`, and\n `delete_comment_reply`; Slides allows the last three only to the\n post's author. Comments notify people the way the Slides editor does.\n- Slides saves comment changes apart from the rest of a write. When it does\n not confirm they saved, the result carries `commentUpdateState` — for\n example `ALL_FAILED_UNKNOWN_REASON` — and the write's other changes may\n still have applied. Do not repeat it: re-read with `list_comments`, then\n decide.\n",
3
+ "content": "# Google Slides usage\n\nActs as the signed-in person in Google Slides through Workspace delegation: read, create, and edit the decks they can open.\n\nConnection purpose: \n\n## Whose decks\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the decks that person can open; an agent acts as the\nowner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument names an account. A call with\nnone mapped, or from an agent whose pool has no owner, fails\n`auth_required`; only an operator can change either. A deck the person cannot see and one that does not exist\nfail alike — Google does not say which.\n\n## Finding a deck\n\nSlides cannot list or search decks; that is Google Drive's job. Take the id\nfrom a Drive search or from a URL (`/presentation/d/<id>/`).\n\n## Reading\n\n- `get_presentation` returns each slide's text in reading order (top to\n bottom, then left to right), speaker notes, and alt text for images,\n videos, and charts. Empty placeholders are listed with their id and type;\n lines and other empty shapes are counted in `omittedElements`. `index`\n is 0-based, the numbering `create_slide` takes.\n- Nothing is cut without a way back. Text past `maxCharsPerSlide`, or past\n what one result can carry (about 192 KB), ends with a marker, and its\n element's `textCursor` (the slide's `notesCursor` for notes) is a\n `get_page` cursor that continues from the cut. A slide too crowded to\n list whole gives `elementsNotShown` and an `elementsCursor`.\n- `get_page` reads any page by objectId — a slide, a layout (its\n placeholders' types and indexes, for `placeholderIdMappings`), a master,\n a notes page (`notesPageId`), or the notes master (`notesMasterId`) —\n listing every element, groups opened,\n and continuing long text across pages from `page.nextCursor`.\n- `list_layouts` lists masters and their layouts; `get_presentation`\n previews them, and `layoutsCursor` continues where the preview stops.\n- `raw: true` returns Slides' own JSON: `get_presentation` a page of\n slides, `get_page` the page and its top-level elements. One too large\n for a result is named (`rawNotShown`) or sent in `rawJson` chunks;\n join the chunks' `json` in order and parse.\n- Every cursor is bound to the deck and the revision it was read at. A\n `conflict` on a cursor means the deck changed: start again without one.\n A page may hold fewer rows than `limit`; follow `page.nextCursor`.\n- `get_slide_thumbnail` returns a link, never the image. The link opens as\n this person for about 30 minutes; do not share it.\n\n## Writing\n\n- `create_presentation` and `create_slide` are additive. A new slide is\n empty: fill it with `batch_update_presentation` (`insertText` into its\n placeholders, after reading their ids with `get_presentation`).\n- `replace_all_text` changes every literal match, case-sensitive unless\n `matchCase: false`. Pass `requiredRevisionId` to refuse a deck that\n changed since you read it.\n- `batch_update_presentation` takes Slides' own Request objects and always\n requires the `revisionId` from the read the requests were built on. All\n requests apply or none do, except comment changes (below). Each write\n returns the new `revisionId` for the next one. A `conflict` means the\n deck changed since that read: re-read it, rebuild the requests, and send\n them with the new `revisionId`. Its replies keep every id at every depth\n whole, in every reply; text is cut first, and a large reply's other fields\n are named in `cut`. Ids too many for this tool's 192 KiB budget are all\n returned anyway, without text; past 256 KiB, more than a program can\n receive, call the tool directly and page with `connecta.result`. A batch\n whose ids pass even that returns its comment state and `repliesNotShown`;\n read what it made with `list_comments`, `get_presentation`, or\n `get_page`, and do not send it again.\n- An id or revision too long to copy into a result is left out and flagged\n (`revisionIdNotShown`), never cut. A failure that says the write\n applied means Google answered 2xx: do not repeat it; re-read instead.\n- Nothing here shares, moves, or deletes a deck.\n\n## Comments\n\n- `list_comments` lists the deck's threads, or one page's with\n `pageObjectId` (a speaker-notes comment is on the notes page). Each has\n its `commentId`, `status` (OPEN or RESOLVED), where it is anchored\n (`pageObjectIds`, `objectIds`), the `quote` it was made on, its\n `headPost`, and `replies`. A post's `author.user` (`users/…`) is\n the stable id; `displayName` is only a name. Slides gives no `user`\n for an anonymous or imported post. Text too long for one result continues\n on the next page: a row with `continued` picks up its thread where the\n last stopped, and `repliesOffset` counts the replies before it. Its\n cursor is bound to the threads and their anchors as well as the revision,\n so a comment added or moved between pages is a `conflict` too. Reading\n comments needs comment access; a view-only account is refused.\n- The projection leaves out each post's HTML (`contentHtml`), whether it\n came from a copied deck, and the text and cell ranges an anchor covers.\n `raw: true` returns each thread as Slides sends it, with\n `commentAnchors` added (its anchors, each with its `pageObjectId`),\n in `rawJson` chunks when one is too large.\n- `create_comment` and `create_comment_reply` are additive and return\n every new id whole. `update_comment_thread` resolves, reopens, or\n reassigns a thread (one at a time; a reassignment needs `content`),\n replacing its status or assignee, and is destructive,\n like `update_comment_post`, `delete_comment`, and\n `delete_comment_reply`; Slides allows the last three only to the\n post's author. Comments notify people the way the Slides editor does.\n- Slides saves comment changes apart from the rest of a write. When it does\n not confirm they saved, the result carries `commentUpdateState` — for\n example `ALL_FAILED_UNKNOWN_REASON` — and the write's other changes may\n still have applied. Do not repeat it: re-read with `list_comments`, then\n decide.\n",
4
4
  "instructionsHeading": "Connection instructions",
5
5
  "fragments": {
6
6
  "guide_0": "# Google Slides usage\n\nActs as the signed-in person in Google Slides through Workspace delegation: read, create, and edit the decks they can open.\n\nConnection purpose: ",
7
- "guide_1": "\n\n## Whose decks\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the decks that person can open. No argument names an\naccount. A call with none mapped fails `auth_required`; only an operator can\nchange the mapping. A deck the person cannot see and one that does not exist\nfail alike — Google does not say which.\n\n## Finding a deck\n\nSlides cannot list or search decks; that is Google Drive's job. Take the id\nfrom a Drive search or from a URL (`/presentation/d/<id>/`).\n\n## Reading\n\n- `get_presentation` returns each slide's text in reading order (top to\n bottom, then left to right), speaker notes, and alt text for images,\n videos, and charts. Empty placeholders are listed with their id and type;\n lines and other empty shapes are counted in `omittedElements`. `index`\n is 0-based, the numbering `create_slide` takes.\n- Nothing is cut without a way back. Text past `maxCharsPerSlide`, or past\n what one result can carry (about 192 KB), ends with a marker, and its\n element's `textCursor` (the slide's `notesCursor` for notes) is a\n `get_page` cursor that continues from the cut. A slide too crowded to\n list whole gives `elementsNotShown` and an `elementsCursor`.\n- `get_page` reads any page by objectId — a slide, a layout (its\n placeholders' types and indexes, for `placeholderIdMappings`), a master,\n a notes page (`notesPageId`), or the notes master (`notesMasterId`) —\n listing every element, groups opened,\n and continuing long text across pages from `page.nextCursor`.\n- `list_layouts` lists masters and their layouts; `get_presentation`\n previews them, and `layoutsCursor` continues where the preview stops.\n- `raw: true` returns Slides' own JSON: `get_presentation` a page of\n slides, `get_page` the page and its top-level elements. One too large\n for a result is named (`rawNotShown`) or sent in `rawJson` chunks;\n join the chunks' `json` in order and parse.\n- Every cursor is bound to the deck and the revision it was read at. A\n `conflict` on a cursor means the deck changed: start again without one.\n A page may hold fewer rows than `limit`; follow `page.nextCursor`.\n- `get_slide_thumbnail` returns a link, never the image. The link opens as\n this person for about 30 minutes; do not share it.\n\n## Writing\n\n- `create_presentation` and `create_slide` are additive. A new slide is\n empty: fill it with `batch_update_presentation` (`insertText` into its\n placeholders, after reading their ids with `get_presentation`).\n- `replace_all_text` changes every literal match, case-sensitive unless\n `matchCase: false`. Pass `requiredRevisionId` to refuse a deck that\n changed since you read it.\n- `batch_update_presentation` takes Slides' own Request objects and always\n requires the `revisionId` from the read the requests were built on. All\n requests apply or none do, except comment changes (below). Each write\n returns the new `revisionId` for the next one. A `conflict` means the\n deck changed since that read: re-read it, rebuild the requests, and send\n them with the new `revisionId`. Its replies keep every id at every depth\n whole, in every reply; text is cut first, and a large reply's other fields\n are named in `cut`. Ids too many for this tool's 192 KiB budget are all\n returned anyway, without text; past 256 KiB, more than a program can\n receive, call the tool directly and page with `connecta.result`. A batch\n whose ids pass even that returns its comment state and `repliesNotShown`;\n read what it made with `list_comments`, `get_presentation`, or\n `get_page`, and do not send it again.\n- An id or revision too long to copy into a result is left out and flagged\n (`revisionIdNotShown`), never cut. A failure that says the write\n applied means Google answered 2xx: do not repeat it; re-read instead.\n- Nothing here shares, moves, or deletes a deck.\n\n## Comments\n\n- `list_comments` lists the deck's threads, or one page's with\n `pageObjectId` (a speaker-notes comment is on the notes page). Each has\n its `commentId`, `status` (OPEN or RESOLVED), where it is anchored\n (`pageObjectIds`, `objectIds`), the `quote` it was made on, its\n `headPost`, and `replies`. A post's `author.user` (`users/…`) is\n the stable id; `displayName` is only a name. Slides gives no `user`\n for an anonymous or imported post. Text too long for one result continues\n on the next page: a row with `continued` picks up its thread where the\n last stopped, and `repliesOffset` counts the replies before it. Its\n cursor is bound to the threads and their anchors as well as the revision,\n so a comment added or moved between pages is a `conflict` too. Reading\n comments needs comment access; a view-only account is refused.\n- The projection leaves out each post's HTML (`contentHtml`), whether it\n came from a copied deck, and the text and cell ranges an anchor covers.\n `raw: true` returns each thread as Slides sends it, with\n `commentAnchors` added (its anchors, each with its `pageObjectId`),\n in `rawJson` chunks when one is too large.\n- `create_comment` and `create_comment_reply` are additive and return\n every new id whole. `update_comment_thread` resolves, reopens, or\n reassigns a thread (one at a time; a reassignment needs `content`),\n replacing its status or assignee, and is destructive,\n like `update_comment_post`, `delete_comment`, and\n `delete_comment_reply`; Slides allows the last three only to the\n post's author. Comments notify people the way the Slides editor does.\n- Slides saves comment changes apart from the rest of a write. When it does\n not confirm they saved, the result carries `commentUpdateState` — for\n example `ALL_FAILED_UNKNOWN_REASON` — and the write's other changes may\n still have applied. Do not repeat it: re-read with `list_comments`, then\n decide.\n"
7
+ "guide_1": "\n\n## Whose decks\n\nEvery call acts as the Workspace account deployment config maps the caller\nto, and reaches exactly the decks that person can open; an agent acts as the\nowner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument names an account. A call with\nnone mapped, or from an agent whose pool has no owner, fails\n`auth_required`; only an operator can change either. A deck the person cannot see and one that does not exist\nfail alike — Google does not say which.\n\n## Finding a deck\n\nSlides cannot list or search decks; that is Google Drive's job. Take the id\nfrom a Drive search or from a URL (`/presentation/d/<id>/`).\n\n## Reading\n\n- `get_presentation` returns each slide's text in reading order (top to\n bottom, then left to right), speaker notes, and alt text for images,\n videos, and charts. Empty placeholders are listed with their id and type;\n lines and other empty shapes are counted in `omittedElements`. `index`\n is 0-based, the numbering `create_slide` takes.\n- Nothing is cut without a way back. Text past `maxCharsPerSlide`, or past\n what one result can carry (about 192 KB), ends with a marker, and its\n element's `textCursor` (the slide's `notesCursor` for notes) is a\n `get_page` cursor that continues from the cut. A slide too crowded to\n list whole gives `elementsNotShown` and an `elementsCursor`.\n- `get_page` reads any page by objectId — a slide, a layout (its\n placeholders' types and indexes, for `placeholderIdMappings`), a master,\n a notes page (`notesPageId`), or the notes master (`notesMasterId`) —\n listing every element, groups opened,\n and continuing long text across pages from `page.nextCursor`.\n- `list_layouts` lists masters and their layouts; `get_presentation`\n previews them, and `layoutsCursor` continues where the preview stops.\n- `raw: true` returns Slides' own JSON: `get_presentation` a page of\n slides, `get_page` the page and its top-level elements. One too large\n for a result is named (`rawNotShown`) or sent in `rawJson` chunks;\n join the chunks' `json` in order and parse.\n- Every cursor is bound to the deck and the revision it was read at. A\n `conflict` on a cursor means the deck changed: start again without one.\n A page may hold fewer rows than `limit`; follow `page.nextCursor`.\n- `get_slide_thumbnail` returns a link, never the image. The link opens as\n this person for about 30 minutes; do not share it.\n\n## Writing\n\n- `create_presentation` and `create_slide` are additive. A new slide is\n empty: fill it with `batch_update_presentation` (`insertText` into its\n placeholders, after reading their ids with `get_presentation`).\n- `replace_all_text` changes every literal match, case-sensitive unless\n `matchCase: false`. Pass `requiredRevisionId` to refuse a deck that\n changed since you read it.\n- `batch_update_presentation` takes Slides' own Request objects and always\n requires the `revisionId` from the read the requests were built on. All\n requests apply or none do, except comment changes (below). Each write\n returns the new `revisionId` for the next one. A `conflict` means the\n deck changed since that read: re-read it, rebuild the requests, and send\n them with the new `revisionId`. Its replies keep every id at every depth\n whole, in every reply; text is cut first, and a large reply's other fields\n are named in `cut`. Ids too many for this tool's 192 KiB budget are all\n returned anyway, without text; past 256 KiB, more than a program can\n receive, call the tool directly and page with `connecta.result`. A batch\n whose ids pass even that returns its comment state and `repliesNotShown`;\n read what it made with `list_comments`, `get_presentation`, or\n `get_page`, and do not send it again.\n- An id or revision too long to copy into a result is left out and flagged\n (`revisionIdNotShown`), never cut. A failure that says the write\n applied means Google answered 2xx: do not repeat it; re-read instead.\n- Nothing here shares, moves, or deletes a deck.\n\n## Comments\n\n- `list_comments` lists the deck's threads, or one page's with\n `pageObjectId` (a speaker-notes comment is on the notes page). Each has\n its `commentId`, `status` (OPEN or RESOLVED), where it is anchored\n (`pageObjectIds`, `objectIds`), the `quote` it was made on, its\n `headPost`, and `replies`. A post's `author.user` (`users/…`) is\n the stable id; `displayName` is only a name. Slides gives no `user`\n for an anonymous or imported post. Text too long for one result continues\n on the next page: a row with `continued` picks up its thread where the\n last stopped, and `repliesOffset` counts the replies before it. Its\n cursor is bound to the threads and their anchors as well as the revision,\n so a comment added or moved between pages is a `conflict` too. Reading\n comments needs comment access; a view-only account is refused.\n- The projection leaves out each post's HTML (`contentHtml`), whether it\n came from a copied deck, and the text and cell ranges an anchor covers.\n `raw: true` returns each thread as Slides sends it, with\n `commentAnchors` added (its anchors, each with its `pageObjectId`),\n in `rawJson` chunks when one is too large.\n- `create_comment` and `create_comment_reply` are additive and return\n every new id whole. `update_comment_thread` resolves, reopens, or\n reassigns a thread (one at a time; a reassignment needs `content`),\n replacing its status or assignee, and is destructive,\n like `update_comment_post`, `delete_comment`, and\n `delete_comment_reply`; Slides allows the last three only to the\n post's author. Comments notify people the way the Slides editor does.\n- Slides saves comment changes apart from the rest of a write. When it does\n not confirm they saved, the result carries `commentUpdateState` — for\n example `ALL_FAILED_UNKNOWN_REASON` — and the write's other changes may\n still have applied. Do not repeat it: re-read with `list_comments`, then\n decide.\n"
8
8
  }
9
9
  };
@@ -2,7 +2,7 @@ import { retryAfterMs } from "../../connectors/guarded-fetch.js";
2
2
  import { ConnectorCallError } from "../../errors.js";
3
3
  import { OperationIndex } from "../_shared/rest/operation-index.js";
4
4
  import { callRest, restCall, restTransport, } from "../_shared/rest/tools.js";
5
- import { valueSafety } from "../_shared/rest/value-safety.js";
5
+ import { valueSafety, withholdEmbeddedUrlsIn, refuseWithheldLinks } from "../_shared/rest/value-safety.js";
6
6
  import { openapi } from "./openapi.generated.js";
7
7
  import { STRIPE_VALUE_SAFETY } from "./value-safety.js";
8
8
  /** Stripe's REST origin. */
@@ -137,7 +137,7 @@ function refuse(call) {
137
137
  * the type, code, decline code, and param still route.
138
138
  */
139
139
  function stripeFailure(status, headers, body, withheld = false) {
140
- const error = record(record(body)["error"]);
140
+ const error = record(record(withholdEmbeddedUrlsIn(body))["error"]);
141
141
  const code = text(error["code"]);
142
142
  const kind = [text(error["type"]), code, text(error["decline_code"])].filter(Boolean).join(" ");
143
143
  const param = text(error["param"]);
@@ -277,6 +277,13 @@ export function stripeRest(mode, connectedAccount) {
277
277
  failure: (status, headers, body, call) => stripeFailure(status, headers, body, call ? SAFETY.withholdsErrors(call.op) : false),
278
278
  readPosts: READ_POSTS,
279
279
  valueSafety: SAFETY,
280
+ admit(call) {
281
+ if (call.method === "GET" ||
282
+ call.method === "HEAD" ||
283
+ READ_POSTS.some(([method, path]) => method === call.method && path === call.op.path))
284
+ return;
285
+ refuseWithheldLinks([call.body, call.query, call.path], "Stripe");
286
+ },
280
287
  refuse,
281
288
  encode,
282
289
  page,
@@ -3,8 +3,8 @@ export const skill = {
3
3
  "content": "- Lists are cursor-paginated: `limit` defaults to 10 and caps at 100, `starting_after` and `ending_before` take an object id and are mutually exclusive, and `has_more` says whether to continue. Page inside `execute_code`.\n- Search filters on a documented per-resource field set, not on arbitrary attributes. Charges search takes `amount`, `created`, `currency`, `customer`, `status`, `refunded`, `disputed`, `metadata`, `billing_details.address.postal_code`, and `payment_method_details.<source>.*` card fields — there is no `payment_intent` field. When the field you want is not searchable, retrieve the parent object and follow its reference (the PaymentIntent's `latest_charge`) instead of retrying the search with another spelling.\n- The account → `stripe_api_search` → `stripe_api_details` → `stripe_api_read` sequence is one program, not four turns: resolve the method once, then call that method as many times as the investigation needs in the same run.\n- Decline outcomes live on the charge — `outcome`, `failure_code`, `failure_message` — reached from the PaymentIntent's `latest_charge`, so \"why did this payment fail\" is a PaymentIntent read followed by one charge read.\n- Resolve ids before acting; never guess one. Stripe ids are typed prefixes — `cus_` customer, `sub_` subscription, `ch_` charge, `pi_` payment intent, `in_` invoice, `acct_` account — and a plausible-looking id belongs to a different object or to nobody.\n- Amounts are integers in the currency's minor unit: `1099` is 10.99 USD, and zero-decimal currencies like JPY take `10` for 10 JPY. Never send a decimal.\n- Treat every create, update, delete, refund, and report run as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
4
4
  "instructionsHeading": "Account instructions",
5
5
  "fragments": {
6
- "oauth": "\n\nThis OAuth session may expose both live and sandbox Stripe accounts. Discover the live account-selection tools, select the intended account and mode, then carry exact context fields such as `stripe_context` and `livemode` wherever the schema requires them. A live-mode write moves real money; a sandbox write changes test data. Never infer the account or mode from connector metadata.\n\n- Discover account and mode with the live account-selection tools before every account-scoped read or write. The published inventory has changed account-tool names; do not assume `list_available_accounts_or_orgs` is available. Select the intended result and carry the exact context fields required by the live schema unchanged. If the account, mode, or supported selector is ambiguous, stop and ask; never guess.\n- Organization accounts are not Stripe Connect connected accounts. A Connect call requires a separate API-key connector whose `connectedAccount` sends Stripe's documented `Stripe-Account` header; OAuth does not support that path.\n- Four generic tools reach any Stripe API method. Find the method with `stripe_api_search`, read its parameters with `stripe_api_details`, then call `stripe_api_read` (GET) or `stripe_api_write` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — `stripe_api_details` is cheaper than a rejected write.\n- Prefer a dedicated tool when one covers the task: `get_balance_summary` for balances and `stripe_analytics` for Sigma or Metrics reporting. Use `stripe_api_search` for everything else instead of assuming a retired dedicated tool still exists.\n- `stripe_implementation_planner` and the query-execution intents of `stripe_analytics` create provider-side planning or query-run state. Connecta therefore classifies both as writes and enforces the configured pool trust policy; their retrieval paths stay behind the same tool boundary.\n- `stripe_api_write` carries the blast radius of the entire write API — every POST, PATCH, PUT, and DELETE, from a customer edit to a subscription cancellation. State the method and path explicitly; execution follows the configured pool trust policy.\n- Any `stripe_api_read` list or `stripe_api_search` that returns full objects belongs inside `execute_code`, projected to the fields the question needs before `return`. Neither `limit` nor `expand` substitutes for that: an unprojected list of customers or invoices truncates long before it answers, and a projected one keeps the customer's name, email, and address out of the transcript.\n- Find an object with `stripe_api_search` (or a list endpoint through `stripe_api_read`) and carry the `id` it returned into the write.\n- This connection's tool list is not a fixed set. Stripe gates parts of its MCP catalog by account, integration, and beta enrollment, so search this connector for what it actually exposes rather than assuming a documented tool is here.\n- Send an `Idempotency-Key` on every write you might retry, if the tool accepts it, and reuse the same key for the retry. A retry with a fresh key is a second charge, not a second attempt.\n",
7
- "key": "\n\n- This is Connecta's REST connector to `api.stripe.com`, holding one operator-managed key. It reaches exactly one account in one mode, so there is no account selection and no `stripe_context` or `livemode` argument.\n- `stripe_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `stripe_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `stripe_api_read` calls GET operations, plus `POST /v1/invoices/create_preview`, which only previews. `stripe_api_write` calls every other POST and every DELETE, from a customer edit to a subscription cancellation, with `method` and `path` stated; execution follows the configured pool trust policy.\n- Paths are concrete: substitute ids into the template search returned (`/v1/payment_intents/pi_123`). Pass `query` and `body` as JSON objects named as `stripe_api_details` lists them. Connecta form-encodes v1 bodies and query lists (`expand: [\"latest_charge\"]`), sends v2 bodies as JSON, and pins `Stripe-Version` to the index's API version. Send `null` or `\"\"` to unset a field.\n- Every call is checked against the pinned index before it is sent. An unknown path returns the nearest operations; an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Stripe, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `starting_after`, or `page` for search and v2 lists. Lists belong inside `execute_code`; pass `select` (dot paths such as `data.id` and `data.status`) to keep only the fields the question needs, which also keeps customer names, emails, and addresses out of the transcript.\n- `get_stripe_account_info` confirms the account, mode, and connected account; `get_balance_summary` returns available and pending balances. Use them when they answer the question.\n- Find records with list and search operations through `stripe_api_read` (`GET /v1/customers/search`, `GET /v1/charges` with `customer`) and carry the returned `id` into the write.\n- `stripe_api_write` sends an `Idempotency-Key` on every POST. Pass `idempotencyKey` for anything you might retry, or reuse the generated one the result returns. An ambiguous failure carries it in its message or as `uncertainCall.recovery.idempotencyKey`. Reuse it only with the exact original arguments and within Stripe's retention (v1 keys may be pruned after 24 hours); after that, look the object up before retrying. A retry with a fresh key is a second charge, not a second attempt.\n- Refused by design: operations that hand out a credential (ephemeral keys, Terminal connection tokens, account links (v1 and v2), Terminal onboarding links, Express login links, account sessions, customer sessions, billing portal sessions, file link creation, Financial Connections session creation, and the meter event session), Stripe Apps secret payloads (`expand` of `payload`), Issuing card `number` and `cvc` expansion on any Issuing path, multipart uploads such as `POST /v1/files` (use the Dashboard), and the meter event stream (send single events with `POST /v2/billing/meter_events`). Each refusal names its reason. Quote PDFs arrive from `files.stripe.com` as base64.\n- Secrets read as `\"[redacted]\"` in every result, expansions included: client secrets (PaymentIntents, SetupIntents, Checkout and Identity sessions, Sources, Financial Connections, invoice confirmation secrets), webhook and event destination signing secrets, Apps secret payloads, app-install authorization codes, Terminal Wi-Fi passwords, forwarded request header values, and Issuing card numbers and CVCs. Webhook and event destination URLs, file link URLs (in an expanded File, a list, or an event too), and pre-signed import URLs keep only their scheme and host. Identity verification `url`s come back whole. A Checkout Session's `url` comes back whole only for a guest payment session (`mode: \"payment\"`, no `customer` or `customer_account`, no customer creation, no saved payment method options); any other session returns only its scheme and host, because its page can show, reuse, or remove a customer's saved payment methods. Create those from your own server code, or send a payment link or the invoice's hosted page instead. These rules follow each object wherever it appears, expansions and events included. A `[redacted]` field is never the real value; do not send it back.\n- Failures of credential-bearing operations (webhook endpoints, Apps secrets, Terminal configurations, forwarding) carry Stripe's type, code, decline code, and param, not its message. Payment, billing, and account failures keep Stripe's message.\n- The hosted OAuth connection's natural-language Sigma, metrics, documentation search, implementation planner, and feedback tools do not exist with a key.\n",
6
+ "oauth": "\n\nThis OAuth session may expose both live and sandbox Stripe accounts. Discover the live account-selection tools, select the intended account and mode, then carry exact context fields such as `stripe_context` and `livemode` wherever the schema requires them. A live-mode write moves real money; a sandbox write changes test data. Never infer the account or mode from connector metadata.\n\n- Discover account and mode with the live account-selection tools before every account-scoped read or write. The published inventory has changed account-tool names; do not assume `list_available_accounts_or_orgs` is available. Select the intended result and carry the exact context fields required by the live schema unchanged. If the account, mode, or supported selector is ambiguous, stop and ask; never guess.\n- Organization accounts are not Stripe Connect connected accounts. A Connect call requires a separate API-key connector whose `connectedAccount` sends Stripe's documented `Stripe-Account` header; OAuth does not support that path.\n- Four generic tools reach any Stripe API method. Find the method with `stripe_api_search`, read its parameters with `stripe_api_details`, then call `stripe_api_read` (GET) or `stripe_api_write` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — `stripe_api_details` is cheaper than a rejected write.\n- Prefer a dedicated tool when one covers the task: `get_balance_summary` for balances and `stripe_analytics` for Sigma or Metrics reporting. Use `stripe_api_search` for everything else instead of assuming a retired dedicated tool still exists.\n- `stripe_implementation_planner` and the query-execution intents of `stripe_analytics` create provider-side planning or query-run state. Connecta therefore classifies both as writes, which a read-only endpoint refuses; their retrieval paths stay behind the same tool boundary.\n- `stripe_api_write` carries the blast radius of the entire write API — every POST, PATCH, PUT, and DELETE, from a customer edit to a subscription cancellation. State the method and path explicitly; a read-only endpoint refuses it.\n- Any `stripe_api_read` list or `stripe_api_search` that returns full objects belongs inside `execute_code`, projected to the fields the question needs before `return`. Neither `limit` nor `expand` substitutes for that: an unprojected list of customers or invoices truncates long before it answers, and a projected one keeps the customer's name, email, and address out of the transcript.\n- Find an object with `stripe_api_search` (or a list endpoint through `stripe_api_read`) and carry the `id` it returned into the write.\n- This connection's tool list is not a fixed set. Stripe gates parts of its MCP catalog by account, integration, and beta enrollment, so search this connector for what it actually exposes rather than assuming a documented tool is here.\n- Send an `Idempotency-Key` on every write you might retry, if the tool accepts it, and reuse the same key for the retry. A retry with a fresh key is a second charge, not a second attempt.\n",
7
+ "key": "\n\n- This is Connecta's REST connector to `api.stripe.com`, holding one operator-managed key. It reaches exactly one account in one mode, so there is no account selection and no `stripe_context` or `livemode` argument.\n- `stripe_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `stripe_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `stripe_api_read` calls GET operations, plus `POST /v1/invoices/create_preview`, which only previews. `stripe_api_write` calls every other POST and every DELETE, from a customer edit to a subscription cancellation, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete: substitute ids into the template search returned (`/v1/payment_intents/pi_123`). Pass `query` and `body` as JSON objects named as `stripe_api_details` lists them. Connecta form-encodes v1 bodies and query lists (`expand: [\"latest_charge\"]`), sends v2 bodies as JSON, and pins `Stripe-Version` to the index's API version. Send `null` or `\"\"` to unset a field.\n- Every call is checked against the pinned index before it is sent. An unknown path returns the nearest operations; an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Stripe, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `starting_after`, or `page` for search and v2 lists. Lists belong inside `execute_code`; pass `select` (dot paths such as `data.id` and `data.status`) to keep only the fields the question needs, which also keeps customer names, emails, and addresses out of the transcript.\n- `get_stripe_account_info` confirms the account, mode, and connected account; `get_balance_summary` returns available and pending balances. Use them when they answer the question.\n- Find records with list and search operations through `stripe_api_read` (`GET /v1/customers/search`, `GET /v1/charges` with `customer`) and carry the returned `id` into the write.\n- `stripe_api_write` sends an `Idempotency-Key` on every POST. Pass `idempotencyKey` for anything you might retry, or reuse the generated one the result returns. An ambiguous failure carries it in its message or as `uncertainCall.recovery.idempotencyKey`. Reuse it only with the exact original arguments and within Stripe's retention (v1 keys may be pruned after 24 hours); after that, look the object up before retrying. A retry with a fresh key is a second charge, not a second attempt.\n- Refused by design: operations that hand out a credential (ephemeral keys, Terminal connection tokens, account links (v1 and v2), Terminal onboarding links, Express login links, account sessions, customer sessions, billing portal sessions, file link creation, Financial Connections session creation, and the meter event session), Stripe Apps secret payloads (`expand` of `payload`), Issuing card `number` and `cvc` expansion on any Issuing path, multipart uploads such as `POST /v1/files` (use the Dashboard), and the meter event stream (send single events with `POST /v2/billing/meter_events`). Each refusal names its reason. Quote PDFs arrive from `files.stripe.com` as base64.\n- Secrets read as `\"[redacted]\"` in every result, expansions included: client secrets (PaymentIntents, SetupIntents, Checkout and Identity sessions, Sources, Financial Connections, invoice confirmation secrets), webhook and event destination signing secrets, Apps secret payloads, app-install authorization codes, Terminal Wi-Fi passwords, forwarded request header values, and Issuing card numbers and CVCs. Webhook and event destination URLs, file link URLs (in an expanded File, a list, or an event too), and pre-signed import URLs keep only their scheme and host. Identity verification `url`s come back whole. A Checkout Session's `url` comes back whole only for a guest payment session (`mode: \"payment\"`, no `customer` or `customer_account`, no customer creation, no saved payment method options); any other session returns only its scheme and host, because its page can show, reuse, or remove a customer's saved payment methods. Create those from your own server code, or send a payment link or the invoice's hosted page instead. These rules follow each object wherever it appears, expansions and events included. A `[redacted]` field is never the real value; do not send it back.\n- Failures of credential-bearing operations (webhook endpoints, Apps secrets, Terminal configurations, forwarding) carry Stripe's type, code, decline code, and param, not its message. Payment, billing, and account failures keep Stripe's message.\n- The hosted OAuth connection's natural-language Sigma, metrics, documentation search, implementation planner, and feedback tools do not exist with a key.\n- Embedded URL safety uses reviewed paths plus global signing parameters. Generic credential-named query parameters (`code`, `key`, `token`, `sig`, etc.) and credential-looking fragments withhold the destination's entire query/fragment only in charge `failure_message` and `outcome.seller_message` wherever charge objects appear, including event partial copies and vendor error text. Ordinary `code`/`key` links can therefore be false positives in these diagnostics. Descriptions, metadata, price nicknames, invoice footers and notes keep those ordinary URL parameters byte for byte. Literal AWS SigV4/Google V4 `X-Amz-*`/`X-Goog-*` Signature, Credential and Security-Token values are withheld in every decoded string before projection. A withheld value is not the original: do not write it back. All writes, including named writes and supported raw/base64/multipart uploads, reject URL destinations whose own query/fragment contains the exact `[redacted]` placeholder, plain or encoded, with `invalid_args` before dispatch. Admission iteratively checks every JSON key and value within the host's accepted request size, without separate depth or node-count ceilings; cyclic direct arguments are refused. A marker outside the destination remains ordinary text. Split strings, encoded host/signing names, bare hosts without `//`, and opaque bearer paths remain outside this scan.\n",
8
8
  "shared": "- Lists are cursor-paginated: `limit` defaults to 10 and caps at 100, `starting_after` and `ending_before` take an object id and are mutually exclusive, and `has_more` says whether to continue. Page inside `execute_code`.\n- Search filters on a documented per-resource field set, not on arbitrary attributes. Charges search takes `amount`, `created`, `currency`, `customer`, `status`, `refunded`, `disputed`, `metadata`, `billing_details.address.postal_code`, and `payment_method_details.<source>.*` card fields — there is no `payment_intent` field. When the field you want is not searchable, retrieve the parent object and follow its reference (the PaymentIntent's `latest_charge`) instead of retrying the search with another spelling.\n- The account → `stripe_api_search` → `stripe_api_details` → `stripe_api_read` sequence is one program, not four turns: resolve the method once, then call that method as many times as the investigation needs in the same run.\n- Decline outcomes live on the charge — `outcome`, `failure_code`, `failure_message` — reached from the PaymentIntent's `latest_charge`, so \"why did this payment fail\" is a PaymentIntent read followed by one charge read.\n- Resolve ids before acting; never guess one. Stripe ids are typed prefixes — `cus_` customer, `sub_` subscription, `ch_` charge, `pi_` payment intent, `in_` invoice, `acct_` account — and a plausible-looking id belongs to a different object or to nobody.\n- Amounts are integers in the currency's minor unit: `1099` is 10.99 USD, and zero-decimal currencies like JPY take `10` for 10 JPY. Never send a decimal.\n- Treat every create, update, delete, refund, and report run as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
9
9
  "oauth_limits": "- Stripe answers a rate limit with `429` and a `Stripe-Rate-Limited-Reason` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of 100 requests per second in live mode and 25 in sandbox mode, and any single endpoint is capped at 25 per second regardless of mode, so paging one list is the real constraint.\n- Use `search_stripe_documentation` when the shape of an object or a flow is unclear; it is a read and costs nothing but a call.\n- An `auth_required` failure means this connector's Stripe authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. A rejected argument or a plan restriction comes back in Stripe's own words instead — read it rather than re-authorizing.\n",
10
10
  "key_limits": "- Stripe answers a rate limit with `429` and a `Stripe-Rate-Limited-Reason` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of ",
@@ -48,6 +48,21 @@
48
48
  // - Issuing card numbers and CVCs come back only as expansions, which the
49
49
  // connector refuses on every Issuing path (`rest.ts`); the card operations
50
50
  // also redact both fields.
51
+ // Embedded URL verdict (#812): reviewed paths + global signing parameters.
52
+ // Evidence: digest-verified Stripe v2555, components/schemas/charge's
53
+ // failure_message ("Message to user further explaining reason for charge
54
+ // failure") and charge_outcome.seller_message (Stripe's human-readable
55
+ // outcome/reason). Scan those paths on charge objects, including expansions,
56
+ // events and previous_attributes. All vendor error text is scanned too.
57
+ // Product/customer/charge/invoice descriptions, metadata, price nicknames,
58
+ // invoice footers and notes are customer data: generic code/key/token names
59
+ // there retain their bytes, even in whole URL strings. The required literal
60
+ // X-Amz-/X-Goog- Signature/Credential/Security-Token defense applies globally.
61
+ // file.url/file_link.url and catalog-import upload/download capabilities are
62
+ // structured and retain existing resource/operation rules. The v2555 review
63
+ // found no documented vendor-minted link embedded in other free text; no
64
+ // Stripe-wide file-host/bucket policy is justified. Errors and diagnostic
65
+ // paths can have false positives for ordinary code/key links.
51
66
  import { redact, refuse, safe, vendorErrors, } from "../_shared/rest/value-safety.js";
52
67
  const EPHEMERAL_KEY = "It mints an ephemeral API key whose `secret` makes authorized Stripe API requests for a customer or Issuing card.";
53
68
  const CONNECTION_TOKEN = "It mints a Terminal connection token whose `secret` connects a reader SDK to this account.";
@@ -698,6 +713,13 @@ const RESOURCES = {
698
713
  },
699
714
  };
700
715
  export const STRIPE_VALUE_SAFETY = {
716
+ embeddedUrls: {
717
+ resources: {
718
+ key: "object",
719
+ partials: ["previous_attributes"],
720
+ paths: { charge: ["failure_message", "outcome.seller_message"] },
721
+ },
722
+ },
701
723
  title: "Stripe",
702
724
  operations: OPERATIONS,
703
725
  fields: FIELDS,
@@ -169,11 +169,11 @@ const VERCEL_MCP_CLASSIFICATION = {
169
169
  },
170
170
  "filter_project_envs": {
171
171
  "verdict": "write",
172
- "reason": "Lists project environment variables and can return decrypted values; reviewed as a write so the pool trust policy gates the disclosure (#801).",
172
+ "reason": "Lists project environment variables and can return decrypted values; reviewed as a write so a read-only endpoint refuses the disclosure (#801).",
173
173
  },
174
174
  "get_project_env": {
175
175
  "verdict": "write",
176
- "reason": "Returns one environment variable's decrypted value; reviewed as a write so the pool trust policy gates the disclosure (#801).",
176
+ "reason": "Returns one environment variable's decrypted value; reviewed as a write so a read-only endpoint refuses the disclosure (#801).",
177
177
  },
178
178
  "create_project_env": {
179
179
  "verdict": "destructive",
@@ -3,7 +3,7 @@ import { ConnectorCallError } from "../../errors.js";
3
3
  import { OperationIndex } from "../_shared/rest/operation-index.js";
4
4
  import { callRest, restCall, unknownOutcome, restTransport, } from "../_shared/rest/tools.js";
5
5
  import { openapi } from "./openapi.generated.js";
6
- import { valueSafety } from "../_shared/rest/value-safety.js";
6
+ import { valueSafety, withholdEmbeddedUrlsIn, refuseWithheldLinks, refuseWithheldUpload, refuseWithheldBase64, } from "../_shared/rest/value-safety.js";
7
7
  import { VERCEL_VALUE_SAFETY } from "./value-safety.js";
8
8
  /** Vercel's public REST origin. Override only for a proxy or test double. */
9
9
  export const VERCEL_API_BASE_URL = "https://api.vercel.com";
@@ -89,7 +89,7 @@ function reviewedCode(value) {
89
89
  * value: it says the status, a reviewed code, and what to do.
90
90
  */
91
91
  function vercelFailure(status, headers, payload, family = false) {
92
- const root = record(payload);
92
+ const root = record(withholdEmbeddedUrlsIn(payload));
93
93
  const error = record(root["error"]);
94
94
  const code = text(error["code"]);
95
95
  const message = text(error["message"])?.trim() ?? text(root["message"])?.trim() ?? `Vercel returned HTTP ${status}.`;
@@ -456,6 +456,22 @@ export function vercelRest(options) {
456
456
  failure: (status, headers, body, call) => vercelFailure(status, headers, body, call ? SAFETY.withholdsErrors(call.op) : false),
457
457
  readPosts: READ_POSTS,
458
458
  valueSafety: SAFETY,
459
+ async admit(call) {
460
+ if (call.method === "GET" ||
461
+ call.method === "HEAD" ||
462
+ READ_POSTS.some(([method, path]) => method === call.method && path === call.op.path))
463
+ return;
464
+ refuseWithheldLinks([call.body, call.query, call.path], "Vercel");
465
+ if (call.method === "POST" && call.op.path === "/v13/deployments") {
466
+ const files = record(call.body)["files"];
467
+ if (Array.isArray(files)) {
468
+ for (const file of files) {
469
+ if (record(file)["encoding"] === "base64")
470
+ await refuseWithheldBase64(record(file)["data"], "Vercel");
471
+ }
472
+ }
473
+ }
474
+ },
459
475
  refuse,
460
476
  scope(call) {
461
477
  const query = { ...call.query };
@@ -909,6 +925,8 @@ export function vercelRest(options) {
909
925
  // Decode first, then validate what was supplied: a required binary body
910
926
  // is present whenever bytes are (the index types it as a string).
911
927
  const bytes = decodeUpload(args);
928
+ await vendor.admit?.(call, ctx);
929
+ await refuseWithheldUpload(bytes, "Vercel");
912
930
  operations.check(call.op, call.query, String(args["base64Body"] ?? args["textBody"]));
913
931
  const digest = call.op.path === "/v2/files" ? await sha1(bytes) : undefined;
914
932
  const query = {};
@@ -3,8 +3,8 @@ export const skill = {
3
3
  "content": "- Resolve team, project, and deployment ids with list reads; never guess one. Team ids begin `team_`, project ids `prj_`, and deployment ids `dpl_`.\n- Diagnose a deployment by reading its state first (`readyState`, `errorCode`, `errorMessage`), then its build logs, then runtime logs for a deployment that built. Narrow time windows before raising limits.\n- Environment variable changes apply only to future deployments; a new deployment is a separate write. Promoting an existing deployment to production does not rebuild it.\n- Treat every create, update, delete, promotion, cancellation, and purchase as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
4
4
  "instructionsHeading": "Account instructions",
5
5
  "fragments": {
6
- "oauth": "\n\nTool names, descriptions, argument schemas, and result schemas come from Vercel's live server. Connecta applies release-reviewed classification; reviewed writes stay writes even when Vercel claims they only read.\n\n- Discover the live catalog before assuming a tool exists. Vercel can change the surface independently of a Connecta release, and account features may affect what the authorization can reach.\n- Complete authorization and grant access to the owning team before acting. Listing this server does not prove team access.\n- MCP pagination and arguments follow the live MCP schemas; do not apply REST pagination or argument shapes to them.\n- Diagnose deployments with `get_deployment`, then build logs, runtime error clusters, and runtime logs.\n- `get_project_env` and `filter_project_envs` can return decrypted environment values, so Connecta reviews them as writes and the pool's trust policy gates them. Prefer reading variable keys and targets without values.\n- Purchase tools change billing. Read a quote first and carry its price, idempotency key, and requested term into the confirmed purchase unchanged.\n- `get_access_to_vercel_url` creates a temporary access grant. Treat the URL it returns as a credential and do not expose it outside the requested task.\n- `deploy_to_vercel` and `import-claude-design-from-url` can create or update live projects. Read the target and deployment mode before invoking them.\n- An `auth_required` failure means this connector's OAuth grant is missing or expired. Run `authorize_connector` for this connector id, then retry.\n",
7
- "key": "\n\n- This is Connecta's REST connector to `api.vercel.com`, holding one operator-managed access token. A default team routes calls; it does not narrow what the token can reach.\n- `vercel_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `vercel_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `vercel_api_read` calls GET operations, plus reviewed POSTs that only read: `POST /v2/observability/query`, registrar availability, price, and search, and `POST /v8/artifacts`. `vercel_api_write` calls every other POST, PUT, PATCH, and DELETE, with `method` and `path` stated; execution follows the configured pool trust policy.\n- Paths are concrete and carry the version search returned: `/v13/deployments/dpl_123`, not `/deployments/dpl_123`. Pass `query` and `body` as JSON objects named as `vercel_api_details` lists them. Every call is checked against the pinned index before it is sent: an unknown path returns the nearest operations, and an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Vercel, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `until` (a millisecond timestamp from `pagination.next`) on most lists, `cursor` on newer ones. Lists belong inside `execute_code`; pass `select` (dot paths such as `deployments.uid` and `deployments.state`) to keep only the fields the question needs.\n- `list_teams` returns team ids and slugs and the connection's default team. `get_deployment_build_logs` reads a deployment's build events without following live output. `get_runtime_logs` collects the runtime log stream for at most `waitMs` or `maxRows`, then stops and says why. Generic reads refuse `follow` and the runtime-log stream, which never end on their own.\n- `vercel_api_upload` sends explicit text or base64 bytes to an octet-stream operation, such as `POST /v2/files` before a deployment (Connecta computes `x-vercel-digest`). `vercel_api_write` sends JSON only.\n- Environment values never come back. `list_project_env_vars` never decrypts. `upsert_project_env_var` (`upsert: false` for create-only), `update_project_env_var`, and `delete_project_env_var` return metadata without values; values are write input only. Generic reads refuse `GET /v1/projects/{idOrName}/env/{id}`, `GET /v10/projects/{idOrName}/env`, `GET /v1/env/{id}`, and any `decrypt`, and Connecta replaces the value of every environment variable in any result with `\"[redacted]\"`, including project reads, shared variables, and audit events.\n- Other secrets read as `[redacted]` in every result: credential-named fields and their whole contents (`token`, `secret`, `password`, `jwt`, `credentials`, `Authorization`, …), protection-bypass secrets (each bypass's metadata stays under a numbered placeholder), deploy hook URLs, drain and webhook headers and signing secrets (including the one a new webhook or drain returns), drain and webhook destination URLs and external route, rewrite, and redirect destinations beyond their scheme and host, the args and values of every header, cookie, and query rule (route transforms and conditions, firewall conditions), team invite codes, synced Global Config items, and credentials inside other URLs. Failures of every reviewed operation, and of any operation whose request takes a credential-named field, say only the status and a reviewed code. Read or rotate withheld secrets in the Vercel dashboard. A `[redacted]` field is never the real value; do not send it back.\n- Refused by design: endpoints that mint credentials (access tokens, AI Gateway API keys, project OIDC and trace tokens, Connect and SSO tokens, Edge Config tokens, KMS-signed tokens, Flags SDK keys, protection-bypass secrets, installation credential rotation), Connect authorization requests, project transfer requests (the code lets another team claim the project), KMS signing, a domain move-out (its answer is a transfer token), Global Config items, backups, and tokens, and domain transfer auth codes. Each refusal names its reason.\n- The hosted OAuth connection's documentation search, runtime error clusters, toolbar threads, agent runs, web analytics summaries, purchase quotes, `deploy_to_vercel`, and access grants for protected URLs do not exist with a token. Configure an OAuth connector for those.\n- A `429` carries Vercel's reset time; Vercel meters endpoints separately, so wait before retrying the same operation. An `auth_required` failure means the token is missing, expired, or outside this team: an operator must replace it in this connection in the operator UI, and `authorize_connector` returns that handoff. A rejected argument comes back in Vercel's own words; read it rather than replacing the token.\n",
6
+ "oauth": "\n\nTool names, descriptions, argument schemas, and result schemas come from Vercel's live server. Connecta applies release-reviewed classification; reviewed writes stay writes even when Vercel claims they only read.\n\n- Discover the live catalog before assuming a tool exists. Vercel can change the surface independently of a Connecta release, and account features may affect what the authorization can reach.\n- Complete authorization and grant access to the owning team before acting. Listing this server does not prove team access.\n- MCP pagination and arguments follow the live MCP schemas; do not apply REST pagination or argument shapes to them.\n- Diagnose deployments with `get_deployment`, then build logs, runtime error clusters, and runtime logs.\n- `get_project_env` and `filter_project_envs` can return decrypted environment values, so Connecta reviews them as writes, and a read-only endpoint refuses them. Prefer reading variable keys and targets without values.\n- Purchase tools change billing. Read a quote first and carry its price, idempotency key, and requested term into the confirmed purchase unchanged.\n- `get_access_to_vercel_url` creates a temporary access grant. Treat the URL it returns as a credential and do not expose it outside the requested task.\n- `deploy_to_vercel` and `import-claude-design-from-url` can create or update live projects. Read the target and deployment mode before invoking them.\n- An `auth_required` failure means this connector's OAuth grant is missing or expired. Run `authorize_connector` for this connector id, then retry.\n",
7
+ "key": "\n\n- This is Connecta's REST connector to `api.vercel.com`, holding one operator-managed access token. A default team routes calls; it does not narrow what the token can reach.\n- `vercel_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `vercel_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `vercel_api_read` calls GET operations, plus reviewed POSTs that only read: `POST /v2/observability/query`, registrar availability, price, and search, and `POST /v8/artifacts`. `vercel_api_write` calls every other POST, PUT, PATCH, and DELETE, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete and carry the version search returned: `/v13/deployments/dpl_123`, not `/deployments/dpl_123`. Pass `query` and `body` as JSON objects named as `vercel_api_details` lists them. Every call is checked against the pinned index before it is sent: an unknown path returns the nearest operations, and an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Vercel, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `until` (a millisecond timestamp from `pagination.next`) on most lists, `cursor` on newer ones. Lists belong inside `execute_code`; pass `select` (dot paths such as `deployments.uid` and `deployments.state`) to keep only the fields the question needs.\n- `list_teams` returns team ids and slugs and the connection's default team. `get_deployment_build_logs` reads a deployment's build events without following live output. `get_runtime_logs` collects the runtime log stream for at most `waitMs` or `maxRows`, then stops and says why. Generic reads refuse `follow` and the runtime-log stream, which never end on their own.\n- `vercel_api_upload` sends explicit text or base64 bytes to an octet-stream operation, such as `POST /v2/files` before a deployment (Connecta computes `x-vercel-digest`). `vercel_api_write` sends JSON only.\n- Environment values never come back. `list_project_env_vars` never decrypts. `upsert_project_env_var` (`upsert: false` for create-only), `update_project_env_var`, and `delete_project_env_var` return metadata without values; values are write input only. Generic reads refuse `GET /v1/projects/{idOrName}/env/{id}`, `GET /v10/projects/{idOrName}/env`, `GET /v1/env/{id}`, and any `decrypt`, and Connecta replaces the value of every environment variable in any result with `\"[redacted]\"`, including project reads, shared variables, and audit events.\n- Other secrets read as `[redacted]` in every result: credential-named fields and their whole contents (`token`, `secret`, `password`, `jwt`, `credentials`, `Authorization`, …), protection-bypass secrets (each bypass's metadata stays under a numbered placeholder), deploy hook URLs, drain and webhook headers and signing secrets (including the one a new webhook or drain returns), drain and webhook destination URLs and external route, rewrite, and redirect destinations beyond their scheme and host, the args and values of every header, cookie, and query rule (route transforms and conditions, firewall conditions), team invite codes, synced Global Config items, and credentials inside other URLs. Failures of every reviewed operation, and of any operation whose request takes a credential-named field, say only the status and a reviewed code. Read or rotate withheld secrets in the Vercel dashboard. A `[redacted]` field is never the real value; do not send it back.\n- Refused by design: endpoints that mint credentials (access tokens, AI Gateway API keys, project OIDC and trace tokens, Connect and SSO tokens, Edge Config tokens, KMS-signed tokens, Flags SDK keys, protection-bypass secrets, installation credential rotation), Connect authorization requests, project transfer requests (the code lets another team claim the project), KMS signing, a domain move-out (its answer is a transfer token), Global Config items, backups, and tokens, and domain transfer auth codes. Each refusal names its reason.\n- The hosted OAuth connection's documentation search, runtime error clusters, toolbar threads, agent runs, web analytics summaries, purchase quotes, `deploy_to_vercel`, and access grants for protected URLs do not exist with a token. Configure an OAuth connector for those.\n- A `429` carries Vercel's reset time; Vercel meters endpoints separately, so wait before retrying the same operation. An `auth_required` failure means the token is missing, expired, or outside this team: an operator must replace it in this connection in the operator UI, and `authorize_connector` returns that handoff. A rejected argument comes back in Vercel's own words; read it rather than replacing the token.\n- Embedded URL safety uses reviewed paths plus global signing parameters. Generic credential-named query parameters (`code`, `key`, `token`, `sig`, etc.) and credential-looking fragments withhold the destination's entire query/fragment only in deployment `errorMessage`, build-event `payload.text`/`text`, and runtime-log `message` on the reviewed deployment/log operations and vendor error text. Ordinary `code`/`key` links can therefore be false positives in these diagnostics. Deployment metadata and other application content keep those ordinary URL parameters byte for byte. Literal AWS SigV4/Google V4 `X-Amz-*`/`X-Goog-*` Signature, Credential and Security-Token values are withheld in every decoded string before projection. A withheld value is not the original: do not write it back. Deployment creation also inspects decoded `files[].data` when `encoding` is `base64` (one decode bounded by the field's encoded length and the host's request limit; malformed base64 is refused). All writes, including named writes and supported raw/base64/multipart uploads, reject URL destinations whose own query/fragment contains the exact `[redacted]` placeholder, plain or encoded, with `invalid_args` before dispatch. Admission iteratively checks every JSON key and value within the host's accepted request size, without separate depth or node-count ceilings; cyclic direct arguments are refused. A marker outside the destination remains ordinary text. Split strings, encoded host/signing names, bare hosts without `//`, and opaque bearer paths remain outside this scan.\n",
8
8
  "shared": "- Resolve team, project, and deployment ids with list reads; never guess one. Team ids begin `team_`, project ids `prj_`, and deployment ids `dpl_`.\n- Diagnose a deployment by reading its state first (`readyState`, `errorCode`, `errorMessage`), then its build logs, then runtime logs for a deployment that built. Narrow time windows before raising limits.\n- Environment variable changes apply only to future deployments; a new deployment is a separate write. Promoting an existing deployment to production does not rebuild it.\n- Treat every create, update, delete, promotion, cancellation, and purchase as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n"
9
9
  }
10
10
  };
@@ -23,6 +23,19 @@
23
23
  //
24
24
  // Residual risk, as with Infisical: a secret a person typed into a free-text
25
25
  // name, description, log line, or identifier is out of scope.
26
+ // Embedded URL verdict (#812): reviewed paths + global signing parameters.
27
+ // Evidence: pinned index/candidates sha256:32399b3ed273 identify deployment
28
+ // reads/creation/listing, build events and runtime logs; the operation/path
29
+ // list below scans errorMessage, deployments.errorMessage, payload.text/text
30
+ // and message respectively, both lists and individual named-stream rows.
31
+ // All vendor error text is scanned too. These diagnostics can have false
32
+ // positives for ordinary code/key links. Deployment meta, descriptions and
33
+ // other application content retain generic query parameters byte for byte.
34
+ // Literal X-Amz-/X-Goog- Signature/Credential/Security-Token values are
35
+ // withheld in every string. Existing structured-capability rules still apply.
36
+ // The original full response document is unavailable at its unversioned URL:
37
+ // current upstream differs and is supplementary evidence only. This is not a
38
+ // claim of complete pinned-response review or of a Vercel-wide host policy.
26
39
  import { redact, refuse as refusal, safe, under, } from "../_shared/rest/value-safety.js";
27
40
  /** A refusal's reason reads as a sentence about the operation: "It mints …". */
28
41
  const refuse = (reason) => refusal(`It ${reason}`);
@@ -287,6 +300,15 @@ const VALUE_SAFETY = {
287
300
  * names, which the heuristic leaves.
288
301
  */
289
302
  export const VERCEL_VALUE_SAFETY = {
303
+ embeddedUrls: {
304
+ operations: {
305
+ "GET /v13/deployments/{idOrUrl}": ["errorMessage"],
306
+ "POST /v13/deployments": ["errorMessage"],
307
+ "GET /v7/deployments": ["deployments.errorMessage"],
308
+ "GET /v3/deployments/{idOrUrl}/events": ["payload.text", "text"],
309
+ "GET /v1/projects/{projectId}/deployments/{deploymentId}/runtime-logs": ["message"],
310
+ },
311
+ },
290
312
  title: "Vercel",
291
313
  operations: VALUE_SAFETY,
292
314
  envBodies: /^\/v\d+\/(?:env(?:\/|$)|projects\/\{[^}]+\}\/env(?:\/|$))/,
@@ -0,0 +1,14 @@
1
+ import { type RemoteMcpAuth } from "../../connectors/remote-mcp.js";
2
+ import { type ProviderOptions } from "../../provider.js";
3
+ /** Wispr Flow's hosted Streamable HTTP endpoint, without a trailing slash. */
4
+ export declare const WISPR_FLOW_MCP_ENDPOINT = "https://api.wisprflow.ai/connect/mcp";
5
+ type WisprFlowOAuth = Extract<RemoteMcpAuth, {
6
+ type: "oauth";
7
+ }>;
8
+ export interface WisprFlowOptions extends ProviderOptions {
9
+ /** OAuth only; defaults to browser authorization with the advertised scopes. */
10
+ auth?: WisprFlowOAuth;
11
+ }
12
+ /** A maintained Wispr Flow Notetaker hosted-MCP connection. */
13
+ export declare const wisprFlow: import("../../provider.js").ProviderFactory<WisprFlowOptions>;
14
+ export {};
@@ -0,0 +1,117 @@
1
+ import { remoteMcp } from "../../connectors/remote-mcp.js";
2
+ import { keys, optionsOf, variants } from "../../config-schema.js";
3
+ import { defineProvider, PROVIDER_COMMON } from "../../provider.js";
4
+ import { skill } from "./skill.generated.js";
5
+ /** Wispr Flow's hosted Streamable HTTP endpoint, without a trailing slash. */
6
+ export const WISPR_FLOW_MCP_ENDPOINT = "https://api.wisprflow.ai/connect/mcp";
7
+ /**
8
+ * Reviewed public-source superset, 2026-10-13. Scalekit documents 14 reads;
9
+ * Claude's connector directory adds four task tools. Wispr's own help page
10
+ * describes read-only access, so the task writes remain classified even if
11
+ * this account never exposes them. No authenticated catalog or schemas were
12
+ * captured. Vendor names, schemas and results remain live.
13
+ */
14
+ const CLASSIFICATION = {
15
+ tools: {
16
+ get_account_info: { verdict: "read", reason: "Returns the signed-in identity and calendar display-name aliases." },
17
+ get_calendar_event: { verdict: "read", reason: "Retrieves an existing calendar event and its attendees." },
18
+ get_meeting: {
19
+ verdict: "read",
20
+ reason: "Retrieves recorded notes, the summary, attendees and requested transcript ranges.",
21
+ },
22
+ get_meeting_by_calendar_id: {
23
+ verdict: "read",
24
+ reason: "Finds the existing recording linked to a calendar event and reads its content.",
25
+ },
26
+ get_meeting_participants_enriched: {
27
+ verdict: "read",
28
+ reason: "Reads participant information and identifies the signed-in attendee.",
29
+ },
30
+ get_scratchpad_note: { verdict: "read", reason: "Returns a bounded range of an existing Scratchpad note." },
31
+ get_upcoming_meeting: {
32
+ verdict: "read",
33
+ reason: "Reads an upcoming event and an available pre-read without generating one.",
34
+ },
35
+ list_meeting_series: {
36
+ verdict: "read",
37
+ reason: "Lists recorded occurrences of an existing recurring meeting series.",
38
+ },
39
+ list_upcoming_meetings: {
40
+ verdict: "read",
41
+ reason: "Lists upcoming calendar events and already-available pre-reads.",
42
+ },
43
+ resolve_calendar_link: {
44
+ verdict: "read",
45
+ reason: "Matches a pasted calendar or conference URL to an existing event.",
46
+ },
47
+ resolve_share_link: {
48
+ verdict: "read",
49
+ reason: "Reads an accessible shared note without creating a link or changing sharing permissions.",
50
+ },
51
+ search_calendar_events: { verdict: "read", reason: "Finds existing calendar events by date, attendees or text." },
52
+ search_meetings: {
53
+ verdict: "read",
54
+ reason: "Finds recorded meetings by title, summary, notes, dates or linked attendees.",
55
+ },
56
+ search_scratchpad_notes: { verdict: "read", reason: "Finds existing Scratchpad notes by text." },
57
+ get_task: { verdict: "read", reason: "Retrieves an existing task without changing its state." },
58
+ search_tasks: { verdict: "read", reason: "Finds existing tasks without changing them." },
59
+ add_task: {
60
+ verdict: "write",
61
+ reason: "Creates a new task; the reviewed operation appends rather than replacing an existing task.",
62
+ },
63
+ update_task: { verdict: "destructive", reason: "Changes the state or fields of an existing task." },
64
+ },
65
+ };
66
+ const OPTIONS = optionsOf()({
67
+ ...PROVIDER_COMMON,
68
+ auth: variants("type", {
69
+ oauth: optionsOf()({
70
+ ...keys("type", "clientMetadataUrl", "scope"),
71
+ client: optionsOf()(keys("issuer", "clientId", "clientSecret", "tokenEndpointAuthMethod")),
72
+ }).shape,
73
+ }),
74
+ });
75
+ function accountSummary(purpose) {
76
+ const summary = `Account-wide Notetaker access: ${purpose}`;
77
+ const bytes = new TextEncoder().encode(summary);
78
+ if (bytes.byteLength <= 120)
79
+ return summary;
80
+ // Streaming decode drops an incomplete final code point; reserve three bytes for the ellipsis.
81
+ return `${new TextDecoder().decode(bytes.subarray(0, 117), { stream: true }).trimEnd()}…`;
82
+ }
83
+ /** A maintained Wispr Flow Notetaker hosted-MCP connection. */
84
+ export const wisprFlow = defineProvider({
85
+ name: "wispr-flow",
86
+ title: "Wispr Flow",
87
+ kind: "mcp",
88
+ readme: "Wispr Flow",
89
+ skill,
90
+ classify: CLASSIFICATION,
91
+ options: OPTIONS,
92
+ bundle: {
93
+ baselineGzip: 169009,
94
+ maxGzip: 229009,
95
+ note: "./providers/wispr-flow initial measured baseline is 169,009 B gzip for the hosted-MCP wrapper; the cap retains the existing baseline + 60,000 B provider headroom policy.",
96
+ },
97
+ create(id, options, provider) {
98
+ const ownership = options.authScope === "personal"
99
+ ? "Each person authorizes separately and may reach a different account."
100
+ : "One shared authorization selects the account for everyone using this connector.";
101
+ return remoteMcp(id, {
102
+ url: WISPR_FLOW_MCP_ENDPOINT,
103
+ ...provider.connectorOptions,
104
+ title: options.title ?? "Wispr Flow",
105
+ description: `Wispr Flow Notetaker meetings, Scratchpad notes, calendar events and tasks: ${options.purpose}`,
106
+ auth: { type: "oauth", scope: "openid offline_access", ...options.auth },
107
+ requireHttps: true,
108
+ classify: provider.classify,
109
+ usageGuide: provider.usageGuide({
110
+ context: [
111
+ `Account-wide Notetaker access: ${options.purpose}. Personal and work accounts hold separate content. ${ownership} Confirm the identity with get_account_info. Dictation history and Notetaker chat history are unavailable.`,
112
+ ],
113
+ summary: accountSummary(options.purpose),
114
+ }),
115
+ });
116
+ },
117
+ });
@@ -0,0 +1,8 @@
1
+ // Generated by scripts/generate-providers.mjs; do not edit.
2
+ export const skill = {
3
+ "content": "- This connection reaches Notetaker meetings, summaries, notes, transcripts, participants and their email addresses, and tasks; Scratchpad notes; calendar events and linked recordings; upcoming meetings with available pre-reads; recurring series; accessible shared notes; and account identity. Meeting results name folders you own or that are shared with you; meetings outside folders list none. Another account needs another authorization. Use `get_account_info` to check which identity this grant reaches.\n- OAuth browser authorization is the only supported auth method. There is no API key. Prefer Google, Apple, Microsoft or enterprise SSO and finish sign-in within about five minutes. Email-and-password sign-in may not complete authorization; Wispr's help pages disagree. Consent covers the account's existing content, without per-meeting consent.\n- Search first, then retrieve by the returned id: `search_meetings` → `get_meeting`, `search_scratchpad_notes` → `get_scratchpad_note`, or `search_calendar_events` → `get_calendar_event`. Resolve pasted URLs with `resolve_calendar_link` or `resolve_share_link` instead of guessing ids. Use `get_meeting_by_calendar_id` for the recording linked to an event; an event without a recording is not missing meeting content. `get_upcoming_meeting` is for future events. For `list_meeting_series`, \"no earlier occurrence\" can mean a one-off meeting or a recording without a linked calendar event, not a failure.\n- `search_meetings` matches titles, summaries and notes, not transcripts. Set `field` to `title`, `content` or `both`; use `query`, `since` and `until` to narrow the request. `attendee_emails` with `attendee_match` set to `any` or `all` only finds recordings linked to calendar events. A missing attendee-filter match does not prove a person never attended. Results include an excerpt and up to five attendees; retrieve the meeting for full content and attendees.\n- List and search tools page with `cursor` and `limit`, normally 25 items and at most 200. Pass the returned `next_cursor` verbatim while `has_more` is true. `list_upcoming_meetings` allows at most 50 items and `window_hours` defaults to 24 with a maximum of 168. A `truncated` result needs a narrower query, even after paging. Reduce results inside `execute_code` before returning them.\n- Notes and transcripts are character-bounded. Read the continuation marker and repeat with `view_content.start_char` or `view_transcript.start_char` set to its exact offset. Request `view_transcript: {}` when source detail matters, then continue through the required ranges. Enterprise note and transcript reads are recorded in audit logs; do not read transcripts speculatively. Summaries are complete. Transcript retention deletes transcripts and audio, not summaries; an unavailable transcript may have expired.\n- Shared-note links require your own meeting-access permissions. If a link copied from email is rejected, ask the user to open it in a browser and pass the final Wispr Flow URL. Transcripts require ownership or an explicit email invitation plus the owner's organization allowing transcript sharing. Domain, team or anyone-with-link access yields summaries only. Reauthorization cannot widen those permissions. Return the provided `share_link` when asked for a link; never fabricate one or assume the link changes who can read it.\n- Timestamps are UTC. Present them in the user's time zone. For a whole local day, supply `since` at local midnight with its UTC offset and `until` at the next local midnight; the upper bound is exclusive. Upcoming pre-reads may be absent because the desktop app generates them on demand; these reads do not generate them.\n- Both official help articles describe read-only access and list tasks or action items as readable, while Wispr's own Claude directory listing advertises \"Create, update, and search action items\" and names `add_task` and `update_task`. Discover this account's actual tools before using them. If exposed, `get_task` and `search_tasks` read, `add_task` creates a new task and is a write, and `update_task` changes existing task state and is destructive. Unknown tools without an explicit, uncontradicted read annotation fail closed.\n- Mac and Windows accounts need Notetaker and MCP access. Personal or organization HIPAA BAA coverage removes all MCP tools. Enterprise administrators can disable MCP or require an approved IP network; Scratchpad and cloud-access policies may further restrict results. An empty catalog or policy denial requires checking account access, not repeated authorization.\n- For \"Too many requests\" or repeated searches that temporarily fail without explanation, wait about one minute before retrying and avoid speculative fan-out. There is no maintained per-runtime admission default; the operator may configure `callAdmission` for this deployment.\n- An `auth_required` failure means this connector's Wispr Flow authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. For guest calls, classification and routing, fetch `skills({ name: \"usage\" })`.\n",
4
+ "instructionsHeading": "Account instructions",
5
+ "fragments": {
6
+ "content": "- This connection reaches Notetaker meetings, summaries, notes, transcripts, participants and their email addresses, and tasks; Scratchpad notes; calendar events and linked recordings; upcoming meetings with available pre-reads; recurring series; accessible shared notes; and account identity. Meeting results name folders you own or that are shared with you; meetings outside folders list none. Another account needs another authorization. Use `get_account_info` to check which identity this grant reaches.\n- OAuth browser authorization is the only supported auth method. There is no API key. Prefer Google, Apple, Microsoft or enterprise SSO and finish sign-in within about five minutes. Email-and-password sign-in may not complete authorization; Wispr's help pages disagree. Consent covers the account's existing content, without per-meeting consent.\n- Search first, then retrieve by the returned id: `search_meetings` → `get_meeting`, `search_scratchpad_notes` → `get_scratchpad_note`, or `search_calendar_events` → `get_calendar_event`. Resolve pasted URLs with `resolve_calendar_link` or `resolve_share_link` instead of guessing ids. Use `get_meeting_by_calendar_id` for the recording linked to an event; an event without a recording is not missing meeting content. `get_upcoming_meeting` is for future events. For `list_meeting_series`, \"no earlier occurrence\" can mean a one-off meeting or a recording without a linked calendar event, not a failure.\n- `search_meetings` matches titles, summaries and notes, not transcripts. Set `field` to `title`, `content` or `both`; use `query`, `since` and `until` to narrow the request. `attendee_emails` with `attendee_match` set to `any` or `all` only finds recordings linked to calendar events. A missing attendee-filter match does not prove a person never attended. Results include an excerpt and up to five attendees; retrieve the meeting for full content and attendees.\n- List and search tools page with `cursor` and `limit`, normally 25 items and at most 200. Pass the returned `next_cursor` verbatim while `has_more` is true. `list_upcoming_meetings` allows at most 50 items and `window_hours` defaults to 24 with a maximum of 168. A `truncated` result needs a narrower query, even after paging. Reduce results inside `execute_code` before returning them.\n- Notes and transcripts are character-bounded. Read the continuation marker and repeat with `view_content.start_char` or `view_transcript.start_char` set to its exact offset. Request `view_transcript: {}` when source detail matters, then continue through the required ranges. Enterprise note and transcript reads are recorded in audit logs; do not read transcripts speculatively. Summaries are complete. Transcript retention deletes transcripts and audio, not summaries; an unavailable transcript may have expired.\n- Shared-note links require your own meeting-access permissions. If a link copied from email is rejected, ask the user to open it in a browser and pass the final Wispr Flow URL. Transcripts require ownership or an explicit email invitation plus the owner's organization allowing transcript sharing. Domain, team or anyone-with-link access yields summaries only. Reauthorization cannot widen those permissions. Return the provided `share_link` when asked for a link; never fabricate one or assume the link changes who can read it.\n- Timestamps are UTC. Present them in the user's time zone. For a whole local day, supply `since` at local midnight with its UTC offset and `until` at the next local midnight; the upper bound is exclusive. Upcoming pre-reads may be absent because the desktop app generates them on demand; these reads do not generate them.\n- Both official help articles describe read-only access and list tasks or action items as readable, while Wispr's own Claude directory listing advertises \"Create, update, and search action items\" and names `add_task` and `update_task`. Discover this account's actual tools before using them. If exposed, `get_task` and `search_tasks` read, `add_task` creates a new task and is a write, and `update_task` changes existing task state and is destructive. Unknown tools without an explicit, uncontradicted read annotation fail closed.\n- Mac and Windows accounts need Notetaker and MCP access. Personal or organization HIPAA BAA coverage removes all MCP tools. Enterprise administrators can disable MCP or require an approved IP network; Scratchpad and cloud-access policies may further restrict results. An empty catalog or policy denial requires checking account access, not repeated authorization.\n- For \"Too many requests\" or repeated searches that temporarily fail without explanation, wait about one minute before retrying and avoid speculative fan-out. There is no maintained per-runtime admission default; the operator may configure `callAdmission` for this deployment.\n- An `auth_required` failure means this connector's Wispr Flow authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. For guest calls, classification and routing, fetch `skills({ name: \"usage\" })`.\n"
7
+ }
8
+ };
@@ -130,14 +130,13 @@ export interface RegistryView {
130
130
  }
131
131
  /**
132
132
  * Connector id → the only tool names this view may see on it. A connector
133
- * absent from the map is visible whole. Derived from `connectorAccess`
134
- * addresses at the auth gate; never from caller input.
133
+ * absent from the map is visible whole. Derived from a pool's `tools`
134
+ * addresses at construction; never from caller input.
135
135
  */
136
136
  export type ToolAccess = ReadonlyMap<string, ReadonlySet<string>>;
137
137
  export interface RegistryScope {
138
138
  connectorIds: "all" | readonly string[];
139
139
  toolAccess?: ToolAccess;
140
- guardedToolAccess?: ToolAccess;
141
140
  subjectKey?: string;
142
141
  principalKey?: string;
143
142
  endpoint?: string;