@zackbart/connecta 0.32.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 (137) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +15 -7
  3. package/bin/connecta.mjs +24 -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/call-admission-context.js +15 -0
  10. package/dist/call-admission.js +30 -43
  11. package/dist/call-recovery.js +6 -2
  12. package/dist/config-value-sources.js +11 -9
  13. package/dist/config.d.ts +37 -75
  14. package/dist/config.js +110 -36
  15. package/dist/connector-access.d.ts +4 -34
  16. package/dist/connector-access.js +15 -99
  17. package/dist/connector-caller.d.ts +7 -1
  18. package/dist/connector-caller.js +3 -2
  19. package/dist/connectors/api-connector.d.ts +27 -4
  20. package/dist/connectors/api-connector.js +2 -0
  21. package/dist/connectors/remote-mcp.js +2 -0
  22. package/dist/describe-config.d.ts +29 -7
  23. package/dist/describe-config.js +7 -17
  24. package/dist/index.d.ts +4 -3
  25. package/dist/index.js +29 -10
  26. package/dist/invocation.js +5 -3
  27. package/dist/membership.d.ts +51 -0
  28. package/dist/membership.js +121 -0
  29. package/dist/module-contracts.d.ts +23 -6
  30. package/dist/operator-record.d.ts +2 -2
  31. package/dist/operator-record.js +2 -4
  32. package/dist/operator-ui/agents.d.ts +74 -0
  33. package/dist/operator-ui/agents.js +104 -0
  34. package/dist/operator-ui/contract.d.ts +149 -28
  35. package/dist/operator-ui/endpoints.js +192 -0
  36. package/dist/operator-ui/fix-prompts.js +4 -4
  37. package/dist/operator-ui/generated.js +5 -4
  38. package/dist/operator-ui/member-page.js +112 -0
  39. package/dist/operator-ui/model.d.ts +14 -6
  40. package/dist/operator-ui/view.d.ts +84 -19
  41. package/dist/operator-ui/view.js +82 -21
  42. package/dist/page-styles.js +1 -1
  43. package/dist/provider.d.ts +61 -8
  44. package/dist/provider.js +46 -7
  45. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  46. package/dist/providers/_shared/google/workspace.js +30 -8
  47. package/dist/providers/_shared/rest/tools.js +2 -2
  48. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  49. package/dist/providers/_shared/rest/value-safety.js +699 -7
  50. package/dist/providers/ccb/index.js +1 -1
  51. package/dist/providers/cloudflare/index.js +3 -3
  52. package/dist/providers/cloudflare/rest.js +25 -3
  53. package/dist/providers/cloudflare/skill.generated.js +1 -1
  54. package/dist/providers/cloudflare/value-safety.js +32 -0
  55. package/dist/providers/docs/index.js +3 -3
  56. package/dist/providers/docs/skill.generated.js +2 -2
  57. package/dist/providers/drive/index.js +3 -3
  58. package/dist/providers/drive/skill.generated.js +2 -2
  59. package/dist/providers/forms/index.js +3 -3
  60. package/dist/providers/forms/skill.generated.js +2 -2
  61. package/dist/providers/gmail/index.js +10 -8
  62. package/dist/providers/gmail/skill.generated.js +2 -2
  63. package/dist/providers/mixpanel/index.js +1 -1
  64. package/dist/providers/notion/index.d.ts +7 -0
  65. package/dist/providers/notion/index.js +100 -11
  66. package/dist/providers/notion/rest.js +45 -5
  67. package/dist/providers/notion/skill.generated.js +2 -2
  68. package/dist/providers/notion/value-safety.js +30 -5
  69. package/dist/providers/overflow/index.js +1 -1
  70. package/dist/providers/planning-center/index.js +1 -1
  71. package/dist/providers/sheets/index.d.ts +3 -0
  72. package/dist/providers/sheets/index.js +6 -3
  73. package/dist/providers/sheets/skill.generated.js +2 -2
  74. package/dist/providers/slides/index.js +3 -3
  75. package/dist/providers/slides/skill.generated.js +2 -2
  76. package/dist/providers/stripe/rest.js +9 -2
  77. package/dist/providers/stripe/skill.generated.js +1 -1
  78. package/dist/providers/stripe/value-safety.js +22 -0
  79. package/dist/providers/vercel/rest.js +20 -2
  80. package/dist/providers/vercel/skill.generated.js +1 -1
  81. package/dist/providers/vercel/value-safety.js +22 -0
  82. package/dist/registry.d.ts +2 -3
  83. package/dist/registry.js +8 -9
  84. package/dist/routes/access-tokens.js +82 -75
  85. package/dist/routes/activity-disclosure.js +17 -18
  86. package/dist/routes/activity.js +34 -54
  87. package/dist/routes/actor-labels.js +33 -4
  88. package/dist/routes/agent-path.js +43 -0
  89. package/dist/routes/connect.js +16 -12
  90. package/dist/routes/credentials.js +16 -40
  91. package/dist/routes/mcp.js +145 -95
  92. package/dist/routes/oauth-management.js +11 -5
  93. package/dist/routes/oauth.js +3 -9
  94. package/dist/routes/operator.js +24 -23
  95. package/dist/routes/shared.d.ts +60 -20
  96. package/dist/routes/shared.js +187 -96
  97. package/dist/routes/ui-config.js +437 -97
  98. package/dist/routes/ui-data.js +2 -2
  99. package/dist/routes/ui.js +9 -16
  100. package/dist/runtime/call-admission.js +9 -0
  101. package/dist/server.js +5 -0
  102. package/dist/storage/keys.js +9 -1
  103. package/dist/types.d.ts +11 -0
  104. package/dist/ui.d.ts +1 -1
  105. package/dist/ui.js +16 -14
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/workspace-address.js +9 -0
  109. package/dist/workspace-subject.js +23 -0
  110. package/documentation/README.md +46 -22
  111. package/documentation/architecture.md +355 -50
  112. package/documentation/auth.md +775 -243
  113. package/documentation/code-mode.md +14 -11
  114. package/documentation/deploying.md +121 -24
  115. package/documentation/integrating.md +92 -21
  116. package/documentation/meta-tools.md +14 -9
  117. package/documentation/migration-0.33.md +344 -0
  118. package/documentation/operating.md +3 -1
  119. package/documentation/operator-tests.md +115 -28
  120. package/documentation/operator-ui.md +326 -61
  121. package/documentation/provider-auth-migration.md +37 -23
  122. package/documentation/provider-migration-0.29.md +2 -2
  123. package/examples/worker/AGENTS.md +30 -8
  124. package/examples/worker/README.md +92 -53
  125. package/examples/worker/src/connecta.config.ts +36 -20
  126. package/examples/worker/wrangler.jsonc +3 -0
  127. package/package.json +3 -2
  128. package/templates/node/.env.example +3 -0
  129. package/templates/node/AGENTS.md +27 -6
  130. package/templates/node/Dockerfile +2 -2
  131. package/templates/node/README.md +67 -42
  132. package/templates/node/docker-compose.yml +3 -2
  133. package/templates/node/package.json +4 -3
  134. package/templates/node/src/adopt-token.ts +23 -0
  135. package/templates/node/src/connecta.config.ts +28 -12
  136. package/templates/node/src/rotate-token.ts +17 -0
  137. package/templates/node/src/provision-token.ts +0 -17
@@ -11,13 +11,11 @@ export const NOTION_MCP_ENDPOINT = "https://mcp.notion.com/mcp";
11
11
  /** Lean by default: smaller than Notion's 100 so a first read stays cheap. */
12
12
  const DEFAULT_PAGE_SIZE = 25;
13
13
  /**
14
- * Notion documents "an average of three requests per second, with some bursts
15
- * beyond the average allowed" per connection. 180 calls per minute is that
16
- * average over a window short bursts pass and a sustained loop does not, and
17
- * `maxConcurrency: 3` is the load-bearing half — an averaged budget cannot stop
18
- * forty calls in one tick, and declaring a queue is also what makes the queue
19
- * settings legal at construction. Per runtime, and a floor on the real request
20
- * rate rather than a ceiling, since one admitted call can spend many fetches.
14
+ * Conservative default for every Notion plan: 180 calls per minute, with
15
+ * Connecta's concurrency cap of three to bound bursts. Business and Enterprise
16
+ * allow 600 requests per minute per connection; all connections also share a
17
+ * workspace-wide limit. This policy is per runtime and counts calls rather
18
+ * than HTTP requests, since one admitted call can spend many fetches.
21
19
  */
22
20
  const NOTION_ADMISSION = {
23
21
  rules: [
@@ -52,9 +50,11 @@ function oauthUsageGuide(purpose, instructions) {
52
50
  }
53
51
  /**
54
52
  * Reviewed in #705's provider audit against https://developers.notion.com/guides/mcp/mcp-supported-tools.
55
- * Retains the release-reviewed inventory, including names absent from today's
56
- * public reference. Live annotations were not reverified without credentials.
57
- * No schema digest is asserted without a captured schema to review.
53
+ * Extended in #814 from the operator's 2026-10-15 hosted catalog capture,
54
+ * recorded in mcp-catalog-2026-10-15.md. Includes names absent from the public
55
+ * reference and retains reviewed names absent from this client's catalog.
56
+ * Annotations were not visible; captured input summaries are not schema
57
+ * documents, so no schema digests are asserted.
58
58
  */
59
59
  const NOTION_MCP_CLASSIFICATION = {
60
60
  tools: {
@@ -201,6 +201,94 @@ const NOTION_MCP_CLASSIFICATION = {
201
201
  "reason": "update view changes existing Notion state or removes it.",
202
202
  },
203
203
  "notion-stop-session": { "verdict": "destructive", "reason": "Stops an existing session and interrupts its work." },
204
+ "notion-check-mcp-next-steps": {
205
+ "verdict": "read",
206
+ "reason": "Returns an upsell or next-step card without changing workspace state or enabling access.",
207
+ },
208
+ "notion-show-advanced-analysis-next-steps": {
209
+ "verdict": "read",
210
+ "reason": "Returns a message and destination link without changing workspace state or enabling access.",
211
+ },
212
+ "notion-get-enabled-skills": {
213
+ "verdict": "read",
214
+ "reason": "Lists the current user's enabled skills; enabling or disabling them is a separate write.",
215
+ },
216
+ "notion-get-file-download-urls": {
217
+ "verdict": "read",
218
+ "reason": "Returns signed, expiring download URLs for existing file references without changing workspace state; use only for downloads and do not expose the URLs.",
219
+ },
220
+ "notion-get-groups": {
221
+ "verdict": "read",
222
+ "reason": "Retrieves visible permission group IDs and names without changing membership or permissions.",
223
+ },
224
+ "notion-list-favorite-pages": {
225
+ "verdict": "read",
226
+ "reason": "Lists existing favorites in sidebar order without changing them.",
227
+ },
228
+ "notion-list-page-backlinks": {
229
+ "verdict": "read",
230
+ "reason": "Retrieves IDs, titles and URLs of existing page backlinks without changing links or pages.",
231
+ },
232
+ "notion-list-private-pages": {
233
+ "verdict": "read",
234
+ "reason": "Lists existing pages and databases in the user's Private sidebar without changing them.",
235
+ },
236
+ "notion-list-recent-pages": {
237
+ "verdict": "read",
238
+ "reason": "Lists the user's existing page-view history without changing pages or recording a new view.",
239
+ },
240
+ "notion-list-shared-pages": {
241
+ "verdict": "read",
242
+ "reason": "Lists existing pages and databases in the user's Shared sidebar without changing sharing.",
243
+ },
244
+ "notion-memory-search": {
245
+ "verdict": "read",
246
+ "reason": "Searches memories saved by connected agents in earlier sessions without saving or changing memories.",
247
+ },
248
+ "notion-query-multiple-data-sources": {
249
+ "verdict": "read",
250
+ "reason": "Runs read-only SQLite SQL across existing data sources without changing them.",
251
+ },
252
+ "notion-delete-comment": {
253
+ "verdict": "destructive",
254
+ "reason": "Deletes a comment and may remove its discussion when no comments remain.",
255
+ },
256
+ "notion-delete-databases": {
257
+ "verdict": "destructive",
258
+ "reason": "Moves existing databases to Trash; an optional confirmation flag does not make this tool read-only.",
259
+ },
260
+ "notion-delete-pages": {
261
+ "verdict": "destructive",
262
+ "reason": "Moves existing pages to Trash; an optional confirmation flag does not make this tool read-only.",
263
+ },
264
+ "notion-restore-pages": {
265
+ "verdict": "destructive",
266
+ "reason": "Restores pages or databases from Trash and changes their workspace state.",
267
+ },
268
+ "notion-set-comment-reaction": {
269
+ "verdict": "destructive",
270
+ "reason": "Adds or removes the user's emoji reaction on a comment; repeatability does not make it read-only.",
271
+ },
272
+ "notion-set-discussion-resolved": {
273
+ "verdict": "destructive",
274
+ "reason": "Resolves or reopens an existing comment discussion.",
275
+ },
276
+ "notion-update-comment": {
277
+ "verdict": "destructive",
278
+ "reason": "Replaces the text of an existing comment.",
279
+ },
280
+ "notion-update-enabled-skills": {
281
+ "verdict": "destructive",
282
+ "reason": "Changes the current user's skill enablement for external MCP clients, Notion AI, or both.",
283
+ },
284
+ "notion-update-folder": {
285
+ "verdict": "destructive",
286
+ "reason": "Changes an existing folder by adding or removing files or adding a subfolder.",
287
+ },
288
+ "notion-upload-skill": {
289
+ "verdict": "destructive",
290
+ "reason": "Preparation returns a credential-bearing upload URL and headers; completion replaces the target page title, Description, body and Files. Both actions stay writes.",
291
+ },
204
292
  },
205
293
  };
206
294
  const COMMON = keys("title", "authScope", "purpose", "instructions", "maxResultBytes");
@@ -214,6 +302,7 @@ const NOTION_OPTIONS = variants(["auth", "type"], {
214
302
  }).shape,
215
303
  token: optionsOf()({
216
304
  ...COMMON,
305
+ callAdmission: CALL_ADMISSION,
217
306
  ...keys("credentialLabel", "defaultPageSize"),
218
307
  auth: optionsOf()(keys("type")),
219
308
  }).shape,
@@ -246,7 +335,7 @@ function notionToken(id, options, provider) {
246
335
  placeholder: "Paste the integration token",
247
336
  },
248
337
  testCredential: rest.testCredential,
249
- callAdmission: NOTION_ADMISSION,
338
+ callAdmission: options.callAdmission === undefined ? NOTION_ADMISSION : options.callAdmission,
250
339
  usageGuide: {
251
340
  content: tokenUsageGuide(options.purpose, options.instructions),
252
341
  summary: "Internal integration over REST. Flattened reads, authoring helpers, and the pinned API index for the rest.",
@@ -2,9 +2,9 @@ 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, restTools, restTransport, } from "../_shared/rest/tools.js";
5
- import { valueSafety } from "../_shared/rest/value-safety.js";
5
+ import { carriesWithheldLink, valueSafety } from "../_shared/rest/value-safety.js";
6
6
  import { openapi } from "./openapi.generated.js";
7
- import { NOTION_VALUE_SAFETY } from "./value-safety.js";
7
+ import { NOTION_FILE_HOSTS, NOTION_VALUE_SAFETY, sanitizeLinks } from "./value-safety.js";
8
8
  /** Notion's REST origin. Every request this connector sends goes to exactly this host. */
9
9
  export const NOTION_API_BASE_URL = "https://api.notion.com";
10
10
  /**
@@ -21,6 +21,8 @@ export const NOTION_API_BASE_URL = "https://api.notion.com";
21
21
  export const NOTION_API_VERSION = "2026-03-11";
22
22
  /** Notion's hard cap on `page_size` for every paginated endpoint. */
23
23
  export const MAX_PAGE_SIZE = 100;
24
+ /** Page retrieval caps `filter_properties` at 100; data-source queries declare no cap. */
25
+ const MAX_PAGE_PROPERTY_FILTERS = 100;
24
26
  /** Notion's cap on `children` per append, and on blocks per page create. */
25
27
  const MAX_CHILDREN_PER_REQUEST = 100;
26
28
  /** Notion's cap on one rich-text run's `text.content`, and on runs in one rich-text array. */
@@ -128,6 +130,9 @@ function retryGuidance(additional) {
128
130
  *
129
131
  * `withheld` replaces every free-text field (message, retry guidance,
130
132
  * committed ids) for an operation whose error text the review withholds.
133
+ * Text that does pass has had its pre-signed file links withheld first
134
+ * (`sanitizeLinks`, as on success), since a message can echo the Markdown it
135
+ * rejected.
131
136
  */
132
137
  function notionFailure(status, headers, payload, withheld = false) {
133
138
  const body = isRecord(payload) ? payload : undefined;
@@ -235,6 +240,20 @@ function incomplete(data) {
235
240
  const status = data["request_status"];
236
241
  return isRecord(status) && status["type"] === "incomplete";
237
242
  }
243
+ /**
244
+ * A read copied into a write: a file link whose signature this connector
245
+ * withheld would be stored as the placeholder, or fail an exact `old_str`
246
+ * match. Refused before dispatch, named tools and generic writes alike.
247
+ */
248
+ function refuseWithheldLinks(call) {
249
+ if (call.method === "GET" || call.method === "HEAD")
250
+ return;
251
+ if (READ_POSTS.some(([method, path]) => method === call.method && path === call.op.path))
252
+ return;
253
+ if (!carriesWithheldLink(call.body, NOTION_FILE_HOSTS))
254
+ return;
255
+ invalid("This write carries a Notion file link whose signature Connecta withheld ([redacted]), so Notion would store the placeholder. A redacted file URL cannot be copied into a write: re-upload the file (POST /v1/file_uploads), reference the existing block instead, and match Markdown edits on the text around the media rather than its URL.");
256
+ }
238
257
  /** The pinned index, shared by every Notion REST connector in the deployment. */
239
258
  let index;
240
259
  let transport;
@@ -272,9 +291,14 @@ const VENDOR = {
272
291
  ? notionTransport()
273
292
  : `This operation is served from ${server}, which this connector does not reach.`;
274
293
  },
275
- failure: (status, headers, payload, call) => notionFailure(status, headers, payload, call ? SAFETY.withholdsErrors(call.op) : false),
294
+ failure: (status, headers, payload, call) => notionFailure(status, headers, sanitizeLinks(payload), call ? SAFETY.withholdsErrors(call.op) : false),
276
295
  readPosts: READ_POSTS,
277
296
  valueSafety: SAFETY,
297
+ // Not an envelope to unwrap: page Markdown carries pre-signed file links
298
+ // inside its text, where redaction's URL rule cannot reach. Every success
299
+ // response passes here, named tools and paging included.
300
+ result: sanitizeLinks,
301
+ admit: refuseWithheldLinks,
278
302
  page,
279
303
  upload: 'Create the upload with POST /v1/file_uploads and mode "external_url" instead; this connector sends no multipart bodies.',
280
304
  bodyHint: "Notion's JSON request body as notion_api_details lists it",
@@ -710,6 +734,17 @@ function pagination(args, defaultPageSize) {
710
734
  ...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
711
735
  };
712
736
  }
737
+ /** Both page GETs and data-source POST queries filter properties on the URL. */
738
+ function propertyQuery(properties, maxFilters) {
739
+ if (properties === undefined)
740
+ return {};
741
+ // The title column's name varies, but its id is always `title`. Fetch it
742
+ // even when projection drops the column so the top-level title survives.
743
+ const filters = [...new Set([...properties, "title"])];
744
+ // Filtering is an optimization. An oversized selection still needs every
745
+ // requested column, so retrieve all properties and keep projecting locally.
746
+ return maxFilters !== undefined && filters.length > maxFilters ? {} : { filter_properties: filters };
747
+ }
713
748
  /** Exactly-one-of validation, phrased so the agent knows what to send next. */
714
749
  function requireExactlyOne(provided, hint) {
715
750
  const present = provided.filter(([, value]) => value !== undefined && value !== null && value !== "");
@@ -806,7 +841,9 @@ function namedTools(defaultPageSize) {
806
841
  },
807
842
  outputSchema: PAGE_OUTPUT_SCHEMA,
808
843
  handler: async (args, ctx) => {
809
- const payload = await notion(ctx, "GET", `/v1/pages/${id(args.page_id)}`);
844
+ const payload = await notion(ctx, "GET", `/v1/pages/${id(args.page_id)}`, {
845
+ query: propertyQuery(args.properties, MAX_PAGE_PROPERTY_FILTERS),
846
+ });
810
847
  if (args.raw)
811
848
  return payload;
812
849
  return projectPage(payload, args.properties);
@@ -1000,7 +1037,10 @@ function namedTools(defaultPageSize) {
1000
1037
  body["filter"] = args.filter;
1001
1038
  if (args.sorts)
1002
1039
  body["sorts"] = args.sorts;
1003
- const payload = await notion(ctx, "POST", `/v1/data_sources/${id(args.data_source_id)}/query`, { body });
1040
+ const payload = await notion(ctx, "POST", `/v1/data_sources/${id(args.data_source_id)}/query`, {
1041
+ query: propertyQuery(args.properties),
1042
+ body,
1043
+ });
1004
1044
  if (args.raw)
1005
1045
  return payload;
1006
1046
  const rows = mappedListEnvelope(payload, (row) => projectPage(row, args.properties));
@@ -4,8 +4,8 @@ export const skill = {
4
4
  "instructionsHeading": "Workspace instructions",
5
5
  "fragments": {
6
6
  "oauth_head": "# Notion MCP usage\n\nOfficial MCP interface: tool names, descriptions, argument schemas, and result\nschemas come from Notion's live server. Connecta preserves that catalog and\napplies release-reviewed classification. Reviewed writes stay writes even when Notion claims they only read.\n\nWorkspace purpose: ",
7
- "oauth": "\n\n- This connection acts as its OAuth-authorized user and reaches what that user\n can. It holds no integration token. An internal integration (bot) is a\n separate connector with `auth: { type: \"token\" }` under another id; never\n switch to it to recover a failed call here.\n- Discover the live catalog before assuming a tool exists. Notion can gate\n tools by workspace, account, client, and rollout independently of Connecta.\n `notion-get-tool-access` reports what the workspace's plan allows.\n- Start broad discovery with `notion-search`, then use `notion-fetch` on\n the exact page, database, data source, or object before changing it.\n- Use the live input schema as the contract. Notion owns these MCP schemas;\n the REST schemas of Connecta's token connector do not apply to MCP tools with\n similar names.\n- Session and agent tools can launch asynchronous work. Read session state and\n events before sending another message, waiting, or stopping a session.\n- File uploads and attachment tools create durable workspace state. Keep\n downloads inside the requested task and do not expose signed attachment URLs.\n `notion-download-skill` returns a signed, expiring archive URL by design;\n the token connector's REST redaction does not apply to hosted tools, so\n use that URL for the download and do not repeat it in replies.\n- Connecta's generic REST tools (`notion_api_*`) and the `integration_*`\n helpers exist only on a token connector.\n- An `auth_required` failure means this connector's OAuth grant is missing or\n expired. Run `authorize_connector` for this connector id, then retry.\n",
8
- "key": "\n## Identity and tools\n\nThis is Connecta's REST connector to `api.notion.com`, acting as one internal\nintegration (a bot) with an operator-managed integration token. It reaches\nexactly the pages and data sources shared with that integration, and the\nintegration's capabilities decide which calls succeed: comment and\nuser-information capabilities are off by default. It never acts as a person.\nHosted Notion MCP is a separate OAuth connector.\n\n- Named tools cover the common path and flatten Notion's payloads:\n `integration_search` (titles), `integration_get_page` (properties),\n `integration_get_page_content` (blocks as text),\n `integration_get_data_source_schema`, and `integration_query_data_source`\n (rows). `integration_create_page` and `integration_append_blocks` author\n Notion's rich-text and block shapes from plain text, Markdown, or checklists.\n- Four generic tools reach every other operation in the pinned API index.\n `notion_api_search` finds operations, not content. `notion_api_details`\n returns one operation's parameters. `notion_api_read` calls GETs plus the\n read-only POST queries: search, data source and meeting note queries, and\n agent, session, and session event queries. `notion_api_write` calls every\n other POST, PATCH, and DELETE with `method` and `path` stated.\n- Every generic call is checked against the index before it is sent. An\n unknown path returns the nearest operations, and an unknown or missing\n parameter returns `validation.issues` with the accepted names. Nothing\n reached Notion, so fix the call from that answer.\n- The generic set replaces the old one-operation tools. Read a database\n container with `GET /v1/databases/{database_id}`, users with `GET /v1/users`\n or `GET /v1/users/me`, and comments with `GET /v1/comments`. Update\n properties, trash or restore (`in_trash`) with `PATCH /v1/pages/{page_id}`,\n and comment with `POST /v1/comments` (`rich_text` or `markdown`).\n- Refused by design: Notion's OAuth token endpoints (`/v1/oauth/*`) and\n multipart file sends. Import a file with `POST /v1/file_uploads` and\n `mode: \"external_url\"` instead.\n- Notion-hosted file URLs in pages and blocks are pre-signed: Connecta\n withholds their signature, credential, and security-token parameters, so a\n returned URL names the file but does not download it. File links inside\n page Markdown come back as Notion sent them: use one only for its download\n and do not repeat it in replies. Credential-named\n fields read as `\"[redacted]\"`, a database property named like a credential\n (`API token`) included. Failures of the OAuth, file upload, bot user, and\n agent operations carry Notion's code, not its message.\n- Not here: the hosted MCP's user identity, its search across connected\n apps, page duplication, team and skill search, attachment downloads, and\n page-to-skill conversion. Those need an OAuth connector.\n\n## Databases contain data sources\n\n- The id in a Notion database URL is a **database id**.\n `GET /v1/databases/{database_id}` through `notion_api_read` lists its\n `data_sources`, usually one. `integration_search` returns data sources\n directly and skips that step.\n- `integration_get_data_source_schema` and `integration_query_data_source`\n take the **data_source_id**, and so does `integration_create_page` when\n adding a row.\n\nSo the sequence for \"find rows in this database\" is the database read, then\n`integration_get_data_source_schema`, then `integration_query_data_source`.\n\n## Reading\n\n- `integration_get_page` returns properties. To read or summarize a whole\n body, prefer `notion_api_read` `GET /v1/pages/{page_id}/markdown`: one\n request returns every nested level as Markdown (headings, lists, to-dos,\n toggles, tables, code, links, mentions, equations). A huge page comes back\n with `truncated: true` and the unread blocks in `unknown_block_ids`; read\n each id as a `page_id` on the same path.\n- `integration_get_page_content` returns the body as flat blocks with ids,\n for quoting a block or appending after one. Nested blocks (toggles, list\n children, table rows) need `depth`, and each level multiplies requests. A\n deep read stops at an internal ceiling and reports `truncated: true` rather\n than spending the whole rate-limit budget.\n- Notion truncates `title`, `rich_text`, `relation`, and `people` at 25\n entries. `integration_get_page` reports each in `truncated_properties` as\n `{ name, id }`; read the complete value with `notion_api_read`\n `GET /v1/pages/{page_id}/properties/{property_id}` and follow its cursor.\n- Every named read projects Notion's payload to ids, plain text, and\n flattened property values. Pass `raw: true` for Notion's untouched response\n when a specific field is missing. It is much larger. Narrow\n `integration_query_data_source` and `integration_get_page` with `properties`\n when the goal is fewer fields. `integration_get_page_content` with\n `raw: true` returns one level and ignores `depth`.\n\n## Writing content\n\n`integration_append_blocks` adds children and nothing else. It cannot move,\nreorder, or replace an existing block, and a block appended through the API can\nnever be relocated by it afterwards. Get the position right the first time with\n`position` and `after_block_id`. Notion caps one call at 100 blocks. Text\nlonger than Notion's 2,000-character run limit is split into runs for you.\n\nFor formatted content, write Markdown to a page with `notion_api_write`\n`PATCH /v1/pages/{page_id}/markdown`. Append with\n`{\"type\":\"insert_content\",\"insert_content\":{\"content\":\"## Notes\\n- one\",\"position\":{\"type\":\"end\"}}}`\n(`start` prepends). Edit text in place with `update_content`, whose\n`content_updates` are `{ old_str, new_str }` pairs copied exactly from the\npage's Markdown, and `replace_content` swaps the whole page for `new_str`.\nNotion refuses an edit that would delete a child page or database with a\n`validation_error` naming them. Removing ordinary text needs no flag; to delete\nthose children, set `allow_deleting_content: true` inside the command body\n(`{\"type\":\"update_content\",\"update_content\":{\"content_updates\":[…],\"allow_deleting_content\":true}}`),\nand leave it out unless the task is to delete them. `integration_create_page`\ntakes the same Markdown for a new page's body.\n\n## Pagination\n\n- Named list tools take `page_size` (max 100) and return `has_more` with\n `next_cursor`; pass it back as `start_cursor`.\n- Generic results arrive as `{ status, data, page? }`. On a list,\n `page.next` is the cursor and `page.param` is `start_cursor`: send it in\n `query` for a GET, and in `body` when `page.in` is `body`, as on the POST\n queries. Pass `select` dot paths relative to `data`, such as `results.id`;\n `page` survives any `select`.\n- A view query is created with `notion_api_write`\n `POST /v1/views/{view_id}/queries`, a write because Notion stores it until\n `expires_at`. Its first page names the continuation in `page.path`: read\n `GET /v1/views/{view_id}/queries/{query_id}` with `start_cursor` in `query`.\n- Meeting-note queries have no cursor. `limit` caps at 50, and\n `page.hasMore` without `page.next` means more notes exist: narrow the\n filter (for example by `created_time`) rather than paging.\n- A data source query stops at 10,000 rows. `integration_query_data_source`\n then returns `incomplete: true`, and a generic query `page.hasMore` without\n `page.next`: narrow the filter rather than trusting the count.\n- Cursors are opaque: never parse or construct one. Follow pages inside\n `execute_code` and reduce there.\n\n## Failures worth reading carefully\n\n- **404** means \"no such object\" *or* \"not shared with this integration\", and\n Notion will not say which. Never conclude a page was deleted from it; check\n that the page is shared with the integration in Notion.\n- **403** is not an expired token. The integration is missing a capability\n (comment capabilities are off by default), the object was never shared, or\n the workspace is past its plan's block limit; Notion's message says which.\n Re-authorizing cannot fix it; an operator must change it in Notion.\n- **429** carries a retry window. Notion allows each integration 180 requests\n a minute (600 on Business and Enterprise) plus a budget shared by the whole\n workspace; Connecta admits 180 calls a minute, three at a time, per runtime.\n Wait out the window rather than retrying immediately. A 429 that says the\n integration's API access is blocked is not retryable.\n- **5xx on a write** may still have been saved. When Notion names the\n object it committed, the failure says so: read that object instead of\n repeating the write. Otherwise check the target before trying again.\n",
7
+ "oauth": "\n\n- This connection acts as its OAuth-authorized user and reaches what that user\n can. It holds no integration token. An internal integration (bot) is a\n separate connector with `auth: { type: \"token\" }` under another id; never\n switch to it to recover a failed call here.\n- Discover the live catalog before assuming a tool exists. Notion can gate\n tools by workspace, account, client, and rollout independently of Connecta.\n `notion-get-tool-access` reports what the workspace's plan allows.\n- Start broad discovery with `notion-search`, then use `notion-fetch` on\n the exact page, database, data source, or object before changing it.\n- Use the live input schema as the contract. Notion owns these MCP schemas;\n the REST schemas of Connecta's token connector do not apply to MCP tools with\n similar names.\n- The next-step card tools return guidance without enabling access. Call\n `notion-check-mcp-next-steps` only when fetch instructs you to, at most\n once. Call `notion-show-advanced-analysis-next-steps` once at turn end\n only when a multi-source query requires the full version.\n- Session and agent tools can launch asynchronous work. Read session state and\n events before sending another message, waiting, or stopping a session.\n- File uploads and attachment tools create durable workspace state. Keep\n downloads inside the requested task and do not expose signed attachment URLs.\n `notion-get-file-download-urls` returns signed download URLs that expire\n after a few minutes. Use them only for the requested download and keep\n stable `notion-file-block://` references in edited or copied Markdown.\n `notion-download-skill` also returns a signed, expiring archive URL.\n The token connector's REST redaction does not apply to hosted tools;\n do not repeat signed download URLs in replies.\n- `notion-upload-skill` is a write for both actions. `prepare` returns a\n credential-bearing upload URL and headers with a ten-minute expiry;\n keep them inside the upload flow and out of replies. `complete` replaces\n the target page's title, Description, body, and Files.\n- Connecta's generic REST tools (`notion_api_*`) and the `integration_*`\n helpers exist only on a token connector.\n- An `auth_required` failure means this connector's OAuth grant is missing or\n expired. Run `authorize_connector` for this connector id, then retry.\n",
8
+ "key": "\n## Identity and tools\n\nThis is Connecta's REST connector to `api.notion.com`, acting as one internal\nintegration (a bot) with an operator-managed integration token. It reaches\nexactly the pages and data sources shared with that integration, and the\nintegration's capabilities decide which calls succeed: comment and\nuser-information capabilities are off by default. It never acts as a person.\nHosted Notion MCP is a separate OAuth connector.\n\n- Named tools cover the common path and flatten Notion's payloads:\n `integration_search` (titles), `integration_get_page` (properties),\n `integration_get_page_content` (blocks as text),\n `integration_get_data_source_schema`, and `integration_query_data_source`\n (rows). `integration_create_page` and `integration_append_blocks` author\n Notion's rich-text and block shapes from plain text, Markdown, or checklists.\n- Four generic tools reach every other operation in the pinned API index.\n `notion_api_search` finds operations, not content. `notion_api_details`\n returns one operation's parameters. `notion_api_read` calls GETs plus the\n read-only POST queries: search, data source and meeting note queries, and\n agent, session, and session event queries. `notion_api_write` calls every\n other POST, PATCH, and DELETE with `method` and `path` stated.\n- Every generic call is checked against the index before it is sent. An\n unknown path returns the nearest operations, and an unknown or missing\n parameter returns `validation.issues` with the accepted names. Nothing\n reached Notion, so fix the call from that answer.\n- The generic set replaces the old one-operation tools. Read a database\n container with `GET /v1/databases/{database_id}`, users with `GET /v1/users`\n or `GET /v1/users/me`, and comments with `GET /v1/comments`. Update\n properties, trash or restore (`in_trash`) with `PATCH /v1/pages/{page_id}`,\n and comment with `POST /v1/comments` (`rich_text` or `markdown`).\n- Refused by design: Notion's OAuth token endpoints (`/v1/oauth/*`) and\n multipart file sends. Import a file with `POST /v1/file_uploads` and\n `mode: \"external_url\"` instead.\n- Notion-minted file links lose their whole query and fragment wherever\n they appear, including page Markdown, async task results, and error text.\n Connecta matches the actual authority: `file.notion.so`,\n `secure.notion-static.com`, `notionusercontent.com` and their subdomains,\n or the `prod-files-secure` and `secure.notion-static.com` S3 buckets,\n virtual-hosted or exactly the first path segment on an S3 endpoint.\n Escaped schemes, slashes, and query/fragment openers are recognized. The\n withheld span runs through the next whitespace, quote, or angle bracket;\n trailing closing punctuation comes back. A returned file URL names the\n file but cannot download it. Links at other authorities, including proxy\n URLs with a Notion URL inside their path or query, come back as written.\n AWS `X-Amz-*` and Google `X-Goog-*` signing `Signature`, `Credential`, and\n `Security-Token` values are withheld at any host.\n- A write whose Notion file link carries the exact `[redacted]` placeholder\n in its own query or fragment, plain or up to four layers encoded, is\n refused before dispatch. Each inline Markdown destination is checked,\n including adjacent links and images, balanced or escaped parentheses, and\n leading spaces, tabs, or one line ending. A marker after the destination's\n close is ordinary text. To keep a file, leave its block in place or\n re-upload it with `POST /v1/file_uploads`; to edit Markdown around media,\n match `old_str` on nearby text rather than the withheld URL.\n- Each string is scanned separately. Links split across rich-text runs,\n encoded hostnames or signing-parameter names, bare hosts without `//`,\n and userinfo before a Notion host are outside this scan's threat model.\n The shared URL rule still removes userinfo from whole URL fields.\n Credential-named fields read as `\"[redacted]\"`, a database property named\n like a credential (`API token`) included. Failures of the OAuth, file upload, bot user, and\n agent operations carry Notion's code, not its message.\n- Not here: the hosted MCP's user identity, its search across connected\n apps, page duplication, team and skill search, attachment downloads, and\n page-to-skill conversion. Those need an OAuth connector.\n\n## Databases contain data sources\n\n- The id in a Notion database URL is a **database id**.\n `GET /v1/databases/{database_id}` through `notion_api_read` lists its\n `data_sources`, usually one. `integration_search` returns data sources\n directly and skips that step.\n- `integration_get_data_source_schema` and `integration_query_data_source`\n take the **data_source_id**, and so does `integration_create_page` when\n adding a row.\n\nSo the sequence for \"find rows in this database\" is the database read, then\n`integration_get_data_source_schema`, then `integration_query_data_source`.\n\n## Reading\n\n- `integration_get_page` returns properties. To read or summarize a whole\n body, prefer `notion_api_read` `GET /v1/pages/{page_id}/markdown`: one\n request returns every nested level as Markdown (headings, lists, to-dos,\n toggles, tables, code, links, mentions, equations). A huge page comes back\n with `truncated: true` and the unread blocks in `unknown_block_ids`; read\n each id as a `page_id` on the same path.\n- `integration_get_page_content` returns the body as flat blocks with ids,\n for quoting a block or appending after one. Nested blocks (toggles, list\n children, table rows) need `depth`, and each level multiplies requests. A\n deep read stops at an internal ceiling and reports `truncated: true` rather\n than spending the whole rate-limit budget.\n- Notion truncates `title`, `rich_text`, `relation`, and `people` at 25\n entries. `integration_get_page` reports each in `truncated_properties` as\n `{ name, id }`; read the complete value with `notion_api_read`\n `GET /v1/pages/{page_id}/properties/{property_id}` and follow its cursor.\n- Every named read projects Notion's payload to ids, plain text, and\n flattened property values. Pass `raw: true` for the full response, with\n the same value-safety redaction, when a specific field is missing. It is\n much larger. Narrow\n `integration_query_data_source` and `integration_get_page` with `properties`\n when the goal is fewer fields. This also narrows the request to Notion with\n repeated `filter_properties` URL parameters, shrinking the response before\n Connecta reads it. The title property is always fetched so the projected\n `title` stays populated. Page retrieval allows at most 100 distinct filters,\n including the title id. A larger selection fetches all properties and still\n projects only the selected names locally. Data source queries have no\n documented filter count limit. `raw: true` returns the upstream response,\n filtered when those filters fit and full when page retrieval falls back.\n Omit `properties` for the full response on either tool.\n `integration_get_page_content` with `raw: true` returns one level and ignores\n `depth`.\n\n## Writing content\n\n`integration_append_blocks` adds children and nothing else. It cannot move,\nreorder, or replace an existing block, and a block appended through the API can\nnever be relocated by it afterwards. Get the position right the first time with\n`position` and `after_block_id`. Notion caps one call at 100 blocks. Text\nlonger than Notion's 2,000-character run limit is split into runs for you.\n\nFor formatted content, write Markdown to a page with `notion_api_write`\n`PATCH /v1/pages/{page_id}/markdown`. Append with\n`{\"type\":\"insert_content\",\"insert_content\":{\"content\":\"## Notes\\n- one\",\"position\":{\"type\":\"end\"}}}`\n(`start` prepends). Edit text in place with `update_content`, whose\n`content_updates` are `{ old_str, new_str }` pairs copied exactly from the\npage's Markdown, and `replace_content` swaps the whole page for `new_str`.\nNotion refuses an edit that would delete a child page or database with a\n`validation_error` naming them. Removing ordinary text needs no flag; to delete\nthose children, set `allow_deleting_content: true` inside the command body\n(`{\"type\":\"update_content\",\"update_content\":{\"content_updates\":[…],\"allow_deleting_content\":true}}`),\nand leave it out unless the task is to delete them. `integration_create_page`\ntakes the same Markdown for a new page's body.\n\n## Pagination\n\n- Named list tools take `page_size` (max 100) and return `has_more` with\n `next_cursor`; pass it back as `start_cursor`.\n- Generic results arrive as `{ status, data, page? }`. On a list,\n `page.next` is the cursor and `page.param` is `start_cursor`: send it in\n `query` for a GET, and in `body` when `page.in` is `body`, as on the POST\n queries. Pass `select` dot paths relative to `data`, such as `results.id`;\n `page` survives any `select`.\n- A view query is created with `notion_api_write`\n `POST /v1/views/{view_id}/queries`, a write because Notion stores it until\n `expires_at`. Its first page names the continuation in `page.path`: read\n `GET /v1/views/{view_id}/queries/{query_id}` with `start_cursor` in `query`.\n- Meeting-note queries have no cursor. `limit` caps at 50, and\n `page.hasMore` without `page.next` means more notes exist: narrow the\n filter (for example by `created_time`) rather than paging.\n- A data source query stops at 10,000 rows. `integration_query_data_source`\n then returns `incomplete: true`, and a generic query `page.hasMore` without\n `page.next`: narrow the filter rather than trusting the count.\n- Cursors are opaque: never parse or construct one. Follow pages inside\n `execute_code` and reduce there.\n\n## Failures worth reading carefully\n\n- **404** means \"no such object\" *or* \"not shared with this integration\", and\n Notion will not say which. Never conclude a page was deleted from it; check\n that the page is shared with the integration in Notion.\n- **403** is not an expired token. The integration is missing a capability\n (comment capabilities are off by default), the object was never shared, or\n the workspace is past its plan's block limit; Notion's message says which.\n Re-authorizing cannot fix it; an operator must change it in Notion.\n- **429** carries a retry window. Notion's [request limits](https://developers.notion.com/reference/request-limits)\n allow 600 requests a minute per connection on Business and Enterprise and\n 180 on other plans, plus a workspace-wide limit shared by all connections.\n Connecta defaults to 180 calls a minute, three at a time, per runtime.\n A token connector's optional `callAdmission` replaces that whole policy;\n account for other runtime traffic and tools that make multiple HTTP requests.\n Wait out the window rather than retrying immediately. A 429 that says the\n integration's API access is blocked is not retryable.\n- **5xx on a write** may still have been saved. When Notion names the\n object it committed, the failure says so: read that object instead of\n repeating the write. Otherwise check the target before trying again.\n",
9
9
  "shared": "\n## Notion's data model\n\n- A database is a container; its rows and schema live in a *data source*\n inside it. Queries, schema reads, and new rows take the data source, and\n Notion rejects a database id in its place.\n- Property names in filters, sorts, and writes must match the schema exactly,\n including case. Read the schema before composing one.\n- A `status` write must use an existing option name; inventing one fails.\n A new `select` or `multi_select` name does not fail: when the connection\n can write to the data source, Notion adds it to the schema as a new\n option. Use the options the schema lists unless a schema change is meant.\n- Writes **replace** a property. Sending one item to a `multi_select` or\n `relation` drops the others, so send the complete intended value.\n- `rollup`, `formula`, `unique_id`, and the created and edited fields are\n computed. They cannot be written.\n- A data source's title column is rarely called \"title\". Read its name from\n the schema before creating or renaming a row.\n- Notion has **no idempotency key**. A retried page create or comment makes a\n duplicate. Confirm with a search before repeating a write that may have\n partially succeeded.\n"
10
10
  }
11
11
  };
@@ -7,11 +7,11 @@
7
7
  // never appears in a response. The OAuth endpoints mint, introspect, and
8
8
  // revoke tokens with a public integration's client secret and are refused.
9
9
  // A file upload's `upload_url` is Notion's own send endpoint and needs the
10
- // integration token, so it is kept. Notion-hosted file URLs in pages and
11
- // blocks are pre-signed: the shared URL rule withholds their signature,
12
- // credential, and security-token query parameters on every response, so a
13
- // returned URL names the file without granting access to it.
14
- import { refuse, safe } from "../_shared/rest/value-safety.js";
10
+ // integration token, so it is kept. Notion-minted file URLs are pre-signed:
11
+ // sanitizeLinks removes their whole query/fragment wherever they appear,
12
+ // including page Markdown and free-form async results, before projections
13
+ // or the shared whole-URL heuristic. The same scan precedes error mapping.
14
+ import { refuse, safe, withholdSignedLinksIn, } from "../_shared/rest/value-safety.js";
15
15
  const OAUTH = "Notion's OAuth token endpoints are not reachable through Connecta: they mint and revoke credentials with a public integration's client secret, which this connector never holds.";
16
16
  const UPLOAD = "A file upload's upload_url is Notion's own send endpoint, which needs the integration token; it grants nothing alone.";
17
17
  const AGENT = "Custom agents name their connected MCP server's host, never its URL or credentials.";
@@ -29,3 +29,28 @@ const OPERATIONS = {
29
29
  "GET /v1/agents/{agent_id}": safe(AGENT, ["connections.account.server_host"]),
30
30
  };
31
31
  export const NOTION_VALUE_SAFETY = { title: "Notion", operations: OPERATIONS };
32
+ /**
33
+ * Where Notion stores the files it signs. Its S3 buckets, virtual-hosted
34
+ * (`prod-files-secure.s3.us-west-2.amazonaws.com`) or as the first path
35
+ * segment on an S3 endpoint (`s3.us-west-2.amazonaws.com/secure.notion-static.com/…`,
36
+ * Notion's file object reference), and its own file, static, and image-proxy
37
+ * hosts and their subdomains (`file.notion.so`, `img.notionusercontent.com`).
38
+ * Never all of `amazonaws.com`, and never a host or path that merely
39
+ * contains one of these names: a person's link is their content.
40
+ */
41
+ export const NOTION_FILE_HOSTS = {
42
+ domains: ["file.notion.so", "secure.notion-static.com", "notionusercontent.com"],
43
+ buckets: ["prod-files-secure", "secure.notion-static.com"],
44
+ };
45
+ /**
46
+ * Every response with each pre-signed file link embedded in its strings
47
+ * withheld. Images in page Markdown render as `![caption](url)` and files,
48
+ * PDFs, audio, and video as `<file src="url">`, so a Notion-hosted file's
49
+ * pre-signed URL arrives inside text rather than as a field: in `GET` and
50
+ * `PATCH /v1/pages/{page_id}/markdown`, and in a completed async task's
51
+ * `result`, which is free-form. Every response and error body is scanned
52
+ * rather than those paths, so no route to page Markdown can skip it.
53
+ */
54
+ export function sanitizeLinks(body) {
55
+ return withholdSignedLinksIn(body, NOTION_FILE_HOSTS);
56
+ }
@@ -1519,7 +1519,7 @@ function overflowConnector(id, options) {
1519
1519
  };
1520
1520
  }
1521
1521
  },
1522
- callAdmission: options.callAdmission ?? OVERFLOW_ADMISSION,
1522
+ callAdmission: options.callAdmission === undefined ? OVERFLOW_ADMISSION : options.callAdmission,
1523
1523
  usageGuide: {
1524
1524
  content: usageGuide(purpose, environment, options.instructions),
1525
1525
  summary: `Overflow ${label}: money units per field, final statuses, deposits, donor-PII care, and writes via mutate.`,
@@ -2443,7 +2443,7 @@ function planningCenterConnector(id, options) {
2443
2443
  };
2444
2444
  }
2445
2445
  },
2446
- callAdmission: options.callAdmission ?? PLANNING_CENTER_ADMISSION,
2446
+ callAdmission: options.callAdmission === undefined ? PLANNING_CENTER_ADMISSION : options.callAdmission,
2447
2447
  usageGuide: {
2448
2448
  content: usageGuide(purpose, options.instructions, excluded),
2449
2449
  summary: "Person ids join every product, offset paging, per-product write limits, JSON:API hatches, pinned versions.",
@@ -46,6 +46,9 @@ export type SheetsOptions = GoogleWorkspaceOptions;
46
46
  * });
47
47
  * ```
48
48
  *
49
+ * Agents never reach that function: give each pool they use an `owner`,
50
+ * the address they act as there (decisions/0006).
51
+ *
49
52
  * Reads are read-only. `create_spreadsheet`, `add_sheet`, and `append_values`
50
53
  * are additive writes the host approves through call_destructive_tool or a
51
54
  * program on a writable endpoint; the value writes, `clear_values`, and the raw
@@ -33,7 +33,7 @@ import { skill } from "./skill.generated.js";
33
33
  */
34
34
  import { apiConnector as api, defined } from "../../connectors/api-connector.js";
35
35
  import { ConnectorCallError } from "../../errors.js";
36
- import { googleOutcomeOf, GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, } from "../_shared/google/workspace.js";
36
+ import { googleOutcomeOf, GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, workspaceConnector, } from "../_shared/google/workspace.js";
37
37
  import { clampText, jsonBytes, RESULT_BUDGET_BYTES } from "../_shared/google/result-size.js";
38
38
  import { asProviderFactory } from "../../provider.js";
39
39
  /**
@@ -1186,6 +1186,9 @@ function usageGuide(purpose, instructions) {
1186
1186
  * });
1187
1187
  * ```
1188
1188
  *
1189
+ * Agents never reach that function: give each pool they use an `owner`,
1190
+ * the address they act as there (decisions/0006).
1191
+ *
1189
1192
  * Reads are read-only. `create_spreadsheet`, `add_sheet`, and `append_values`
1190
1193
  * are additive writes the host approves through call_destructive_tool or a
1191
1194
  * program on a writable endpoint; the value writes, `clear_values`, and the raw
@@ -1218,7 +1221,7 @@ function sheetsConnector(id, options) {
1218
1221
  notFound: "ambiguous",
1219
1222
  connection,
1220
1223
  });
1221
- return api(id, {
1224
+ return workspaceConnector(connection, api(id, {
1222
1225
  ...defined({
1223
1226
  callAdmission: options.callAdmission,
1224
1227
  maxResultBytes: options.maxResultBytes,
@@ -1233,5 +1236,5 @@ function sheetsConnector(id, options) {
1233
1236
  required: true,
1234
1237
  },
1235
1238
  tools: tools(client),
1236
- });
1239
+ }));
1237
1240
  }
@@ -1,10 +1,10 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "# Google Sheets usage\n\nActs as the signed-in person in their own Google Sheets through Workspace delegation: it reaches only spreadsheets they can open, starting from an id.\n\nConnection purpose: \n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto. No argument names an account. A call with none mapped fails\n`auth_required`; only an operator can change the mapping. An id that is\nunknown or not shared with this person fails the same way — Google does not\nsay which.\n\n## Finding a spreadsheet\n\n- Sheets cannot list or search files. Find one by name with a Google Drive\n search, or take the id from its URL (between `/d/` and `/edit`).\n- `get_spreadsheet` first: sheet titles, ids, grid sizes, and named ranges,\n so ranges are named right rather than guessed.\n\n## Ranges\n\n- A1 notation. Quote a sheet title with spaces or punctuation:\n `'Q3 Budget'!A1:D20`, doubling any `'` inside it. A bare title is the\n whole sheet; `Sheet1!B:B` a whole column; a named range works by name.\n- `get_values` pages by whole rows under `maxCells` and `maxBytes`:\n page with `page.nextCursor` and the same spreadsheetId, ranges, and\n render options. A range with explicit rows (`A1:D500`, `B:D`) resumes\n where it stopped; a sheet title or named range is re-read and its earlier\n rows skipped, so `rowOffset` says how far into Google's echoed range the\n page starts.\n- A row too wide or too large for one page is refused, not cut: narrow the\n columns or lower `maxCellChars`. The default `maxBytes` is what\n `execute_code` can receive; raise it only for a direct `call_tool`\n read.\n- A cell over characters ends with a truncation marker;\n read it alone with `maxCellChars` for the rest. Reduce inside\n `execute_code` before returning a large read.\n\n## Writing\n\n- `valueInputOption` is required: `RAW` stores text as given;\n `USER_ENTERED` parses it as typed, so a leading `=` makes a formula. Use\n RAW for data from outside the spreadsheet.\n- In written rows, `null` leaves a cell as it was and `\"\"` empties it.\n- `append_values` inserts rows below the table and overwrites nothing;\n `update_values` and `batch_update_values` overwrite the cells they\n cover; `clear_values` empties values but keeps formatting.\n- `batch_update_spreadsheet` takes Google's own Request objects for\n everything else (formatting, sorting, deleting rows or sheets); it is\n atomic, always needs approval, and can destroy data. Replies too large\n to return come back cut to their kind, ids, and counts with a `note`;\n the requests still applied, so never send them again to see the rest.\n- Sheets has no revision check on writes: edits are last-writer-wins, so a\n person editing the same cells meanwhile is overwritten without warning.\n Read just before writing over anything a person may be editing.\n- Moving, sharing, or deleting a spreadsheet file is Drive's job.\n",
3
+ "content": "# Google Sheets usage\n\nActs as the signed-in person in their own Google Sheets through Workspace delegation: it reaches only spreadsheets they can open, starting from an id.\n\nConnection purpose: \n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto; an agent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument\nnames an account. A call with none mapped, or from an agent whose pool has\nno owner, fails `auth_required`; only an operator can change either. An id that is\nunknown or not shared with this person fails the same way — Google does not\nsay which.\n\n## Finding a spreadsheet\n\n- Sheets cannot list or search files. Find one by name with a Google Drive\n search, or take the id from its URL (between `/d/` and `/edit`).\n- `get_spreadsheet` first: sheet titles, ids, grid sizes, and named ranges,\n so ranges are named right rather than guessed.\n\n## Ranges\n\n- A1 notation. Quote a sheet title with spaces or punctuation:\n `'Q3 Budget'!A1:D20`, doubling any `'` inside it. A bare title is the\n whole sheet; `Sheet1!B:B` a whole column; a named range works by name.\n- `get_values` pages by whole rows under `maxCells` and `maxBytes`:\n page with `page.nextCursor` and the same spreadsheetId, ranges, and\n render options. A range with explicit rows (`A1:D500`, `B:D`) resumes\n where it stopped; a sheet title or named range is re-read and its earlier\n rows skipped, so `rowOffset` says how far into Google's echoed range the\n page starts.\n- A row too wide or too large for one page is refused, not cut: narrow the\n columns or lower `maxCellChars`. The default `maxBytes` is what\n `execute_code` can receive; raise it only for a direct `call_tool`\n read.\n- A cell over characters ends with a truncation marker;\n read it alone with `maxCellChars` for the rest. Reduce inside\n `execute_code` before returning a large read.\n\n## Writing\n\n- `valueInputOption` is required: `RAW` stores text as given;\n `USER_ENTERED` parses it as typed, so a leading `=` makes a formula. Use\n RAW for data from outside the spreadsheet.\n- In written rows, `null` leaves a cell as it was and `\"\"` empties it.\n- `append_values` inserts rows below the table and overwrites nothing;\n `update_values` and `batch_update_values` overwrite the cells they\n cover; `clear_values` empties values but keeps formatting.\n- `batch_update_spreadsheet` takes Google's own Request objects for\n everything else (formatting, sorting, deleting rows or sheets); it is\n atomic, always needs approval, and can destroy data. Replies too large\n to return come back cut to their kind, ids, and counts with a `note`;\n the requests still applied, so never send them again to see the rest.\n- Sheets has no revision check on writes: edits are last-writer-wins, so a\n person editing the same cells meanwhile is overwritten without warning.\n Read just before writing over anything a person may be editing.\n- Moving, sharing, or deleting a spreadsheet file is Drive's job.\n",
4
4
  "instructionsHeading": "Connection instructions",
5
5
  "fragments": {
6
6
  "guide_0": "# Google Sheets usage\n\nActs as the signed-in person in their own Google Sheets through Workspace delegation: it reaches only spreadsheets they can open, starting from an id.\n\nConnection purpose: ",
7
- "guide_1": "\n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto. No argument names an account. A call with none mapped fails\n`auth_required`; only an operator can change the mapping. An id that is\nunknown or not shared with this person fails the same way — Google does not\nsay which.\n\n## Finding a spreadsheet\n\n- Sheets cannot list or search files. Find one by name with a Google Drive\n search, or take the id from its URL (between `/d/` and `/edit`).\n- `get_spreadsheet` first: sheet titles, ids, grid sizes, and named ranges,\n so ranges are named right rather than guessed.\n\n## Ranges\n\n- A1 notation. Quote a sheet title with spaces or punctuation:\n `'Q3 Budget'!A1:D20`, doubling any `'` inside it. A bare title is the\n whole sheet; `Sheet1!B:B` a whole column; a named range works by name.\n- `get_values` pages by whole rows under `maxCells` and `maxBytes`:\n page with `page.nextCursor` and the same spreadsheetId, ranges, and\n render options. A range with explicit rows (`A1:D500`, `B:D`) resumes\n where it stopped; a sheet title or named range is re-read and its earlier\n rows skipped, so `rowOffset` says how far into Google's echoed range the\n page starts.\n- A row too wide or too large for one page is refused, not cut: narrow the\n columns or lower `maxCellChars`. The default `maxBytes` is what\n `execute_code` can receive; raise it only for a direct `call_tool`\n read.\n- A cell over ",
7
+ "guide_1": "\n\n## Whose spreadsheets\n\nEvery call acts as the Workspace account deployment config maps the caller\nto; an agent acts as the owner of the pool it came through, unless the deployment fixes one shared account that every caller, agent or person, acts as. No argument\nnames an account. A call with none mapped, or from an agent whose pool has\nno owner, fails `auth_required`; only an operator can change either. An id that is\nunknown or not shared with this person fails the same way — Google does not\nsay which.\n\n## Finding a spreadsheet\n\n- Sheets cannot list or search files. Find one by name with a Google Drive\n search, or take the id from its URL (between `/d/` and `/edit`).\n- `get_spreadsheet` first: sheet titles, ids, grid sizes, and named ranges,\n so ranges are named right rather than guessed.\n\n## Ranges\n\n- A1 notation. Quote a sheet title with spaces or punctuation:\n `'Q3 Budget'!A1:D20`, doubling any `'` inside it. A bare title is the\n whole sheet; `Sheet1!B:B` a whole column; a named range works by name.\n- `get_values` pages by whole rows under `maxCells` and `maxBytes`:\n page with `page.nextCursor` and the same spreadsheetId, ranges, and\n render options. A range with explicit rows (`A1:D500`, `B:D`) resumes\n where it stopped; a sheet title or named range is re-read and its earlier\n rows skipped, so `rowOffset` says how far into Google's echoed range the\n page starts.\n- A row too wide or too large for one page is refused, not cut: narrow the\n columns or lower `maxCellChars`. The default `maxBytes` is what\n `execute_code` can receive; raise it only for a direct `call_tool`\n read.\n- A cell over ",
8
8
  "guide_2": " characters ends with a truncation marker;\n read it alone with `maxCellChars` for the rest. Reduce inside\n `execute_code` before returning a large read.\n\n## Writing\n\n- `valueInputOption` is required: `RAW` stores text as given;\n `USER_ENTERED` parses it as typed, so a leading `=` makes a formula. Use\n RAW for data from outside the spreadsheet.\n- In written rows, `null` leaves a cell as it was and `\"\"` empties it.\n- `append_values` inserts rows below the table and overwrites nothing;\n `update_values` and `batch_update_values` overwrite the cells they\n cover; `clear_values` empties values but keeps formatting.\n- `batch_update_spreadsheet` takes Google's own Request objects for\n everything else (formatting, sorting, deleting rows or sheets); it is\n atomic, always needs approval, and can destroy data. Replies too large\n to return come back cut to their kind, ids, and counts with a `note`;\n the requests still applied, so never send them again to see the rest.\n- Sheets has no revision check on writes: edits are last-writer-wins, so a\n person editing the same cells meanwhile is overwritten without warning.\n Read just before writing over anything a person may be editing.\n- Moving, sharing, or deleting a spreadsheet file is Drive's job.\n"
9
9
  }
10
10
  };
@@ -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";
@@ -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,