@wardby/cli 0.7.0 → 0.8.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/.env.example +5 -5
- package/README.md +6 -1
- package/deploy/local/docker-compose.quickstart-coding.yml +6 -0
- package/dist/claude-coding-worker/driver.d.ts +12 -0
- package/dist/claude-coding-worker/driver.js +29 -7
- package/dist/claude-coding-worker/main.js +5 -1
- package/dist/claude-coding-worker/sdk.js +17 -1
- package/dist/claude-plugin/command.d.ts +61 -0
- package/dist/claude-plugin/command.js +376 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +2 -1
- package/dist/cli.js +47 -3
- package/dist/coding/code-host.d.ts +16 -0
- package/dist/coding/code-host.js +60 -0
- package/dist/coding/mcp-token.d.ts +12 -0
- package/dist/coding/mcp-token.js +26 -0
- package/dist/coding/protocol.js +2 -0
- package/dist/coding/redact.js +6 -1
- package/dist/coding/transcript/recorder.d.ts +5 -4
- package/dist/coding/transcript/recorder.js +1 -1
- package/dist/coding-worker/debug-trace.d.ts +3 -1
- package/dist/coding-worker/debug-trace.js +4 -2
- package/dist/coding-worker/driver.js +8 -1
- package/dist/coding-worker/main.js +5 -1
- package/dist/coding-worker/mcp-config.d.ts +27 -0
- package/dist/coding-worker/mcp-config.js +32 -0
- package/dist/coding-worker/sdk.d.ts +36 -0
- package/dist/coding-worker/sdk.js +28 -1
- package/dist/coding-worker/types.d.ts +4 -0
- package/dist/config/mcp-call-limit.d.ts +8 -0
- package/dist/config/mcp-call-limit.js +18 -0
- package/dist/config/mcp-servers.d.ts +8 -0
- package/dist/config/mcp-servers.js +12 -0
- package/dist/config/test-simulators.d.ts +10 -0
- package/dist/config/test-simulators.js +28 -0
- package/dist/config/tool-timeout.d.ts +1 -0
- package/dist/config/tool-timeout.js +18 -0
- package/dist/core/coding-mcp.d.ts +36 -0
- package/dist/core/coding-mcp.js +253 -0
- package/dist/core/coding-repository.d.ts +46 -0
- package/dist/core/coding-repository.js +92 -0
- package/dist/core/delegation-slots.d.ts +27 -0
- package/dist/core/delegation-slots.js +44 -0
- package/dist/core/dispatch.d.ts +6 -2
- package/dist/core/dispatch.js +36 -11
- package/dist/core/error-code.js +1 -0
- package/dist/core/grants.d.ts +28 -3
- package/dist/core/grants.js +46 -4
- package/dist/core/host-events.js +7 -3
- package/dist/core/host-status.js +2 -2
- package/dist/core/issue-status.js +4 -3
- package/dist/core/mark-ready.d.ts +1 -1
- package/dist/core/mark-ready.js +1 -1
- package/dist/core/mcp-client/client.d.ts +47 -0
- package/dist/core/mcp-client/client.js +141 -0
- package/dist/core/mcp-client/jsonrpc.d.ts +15 -0
- package/dist/core/mcp-client/jsonrpc.js +54 -0
- package/dist/core/mcp-client/results.d.ts +13 -0
- package/dist/core/mcp-client/results.js +42 -0
- package/dist/core/mcp-servers.d.ts +90 -0
- package/dist/core/mcp-servers.js +230 -0
- package/dist/core/mcp-tools.d.ts +108 -0
- package/dist/core/mcp-tools.js +287 -0
- package/dist/core/merge-order-check.js +38 -19
- package/dist/core/notification-dispatcher.js +2 -1
- package/dist/core/related-pull-requests.js +26 -10
- package/dist/core/review-fix.js +2 -2
- package/dist/core/review-host-tools.js +3 -1
- package/dist/core/runner.d.ts +66 -16
- package/dist/core/runner.js +281 -73
- package/dist/core/secret-broker-config.d.ts +2 -0
- package/dist/core/secret-broker-config.js +1 -1
- package/dist/core/secret-guard.d.ts +5 -2
- package/dist/core/secret-guard.js +22 -7
- package/dist/core/secrets.d.ts +11 -1
- package/dist/core/secrets.js +27 -1
- package/dist/core/serializable-retry.d.ts +18 -0
- package/dist/core/serializable-retry.js +43 -0
- package/dist/core/tool-admin.d.ts +5 -1
- package/dist/core/tool-admin.js +7 -2
- package/dist/core/tool-names.d.ts +13 -0
- package/dist/core/tool-names.js +20 -0
- package/dist/core/webhooks.d.ts +4 -0
- package/dist/core/webhooks.js +68 -49
- package/dist/core/workflow-run-events.js +2 -1
- package/dist/generated/prisma/browser.d.ts +31 -0
- package/dist/generated/prisma/client.d.ts +31 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +50 -50
- package/dist/generated/prisma/internal/class.d.ts +66 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +530 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +75 -2
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +79 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +75 -2
- package/dist/generated/prisma/models/Agent.d.ts +271 -0
- package/dist/generated/prisma/models/AgentMcpServer.d.ts +1412 -0
- package/dist/generated/prisma/models/AgentMcpServer.js +1 -0
- package/dist/generated/prisma/models/AgentRepository.d.ts +40 -1
- package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +0 -7
- package/dist/generated/prisma/models/CodingProxySession.d.ts +119 -2
- package/dist/generated/prisma/models/CodingRun.d.ts +76 -1
- package/dist/generated/prisma/models/HostProvider.d.ts +2176 -0
- package/dist/generated/prisma/models/HostProvider.js +1 -0
- package/dist/generated/prisma/models/HostProviderCredential.d.ts +1285 -0
- package/dist/generated/prisma/models/HostProviderCredential.js +1 -0
- package/dist/generated/prisma/models/HostProviderOperation.d.ts +1470 -0
- package/dist/generated/prisma/models/HostProviderOperation.js +1 -0
- package/dist/generated/prisma/models/HostProviderTrustedAccount.d.ts +1145 -0
- package/dist/generated/prisma/models/HostProviderTrustedAccount.js +1 -0
- package/dist/generated/prisma/models/McpServer.d.ts +1500 -0
- package/dist/generated/prisma/models/McpServer.js +1 -0
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
- package/dist/generated/prisma/models/Principal.d.ts +167 -0
- package/dist/generated/prisma/models/ResourceGrant.d.ts +1 -1
- package/dist/generated/prisma/models/Secret.d.ts +387 -0
- package/dist/generated/prisma/models/Tool.d.ts +223 -1
- package/dist/generated/prisma/models.d.ts +6 -0
- package/dist/help/search.js +12 -3
- package/dist/help-index.json +719 -47
- package/dist/import/create.js +16 -1
- package/dist/import/neutral-schema.d.ts +1 -0
- package/dist/import/neutral-schema.js +2 -0
- package/dist/mcp/auth/grants-cli.d.ts +6 -4
- package/dist/mcp/auth/grants-cli.js +42 -3
- package/dist/mcp/auth/resource-server.d.ts +3 -1
- package/dist/mcp/auth/resource-server.js +17 -2
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/tools/agents.js +100 -15
- package/dist/mcp/tools/grants.js +66 -28
- package/dist/mcp/tools/host-providers.d.ts +2 -0
- package/dist/mcp/tools/host-providers.js +215 -0
- package/dist/mcp/tools/mcp-servers.d.ts +26 -0
- package/dist/mcp/tools/mcp-servers.js +481 -0
- package/dist/mcp/tools/repositories.js +50 -10
- package/dist/mcp/tools/runs.js +178 -18
- package/dist/mcp/tools/secret-elicitation.d.ts +2 -0
- package/dist/mcp/tools/secret-elicitation.js +25 -2
- package/dist/mcp/tools/secrets.js +38 -4
- package/dist/mcp/tools/tools.d.ts +6 -4
- package/dist/mcp/tools/tools.js +40 -8
- package/dist/mcp/tools/trigger.js +28 -6
- package/dist/mcp/transport/http-limits.d.ts +2 -0
- package/dist/mcp/transport/http-limits.js +6 -2
- package/dist/mcp/webhooks/ingress.js +10 -0
- package/dist/native-worker/gateway-server.d.ts +14 -0
- package/dist/native-worker/gateway-server.js +28 -7
- package/dist/native-worker/gateway.d.ts +2 -0
- package/dist/native-worker/gateway.js +5 -1
- package/dist/native-worker/http-server.d.ts +4 -0
- package/dist/native-worker/http-server.js +51 -0
- package/dist/native-worker/ledger.d.ts +6 -2
- package/dist/native-worker/ledger.js +5 -4
- package/dist/native-worker/protocol.d.ts +1 -0
- package/dist/native-worker/protocol.js +4 -1
- package/dist/providers/coding-proxy/capability-hash.d.ts +2 -0
- package/dist/providers/coding-proxy/capability-hash.js +5 -0
- package/dist/providers/coding-proxy/mcp-route.d.ts +34 -0
- package/dist/providers/coding-proxy/mcp-route.js +166 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +2 -0
- package/dist/providers/coding-proxy/memory-ledger.js +15 -0
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +30 -2
- package/dist/providers/coding-proxy/proxy.d.ts +7 -1
- package/dist/providers/coding-proxy/proxy.js +134 -34
- package/dist/providers/coding-proxy/runtime.d.ts +6 -0
- package/dist/providers/coding-proxy/runtime.js +32 -1
- package/dist/providers/coding-proxy/server.d.ts +3 -0
- package/dist/providers/coding-proxy/server.js +10 -0
- package/dist/providers/coding-proxy/types.d.ts +13 -0
- package/dist/providers/executor/composition.js +5 -0
- package/dist/providers/executor/container.d.ts +33 -1
- package/dist/providers/executor/container.js +76 -7
- package/dist/providers/host-tools/invoke.d.ts +37 -0
- package/dist/providers/host-tools/invoke.js +117 -0
- package/dist/providers/host-tools/operations.d.ts +65 -0
- package/dist/providers/host-tools/operations.js +149 -0
- package/dist/providers/host-tools/registry.d.ts +79 -0
- package/dist/providers/host-tools/registry.js +362 -0
- package/dist/providers/host-tools/request-guard.d.ts +17 -0
- package/dist/providers/host-tools/request-guard.js +99 -0
- package/dist/providers/host-tools/self-test.d.ts +64 -0
- package/dist/providers/host-tools/self-test.js +142 -0
- package/dist/providers/jobs/docker-isolation.d.ts +2 -2
- package/dist/providers/jobs/docker-isolation.js +30 -8
- package/dist/providers/jobs/docker.d.ts +8 -0
- package/dist/providers/jobs/docker.js +33 -9
- package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-isolation.js +15 -2
- package/dist/providers/jobs/kubernetes.d.ts +5 -0
- package/dist/providers/jobs/kubernetes.js +6 -1
- package/dist/providers/jobs/types.d.ts +6 -0
- package/dist/providers/review-host/types.d.ts +6 -4
- package/dist/providers/review-host/types.js +1 -1
- package/dist/providers/vcs/git.d.ts +6 -2
- package/dist/providers/vcs/git.js +15 -4
- package/dist/providers/vcs/github.d.ts +2 -71
- package/dist/providers/vcs/github.js +8 -11
- package/dist/providers/vcs/index.d.ts +1 -1
- package/dist/providers/vcs/index.js +1 -1
- package/dist/providers/vcs/remote.d.ts +4 -0
- package/dist/providers/vcs/remote.js +7 -1
- package/dist/providers/vcs/routing.d.ts +12 -6
- package/dist/providers/vcs/routing.js +21 -6
- package/dist/providers/vcs/types.d.ts +71 -3
- package/dist/quickstart/index.d.ts +5 -0
- package/dist/quickstart/index.js +21 -0
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/eval-core.d.ts +12 -2
- package/dist/sandbox/eval-core.js +20 -4
- package/dist/sandbox/host-functions.d.ts +4 -0
- package/dist/sandbox/host-functions.js +7 -2
- package/dist/sandbox/limits.d.ts +4 -1
- package/dist/sandbox/limits.js +4 -1
- package/dist/sandbox/run-in-sandbox.d.ts +6 -0
- package/dist/sandbox/run-in-sandbox.js +14 -2
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +1 -1
- package/dist/sandbox/secret-broker.d.ts +1 -0
- package/dist/sandbox/secret-broker.js +1 -1
- package/dist/sandbox/tool-timeout.d.ts +32 -0
- package/dist/sandbox/tool-timeout.js +52 -0
- package/dist/sandbox/user-tool.d.ts +8 -1
- package/dist/sandbox/user-tool.js +9 -3
- package/dist/viewer/graph.js +2 -1
- package/dist/wardby-bin.js +5 -0
- package/docs/README.md +11 -1
- package/docs/agent-recipes.md +37 -4
- package/docs/architecture-runtime.md +1 -1
- package/docs/claude-code-plugin.md +164 -0
- package/docs/code-review-agents.md +6 -0
- package/docs/coding-agent-setup.md +77 -1
- package/docs/coding-worker-byo-images.md +4 -2
- package/docs/coding-worker-isolation.md +78 -7
- package/docs/debugging-agent-runs.md +242 -0
- package/docs/getting-started-gke.md +5 -4
- package/docs/getting-started-identity-provider.md +43 -37
- package/docs/getting-started.md +8 -0
- package/docs/host-providers.md +294 -0
- package/docs/mcp-servers.md +254 -0
- package/docs/native-sandbox.md +25 -9
- package/docs/security-deployment.md +110 -31
- package/docs/tool-secrets.md +3 -0
- package/docs/viewer-api.md +6 -0
- package/docs/viewer-api.openapi.yml +1 -1
- package/help/admin-viewer.md +4 -0
- package/help/agent-recipes.md +33 -2
- package/help/brokered-secrets.md +3 -0
- package/help/build-claude-code-image.md +1 -1
- package/help/claude-code-run-pane.md +40 -0
- package/help/errors/coding-mcp-pins-unavailable.md +35 -0
- package/help/errors/database-auth-invalid.md +1 -1
- package/help/errors/host-provider-error.md +40 -0
- package/help/errors/host-tool-failed.md +36 -0
- package/help/errors/mcp-call-limit.md +22 -0
- package/help/errors/mcp-call-timeout.md +22 -0
- package/help/errors/mcp-server-auth-failed.md +22 -0
- package/help/errors/mcp-server-credentials-unavailable.md +45 -0
- package/help/errors/mcp-server-error.md +28 -0
- package/help/errors/mcp-server-unreachable.md +22 -0
- package/help/errors/mcp-servers-off.md +21 -0
- package/help/errors/mcp-tool-not-allowed.md +26 -0
- package/help/errors/mcp-tool-removed.md +22 -0
- package/help/errors/mcp-unavailable.md +25 -0
- package/help/errors/native-sandbox-capacity.md +1 -1
- package/help/errors/native-sandbox-deadline-exceeded.md +1 -1
- package/help/errors/native-sandbox-docker-failed.md +1 -1
- package/help/errors/native-sandbox-gateway-error.md +1 -1
- package/help/errors/native-sandbox-gateway-unavailable.md +1 -1
- package/help/errors/native-sandbox-image-not-pinned.md +1 -1
- package/help/errors/native-sandbox-isolation-mismatch.md +1 -1
- package/help/errors/native-sandbox-network-unenforced.md +1 -1
- package/help/errors/native-sandbox-requires-catalog.md +1 -1
- package/help/errors/native-sandbox-unavailable.md +1 -1
- package/help/errors/native-sandbox-warm-delivery-failed.md +1 -1
- package/help/errors/native-sandbox-worker-exited.md +1 -1
- package/help/errors/native-sandbox-worker-lost.md +1 -1
- package/help/errors/native-sandbox-worker-unready.md +1 -1
- package/help/errors/repository-not-listed.md +36 -0
- package/help/errors/repository-required.md +36 -0
- package/help/errors/tool-timeout.md +38 -0
- package/help/host-providers.md +40 -0
- package/help/identity-and-access.md +4 -1
- package/help/mcp-servers.md +73 -0
- package/help/mcp.md +7 -2
- package/help/native-capabilities.md +27 -1
- package/help/native-sandbox.md +1 -1
- package/help/operating-agents.md +16 -0
- package/help/related-pull-requests.md +7 -3
- package/help/repo-instructions-and-skills.md +3 -1
- package/help/resume-runs-locally.md +3 -0
- package/help/stop-run.md +1 -1
- package/help/troubleshooting/coding-workers.md +9 -1
- package/help/troubleshooting/debugging-agent-runs.md +29 -0
- package/package.json +2 -1
- package/prisma/migrations/20261102000000_host_providers/migration.sql +56 -0
- package/prisma/migrations/20261104000000_coding_agent_repository_list/migration.sql +5 -0
- package/prisma/migrations/20261106000000_tool_timeout/migration.sql +4 -0
- package/prisma/migrations/20261107000000_mcp_servers/migration.sql +33 -0
- package/prisma/migrations/20261108000000_coding_mcp/migration.sql +5 -0
- package/prisma/migrations/20261109000000_task_run_index/migration.sql +3 -0
- package/prisma/schema.prisma +159 -14
- package/scripts/git-askpass.sh +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between Wardby and a host provider's bound tools. A bound tool makes
|
|
3
|
+
* one host operation; paging, markers, safeguards and wording stay in Wardby. Wardby
|
|
4
|
+
* calls each tool with `{ workspace, repo, repository, ...operation fields }`.
|
|
5
|
+
*
|
|
6
|
+
* A tool answers with its operation's result, or `{ error: <decline code> }` when the
|
|
7
|
+
* host refused. Results are untrusted and validated here, and a tool's own text never
|
|
8
|
+
* reaches a model, a run record or a comment.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
export const HOST_TOOL_CONTRACT_VERSION = 1;
|
|
12
|
+
/** What a tool may say about a host refusal: fixed words, so no tool text passes through. */
|
|
13
|
+
export const HOST_TOOL_DECLINE_CODES = [
|
|
14
|
+
"unauthorized",
|
|
15
|
+
"forbidden",
|
|
16
|
+
"not_found",
|
|
17
|
+
"conflict",
|
|
18
|
+
"rate_limited",
|
|
19
|
+
"unavailable",
|
|
20
|
+
];
|
|
21
|
+
// What a paged operation returns; its tool takes the last one back as the optional `page` input.
|
|
22
|
+
const page = z.string().nullable();
|
|
23
|
+
const pullRequest = z
|
|
24
|
+
.object({
|
|
25
|
+
number: z.number().int().positive(),
|
|
26
|
+
title: z.string(),
|
|
27
|
+
body: z.string(),
|
|
28
|
+
author: z.string().nullable(),
|
|
29
|
+
// A merged pull request is "closed" with `merged` set: consumers compare state to "open".
|
|
30
|
+
state: z.enum(["open", "closed"]),
|
|
31
|
+
merged: z.boolean(),
|
|
32
|
+
draft: z.boolean(),
|
|
33
|
+
baseRef: z.string(),
|
|
34
|
+
headRef: z.string(),
|
|
35
|
+
headSha: z.string().min(1),
|
|
36
|
+
isFork: z.boolean(),
|
|
37
|
+
htmlUrl: z.string().url(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
const written = z.object({ id: z.string().min(1) }).strict();
|
|
41
|
+
const read = (result) => ({
|
|
42
|
+
methods: ["GET"],
|
|
43
|
+
readOnly: true,
|
|
44
|
+
credential: "repository",
|
|
45
|
+
result,
|
|
46
|
+
});
|
|
47
|
+
const write = (methods, result) => ({
|
|
48
|
+
methods,
|
|
49
|
+
readOnly: false,
|
|
50
|
+
credential: "repository",
|
|
51
|
+
result,
|
|
52
|
+
});
|
|
53
|
+
export const HOST_OPERATIONS = {
|
|
54
|
+
getPullRequest: read(pullRequest),
|
|
55
|
+
listPullRequestFiles: read(z
|
|
56
|
+
.object({
|
|
57
|
+
files: z.array(z
|
|
58
|
+
.object({
|
|
59
|
+
filename: z.string(),
|
|
60
|
+
status: z.string(),
|
|
61
|
+
additions: z.number().int().nonnegative(),
|
|
62
|
+
deletions: z.number().int().nonnegative(),
|
|
63
|
+
previousFilename: z.string().optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict()),
|
|
66
|
+
nextPage: page,
|
|
67
|
+
})
|
|
68
|
+
.strict()),
|
|
69
|
+
getDiff: read(z.object({ diff: z.string() }).strict()),
|
|
70
|
+
findPullRequest: read(z.object({ pullRequest: pullRequest.nullable() }).strict()),
|
|
71
|
+
readFile: read(z.discriminatedUnion("kind", [
|
|
72
|
+
z.object({ kind: z.literal("file"), content: z.string() }).strict(),
|
|
73
|
+
z.object({ kind: z.literal("directory") }).strict(),
|
|
74
|
+
z.object({ kind: z.literal("not_found") }).strict(),
|
|
75
|
+
])),
|
|
76
|
+
listFiles: read(z
|
|
77
|
+
.object({
|
|
78
|
+
entries: z.array(z
|
|
79
|
+
.object({
|
|
80
|
+
path: z.string(),
|
|
81
|
+
type: z.enum(["file", "directory"]),
|
|
82
|
+
size: z.number().int().nonnegative().optional(),
|
|
83
|
+
})
|
|
84
|
+
.strict()),
|
|
85
|
+
nextPage: page,
|
|
86
|
+
})
|
|
87
|
+
.strict()),
|
|
88
|
+
listComments: read(z
|
|
89
|
+
.object({
|
|
90
|
+
comments: z.array(z
|
|
91
|
+
.object({
|
|
92
|
+
id: z.string().min(1),
|
|
93
|
+
author: z.string().nullable(),
|
|
94
|
+
authorId: z.string().nullable(),
|
|
95
|
+
body: z.string(),
|
|
96
|
+
createdAt: z.string(),
|
|
97
|
+
path: z.string().nullable(),
|
|
98
|
+
line: z.number().int().positive().nullable(),
|
|
99
|
+
parentId: z.string().nullable(),
|
|
100
|
+
resolved: z.boolean(),
|
|
101
|
+
})
|
|
102
|
+
.strict()),
|
|
103
|
+
nextPage: page,
|
|
104
|
+
})
|
|
105
|
+
.strict()),
|
|
106
|
+
listCommitStatuses: read(z
|
|
107
|
+
.object({
|
|
108
|
+
statuses: z.array(z
|
|
109
|
+
.object({
|
|
110
|
+
key: z.string(),
|
|
111
|
+
state: z.enum(["pending", "success", "failure", "stopped"]),
|
|
112
|
+
url: z.string().nullable(),
|
|
113
|
+
})
|
|
114
|
+
.strict()),
|
|
115
|
+
nextPage: page,
|
|
116
|
+
})
|
|
117
|
+
.strict()),
|
|
118
|
+
getRepositoryPermission: {
|
|
119
|
+
methods: ["GET"],
|
|
120
|
+
readOnly: true,
|
|
121
|
+
credential: "permission",
|
|
122
|
+
result: z.object({ permission: z.enum(["none", "read", "triage", "write", "maintain", "admin"]) }).strict(),
|
|
123
|
+
},
|
|
124
|
+
openPullRequest: write(["POST"], pullRequest),
|
|
125
|
+
updatePullRequest: write(["PUT"], pullRequest),
|
|
126
|
+
createComment: write(["POST"], written),
|
|
127
|
+
editComment: write(["PUT"], written),
|
|
128
|
+
resolveThread: write(["POST"], z.object({}).strict()),
|
|
129
|
+
submitVerdict: write(["POST"], z.object({}).strict()),
|
|
130
|
+
setCommitStatus: write(["POST"], z.object({}).strict()),
|
|
131
|
+
// PR 5 gives this its HostEvent schema. Nothing calls it before then.
|
|
132
|
+
normalizeEvent: { methods: [], readOnly: false, credential: "none", result: z.unknown() },
|
|
133
|
+
};
|
|
134
|
+
export const REQUIRED_OPERATIONS = [
|
|
135
|
+
"getPullRequest",
|
|
136
|
+
"listPullRequestFiles",
|
|
137
|
+
"getDiff",
|
|
138
|
+
"readFile",
|
|
139
|
+
"listFiles",
|
|
140
|
+
"listComments",
|
|
141
|
+
"createComment",
|
|
142
|
+
"submitVerdict",
|
|
143
|
+
"findPullRequest",
|
|
144
|
+
"openPullRequest",
|
|
145
|
+
"updatePullRequest",
|
|
146
|
+
];
|
|
147
|
+
export function isHostOperation(name) {
|
|
148
|
+
return Object.hasOwn(HOST_OPERATIONS, name);
|
|
149
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { PrismaClient } from "#prisma";
|
|
3
|
+
import type { SecretCipher } from "../secrets/types.js";
|
|
4
|
+
export type HostProviderErrorCode = "host_provider_not_found" | "host_provider_missing_operation" | "host_provider_tool_not_found" | "host_provider_secret_not_found" | "host_provider_secret_not_brokered" | "host_provider_secret_hosts" | "host_provider_secret_placement" | "host_provider_webhook_secret_shared" | "host_provider_webhook_secret_short" | "host_provider_permission_secret_shared" | "host_provider_username_is_secret" | "host_provider_in_use" | "host_provider_operation_not_bound" | "host_provider_tool_deleted";
|
|
5
|
+
export declare class HostProviderError extends Error {
|
|
6
|
+
readonly code: HostProviderErrorCode;
|
|
7
|
+
readonly detail: string;
|
|
8
|
+
constructor(code: HostProviderErrorCode, detail: string);
|
|
9
|
+
}
|
|
10
|
+
export declare const RegisterHostProviderSchema: z.ZodObject<{
|
|
11
|
+
provider: z.ZodString;
|
|
12
|
+
gitHost: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
13
|
+
apiHosts: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
14
|
+
gitUsername: z.ZodString;
|
|
15
|
+
pullRequestUrlTemplate: z.ZodString;
|
|
16
|
+
repositoryApiPath: z.ZodString;
|
|
17
|
+
permissionApiPath: z.ZodOptional<z.ZodString>;
|
|
18
|
+
webhook: z.ZodObject<{
|
|
19
|
+
header: z.ZodString;
|
|
20
|
+
algorithm: z.ZodLiteral<"sha256">;
|
|
21
|
+
deliveryHeader: z.ZodString;
|
|
22
|
+
eventHeader: z.ZodString;
|
|
23
|
+
secret: z.ZodString;
|
|
24
|
+
}, z.core.$strict>;
|
|
25
|
+
credentials: z.ZodArray<z.ZodObject<{
|
|
26
|
+
repositories: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
27
|
+
secret: z.ZodString;
|
|
28
|
+
}, z.core.$strict>>;
|
|
29
|
+
permissionCredential: z.ZodOptional<z.ZodString>;
|
|
30
|
+
trustedAccounts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
tools: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
export type RegisterHostProviderInput = z.infer<typeof RegisterHostProviderSchema>;
|
|
34
|
+
export declare function findCredential<T extends {
|
|
35
|
+
repositoryPattern: string;
|
|
36
|
+
}>(credentials: readonly T[], repositoryPath: string): T | null;
|
|
37
|
+
export interface OperationView {
|
|
38
|
+
operation: string;
|
|
39
|
+
toolName: string | null;
|
|
40
|
+
status: "approved" | "changed" | "tool_deleted";
|
|
41
|
+
approvedAt: Date;
|
|
42
|
+
approvedById: string;
|
|
43
|
+
approvedSha256: string;
|
|
44
|
+
currentSha256: string | null;
|
|
45
|
+
/** The approved snapshot's time limit (null: the default), and the tool's now; a change waits for approval. */
|
|
46
|
+
approvedTimeoutMs: number | null;
|
|
47
|
+
currentTimeoutMs: number | null;
|
|
48
|
+
approvedCode?: string;
|
|
49
|
+
approvedParamsZod?: string;
|
|
50
|
+
currentCode?: string;
|
|
51
|
+
currentParamsZod?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface HostProviderView {
|
|
54
|
+
provider: string;
|
|
55
|
+
contractVersion: number;
|
|
56
|
+
gitHost: string;
|
|
57
|
+
apiHosts: string[];
|
|
58
|
+
gitUsername: string;
|
|
59
|
+
pullRequestUrlTemplate: string;
|
|
60
|
+
repositoryApiPath: string;
|
|
61
|
+
permissionApiPath: string | null;
|
|
62
|
+
mode: "permission" | "allowlist";
|
|
63
|
+
webhook: unknown;
|
|
64
|
+
webhookSecret: string;
|
|
65
|
+
permissionSecret: string | null;
|
|
66
|
+
credentials: {
|
|
67
|
+
repositories: string;
|
|
68
|
+
secret: string;
|
|
69
|
+
kind: string;
|
|
70
|
+
}[];
|
|
71
|
+
trustedAccounts: string[];
|
|
72
|
+
operations: OperationView[];
|
|
73
|
+
}
|
|
74
|
+
export declare function registerHostProvider(db: PrismaClient, cipher: SecretCipher, principalId: string, input: RegisterHostProviderInput): Promise<HostProviderView>;
|
|
75
|
+
export declare function approveHostProviderTools(db: PrismaClient, principalId: string, provider: string, operations: readonly string[]): Promise<HostProviderView>;
|
|
76
|
+
export declare function getHostProviderView(db: PrismaClient, provider: string, options?: {
|
|
77
|
+
includeCode?: boolean;
|
|
78
|
+
}): Promise<HostProviderView | null>;
|
|
79
|
+
export declare function deleteHostProvider(db: PrismaClient, provider: string): Promise<void>;
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registering a tool-backed host provider, approving its tools, and reading it back.
|
|
3
|
+
* Registering copies each tool's code and schema: Wardby only ever runs that copy, so
|
|
4
|
+
* update_tool changes nothing until approved, and a stray edit can't take a provider down.
|
|
5
|
+
* Views carry secret names and metadata, never a value.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { isValidHostProviderName } from "../../coding/code-host.js";
|
|
10
|
+
import { isExactHostname, parseSecretBrokerConfig } from "../../core/secret-broker-config.js";
|
|
11
|
+
import { MIN_WEBHOOK_SECRET_LENGTH } from "../../core/secrets.js";
|
|
12
|
+
import { HOST_TOOL_CONTRACT_VERSION, isHostOperation, REQUIRED_OPERATIONS } from "./operations.js";
|
|
13
|
+
export class HostProviderError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
detail;
|
|
16
|
+
constructor(code, detail) {
|
|
17
|
+
super(`${code}: ${detail}`);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.detail = detail;
|
|
20
|
+
this.name = "HostProviderError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const HTTP_TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/;
|
|
24
|
+
const REPOSITORY_PATTERN = /^[a-z0-9][a-z0-9._-]{0,99}\/(?:\*|[a-z0-9][a-z0-9._-]{0,99})$/;
|
|
25
|
+
const SECRET_NAME = z.string().min(1).max(1024);
|
|
26
|
+
const hostname = z
|
|
27
|
+
.string()
|
|
28
|
+
.transform((h) => h.toLowerCase())
|
|
29
|
+
.refine(isExactHostname, "must be an exact hostname, with no scheme, port, wildcard or IP");
|
|
30
|
+
/** Each of `fields` once, and no other `{...}`. */
|
|
31
|
+
function hasExactlyFields(template, fields) {
|
|
32
|
+
const found = [...template.matchAll(/\{([^{}]*)\}/g)].map((m) => m[1]).sort();
|
|
33
|
+
return JSON.stringify(found) === JSON.stringify([...fields].sort());
|
|
34
|
+
}
|
|
35
|
+
const apiPath = (fields) => z
|
|
36
|
+
.string()
|
|
37
|
+
.max(512)
|
|
38
|
+
.startsWith("/")
|
|
39
|
+
.refine((p) => hasExactlyFields(p, fields), `must contain ${fields.map((f) => `{${f}}`).join(" and ")} once each`)
|
|
40
|
+
// eslint-disable-next-line no-control-regex
|
|
41
|
+
.refine((p) => !/[?#\\\x00-\x20\x7f]|%2f|%5c|\.\./i.test(p), "must be a plain path");
|
|
42
|
+
export const RegisterHostProviderSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
provider: z.string().refine(isValidHostProviderName, "reserved or badly formed provider name"),
|
|
45
|
+
gitHost: hostname,
|
|
46
|
+
apiHosts: z.array(hostname).min(1).max(8),
|
|
47
|
+
// Refused: a name git's password prompt would contain, since scripts/git-askpass.sh answers by matching "Username".
|
|
48
|
+
gitUsername: z
|
|
49
|
+
.string()
|
|
50
|
+
.regex(/^[A-Za-z0-9._-]{1,64}$/)
|
|
51
|
+
.refine((u) => !/username/i.test(u), 'must not contain "username"'),
|
|
52
|
+
pullRequestUrlTemplate: z.string().max(512),
|
|
53
|
+
repositoryApiPath: apiPath(["repository"]),
|
|
54
|
+
permissionApiPath: apiPath(["workspace", "repo"]).optional(),
|
|
55
|
+
webhook: z
|
|
56
|
+
.object({
|
|
57
|
+
header: z.string().regex(HTTP_TOKEN),
|
|
58
|
+
algorithm: z.literal("sha256"),
|
|
59
|
+
deliveryHeader: z.string().regex(HTTP_TOKEN),
|
|
60
|
+
eventHeader: z.string().regex(HTTP_TOKEN),
|
|
61
|
+
secret: SECRET_NAME,
|
|
62
|
+
})
|
|
63
|
+
.strict(),
|
|
64
|
+
credentials: z
|
|
65
|
+
.array(z
|
|
66
|
+
.object({
|
|
67
|
+
repositories: z
|
|
68
|
+
.string()
|
|
69
|
+
.transform((r) => r.toLowerCase())
|
|
70
|
+
.refine((r) => REPOSITORY_PATTERN.test(r), 'must be "workspace/repo" or "workspace/*"'),
|
|
71
|
+
secret: SECRET_NAME,
|
|
72
|
+
})
|
|
73
|
+
.strict())
|
|
74
|
+
.min(1)
|
|
75
|
+
.max(32),
|
|
76
|
+
permissionCredential: SECRET_NAME.optional(),
|
|
77
|
+
trustedAccounts: z
|
|
78
|
+
.array(z.string().regex(/^[A-Za-z0-9:{}_-]{1,128}$/))
|
|
79
|
+
.max(256)
|
|
80
|
+
.optional(),
|
|
81
|
+
tools: z.record(z.string(), z.string().min(1).max(200)),
|
|
82
|
+
})
|
|
83
|
+
.strict()
|
|
84
|
+
.superRefine((input, ctx) => {
|
|
85
|
+
let template = null;
|
|
86
|
+
try {
|
|
87
|
+
template = new URL(input.pullRequestUrlTemplate.replaceAll(/[{}]/g, ""));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// reported below
|
|
91
|
+
}
|
|
92
|
+
if (!template ||
|
|
93
|
+
template.protocol !== "https:" ||
|
|
94
|
+
template.hostname !== input.gitHost ||
|
|
95
|
+
template.username ||
|
|
96
|
+
template.password ||
|
|
97
|
+
template.port ||
|
|
98
|
+
!hasExactlyFields(input.pullRequestUrlTemplate, ["repository", "number"])) {
|
|
99
|
+
ctx.addIssue({
|
|
100
|
+
code: "custom",
|
|
101
|
+
path: ["pullRequestUrlTemplate"],
|
|
102
|
+
message: "must be an https URL on gitHost, with no credentials or port, and with {repository} and {number} once each",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if ((input.permissionCredential === undefined) !== (input.permissionApiPath === undefined)) {
|
|
106
|
+
ctx.addIssue({
|
|
107
|
+
code: "custom",
|
|
108
|
+
path: ["permissionApiPath"],
|
|
109
|
+
message: "permissionCredential and permissionApiPath go together",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const patterns = input.credentials.map((c) => c.repositories);
|
|
113
|
+
if (new Set(patterns).size !== patterns.length) {
|
|
114
|
+
ctx.addIssue({ code: "custom", path: ["credentials"], message: "a repository pattern is listed twice" });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
export function findCredential(credentials, repositoryPath) {
|
|
118
|
+
const parts = repositoryPath.split("/");
|
|
119
|
+
if (parts.length !== 2)
|
|
120
|
+
return null;
|
|
121
|
+
return (credentials.find((c) => c.repositoryPattern === repositoryPath) ??
|
|
122
|
+
credentials.find((c) => c.repositoryPattern === `${parts[0]}/*`) ??
|
|
123
|
+
null);
|
|
124
|
+
}
|
|
125
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
126
|
+
// A tool's timeoutMs is part of what is approved; a snapshot without one hashes as before.
|
|
127
|
+
const snapshotHash = (s) => sha256(`${s.paramsZod}\u0000${s.code}${s.timeoutMs === null ? "" : `\u0000${s.timeoutMs}`}`);
|
|
128
|
+
async function requireSecret(tx, ownerId, name) {
|
|
129
|
+
const secret = await tx.secret.findUnique({ where: { ownerId_name: { ownerId, name } } });
|
|
130
|
+
if (!secret)
|
|
131
|
+
throw new HostProviderError("host_provider_secret_not_found", `secret "${name}" not found`);
|
|
132
|
+
return secret;
|
|
133
|
+
}
|
|
134
|
+
function requireBroker(name, stored, hosts) {
|
|
135
|
+
if (stored === null || stored === undefined) {
|
|
136
|
+
throw new HostProviderError("host_provider_secret_not_brokered", `secret "${name}" must be brokered`);
|
|
137
|
+
}
|
|
138
|
+
const broker = parseSecretBrokerConfig(stored);
|
|
139
|
+
if (broker.placement.kind !== "header" && broker.placement.kind !== "query") {
|
|
140
|
+
throw new HostProviderError("host_provider_secret_placement", `secret "${name}" must be placed in a header or query parameter`);
|
|
141
|
+
}
|
|
142
|
+
const missing = hosts.filter((h) => !broker.hosts.includes(h));
|
|
143
|
+
if (missing.length > 0) {
|
|
144
|
+
throw new HostProviderError("host_provider_secret_hosts", `secret "${name}" must allow ${missing.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
return broker;
|
|
147
|
+
}
|
|
148
|
+
export async function registerHostProvider(db, cipher, principalId, input) {
|
|
149
|
+
const bound = Object.keys(input.tools);
|
|
150
|
+
const unknown = bound.filter((name) => !isHostOperation(name));
|
|
151
|
+
if (unknown.length > 0) {
|
|
152
|
+
throw new HostProviderError("host_provider_operation_not_bound", `unknown operations: ${unknown.join(", ")}`);
|
|
153
|
+
}
|
|
154
|
+
const required = input.permissionCredential
|
|
155
|
+
? [...REQUIRED_OPERATIONS, "getRepositoryPermission"]
|
|
156
|
+
: REQUIRED_OPERATIONS;
|
|
157
|
+
const missing = required.filter((name) => !bound.includes(name));
|
|
158
|
+
if (missing.length > 0) {
|
|
159
|
+
throw new HostProviderError("host_provider_missing_operation", `missing operations: ${missing.join(", ")}`);
|
|
160
|
+
}
|
|
161
|
+
await db.$transaction(async (tx) => {
|
|
162
|
+
const credentials = [];
|
|
163
|
+
for (const credential of input.credentials) {
|
|
164
|
+
const secret = await requireSecret(tx, principalId, credential.secret);
|
|
165
|
+
requireBroker(credential.secret, secret.broker, [...input.apiHosts, input.gitHost]);
|
|
166
|
+
if ((await cipher.decrypt(secret.ciphertext)) === input.gitUsername) {
|
|
167
|
+
throw new HostProviderError("host_provider_username_is_secret", "gitUsername must not be a credential's value");
|
|
168
|
+
}
|
|
169
|
+
credentials.push({ repositoryPattern: credential.repositories, secretId: secret.id });
|
|
170
|
+
}
|
|
171
|
+
let permissionSecretId = null;
|
|
172
|
+
if (input.permissionCredential) {
|
|
173
|
+
const secret = await requireSecret(tx, principalId, input.permissionCredential);
|
|
174
|
+
requireBroker(input.permissionCredential, secret.broker, input.apiHosts);
|
|
175
|
+
permissionSecretId = secret.id;
|
|
176
|
+
if (credentials.some((c) => c.secretId === secret.id) ||
|
|
177
|
+
(await tx.hostProviderCredential.count({
|
|
178
|
+
where: { secretId: secret.id, provider: { not: input.provider } },
|
|
179
|
+
})) > 0) {
|
|
180
|
+
throw new HostProviderError("host_provider_permission_secret_shared", "the permission secret must not also be a credential of any provider, so no other operation can reach it");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if ((await tx.hostProvider.count({
|
|
184
|
+
where: {
|
|
185
|
+
permissionSecretId: {
|
|
186
|
+
in: [...credentials.map((c) => c.secretId), ...(permissionSecretId ? [permissionSecretId] : [])],
|
|
187
|
+
},
|
|
188
|
+
name: { not: input.provider },
|
|
189
|
+
},
|
|
190
|
+
})) > 0) {
|
|
191
|
+
throw new HostProviderError("host_provider_permission_secret_shared", "another provider's permission secret must not be a credential or permission secret here");
|
|
192
|
+
}
|
|
193
|
+
const webhookSecret = await requireSecret(tx, principalId, input.webhook.secret);
|
|
194
|
+
if (credentials.some((c) => c.secretId === webhookSecret.id) ||
|
|
195
|
+
permissionSecretId === webhookSecret.id ||
|
|
196
|
+
(await tx.agentSecret.count({ where: { secretId: webhookSecret.id } })) > 0 ||
|
|
197
|
+
(await tx.hostProviderCredential.count({
|
|
198
|
+
where: { secretId: webhookSecret.id, provider: { not: input.provider } },
|
|
199
|
+
})) > 0 ||
|
|
200
|
+
(await tx.hostProvider.count({
|
|
201
|
+
where: { permissionSecretId: webhookSecret.id, name: { not: input.provider } },
|
|
202
|
+
})) > 0 ||
|
|
203
|
+
(await tx.hostProvider.count({
|
|
204
|
+
where: {
|
|
205
|
+
webhookSecretId: {
|
|
206
|
+
in: [...credentials.map((c) => c.secretId), ...(permissionSecretId ? [permissionSecretId] : [])],
|
|
207
|
+
},
|
|
208
|
+
name: { not: input.provider },
|
|
209
|
+
},
|
|
210
|
+
})) > 0) {
|
|
211
|
+
throw new HostProviderError("host_provider_webhook_secret_shared", "the webhook secret must not be shared with any other provider's credentials or attached to an agent, so no tool can read it");
|
|
212
|
+
}
|
|
213
|
+
if ((await cipher.decrypt(webhookSecret.ciphertext)).length < MIN_WEBHOOK_SECRET_LENGTH) {
|
|
214
|
+
throw new HostProviderError("host_provider_webhook_secret_short", `the webhook secret needs at least ${MIN_WEBHOOK_SECRET_LENGTH} characters`);
|
|
215
|
+
}
|
|
216
|
+
const operations = [];
|
|
217
|
+
for (const [operation, name] of Object.entries(input.tools)) {
|
|
218
|
+
const tool = await tx.tool.findUnique({ where: { ownerId_name: { ownerId: principalId, name } } });
|
|
219
|
+
if (!tool)
|
|
220
|
+
throw new HostProviderError("host_provider_tool_not_found", `tool "${name}" not found`);
|
|
221
|
+
operations.push({
|
|
222
|
+
operation,
|
|
223
|
+
toolId: tool.id,
|
|
224
|
+
code: tool.code,
|
|
225
|
+
paramsZod: tool.paramsZod,
|
|
226
|
+
timeoutMs: tool.timeoutMs,
|
|
227
|
+
approvedById: principalId,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const { secret: _webhookSecretName, ...webhook } = input.webhook;
|
|
231
|
+
const fields = {
|
|
232
|
+
contractVersion: HOST_TOOL_CONTRACT_VERSION,
|
|
233
|
+
gitHost: input.gitHost,
|
|
234
|
+
apiHosts: input.apiHosts,
|
|
235
|
+
gitUsername: input.gitUsername,
|
|
236
|
+
pullRequestUrlTemplate: input.pullRequestUrlTemplate,
|
|
237
|
+
repositoryApiPath: input.repositoryApiPath,
|
|
238
|
+
permissionApiPath: input.permissionApiPath ?? null,
|
|
239
|
+
webhook,
|
|
240
|
+
webhookSecretId: webhookSecret.id,
|
|
241
|
+
permissionSecretId,
|
|
242
|
+
createdById: principalId,
|
|
243
|
+
};
|
|
244
|
+
await tx.hostProvider.upsert({
|
|
245
|
+
where: { name: input.provider },
|
|
246
|
+
create: { name: input.provider, ...fields },
|
|
247
|
+
update: fields,
|
|
248
|
+
});
|
|
249
|
+
await tx.hostProviderOperation.deleteMany({ where: { provider: input.provider } });
|
|
250
|
+
await tx.hostProviderCredential.deleteMany({ where: { provider: input.provider } });
|
|
251
|
+
await tx.hostProviderTrustedAccount.deleteMany({ where: { provider: input.provider } });
|
|
252
|
+
await tx.hostProviderOperation.createMany({ data: operations.map((o) => ({ provider: input.provider, ...o })) });
|
|
253
|
+
await tx.hostProviderCredential.createMany({
|
|
254
|
+
data: credentials.map((c) => ({ provider: input.provider, credentialKind: "static", ...c })),
|
|
255
|
+
});
|
|
256
|
+
await tx.hostProviderTrustedAccount.createMany({
|
|
257
|
+
data: [...new Set(input.trustedAccounts ?? [])].map((accountId) => ({
|
|
258
|
+
provider: input.provider,
|
|
259
|
+
accountId,
|
|
260
|
+
addedById: principalId,
|
|
261
|
+
})),
|
|
262
|
+
});
|
|
263
|
+
}, { isolationLevel: "Serializable" });
|
|
264
|
+
return (await getHostProviderView(db, input.provider));
|
|
265
|
+
}
|
|
266
|
+
export async function approveHostProviderTools(db, principalId, provider, operations) {
|
|
267
|
+
await db.$transaction(async (tx) => {
|
|
268
|
+
if (!(await tx.hostProvider.findUnique({ where: { name: provider } }))) {
|
|
269
|
+
throw new HostProviderError("host_provider_not_found", `provider "${provider}" not found`);
|
|
270
|
+
}
|
|
271
|
+
for (const operation of operations) {
|
|
272
|
+
const row = await tx.hostProviderOperation.findUnique({
|
|
273
|
+
where: { provider_operation: { provider, operation } },
|
|
274
|
+
include: { tool: true },
|
|
275
|
+
});
|
|
276
|
+
if (!row)
|
|
277
|
+
throw new HostProviderError("host_provider_operation_not_bound", `"${operation}" isn't bound`);
|
|
278
|
+
if (!row.tool)
|
|
279
|
+
throw new HostProviderError("host_provider_tool_deleted", `"${operation}"'s tool was deleted`);
|
|
280
|
+
await tx.hostProviderOperation.update({
|
|
281
|
+
where: { provider_operation: { provider, operation } },
|
|
282
|
+
data: {
|
|
283
|
+
code: row.tool.code,
|
|
284
|
+
paramsZod: row.tool.paramsZod,
|
|
285
|
+
timeoutMs: row.tool.timeoutMs,
|
|
286
|
+
approvedById: principalId,
|
|
287
|
+
approvedAt: new Date(),
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}, { isolationLevel: "Serializable" });
|
|
292
|
+
return (await getHostProviderView(db, provider));
|
|
293
|
+
}
|
|
294
|
+
export async function getHostProviderView(db, provider, options = {}) {
|
|
295
|
+
const row = await db.hostProvider.findUnique({
|
|
296
|
+
where: { name: provider },
|
|
297
|
+
include: {
|
|
298
|
+
webhookSecret: { select: { name: true } },
|
|
299
|
+
permissionSecret: { select: { name: true } },
|
|
300
|
+
credentials: { include: { secret: { select: { name: true } } }, orderBy: { repositoryPattern: "asc" } },
|
|
301
|
+
trustedAccounts: { orderBy: { accountId: "asc" } },
|
|
302
|
+
operations: { include: { tool: true }, orderBy: { operation: "asc" } },
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
if (!row)
|
|
306
|
+
return null;
|
|
307
|
+
return {
|
|
308
|
+
provider: row.name,
|
|
309
|
+
contractVersion: row.contractVersion,
|
|
310
|
+
gitHost: row.gitHost,
|
|
311
|
+
apiHosts: row.apiHosts,
|
|
312
|
+
gitUsername: row.gitUsername,
|
|
313
|
+
pullRequestUrlTemplate: row.pullRequestUrlTemplate,
|
|
314
|
+
repositoryApiPath: row.repositoryApiPath,
|
|
315
|
+
permissionApiPath: row.permissionApiPath,
|
|
316
|
+
mode: row.permissionSecretId ? "permission" : "allowlist",
|
|
317
|
+
webhook: row.webhook,
|
|
318
|
+
webhookSecret: row.webhookSecret.name,
|
|
319
|
+
permissionSecret: row.permissionSecret?.name ?? null,
|
|
320
|
+
credentials: row.credentials.map((c) => ({
|
|
321
|
+
repositories: c.repositoryPattern,
|
|
322
|
+
secret: c.secret.name,
|
|
323
|
+
kind: c.credentialKind,
|
|
324
|
+
})),
|
|
325
|
+
trustedAccounts: row.trustedAccounts.map((a) => a.accountId),
|
|
326
|
+
operations: row.operations.map((o) => {
|
|
327
|
+
const approvedSha256 = snapshotHash(o);
|
|
328
|
+
const currentSha256 = o.tool ? snapshotHash(o.tool) : null;
|
|
329
|
+
return {
|
|
330
|
+
operation: o.operation,
|
|
331
|
+
toolName: o.tool?.name ?? null,
|
|
332
|
+
status: !o.tool ? "tool_deleted" : currentSha256 === approvedSha256 ? "approved" : "changed",
|
|
333
|
+
approvedAt: o.approvedAt,
|
|
334
|
+
approvedById: o.approvedById,
|
|
335
|
+
approvedSha256,
|
|
336
|
+
currentSha256,
|
|
337
|
+
approvedTimeoutMs: o.timeoutMs,
|
|
338
|
+
currentTimeoutMs: o.tool?.timeoutMs ?? null,
|
|
339
|
+
...(options.includeCode
|
|
340
|
+
? {
|
|
341
|
+
approvedCode: o.code,
|
|
342
|
+
approvedParamsZod: o.paramsZod,
|
|
343
|
+
...(o.tool && currentSha256 !== approvedSha256
|
|
344
|
+
? { currentCode: o.tool.code, currentParamsZod: o.tool.paramsZod }
|
|
345
|
+
: {}),
|
|
346
|
+
}
|
|
347
|
+
: {}),
|
|
348
|
+
};
|
|
349
|
+
}),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
export async function deleteHostProvider(db, provider) {
|
|
353
|
+
await db.$transaction(async (tx) => {
|
|
354
|
+
if (!(await tx.hostProvider.findUnique({ where: { name: provider } }))) {
|
|
355
|
+
throw new HostProviderError("host_provider_not_found", `provider "${provider}" not found`);
|
|
356
|
+
}
|
|
357
|
+
const links = await tx.agentRepository.count({ where: { provider } });
|
|
358
|
+
if (links > 0)
|
|
359
|
+
throw new HostProviderError("host_provider_in_use", `${links} agent link(s) use this provider`);
|
|
360
|
+
await tx.hostProvider.delete({ where: { name: provider } });
|
|
361
|
+
}, { isolationLevel: "Serializable" });
|
|
362
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where one bound tool's request may go: the provider's API hosts, inside the path of
|
|
3
|
+
* the repository this call is for, using the operation's own methods. Wardby picks
|
|
4
|
+
* the repository, so a tool can't point a token at another one. Pure.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MAX_HOST_TOOL_FETCHES = 3;
|
|
7
|
+
export interface HostToolRequestScope {
|
|
8
|
+
apiHosts: readonly string[];
|
|
9
|
+
pathPrefix: string;
|
|
10
|
+
methods: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export declare class HostToolRequestDenied extends Error {
|
|
13
|
+
constructor();
|
|
14
|
+
}
|
|
15
|
+
export declare function assertHostToolRequest(url: string, method: string, scope: HostToolRequestScope): void;
|
|
16
|
+
/** Request headers a bound tool may set: no override, routing or credential headers. */
|
|
17
|
+
export declare function assertHostToolHeaders(headers: unknown): void;
|