@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.
- package/CHANGELOG.md +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- 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 {
|
|
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 {
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
|
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 {
|
|
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},
|
|
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}
|
|
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},
|
|
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
|
|
1126
|
+
const fields = isRead ? fieldsContract(isList ? "item" : "result", row) : undefined;
|
|
1126
1127
|
const input = Schema.Struct({
|
|
1127
1128
|
...tool.contract.input.fields,
|
|
1128
|
-
...(
|
|
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
|
|
1144
|
-
const problem =
|
|
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,
|
|
1159
|
+
items: asArray(result["items"]).map((item) => retainTruncated(item, selectFields(item, selected))),
|
|
1165
1160
|
}
|
|
1166
|
-
: retainTruncated(result, selectFields(result,
|
|
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
|
|
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 {
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
|
50
|
-
const
|
|
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({
|
|
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*
|
|
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;
|