@zackbart/connecta 0.30.0 → 0.32.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.
- package/CHANGELOG.md +66 -0
- package/README.md +21 -16
- package/bin/connecta.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-recovery.js +25 -0
- package/dist/catalog-service.js +7 -2
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/config-value-sources.js +6 -3
- package/dist/config.d.ts +26 -8
- package/dist/config.js +38 -16
- package/dist/connector-access.d.ts +2 -1
- package/dist/connectors/guarded-fetch.d.ts +18 -1
- package/dist/connectors/guarded-fetch.js +21 -2
- package/dist/describe-config.d.ts +6 -3
- package/dist/describe-config.js +3 -2
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.js +4 -3
- package/dist/invocation.js +39 -4
- package/dist/meta-tools.js +17 -19
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -0
- package/dist/operator-ui/contract.d.ts +5 -4
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -1
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +12 -0
- package/dist/program-writes.js +4 -49
- package/dist/provider.d.ts +6 -5
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/google/workspace.js +1 -1
- package/dist/providers/_shared/rest/dispatch.js +41 -0
- package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
- package/dist/providers/_shared/rest/operation-index.js +527 -0
- package/dist/providers/_shared/rest/tools.d.ts +141 -0
- package/dist/providers/_shared/rest/tools.js +569 -0
- package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
- package/dist/providers/_shared/rest/value-safety.js +580 -0
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/cloudflare/graphql.js +385 -0
- package/dist/providers/cloudflare/index.d.ts +47 -51
- package/dist/providers/cloudflare/index.js +188 -1997
- package/dist/providers/cloudflare/openapi.generated.js +3347 -0
- package/dist/providers/cloudflare/rest.d.ts +37 -0
- package/dist/providers/cloudflare/rest.js +1056 -0
- package/dist/providers/cloudflare/skill.generated.js +7 -4
- package/dist/providers/cloudflare/value-safety.js +1247 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +274 -23
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +35 -31
- package/dist/providers/notion/index.js +76 -1499
- package/dist/providers/notion/openapi.generated.js +75 -0
- package/dist/providers/notion/rest.d.ts +24 -0
- package/dist/providers/notion/rest.js +1186 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +286 -24
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +1 -1
- package/dist/providers/sheets/index.js +3 -3
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/stripe/index.d.ts +26 -20
- package/dist/providers/stripe/index.js +103 -130
- package/dist/providers/stripe/openapi.generated.js +649 -0
- package/dist/providers/stripe/rest.d.ts +14 -0
- package/dist/providers/stripe/rest.js +395 -0
- package/dist/providers/stripe/skill.generated.js +7 -5
- package/dist/providers/stripe/value-safety.js +705 -0
- package/dist/providers/vercel/index.d.ts +31 -22
- package/dist/providers/vercel/index.js +96 -853
- package/dist/providers/vercel/openapi.generated.js +456 -0
- package/dist/providers/vercel/rest.d.ts +20 -0
- package/dist/providers/vercel/rest.js +1002 -0
- package/dist/providers/vercel/skill.generated.js +4 -5
- package/dist/providers/vercel/value-safety.js +328 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/routes/mcp.js +30 -10
- package/dist/routes/operator.js +14 -7
- package/dist/routes/ui-config.js +2 -2
- package/dist/server.js +3 -0
- package/dist/skills.js +5 -4
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +1 -0
- package/documentation/architecture.md +155 -28
- package/documentation/auth.md +86 -27
- package/documentation/code-mode.md +81 -72
- package/documentation/deploying.md +10 -5
- package/documentation/integrating.md +72 -9
- package/documentation/meta-tools.md +52 -38
- package/documentation/operating.md +11 -8
- package/documentation/operator-tests.md +2 -3
- package/documentation/operator-ui.md +9 -3
- package/documentation/provider-auth-migration.md +308 -0
- package/documentation/provider-migration-0.29.md +18 -6
- package/examples/worker/AGENTS.md +5 -0
- package/examples/worker/README.md +2 -2
- package/examples/worker/src/connecta.config.ts +5 -0
- package/package.json +7 -2
- package/templates/node/AGENTS.md +6 -4
- package/templates/node/README.md +6 -4
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +5 -0
- package/dist/providers/vercel/mcp-ownership.js +0 -212
- package/dist/tool-safety.d.ts +0 -9
|
@@ -48,7 +48,7 @@ export type DocsOptions = GoogleWorkspaceOptions;
|
|
|
48
48
|
*
|
|
49
49
|
* Reads run in programs. `create_document`, `append_text`, and `insert_text`
|
|
50
50
|
* are additive writes the host approves through call_destructive_tool or a
|
|
51
|
-
*
|
|
51
|
+
* program on a writable endpoint; `replace_all_text` and `batch_update_document` are
|
|
52
52
|
* destructive.
|
|
53
53
|
*/
|
|
54
54
|
export declare const docs: import("../../provider.js").ProviderFactory<GoogleWorkspaceOptions>;
|
|
@@ -935,7 +935,7 @@ function toolDefinitions(client) {
|
|
|
935
935
|
name: "create_document",
|
|
936
936
|
description: "Create a new Google Doc in the user's My Drive root with a title and optional plain-text body. Cannot choose a folder, apply a template, or copy a document.",
|
|
937
937
|
// Additive: a new document changes nothing that existed. Not read-only,
|
|
938
|
-
// so program calls require a
|
|
938
|
+
// so program calls require a writable endpoint; other writes use
|
|
939
939
|
// call_destructive_tool. The host controls approval.
|
|
940
940
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
941
941
|
inputSchema: input({
|
|
@@ -1188,7 +1188,7 @@ function usageGuide(purpose, instructions) {
|
|
|
1188
1188
|
*
|
|
1189
1189
|
* Reads run in programs. `create_document`, `append_text`, and `insert_text`
|
|
1190
1190
|
* are additive writes the host approves through call_destructive_tool or a
|
|
1191
|
-
*
|
|
1191
|
+
* program on a writable endpoint; `replace_all_text` and `batch_update_document` are
|
|
1192
1192
|
* destructive.
|
|
1193
1193
|
*/
|
|
1194
1194
|
export const docs = asProviderFactory({
|
|
@@ -48,6 +48,6 @@ export type DriveOptions = GoogleWorkspaceOptions;
|
|
|
48
48
|
* The `drive` scope reaches everything the user can; the tool surface is what
|
|
49
49
|
* keeps this connection from deleting a file permanently or transferring
|
|
50
50
|
* ownership. Every write crosses host approval through call_destructive_tool
|
|
51
|
-
* or a
|
|
51
|
+
* or a program on a writable endpoint.
|
|
52
52
|
*/
|
|
53
53
|
export declare const drive: import("../../provider.js").ProviderFactory<GoogleWorkspaceOptions>;
|
|
@@ -1473,7 +1473,7 @@ function usageGuide(purpose, instructions) {
|
|
|
1473
1473
|
* The `drive` scope reaches everything the user can; the tool surface is what
|
|
1474
1474
|
* keeps this connection from deleting a file permanently or transferring
|
|
1475
1475
|
* ownership. Every write crosses host approval through call_destructive_tool
|
|
1476
|
-
* or a
|
|
1476
|
+
* or a program on a writable endpoint.
|
|
1477
1477
|
*/
|
|
1478
1478
|
export const drive = asProviderFactory({
|
|
1479
1479
|
name: "drive",
|
|
@@ -48,7 +48,7 @@ export type FormsOptions = GoogleWorkspaceOptions;
|
|
|
48
48
|
*
|
|
49
49
|
* Listing forms needs Drive, which this connection does not request; pair it
|
|
50
50
|
* with a Drive connection for that. `create_form` is an additive write the
|
|
51
|
-
* host approves through call_destructive_tool or a
|
|
51
|
+
* host approves through call_destructive_tool or a program on a writable endpoint;
|
|
52
52
|
* `update_form_info` and `batch_update_form` are destructive.
|
|
53
53
|
*/
|
|
54
54
|
export declare const forms: import("../../provider.js").ProviderFactory<GoogleWorkspaceOptions>;
|
|
@@ -806,7 +806,7 @@ function tools(client) {
|
|
|
806
806
|
name: "create_form",
|
|
807
807
|
description: "Create an empty Google Form with a title in the user's Drive. Google sets only the title here: add questions and a description with batch_update_form.",
|
|
808
808
|
// Additive: a new, empty form changes nothing that existed. Not
|
|
809
|
-
// read-only: program calls require a
|
|
809
|
+
// read-only: program calls require a writable endpoint; other writes use
|
|
810
810
|
// call_destructive_tool. The host controls approval.
|
|
811
811
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
812
812
|
inputSchema: input({
|
|
@@ -1168,7 +1168,7 @@ function usageGuide(purpose, instructions) {
|
|
|
1168
1168
|
*
|
|
1169
1169
|
* Listing forms needs Drive, which this connection does not request; pair it
|
|
1170
1170
|
* with a Drive connection for that. `create_form` is an additive write the
|
|
1171
|
-
* host approves through call_destructive_tool or a
|
|
1171
|
+
* host approves through call_destructive_tool or a program on a writable endpoint;
|
|
1172
1172
|
* `update_form_info` and `batch_update_form` are destructive.
|
|
1173
1173
|
*/
|
|
1174
1174
|
export const forms = asProviderFactory({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Setup\n\nCreate a GitHub App, generate its RSA private key, and install it on every\nconfigured owner. Org grants require **All repositories**, including future\nrepositories. Exact repo grants work with selected-repository installations.\nRead-only grants request read permissions; read-write grants request only the\npermissions needed by each operation. The App needs repository Metadata,\nContents, Issues, Pull requests, Actions, Checks and Commit statuses for the\nreviewed read tools. Grant Contents, Issues, Pull requests and Actions write\nonly for the operations the deployment needs. Workflow-file edits also need\nthe App's Workflows write permission and `workflows: \"write\"` on the effective\nscope. Org grants authorize repository operations, not organization administration.\n\nPass `app: { appId, privateKey }` with the key from an environment secret, or\nomit privateKey and populate the connector's encrypted `privateKey` vault field.\nNever put private keys or installation tokens in agent instructions. Tokens\nstay in bounded runtime memory, refresh a minute before expiry, and are never\npersisted. Key rotation partitions the caches by the key's fingerprint.\nConfigured repository names bind to repository IDs on first use. Bindings last\nfor the provider instance; name lookups expire after five minutes. A rename\nkeeps its grant, while a replacement repository at the old name does not.\nReconfigure to authorize a replacement. Transfers leave the original\ninstallation and cannot use its tokens. Org grants bind to installation IDs.\n\n## Choose the target first\n\n`list_scopes({})` returns configured org/repo grants without network access.\n`list_scopes({ owner })` resolves that owner's installation and pages reachable\nrepositories. Carry its `next_page`. The connection context lists configured\nscopes and access. A repo entry overrides and may narrow its parent org entry.\nAn org grant covers current and future repositories, as long as the installation\nkeeps All repositories selected.\n\nEvery hosted call requires explicit `owner` and `repo`. Connecta checks scope\nand write access before reading the key or minting a token, and narrows each\nrepo call's token with `repository_ids` and the reviewed permissions.\nAn internal metadata-only installation token resolves names through GitHub's\ninstallation repository list; it never authenticates hosted calls or search.\nExpiry and misses refresh the name lookup without rebinding configured grants.\nUse discovery or the configured name to refresh a rename before using its new\nname; names that cannot map to a configured grant fail before operation tokens\nare minted. Cross-owner\nPR heads, cross-repository issue parents, opaque comment/review-thread IDs and\nsymlink writes are refused. The first release acts as the App; personal OAuth\nattribution is planned separately. Classification describes behavior; scope\nchecks authorize targets
|
|
3
|
+
"content": "## Setup\n\nCreate a GitHub App, generate its RSA private key, and install it on every\nconfigured owner. Org grants require **All repositories**, including future\nrepositories. Exact repo grants work with selected-repository installations.\nRead-only grants request read permissions; read-write grants request only the\npermissions needed by each operation. The App needs repository Metadata,\nContents, Issues, Pull requests, Actions, Checks and Commit statuses for the\nreviewed read tools. Grant Contents, Issues, Pull requests and Actions write\nonly for the operations the deployment needs. Workflow-file edits also need\nthe App's Workflows write permission and `workflows: \"write\"` on the effective\nscope. Org grants authorize repository operations, not organization administration.\n\nPass `app: { appId, privateKey }` with the key from an environment secret, or\nomit privateKey and populate the connector's encrypted `privateKey` vault field.\nNever put private keys or installation tokens in agent instructions. Tokens\nstay in bounded runtime memory, refresh a minute before expiry, and are never\npersisted. Key rotation partitions the caches by the key's fingerprint.\nConfigured repository names bind to repository IDs on first use. Bindings last\nfor the provider instance; name lookups expire after five minutes. A rename\nkeeps its grant, while a replacement repository at the old name does not.\nReconfigure to authorize a replacement. Transfers leave the original\ninstallation and cannot use its tokens. Org grants bind to installation IDs.\n\n## Choose the target first\n\n`list_scopes({})` returns configured org/repo grants without network access.\n`list_scopes({ owner })` resolves that owner's installation and pages reachable\nrepositories. Carry its `next_page`. The connection context lists configured\nscopes and access. A repo entry overrides and may narrow its parent org entry.\nAn org grant covers current and future repositories, as long as the installation\nkeeps All repositories selected.\n\nEvery hosted call requires explicit `owner` and `repo`. Connecta checks scope\nand write access before reading the key or minting a token, and narrows each\nrepo call's token with `repository_ids` and the reviewed permissions.\nAn internal metadata-only installation token resolves names through GitHub's\ninstallation repository list; it never authenticates hosted calls or search.\nExpiry and misses refresh the name lookup without rebinding configured grants.\nUse discovery or the configured name to refresh a rename before using its new\nname; names that cannot map to a configured grant fail before operation tokens\nare minted. Cross-owner\nPR heads, cross-repository issue parents, opaque comment/review-thread IDs and\nsymlink writes are refused. The first release acts as the App; personal OAuth\nattribution is planned separately. Classification describes behavior; scope\nchecks authorize targets whether or not the endpoint is read-only.\n\n## Tools and paging\n\nHosted schemas and results come from GitHub. Reviewed reads cover files,\ncommits, branches, tags, releases, issues, labels, PR details/diffs/reviews,\nand Actions workflows/runs/jobs. Reviewed writes cover issues, comments, PR\ncreation/updates/reviews, branches, file edits and Actions dispatch/rerun/cancel.\nCompound `issue_write` is always a write, including update/close. Review methods\nare limited to create/submit_pending/delete_pending; resolve/unresolve by opaque thread ID are\nnot exposed. Scope-changing tools, repo creation/forks, deletion/transfer,\nadministration, secrets and Copilot delegation are absent. Unknown upstream\ntools, arguments and methods require review before use.\n\nUse `search_scoped` for repositories, issues, PRs or code across installations.\nSupply literal words and an optional subset of configured org/repo selectors.\nScope qualifiers and Boolean operators are refused. Results have a partition\nID, next page, and an incomplete-results flag. Pass pages keyed by those exact\npartition IDs. Each call makes at most 20 partition requests; select fewer\nscopes when needed. Search pages stop at 10 and retain incompleteness. Returned\nrepositories are checked by ID against both deployment scopes and selected\npartitions. Issue and PR results resolve their repository URL to an ID before\nany result is returned.\nInstallation tokens do not override GitHub's own code-search access restrictions.\n\nREST complements own `create_release`, `update_release`, `delete_release` and\n`merge_pull_request`. Publishing a draft is `update_release({ draft: false })`.\nMerge requires an expected head SHA, a same-repository head/base and explicit\nworkflow-write access on the effective scope. GitHub atomically guards only the\nhead SHA, so a concurrent base retarget can introduce workflow changes despite\nan earlier file comparison. Merge is therefore disabled by default.\nRead current state before updates. A dispatched write is attempted once;\nGitHub errors never trigger an automatic write retry. Honor retry timing on\nrate limits; an ambiguous 404 may mean absence or an installation permission\ngap. Fix the App installation/key/permissions for auth errors; the App has no\ninteractive OAuth reconnect flow.\n",
|
|
4
4
|
"instructionsHeading": "Repository instructions",
|
|
5
5
|
"fragments": {
|
|
6
|
-
"guide": "## Setup\n\nCreate a GitHub App, generate its RSA private key, and install it on every\nconfigured owner. Org grants require **All repositories**, including future\nrepositories. Exact repo grants work with selected-repository installations.\nRead-only grants request read permissions; read-write grants request only the\npermissions needed by each operation. The App needs repository Metadata,\nContents, Issues, Pull requests, Actions, Checks and Commit statuses for the\nreviewed read tools. Grant Contents, Issues, Pull requests and Actions write\nonly for the operations the deployment needs. Workflow-file edits also need\nthe App's Workflows write permission and `workflows: \"write\"` on the effective\nscope. Org grants authorize repository operations, not organization administration.\n\nPass `app: { appId, privateKey }` with the key from an environment secret, or\nomit privateKey and populate the connector's encrypted `privateKey` vault field.\nNever put private keys or installation tokens in agent instructions. Tokens\nstay in bounded runtime memory, refresh a minute before expiry, and are never\npersisted. Key rotation partitions the caches by the key's fingerprint.\nConfigured repository names bind to repository IDs on first use. Bindings last\nfor the provider instance; name lookups expire after five minutes. A rename\nkeeps its grant, while a replacement repository at the old name does not.\nReconfigure to authorize a replacement. Transfers leave the original\ninstallation and cannot use its tokens. Org grants bind to installation IDs.\n\n## Choose the target first\n\n`list_scopes({})` returns configured org/repo grants without network access.\n`list_scopes({ owner })` resolves that owner's installation and pages reachable\nrepositories. Carry its `next_page`. The connection context lists configured\nscopes and access. A repo entry overrides and may narrow its parent org entry.\nAn org grant covers current and future repositories, as long as the installation\nkeeps All repositories selected.\n\nEvery hosted call requires explicit `owner` and `repo`. Connecta checks scope\nand write access before reading the key or minting a token, and narrows each\nrepo call's token with `repository_ids` and the reviewed permissions.\nAn internal metadata-only installation token resolves names through GitHub's\ninstallation repository list; it never authenticates hosted calls or search.\nExpiry and misses refresh the name lookup without rebinding configured grants.\nUse discovery or the configured name to refresh a rename before using its new\nname; names that cannot map to a configured grant fail before operation tokens\nare minted. Cross-owner\nPR heads, cross-repository issue parents, opaque comment/review-thread IDs and\nsymlink writes are refused. The first release acts as the App; personal OAuth\nattribution is planned separately. Classification describes behavior; scope\nchecks authorize targets
|
|
6
|
+
"guide": "## Setup\n\nCreate a GitHub App, generate its RSA private key, and install it on every\nconfigured owner. Org grants require **All repositories**, including future\nrepositories. Exact repo grants work with selected-repository installations.\nRead-only grants request read permissions; read-write grants request only the\npermissions needed by each operation. The App needs repository Metadata,\nContents, Issues, Pull requests, Actions, Checks and Commit statuses for the\nreviewed read tools. Grant Contents, Issues, Pull requests and Actions write\nonly for the operations the deployment needs. Workflow-file edits also need\nthe App's Workflows write permission and `workflows: \"write\"` on the effective\nscope. Org grants authorize repository operations, not organization administration.\n\nPass `app: { appId, privateKey }` with the key from an environment secret, or\nomit privateKey and populate the connector's encrypted `privateKey` vault field.\nNever put private keys or installation tokens in agent instructions. Tokens\nstay in bounded runtime memory, refresh a minute before expiry, and are never\npersisted. Key rotation partitions the caches by the key's fingerprint.\nConfigured repository names bind to repository IDs on first use. Bindings last\nfor the provider instance; name lookups expire after five minutes. A rename\nkeeps its grant, while a replacement repository at the old name does not.\nReconfigure to authorize a replacement. Transfers leave the original\ninstallation and cannot use its tokens. Org grants bind to installation IDs.\n\n## Choose the target first\n\n`list_scopes({})` returns configured org/repo grants without network access.\n`list_scopes({ owner })` resolves that owner's installation and pages reachable\nrepositories. Carry its `next_page`. The connection context lists configured\nscopes and access. A repo entry overrides and may narrow its parent org entry.\nAn org grant covers current and future repositories, as long as the installation\nkeeps All repositories selected.\n\nEvery hosted call requires explicit `owner` and `repo`. Connecta checks scope\nand write access before reading the key or minting a token, and narrows each\nrepo call's token with `repository_ids` and the reviewed permissions.\nAn internal metadata-only installation token resolves names through GitHub's\ninstallation repository list; it never authenticates hosted calls or search.\nExpiry and misses refresh the name lookup without rebinding configured grants.\nUse discovery or the configured name to refresh a rename before using its new\nname; names that cannot map to a configured grant fail before operation tokens\nare minted. Cross-owner\nPR heads, cross-repository issue parents, opaque comment/review-thread IDs and\nsymlink writes are refused. The first release acts as the App; personal OAuth\nattribution is planned separately. Classification describes behavior; scope\nchecks authorize targets whether or not the endpoint is read-only.\n\n## Tools and paging\n\nHosted schemas and results come from GitHub. Reviewed reads cover files,\ncommits, branches, tags, releases, issues, labels, PR details/diffs/reviews,\nand Actions workflows/runs/jobs. Reviewed writes cover issues, comments, PR\ncreation/updates/reviews, branches, file edits and Actions dispatch/rerun/cancel.\nCompound `issue_write` is always a write, including update/close. Review methods\nare limited to create/submit_pending/delete_pending; resolve/unresolve by opaque thread ID are\nnot exposed. Scope-changing tools, repo creation/forks, deletion/transfer,\nadministration, secrets and Copilot delegation are absent. Unknown upstream\ntools, arguments and methods require review before use.\n\nUse `search_scoped` for repositories, issues, PRs or code across installations.\nSupply literal words and an optional subset of configured org/repo selectors.\nScope qualifiers and Boolean operators are refused. Results have a partition\nID, next page, and an incomplete-results flag. Pass pages keyed by those exact\npartition IDs. Each call makes at most 20 partition requests; select fewer\nscopes when needed. Search pages stop at 10 and retain incompleteness. Returned\nrepositories are checked by ID against both deployment scopes and selected\npartitions. Issue and PR results resolve their repository URL to an ID before\nany result is returned.\nInstallation tokens do not override GitHub's own code-search access restrictions.\n\nREST complements own `create_release`, `update_release`, `delete_release` and\n`merge_pull_request`. Publishing a draft is `update_release({ draft: false })`.\nMerge requires an expected head SHA, a same-repository head/base and explicit\nworkflow-write access on the effective scope. GitHub atomically guards only the\nhead SHA, so a concurrent base retarget can introduce workflow changes despite\nan earlier file comparison. Merge is therefore disabled by default.\nRead current state before updates. A dispatched write is attempted once;\nGitHub errors never trigger an automatic write retry. Honor retry timing on\nrate limits; an ambiguous 404 may mean absence or an installation permission\ngap. Fix the App installation/key/permissions for auth errors; the App has no\ninteractive OAuth reconnect flow.\n"
|
|
7
7
|
}
|
|
8
8
|
};
|
|
@@ -46,6 +46,6 @@ export type GmailOptions = GoogleWorkspaceOptions;
|
|
|
46
46
|
*
|
|
47
47
|
* `gmail.compose` technically allows sending; this connection never does,
|
|
48
48
|
* because no tool reaches a send method. Drafts are additive writes the host
|
|
49
|
-
* approves through call_destructive_tool or a
|
|
49
|
+
* approves through call_destructive_tool or a program on a writable endpoint.
|
|
50
50
|
*/
|
|
51
51
|
export declare const gmail: import("../../provider.js").ProviderFactory<GoogleWorkspaceOptions>;
|
|
@@ -58,6 +58,11 @@ const SUMMARY_CONCURRENCY = 5;
|
|
|
58
58
|
/** A full thread without attachment bodies; Gmail's message cap is 25 MB. */
|
|
59
59
|
const GMAIL_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
60
60
|
const MAX_RECIPIENTS = 100;
|
|
61
|
+
/** Gmail cannot range-read attachment JSON, so text and binary downloads are bounded. */
|
|
62
|
+
const MAX_ATTACHMENT_BYTES = 1024 * 1024;
|
|
63
|
+
/** Same direct-call ceiling and minimum result budget as Drive content reads. */
|
|
64
|
+
const MAX_ATTACHMENT_RESULT_BYTES = 4 * 1024 * 1024;
|
|
65
|
+
const MIN_ATTACHMENT_RESULT_BYTES = 64 * 1024;
|
|
61
66
|
function asRecord(value) {
|
|
62
67
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
63
68
|
}
|
|
@@ -118,22 +123,37 @@ function base64UrlBytes(data) {
|
|
|
118
123
|
}
|
|
119
124
|
function charsetOf(part) {
|
|
120
125
|
const type = header(part, "Content-Type") ?? "";
|
|
121
|
-
|
|
126
|
+
// Consume complete parameters so a semicolon inside another quoted value
|
|
127
|
+
// cannot masquerade as a charset. MIME allows whitespace between tokens.
|
|
128
|
+
const parameters = /;\s*([^=;\s]+)\s*=\s*(?:"((?:\\.|[^"\\])*)"|([^;\s]*))/g;
|
|
129
|
+
for (const match of type.matchAll(parameters)) {
|
|
130
|
+
if (match[1].toLowerCase() === "charset") {
|
|
131
|
+
return (match[2]?.replace(/\\(.)/g, "$1") ?? match[3] ?? "").trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return "utf-8";
|
|
122
135
|
}
|
|
123
|
-
/** Bytes as text,
|
|
136
|
+
/** Bytes as text, with an explicit notice when the declared charset is unsupported. */
|
|
124
137
|
function decodeIn(part, data) {
|
|
125
138
|
let bytes;
|
|
126
139
|
try {
|
|
127
140
|
bytes = base64UrlBytes(data);
|
|
128
141
|
}
|
|
129
142
|
catch {
|
|
130
|
-
return "";
|
|
143
|
+
return { text: "" };
|
|
131
144
|
}
|
|
145
|
+
return decodeBytes(part, bytes);
|
|
146
|
+
}
|
|
147
|
+
function decodeBytes(part, bytes) {
|
|
148
|
+
const charset = charsetOf(part);
|
|
132
149
|
try {
|
|
133
|
-
return new TextDecoder(
|
|
150
|
+
return { text: new TextDecoder(charset).decode(bytes) };
|
|
134
151
|
}
|
|
135
152
|
catch {
|
|
136
|
-
return
|
|
153
|
+
return {
|
|
154
|
+
text: new TextDecoder().decode(bytes),
|
|
155
|
+
note: `Gmail declared unsupported charset "${clipped(charset, FIELD_BYTES)}"; decoded as UTF-8.`,
|
|
156
|
+
};
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
159
|
function mimeOf(part) {
|
|
@@ -161,13 +181,13 @@ const BODY_TYPES = new Set(["text/plain", "text/html"]);
|
|
|
161
181
|
const MAX_PART_DEPTH = 20;
|
|
162
182
|
/**
|
|
163
183
|
* Walk a payload keeping ancestry: the message's own parts, in order, and the
|
|
164
|
-
* attachment roots, whose subtrees are
|
|
184
|
+
* attachment roots, whose subtrees are entered only for a resolved forwarded root.
|
|
165
185
|
*/
|
|
166
|
-
function walk(root) {
|
|
186
|
+
function walk(root, enterAttachmentRoot = false) {
|
|
167
187
|
const own = [];
|
|
168
188
|
const attachments = [];
|
|
169
189
|
const visit = (part, depth) => {
|
|
170
|
-
if (isAttachment(part)) {
|
|
190
|
+
if (isAttachment(part) && !(enterAttachmentRoot && depth === 0)) {
|
|
171
191
|
attachments.push(part);
|
|
172
192
|
return;
|
|
173
193
|
}
|
|
@@ -180,6 +200,14 @@ function walk(root) {
|
|
|
180
200
|
visit(root, 0);
|
|
181
201
|
return { own, attachments };
|
|
182
202
|
}
|
|
203
|
+
/** The same plain-first selection for messages and parsed forwarded attachments. */
|
|
204
|
+
function ownBody(own) {
|
|
205
|
+
return own.find((part) => mimeOf(part) === "text/plain") ?? own.find((part) => mimeOf(part) === "text/html");
|
|
206
|
+
}
|
|
207
|
+
function bodyText(part, decoded) {
|
|
208
|
+
const body = mimeOf(part) === "text/html" ? htmlToText(decoded.text) : decoded.text;
|
|
209
|
+
return decoded.note ? `${body}\n[${decoded.note}]` : body;
|
|
210
|
+
}
|
|
183
211
|
/** Gmail stores a large body apart from the message, behind an attachment id. */
|
|
184
212
|
const MAX_STORED_BODY_BYTES = 1024 * 1024;
|
|
185
213
|
/**
|
|
@@ -190,11 +218,10 @@ const MAX_STORED_BODY_BYTES = 1024 * 1024;
|
|
|
190
218
|
*/
|
|
191
219
|
async function readBody(client, ctx, messageId, payload, max) {
|
|
192
220
|
const own = walk(payload).own;
|
|
193
|
-
const
|
|
194
|
-
const chosen = plain ?? own.find((part) => mimeOf(part) === "text/html");
|
|
221
|
+
const chosen = ownBody(own);
|
|
195
222
|
if (!chosen)
|
|
196
223
|
return { body: "", bodyTruncated: false, bodyFormat: "none" };
|
|
197
|
-
const format = plain ? "text" : "html";
|
|
224
|
+
const format = mimeOf(chosen) === "text/plain" ? "text" : "html";
|
|
198
225
|
const stored = asRecord(chosen["body"]);
|
|
199
226
|
let data = text(stored["data"]);
|
|
200
227
|
const attachmentId = text(stored["attachmentId"]);
|
|
@@ -216,9 +243,9 @@ async function readBody(client, ctx, messageId, payload, max) {
|
|
|
216
243
|
}, ctx));
|
|
217
244
|
data = text(fetched["data"]);
|
|
218
245
|
}
|
|
219
|
-
const decoded = data ? decodeIn(chosen, data) : "";
|
|
246
|
+
const decoded = data ? decodeIn(chosen, data) : { text: "" };
|
|
220
247
|
// Uncut: the caller caps it, by characters and by the result's bytes.
|
|
221
|
-
return { text:
|
|
248
|
+
return { text: bodyText(chosen, decoded), bodyFormat: format };
|
|
222
249
|
}
|
|
223
250
|
/**
|
|
224
251
|
* Gmail's ids are short opaque tokens — message and thread ids are sixteen
|
|
@@ -243,7 +270,7 @@ function ids(fields) {
|
|
|
243
270
|
const kept = {};
|
|
244
271
|
const omitted = [];
|
|
245
272
|
for (const [name, [value, maxBytes]] of Object.entries(fields)) {
|
|
246
|
-
const id = idOf(value, maxBytes);
|
|
273
|
+
const id = name === "partId" ? partIdOf(value) : idOf(value, maxBytes);
|
|
247
274
|
if (id !== undefined)
|
|
248
275
|
kept[name] = id;
|
|
249
276
|
else if (text(value) !== undefined)
|
|
@@ -269,11 +296,180 @@ function attachmentsOf(payload) {
|
|
|
269
296
|
filename: clipped(text(part["filename"]) ?? "", FIELD_BYTES),
|
|
270
297
|
mimeType: clipped(text(part["mimeType"]), FIELD_BYTES),
|
|
271
298
|
size: typeof asRecord(part["body"])["size"] === "number" ? asRecord(part["body"])["size"] : undefined,
|
|
272
|
-
...ids({
|
|
299
|
+
...ids({
|
|
300
|
+
attachmentId: [asRecord(part["body"])["attachmentId"], ATTACHMENT_ID_BYTES],
|
|
301
|
+
partId: [part["partId"]],
|
|
302
|
+
}),
|
|
273
303
|
})),
|
|
274
304
|
omitted: Math.max(0, all.length - MAX_LISTED_ATTACHMENTS),
|
|
275
305
|
};
|
|
276
306
|
}
|
|
307
|
+
/** Unlike opaque ids, Gmail's root part id may be empty. */
|
|
308
|
+
function partIdOf(value) {
|
|
309
|
+
return typeof value === "string" && jsonBytes(value) <= ID_BYTES ? value : undefined;
|
|
310
|
+
}
|
|
311
|
+
function declaredSize(body) {
|
|
312
|
+
const size = body["size"];
|
|
313
|
+
return typeof size === "number" && Number.isSafeInteger(size) && size >= 0 ? size : undefined;
|
|
314
|
+
}
|
|
315
|
+
/** Include every descendant, even carried files that body selection will skip. */
|
|
316
|
+
function forwardedSize(part, depth = 0) {
|
|
317
|
+
if (depth > MAX_PART_DEPTH)
|
|
318
|
+
return undefined;
|
|
319
|
+
const body = asRecord(part["body"]);
|
|
320
|
+
const children = asArray(part["parts"]);
|
|
321
|
+
let total = declaredSize(body);
|
|
322
|
+
if (total === undefined) {
|
|
323
|
+
if (children.length === 0 || body["size"] !== undefined || body["attachmentId"] || body["data"])
|
|
324
|
+
return undefined;
|
|
325
|
+
total = 0;
|
|
326
|
+
}
|
|
327
|
+
for (const child of children) {
|
|
328
|
+
const size = forwardedSize(asRecord(child), depth + 1);
|
|
329
|
+
if (size === undefined)
|
|
330
|
+
return undefined;
|
|
331
|
+
total += size;
|
|
332
|
+
if (!Number.isSafeInteger(total))
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
return total;
|
|
336
|
+
}
|
|
337
|
+
/** Validate both Gmail byte counts before content can be returned as complete. */
|
|
338
|
+
async function attachmentBytes(client, ctx, messageId, part) {
|
|
339
|
+
const stored = asRecord(part["body"]);
|
|
340
|
+
let source = stored;
|
|
341
|
+
const freshId = text(stored["attachmentId"]);
|
|
342
|
+
if (freshId) {
|
|
343
|
+
source = asRecord(await client.json({
|
|
344
|
+
method: "GET",
|
|
345
|
+
path: `/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(freshId)}`,
|
|
346
|
+
}, ctx));
|
|
347
|
+
}
|
|
348
|
+
const data = source["data"];
|
|
349
|
+
if (typeof data !== "string")
|
|
350
|
+
return undefined;
|
|
351
|
+
let bytes;
|
|
352
|
+
try {
|
|
353
|
+
bytes = base64UrlBytes(data);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
throw new ConnectorCallError("connector_call_failed", "Gmail returned invalid base64url attachment data.", {
|
|
357
|
+
retryable: false,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (bytes.length !== declaredSize(stored) ||
|
|
361
|
+
(source["size"] !== undefined && bytes.length !== declaredSize(source))) {
|
|
362
|
+
throw new ConnectorCallError("connector_call_failed", "Gmail attachment data does not match its declared byte size.", { retryable: false });
|
|
363
|
+
}
|
|
364
|
+
return bytes;
|
|
365
|
+
}
|
|
366
|
+
/** Read only a carried part of a fresh message, never an arbitrary download id. */
|
|
367
|
+
async function readAttachment(client, ctx, args) {
|
|
368
|
+
const messageId = String(args["messageId"]);
|
|
369
|
+
const partId = args["partId"];
|
|
370
|
+
const attachmentId = text(args["attachmentId"]);
|
|
371
|
+
if (partId === undefined && attachmentId === undefined) {
|
|
372
|
+
throw new ConnectorCallError("invalid_args", "Supply partId from the attachment listing, or attachmentId.");
|
|
373
|
+
}
|
|
374
|
+
const message = asRecord(await client.json({ method: "GET", path: `/messages/${encodeURIComponent(messageId)}`, query: { format: "full" } }, ctx));
|
|
375
|
+
const attachments = walk(asRecord(message["payload"])).attachments;
|
|
376
|
+
// partId is immutable and authoritative when supplied. An attachmentId
|
|
377
|
+
// from the previous listing may have rotated since that messages.get.
|
|
378
|
+
const part = attachments.find((entry) => partId !== undefined ? entry["partId"] === partId : asRecord(entry["body"])["attachmentId"] === attachmentId);
|
|
379
|
+
if (!part) {
|
|
380
|
+
throw new ConnectorCallError("not_found", "This attachment is not among the message's carried parts. Read get_message again and use its immutable partId.");
|
|
381
|
+
}
|
|
382
|
+
const stored = asRecord(part["body"]);
|
|
383
|
+
const mime = mimeOf(part).split(";")[0].trim();
|
|
384
|
+
const parsedForward = mime === "message/rfc822" &&
|
|
385
|
+
!text(stored["attachmentId"]) &&
|
|
386
|
+
!text(stored["data"]) &&
|
|
387
|
+
asArray(part["parts"]).length > 0;
|
|
388
|
+
const size = parsedForward ? forwardedSize(part) : declaredSize(stored);
|
|
389
|
+
const maxBytes = typeof args["maxBytes"] === "number" ? args["maxBytes"] : RESULT_BUDGET_BYTES;
|
|
390
|
+
const maxChars = typeof args["maxChars"] === "number" ? args["maxChars"] : DEFAULT_MESSAGE_BODY_CHARS;
|
|
391
|
+
const base = compact({
|
|
392
|
+
messageId,
|
|
393
|
+
...ids({ partId: [part["partId"]] }),
|
|
394
|
+
filename: clipped(text(part["filename"]) ?? "", FIELD_BYTES),
|
|
395
|
+
mimeType: clipped(text(part["mimeType"]) ?? "application/octet-stream", FIELD_BYTES),
|
|
396
|
+
size,
|
|
397
|
+
});
|
|
398
|
+
const unavailable = (note) => ({ ...base, format: "unavailable", contentTruncated: false, note });
|
|
399
|
+
const isText = mime.startsWith("text/") ||
|
|
400
|
+
["application/json", "application/xml", "message/rfc822"].includes(mime) ||
|
|
401
|
+
mime.endsWith("+json") ||
|
|
402
|
+
mime.endsWith("+xml");
|
|
403
|
+
let envelope = { ...base, format: isText ? "text" : "base64", content: "", contentTruncated: false };
|
|
404
|
+
const fitting = Math.floor(Math.max(0, maxBytes - jsonBytes(envelope)) / 4) * 3;
|
|
405
|
+
const limit = isText ? MAX_ATTACHMENT_BYTES : Math.min(MAX_ATTACHMENT_BYTES, fitting);
|
|
406
|
+
const tooLarge = () => unavailable(limit < MAX_ATTACHMENT_BYTES
|
|
407
|
+
? `This binary attachment does not fit maxBytes (${maxBytes}), which holds ${limit} attachment bytes. Raise maxBytes, up to ${MAX_ATTACHMENT_RESULT_BYTES} for a direct call_tool only, to read up to ${MAX_ATTACHMENT_BYTES} bytes. No content is returned.`
|
|
408
|
+
: `This attachment exceeds the ${MAX_ATTACHMENT_BYTES}-byte download and inline cap. No content is returned; open it in Gmail.`);
|
|
409
|
+
// Declared size is checked before downloading, but actual bytes are checked
|
|
410
|
+
// too: inconsistent metadata must never bypass the cap or whole-result budget.
|
|
411
|
+
if (size === undefined)
|
|
412
|
+
return unavailable("Gmail did not declare a valid attachment size; no content is downloaded. Open it in Gmail.");
|
|
413
|
+
if (size > limit)
|
|
414
|
+
return tooLarge();
|
|
415
|
+
let decoded;
|
|
416
|
+
if (parsedForward) {
|
|
417
|
+
const own = walk(part, true).own;
|
|
418
|
+
const chosen = ownBody(own);
|
|
419
|
+
if (!chosen)
|
|
420
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
421
|
+
const bytes = await attachmentBytes(client, ctx, messageId, chosen);
|
|
422
|
+
if (!bytes)
|
|
423
|
+
return unavailable("Gmail returned no readable forwarded message body data; open it in Gmail.");
|
|
424
|
+
const rendered = decodeBytes(chosen, bytes);
|
|
425
|
+
if (!rendered.text.trim())
|
|
426
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
427
|
+
const headers = ["From", "To", "Date", "Subject"].flatMap((name) => {
|
|
428
|
+
const value = own.map((entry) => header(entry, name)).find((entry) => entry !== undefined);
|
|
429
|
+
return value === undefined ? [] : [`${name}: ${value}`];
|
|
430
|
+
});
|
|
431
|
+
const body = mimeOf(chosen) === "text/html" ? htmlToText(rendered.text) : rendered.text;
|
|
432
|
+
if (!body.trim())
|
|
433
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
434
|
+
decoded = headers.length > 0 ? `${headers.join("\n")}\n\n${body}` : body;
|
|
435
|
+
envelope = { ...envelope, ...compact({ note: rendered.note }) };
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const bytes = await attachmentBytes(client, ctx, messageId, part);
|
|
439
|
+
if (!bytes)
|
|
440
|
+
return unavailable("Gmail returned no attachment data; open it in Gmail.");
|
|
441
|
+
if (bytes.length > limit)
|
|
442
|
+
return tooLarge();
|
|
443
|
+
if (!isText) {
|
|
444
|
+
let binary = "";
|
|
445
|
+
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
446
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
|
|
447
|
+
}
|
|
448
|
+
return { ...envelope, content: btoa(binary) };
|
|
449
|
+
}
|
|
450
|
+
const rendered = decodeBytes(part, bytes);
|
|
451
|
+
decoded = rendered.text;
|
|
452
|
+
if (mime === "message/rfc822" && !decoded.trim()) {
|
|
453
|
+
return unavailable("This forwarded message has no readable text body; open it in Gmail.");
|
|
454
|
+
}
|
|
455
|
+
envelope = { ...envelope, ...compact({ note: rendered.note }) };
|
|
456
|
+
}
|
|
457
|
+
const characters = Array.from(decoded);
|
|
458
|
+
const marker = (dropped) => `\n[… ${dropped} more characters truncated; raise maxChars or maxBytes for a direct call_tool, or open it in Gmail]`;
|
|
459
|
+
const byChars = characters.length > maxChars
|
|
460
|
+
? characters.slice(0, maxChars).join("") + marker(characters.length - maxChars)
|
|
461
|
+
: decoded;
|
|
462
|
+
if (jsonBytes({ ...envelope, content: byChars }) <= maxBytes) {
|
|
463
|
+
return { ...envelope, content: byChars, contentTruncated: characters.length > maxChars };
|
|
464
|
+
}
|
|
465
|
+
// Reserve the longest marker, then clamp only the original prefix. A text
|
|
466
|
+
// cut by both limits still has one marker, counted in the result's budget.
|
|
467
|
+
const prefix = characters.slice(0, maxChars).join("");
|
|
468
|
+
const contentBudget = maxBytes - jsonBytes(envelope) + jsonBytes("");
|
|
469
|
+
const kept = clampText(prefix, contentBudget - jsonBytes(marker(characters.length)) + 2, () => "");
|
|
470
|
+
const content = kept + marker(characters.length - Array.from(kept).length);
|
|
471
|
+
return { ...envelope, content, contentTruncated: true };
|
|
472
|
+
}
|
|
277
473
|
/**
|
|
278
474
|
* Per-field byte bounds, so every result is bounded by construction: a page
|
|
279
475
|
* of 25 summaries, or one message beside its body, stays under
|
|
@@ -428,10 +624,10 @@ function resultLimitMarker(dropped) {
|
|
|
428
624
|
* all, against the budget. The bounds above make a refusal here a
|
|
429
625
|
* should-not-happen; it exists so nothing oversized is ever handed on.
|
|
430
626
|
*/
|
|
431
|
-
function deliverable(tool, result) {
|
|
627
|
+
function deliverable(tool, result, budget = RESULT_BUDGET_BYTES) {
|
|
432
628
|
const size = jsonBytes(result);
|
|
433
|
-
if (size >
|
|
434
|
-
throw new ConnectorCallError("connector_call_failed", `Gmail's answer to ${tool} is ${size} bytes even after cutting, past connecta's ${
|
|
629
|
+
if (size > budget) {
|
|
630
|
+
throw new ConnectorCallError("connector_call_failed", `Gmail's answer to ${tool} is ${size} bytes even after cutting, past connecta's ${budget / 1024} KiB result limit. Open it in Gmail.`, { retryable: false });
|
|
435
631
|
}
|
|
436
632
|
return result;
|
|
437
633
|
}
|
|
@@ -870,6 +1066,7 @@ const ATTACHMENT_SCHEMA = {
|
|
|
870
1066
|
mimeType: { type: "string" },
|
|
871
1067
|
size: { type: "integer" },
|
|
872
1068
|
attachmentId: { type: "string" },
|
|
1069
|
+
partId: { type: "string" },
|
|
873
1070
|
omittedIds: OMITTED_IDS_SCHEMA,
|
|
874
1071
|
},
|
|
875
1072
|
};
|
|
@@ -1018,7 +1215,7 @@ function tools(client) {
|
|
|
1018
1215
|
},
|
|
1019
1216
|
{
|
|
1020
1217
|
name: "get_thread",
|
|
1021
|
-
description: "Get a Gmail thread's messages, oldest first, with headers, plain-text bodies capped by maxBodyChars, and attachment
|
|
1218
|
+
description: "Get a Gmail thread's messages, oldest first, with headers, plain-text bodies capped by maxBodyChars, and attachment metadata. Read attachments with get_attachment using partId. A long thread pages by cursor.",
|
|
1022
1219
|
annotations: readOnly,
|
|
1023
1220
|
inputSchema: input({
|
|
1024
1221
|
threadId: idProperty("Thread id from search_threads or a message's threadId."),
|
|
@@ -1079,7 +1276,7 @@ function tools(client) {
|
|
|
1079
1276
|
},
|
|
1080
1277
|
{
|
|
1081
1278
|
name: "get_message",
|
|
1082
|
-
description: "Get one Gmail message with headers, its plain-text body (HTML converted only when no text part exists), and attachment metadata.
|
|
1279
|
+
description: "Get one Gmail message with headers, its plain-text body (HTML converted only when no text part exists), and attachment metadata. Read attachments with get_attachment using partId.",
|
|
1083
1280
|
annotations: readOnly,
|
|
1084
1281
|
inputSchema: input({
|
|
1085
1282
|
messageId: idProperty("Message id from get_thread, or a draft's messageId."),
|
|
@@ -1108,6 +1305,58 @@ function tools(client) {
|
|
|
1108
1305
|
return message;
|
|
1109
1306
|
},
|
|
1110
1307
|
},
|
|
1308
|
+
{
|
|
1309
|
+
name: "get_attachment",
|
|
1310
|
+
description: "Read a Gmail attachment by immutable partId: text with its charset, or small binaries as standard base64 for Drive create_file. Refuses oversized downloads; text is capped by maxChars and the whole result by maxBytes.",
|
|
1311
|
+
annotations: readOnly,
|
|
1312
|
+
inputSchema: {
|
|
1313
|
+
...input({
|
|
1314
|
+
messageId: idProperty("Message id from get_message, get_thread, or get_draft."),
|
|
1315
|
+
partId: {
|
|
1316
|
+
type: "string",
|
|
1317
|
+
maxLength: ID_BYTES,
|
|
1318
|
+
description: "Immutable partId from the attachment listing, including an empty root id. Preferred; authoritative when attachmentId is also supplied.",
|
|
1319
|
+
},
|
|
1320
|
+
attachmentId: {
|
|
1321
|
+
type: "string",
|
|
1322
|
+
minLength: 1,
|
|
1323
|
+
maxLength: ATTACHMENT_ID_BYTES,
|
|
1324
|
+
pattern: ID_PATTERN,
|
|
1325
|
+
description: "Alternative attachmentId from the listing. Gmail can rotate it between reads; prefer partId.",
|
|
1326
|
+
},
|
|
1327
|
+
maxChars: {
|
|
1328
|
+
type: "integer",
|
|
1329
|
+
minimum: 1,
|
|
1330
|
+
maximum: MAX_BODY_CHARS,
|
|
1331
|
+
description: `Text code points kept; defaults to ${DEFAULT_MESSAGE_BODY_CHARS}. Longer text ends with a truncation marker. Ignored for binaries.`,
|
|
1332
|
+
},
|
|
1333
|
+
maxBytes: {
|
|
1334
|
+
type: "integer",
|
|
1335
|
+
minimum: MIN_ATTACHMENT_RESULT_BYTES,
|
|
1336
|
+
maximum: MAX_ATTACHMENT_RESULT_BYTES,
|
|
1337
|
+
description: `Whole-result UTF-8 JSON byte budget; defaults to ${RESULT_BUDGET_BYTES}. Raise it only for a direct call_tool; execute_code cannot receive large results.`,
|
|
1338
|
+
},
|
|
1339
|
+
}, ["messageId"]),
|
|
1340
|
+
anyOf: [{ required: ["partId"] }, { required: ["attachmentId"] }],
|
|
1341
|
+
},
|
|
1342
|
+
outputSchema: {
|
|
1343
|
+
type: "object",
|
|
1344
|
+
properties: {
|
|
1345
|
+
messageId: { type: "string" },
|
|
1346
|
+
partId: { type: "string" },
|
|
1347
|
+
omittedIds: OMITTED_IDS_SCHEMA,
|
|
1348
|
+
filename: { type: "string" },
|
|
1349
|
+
mimeType: { type: "string" },
|
|
1350
|
+
size: { type: "integer" },
|
|
1351
|
+
format: { type: "string", enum: ["text", "base64", "unavailable"] },
|
|
1352
|
+
content: { type: "string" },
|
|
1353
|
+
contentTruncated: { type: "boolean" },
|
|
1354
|
+
note: { type: "string" },
|
|
1355
|
+
},
|
|
1356
|
+
required: ["messageId", "filename", "mimeType", "format", "contentTruncated"],
|
|
1357
|
+
},
|
|
1358
|
+
handler: async (args, ctx) => await readAttachment(client, ctx, args),
|
|
1359
|
+
},
|
|
1111
1360
|
{
|
|
1112
1361
|
name: "list_labels",
|
|
1113
1362
|
description: "List the user's Gmail labels, system and user-created, with the ids messages carry; no message counts. Search by name with label: in search_threads.",
|
|
@@ -1271,7 +1520,7 @@ function tools(client) {
|
|
|
1271
1520
|
name: "create_draft",
|
|
1272
1521
|
description: "Create a Gmail draft, optionally as a reply in an existing thread. It is saved, never sent: the user reviews and sends it in Gmail.",
|
|
1273
1522
|
// Additive: a new draft changes nothing that existed. Not read-only, so
|
|
1274
|
-
// program calls require a
|
|
1523
|
+
// program calls require a writable endpoint; other writes use
|
|
1275
1524
|
// call_destructive_tool. The host controls approval.
|
|
1276
1525
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
1277
1526
|
inputSchema: input({
|
|
@@ -1384,7 +1633,7 @@ function usageGuide(purpose, instructions) {
|
|
|
1384
1633
|
*
|
|
1385
1634
|
* `gmail.compose` technically allows sending; this connection never does,
|
|
1386
1635
|
* because no tool reaches a send method. Drafts are additive writes the host
|
|
1387
|
-
* approves through call_destructive_tool or a
|
|
1636
|
+
* approves through call_destructive_tool or a program on a writable endpoint.
|
|
1388
1637
|
*/
|
|
1389
1638
|
export const gmail = asProviderFactory({
|
|
1390
1639
|
name: "gmail",
|
|
@@ -1430,7 +1679,9 @@ function gmailConnector(id, options) {
|
|
|
1430
1679
|
// Every tool's result passes the whole-size postcondition on its way out.
|
|
1431
1680
|
tools: tools(client).map((tool) => ({
|
|
1432
1681
|
...tool,
|
|
1433
|
-
handler: async (args, ctx) => deliverable(tool.name, await tool.handler(args, ctx)
|
|
1682
|
+
handler: async (args, ctx) => deliverable(tool.name, await tool.handler(args, ctx), tool.name === "get_attachment" && typeof args["maxBytes"] === "number"
|
|
1683
|
+
? args["maxBytes"]
|
|
1684
|
+
: RESULT_BUDGET_BYTES),
|
|
1434
1685
|
})),
|
|
1435
1686
|
});
|
|
1436
1687
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "# Gmail usage (draft-only)\n\nActs as the signed-in person's own Gmail through Workspace delegation: read mail and write drafts, never send.\n\nConnection purpose: \n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto. No argument names a mailbox. A call with none mapped fails\n`auth_required`; only an operator can change the mapping.\n\n## Reading\n\n- `search_threads` takes Gmail search syntax; reuse the person's own\n phrasing (`from:`, `subject:`, `newer_than:7d`, `is:unread`,\n `label:`). Page with `page.nextCursor` and the same query.\n- `get_thread` returns messages oldest first with bodies capped by\n `maxBodyChars`; a cut body ends with a truncation marker. A long thread\n pages: follow `page.nextCursor` until `hasMore` is false, with the same\n arguments. `conflict` means the thread changed under the cursor; read it\n again from the start. Reduce inside\n `execute_code` before returning a long thread.\n-
|
|
3
|
+
"content": "# Gmail usage (draft-only)\n\nActs as the signed-in person's own Gmail through Workspace delegation: read mail and write drafts, never send.\n\nConnection purpose: \n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto. No argument names a mailbox. A call with none mapped fails\n`auth_required`; only an operator can change the mapping.\n\n## Reading\n\n- `search_threads` takes Gmail search syntax; reuse the person's own\n phrasing (`from:`, `subject:`, `newer_than:7d`, `is:unread`,\n `label:`). Page with `page.nextCursor` and the same query.\n- `get_thread` returns messages oldest first with bodies capped by\n `maxBodyChars`; a cut body ends with a truncation marker. A long thread\n pages: follow `page.nextCursor` until `hasMore` is false, with the same\n arguments. `conflict` means the thread changed under the cursor; read it\n again from the start. Reduce inside\n `execute_code` before returning a long thread.\n- Results default to at most 192 KiB; `list_labels` pages the same way.\n- `get_message` and `get_thread` list attachment metadata. An attached or\n forwarded email is an attachment, not the body.\n- `bodyFormat: \"unavailable\"` means Gmail stored the body apart from the\n message and it is too large to read here; say so rather than summarizing.\n\n## Attachments\n\n- `get_attachment` takes `messageId` and the listing's immutable `partId`.\n Prefer `partId`, which stays stable across message reads, including an\n empty id for a root attachment. `attachmentId` is an alternative that\n Gmail can rotate between reads. When both are supplied, `partId` wins and\n the fresh message's download id is used. Read the listing again after\n an unknown-id refusal; never store an attachmentId for later use.\n- Small attachments can have inline data and no attachmentId. Use their\n partId too. Only carried attachment parts can be read, never arbitrary\n message body parts or blind download ids.\n- Text, JSON, XML, calendar invites, CSV, and `message/rfc822` return\n `format: \"text\"` and `content` decoded in the declared charset. An unsupported\n charset produces an explicit note that decoding used UTF-8. Message bodies\n include the same notice in their text.\n Parsed forwarded emails render From, To, Date, Subject, and their own\n plain body, else HTML converted to text. Raw forwarded data is preferred\n when present. A forwarded email without readable text returns an\n `unavailable` note.\n `maxChars` defaults to 12,000 code points, at most 100,000. A character\n or byte cut ends with a truncation marker and `contentTruncated: true`.\n Reduce attachment text inside `execute_code` before returning it.\n- Binary attachments return `format: \"base64\"` and standard base64 in\n `content`. Pass it unchanged to Drive `create_file` as `contentBase64`,\n with `name: filename` and `mimeType`. File creation is a write: use\n `call_destructive_tool` or a program on a writable endpoint for that step.\n- Downloads, text and binary, are limited to 1 MiB. Declared size is\n checked before a separate download. Parsed forwarded emails check the sum\n of descendant sizes before downloading any stored body, and return that\n sum as `size`. Attachment data must match both the message part's declared\n size and the download response's size when supplied; inconsistent payloads\n fail rather than return incomplete files. Binary content must also fit the\n whole-result `maxBytes`, which defaults to 192 KiB. `format: \"unavailable\"`\n returns a note and no content when it cannot fit. Raising `maxChars`\n or `maxBytes` cannot bypass the download cap.\n- Raise `maxBytes` only for a direct `call_tool` read, from 64 KiB up to\n 4 MiB, as with Drive `get_file_content`. `execute_code` cannot receive\n large results; keep the default there. Direct-call results may be paged.\n\n## Drafting\n\n- `create_draft` saves a draft and never sends it; tell the person it is\n waiting in Drafts. There is no send, delete, or label tool.\n- To reply, pass `replyToMessageId`: the draft joins that thread with\n In-Reply-To and References set, and subject (Re: …) and `to` default\n from that message's Reply-To, else its From. Keep the Re: subject or Gmail\n may start a new thread.\n- `update_draft` replaces the body entirely; send the full new text.\n From, To, Cc, Bcc, Reply-To, Subject, the thread, and reply headers are\n kept unless restated; no other header is. A draft with attachments or\n inline images is refused, unchanged — Gmail's update would delete them.\n- Gmail sets From to the mailbox itself; there is no From argument.\n",
|
|
4
4
|
"instructionsHeading": "Connection instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"guide_0": "# Gmail usage (draft-only)\n\nActs as the signed-in person's own Gmail through Workspace delegation: read mail and write drafts, never send.\n\nConnection purpose: ",
|
|
7
|
-
"guide_1": "\n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto. No argument names a mailbox. A call with none mapped fails\n`auth_required`; only an operator can change the mapping.\n\n## Reading\n\n- `search_threads` takes Gmail search syntax; reuse the person's own\n phrasing (`from:`, `subject:`, `newer_than:7d`, `is:unread`,\n `label:`). Page with `page.nextCursor` and the same query.\n- `get_thread` returns messages oldest first with bodies capped by\n `maxBodyChars`; a cut body ends with a truncation marker. A long thread\n pages: follow `page.nextCursor` until `hasMore` is false, with the same\n arguments. `conflict` means the thread changed under the cursor; read it\n again from the start. Reduce inside\n `execute_code` before returning a long thread.\n-
|
|
7
|
+
"guide_1": "\n\n## Whose mailbox\n\nEvery call reads and writes the mailbox deployment config maps the caller\nto. No argument names a mailbox. A call with none mapped fails\n`auth_required`; only an operator can change the mapping.\n\n## Reading\n\n- `search_threads` takes Gmail search syntax; reuse the person's own\n phrasing (`from:`, `subject:`, `newer_than:7d`, `is:unread`,\n `label:`). Page with `page.nextCursor` and the same query.\n- `get_thread` returns messages oldest first with bodies capped by\n `maxBodyChars`; a cut body ends with a truncation marker. A long thread\n pages: follow `page.nextCursor` until `hasMore` is false, with the same\n arguments. `conflict` means the thread changed under the cursor; read it\n again from the start. Reduce inside\n `execute_code` before returning a long thread.\n- Results default to at most 192 KiB; `list_labels` pages the same way.\n- `get_message` and `get_thread` list attachment metadata. An attached or\n forwarded email is an attachment, not the body.\n- `bodyFormat: \"unavailable\"` means Gmail stored the body apart from the\n message and it is too large to read here; say so rather than summarizing.\n\n## Attachments\n\n- `get_attachment` takes `messageId` and the listing's immutable `partId`.\n Prefer `partId`, which stays stable across message reads, including an\n empty id for a root attachment. `attachmentId` is an alternative that\n Gmail can rotate between reads. When both are supplied, `partId` wins and\n the fresh message's download id is used. Read the listing again after\n an unknown-id refusal; never store an attachmentId for later use.\n- Small attachments can have inline data and no attachmentId. Use their\n partId too. Only carried attachment parts can be read, never arbitrary\n message body parts or blind download ids.\n- Text, JSON, XML, calendar invites, CSV, and `message/rfc822` return\n `format: \"text\"` and `content` decoded in the declared charset. An unsupported\n charset produces an explicit note that decoding used UTF-8. Message bodies\n include the same notice in their text.\n Parsed forwarded emails render From, To, Date, Subject, and their own\n plain body, else HTML converted to text. Raw forwarded data is preferred\n when present. A forwarded email without readable text returns an\n `unavailable` note.\n `maxChars` defaults to 12,000 code points, at most 100,000. A character\n or byte cut ends with a truncation marker and `contentTruncated: true`.\n Reduce attachment text inside `execute_code` before returning it.\n- Binary attachments return `format: \"base64\"` and standard base64 in\n `content`. Pass it unchanged to Drive `create_file` as `contentBase64`,\n with `name: filename` and `mimeType`. File creation is a write: use\n `call_destructive_tool` or a program on a writable endpoint for that step.\n- Downloads, text and binary, are limited to 1 MiB. Declared size is\n checked before a separate download. Parsed forwarded emails check the sum\n of descendant sizes before downloading any stored body, and return that\n sum as `size`. Attachment data must match both the message part's declared\n size and the download response's size when supplied; inconsistent payloads\n fail rather than return incomplete files. Binary content must also fit the\n whole-result `maxBytes`, which defaults to 192 KiB. `format: \"unavailable\"`\n returns a note and no content when it cannot fit. Raising `maxChars`\n or `maxBytes` cannot bypass the download cap.\n- Raise `maxBytes` only for a direct `call_tool` read, from 64 KiB up to\n 4 MiB, as with Drive `get_file_content`. `execute_code` cannot receive\n large results; keep the default there. Direct-call results may be paged.\n\n## Drafting\n\n- `create_draft` saves a draft and never sends it; tell the person it is\n waiting in Drafts. There is no send, delete, or label tool.\n- To reply, pass `replyToMessageId`: the draft joins that thread with\n In-Reply-To and References set, and subject (Re: …) and `to` default\n from that message's Reply-To, else its From. Keep the Re: subject or Gmail\n may start a new thread.\n- `update_draft` replaces the body entirely; send the full new text.\n From, To, Cc, Bcc, Reply-To, Subject, the thread, and reply headers are\n kept unless restated; no other header is. A draft with attachments or\n inline images is refused, unchanged — Gmail's update would delete them.\n- Gmail sets From to the mailbox itself; there is no From argument.\n"
|
|
8
8
|
}
|
|
9
9
|
};
|
|
@@ -302,7 +302,7 @@ export const linear = defineProvider({
|
|
|
302
302
|
// the summary without the description.
|
|
303
303
|
const accessNote = access === "read-only"
|
|
304
304
|
? "Read-only connection: bound to Linear's read-only endpoint, whose token is scope-limited downstream, so every write fails at Linear regardless of arguments. Route writes to a connector configured for read-write access."
|
|
305
|
-
: "Read-write connection: treat every `save_`, `create_`, `delete_`, `resolve_`, `submit_`, and `merge_` operation as a write. Connecta classifies the maintained writes explicitly and
|
|
305
|
+
: "Read-write connection: treat every `save_`, `create_`, `delete_`, `resolve_`, `submit_`, and `merge_` operation as a write. Connecta classifies the maintained writes explicitly, and a read-only endpoint refuses them. Unknown tools without an explicit, uncontradicted read annotation fail closed.";
|
|
306
306
|
return remoteMcp(id, {
|
|
307
307
|
url: LINEAR_MCP_ENDPOINTS[access],
|
|
308
308
|
...provider.connectorOptions,
|