@zackbart/connecta 0.28.0 → 0.29.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 +961 -5
- package/README.md +62 -39
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1632 -1509
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +526 -0
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +220 -0
- package/dist/connectors/api-connector.js +214 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +13 -99
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +108 -25
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1780 -351
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +25 -257
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-partition.js +17 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/basecamp/index.d.ts +43 -0
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/breeze/index.d.ts +34 -0
- package/dist/providers/breeze/index.js +1446 -0
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/ccb/index.d.ts +103 -0
- package/dist/providers/ccb/index.js +1725 -0
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/overflow/index.d.ts +60 -0
- package/dist/providers/overflow/index.js +1535 -0
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/planning-center/index.d.ts +59 -0
- package/dist/providers/planning-center/index.js +2199 -0
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/tithely/index.d.ts +49 -0
- package/dist/providers/tithely/index.js +1003 -0
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +136 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +665 -230
- package/documentation/auth.md +962 -458
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +125 -35
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { remoteMcp } from "../../connectors/remote-mcp.js";
|
|
2
|
+
import { array, keys, optionsOf } from "../../config-schema.js";
|
|
3
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
4
|
+
import { redactSentSecrets } from "../../sent-secrets.js";
|
|
5
|
+
import { defineProvider, PROVIDER_COMMON } from "../../provider.js";
|
|
6
|
+
import { AppAuth } from "./auth.js";
|
|
7
|
+
import { HOSTED_CLASSIFICATION, HOSTED_ROUTES, hostedTarget } from "./hosted.js";
|
|
8
|
+
import { restConnector } from "./rest.js";
|
|
9
|
+
import { parseScopes, ScopePolicy } from "./scope.js";
|
|
10
|
+
import { skill } from "./skill.generated.js";
|
|
11
|
+
export const GITHUB_MCP_ENDPOINT = "https://api.githubcopilot.com/mcp/";
|
|
12
|
+
const CLASSIFICATION = {
|
|
13
|
+
unlisted: "hide",
|
|
14
|
+
tools: {
|
|
15
|
+
...HOSTED_CLASSIFICATION.tools,
|
|
16
|
+
list_scopes: "read",
|
|
17
|
+
search_scoped: "read",
|
|
18
|
+
create_release: "write",
|
|
19
|
+
update_release: "destructive",
|
|
20
|
+
delete_release: "destructive",
|
|
21
|
+
merge_pull_request: "destructive",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
/** One GitHub App, multiple owner installations, explicit org/repo grants. */
|
|
25
|
+
export const github = defineProvider({
|
|
26
|
+
name: "github",
|
|
27
|
+
title: "GitHub",
|
|
28
|
+
kind: "composed",
|
|
29
|
+
skill,
|
|
30
|
+
classify: CLASSIFICATION,
|
|
31
|
+
bundle: {
|
|
32
|
+
baselineGzip: 164063,
|
|
33
|
+
maxGzip: 224063,
|
|
34
|
+
note: "./providers/github initial measured baseline includes the hosted MCP SDK plus portable GitHub App signing, exact routing and REST complements; cap retains the existing 60,000-byte provider headroom policy.",
|
|
35
|
+
},
|
|
36
|
+
options: optionsOf()({
|
|
37
|
+
...PROVIDER_COMMON,
|
|
38
|
+
app: optionsOf()(keys("appId", "privateKey")),
|
|
39
|
+
scopes: array(optionsOf()(keys("org", "repo", "access", "workflows"))),
|
|
40
|
+
}),
|
|
41
|
+
create(id, options, provider) {
|
|
42
|
+
if (options.authScope === "personal")
|
|
43
|
+
throw new Error("shared GitHub App identity; per-user OAuth attribution is planned separately for 0.29.");
|
|
44
|
+
const policy = new ScopePolicy(parseScopes(options.scopes));
|
|
45
|
+
const auth = new AppAuth(options.app, policy);
|
|
46
|
+
const rest = restConnector(id, auth, policy);
|
|
47
|
+
const usageGuide = provider.usageGuide({
|
|
48
|
+
context: [`Act as the GitHub App. Reachable configuration: ${policy.summary()}.`, `Purpose: ${options.purpose}`],
|
|
49
|
+
summary: "GitHub App scopes, installation routing, confined search and workflow-file limits.",
|
|
50
|
+
required: true,
|
|
51
|
+
});
|
|
52
|
+
// Each operation owns its token-bound client, transport and callback.
|
|
53
|
+
// It closes them before returning; no shared header or client mutation.
|
|
54
|
+
const hosted = (token, classify = HOSTED_CLASSIFICATION) => remoteMcp(id, {
|
|
55
|
+
url: GITHUB_MCP_ENDPOINT,
|
|
56
|
+
requireHttps: true,
|
|
57
|
+
redirects: "none",
|
|
58
|
+
classify,
|
|
59
|
+
auth: {
|
|
60
|
+
type: "request",
|
|
61
|
+
token: async () => token,
|
|
62
|
+
headers: {
|
|
63
|
+
"X-MCP-Toolsets": "repos,git,issues,pull_requests,actions,labels",
|
|
64
|
+
"X-MCP-Tools": Object.keys(HOSTED_ROUTES).join(","),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const withHosted = async (token, ctx, run) => {
|
|
69
|
+
const client = hosted(token);
|
|
70
|
+
try {
|
|
71
|
+
return await run(client);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof ConnectorCallError && error.code === "auth_required")
|
|
75
|
+
await auth.rejectToken(token);
|
|
76
|
+
throw redactSentSecrets(ctx, error);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await client.closeScope?.(ctx);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const credential = options.app.privateKey === undefined
|
|
83
|
+
? {
|
|
84
|
+
label: "GitHub App private key",
|
|
85
|
+
description: "RSA PEM for the deployment's GitHub App. Stored encrypted in the configured credential vault.",
|
|
86
|
+
fields: [{ name: "privateKey", label: "Private key PEM", inputType: "password" }],
|
|
87
|
+
}
|
|
88
|
+
: undefined;
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
title: options.title ?? "GitHub",
|
|
92
|
+
kind: "mcp",
|
|
93
|
+
authScope: "shared",
|
|
94
|
+
...provider.connectorOptions,
|
|
95
|
+
...(credential ? { credential } : {}),
|
|
96
|
+
classification: provider.classify,
|
|
97
|
+
description: `GitHub App access to ${policy.summary()}.`,
|
|
98
|
+
usageGuide,
|
|
99
|
+
describe: () => ({
|
|
100
|
+
source: { kind: "custom" },
|
|
101
|
+
endpoint: { origin: "https://api.githubcopilot.com", path: "/mcp/" },
|
|
102
|
+
auth: { mode: "request", header: "Authorization", scheme: "Bearer" },
|
|
103
|
+
}),
|
|
104
|
+
async listTools(ctx) {
|
|
105
|
+
// Catalog discovery authenticates only a configured scope, with read
|
|
106
|
+
// permissions. Fetch one complete vendor catalog and preserve schemas.
|
|
107
|
+
const first = policy.scopes[0];
|
|
108
|
+
const owner = first.org ?? first.repo.split("/")[0];
|
|
109
|
+
const repos = first.repo ? [first.repo.split("/")[1]] : undefined;
|
|
110
|
+
const token = await auth.token(owner, repos, {
|
|
111
|
+
contents: "read",
|
|
112
|
+
issues: "read",
|
|
113
|
+
pull_requests: "read",
|
|
114
|
+
actions: "read",
|
|
115
|
+
checks: "read",
|
|
116
|
+
statuses: "read",
|
|
117
|
+
}, ctx);
|
|
118
|
+
const facts = await withHosted(token, ctx, (client) => client.listTools(ctx));
|
|
119
|
+
const complement = await rest.listTools(ctx);
|
|
120
|
+
// Names not reviewed never leave this boundary, including collisions
|
|
121
|
+
// with the REST complement. The registry alone applies verdicts.
|
|
122
|
+
return [...facts.filter((tool) => Object.hasOwn(HOSTED_ROUTES, tool.name)), ...complement];
|
|
123
|
+
},
|
|
124
|
+
async callTool(name, args, ctx, callOptions) {
|
|
125
|
+
if (Object.hasOwn(HOSTED_ROUTES, name)) {
|
|
126
|
+
const route = hostedTarget(name, args, policy);
|
|
127
|
+
const token = await auth.repo(route.target, route.permissions, ctx);
|
|
128
|
+
return withHosted(token, ctx, (client) => client.callTool(name, args, ctx, callOptions));
|
|
129
|
+
}
|
|
130
|
+
if (!Object.hasOwn(CLASSIFICATION.tools, name))
|
|
131
|
+
throw new ConnectorCallError("invalid_args", "This GitHub tool is not in the reviewed allowlist.");
|
|
132
|
+
const result = await rest.callTool(name, args, ctx, callOptions);
|
|
133
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result };
|
|
134
|
+
},
|
|
135
|
+
async status() {
|
|
136
|
+
return { state: "ok", message: "GitHub App configured; installation access is resolved lazily on use." };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// GitHub downloads PKCS#1 PEMs. WebCrypto imports PKCS#8, so wrap the RSA
|
|
2
|
+
// sequence without interpreting or reserializing its private integers.
|
|
3
|
+
function sequence(tag, content) {
|
|
4
|
+
let length = content.length;
|
|
5
|
+
const octets = [];
|
|
6
|
+
if (length < 128)
|
|
7
|
+
octets.push(length);
|
|
8
|
+
else {
|
|
9
|
+
while (length) {
|
|
10
|
+
octets.unshift(length & 255);
|
|
11
|
+
length >>>= 8;
|
|
12
|
+
}
|
|
13
|
+
octets.unshift(128 | octets.length);
|
|
14
|
+
}
|
|
15
|
+
return new Uint8Array([tag, ...octets, ...content]);
|
|
16
|
+
}
|
|
17
|
+
function element(bytes, offset) {
|
|
18
|
+
const tag = bytes[offset++];
|
|
19
|
+
let length = bytes[offset++];
|
|
20
|
+
if (tag === undefined || length === undefined)
|
|
21
|
+
throw new Error("invalid DER");
|
|
22
|
+
if (length & 128) {
|
|
23
|
+
const count = length & 127;
|
|
24
|
+
if (!count || count > 4 || offset + count > bytes.length)
|
|
25
|
+
throw new Error("invalid DER");
|
|
26
|
+
length = 0;
|
|
27
|
+
for (let i = 0; i < count; i++)
|
|
28
|
+
length = length * 256 + bytes[offset++];
|
|
29
|
+
}
|
|
30
|
+
if (offset + length > bytes.length)
|
|
31
|
+
throw new Error("invalid DER");
|
|
32
|
+
return { tag, start: offset, end: offset + length };
|
|
33
|
+
}
|
|
34
|
+
const ALGORITHM = new Uint8Array([
|
|
35
|
+
0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00,
|
|
36
|
+
]);
|
|
37
|
+
export function privateKeyDer(pem) {
|
|
38
|
+
try {
|
|
39
|
+
const match = /^-----BEGIN (RSA PRIVATE KEY|PRIVATE KEY)-----\s*([A-Za-z0-9+/=\s]+)\s*-----END \1-----$/.exec(pem.replace(/\\n/g, "\n").trim());
|
|
40
|
+
if (!match)
|
|
41
|
+
throw new Error("invalid PEM");
|
|
42
|
+
const binary = atob(match[2].replace(/\s/g, ""));
|
|
43
|
+
const der = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
44
|
+
const outer = element(der, 0);
|
|
45
|
+
if (outer.tag !== 0x30 || outer.end !== der.length)
|
|
46
|
+
throw new Error("invalid DER");
|
|
47
|
+
const version = element(der, outer.start);
|
|
48
|
+
if (version.tag !== 2 || version.end - version.start !== 1 || der[version.start] !== 0)
|
|
49
|
+
throw new Error("invalid version");
|
|
50
|
+
let rsa = der;
|
|
51
|
+
if (match[1] === "PRIVATE KEY") {
|
|
52
|
+
const algorithm = element(der, version.end);
|
|
53
|
+
if (algorithm.end - version.end !== ALGORITHM.length ||
|
|
54
|
+
!ALGORITHM.every((byte, i) => byte === der[version.end + i]))
|
|
55
|
+
throw new Error("not RSA");
|
|
56
|
+
const octet = element(der, algorithm.end);
|
|
57
|
+
if (octet.tag !== 4 || octet.end !== outer.end)
|
|
58
|
+
throw new Error("invalid PKCS8");
|
|
59
|
+
rsa = der.slice(octet.start, octet.end);
|
|
60
|
+
}
|
|
61
|
+
const root = element(rsa, 0);
|
|
62
|
+
let offset = root.start;
|
|
63
|
+
const integers = [];
|
|
64
|
+
while (offset < root.end) {
|
|
65
|
+
const integer = element(rsa, offset);
|
|
66
|
+
if (integer.tag !== 2 || integer.end <= integer.start || integer.end > root.end)
|
|
67
|
+
throw new Error("invalid RSA");
|
|
68
|
+
integers.push(integer);
|
|
69
|
+
offset = integer.end;
|
|
70
|
+
}
|
|
71
|
+
if (root.tag !== 0x30 || root.end !== rsa.length || integers.length !== 9 || rsa[integers[0].start] !== 0)
|
|
72
|
+
throw new Error("invalid RSA");
|
|
73
|
+
const modulus = integers[1];
|
|
74
|
+
const first = modulus.start + (rsa[modulus.start] === 0 ? 1 : 0);
|
|
75
|
+
const bits = (modulus.end - first - 1) * 8 + 32 - Math.clz32(rsa[first]);
|
|
76
|
+
if (bits < 2048)
|
|
77
|
+
throw new Error("weak RSA");
|
|
78
|
+
if (match[1] === "PRIVATE KEY")
|
|
79
|
+
return der;
|
|
80
|
+
return sequence(0x30, new Uint8Array([2, 1, 0, ...ALGORITHM, ...sequence(4, rsa)]));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new Error("app.privateKey to be a complete PKCS#1 or PKCS#8 RSA PEM with at least 2048 bits.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function base64url(bytes) {
|
|
87
|
+
let text = "";
|
|
88
|
+
for (const byte of bytes)
|
|
89
|
+
text += String.fromCharCode(byte);
|
|
90
|
+
return btoa(text).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
91
|
+
}
|
|
92
|
+
export async function appJwt(appId, der, now) {
|
|
93
|
+
const encoder = new TextEncoder();
|
|
94
|
+
const seconds = Math.floor(now / 1000);
|
|
95
|
+
const data = `${base64url(encoder.encode(JSON.stringify({ alg: "RS256", typ: "JWT" })))}.${base64url(encoder.encode(JSON.stringify({ iat: seconds - 60, exp: seconds + 540, iss: appId })))}`;
|
|
96
|
+
const key = await crypto.subtle.importKey("pkcs8", der, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, [
|
|
97
|
+
"sign",
|
|
98
|
+
]);
|
|
99
|
+
return `${data}.${base64url(new Uint8Array(await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, encoder.encode(data))))}`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { api } from "../../connectors/api.js";
|
|
2
|
+
import { ConnectorCallError } from "../../errors.js";
|
|
3
|
+
import { target } from "./scope.js";
|
|
4
|
+
const text = (description) => ({ type: "string", minLength: 1, description });
|
|
5
|
+
const ownerRepo = { owner: text("Configured repository owner."), repo: text("Repository name under that owner.") };
|
|
6
|
+
const object = (properties, required = []) => ({
|
|
7
|
+
type: "object",
|
|
8
|
+
properties,
|
|
9
|
+
required,
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
});
|
|
12
|
+
const integer = (description) => ({ type: "integer", minimum: 1, description });
|
|
13
|
+
const bool = (description) => ({ type: "boolean", description });
|
|
14
|
+
const jsonObject = { type: "object", additionalProperties: true };
|
|
15
|
+
const scopeRecord = object({
|
|
16
|
+
org: { type: "string" },
|
|
17
|
+
repo: { type: "string" },
|
|
18
|
+
access: { type: "string", enum: ["read", "read-write"] },
|
|
19
|
+
workflows: { type: "string", enum: ["write"] },
|
|
20
|
+
}, ["access"]);
|
|
21
|
+
const selector = object({
|
|
22
|
+
org: text("Configured org scope."),
|
|
23
|
+
repo: text("Exact owner/repo within configured scope."),
|
|
24
|
+
});
|
|
25
|
+
const repoPath = (t) => `/repos/${encodeURIComponent(t.owner)}/${encodeURIComponent(t.repo)}`;
|
|
26
|
+
const row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
27
|
+
function selectors(input, policy) {
|
|
28
|
+
if (!Array.isArray(input) || input.length === 0 || input.length > 100)
|
|
29
|
+
throw new ConnectorCallError("invalid_args", "Select between 1 and 100 configured scopes.");
|
|
30
|
+
return input.map((item) => {
|
|
31
|
+
const value = row(item);
|
|
32
|
+
if ((value.org === undefined) === (value.repo === undefined))
|
|
33
|
+
throw new ConnectorCallError("invalid_args", "Each selector names either org or repo.");
|
|
34
|
+
if (value.org !== undefined) {
|
|
35
|
+
const owner = policy.owner(value.org);
|
|
36
|
+
const grant = policy.scopes.find((scope) => scope.org === owner);
|
|
37
|
+
if (!grant)
|
|
38
|
+
throw new ConnectorCallError("invalid_args", "This org is not configured for org-wide access.");
|
|
39
|
+
return grant;
|
|
40
|
+
}
|
|
41
|
+
const parts = typeof value.repo === "string" ? value.repo.split("/") : [];
|
|
42
|
+
const t = target(parts[0], parts.length === 2 ? parts[1] : undefined);
|
|
43
|
+
const grant = policy.scope(t);
|
|
44
|
+
return { repo: `${t.owner}/${t.repo}`, access: grant.access };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function searchPartitions(scopes) {
|
|
48
|
+
const orgs = new Set(scopes.flatMap((scope) => (scope.org ? [scope.org] : [])));
|
|
49
|
+
const repos = new Map();
|
|
50
|
+
for (const scope of scopes) {
|
|
51
|
+
if (!scope.repo)
|
|
52
|
+
continue;
|
|
53
|
+
const [owner, repo] = scope.repo.split("/");
|
|
54
|
+
if (orgs.has(owner))
|
|
55
|
+
continue;
|
|
56
|
+
if (!repos.has(owner))
|
|
57
|
+
repos.set(owner, new Set());
|
|
58
|
+
repos.get(owner).add(repo);
|
|
59
|
+
}
|
|
60
|
+
const partitions = [...orgs].sort().map((owner) => ({ id: `org:${owner}`, owner, qualifier: `org:${owner}` }));
|
|
61
|
+
const result = [...partitions];
|
|
62
|
+
for (const [owner, names] of [...repos].sort(([a], [b]) => a.localeCompare(b))) {
|
|
63
|
+
const sorted = [...names].sort();
|
|
64
|
+
for (let offset = 0; offset < sorted.length; offset += 5) {
|
|
65
|
+
const chunk = sorted.slice(offset, offset + 5);
|
|
66
|
+
result.push({
|
|
67
|
+
id: chunk.map((repo) => `${owner}/${repo}`).join(","),
|
|
68
|
+
owner,
|
|
69
|
+
repos: chunk,
|
|
70
|
+
qualifier: chunk.map((repo) => `repo:${owner}/${repo}`).join(" "),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (result.length > 20)
|
|
75
|
+
throw new ConnectorCallError("invalid_args", "Search fan-out exceeds 20 partitions. Select fewer scopes.");
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function resultTarget(item, kind) {
|
|
79
|
+
if (kind === "repositories" || kind === "code") {
|
|
80
|
+
const name = kind === "code" ? row(item.repository).full_name : item.full_name;
|
|
81
|
+
const parts = typeof name === "string" ? name.split("/") : [];
|
|
82
|
+
return target(parts[0], parts.length === 2 ? parts[1] : undefined);
|
|
83
|
+
}
|
|
84
|
+
let url;
|
|
85
|
+
try {
|
|
86
|
+
url = new URL(item.repository_url);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new ConnectorCallError("connector_call_failed", "GitHub search omitted the repository binding.");
|
|
90
|
+
}
|
|
91
|
+
const match = /^\/repos\/([^/]+)\/([^/]+)$/.exec(url.pathname);
|
|
92
|
+
if (url.origin !== "https://api.github.com" || !match || url.search || url.hash)
|
|
93
|
+
throw new ConnectorCallError("connector_call_failed", "GitHub search returned an invalid repository binding.");
|
|
94
|
+
return target(match[1], match[2]);
|
|
95
|
+
}
|
|
96
|
+
export function restConnector(id, auth, policy) {
|
|
97
|
+
const scopedRequest = async (args, permissions, method, suffix, body, ctx) => {
|
|
98
|
+
const t = target(args.owner, args.repo);
|
|
99
|
+
policy.scope(t, true);
|
|
100
|
+
const token = await auth.repo(t, permissions, ctx);
|
|
101
|
+
const data = await auth.json({ method, path: `${repoPath(t)}${suffix}`, ...(body !== undefined ? { body } : {}) }, token, ctx);
|
|
102
|
+
return data ?? { deleted: true };
|
|
103
|
+
};
|
|
104
|
+
const tools = [
|
|
105
|
+
{
|
|
106
|
+
name: "list_scopes",
|
|
107
|
+
description: "List configured GitHub scopes and reachable repositories for one owner.",
|
|
108
|
+
annotations: { readOnlyHint: true },
|
|
109
|
+
inputSchema: object({
|
|
110
|
+
owner: text("Configured owner; omit to return configuration without network access."),
|
|
111
|
+
page: integer("Repository page, default 1, at most 1000."),
|
|
112
|
+
per_page: { ...integer("Repositories per page, default 100."), maximum: 100 },
|
|
113
|
+
}),
|
|
114
|
+
outputSchema: object({
|
|
115
|
+
acting_as: { type: "string" },
|
|
116
|
+
scopes: { type: "array", items: scopeRecord },
|
|
117
|
+
repositories: { type: "array", items: { type: "string" } },
|
|
118
|
+
next_page: { type: ["integer", "null"] },
|
|
119
|
+
}, ["acting_as", "scopes"]),
|
|
120
|
+
async handler(args, ctx) {
|
|
121
|
+
if (args.owner === undefined)
|
|
122
|
+
return { acting_as: "GitHub App", scopes: policy.scopes };
|
|
123
|
+
const owner = policy.owner(args.owner);
|
|
124
|
+
const page = args.page ?? 1;
|
|
125
|
+
const count = args.per_page ?? 100;
|
|
126
|
+
if (page > 1000)
|
|
127
|
+
throw new ConnectorCallError("invalid_args", "Repository page must be at most 1000.");
|
|
128
|
+
const token = await auth.token(owner, undefined, { metadata: "read" }, ctx);
|
|
129
|
+
const data = row(await auth.json({ method: "GET", path: "/installation/repositories", query: { page, per_page: count } }, token, ctx));
|
|
130
|
+
if (!Array.isArray(data.repositories))
|
|
131
|
+
throw new ConnectorCallError("connector_call_failed", "GitHub returned an invalid repository list.");
|
|
132
|
+
const repositories = [];
|
|
133
|
+
for (const item of data.repositories) {
|
|
134
|
+
const repository = row(item);
|
|
135
|
+
const t = resultTarget(repository, "repositories");
|
|
136
|
+
if (t.owner !== owner)
|
|
137
|
+
throw new ConnectorCallError("connector_call_failed", "GitHub returned a repository from another installation owner.");
|
|
138
|
+
await auth.checkResult(t, repository.id, token, ctx);
|
|
139
|
+
repositories.push(`${t.owner}/${t.repo}`);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
acting_as: "GitHub App",
|
|
143
|
+
scopes: policy.scopes.filter((scope) => scope.org === owner || scope.repo?.startsWith(`${owner}/`)),
|
|
144
|
+
repositories,
|
|
145
|
+
next_page: repositories.length === count ? page + 1 : null,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "search_scoped",
|
|
151
|
+
description: "Search GitHub repositories, issues, pull requests or code within configured scopes.",
|
|
152
|
+
annotations: { readOnlyHint: true },
|
|
153
|
+
inputSchema: object({
|
|
154
|
+
terms: text("Literal words only; scope qualifiers and Boolean operators are refused."),
|
|
155
|
+
kind: {
|
|
156
|
+
type: "string",
|
|
157
|
+
enum: ["repositories", "issues", "pull-requests", "code"],
|
|
158
|
+
description: "Resource to search.",
|
|
159
|
+
},
|
|
160
|
+
scopes: {
|
|
161
|
+
type: "array",
|
|
162
|
+
minItems: 1,
|
|
163
|
+
maxItems: 100,
|
|
164
|
+
items: selector,
|
|
165
|
+
description: "Optional subset; defaults to all configured scopes.",
|
|
166
|
+
},
|
|
167
|
+
pages: {
|
|
168
|
+
type: "object",
|
|
169
|
+
additionalProperties: { type: "integer", minimum: 1, maximum: 10 },
|
|
170
|
+
description: "Page per returned partition ID, at most 10.",
|
|
171
|
+
},
|
|
172
|
+
per_page: { ...integer("Results per partition, default 30."), maximum: 100 },
|
|
173
|
+
}, ["terms", "kind"]),
|
|
174
|
+
outputSchema: object({ partitions: { type: "array", items: jsonObject }, incomplete_results: { type: "boolean" } }, ["partitions", "incomplete_results"]),
|
|
175
|
+
async handler(args, ctx) {
|
|
176
|
+
if (!/^[a-z0-9_. -]{1,128}$/i.test(args.terms) || /\b(?:OR|NOT|AND)\b/i.test(args.terms))
|
|
177
|
+
throw new ConnectorCallError("invalid_args", "Search terms must be literal words, without qualifiers or Boolean operators.");
|
|
178
|
+
const selected = args.scopes === undefined ? policy.scopes : selectors(args.scopes, policy);
|
|
179
|
+
const partitions = searchPartitions(selected);
|
|
180
|
+
if (Object.keys(args.pages ?? {}).some((key) => !partitions.some((partition) => partition.id === key)))
|
|
181
|
+
throw new ConnectorCallError("invalid_args", "Pagination keys must match the selected search partitions.");
|
|
182
|
+
const type = args.kind === "issues" ? " is:issue" : args.kind === "pull-requests" ? " is:pr" : "";
|
|
183
|
+
const queries = partitions.map((partition) => `${partition.qualifier} ${args.terms.trim()}${type}`);
|
|
184
|
+
if (queries.some((query) => query.length > 256))
|
|
185
|
+
throw new ConnectorCallError("invalid_args", "The scoped search query exceeds GitHub's 256-character bound. Select fewer repositories.");
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const output = [];
|
|
188
|
+
let incomplete = false;
|
|
189
|
+
for (const partition of partitions) {
|
|
190
|
+
const page = args.pages?.[partition.id] ?? 1;
|
|
191
|
+
const count = args.per_page ?? 30;
|
|
192
|
+
const permissions = args.kind === "repositories"
|
|
193
|
+
? { metadata: "read" }
|
|
194
|
+
: args.kind === "code"
|
|
195
|
+
? { contents: "read" }
|
|
196
|
+
: { issues: "read", pull_requests: "read" };
|
|
197
|
+
const token = await auth.token(partition.owner, partition.repos, permissions, ctx);
|
|
198
|
+
const kind = args.kind === "pull-requests" ? "issues" : args.kind;
|
|
199
|
+
const query = `${partition.qualifier} ${args.terms.trim()}${type}`;
|
|
200
|
+
const data = row(await auth.json({ method: "GET", path: `/search/${kind}`, query: { q: query, page, per_page: count } }, token, ctx));
|
|
201
|
+
if (!Array.isArray(data.items))
|
|
202
|
+
throw new ConnectorCallError("connector_call_failed", "GitHub returned an invalid search result.");
|
|
203
|
+
const items = [];
|
|
204
|
+
for (const value of data.items) {
|
|
205
|
+
const item = row(value);
|
|
206
|
+
const t = resultTarget(item, args.kind);
|
|
207
|
+
policy.owner(t.owner);
|
|
208
|
+
if (t.owner !== partition.owner)
|
|
209
|
+
throw new ConnectorCallError("connector_call_failed", "GitHub search escaped its selected partition; results were withheld.");
|
|
210
|
+
await auth.checkResult(t, args.kind === "repositories" ? item.id : args.kind === "code" ? row(item.repository).id : undefined, token, ctx);
|
|
211
|
+
const key = `${args.kind}:${t.owner}/${t.repo}:${args.kind === "code" ? item.path : item.id}`;
|
|
212
|
+
if (!seen.has(key)) {
|
|
213
|
+
seen.add(key);
|
|
214
|
+
items.push(value);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const more = data.items.length === count || data.total_count > page * count;
|
|
218
|
+
const limited = data.incomplete_results === true || (more && page === 10);
|
|
219
|
+
incomplete ||= limited;
|
|
220
|
+
output.push({
|
|
221
|
+
scope: partition.id,
|
|
222
|
+
items,
|
|
223
|
+
total_count: data.total_count,
|
|
224
|
+
incomplete_results: limited,
|
|
225
|
+
next_page: more && page < 10 ? page + 1 : null,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return { partitions: output, incomplete_results: incomplete };
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "create_release",
|
|
233
|
+
description: "Create a GitHub release, optionally as a draft.",
|
|
234
|
+
annotations: { readOnlyHint: false },
|
|
235
|
+
inputSchema: object({
|
|
236
|
+
...ownerRepo,
|
|
237
|
+
tag_name: text("Release tag."),
|
|
238
|
+
target_commitish: text("Branch or commit for a new tag."),
|
|
239
|
+
name: text("Release title."),
|
|
240
|
+
body: { type: "string", description: "Release notes." },
|
|
241
|
+
draft: bool("Create a draft release."),
|
|
242
|
+
prerelease: bool("Mark as a prerelease."),
|
|
243
|
+
}, ["owner", "repo", "tag_name"]),
|
|
244
|
+
outputSchema: jsonObject,
|
|
245
|
+
handler: (args, ctx) => {
|
|
246
|
+
const { owner: _owner, repo: _repo, ...body } = args;
|
|
247
|
+
return scopedRequest(args, { contents: "write" }, "POST", "/releases", body, ctx);
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "update_release",
|
|
252
|
+
description: "Update or publish an existing GitHub release.",
|
|
253
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
254
|
+
inputSchema: object({
|
|
255
|
+
...ownerRepo,
|
|
256
|
+
release_id: integer("Release ID in this repository."),
|
|
257
|
+
tag_name: text("Release tag."),
|
|
258
|
+
name: text("Release title."),
|
|
259
|
+
body: { type: "string", description: "Release notes." },
|
|
260
|
+
draft: bool("Set false to publish a draft."),
|
|
261
|
+
prerelease: bool("Mark as a prerelease."),
|
|
262
|
+
}, ["owner", "repo", "release_id"]),
|
|
263
|
+
outputSchema: jsonObject,
|
|
264
|
+
handler: (args, ctx) => {
|
|
265
|
+
const { owner: _owner, repo: _repo, release_id, ...body } = args;
|
|
266
|
+
return scopedRequest(args, { contents: "write" }, "PATCH", `/releases/${release_id}`, body, ctx);
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "delete_release",
|
|
271
|
+
description: "Delete a GitHub release from the scoped repository.",
|
|
272
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
273
|
+
inputSchema: object({ ...ownerRepo, release_id: integer("Release ID in this repository.") }, [
|
|
274
|
+
"owner",
|
|
275
|
+
"repo",
|
|
276
|
+
"release_id",
|
|
277
|
+
]),
|
|
278
|
+
outputSchema: jsonObject,
|
|
279
|
+
handler: (args, ctx) => scopedRequest(args, { contents: "write" }, "DELETE", `/releases/${args.release_id}`, undefined, ctx),
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "merge_pull_request",
|
|
283
|
+
description: "Merge a same-repository GitHub pull request with explicit workflow-write scope.",
|
|
284
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
285
|
+
inputSchema: object({
|
|
286
|
+
...ownerRepo,
|
|
287
|
+
pull_number: integer("Pull request number."),
|
|
288
|
+
sha: text("Expected head commit SHA; reread after a conflict."),
|
|
289
|
+
merge_method: {
|
|
290
|
+
type: "string",
|
|
291
|
+
enum: ["merge", "squash", "rebase"],
|
|
292
|
+
description: "Merge strategy, default merge.",
|
|
293
|
+
},
|
|
294
|
+
}, ["owner", "repo", "pull_number", "sha"]),
|
|
295
|
+
outputSchema: jsonObject,
|
|
296
|
+
async handler(args, ctx) {
|
|
297
|
+
const t = target(args.owner, args.repo);
|
|
298
|
+
// GitHub's atomic sha guard binds only the head, not the base/ref.
|
|
299
|
+
// Retargeting can turn a file-safe diff into workflow changes. Require
|
|
300
|
+
// explicit workflow permission before any authentication for merges.
|
|
301
|
+
policy.scope(t, true, true);
|
|
302
|
+
const reader = await auth.repo(t, { pull_requests: "read", contents: "read" }, ctx);
|
|
303
|
+
const pr = row(await auth.json({ method: "GET", path: `${repoPath(t)}/pulls/${args.pull_number}` }, reader, ctx));
|
|
304
|
+
const head = row(pr.head);
|
|
305
|
+
const base = row(pr.base);
|
|
306
|
+
if (head.sha !== args.sha ||
|
|
307
|
+
row(head.repo).full_name?.toLowerCase() !== `${t.owner}/${t.repo}` ||
|
|
308
|
+
row(base.repo).full_name?.toLowerCase() !== `${t.owner}/${t.repo}`)
|
|
309
|
+
throw new ConnectorCallError("conflict", "Merge requires the expected head SHA and a same-repository head and base. Reread the pull request.");
|
|
310
|
+
const token = await auth.repo(t, { contents: "write", pull_requests: "write", workflows: "write" }, ctx);
|
|
311
|
+
// GitHub verifies sha atomically. No automatic write retry.
|
|
312
|
+
return auth.json({
|
|
313
|
+
method: "PUT",
|
|
314
|
+
path: `${repoPath(t)}/pulls/${args.pull_number}/merge`,
|
|
315
|
+
body: { sha: args.sha, merge_method: args.merge_method ?? "merge" },
|
|
316
|
+
}, token, ctx);
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
return api(id, { tools });
|
|
321
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type GitHubScope = ({
|
|
2
|
+
org: string;
|
|
3
|
+
repo?: never;
|
|
4
|
+
} | {
|
|
5
|
+
repo: string;
|
|
6
|
+
org?: never;
|
|
7
|
+
}) & {
|
|
8
|
+
access: "read" | "read-write";
|
|
9
|
+
/** Allow edits under .github/workflows. Omitted by default. */
|
|
10
|
+
workflows?: "write";
|
|
11
|
+
};
|
|
12
|
+
export interface Target {
|
|
13
|
+
owner: string;
|
|
14
|
+
repo: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function target(owner: unknown, repo: unknown): Target;
|
|
17
|
+
export declare function parseScopes(input: readonly GitHubScope[]): readonly GitHubScope[];
|
|
18
|
+
export declare class ScopePolicy {
|
|
19
|
+
readonly scopes: readonly GitHubScope[];
|
|
20
|
+
private readonly aliases;
|
|
21
|
+
constructor(scopes: readonly GitHubScope[]);
|
|
22
|
+
scope(t: Target, write?: boolean, workflows?: boolean): GitHubScope;
|
|
23
|
+
bindAlias(t: Target, grant: GitHubScope): void;
|
|
24
|
+
summary(): string;
|
|
25
|
+
owner(owner: unknown): string;
|
|
26
|
+
repositories(owner: string): string[] | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Git paths are literal, canonical paths. Reject aliases before authentication. */
|
|
29
|
+
export declare function workflowPath(path: unknown): boolean;
|