@wardby/cli 0.7.0 → 0.9.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 +11 -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 +64 -0
- package/dist/claude-plugin/command.js +379 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +2 -1
- package/dist/cli.js +71 -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 +3 -9
- 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 +39 -11
- package/dist/core/engine-native.d.ts +2 -0
- package/dist/core/engine-native.js +13 -0
- package/dist/core/error-code.js +2 -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/listen-address.d.ts +10 -0
- package/dist/core/listen-address.js +14 -0
- 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/power-idle.d.ts +8 -2
- package/dist/core/power-idle.js +17 -3
- package/dist/core/power.d.ts +5 -0
- package/dist/core/power.js +5 -0
- package/dist/core/related-pull-requests.js +26 -10
- package/dist/core/retention-days.d.ts +5 -0
- package/dist/core/retention-days.js +14 -0
- package/dist/core/review-fix.js +2 -2
- package/dist/core/review-host-tools.js +3 -1
- package/dist/core/run-steps/config.d.ts +25 -0
- package/dist/core/run-steps/config.js +40 -0
- package/dist/core/run-steps/control-chars.d.ts +16 -0
- package/dist/core/run-steps/control-chars.js +26 -0
- package/dist/core/run-steps/detail.d.ts +32 -0
- package/dist/core/run-steps/detail.js +240 -0
- package/dist/core/run-steps/extract.d.ts +52 -0
- package/dist/core/run-steps/extract.js +467 -0
- package/dist/core/run-steps/ingress.d.ts +51 -0
- package/dist/core/run-steps/ingress.js +160 -0
- package/dist/core/run-steps/last-step.d.ts +15 -0
- package/dist/core/run-steps/last-step.js +35 -0
- package/dist/core/run-steps/native.d.ts +48 -0
- package/dist/core/run-steps/native.js +164 -0
- package/dist/core/run-steps/seen.d.ts +16 -0
- package/dist/core/run-steps/seen.js +35 -0
- package/dist/core/run-steps/summarize.d.ts +57 -0
- package/dist/core/run-steps/summarize.js +304 -0
- package/dist/core/run-steps/sweep.d.ts +21 -0
- package/dist/core/run-steps/sweep.js +41 -0
- package/dist/core/run-steps/types.d.ts +89 -0
- package/dist/core/run-steps/types.js +39 -0
- package/dist/core/run-steps/wire.d.ts +35 -0
- package/dist/core/run-steps/wire.js +47 -0
- package/dist/core/run-steps/writer.d.ts +49 -0
- package/dist/core/run-steps/writer.js +279 -0
- package/dist/core/runner.d.ts +70 -16
- package/dist/core/runner.js +296 -74
- 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 +39 -0
- package/dist/generated/prisma/client.d.ts +39 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +80 -50
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +5 -0
- package/dist/generated/prisma/internal/class.d.ts +77 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +632 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +94 -3
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +98 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +94 -3
- package/dist/generated/prisma/models/Agent.d.ts +372 -1
- 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 +159 -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/Run.d.ts +503 -1
- package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
- package/dist/generated/prisma/models/RunStep.js +1 -0
- 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 +7 -0
- package/dist/help/search.js +12 -3
- package/dist/help-index.json +812 -52
- 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/access.d.ts +1 -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 +6 -0
- package/dist/mcp/index.js +21 -0
- package/dist/mcp/tools/agents.js +135 -20
- 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/power.js +2 -2
- package/dist/mcp/tools/repositories.js +50 -10
- package/dist/mcp/tools/run-access.d.ts +7 -0
- package/dist/mcp/tools/run-access.js +14 -0
- package/dist/mcp/tools/run-steps.d.ts +16 -0
- package/dist/mcp/tools/run-steps.js +85 -0
- package/dist/mcp/tools/runs.js +186 -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 +18 -1
- package/dist/native-worker/gateway-server.js +38 -9
- package/dist/native-worker/gateway.d.ts +5 -0
- package/dist/native-worker/gateway.js +13 -1
- package/dist/native-worker/http-server.d.ts +6 -4
- package/dist/native-worker/http-server.js +53 -8
- 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 +16 -0
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +1 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +36 -2
- package/dist/providers/coding-proxy/proxy.d.ts +33 -1
- package/dist/providers/coding-proxy/proxy.js +266 -75
- package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
- package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
- package/dist/providers/coding-proxy/runtime.d.ts +13 -0
- package/dist/providers/coding-proxy/runtime.js +87 -4
- 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 +18 -0
- package/dist/providers/engine/types.d.ts +26 -0
- package/dist/providers/executor/composition.js +5 -0
- package/dist/providers/executor/container.d.ts +47 -1
- package/dist/providers/executor/container.js +84 -8
- 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 +13 -0
- package/dist/providers/jobs/kubernetes.js +28 -5
- 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/serve.js +26 -18
- package/dist/viewer/api-schema.d.ts +66 -0
- package/dist/viewer/api-schema.js +31 -0
- package/dist/viewer/graph.js +2 -1
- package/dist/viewer/openapi.js +2 -1
- package/dist/viewer/run-detail.d.ts +2 -0
- package/dist/viewer/run-detail.js +26 -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 +208 -0
- package/docs/code-review-agents.md +6 -0
- package/docs/coding-agent-setup.md +94 -1
- package/docs/coding-worker-byo-images.md +4 -2
- package/docs/coding-worker-isolation.md +94 -7
- package/docs/debugging-agent-runs.md +354 -0
- package/docs/getting-started-gke.md +33 -11
- 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 +263 -0
- package/docs/native-sandbox.md +31 -9
- package/docs/security-deployment.md +161 -31
- package/docs/tool-secrets.md +3 -0
- package/docs/viewer-api.md +27 -12
- package/docs/viewer-api.openapi.yml +78 -2
- package/help/admin-viewer.md +5 -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 +63 -0
- package/help/deploy-gke.md +5 -0
- package/help/errors/coding-mcp-gateway-unavailable.md +46 -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/low-power-mode.md +19 -8
- package/help/mcp-servers.md +73 -0
- package/help/mcp.md +9 -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 +7 -0
- package/help/run-steps.md +140 -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/migrations/20261110000000_run_steps/migration.sql +26 -0
- package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
- package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
- package/prisma/schema.prisma +211 -14
- package/scripts/git-askpass.sh +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { codeProviderOf, normalizeHostedRepository } from "../../coding/code-host.js";
|
|
2
|
+
import { loadTestSimulators, simulatorHostFetch } from "../../config/test-simulators.js";
|
|
3
|
+
import { parseSecretBrokerConfig } from "../../core/secret-broker-config.js";
|
|
4
|
+
import { safeFetch } from "../../sandbox/safe-fetch.js";
|
|
5
|
+
import { invokeHostTool } from "./invoke.js";
|
|
6
|
+
import { HOST_OPERATIONS, isHostOperation } from "./operations.js";
|
|
7
|
+
import { findCredential } from "./registry.js";
|
|
8
|
+
import { effectiveToolTimeoutMs } from "../../sandbox/tool-timeout.js";
|
|
9
|
+
import { loadToolMaxTimeoutMs } from "../../config/tool-timeout.js";
|
|
10
|
+
/**
|
|
11
|
+
* The fetch for bound tools: the real one, or under WARDBY_TEST_SIMULATORS the simulator's. Under
|
|
12
|
+
* the switch only `sim-` credentials are sent, as the switch refuses every other real credential.
|
|
13
|
+
*/
|
|
14
|
+
export function hostToolFetch(env = process.env) {
|
|
15
|
+
const simulators = loadTestSimulators(env);
|
|
16
|
+
return simulators ? { fetchImpl: simulatorHostFetch(simulators, safeFetch), simulated: true } : { simulated: false };
|
|
17
|
+
}
|
|
18
|
+
export async function loadProviderForCalls(db, name) {
|
|
19
|
+
const row = await db.hostProvider.findUnique({
|
|
20
|
+
where: { name },
|
|
21
|
+
include: { permissionSecret: true, credentials: { include: { secret: true } }, operations: true },
|
|
22
|
+
});
|
|
23
|
+
if (!row)
|
|
24
|
+
return null;
|
|
25
|
+
return {
|
|
26
|
+
name: row.name,
|
|
27
|
+
apiHosts: row.apiHosts,
|
|
28
|
+
repositoryApiPath: row.repositoryApiPath,
|
|
29
|
+
permissionApiPath: row.permissionApiPath,
|
|
30
|
+
permissionSecret: row.permissionSecret,
|
|
31
|
+
credentials: row.credentials,
|
|
32
|
+
operations: row.operations,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// No URL-encoding: callHostOperation only takes a repository in its normalized form, which admits only [A-Za-z0-9._-], and the request guard compares a raw prefix.
|
|
36
|
+
function encodePath(template, fields) {
|
|
37
|
+
return template.replace(/\{([a-z]+)\}/g, (_m, field) => fields[field]);
|
|
38
|
+
}
|
|
39
|
+
/** The repository is compared in its normalized form, so callers must pass one. */
|
|
40
|
+
export async function callHostOperation(provider, cipher, repository, operation, fields, options = {}) {
|
|
41
|
+
let ours = false;
|
|
42
|
+
try {
|
|
43
|
+
ours = normalizeHostedRepository(repository) === repository && codeProviderOf(repository) === provider.name;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Not a repository we can name: denied below.
|
|
47
|
+
}
|
|
48
|
+
if (!ours)
|
|
49
|
+
return { ok: false, code: "host_tool_request_denied" };
|
|
50
|
+
const snapshot = provider.operations.find((o) => o.operation === operation);
|
|
51
|
+
if (!snapshot)
|
|
52
|
+
return { ok: false, code: "host_tool_failed" };
|
|
53
|
+
const path = repository.slice(`${provider.name}:`.length);
|
|
54
|
+
const [workspace, repo] = path.split("/");
|
|
55
|
+
const permission = HOST_OPERATIONS[operation].credential === "permission";
|
|
56
|
+
const stored = permission ? provider.permissionSecret : (findCredential(provider.credentials, path)?.secret ?? null);
|
|
57
|
+
const template = permission ? provider.permissionApiPath : provider.repositoryApiPath;
|
|
58
|
+
if (!stored || !template)
|
|
59
|
+
return { ok: false, code: "host_tool_no_credential" };
|
|
60
|
+
const credential = async () => {
|
|
61
|
+
const value = await cipher.decrypt(stored.ciphertext);
|
|
62
|
+
if (options.simulated && !value.startsWith("sim-"))
|
|
63
|
+
return undefined;
|
|
64
|
+
return { value, broker: parseSecretBrokerConfig(stored.broker) };
|
|
65
|
+
};
|
|
66
|
+
return invokeHostTool({
|
|
67
|
+
provider: provider.name,
|
|
68
|
+
operation,
|
|
69
|
+
snapshot,
|
|
70
|
+
params: { ...fields, workspace, repo, repository: path },
|
|
71
|
+
apiHosts: provider.apiHosts,
|
|
72
|
+
pathPrefix: encodePath(template, { repository: path, workspace, repo }),
|
|
73
|
+
credential,
|
|
74
|
+
fetchImpl: options.fetchImpl,
|
|
75
|
+
timeoutMs: effectiveToolTimeoutMs(snapshot.timeoutMs, loadToolMaxTimeoutMs()),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function selfTestHostProvider(provider, cipher, input, options = {}) {
|
|
79
|
+
const repository = normalizeHostedRepository(input.repository);
|
|
80
|
+
if (codeProviderOf(repository) !== provider.name)
|
|
81
|
+
throw new Error("host_provider_repository_mismatch");
|
|
82
|
+
const results = [];
|
|
83
|
+
const skip = (operation, code) => results.push({ operation, outcome: "skipped", code });
|
|
84
|
+
const check = async (operation, fields) => {
|
|
85
|
+
const result = await callHostOperation(provider, cipher, repository, operation, fields, options);
|
|
86
|
+
results.push(result.ok ? { operation, outcome: "pass" } : { operation, outcome: "fail", code: result.code });
|
|
87
|
+
return result;
|
|
88
|
+
};
|
|
89
|
+
const bound = (name) => provider.operations.some((o) => o.operation === name);
|
|
90
|
+
// The pull request comes first: its head is the ref for the checks that need one.
|
|
91
|
+
let ref = input.ref;
|
|
92
|
+
const number = input.pullRequest;
|
|
93
|
+
if (!bound("getPullRequest"))
|
|
94
|
+
skip("getPullRequest", "not_bound");
|
|
95
|
+
else if (number === undefined)
|
|
96
|
+
skip("getPullRequest", "needs_pull_request");
|
|
97
|
+
else {
|
|
98
|
+
const pr = await check("getPullRequest", { number });
|
|
99
|
+
if (pr.ok)
|
|
100
|
+
ref ??= pr.value.headSha;
|
|
101
|
+
}
|
|
102
|
+
for (const operation of ["listPullRequestFiles", "getDiff", "listComments"]) {
|
|
103
|
+
if (!bound(operation))
|
|
104
|
+
skip(operation, "not_bound");
|
|
105
|
+
else if (number === undefined)
|
|
106
|
+
skip(operation, "needs_pull_request");
|
|
107
|
+
else
|
|
108
|
+
await check(operation, { number });
|
|
109
|
+
}
|
|
110
|
+
if (!bound("findPullRequest"))
|
|
111
|
+
skip("findPullRequest", "not_bound");
|
|
112
|
+
else if (input.headRef === undefined)
|
|
113
|
+
skip("findPullRequest", "needs_head_ref");
|
|
114
|
+
else
|
|
115
|
+
await check("findPullRequest", { headRef: input.headRef });
|
|
116
|
+
for (const [operation, fields] of [
|
|
117
|
+
["readFile", { path: "README.md" }],
|
|
118
|
+
["listFiles", { path: "" }],
|
|
119
|
+
["listCommitStatuses", {}],
|
|
120
|
+
]) {
|
|
121
|
+
if (!bound(operation))
|
|
122
|
+
skip(operation, "not_bound");
|
|
123
|
+
else if (ref === undefined)
|
|
124
|
+
skip(operation, "needs_ref");
|
|
125
|
+
else
|
|
126
|
+
await check(operation, { ref, ...fields });
|
|
127
|
+
}
|
|
128
|
+
const mode = provider.permissionSecret ? "permission" : "allowlist";
|
|
129
|
+
if (mode === "allowlist")
|
|
130
|
+
skip("getRepositoryPermission", "allowlist_mode");
|
|
131
|
+
else if (!bound("getRepositoryPermission"))
|
|
132
|
+
skip("getRepositoryPermission", "not_bound");
|
|
133
|
+
else if (input.accountId === undefined)
|
|
134
|
+
skip("getRepositoryPermission", "needs_account");
|
|
135
|
+
else
|
|
136
|
+
await check("getRepositoryPermission", { accountId: input.accountId });
|
|
137
|
+
for (const { operation } of provider.operations) {
|
|
138
|
+
if (isHostOperation(operation) && !HOST_OPERATIONS[operation].readOnly)
|
|
139
|
+
skip(operation, "not_read_only");
|
|
140
|
+
}
|
|
141
|
+
return { provider: provider.name, repository, mode, results };
|
|
142
|
+
}
|
|
@@ -214,6 +214,6 @@ export declare function assertStorageVolumeInspection(volume: DockerVolumeInspec
|
|
|
214
214
|
export declare function assertKeeperContainerInspection(container: DockerContainerInspection, spec: JobSpec): void;
|
|
215
215
|
export declare function assertNetworkKeeperContainerInspection(container: DockerContainerInspection, spec: JobSpec): void;
|
|
216
216
|
export declare function assertProxyContainerInspection(container: DockerContainerInspection, runId: string): void;
|
|
217
|
-
export declare function assertWorkerContainerInspection(container: DockerContainerInspection, spec: JobSpec, expectedCapability: string, networkKeeperId?: string): void;
|
|
218
|
-
export declare function assertClaudeAgentContainerInspection(container: DockerContainerInspection, spec: JobSpec, expectedCapability: string): void;
|
|
217
|
+
export declare function assertWorkerContainerInspection(container: DockerContainerInspection, spec: JobSpec, expectedCapability: string, networkKeeperId?: string, expectedMcpToken?: string): void;
|
|
218
|
+
export declare function assertClaudeAgentContainerInspection(container: DockerContainerInspection, spec: JobSpec, expectedCapability: string, expectedMcpToken?: string): void;
|
|
219
219
|
export declare function assertClaudeToolRunnerContainerInspection(container: DockerContainerInspection, spec: JobSpec, expectedSetup: string, networkKeeperId?: string): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { MCP_TOKEN_ENV } from "../../coding/mcp-token.js";
|
|
2
3
|
export const CODING_WORKER_UID = 10001;
|
|
3
4
|
export const CODING_WORKER_GID = 10001;
|
|
4
5
|
// The keeper needs headroom for runc's short-lived exec process plus Node's threads.
|
|
@@ -101,6 +102,8 @@ function validateSpec(spec) {
|
|
|
101
102
|
if (spec.sessionResume !== undefined && (spec.sessionResume !== true || spec.provider !== "claude-code")) {
|
|
102
103
|
throw isolationError();
|
|
103
104
|
}
|
|
105
|
+
if (spec.mcpTools !== undefined && spec.mcpTools !== true)
|
|
106
|
+
throw isolationError();
|
|
104
107
|
if (spec.services !== undefined) {
|
|
105
108
|
// docker-services.ts re-validates every entry against the catalog schema. Both providers' shells
|
|
106
109
|
// share the network keeper's namespace: Codex's worker, and Claude Code's tool runner.
|
|
@@ -306,6 +309,27 @@ function workerNetworkMode(spec) {
|
|
|
306
309
|
const names = isolationNames(spec.runId);
|
|
307
310
|
return spec.services === undefined ? names.network : `container:${names.networkKeeperContainer}`;
|
|
308
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* A run with MCP tools only: the agent's MCP token, by name. Like the capability, its value comes
|
|
314
|
+
* from the docker process's own environment (DockerCommandOptions.env), never the command line.
|
|
315
|
+
*/
|
|
316
|
+
function mcpTokenEnvArgs(spec) {
|
|
317
|
+
return spec.mcpTools ? ["--env", MCP_TOKEN_ENV] : [];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The agent container's exact WARDBY_ environment: the proxy URL, the capability, and for a run with
|
|
321
|
+
* MCP tools its MCP token. Anything missing, changed or extra fails the attestation.
|
|
322
|
+
*/
|
|
323
|
+
function agentEnvironmentMatches(environment, spec, expectedCapability, expectedMcpToken) {
|
|
324
|
+
if (spec.mcpTools && expectedMcpToken === undefined)
|
|
325
|
+
return false;
|
|
326
|
+
const expected = [
|
|
327
|
+
`WARDBY_PROXY_URL=http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}`,
|
|
328
|
+
`WARDBY_RUN_CAPABILITY=${expectedCapability}`,
|
|
329
|
+
...(spec.mcpTools ? [`${MCP_TOKEN_ENV}=${expectedMcpToken}`] : []),
|
|
330
|
+
];
|
|
331
|
+
return environment.length === expected.length && expected.every((value) => environment.includes(value));
|
|
332
|
+
}
|
|
309
333
|
export function buildWorkerCreateArgs(spec, proxyPort = CODING_PROXY_PORT) {
|
|
310
334
|
validateSpec(spec);
|
|
311
335
|
assertIntegerRange(proxyPort, 1, 65_535);
|
|
@@ -360,6 +384,7 @@ export function buildWorkerCreateArgs(spec, proxyPort = CODING_PROXY_PORT) {
|
|
|
360
384
|
`WARDBY_PROXY_URL=http://${CODING_PROXY_ALIAS}:${proxyPort}`,
|
|
361
385
|
"--env",
|
|
362
386
|
"WARDBY_RUN_CAPABILITY",
|
|
387
|
+
...mcpTokenEnvArgs(spec),
|
|
363
388
|
"--restart",
|
|
364
389
|
"no",
|
|
365
390
|
"--stop-signal",
|
|
@@ -459,6 +484,7 @@ export function buildClaudeAgentCreateArgs(spec, proxyPort = CODING_PROXY_PORT)
|
|
|
459
484
|
`WARDBY_PROXY_URL=http://${CODING_PROXY_ALIAS}:${proxyPort}`,
|
|
460
485
|
"--env",
|
|
461
486
|
"WARDBY_RUN_CAPABILITY",
|
|
487
|
+
...mcpTokenEnvArgs(spec),
|
|
462
488
|
"--restart",
|
|
463
489
|
"no",
|
|
464
490
|
"--stop-signal",
|
|
@@ -780,7 +806,7 @@ function assertExactWorkerMounts(container, names) {
|
|
|
780
806
|
}
|
|
781
807
|
}
|
|
782
808
|
}
|
|
783
|
-
export function assertWorkerContainerInspection(container, spec, expectedCapability, networkKeeperId) {
|
|
809
|
+
export function assertWorkerContainerInspection(container, spec, expectedCapability, networkKeeperId, expectedMcpToken) {
|
|
784
810
|
validateSpec(spec);
|
|
785
811
|
const names = isolationNames(spec.runId);
|
|
786
812
|
const host = container.HostConfig;
|
|
@@ -822,9 +848,7 @@ export function assertWorkerContainerInspection(container, spec, expectedCapabil
|
|
|
822
848
|
Object.keys(container.NetworkSettings?.Ports ?? {}).length !== 0) {
|
|
823
849
|
throw isolationError();
|
|
824
850
|
}
|
|
825
|
-
if (wardbyEnvironment
|
|
826
|
-
!wardbyEnvironment.includes(`WARDBY_PROXY_URL=http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}`) ||
|
|
827
|
-
!wardbyEnvironment.includes(`WARDBY_RUN_CAPABILITY=${expectedCapability}`)) {
|
|
851
|
+
if (!agentEnvironmentMatches(wardbyEnvironment, spec, expectedCapability, expectedMcpToken)) {
|
|
828
852
|
throw isolationError();
|
|
829
853
|
}
|
|
830
854
|
const tmpfs = host.Tmpfs ?? {};
|
|
@@ -893,16 +917,14 @@ function assertClaudeContainerBaseline(container, image, spec, limits) {
|
|
|
893
917
|
!host.Tmpfs?.["/home/wardby"]?.includes("noexec"))
|
|
894
918
|
throw isolationError();
|
|
895
919
|
}
|
|
896
|
-
export function assertClaudeAgentContainerInspection(container, spec, expectedCapability) {
|
|
920
|
+
export function assertClaudeAgentContainerInspection(container, spec, expectedCapability, expectedMcpToken) {
|
|
897
921
|
validateSpec(spec);
|
|
898
922
|
if (spec.provider !== "claude-code")
|
|
899
923
|
throw isolationError();
|
|
900
924
|
const names = isolationNames(spec.runId);
|
|
901
925
|
assertClaudeContainerBaseline(container, spec.image, spec, claudeAgentLimits(spec));
|
|
902
926
|
const environment = (container.Config?.Env ?? []).filter((value) => value.startsWith("WARDBY_"));
|
|
903
|
-
if (environment
|
|
904
|
-
!environment.includes(`WARDBY_PROXY_URL=http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}`) ||
|
|
905
|
-
!environment.includes(`WARDBY_RUN_CAPABILITY=${expectedCapability}`))
|
|
927
|
+
if (!agentEnvironmentMatches(environment, spec, expectedCapability, expectedMcpToken))
|
|
906
928
|
throw isolationError();
|
|
907
929
|
assertExactMountSet(container, names, [
|
|
908
930
|
{ path: WORKER_PATHS.input, writable: false, subpath: "input" },
|
|
@@ -88,6 +88,12 @@ export interface DockerJobLauncherOptions {
|
|
|
88
88
|
workspaceRoot: string;
|
|
89
89
|
proxyContainer: string;
|
|
90
90
|
resolveCapability: (runId: string) => Promise<string>;
|
|
91
|
+
/**
|
|
92
|
+
* The run's MCP token (the capability vault), asked for only when the spec has mcpTools; the agent
|
|
93
|
+
* container gets it as WARDBY_MCP_TOKEN, through the docker process's environment like the
|
|
94
|
+
* capability. Absent = a run with MCP tools can't be launched.
|
|
95
|
+
*/
|
|
96
|
+
resolveMcpToken?: (runId: string) => Promise<string | undefined>;
|
|
91
97
|
/** Consult the durable application state before a startup sweep deletes resources. */
|
|
92
98
|
isRunActive: (runId: string, handle: JobHandle) => Promise<boolean>;
|
|
93
99
|
docker?: DockerCommandRunner;
|
|
@@ -131,6 +137,8 @@ export declare class DockerJobLauncher implements WorkspaceJobLauncher {
|
|
|
131
137
|
remove(handle: JobHandle): Promise<void>;
|
|
132
138
|
private initialize;
|
|
133
139
|
private launchLocked;
|
|
140
|
+
/** The run's MCP token for a spec with mcpTools (refused unless well formed); undefined otherwise, unasked. */
|
|
141
|
+
private mcpTokenFor;
|
|
134
142
|
private provision;
|
|
135
143
|
/**
|
|
136
144
|
* A run with services (docs/coding-services.md). The network keeper joins the run's internal
|
|
@@ -12,6 +12,7 @@ import { assertDockerHostSupportsIsolation, assertClaudeAgentContainerInspection
|
|
|
12
12
|
import { serviceUnreadyError } from "../../coding/services/wording.js";
|
|
13
13
|
import { assertServiceContainerInspection, buildDockerServicePlan, dockerServiceContainerNames, } from "./docker-services.js";
|
|
14
14
|
import { claudeToolSetup } from "./claude-tool-setup.js";
|
|
15
|
+
import { isMcpToken, MCP_TOKEN_ENV } from "../../coding/mcp-token.js";
|
|
15
16
|
import { reportServiceState, } from "./service-state.js";
|
|
16
17
|
import { replaceDirectoryFromStaging } from "./workspace-swap.js";
|
|
17
18
|
const BACKEND = "docker";
|
|
@@ -95,7 +96,7 @@ export class NodeDockerCommandRunner {
|
|
|
95
96
|
if (args.some((argument) => argument.includes("\0")))
|
|
96
97
|
throw new Error("docker_argument_invalid");
|
|
97
98
|
const extraEnv = options.env ?? {};
|
|
98
|
-
if (Object.keys(extraEnv).some((key) => key !== "WARDBY_RUN_CAPABILITY" && key !== CLAUDE_TOOL_SETUP_ENV))
|
|
99
|
+
if (Object.keys(extraEnv).some((key) => key !== "WARDBY_RUN_CAPABILITY" && key !== MCP_TOKEN_ENV && key !== CLAUDE_TOOL_SETUP_ENV))
|
|
99
100
|
throw new Error("docker_environment_invalid");
|
|
100
101
|
const env = {
|
|
101
102
|
PATH: this.path,
|
|
@@ -447,6 +448,10 @@ function serviceUnready(name, cause) {
|
|
|
447
448
|
function capabilityFromInspection(container) {
|
|
448
449
|
return container.Config?.Env?.find((value) => value.startsWith("WARDBY_RUN_CAPABILITY="))?.slice("WARDBY_RUN_CAPABILITY=".length);
|
|
449
450
|
}
|
|
451
|
+
function mcpTokenFromInspection(container) {
|
|
452
|
+
const prefix = `${MCP_TOKEN_ENV}=`;
|
|
453
|
+
return container.Config?.Env?.find((value) => value.startsWith(prefix))?.slice(prefix.length);
|
|
454
|
+
}
|
|
450
455
|
function dockerState(inspection) {
|
|
451
456
|
const state = inspection.State;
|
|
452
457
|
if (!state)
|
|
@@ -655,14 +660,19 @@ export class DockerJobLauncher {
|
|
|
655
660
|
const capability = ensureCapability(await this.options.resolveCapability(spec.runId));
|
|
656
661
|
if (hash(capability) !== existing.capabilityHash)
|
|
657
662
|
throw new Error("docker_capability_changed");
|
|
663
|
+
const mcpToken = await this.mcpTokenFor(spec);
|
|
664
|
+
if ((mcpToken === undefined ? undefined : hash(mcpToken)) !== existing.mcpTokenHash) {
|
|
665
|
+
throw new Error("docker_mcp_token_changed");
|
|
666
|
+
}
|
|
658
667
|
if (existing.phase === "active") {
|
|
659
668
|
await this.refresh(existing);
|
|
660
669
|
return clone(existing.handle);
|
|
661
670
|
}
|
|
662
|
-
return this.provision(existing, plan, services, capability);
|
|
671
|
+
return this.provision(existing, plan, services, capability, mcpToken);
|
|
663
672
|
}
|
|
664
673
|
await this.validateInputArtifact(spec.inputArtifact);
|
|
665
674
|
const capability = ensureCapability(await this.options.resolveCapability(spec.runId));
|
|
675
|
+
const mcpToken = await this.mcpTokenFor(spec);
|
|
666
676
|
const record = {
|
|
667
677
|
schemaVersion: STATE_SCHEMA_VERSION,
|
|
668
678
|
runId: spec.runId,
|
|
@@ -673,12 +683,13 @@ export class DockerJobLauncher {
|
|
|
673
683
|
createdAt: this.now(),
|
|
674
684
|
deadlineAt: this.now() + plan.deadlineMs,
|
|
675
685
|
capabilityHash: hash(capability),
|
|
686
|
+
...(mcpToken !== undefined ? { mcpTokenHash: hash(mcpToken) } : {}),
|
|
676
687
|
phase: "provisioning",
|
|
677
688
|
};
|
|
678
689
|
// This durable record is the idempotency fence before any Docker side effect.
|
|
679
690
|
await this.writeRecord(record);
|
|
680
691
|
try {
|
|
681
|
-
return await this.provision(record, plan, services, capability);
|
|
692
|
+
return await this.provision(record, plan, services, capability, mcpToken);
|
|
682
693
|
}
|
|
683
694
|
catch (error) {
|
|
684
695
|
await this.options
|
|
@@ -691,7 +702,16 @@ export class DockerJobLauncher {
|
|
|
691
702
|
throw error;
|
|
692
703
|
}
|
|
693
704
|
}
|
|
694
|
-
|
|
705
|
+
/** The run's MCP token for a spec with mcpTools (refused unless well formed); undefined otherwise, unasked. */
|
|
706
|
+
async mcpTokenFor(spec) {
|
|
707
|
+
if (!spec.mcpTools)
|
|
708
|
+
return undefined;
|
|
709
|
+
const token = await this.options.resolveMcpToken?.(spec.runId);
|
|
710
|
+
if (!isMcpToken(token))
|
|
711
|
+
throw new Error("docker_mcp_token_invalid");
|
|
712
|
+
return token;
|
|
713
|
+
}
|
|
714
|
+
async provision(record, plan, services, capability, mcpToken) {
|
|
695
715
|
await this.assertHost();
|
|
696
716
|
const labels = resourceLabels(record);
|
|
697
717
|
await this.createAndAssert(appendLabels(plan.networkCreateArgs, labels), ["network", "inspect", plan.names.network], (value) => {
|
|
@@ -740,14 +760,14 @@ export class DockerJobLauncher {
|
|
|
740
760
|
await this.createAndAssert(workerArgs, ["container", "inspect", plan.names.workerContainer], (value) => {
|
|
741
761
|
const inspected = value;
|
|
742
762
|
if (record.spec.provider === "claude-code") {
|
|
743
|
-
assertClaudeAgentContainerInspection(inspected, record.spec, capability);
|
|
763
|
+
assertClaudeAgentContainerInspection(inspected, record.spec, capability, mcpToken);
|
|
744
764
|
}
|
|
745
765
|
else {
|
|
746
|
-
assertWorkerContainerInspection(inspected, record.spec, capability, networkKeeperId);
|
|
766
|
+
assertWorkerContainerInspection(inspected, record.spec, capability, networkKeeperId, mcpToken);
|
|
747
767
|
}
|
|
748
768
|
if (!labelsMatch(inspected.Config?.Labels, labels))
|
|
749
769
|
throw new Error("docker_resource_attestation_failed");
|
|
750
|
-
}, { env: { WARDBY_RUN_CAPABILITY: capability } });
|
|
770
|
+
}, { env: { WARDBY_RUN_CAPABILITY: capability, ...(mcpToken !== undefined ? { [MCP_TOKEN_ENV]: mcpToken } : {}) } });
|
|
751
771
|
await this.startContainer(plan.names.workerContainer);
|
|
752
772
|
record.phase = "active";
|
|
753
773
|
await this.writeRecord(record);
|
|
@@ -939,8 +959,12 @@ export class DockerJobLauncher {
|
|
|
939
959
|
const capability = capabilityFromInspection(inspection);
|
|
940
960
|
if (!capability || hash(capability) !== record.capabilityHash)
|
|
941
961
|
throw new Error("docker_resource_attestation_failed");
|
|
962
|
+
// Only a run with MCP tools carries a token, and only the one its record hashed.
|
|
963
|
+
const mcpToken = record.spec.mcpTools ? mcpTokenFromInspection(inspection) : undefined;
|
|
964
|
+
if (record.spec.mcpTools && (!mcpToken || hash(mcpToken) !== record.mcpTokenHash))
|
|
965
|
+
throw new Error("docker_resource_attestation_failed");
|
|
942
966
|
if (record.spec.provider === "claude-code") {
|
|
943
|
-
assertClaudeAgentContainerInspection(inspection, record.spec, capability);
|
|
967
|
+
assertClaudeAgentContainerInspection(inspection, record.spec, capability, mcpToken);
|
|
944
968
|
const tool = await this.inspect([
|
|
945
969
|
"container",
|
|
946
970
|
"inspect",
|
|
@@ -956,7 +980,7 @@ export class DockerJobLauncher {
|
|
|
956
980
|
}
|
|
957
981
|
}
|
|
958
982
|
else {
|
|
959
|
-
assertWorkerContainerInspection(inspection, record.spec, capability, await this.attestedNetworkKeeperId(record));
|
|
983
|
+
assertWorkerContainerInspection(inspection, record.spec, capability, await this.attestedNetworkKeeperId(record), mcpToken);
|
|
960
984
|
}
|
|
961
985
|
const phase = dockerState(inspection);
|
|
962
986
|
if (!phase || phase === "provisioning" || phase === "active")
|
|
@@ -13,6 +13,8 @@ export declare const WORKER_CONTAINER = "worker";
|
|
|
13
13
|
export declare const TOOL_RUNNER_CONTAINER = "tool-runner";
|
|
14
14
|
/** The run Secret's key holding the tool runner's setup (claude-tool-setup.ts). */
|
|
15
15
|
export declare const TOOL_SETUP_SECRET_KEY = "tool-setup";
|
|
16
|
+
/** The run Secret's key for the MCP token (a run with MCP tools only); the agent container alone reads it. */
|
|
17
|
+
export declare const MCP_TOKEN_SECRET_KEY = "mcp-token";
|
|
16
18
|
/** Where the tool runner listens and the agent's relay connects (claude-tool-runner/main.mjs). */
|
|
17
19
|
export declare const TOOL_SOCKET_PATH = "/run/wardby/tool/runner.sock";
|
|
18
20
|
/** Every service sidecar's container name starts with this (service-postgres, service-redis, ...). */
|
|
@@ -82,7 +84,7 @@ export interface RunPodOptions {
|
|
|
82
84
|
}
|
|
83
85
|
export declare function buildRunPod(spec: JobSpec, options: RunPodOptions): V1Pod;
|
|
84
86
|
export declare function buildRunNetworkPolicy(spec: JobSpec, namespace: string): V1NetworkPolicy;
|
|
85
|
-
export declare function buildCapabilitySecret(spec: JobSpec, namespace: string, capability: string, toolSetup?: string): V1Secret;
|
|
87
|
+
export declare function buildCapabilitySecret(spec: JobSpec, namespace: string, capability: string, toolSetup?: string, mcpToken?: string): V1Secret;
|
|
86
88
|
/** A copy of a container's resources with every quantity in one rendering ("1" and "1000m" alike); see normalizeResources. */
|
|
87
89
|
export declare function canonicalResources(r?: V1Container["resources"]): V1Container["resources"];
|
|
88
90
|
/**
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { isIP } from "node:net";
|
|
9
9
|
import { StoredCodingServicesSchema } from "../../coding/services/catalog.js";
|
|
10
|
+
import { MCP_TOKEN_ENV } from "../../coding/mcp-token.js";
|
|
10
11
|
import { CLAUDE_CONFIG_PATH, CLAUDE_CONFIG_SCRATCH_MIB, CLAUDE_TOOL_SETUP_ENV, CODING_PROXY_ALIAS, CODING_PROXY_DENY_PORT, CODING_PROXY_PORT, CODING_WORKER_GID, CODING_WORKER_UID, WORKER_STOP_GRACE_SECONDS, claudeToolLimits, isRepositoryDigest, } from "./docker-isolation.js";
|
|
11
12
|
import { GVISOR_RUNTIME_CLASS, KubernetesPlatformError, STORAGE_INIT_EPHEMERAL_MIB, TOOL_RUNNER_EPHEMERAL_MIB, WORKER_EPHEMERAL_MIB, conformResources, describeMib, normalizePlatformMetadata, platformProfile, podEphemeralStorageMib, } from "./kubernetes-platform.js";
|
|
12
13
|
export const KUBERNETES_ISOLATION_ERROR = "kubernetes_isolation_unsupported";
|
|
@@ -19,6 +20,8 @@ export const WORKER_CONTAINER = "worker";
|
|
|
19
20
|
export const TOOL_RUNNER_CONTAINER = "tool-runner";
|
|
20
21
|
/** The run Secret's key holding the tool runner's setup (claude-tool-setup.ts). */
|
|
21
22
|
export const TOOL_SETUP_SECRET_KEY = "tool-setup";
|
|
23
|
+
/** The run Secret's key for the MCP token (a run with MCP tools only); the agent container alone reads it. */
|
|
24
|
+
export const MCP_TOKEN_SECRET_KEY = "mcp-token";
|
|
22
25
|
/** Where the tool runner listens and the agent's relay connects (claude-tool-runner/main.mjs). */
|
|
23
26
|
export const TOOL_SOCKET_PATH = "/run/wardby/tool/runner.sock";
|
|
24
27
|
const CODEX_ENTRYPOINT = "/opt/wardby/coding-worker/main.js";
|
|
@@ -172,6 +175,8 @@ export function validateKubernetesSpec(spec) {
|
|
|
172
175
|
if (spec.sessionResume !== undefined && (spec.sessionResume !== true || spec.provider !== "claude-code")) {
|
|
173
176
|
throw isolationError("session-resume-not-claude");
|
|
174
177
|
}
|
|
178
|
+
if (spec.mcpTools !== undefined && spec.mcpTools !== true)
|
|
179
|
+
throw isolationError("mcp-tools-invalid");
|
|
175
180
|
if (!isRegistryDigest(spec.image))
|
|
176
181
|
throw isolationError("image-not-registry-digest");
|
|
177
182
|
if (spec.services !== undefined) {
|
|
@@ -313,6 +318,10 @@ export function buildRunPod(spec, options) {
|
|
|
313
318
|
env: [
|
|
314
319
|
{ name: "WARDBY_PROXY_URL", value: `http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}` },
|
|
315
320
|
{ name: "WARDBY_RUN_CAPABILITY", valueFrom: { secretKeyRef: { name: names.secret, key: "capability" } } },
|
|
321
|
+
// A run with MCP tools only: the agent's bearer for the proxy's /mcp route. Never the tool runner's.
|
|
322
|
+
...(spec.mcpTools
|
|
323
|
+
? [{ name: MCP_TOKEN_ENV, valueFrom: { secretKeyRef: { name: names.secret, key: MCP_TOKEN_SECRET_KEY } } }]
|
|
324
|
+
: []),
|
|
316
325
|
],
|
|
317
326
|
securityContext: containerSecurity(),
|
|
318
327
|
resources: conformResources(profile, {
|
|
@@ -464,14 +473,18 @@ export function buildRunNetworkPolicy(spec, namespace) {
|
|
|
464
473
|
},
|
|
465
474
|
};
|
|
466
475
|
}
|
|
467
|
-
export function buildCapabilitySecret(spec, namespace, capability, toolSetup) {
|
|
476
|
+
export function buildCapabilitySecret(spec, namespace, capability, toolSetup, mcpToken) {
|
|
468
477
|
const names = kubernetesRunNames(spec.runId);
|
|
469
478
|
return {
|
|
470
479
|
apiVersion: "v1",
|
|
471
480
|
kind: "Secret",
|
|
472
481
|
type: "Opaque",
|
|
473
482
|
metadata: { name: names.secret, namespace, labels: runLabels(spec.runId) },
|
|
474
|
-
stringData: {
|
|
483
|
+
stringData: {
|
|
484
|
+
capability,
|
|
485
|
+
...(toolSetup !== undefined ? { [TOOL_SETUP_SECRET_KEY]: toolSetup } : {}),
|
|
486
|
+
...(mcpToken !== undefined ? { [MCP_TOKEN_SECRET_KEY]: mcpToken } : {}),
|
|
487
|
+
},
|
|
475
488
|
};
|
|
476
489
|
}
|
|
477
490
|
/**
|
|
@@ -14,6 +14,11 @@ export interface KubernetesJobLauncherOptions {
|
|
|
14
14
|
config: KubernetesJobConfig;
|
|
15
15
|
workspaceRoot: string;
|
|
16
16
|
resolveCapability: (runId: string) => Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* The run's MCP token (the capability vault), asked for only when the spec has mcpTools; it goes into
|
|
19
|
+
* the run's Secret next to the capability. Absent = a run with MCP tools can't be launched.
|
|
20
|
+
*/
|
|
21
|
+
resolveMcpToken?: (runId: string) => Promise<string | undefined>;
|
|
17
22
|
/**
|
|
18
23
|
* Cluster preflight run once before the first launch; failure fails every launch. It may return the
|
|
19
24
|
* proxy ClusterIP it validated; otherwise the launcher reads the proxy witness once itself. Either way
|
|
@@ -95,6 +100,14 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
|
|
|
95
100
|
hasCapacityFor(spec: JobSpec): Promise<boolean>;
|
|
96
101
|
private readonly warnedOnce;
|
|
97
102
|
private warnOnce;
|
|
103
|
+
/**
|
|
104
|
+
* Wakes what the run needs before anything is created: the coding proxy always, and the native
|
|
105
|
+
* gateway too for a run with MCP tools (its MCP calls go proxy -> gateway; without a gateway the
|
|
106
|
+
* worker would start with its MCP tools missing). Both wake at once, so a cold start waits for
|
|
107
|
+
* the slower of the two, not their sum. A gateway that does not wake fails the launch with
|
|
108
|
+
* coding_mcp_gateway_unavailable; the proxy's own wake error is reported first.
|
|
109
|
+
*/
|
|
110
|
+
private wakeForRun;
|
|
98
111
|
launch(spec: JobSpec): Promise<JobHandle>;
|
|
99
112
|
status(handle: JobHandle): Promise<JobStatus>;
|
|
100
113
|
collect(handle: JobHandle): Promise<JobResult>;
|
|
@@ -29,13 +29,14 @@ import { finished } from "node:stream/promises";
|
|
|
29
29
|
import { createHash } from "node:crypto";
|
|
30
30
|
import { isIP } from "node:net";
|
|
31
31
|
import { logger } from "../../core/logger.js";
|
|
32
|
-
import { DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
|
|
32
|
+
import { CODING_MCP_GATEWAY_UNAVAILABLE, DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
|
|
33
33
|
import { MAX_CODING_ARTIFACT_BYTES, MAX_CODING_INPUT_BYTES, MAX_DEBUG_TRACE_LINE_BYTES, parseCodingWorkerOutputJson, } from "../../coding/protocol.js";
|
|
34
34
|
import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
|
|
35
35
|
import { parseWorkerDiagnosticLine } from "./docker.js";
|
|
36
36
|
import { KubernetesAlreadyExistsError, KubernetesConflictError, KubernetesQuotaExceededError, } from "./kubernetes-api.js";
|
|
37
37
|
import { ENFORCEMENT_PROBE_DENY_REACHABLE, ENFORCEMENT_PROBE_PROVEN, ENFORCEMENT_PROBE_DENY_REFUSED, ENFORCEMENT_PROBE_PROXY_UNREACHABLE, KEEPER_CONTAINER, KEEPER_SEEDED_MARKER, KUBERNETES_ISOLATION_ERROR, STORAGE_ROOT, TOOL_RUNNER_CONTAINER, WORKER_CONTAINER, assertRunNetworkPolicyMatches, assertRunPodMatches, buildCapabilitySecret, buildRunNetworkPolicy, buildRunPod, enforcementStreakScript, kubernetesRunNames, kubernetesRunNamesForToken, runLabels, serviceContainerName, validateKubernetesSpec, } from "./kubernetes-isolation.js";
|
|
38
38
|
import { claudeToolSetup } from "./claude-tool-setup.js";
|
|
39
|
+
import { isMcpToken } from "../../coding/mcp-token.js";
|
|
39
40
|
import { podQuotaUsage, quotaShortfall } from "./kubernetes-quota.js";
|
|
40
41
|
import { readProxyWitness } from "./kubernetes-witness.js";
|
|
41
42
|
import { safeExtract } from "./safe-extract.js";
|
|
@@ -499,6 +500,25 @@ export class KubernetesJobLauncher {
|
|
|
499
500
|
this.warnedOnce.add(message);
|
|
500
501
|
this.warn(message);
|
|
501
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Wakes what the run needs before anything is created: the coding proxy always, and the native
|
|
505
|
+
* gateway too for a run with MCP tools (its MCP calls go proxy -> gateway; without a gateway the
|
|
506
|
+
* worker would start with its MCP tools missing). Both wake at once, so a cold start waits for
|
|
507
|
+
* the slower of the two, not their sum. A gateway that does not wake fails the launch with
|
|
508
|
+
* coding_mcp_gateway_unavailable; the proxy's own wake error is reported first.
|
|
509
|
+
*/
|
|
510
|
+
async wakeForRun(spec) {
|
|
511
|
+
const opts = { timeoutMs: wakeTimeoutMs(this.readyTimeoutMs), runId: spec.runId };
|
|
512
|
+
const [proxy, gateway] = await Promise.allSettled([
|
|
513
|
+
this.power.ensureAwake("proxy", opts),
|
|
514
|
+
spec.mcpTools ? this.power.ensureAwake("gateway", opts) : Promise.resolve(),
|
|
515
|
+
]);
|
|
516
|
+
if (proxy.status === "rejected")
|
|
517
|
+
throw proxy.reason;
|
|
518
|
+
if (gateway.status === "rejected") {
|
|
519
|
+
throw new Error(`${CODING_MCP_GATEWAY_UNAVAILABLE}: the native gateway, which serves this run's MCP tools, did not wake.`, { cause: gateway.reason });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
502
522
|
async launch(spec) {
|
|
503
523
|
try {
|
|
504
524
|
validateKubernetesSpec(spec);
|
|
@@ -515,9 +535,8 @@ export class KubernetesJobLauncher {
|
|
|
515
535
|
}
|
|
516
536
|
// Low-power mode: the proxy may be scaled to zero; wake it before the preflight reads it. Guarded
|
|
517
537
|
// (not just a no-op await) so mode off keeps launch() joining an in-flight warm-up synchronously.
|
|
518
|
-
if (this.power.enabled)
|
|
519
|
-
await this.
|
|
520
|
-
}
|
|
538
|
+
if (this.power.enabled)
|
|
539
|
+
await this.wakeForRun(spec);
|
|
521
540
|
await this.runPreflight();
|
|
522
541
|
const names = kubernetesRunNames(spec.runId);
|
|
523
542
|
const handle = { backend: BACKEND, id: `${this.namespace}/${names.token}` };
|
|
@@ -838,6 +857,10 @@ export class KubernetesJobLauncher {
|
|
|
838
857
|
const capability = await this.options.resolveCapability(spec.runId);
|
|
839
858
|
if (!CAPABILITY.test(capability))
|
|
840
859
|
throw new Error("kubernetes_capability_invalid");
|
|
860
|
+
const mcpToken = spec.mcpTools ? await this.options.resolveMcpToken?.(spec.runId) : undefined;
|
|
861
|
+
if (spec.mcpTools && !isMcpToken(mcpToken)) {
|
|
862
|
+
throw new Error("kubernetes_mcp_token_invalid");
|
|
863
|
+
}
|
|
841
864
|
// Re-read (not just the ClusterIP): the gate below reads "deny port unreachable" as evidence,
|
|
842
865
|
// which is only meaningful against a Service that actually exposes it with a ready backend.
|
|
843
866
|
const witness = await readProxyWitness(this.api, this.namespace, proxyService);
|
|
@@ -851,7 +874,7 @@ export class KubernetesJobLauncher {
|
|
|
851
874
|
});
|
|
852
875
|
const policy = buildRunNetworkPolicy(spec, this.namespace);
|
|
853
876
|
const toolSetup = spec.provider === "claude-code" ? claudeToolSetup(spec, capability) : undefined;
|
|
854
|
-
await this.createIfMissing(() => this.api.createSecret(this.namespace, buildCapabilitySecret(spec, this.namespace, capability, toolSetup)));
|
|
877
|
+
await this.createIfMissing(() => this.api.createSecret(this.namespace, buildCapabilitySecret(spec, this.namespace, capability, toolSetup, mcpToken)));
|
|
855
878
|
await this.createIfMissing(() => this.api.createNetworkPolicy(this.namespace, policy));
|
|
856
879
|
await this.createIfMissing(() => this.api.createPod(this.namespace, pod));
|
|
857
880
|
await this.waitForKeeper(names, spec.runId, (spec.services ?? []).map((service) => service.name), spec.provider === "claude-code");
|
|
@@ -29,6 +29,12 @@ export interface JobSpec {
|
|
|
29
29
|
* scratch so a long session never fills the worker's shared /tmp.
|
|
30
30
|
*/
|
|
31
31
|
sessionResume?: true;
|
|
32
|
+
/**
|
|
33
|
+
* Set only when the run has pinned MCP tools (remote MCP servers); absent otherwise, never false.
|
|
34
|
+
* The launcher then delivers the run's MCP token, from the capability vault, as WARDBY_MCP_TOKEN to
|
|
35
|
+
* the agent container only (never Claude's tool runner). The token itself is never in the spec.
|
|
36
|
+
*/
|
|
37
|
+
mcpTools?: true;
|
|
32
38
|
timeoutSec: number;
|
|
33
39
|
limits: JobResourceLimits;
|
|
34
40
|
labels: Record<string, string>;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host-neutral code-review surface for native agents. GitHub (./github.ts) and
|
|
3
|
-
* local git repositories (./local.ts) implement it
|
|
3
|
+
* local git repositories (./local.ts) implement it, and registered host providers add their own. Every method
|
|
4
4
|
* takes the repository and mints its own least-permission credential — no
|
|
5
5
|
* method accepts a token or a permission. See
|
|
6
6
|
* docs/private/2026-09-25-code-review-host-design.md §5.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
export
|
|
8
|
+
/** Compiled-in hosts. Registered host providers add their own names at runtime. */
|
|
9
|
+
export type BuiltInHostProvider = "github" | "local";
|
|
10
|
+
export type ReviewHostProvider = BuiltInHostProvider | (string & {});
|
|
11
|
+
export declare const REVIEW_HOST_PROVIDERS: readonly BuiltInHostProvider[];
|
|
10
12
|
/** Providers with host user accounts to link (link_host_account); a local repository has none. */
|
|
11
|
-
export declare const HOST_ACCOUNT_PROVIDERS: readonly
|
|
13
|
+
export declare const HOST_ACCOUNT_PROVIDERS: readonly BuiltInHostProvider[];
|
|
12
14
|
export type ReviewVerdict = "APPROVE" | "CHANGES_REQUESTED" | "COMMENT";
|
|
13
15
|
export type CheckConclusion = "success" | "failure" | "neutral";
|
|
14
16
|
export type CommentSide = "LEFT" | "RIGHT";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host-neutral code-review surface for native agents. GitHub (./github.ts) and
|
|
3
|
-
* local git repositories (./local.ts) implement it
|
|
3
|
+
* local git repositories (./local.ts) implement it, and registered host providers add their own. Every method
|
|
4
4
|
* takes the repository and mints its own least-permission credential — no
|
|
5
5
|
* method accepts a token or a permission. See
|
|
6
6
|
* docs/private/2026-09-25-code-review-host-design.md §5.
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
1
|
+
import type { RepositoryFileInput } from "./types.js";
|
|
2
|
+
import { type GitRemote } from "./remote.js";
|
|
3
3
|
import type { ContinuationFinishedDetails, ContinuationOutcome, ContinuationTarget, DescriptionWriteOutcome, FinalizeChangesDetails, FinalizeChangesResult, PreparedWorkspace, VcsPrepareInput, VcsProvider } from "./types.js";
|
|
4
4
|
export declare const DEFAULT_MAX_CHANGED_FILES = 1000;
|
|
5
5
|
export declare const DEFAULT_MAX_DIFF_BYTES: number;
|
|
6
6
|
export interface GitCommandOptions {
|
|
7
7
|
cwd?: string;
|
|
8
8
|
authToken?: string;
|
|
9
|
+
/** The username git sends with the token (default: git-askpass.sh's GitHub default). Not secret. */
|
|
10
|
+
authUsername?: string;
|
|
9
11
|
maxOutputBytes?: number;
|
|
10
12
|
/** Non-zero exit codes that still resolve with the output (`merge-tree` exits 1 on conflicts). */
|
|
11
13
|
acceptExitCodes?: readonly number[];
|
|
@@ -127,6 +129,8 @@ export declare class GitVcsProvider implements VcsProvider {
|
|
|
127
129
|
* problem).
|
|
128
130
|
*/
|
|
129
131
|
private assertSafeLocalConfig;
|
|
132
|
+
/** The git credentials for a remote's access token; none when the remote needs no token. */
|
|
133
|
+
private gitAuth;
|
|
130
134
|
private pushOnce;
|
|
131
135
|
private remoteHead;
|
|
132
136
|
private revParse;
|