@zackbart/connecta 0.38.0 → 0.40.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 (180) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +3 -2
  3. package/bin/connecta.mjs +4 -8
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +6 -4
  6. package/dist/agent-routes.js +231 -0
  7. package/dist/auth/downstream-oauth/consent.js +519 -0
  8. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  9. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  10. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  11. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  12. package/dist/auth/downstream-oauth.js +66 -2382
  13. package/dist/auth/native.js +37 -0
  14. package/dist/auth/oauth-grant.js +13 -0
  15. package/dist/auth/static-oauth.js +153 -131
  16. package/dist/auth-elicitation.js +7 -1
  17. package/dist/authorization-handoff.js +11 -5
  18. package/dist/catalog-cache.js +219 -59
  19. package/dist/catalog-policy.js +18 -0
  20. package/dist/catalog-service.js +284 -47
  21. package/dist/catalog.js +18 -2
  22. package/dist/config.d.ts +16 -11
  23. package/dist/config.js +9 -6
  24. package/dist/connector-caller.d.ts +1 -1
  25. package/dist/connector-health.js +59 -0
  26. package/dist/connector-status.js +117 -0
  27. package/dist/connectors/api-connector.d.ts +7 -2
  28. package/dist/connectors/api-connector.js +7 -4
  29. package/dist/connectors/negotiation-cache.js +22 -4
  30. package/dist/connectors/remote-mcp/call.js +164 -0
  31. package/dist/connectors/remote-mcp/catalog.js +278 -0
  32. package/dist/connectors/remote-mcp/config.js +263 -0
  33. package/dist/connectors/remote-mcp/connection.js +682 -0
  34. package/dist/connectors/remote-mcp/errors.js +488 -0
  35. package/dist/connectors/remote-mcp/oauth.js +473 -0
  36. package/dist/connectors/remote-mcp/resources.js +473 -0
  37. package/dist/connectors/remote-mcp/runtime.js +1 -0
  38. package/dist/connectors/remote-mcp/transport.js +555 -0
  39. package/dist/connectors/remote-mcp.js +62 -2863
  40. package/dist/credential-contract.d.ts +23 -0
  41. package/dist/credential-reads.js +17 -0
  42. package/dist/credential-verification.js +53 -0
  43. package/dist/credentials.d.ts +7 -0
  44. package/dist/credentials.js +60 -25
  45. package/dist/describe-config.d.ts +12 -2
  46. package/dist/describe-config.js +2 -0
  47. package/dist/discovery-health.js +199 -0
  48. package/dist/downstream-input.js +2 -0
  49. package/dist/errors.d.ts +55 -17
  50. package/dist/errors.js +95 -41
  51. package/dist/execute.js +152 -114
  52. package/dist/executor-result.js +3 -1
  53. package/dist/guest-functions.js +83 -0
  54. package/dist/guest-runtime.js +60 -19
  55. package/dist/guest-types.d.ts +19 -5
  56. package/dist/http/argument-problems.js +35 -0
  57. package/dist/http/errors.js +5 -4
  58. package/dist/http/fields.js +192 -13
  59. package/dist/http/hatch-response.js +73 -0
  60. package/dist/http/hatch.js +88 -23
  61. package/dist/http/oauth.js +3 -2
  62. package/dist/http/provider.js +32 -4
  63. package/dist/http/safety.js +8 -0
  64. package/dist/http/tools.js +44 -23
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +9 -2
  67. package/dist/invocation.js +26 -8
  68. package/dist/membership.d.ts +13 -5
  69. package/dist/membership.js +34 -2
  70. package/dist/meta-tools.js +54 -31
  71. package/dist/native-recovery.js +4 -16
  72. package/dist/native-tools.js +5 -2
  73. package/dist/oauth-sealing.js +10 -0
  74. package/dist/operation-vocabulary.js +135 -13
  75. package/dist/operator-record.d.ts +2 -2
  76. package/dist/operator-record.js +1 -0
  77. package/dist/operator-ui/contract.d.ts +10 -0
  78. package/dist/operator-ui/generated.js +2 -2
  79. package/dist/operator-ui/model.d.ts +22 -1
  80. package/dist/provider-call-facts.js +6 -2
  81. package/dist/providers/_shared/rest/operation-index.js +48 -50
  82. package/dist/providers/_shared/rest/value-safety.js +80 -0
  83. package/dist/providers/breeze/api.js +7 -1
  84. package/dist/providers/breeze/skill.generated.js +1 -1
  85. package/dist/providers/breeze/tools.js +4 -3
  86. package/dist/providers/ccb/api.js +2 -1
  87. package/dist/providers/ccb/index.js +5 -0
  88. package/dist/providers/ccb/skill.generated.js +1 -1
  89. package/dist/providers/cloudflare/api.js +8 -1
  90. package/dist/providers/cloudflare/skill.generated.js +1 -1
  91. package/dist/providers/docs/lib/schemas.js +24 -22
  92. package/dist/providers/docs/safety.js +2 -0
  93. package/dist/providers/docs/skill.generated.js +1 -1
  94. package/dist/providers/docs/tools.js +2 -4
  95. package/dist/providers/drive/api.js +1 -0
  96. package/dist/providers/drive/lib/contracts.js +8 -5
  97. package/dist/providers/drive/skill.generated.js +1 -1
  98. package/dist/providers/drive/tools.js +11 -16
  99. package/dist/providers/forms/api.js +1 -0
  100. package/dist/providers/forms/skill.generated.js +1 -1
  101. package/dist/providers/forms/tools.js +10 -7
  102. package/dist/providers/github/api.js +1 -0
  103. package/dist/providers/github/lib/discovery.js +1 -1
  104. package/dist/providers/github/skill.generated.js +1 -1
  105. package/dist/providers/github/tools.js +61 -17
  106. package/dist/providers/gmail/lib/schemas.js +60 -50
  107. package/dist/providers/gmail/skill.generated.js +1 -1
  108. package/dist/providers/gmail/tools.js +7 -5
  109. package/dist/providers/granola/index.js +2 -0
  110. package/dist/providers/granola/skill.generated.js +1 -1
  111. package/dist/providers/overflow/api.js +7 -1
  112. package/dist/providers/overflow/skill.generated.js +1 -1
  113. package/dist/providers/planning-center/api.js +3 -1
  114. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  115. package/dist/providers/planning-center/skill.generated.js +1 -1
  116. package/dist/providers/quickbooks/safety.js +80 -39
  117. package/dist/providers/quickbooks/skill.generated.js +1 -1
  118. package/dist/providers/sheets/api.js +1 -0
  119. package/dist/providers/sheets/skill.generated.js +1 -1
  120. package/dist/providers/sheets/tools.js +13 -14
  121. package/dist/providers/slides/skill.generated.js +1 -1
  122. package/dist/providers/slides/tools.js +15 -13
  123. package/dist/providers/stripe/skill.generated.js +1 -1
  124. package/dist/providers/tithely/api.js +3 -1
  125. package/dist/providers/tithely/skill.generated.js +1 -1
  126. package/dist/providers/vercel/api.js +1 -1
  127. package/dist/providers/vercel/skill.generated.js +1 -1
  128. package/dist/providers/vercel/tools.js +4 -3
  129. package/dist/registry.d.ts +10 -4
  130. package/dist/registry.js +41 -13
  131. package/dist/routes/credentials.js +129 -60
  132. package/dist/routes/dispatch.js +5 -1
  133. package/dist/routes/mcp.js +30 -14
  134. package/dist/routes/operator.js +2 -2
  135. package/dist/routes/shared.d.ts +6 -1
  136. package/dist/routes/shared.js +7 -1
  137. package/dist/routes/ui-config.js +2 -2
  138. package/dist/routes/ui-data.js +126 -20
  139. package/dist/routes/ui.js +16 -0
  140. package/dist/runtime/call.js +7 -2
  141. package/dist/runtime/native-storage.js +12 -0
  142. package/dist/runtime/request-reads.js +207 -0
  143. package/dist/runtime/request-scope.js +16 -0
  144. package/dist/runtime/storage.js +12 -0
  145. package/dist/skills.js +37 -20
  146. package/dist/storage/keys.js +53 -3
  147. package/dist/storage/memory.js +8 -2
  148. package/dist/storage/sql-native.js +53 -1
  149. package/dist/storage/sql.js +12 -2
  150. package/dist/storage-digest.js +32 -0
  151. package/dist/timeout.js +8 -0
  152. package/dist/types.d.ts +33 -0
  153. package/dist/usage-guide.js +16 -35
  154. package/dist/validate.d.ts +3 -0
  155. package/dist/validate.js +220 -158
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/dist/write-outcome.js +0 -3
  159. package/documentation/README.md +13 -13
  160. package/documentation/architecture.md +385 -20
  161. package/documentation/auth.md +233 -64
  162. package/documentation/code-mode.md +122 -39
  163. package/documentation/deploying.md +6 -3
  164. package/documentation/integrating.md +16 -2
  165. package/documentation/meta-tools.md +240 -49
  166. package/documentation/migration-0.33.md +5 -1
  167. package/documentation/migration-0.34.md +3 -1
  168. package/documentation/operator-tests.md +1 -1
  169. package/documentation/operator-ui.md +105 -12
  170. package/documentation/pool-membership-upgrade.md +44 -0
  171. package/documentation/providers.md +105 -0
  172. package/examples/worker/AGENTS.md +10 -10
  173. package/examples/worker/README.md +4 -3
  174. package/examples/worker/src/connecta.config.ts +1 -0
  175. package/guides/connecting-agents.md +2 -1
  176. package/guides/how-it-works.md +4 -2
  177. package/package.json +1 -1
  178. package/templates/node/AGENTS.md +13 -12
  179. package/templates/node/package.json +1 -1
  180. package/templates/node/src/connecta.config.ts +1 -0
@@ -1,7 +1,8 @@
1
1
  // Effect declarations are the catalog and runtime decoder for every named Docs tool.
2
+ import { routeDescription } from "../../../agent-routes.js";
2
3
  import { Schema } from "effect";
3
4
  import { Integer, isMinChars, isMaxChars } from "../../../http/json-schema.js";
4
- import { fieldsInput } from "../../../http/fields.js";
5
+ import { fieldsContract } from "../../../http/fields.js";
5
6
  import { REQUEST_KINDS } from "./requests.js";
6
7
  const matches = (pattern) => Schema.makeFilter((value) => new RegExp(pattern).test(value), {
7
8
  expected: `a string matching ${pattern}`,
@@ -12,27 +13,6 @@ const Request = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema
12
13
  expected: "exactly one reviewed Docs request kind",
13
14
  toJsonSchema: () => ({ minProperties: 1, maxProperties: 1, propertyNames: { enum: [...REQUEST_KINDS] } }),
14
15
  }));
15
- export const GET_DOCUMENT_INPUT = Schema.Struct({
16
- "documentId": Schema.String.check(isMinChars(1), isMaxChars(256), matches("^[A-Za-z0-9_-]+$")).annotate({
17
- description: "Document id: the part after /d/ in its URL, or a Drive file id for a Google Doc.",
18
- }),
19
- "tabId": Schema.optionalKey(Schema.String.check(isMinChars(1), isMaxChars(128), matches("^[A-Za-z0-9._-]+$")).annotate({
20
- description: "Render only this tab (from a previous get_document); omit for every tab.",
21
- })),
22
- "maxChars": Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1000000)).annotate({
23
- description: "Characters of text kept across all rendered tabs, 0 to 1000000; defaults to 20000. Connecta's cap. Longer text ends with a truncation marker. Above the default, call directly (call_tool, connecta.result to page): execute_code carries at most 256 KiB per result.",
24
- })),
25
- "withIndexes": Schema.optionalKey(Schema.Boolean.annotate({
26
- description: "Add each tab's elements: every paragraph, table, and table of contents with UTF-16 startIndex and endIndex, 64 KiB per page (connecta's cap); page with cursor.",
27
- })),
28
- "cursor": Schema.optionalKey(Schema.String.check(isMinChars(1)).annotate({
29
- description: "Opaque page.nextCursor from the previous indexed read; keep the same arguments and document revision.",
30
- })),
31
- "suggestions": Schema.optionalKey(Schema.Literals(["inline", "accepted", "rejected"]).annotate({
32
- description: "How suggested edits read: inline (editors' default; the indexes edits use), or a preview with all accepted or all rejected (indexes not valid for edits).",
33
- })),
34
- fields: Schema.optionalKey(fieldsInput("Dot paths to keep in the result. Page and truncation metadata are always returned.")),
35
- });
36
16
  export const GET_DOCUMENT_OUTPUT = Schema.Struct({
37
17
  "documentId": Schema.optionalKey(Schema.String),
38
18
  "title": Schema.optionalKey(Schema.String),
@@ -60,6 +40,28 @@ export const GET_DOCUMENT_OUTPUT = Schema.Struct({
60
40
  "dropped": Schema.optionalKey(Schema.Array(Schema.String)),
61
41
  "truncated": Schema.optionalKey(Schema.Array(Schema.String)),
62
42
  });
43
+ export const GET_DOCUMENT_FIELDS = fieldsContract("result", GET_DOCUMENT_OUTPUT, "Page and truncation metadata are always returned.");
44
+ export const GET_DOCUMENT_INPUT = Schema.Struct({
45
+ "documentId": Schema.String.check(isMinChars(1), isMaxChars(256), matches("^[A-Za-z0-9_-]+$")).annotate({
46
+ description: "Document id: the part after /d/ in its URL, or a Drive file id for a Google Doc.",
47
+ }),
48
+ "tabId": Schema.optionalKey(Schema.String.check(isMinChars(1), isMaxChars(128), matches("^[A-Za-z0-9._-]+$")).annotate({
49
+ description: "Render only this tab (from a previous get_document); omit for every tab.",
50
+ })),
51
+ "maxChars": Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1000000)).annotate({
52
+ description: routeDescription((phrases) => `Characters of text kept across all rendered tabs, 0 to 1000000; defaults to 20000. Connecta's cap. Longer text ends with a truncation marker. Above the default, ${phrases.callDirectlyAndPage}${phrases.program ? `: ${phrases.program} carries at most 256 KiB per result` : ""}.`),
53
+ })),
54
+ "withIndexes": Schema.optionalKey(Schema.Boolean.annotate({
55
+ description: "Add each tab's elements: every paragraph, table, and table of contents with UTF-16 startIndex and endIndex, 64 KiB per page (connecta's cap); page with cursor.",
56
+ })),
57
+ "cursor": Schema.optionalKey(Schema.String.check(isMinChars(1)).annotate({
58
+ description: "Opaque page.nextCursor from the previous indexed read; keep the same arguments and document revision.",
59
+ })),
60
+ "suggestions": Schema.optionalKey(Schema.Literals(["inline", "accepted", "rejected"]).annotate({
61
+ description: "How suggested edits read: inline (editors' default; the indexes edits use), or a preview with all accepted or all rejected (indexes not valid for edits).",
62
+ })),
63
+ fields: Schema.optionalKey(GET_DOCUMENT_FIELDS.input),
64
+ });
63
65
  export const CREATE_DOCUMENT_INPUT = Schema.Struct({
64
66
  "title": Schema.String.check(isMinChars(1), isMaxChars(1000), matches("^[^\\r\\n]*$")).annotate({
65
67
  description: "Document title, one line. Connecta's 1,000-character cap.",
@@ -1,6 +1,7 @@
1
1
  import { Effect } from "effect";
2
2
  import { RequestSecrets } from "../../http/services.js";
3
3
  import { googleSafety } from "../_shared/google/kit.js";
4
+ import { pathWithholds } from "../_shared/rest/value-safety.js";
4
5
  /** ImageProperties.contentUri delegates the requester's image access for 30 minutes.
5
6
  * Keep ordinary authored links/sourceUri, but withhold that vendor capability on
6
7
  * every document response, including tabs, headers and suggested image changes.
@@ -29,6 +30,7 @@ export function docsSafety(operations) {
29
30
  };
30
31
  return visit(data);
31
32
  },
33
+ withholds: () => pathWithholds({ paths: ["imageProperties.contentUri"], anyDepth: true }),
32
34
  },
33
35
  };
34
36
  }
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## Find the document first\n\nDocument ids join Docs to Drive: take the part after `/d/` in a document URL,\nor find the file by name through a Google Drive connection. An id the person\nprovides works as is. Docs has no\naccount-data search or listing; its operation search discovers API methods.\nA missing document may be absent or inaccessible to the mapped Workspace user.\n\n## Read before editing\n\nThe rendering follows document tabs, including nested tabs. Headers, footers,\nfloating images and comments are not rendered; `notRendered` names omitted\nkinds. Suggested insertions and deletions can appear together without marks.\nAn accepted or rejected suggestions preview cannot supply edit indexes.\nIndexed rows page with `page.nextCursor`; keep the same arguments, starting\nwith `maxChars: 0` when only indexes are needed.\n\nUse the inline view's UTF-16 start/end indexes and revision to plan edits.\nIndexes shift after each edit, including earlier requests in the same atomic\nbatch. Apply independent deletions from the end backwards. A revision is a\nprecondition, never an idempotency key. Re-read and recompute after a revision\nconflict.\n\nStyles, named ranges, headers and footers live in the complete document\nresource. Read it with\n`docs_api_read({path:\"/documents/doc_123\",query:{includeTabsContent:true}})`\nwhen a rendering cannot answer the task. Paths omit `/v1`; the hatch reply is\n`{status,data}`, with the vendor document in `data` (a program call's\n`delivery.data.data`). A Docs\ncreate payload sets a title only; initial text is a separate edit. Appending\njoins the final paragraph, so begin with a newline when a new paragraph is\nintended. A broad text replacement can rewrite every tab.\nBatch kinds include `updateTextStyle` for formatting, `insertTable`,\n`createParagraphBullets`, and `deleteContentRange`.\n\n## Recover a partial create\n\nIf creation succeeded but initial text failed, keep the created document id.\nAppend the missing text directly only when the failure proves Google refused\nit. Otherwise read that document and add only what is missing. Creating again\nwould make another document. If the create reply lost its id, search Drive for\nthe title before attempting a fresh create.\n\nPreview comment and suggestion mutations are unavailable. Comments belong to\nDrive. Copying a template or choosing a destination folder also belongs to\nDrive; a Docs create starts in My Drive's root.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `docs_api_details`, `docs_api_search`, `get_document`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `docs_api_search` finds this API's operations and reads no account data, `docs_api_details` shows one operation's contract, and `docs_api_read` calls a reviewed read. `docs_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: A revision-guarded batch update returned ABORTED or FAILED_PRECONDITION with HTTP 400 or 409; The document content or revision changed during indexed content paging.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## Find the document first\n\nDocument ids join Docs to Drive: take the part after `/d/` in a document URL,\nor find the file by name through a Google Drive connection. An id the person\nprovides works as is. Docs has no\naccount-data search or listing; its operation search discovers API methods.\nA missing document may be absent or inaccessible to the mapped Workspace user.\n\n## Read before editing\n\nThe rendering follows document tabs, including nested tabs. Headers, footers,\nfloating images and comments are not rendered; `notRendered` names omitted\nkinds. Suggested insertions and deletions can appear together without marks.\nAn accepted or rejected suggestions preview cannot supply edit indexes.\nIndexed rows page with `page.nextCursor`; keep the same arguments, starting\nwith `maxChars: 0` when only indexes are needed.\n\nUse the inline view's UTF-16 start/end indexes and revision to plan edits.\nIndexes shift after each edit, including earlier requests in the same atomic\nbatch. Apply independent deletions from the end backwards. A revision is a\nprecondition, never an idempotency key. Re-read and recompute after a revision\nconflict.\n\nStyles, named ranges, headers and footers live in the complete document\nresource. Read it with\n`docs_api_read({path:\"/documents/doc_123\",query:{includeTabsContent:true}})`\nwhen a rendering cannot answer the task. Paths omit `/v1`; the hatch reply is\n`{status,data}`, with the vendor document in `data` (a program call's\n`delivery.data.data`). A Docs\ncreate payload sets a title only; initial text is a separate edit. Appending\njoins the final paragraph, so begin with a newline when a new paragraph is\nintended. A broad text replacement can rewrite every tab.\nBatch kinds include `updateTextStyle` for formatting, `insertTable`,\n`createParagraphBullets`, and `deleteContentRange`.\n\n## Recover a partial create\n\nIf creation succeeded but initial text failed, keep the created document id.\nAppend the missing text directly only when the failure proves Google refused\nit. Otherwise read that document and add only what is missing. Creating again\nwould make another document. If the create reply lost its id, search Drive for\nthe title before attempting a fresh create.\n\nPreview comment and suggestion mutations are unavailable. Comments belong to\nDrive. Copying a template or choosing a destination folder also belongs to\nDrive; a Docs create starts in My Drive's root.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `docs_api_details`, `docs_api_search`, `get_document`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `docs_api_search` finds this API's operations and reads no account data, `docs_api_details` shows one operation's contract, and `docs_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `docs_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: A revision-guarded batch update returned ABORTED or FAILED_PRECONDITION with HTTP 400 or 409; The document content or revision changed during indexed content paging.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Connection instructions"
5
5
  };
@@ -4,7 +4,7 @@ import { Effect, Option } from "effect";
4
4
  import { canonicalJson } from "../../canonical-json.js";
5
5
  import { ConnectorCallError } from "../../errors.js";
6
6
  import { Refusal } from "../../http/errors.js";
7
- import { fieldsProblem, selectFields } from "../../http/fields.js";
7
+ import { selectFields } from "../../http/fields.js";
8
8
  import { Recovery } from "../../http/services.js";
9
9
  import { requestSentSecrets, redactBeforeCut } from "../../sent-secrets.js";
10
10
  import { providerCallFailure } from "../../write-outcome.js";
@@ -515,9 +515,7 @@ export function docsTools({ connection }) {
515
515
  ...(tool.writeSteps ? { writeSteps: tool.writeSteps } : {}),
516
516
  run: (args) => Effect.gen(function* () {
517
517
  const fields = isRead ? args["fields"] : undefined;
518
- const problem = isRead
519
- ? fieldsProblem(fields, new Set(Object.keys(schemas.GET_DOCUMENT_OUTPUT.fields)), "the result")
520
- : undefined;
518
+ const problem = isRead ? schemas.GET_DOCUMENT_FIELDS.problem(fields) : undefined;
521
519
  if (problem)
522
520
  return yield* Effect.fail(new Refusal({ error: problem }));
523
521
  const operation = yield* Effect.serviceOption(Operation);
@@ -66,6 +66,7 @@ export const safety = {
66
66
  return { method: op.method, template: op.path };
67
67
  },
68
68
  redact: (data, op) => withholdDriveLinks(values.redact(data, op.method, op.template)),
69
+ withholds: (op) => values.withholds(op.method, op.template),
69
70
  withholdsErrors: (op) => values.withholdsErrors(driveIndex.ops.find((entry) => entry.method === op.method && entry.path === op.template)),
70
71
  },
71
72
  sensitiveFields: { names: [], choice: "drop" },
@@ -1,10 +1,13 @@
1
1
  // Drive named tool contracts, kept separate from the workflow programs.
2
+ import { routeDescription } from "../../../agent-routes.js";
2
3
  import { Schema } from "effect";
3
4
  import { Integer, isMinChars, isMaxChars } from "../../../http/json-schema.js";
4
5
  const matchesPattern = (pattern) => Schema.makeFilter((value) => pattern.test(value), {
5
6
  expected: `a string matching ${pattern.source}`,
6
7
  toJsonSchema: () => ({ pattern: pattern.source }),
7
8
  });
9
+ /** maxBytes on every read: a program receives the default; only a direct call raises it. */
10
+ const MAX_BYTES_DESCRIPTION = routeDescription((phrases) => `Serialized bytes of the whole result; defaults to 196608${phrases.program ? `, what ${phrases.program} can receive` : ""}. Raise it, to 4194304, ${phrases.onlyDirectRead}.`);
8
11
  export const search_files = {
9
12
  input: Schema.Struct({
10
13
  "query": Schema.optionalKey(Schema.String.check(isMaxChars(2048)).annotate({
@@ -35,7 +38,7 @@ export const search_files = {
35
38
  "description": "Opaque page.nextCursor from the previous page. Pass it back unchanged with the same arguments.",
36
39
  })),
37
40
  "maxBytes": Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(65536), Schema.isLessThanOrEqualTo(4194304)).annotate({
38
- "description": "Serialized bytes of the whole result; defaults to 196608, what execute_code can receive. Raise it, to 4194304, only for a direct call_tool read.",
41
+ "description": MAX_BYTES_DESCRIPTION,
39
42
  })),
40
43
  }),
41
44
  output: Schema.Struct({
@@ -81,7 +84,7 @@ export const list_folder_items = {
81
84
  "description": "Opaque page.nextCursor from the previous page. Pass it back unchanged with the same arguments.",
82
85
  })),
83
86
  "maxBytes": Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(65536), Schema.isLessThanOrEqualTo(4194304)).annotate({
84
- "description": "Serialized bytes of the whole result; defaults to 196608, what execute_code can receive. Raise it, to 4194304, only for a direct call_tool read.",
87
+ "description": MAX_BYTES_DESCRIPTION,
85
88
  })),
86
89
  }),
87
90
  output: Schema.Struct({
@@ -150,7 +153,7 @@ export const get_file_content = {
150
153
  "description": "Characters (code points) of text kept, 1 to 100000; defaults to 20000. Longer text ends with a truncation marker. Binaries ignore it.",
151
154
  })),
152
155
  "maxBytes": Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(65536), Schema.isLessThanOrEqualTo(4194304)).annotate({
153
- "description": "Serialized bytes of the whole result; defaults to 196608, what execute_code can receive. Raise it, to 4194304, only for a direct call_tool read.",
156
+ "description": MAX_BYTES_DESCRIPTION,
154
157
  })),
155
158
  }),
156
159
  output: Schema.Struct({
@@ -177,7 +180,7 @@ export const list_permissions = {
177
180
  "description": "Opaque page.nextCursor from the previous page. Pass it back unchanged with the same arguments.",
178
181
  })),
179
182
  "maxBytes": Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(65536), Schema.isLessThanOrEqualTo(4194304)).annotate({
180
- "description": "Serialized bytes of the whole result; defaults to 196608, what execute_code can receive. Raise it, to 4194304, only for a direct call_tool read.",
183
+ "description": MAX_BYTES_DESCRIPTION,
181
184
  })),
182
185
  }),
183
186
  output: Schema.Struct({
@@ -214,7 +217,7 @@ export const list_shared_drives = {
214
217
  "description": "Opaque page.nextCursor from the previous page. Pass it back unchanged with the same arguments.",
215
218
  })),
216
219
  "maxBytes": Schema.optionalKey(Integer.check(Schema.isGreaterThanOrEqualTo(65536), Schema.isLessThanOrEqualTo(4194304)).annotate({
217
- "description": "Serialized bytes of the whole result; defaults to 196608, what execute_code can receive. Raise it, to 4194304, only for a direct call_tool read.",
220
+ "description": MAX_BYTES_DESCRIPTION,
218
221
  })),
219
222
  }),
220
223
  output: Schema.Struct({
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## Finding files and joining Workspace products\n\nDrive searches file data; operation discovery searches API capabilities.\nUse file ids from reads, Drive URLs or the person: an id they give, such as\n`pdf1`, works as is. The same id identifies a Google Doc,\nSheet, Slides deck or Form in its product connector. Shared-drive ids identify\nmembership and the root folder; folder parents determine inherited access.\nA missing file and a file the delegated user cannot see fail alike.\n\nDrive query values use single quotes: `name contains 'budget'`,\n`mimeType = 'application/pdf'`, `'folder_123' in parents`, or\n`modifiedTime > '2026-01-01T00:00:00'`. Trashed files are excluded by default.\n`incompleteSearch: true` means Google did not search every corpus; narrow the\nquery or choose a single shared drive. It is never an exact count.\n\n`maxBytes` is the byte budget and `limit` is rows per page; keep a high limit\nfor large folders and continue with the same listing arguments. Named `fields`\nmay narrow and `maxBytes` may change between pages. Hatch paths omit `/drive/v3`.\n\n## Content and file decisions\n\nThe content workflow exports Docs as Markdown, Sheets as CSV of the first\nsheet only, and Slides as text. Other text files retain their declared charset.\nSmall binary files can be returned as base64. A drawing, unsupported native\nfile, folder, shortcut or oversized binary returns an unavailable explanation.\nFor a shortcut, read its target id instead. Drive exports at most 10 MB.\n\nContent containing sent credentials is withheld as a whole binary value; there\nis no partial-content recovery around that boundary. Withheld MIME, shortcut,\nfolder or parent metadata cannot decide a subsequent read or move. Re-read\nthrough a safe source before trying the workflow again.\n\n## Access and recovery\n\nA file or copy inherits its destination folder's access. Read permission holders\nbefore moving, copying or creating in a shared folder, and read them again after\nchanging access. Inherited permissions name the parent folder or shared drive\nthat must be changed instead. An owner cannot be removed or replaced here.\n\nAll new grants use `share_file`, which sends no notification email by default.\nConfirm intent before granting `anyone` or domain access. Permission creation\nthrough the hatch is refused, including public-link grants; the named workflow\nowns grantee validation and notification behavior.\n\nContent updates replace the complete content. A create with an uncertain outcome\nneeds a search for the intended file before another create. Recoverable trash\ncan be restored for 30 days. Permanent deletion, ownership transfer, watch\nregistration, credential minting and generic media transport are outside the\nhatch. Use the named content and upload workflows for bounded bytes.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `drive_api_details`, `drive_api_read`, `drive_api_search`, `list_folder_items`, `list_permissions`, `list_shared_drives`, `search_files`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `drive_api_search` finds this API's operations and reads no account data, `drive_api_details` shows one operation's contract, and `drive_api_read` calls a reviewed read. `drive_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 51 operations are refused by design; use `drive_api_details` with the method and path to inspect the original operation summary and its separate refusal. Use details for refused methods outside the read/write call schemas; those methods remain unavailable.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: A Drive source page changed before its local cursor resumed.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## Finding files and joining Workspace products\n\nDrive searches file data; operation discovery searches API capabilities.\nUse file ids from reads, Drive URLs or the person: an id they give, such as\n`pdf1`, works as is. The same id identifies a Google Doc,\nSheet, Slides deck or Form in its product connector. Shared-drive ids identify\nmembership and the root folder; folder parents determine inherited access.\nA missing file and a file the delegated user cannot see fail alike.\n\nDrive query values use single quotes: `name contains 'budget'`,\n`mimeType = 'application/pdf'`, `'folder_123' in parents`, or\n`modifiedTime > '2026-01-01T00:00:00'`. Trashed files are excluded by default.\n`incompleteSearch: true` means Google did not search every corpus; narrow the\nquery or choose a single shared drive. It is never an exact count.\n\n`maxBytes` is the byte budget and `limit` is rows per page; keep a high limit\nfor large folders and continue with the same listing arguments. Named `fields`\nmay narrow and `maxBytes` may change between pages. Hatch paths omit `/drive/v3`.\n\n## Content and file decisions\n\nThe content workflow exports Docs as Markdown, Sheets as CSV of the first\nsheet only, and Slides as text. Other text files retain their declared charset.\nSmall binary files can be returned as base64. A drawing, unsupported native\nfile, folder, shortcut or oversized binary returns an unavailable explanation.\nFor a shortcut, read its target id instead. Drive exports at most 10 MB.\n\nContent containing sent credentials is withheld as a whole binary value; there\nis no partial-content recovery around that boundary. Withheld MIME, shortcut,\nfolder or parent metadata cannot decide a subsequent read or move. Re-read\nthrough a safe source before trying the workflow again.\n\n## Access and recovery\n\nA file or copy inherits its destination folder's access. Read permission holders\nbefore moving, copying or creating in a shared folder, and read them again after\nchanging access. Inherited permissions name the parent folder or shared drive\nthat must be changed instead. An owner cannot be removed or replaced here.\n\nAll new grants use `share_file`, which sends no notification email by default.\nConfirm intent before granting `anyone` or domain access. Permission creation\nthrough the hatch is refused, including public-link grants; the named workflow\nowns grantee validation and notification behavior.\n\nContent updates replace the complete content. A create with an uncertain outcome\nneeds a search for the intended file before another create. Recoverable trash\ncan be restored for 30 days. Permanent deletion, ownership transfer, watch\nregistration, credential minting and generic media transport are outside the\nhatch. Use the named content and upload workflows for bounded bytes.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `drive_api_details`, `drive_api_read`, `drive_api_search`, `list_folder_items`, `list_permissions`, `list_shared_drives`, `search_files`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `drive_api_search` finds this API's operations and reads no account data, `drive_api_details` shows one operation's contract, and `drive_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `drive_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 51 operations are refused by design; use `drive_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: A Drive source page changed before its local cursor resumed.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Connection instructions"
5
5
  };
@@ -1,3 +1,4 @@
1
+ import { routePhrases } from "../../agent-routes.js";
1
2
  import { asRecord, asArray, compact } from "../_shared/json.js";
2
3
  import { decodeBase64, decodeBase64url } from "../../bytes.js";
3
4
  // Google Drive named workflows, bounded projections and bound continuation on the provider kit.
@@ -8,7 +9,7 @@ import { Classification, Dispatches, Signal } from "../../http/services.js";
8
9
  import { RESULT_BUDGET_BYTES, clampText, jsonBytes } from "../_shared/google/result-size.js";
9
10
  import { googleOutcomeOf, googleFailureMessage } from "../_shared/google/kit.js";
10
11
  import { read, write } from "../../http/tools.js";
11
- import { fieldsInput, fieldsProblem, selectFields } from "../../http/fields.js";
12
+ import { fieldsContract, selectFields } from "../../http/fields.js";
12
13
  import { catalogSchema } from "../../http/json-schema.js";
13
14
  import { Refusal } from "../../http/errors.js";
14
15
  import { Operation } from "../../runtime/call.js";
@@ -432,7 +433,7 @@ function capped(ctx, whole, max, partial, fits) {
432
433
  if (fits(byChars))
433
434
  return { content: byChars, contentTruncated: true };
434
435
  // The bytes bind first: the most characters that fit, by binary search.
435
- const raise = `maxBytes (up to ${MAX_RESULT_BYTES}, for a direct call_tool only)`;
436
+ const raise = `maxBytes (up to ${MAX_RESULT_BYTES}, ${routePhrases(ctx).directOnly})`;
436
437
  let low = 0;
437
438
  let high = Math.min(total, max);
438
439
  while (low < high) {
@@ -586,7 +587,7 @@ function readContent(client, ctx, fileId, maxChars, maxBytes) {
586
587
  format: "unavailable",
587
588
  contentTruncated: false,
588
589
  note: limit < BINARY_INLINE_BYTES
589
- ? `A binary file of ${bytes} does not fit this result's maxBytes (${maxBytes}), which holds ${limit} bytes of file; no content is returned. Raise maxBytes, up to ${MAX_RESULT_BYTES} for a direct call_tool only, to read up to ${BINARY_INLINE_BYTES} bytes, or open webViewLink.`
590
+ ? `A binary file of ${bytes} does not fit this result's maxBytes (${maxBytes}), which holds ${limit} bytes of file; no content is returned. Raise maxBytes, up to ${MAX_RESULT_BYTES} ${routePhrases(ctx).directOnly}, to read up to ${BINARY_INLINE_BYTES} bytes, or open webViewLink.`
590
591
  : `A binary file of ${bytes} is past this connection's ${BINARY_INLINE_BYTES}-byte inline cap; no content is returned. Open webViewLink.`,
591
592
  });
592
593
  // Metadata that already says too large saves the download; metadata that
@@ -1112,7 +1113,7 @@ function guarded(tool) {
1112
1113
  return result;
1113
1114
  const raisable = "maxBytes" in tool.contract.input.fields;
1114
1115
  throw new ConnectorCallError("connector_call_failed", tool.annotations.readOnlyHint
1115
- ? `This ${tool.name} result serializes to ${bytes} bytes, past ${raisable ? `this call's maxBytes of ${budget}` : `the ${budget} bytes one result may carry`}; nothing is returned. ${raisable ? `Raise maxBytes (up to ${MAX_RESULT_BYTES}, for a direct call_tool only), or narrow the request.` : "Read the item in Drive instead."}`
1116
+ ? `This ${tool.name} result serializes to ${bytes} bytes, past ${raisable ? `this call's maxBytes of ${budget}` : `the ${budget} bytes one result may carry`}; nothing is returned. ${raisable ? `Raise maxBytes (up to ${MAX_RESULT_BYTES}, ${routePhrases(ctx).directOnly}), or narrow the request.` : "Read the item in Drive instead."}`
1116
1117
  : `${tool.name} applied, but its result serializes to ${bytes} bytes, past the ${budget} bytes one result may carry, so it is not returned. Re-read the item with get_file; do not repeat the write.`, { retryable: false });
1117
1118
  }),
1118
1119
  };
@@ -1122,16 +1123,10 @@ function buildTool(connection, tool) {
1122
1123
  const isList = ["search_files", "list_folder_items", "list_permissions", "list_shared_drives"].includes(tool.name);
1123
1124
  const output = asRecord(catalogSchema(tool.contract.output, "output"));
1124
1125
  const row = isList ? asRecord(asRecord(asRecord(output["properties"])["items"])["items"]) : output;
1125
- const roots = new Set(Object.keys(asRecord(row["properties"])));
1126
+ const fields = isRead ? fieldsContract(isList ? "item" : "result", row) : undefined;
1126
1127
  const input = Schema.Struct({
1127
1128
  ...tool.contract.input.fields,
1128
- ...(isRead
1129
- ? {
1130
- fields: Schema.optionalKey(fieldsInput(isList
1131
- ? "Dot paths to keep in each item; only narrows the result."
1132
- : "Dot paths to keep in the result; only narrows the result.")),
1133
- }
1134
- : {}),
1129
+ ...(fields ? { fields: Schema.optionalKey(fields.input) } : {}),
1135
1130
  });
1136
1131
  const spec = {
1137
1132
  name: tool.name,
@@ -1140,8 +1135,8 @@ function buildTool(connection, tool) {
1140
1135
  output: tool.contract.output,
1141
1136
  ...(tool.writeSteps ? { writeSteps: tool.writeSteps } : {}),
1142
1137
  run: (args) => Effect.gen(function* () {
1143
- const fields = args["fields"];
1144
- const problem = isRead ? fieldsProblem(fields, roots, isList ? "an item" : "the result") : undefined;
1138
+ const selected = args["fields"];
1139
+ const problem = fields?.problem(selected);
1145
1140
  if (problem)
1146
1141
  return yield* Effect.fail(new Refusal({ error: problem }));
1147
1142
  const active = yield* Effect.serviceOption(Operation);
@@ -1161,9 +1156,9 @@ function buildTool(connection, tool) {
1161
1156
  ? isList
1162
1157
  ? {
1163
1158
  ...result,
1164
- items: asArray(result["items"]).map((item) => retainTruncated(item, selectFields(item, fields))),
1159
+ items: asArray(result["items"]).map((item) => retainTruncated(item, selectFields(item, selected))),
1165
1160
  }
1166
- : retainTruncated(result, selectFields(result, fields))
1161
+ : retainTruncated(result, selectFields(result, selected))
1167
1162
  : result;
1168
1163
  }).pipe(Effect.catchDefect((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error))),
1169
1164
  };
@@ -55,6 +55,7 @@ export const safety = {
55
55
  return { method: op.method, template: op.path };
56
56
  },
57
57
  redact: (data, op) => values.redact(data, op.method, op.template),
58
+ withholds: (op) => values.withholds(op.method, op.template),
58
59
  withholdsErrors: (op) => values.withholdsErrors(formsIndex.ops.find((entry) => entry.method === op.method && entry.path === op.template)),
59
60
  },
60
61
  sensitiveFields: { names: [], choice: "drop" },
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## Finding forms and joining answers\n\nThe Forms API cannot list or search forms. Find the file through Drive, or use\nthe id after `/forms/d/` in its edit URL. The `/forms/d/e/…` responder link is\nnot a form id. A form id is also its Drive file id.\n\nA form contains items; an item may ask one question, several grid questions,\nor no question. Responses join by `questionId`, never by `itemId`. Named\nresponse reads label answers with the current question titles in form order;\nan answer to a deleted question has no title. A response may contain several\nvalues or uploaded-file references for one question. File ids join to Drive.\n\n## Reading complete responses\n\nResponses and respondent emails can contain personal information. Filter and\naggregate in `execute_code`, returning named answers only when the task needs\nthem. `submittedAfter` is exclusive UTC: retain the newest `lastSubmittedTime`\nfor the next incremental read. Google does not promise a stable response\nsnapshot. A continuation inside a changed vendor page fails `conflict`; start\nagain or use that newest timestamp.\n\nLarge grids continue inside the same item: `questionsFrom` places the returned\nrows. `omittedQuestionIds` identifies answers left out for size; read those ids\nwith `get_response`. The structural hatch exposes quiz grading and correct\nanswers, images, and option navigation that named projections omit. Temporary\nimage access destinations remain withheld.\n\n## Editing without losing structure\n\nRead the form's current revision before editing. A stale revision refuses the\nbatch as `conflict`; re-read and rebuild the requested edit. A revision guards\nconcurrent changes and does not make a repeated write safe.\n\nCreation sets only title and Drive file name. Add descriptions or questions\nafterward. `updateItem` can replace grading, images and section navigation;\nread the full item through the hatch first and use a precise `updateMask`.\nItem locations are indexes, while response answers use question ids.\n\nPublishing, sharing, deleting forms, and watch administration are outside this\nconnection. Responses are read-only; no tool submits or deletes a response.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `forms_api_details`, `forms_api_read`, `forms_api_search`, `get_form`, `list_responses`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `forms_api_search` finds this API's operations and reads no account data, `forms_api_details` shows one operation's contract, and `forms_api_read` calls a reviewed read. `forms_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 5 operations are refused by design; use `forms_api_details` with the method and path to inspect the original operation summary and its separate refusal. Use details for refused methods outside the read/write call schemas; those methods remain unavailable.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: A revision-guarded batch update returned ABORTED or FAILED_PRECONDITION with HTTP 400 or 409.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## Finding forms and joining answers\n\nThe Forms API cannot list or search forms. Find the file through Drive, or use\nthe id after `/forms/d/` in its edit URL. The `/forms/d/e/…` responder link is\nnot a form id. A form id is also its Drive file id.\n\nA form contains items; an item may ask one question, several grid questions,\nor no question. Responses join by `questionId`, never by `itemId`. Named\nresponse reads label answers with the current question titles in form order;\nan answer to a deleted question has no title. A response may contain several\nvalues or uploaded-file references for one question. File ids join to Drive.\n\n## Reading complete responses\n\nResponses and respondent emails can contain personal information. Filter and\naggregate in `execute_code`, returning named answers only when the task needs\nthem. `submittedAfter` is exclusive UTC: retain the newest `lastSubmittedTime`\nfor the next incremental read. Google does not promise a stable response\nsnapshot. A continuation inside a changed vendor page fails `conflict`; start\nagain or use that newest timestamp.\n\nLarge grids continue inside the same item: `questionsFrom` places the returned\nrows. `omittedQuestionIds` identifies answers left out for size; read those ids\nwith `get_response`. The structural hatch exposes quiz grading and correct\nanswers, images, and option navigation that named projections omit. Temporary\nimage access destinations remain withheld.\n\n## Editing without losing structure\n\nRead the form's current revision before editing. A stale revision refuses the\nbatch as `conflict`; re-read and rebuild the requested edit. A revision guards\nconcurrent changes and does not make a repeated write safe.\n\nCreation sets only title and Drive file name. Add descriptions or questions\nafterward. `updateItem` can replace grading, images and section navigation;\nread the full item through the hatch first and use a precise `updateMask`.\nItem locations are indexes, while response answers use question ids.\n\nPublishing, sharing, deleting forms, and watch administration are outside this\nconnection. Responses are read-only; no tool submits or deletes a response.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `forms_api_details`, `forms_api_read`, `forms_api_search`, `get_form`, `list_responses`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `forms_api_search` finds this API's operations and reads no account data, `forms_api_details` shows one operation's contract, and `forms_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `forms_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 5 operations are refused by design; use `forms_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: A revision-guarded batch update returned ABORTED or FAILED_PRECONDITION with HTTP 400 or 409.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Connection instructions"
5
5
  };
@@ -8,7 +8,7 @@ import { encodeForAgent, redactBeforeCut, redactSentSecrets } from "../../sent-s
8
8
  import { Integer, isMaxChars, isMinChars } from "../../http/json-schema.js";
9
9
  import { boundCursor, StandardPage } from "../../http/paging.js";
10
10
  import { read, write } from "../../http/tools.js";
11
- import { fieldsInput, fieldsProblem, selectFields } from "../../http/fields.js";
11
+ import { fieldsContract, selectFields } from "../../http/fields.js";
12
12
  import { Refusal } from "../../http/errors.js";
13
13
  import { Operation } from "../../runtime/call.js";
14
14
  import { formsIndex, FORMS_REQUEST_KINDS } from "./lib/hatch.js";
@@ -551,6 +551,9 @@ const BatchRecord = optional({
551
551
  replyCount: Integer,
552
552
  createdItemCount: Integer,
553
553
  });
554
+ const FORM_FIELDS = fieldsContract("result", FormRecord, "Page and cut metadata are preserved.");
555
+ const RESPONSE_FIELDS = fieldsContract("result", ResponseRecord, "Cut and recovery metadata are preserved.");
556
+ const RESPONSE_ITEM_FIELDS = fieldsContract("item", ResponseRecord, "It only narrows safe data.");
554
557
  const contextOf = Effect.gen(function* () {
555
558
  const operation = yield* Effect.serviceOption(Operation);
556
559
  if (Option.isNone(operation) || !operation.value)
@@ -609,12 +612,12 @@ export function formsTools({ connection }) {
609
612
  input: Schema.Struct({
610
613
  formId,
611
614
  cursor,
612
- fields: Schema.optionalKey(fieldsInput("Dot paths to keep in the form; page and cut metadata is preserved.")),
615
+ fields: Schema.optionalKey(FORM_FIELDS.input),
613
616
  }),
614
617
  output: FormRecord,
615
618
  run: (args) => Effect.gen(function* () {
616
619
  const ctx = yield* contextOf;
617
- const problem = fieldsProblem(args.fields, new Set(Object.keys(FormRecord.fields)), "the form");
620
+ const problem = FORM_FIELDS.problem(args.fields);
618
621
  if (problem)
619
622
  return yield* Effect.fail(new Refusal({ error: problem }));
620
623
  const formId = args["formId"];
@@ -764,7 +767,7 @@ export function formsTools({ connection }) {
764
767
  name: "list_responses",
765
768
  description: "List a Google Form's submitted responses, each answer labeled with its question's title in form order. Long answers are cut; get_response reads one whole.",
766
769
  input: Schema.Struct({
767
- fields: Schema.optionalKey(fieldsInput("Dot paths to keep in each response item; only narrows safe data.")),
770
+ fields: Schema.optionalKey(RESPONSE_ITEM_FIELDS.input),
768
771
  formId,
769
772
  submittedAfter: Schema.optionalKey(Schema.String.check(pattern("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?Z$")).annotate({
770
773
  description: "Exclusive submitted-after UTC instant, e.g. 2026-10-01T00:00:00Z.",
@@ -783,7 +786,7 @@ export function formsTools({ connection }) {
783
786
  writeSteps: pairedReads,
784
787
  run: (args) => Effect.gen(function* () {
785
788
  const ctx = yield* contextOf;
786
- const problem = fieldsProblem(args.fields, new Set(Object.keys(ResponseRecord.fields)), "a response item");
789
+ const problem = RESPONSE_ITEM_FIELDS.problem(args.fields);
787
790
  if (problem)
788
791
  return yield* Effect.fail(new Refusal({ error: problem }));
789
792
  const formId = args["formId"];
@@ -918,13 +921,13 @@ export function formsTools({ connection }) {
918
921
  questionIds: Schema.optionalKey(Schema.Array(id("Question id from get_form or omittedQuestionIds."))
919
922
  .check(Schema.isMinLength(1), Schema.isMaxLength(MAX_QUESTION_IDS), Schema.isUnique())
920
923
  .annotate({ description: "Return only these questions for a large response." })),
921
- fields: Schema.optionalKey(fieldsInput("Dot paths to keep in the response; cut and recovery metadata is preserved.")),
924
+ fields: Schema.optionalKey(RESPONSE_FIELDS.input),
922
925
  }),
923
926
  output: ResponseRecord,
924
927
  writeSteps: pairedReads,
925
928
  run: (args) => Effect.gen(function* () {
926
929
  const ctx = yield* contextOf;
927
- const problem = fieldsProblem(args.fields, new Set(Object.keys(ResponseRecord.fields)), "the response");
930
+ const problem = RESPONSE_FIELDS.problem(args.fields);
928
931
  if (problem)
929
932
  return yield* Effect.fail(new Refusal({ error: problem }));
930
933
  const formId = args["formId"];
@@ -112,6 +112,7 @@ export function githubConnection() {
112
112
  return { method: op.method, template: op.path };
113
113
  },
114
114
  redact: (data, op) => githubSafety.redact(data, op.method, op.template),
115
+ withholds: (op) => githubSafety.withholds(op.method, op.template),
115
116
  withholdsErrors: () => true,
116
117
  },
117
118
  sensitiveFields: { names: [], choice: "drop" },
@@ -42,7 +42,7 @@ class RepositoryIndex extends OperationIndex {
42
42
  if (method === "GET" &&
43
43
  error instanceof ConnectorCallError &&
44
44
  error.code === "invalid_args" &&
45
- error.message.includes(" operation in the pinned API index ") &&
45
+ error.message.includes(" operation at that path in the pinned API index ") &&
46
46
  /^\/repos\/[^/]+\/[^/]+\/(?:commits|compare)\/[^/]+\/.+/.test(path))
47
47
  throw new ConnectorCallError("invalid_args", `${error.message} Encode '/' inside refs as %2F.`);
48
48
  throw error;
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## Choose the target\n\nAct as the configured GitHub App. `list_scopes` identifies its configured grants\nand reachable repositories. An exact repository grant can narrow its parent\norganization grant. Organization access covers repository operations, including\nfuture repositories in the installation, rather than organization administration.\nA repository rename keeps its ID-bound grant; a replacement at the old name does\nnot inherit it. A transfer cannot use the old installation's token.\n\nUse `search_scoped` for literal-word data search across installations. Each item\ncarries its partition and verified repository. Search completeness can be false\nat GitHub's search limit; narrow the selected scopes or terms when that matters.\nInstallation tokens do not override GitHub's own code-search restrictions.\nKeep the same `limit` with every named-list cursor. Omission uses 30, so a\ncursor issued with limit 30 can continue without an explicit limit.\n\n## Join the right identifiers\n\nAn issue or pull request number belongs to one owner/repository pair; a numeric\nrecord ID is a different identifier. An issue-list response can contain pull\nrequests: the `pull_request` property distinguishes them. Issue IDs returned for\npull requests are not necessarily the PR IDs from the pull-request endpoint.\nReviews join by pull request number; their `commit_id` identifies the reviewed\nrevision. Compare review and head commit IDs when deciding whether feedback is\ncurrent. Issue comments and pull-request review comments are different resources.\n\nRead files at an explicit Git ref when a stable revision matters. Nested contents\npaths are relative: `/repos/alice/widget/contents/lib/index.ts` reads that file,\nand `/repos/alice/widget/contents` lists the root directory. No dot segments,\nempty segments, backslashes or encoded traversal are accepted. In `commits/{ref}`\nand `compare/{basehead}`, encode branch-name slashes as `%2F`, for example\n`commits/feature%2Fx` or `compare/main...feature%2Fx`. Refs stay inside the selected\nrepository; owner-prefixed object refs and cross-repository compares are unavailable.\nFor a PR list, use `head:\"alice:feature/x\"`: the required `login:branch` filter\nselects this repository's PRs and never reads the login's repository. A bare\n`head` branch is refused because GitHub silently ignores it. `base` stays a branch name.\nContents are\nbase64 encoded; decode the inline content before interpreting it. Directory\nlistings have a vendor cap and are not recursive. Missing inline content or a\nmissing patch does not establish an empty file or an unchanged diff. Raw diff,\nlog/archive downloads, secret/admin operations and arbitrary GraphQL are unavailable.\n\n## Release and merge semantics\n\nPublishing a draft release updates that release's `draft` to false. Deleting a\nrelease does not delete its Git tag. Resolve the intended repository and existing\nrelease before changing it; a tag name is not a numeric release ID.\n\nUse `merge_pull_request` for merges. It requires the expected head SHA and a\nsame-repository head and base, plus explicit workflow-write access. GitHub's\natomic SHA guard binds the head rather than the base: a concurrent base retarget\ncan introduce workflow changes after the preflight. A response with `merged:false`\nis a failed merge, not a completed one. Inspect current PR state before a new attempt.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `github_api_details`, `github_api_read`, `github_api_search`, `list_scopes`, `search_scoped`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 30, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `github_api_search` finds this API's operations and reads no account data, `github_api_details` shows one operation's contract, and `github_api_read` calls a reviewed read. `github_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 5 operations are refused by design; use `github_api_details` with the method and path to inspect the original operation summary and its separate refusal. Use details for refused methods outside the read/write call schemas; those methods remain unavailable.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: HTTP 409 is a conflict; a merged:false response and the expected-SHA/same-repository preflight also refuse a merge.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## Choose the target\n\nAct as the configured GitHub App. `list_scopes` identifies its configured grants\nand reachable repositories. An exact repository grant can narrow its parent\norganization grant. Organization access covers repository operations, including\nfuture repositories in the installation, rather than organization administration.\nA repository rename keeps its ID-bound grant; a replacement at the old name does\nnot inherit it. A transfer cannot use the old installation's token.\n\nUse `search_scoped` for literal-word data search across installations. Each item\ncarries its partition and verified repository. Search completeness can be false\nat GitHub's search limit; narrow the selected scopes or terms when that matters.\nInstallation tokens do not override GitHub's own code-search restrictions.\nKeep the same `limit` with every named-list cursor. Omission uses 30, so a\ncursor issued with limit 30 can continue without an explicit limit.\n\n## Join the right identifiers\n\nAn issue or pull request number belongs to one owner/repository pair; a numeric\nrecord ID is a different identifier. An issue-list response can contain pull\nrequests: the `pull_request` property distinguishes them. Issue IDs returned for\npull requests are not necessarily the PR IDs from the pull-request endpoint.\nReviews join by pull request number; their `commit_id` identifies the reviewed\nrevision. Compare review and head commit IDs when deciding whether feedback is\ncurrent. Issue comments and pull-request review comments are different resources.\n\nRead files at an explicit Git ref when a stable revision matters. Nested contents\npaths are relative: `/repos/alice/widget/contents/lib/index.ts` reads that file,\nand `/repos/alice/widget/contents` lists the root directory. No dot segments,\nempty segments, backslashes or encoded traversal are accepted. In `commits/{ref}`\nand `compare/{basehead}`, encode branch-name slashes as `%2F`, for example\n`commits/feature%2Fx` or `compare/main...feature%2Fx`. Refs stay inside the selected\nrepository; owner-prefixed object refs and cross-repository compares are unavailable.\nFor a PR list, use `head:\"alice:feature/x\"`: the required `login:branch` filter\nselects this repository's PRs and never reads the login's repository. A bare\n`head` branch is refused because GitHub silently ignores it. `base` stays a branch name.\nContents are\nbase64 encoded; decode the inline content before interpreting it. Directory\nlistings have a vendor cap and are not recursive. Missing inline content or a\nmissing patch does not establish an empty file or an unchanged diff. Raw diff,\nlog/archive downloads, secret/admin operations and arbitrary GraphQL are unavailable.\n\n## Release and merge semantics\n\nPublishing a draft release updates that release's `draft` to false. Deleting a\nrelease does not delete its Git tag. Resolve the intended repository and existing\nrelease before changing it; a tag name is not a numeric release ID.\n\nUse `merge_pull_request` for merges. It requires the expected head SHA and a\nsame-repository head and base, plus explicit workflow-write access. GitHub's\natomic SHA guard binds the head rather than the base: a concurrent base retarget\ncan introduce workflow changes after the preflight. A response with `merged:false`\nis a failed merge, not a completed one. Inspect current PR state before a new attempt.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `github_api_details`, `github_api_read`, `github_api_search`, `list_scopes`, `search_scoped`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 30, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `github_api_search` finds this API's operations and reads no account data, `github_api_details` shows one operation's contract, and `github_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `github_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 5 operations are refused by design; use `github_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. `conflict` means: HTTP 409 is a conflict; a merged:false response and the expected-SHA/same-repository preflight also refuse a merge.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Repository instructions"
5
5
  };
@@ -3,7 +3,7 @@ import { api } from "../../connectors/api.js";
3
3
  import { deriveContext } from "../../derived-context.js";
4
4
  import { ConnectorCallError } from "../../errors.js";
5
5
  import { Refusal } from "../../http/errors.js";
6
- import { fieldsInput, fieldsProblem, selectFields } from "../../http/fields.js";
6
+ import { fieldsContract, selectFields } from "../../http/fields.js";
7
7
  import { hatch } from "../../http/hatch.js";
8
8
  import { githubPage } from "../../http/hatch-paging.js";
9
9
  import { Integer, isMinChars } from "../../http/json-schema.js";
@@ -44,10 +44,9 @@ const common = {
44
44
  default: 30,
45
45
  })),
46
46
  cursor: Schema.optionalKey(text("Opaque page.nextCursor from this same search or scope listing.")),
47
- fields: Schema.optionalKey(fieldsInput("Dot paths into each item; only narrows already-safe data.")),
48
47
  };
49
- const scopeNames = new Set(["org", "repo", "owner", "id", "full_name", "access", "workflows"]);
50
- const searchNames = new Set(["scope", "repository", "item"]);
48
+ const SCOPE_FIELDS = fieldsContract("item", ScopeRow, "It only narrows already-safe data.");
49
+ const SEARCH_FIELDS = fieldsContract("item", SearchRow, "Vendor fields are under item, e.g. item.title; it only narrows already-safe data.", "an item (vendor fields are under item)");
51
50
  const contextOf = Effect.gen(function* () {
52
51
  const op = yield* Effect.serviceOption(Operation);
53
52
  if (Option.isNone(op) || !op.value)
@@ -74,6 +73,36 @@ function selectors(input, policy) {
74
73
  return { repo: `${t.owner}/${t.repo}`, access: grant.access };
75
74
  });
76
75
  }
76
+ const SEARCH_TERMS = /^[a-z0-9_. -]{1,128}$/i;
77
+ const BOOLEAN_TERMS = /\b(?:OR|NOT|AND)\b/i;
78
+ /**
79
+ * Record every selector problem by shape alone: a refusal never repeats a
80
+ * submitted org or repository, and list_scopes names the configured grants.
81
+ */
82
+ function selectorIssues(problems, input, policy) {
83
+ input?.forEach((item, index) => {
84
+ const value = row(item), at = `/scopes/${index}`;
85
+ if ((value.org === undefined) === (value.repo === undefined))
86
+ return problems.add(at, 'exactly one key: {"org": "<org>"} for an org-wide grant or {"repo": "<owner>/<repo>"}', value.org === undefined ? "neither org nor repo" : "both org and repo");
87
+ let admitted = false;
88
+ try {
89
+ if (value.org !== undefined)
90
+ admitted = policy.scopes.some((scope) => scope.org === policy.owner(value.org));
91
+ else {
92
+ const parts = typeof value.repo === "string" ? value.repo.split("/") : [];
93
+ admitted = !!policy.scope(target(parts[0], parts.length === 2 ? parts[1] : undefined));
94
+ }
95
+ }
96
+ catch (error) {
97
+ if (!(error instanceof ConnectorCallError))
98
+ throw error;
99
+ }
100
+ if (!admitted)
101
+ problems.add(`${at}/${value.org !== undefined ? "org" : "repo"}`, value.org !== undefined
102
+ ? "an organization configured for org-wide access; list_scopes names them"
103
+ : '"<owner>/<repo>" of a configured repository or one in a configured org; list_scopes names them');
104
+ });
105
+ }
77
106
  function searchPartitions(scopes) {
78
107
  const orgs = new Set(scopes.flatMap((scope) => (scope.org ? [scope.org] : [])));
79
108
  const repos = new Map();
@@ -232,18 +261,30 @@ export function restConnector(id, auth, policy) {
232
261
  throw new ConnectorCallError("invalid_args", `This cursor was issued with limit ${position.limit}; repeat limit ${position.limit} or start a new listing.`);
233
262
  return position;
234
263
  };
235
- const namesCheck = (fields, names) => checked(() => {
236
- const bad = fieldsProblem(fields, names, "an item");
237
- if (bad)
238
- throw bad;
239
- });
240
264
  const scopesTool = read(connection, {
241
265
  name: "list_scopes",
242
266
  description: "List configured GitHub grants locally, or verified reachable repositories for one configured owner. Without owner this reads configuration without network access.",
243
- input: Schema.Struct({ owner: Schema.optionalKey(text("Configured owner; omit for local grants.")), ...common }),
267
+ input: Schema.Struct({
268
+ owner: Schema.optionalKey(text("Configured owner; omit for local grants.")),
269
+ ...common,
270
+ fields: Schema.optionalKey(SCOPE_FIELDS.input),
271
+ }),
244
272
  output: Schema.Struct({ items: Schema.Array(ScopeRow), page: StandardPage }),
273
+ check: (args, problems) => {
274
+ SCOPE_FIELDS.issues(problems, args.fields);
275
+ if (args.owner === undefined)
276
+ return;
277
+ try {
278
+ policy.owner(args.owner);
279
+ }
280
+ catch (error) {
281
+ if (!(error instanceof ConnectorCallError))
282
+ throw error;
283
+ // Without owner, the listing reads the configured grants locally.
284
+ problems.add("/owner", "a configured owner; omit owner to list the configured grants");
285
+ }
286
+ },
245
287
  run: (args) => Effect.gen(function* () {
246
- yield* namesCheck(args.fields, scopeNames);
247
288
  const limit = args.limit ?? 30, normalized = { ...args, limit };
248
289
  const raw = yield* decode("list_scopes", normalized);
249
290
  const position = yield* checked(() => parse(raw, limit));
@@ -309,15 +350,18 @@ export function restConnector(id, auth, policy) {
309
350
  .check(Schema.isMinLength(1), Schema.isMaxLength(100))
310
351
  .annotate({ description: "Optional subset of the configured scopes." })),
311
352
  ...common,
353
+ fields: Schema.optionalKey(SEARCH_FIELDS.input),
312
354
  }),
313
355
  output: Schema.Struct({ items: Schema.Array(SearchRow), page: StandardPage, incomplete_results: Schema.Boolean }),
356
+ check: (args, problems) => {
357
+ // Rows wrap the vendor's record under item, so a vendor field is item.<field>.
358
+ SEARCH_FIELDS.issues(problems, args.fields);
359
+ if (!SEARCH_TERMS.test(args.terms) || BOOLEAN_TERMS.test(args.terms))
360
+ problems.add("/terms", "literal words of letters, digits, spaces, _ . or -, at most 128 characters, without qualifiers such as is:open or label:x and without OR, AND or NOT; kind and scopes select the rest");
361
+ selectorIssues(problems, args.scopes, policy);
362
+ },
314
363
  run: (args) => Effect.gen(function* () {
315
- yield* namesCheck(args.fields, searchNames);
316
- const selected = yield* checked(() => {
317
- if (!/^[a-z0-9_. -]{1,128}$/i.test(args.terms) || /\b(?:OR|NOT|AND)\b/i.test(args.terms))
318
- throw new ConnectorCallError("invalid_args", "Search terms must be literal words, without qualifiers or Boolean operators.");
319
- return args.scopes === undefined ? policy.scopes : selectors(args.scopes, policy);
320
- });
364
+ const selected = yield* checked(() => args.scopes === undefined ? policy.scopes : selectors(args.scopes, policy));
321
365
  const partitions = yield* checked(() => searchPartitions(selected)), limit = args.limit ?? 30, normalized = { ...args, limit };
322
366
  const raw = yield* decode("search_scoped", normalized), at = yield* checked(() => parse(raw, limit));
323
367
  const partitionIndex = at.partition ?? 0, page = at.page ?? 1;