@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,287 @@
|
|
|
1
|
+
import { ownerCanUseMcpServer } from "./grants.js";
|
|
2
|
+
import { loadMcpServersMode, MCP_SERVERS_OFF_MESSAGE } from "../config/mcp-servers.js";
|
|
3
|
+
import { loadToolMaxTimeoutMs } from "../config/tool-timeout.js";
|
|
4
|
+
import { McpClientError, McpHttpClient } from "./mcp-client/client.js";
|
|
5
|
+
import { toolResultText } from "./mcp-client/results.js";
|
|
6
|
+
import { exposedToolName, loadServerSecrets, logCredentialsUnavailable, McpCredentialsUnavailableError, McpServerInputError, redactForServer, serverFetch, snapshotOf, } from "./mcp-servers.js";
|
|
7
|
+
import { parseSecretBrokerConfig } from "./secret-broker-config.js";
|
|
8
|
+
import { withEscapedForms } from "./secret-spans.js";
|
|
9
|
+
import { consoleRedactionValues, encodedForms } from "../sandbox/secret-broker.js";
|
|
10
|
+
export const DEFAULT_MCP_CALL_TIMEOUT_MS = 30_000;
|
|
11
|
+
/** What a grantee's run sees when the server owner's credentials can't be loaded. */
|
|
12
|
+
export const SHARED_CREDENTIALS_MESSAGE = "The MCP server's credentials can't be used; ask its owner to check it.";
|
|
13
|
+
/**
|
|
14
|
+
* A Codex coding agent runs shell commands that can read the run's MCP token, so it may use only tools
|
|
15
|
+
* the server marks read-only (annotations.readOnlyHint === true in the saved tool list).
|
|
16
|
+
*/
|
|
17
|
+
export function isMarkedReadOnly(entry) {
|
|
18
|
+
return entry?.annotations?.readOnlyHint === true;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether a coding provider gets the Codex read-only rule. Fails closed: anything but exactly
|
|
22
|
+
* "claude-code" (Codex, an unknown value, or a coding agent with no profile) counts as Codex. A run
|
|
23
|
+
* is judged by the provider it was dispatched with (CodingRun.provider), never the agent's current
|
|
24
|
+
* profile, which can change between dispatch and launch.
|
|
25
|
+
*/
|
|
26
|
+
export function isCodexCoding(provider) {
|
|
27
|
+
return provider !== "claude-code";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds pins for an agent's attachments: allowed tools present in the snapshot only, from servers the
|
|
31
|
+
* agent's owner may still use (owns, or holds use on); none when the mode is off.
|
|
32
|
+
*/
|
|
33
|
+
export async function loadMcpToolPins(db, agentId, options = {}) {
|
|
34
|
+
if (loadMcpServersMode() === "off")
|
|
35
|
+
return [];
|
|
36
|
+
const rows = await db.agentMcpServer.findMany({
|
|
37
|
+
where: { agentId },
|
|
38
|
+
include: { server: true, agent: { select: { ownerId: true } } },
|
|
39
|
+
orderBy: [{ createdAt: "asc" }, { serverId: "asc" }],
|
|
40
|
+
});
|
|
41
|
+
const pins = [];
|
|
42
|
+
for (const row of rows) {
|
|
43
|
+
if (!(await ownerCanUseMcpServer(db, row.agent.ownerId, row.server)))
|
|
44
|
+
continue;
|
|
45
|
+
const byName = new Map(snapshotOf(row.server).map((t) => [t.name, t]));
|
|
46
|
+
for (const toolName of row.allowedTools) {
|
|
47
|
+
const entry = byName.get(toolName);
|
|
48
|
+
if (!entry)
|
|
49
|
+
continue;
|
|
50
|
+
// A refresh can drop readOnlyHint after attach: such a tool stays out of a Codex run.
|
|
51
|
+
if (options.readOnlyOnly === true && !isMarkedReadOnly(entry))
|
|
52
|
+
continue;
|
|
53
|
+
let exposedName;
|
|
54
|
+
try {
|
|
55
|
+
exposedName = exposedToolName(row.server.name, toolName);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
pins.push({
|
|
61
|
+
exposedName,
|
|
62
|
+
toolName,
|
|
63
|
+
serverId: row.server.id,
|
|
64
|
+
serverName: row.server.name,
|
|
65
|
+
...(row.callTimeoutMs !== null ? { callTimeoutMs: row.callTimeoutMs } : {}),
|
|
66
|
+
definition: { description: entry.description, inputSchema: entry.inputSchema },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return pins;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The pins whose exposed name no other tool of the run uses. Attach refuses clashes; this covers a
|
|
74
|
+
* name that became a built-in (or an attached tool) later: the MCP tool is left out, with a warning.
|
|
75
|
+
*/
|
|
76
|
+
export function withoutClashingPins(pins, otherNames, log) {
|
|
77
|
+
const taken = new Set(otherNames);
|
|
78
|
+
return pins.filter((pin) => {
|
|
79
|
+
if (!taken.has(pin.exposedName)) {
|
|
80
|
+
taken.add(pin.exposedName);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
log.warn({ serverId: pin.serverId, tool: pin.toolName, exposedName: pin.exposedName }, "MCP tool left out of the run: its name clashes with another tool");
|
|
84
|
+
return false;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function mcpToolDefs(pins) {
|
|
88
|
+
return pins.map((pin) => ({
|
|
89
|
+
name: pin.exposedName,
|
|
90
|
+
description: `From MCP server ${pin.serverName}: ${pin.definition.description}`,
|
|
91
|
+
jsonSchema: pin.definition.inputSchema,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
function error(code, message) {
|
|
95
|
+
return JSON.stringify({ error: code, message });
|
|
96
|
+
}
|
|
97
|
+
function parseArgs(argsJson) {
|
|
98
|
+
if (argsJson.trim() === "")
|
|
99
|
+
return {};
|
|
100
|
+
try {
|
|
101
|
+
const value = JSON.parse(argsJson);
|
|
102
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
103
|
+
? value
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Never throws. Re-checks the live attachment, validates the arguments, runs initialize → tools/call →
|
|
112
|
+
* close under the time limit, and returns the redacted result text or a JSON error. One log line per call.
|
|
113
|
+
*/
|
|
114
|
+
export async function handleMcpTool(pin, argsJson, ctx) {
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
const done = (text, outcome) => {
|
|
117
|
+
ctx.log.info({
|
|
118
|
+
runId: ctx.runId,
|
|
119
|
+
agentId: ctx.agentId,
|
|
120
|
+
serverId: pin.serverId,
|
|
121
|
+
tool: pin.toolName,
|
|
122
|
+
outcome,
|
|
123
|
+
durationMs: Date.now() - startedAt,
|
|
124
|
+
resultChars: text.length,
|
|
125
|
+
}, "mcp tool call");
|
|
126
|
+
return text;
|
|
127
|
+
};
|
|
128
|
+
const fail = (code, message) => done(error(code, message), code);
|
|
129
|
+
let values = [];
|
|
130
|
+
try {
|
|
131
|
+
if (loadMcpServersMode() === "off")
|
|
132
|
+
return fail("mcp_servers_off", MCP_SERVERS_OFF_MESSAGE);
|
|
133
|
+
const row = await ctx.db.agentMcpServer.findUnique({
|
|
134
|
+
where: { agentId_serverId: { agentId: ctx.agentId, serverId: pin.serverId } },
|
|
135
|
+
include: { server: true, agent: { select: { ownerId: true } } },
|
|
136
|
+
});
|
|
137
|
+
if (!row || !row.allowedTools.includes(pin.toolName)) {
|
|
138
|
+
return fail("mcp_tool_not_allowed", `The tool "${pin.exposedName}" is no longer allowed for this agent: its MCP server was detached or the tool was taken off the allowed list.`);
|
|
139
|
+
}
|
|
140
|
+
// A revoked or lowered grant stops the very next call, even before the attachment is pruned.
|
|
141
|
+
if (!(await ownerCanUseMcpServer(ctx.db, row.agent.ownerId, row.server))) {
|
|
142
|
+
return fail("mcp_tool_not_allowed", `The tool "${pin.exposedName}" is no longer allowed for this agent: its owner no longer has use access to MCP server "${row.server.name}".`);
|
|
143
|
+
}
|
|
144
|
+
const live = snapshotOf(row.server).find((t) => t.name === pin.toolName);
|
|
145
|
+
if (!live) {
|
|
146
|
+
return fail("mcp_tool_removed", `The tool "${pin.exposedName}" is no longer in MCP server "${row.server.name}"'s tool list.`);
|
|
147
|
+
}
|
|
148
|
+
if (ctx.readOnlyOnly === true && !isMarkedReadOnly(live)) {
|
|
149
|
+
return fail("mcp_tool_not_allowed", `The tool "${pin.exposedName}" is no longer allowed for this agent: Codex agents can use only tools the server marks read-only.`);
|
|
150
|
+
}
|
|
151
|
+
const args = parseArgs(argsJson);
|
|
152
|
+
if (!args)
|
|
153
|
+
return fail("invalid_arguments", "The tool's arguments must be a JSON object.");
|
|
154
|
+
const configured = Math.min(row.callTimeoutMs ?? pin.callTimeoutMs ?? DEFAULT_MCP_CALL_TIMEOUT_MS, loadToolMaxTimeoutMs());
|
|
155
|
+
const untilDeadline = ctx.deadlineAt ? ctx.deadlineAt.getTime() - Date.now() : Infinity;
|
|
156
|
+
const limitMs = Math.max(0, Math.min(configured, untilDeadline));
|
|
157
|
+
const timedOut = `The MCP server did not answer within ${limitMs} ms.`;
|
|
158
|
+
if (ctx.signal.aborted)
|
|
159
|
+
return fail("mcp_call_timeout", "The call was stopped.");
|
|
160
|
+
if (limitMs <= 0)
|
|
161
|
+
return fail("mcp_call_timeout", "The run's time limit has been reached.");
|
|
162
|
+
const server = {
|
|
163
|
+
id: row.server.id,
|
|
164
|
+
ownerId: row.server.ownerId,
|
|
165
|
+
name: row.server.name,
|
|
166
|
+
url: row.server.url,
|
|
167
|
+
secretNames: row.server.secretNames,
|
|
168
|
+
};
|
|
169
|
+
let secrets;
|
|
170
|
+
try {
|
|
171
|
+
secrets = await loadServerSecrets(ctx.db, ctx.cipher, server);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const shared = row.agent.ownerId !== server.ownerId;
|
|
175
|
+
if (err instanceof McpCredentialsUnavailableError) {
|
|
176
|
+
logCredentialsUnavailable(ctx.log, err);
|
|
177
|
+
return fail(err.code, shared ? SHARED_CREDENTIALS_MESSAGE : err.message);
|
|
178
|
+
}
|
|
179
|
+
// The detail names the server owner's secrets and speaks to that owner: a grantee's run gets neither.
|
|
180
|
+
if (err instanceof McpServerInputError && shared) {
|
|
181
|
+
return fail("mcp_server_error", SHARED_CREDENTIALS_MESSAGE);
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
values = secrets.values;
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
let stopped = false;
|
|
188
|
+
const onStop = () => {
|
|
189
|
+
stopped = true;
|
|
190
|
+
controller.abort();
|
|
191
|
+
};
|
|
192
|
+
ctx.signal.addEventListener("abort", onStop, { once: true });
|
|
193
|
+
const timer = setTimeout(() => controller.abort(), limitMs);
|
|
194
|
+
// The race ends the call even if a transport ignores the signal.
|
|
195
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
196
|
+
controller.signal.addEventListener("abort", () => reject(new McpClientError("mcp_call_timeout", "aborted")), {
|
|
197
|
+
once: true,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
aborted.catch(() => { });
|
|
201
|
+
// A stop that fired while the secrets were loading came before the listener; act on it now.
|
|
202
|
+
if (ctx.signal.aborted)
|
|
203
|
+
onStop();
|
|
204
|
+
const client = new McpHttpClient({
|
|
205
|
+
fetch: serverFetch(server, secrets.accessor, { timeoutMs: limitMs, fetchImpl: ctx.fetchImpl }),
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
});
|
|
208
|
+
try {
|
|
209
|
+
const result = await Promise.race([
|
|
210
|
+
(async () => {
|
|
211
|
+
await client.initialize();
|
|
212
|
+
return client.callTool(pin.toolName, args);
|
|
213
|
+
})(),
|
|
214
|
+
aborted,
|
|
215
|
+
]);
|
|
216
|
+
const { text, isError } = toolResultText(result, (raw) => redactForServer(raw, values));
|
|
217
|
+
return done(text, isError ? "mcp_server_error" : "ok");
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
if (controller.signal.aborted) {
|
|
221
|
+
return fail("mcp_call_timeout", stopped ? "The call was stopped." : timedOut);
|
|
222
|
+
}
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
ctx.signal.removeEventListener("abort", onStop);
|
|
228
|
+
// Best effort; never waits on a server that already ran out of time.
|
|
229
|
+
if (controller.signal.aborted)
|
|
230
|
+
void client.close().catch(() => { });
|
|
231
|
+
else
|
|
232
|
+
await client.close().catch(() => { });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
const message = redactForServer(err instanceof Error ? err.message : "The MCP server call failed.", values);
|
|
237
|
+
if (err instanceof McpClientError)
|
|
238
|
+
return fail(err.code, message);
|
|
239
|
+
if (err instanceof McpServerInputError)
|
|
240
|
+
return fail("mcp_server_error", message);
|
|
241
|
+
return fail("mcp_server_unreachable", "The MCP server could not be reached.");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Every secret value of every server attached to the agent, named `mcp:<server>/<secret>`, in
|
|
246
|
+
* every form the server's own redaction looks for. A secret that no longer exists has no value to
|
|
247
|
+
* leak and is skipped; a read that fails rejects (callers fail closed).
|
|
248
|
+
*/
|
|
249
|
+
export async function mcpNamedSecretValues(db, cipher, agentId) {
|
|
250
|
+
const rows = await db.agentMcpServer.findMany({
|
|
251
|
+
where: { agentId },
|
|
252
|
+
select: { server: { select: { name: true, ownerId: true, secretNames: true } } },
|
|
253
|
+
});
|
|
254
|
+
const named = [];
|
|
255
|
+
for (const { server } of rows) {
|
|
256
|
+
for (const secretName of server.secretNames) {
|
|
257
|
+
const stored = await db.secret.findUnique({
|
|
258
|
+
where: { ownerId_name: { ownerId: server.ownerId, name: secretName } },
|
|
259
|
+
select: { ciphertext: true, broker: true },
|
|
260
|
+
});
|
|
261
|
+
if (!stored)
|
|
262
|
+
continue;
|
|
263
|
+
const value = await cipher.decrypt(stored.ciphertext);
|
|
264
|
+
const base = consoleRedactionValues({ value, broker: readableBroker(stored.broker) });
|
|
265
|
+
named.push({
|
|
266
|
+
name: `mcp:${server.name}/${secretName}`,
|
|
267
|
+
values: withEscapedForms([...new Set([...base, ...base.flatMap(encodedForms)])]),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return named;
|
|
272
|
+
}
|
|
273
|
+
/** A stored broker config, or null when there is none or it cannot be read (the value alone is then redacted). */
|
|
274
|
+
function readableBroker(stored) {
|
|
275
|
+
if (stored === null || stored === undefined)
|
|
276
|
+
return null;
|
|
277
|
+
try {
|
|
278
|
+
return parseSecretBrokerConfig(stored);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** Secret values for every attached server of this agent (for run-text masking and gateway cause checks). */
|
|
285
|
+
export async function mcpSecretValuesForAgent(db, cipher, agentId) {
|
|
286
|
+
return (await mcpNamedSecretValues(db, cipher, agentId)).flatMap((n) => n.values);
|
|
287
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { codeProviderOf, normalizeHostedRepository, pullRequestWebUrl } from "../coding/code-host.js";
|
|
2
2
|
import { logger } from "./logger.js";
|
|
3
3
|
import { maybeMarkReady } from "./mark-ready.js";
|
|
4
4
|
import { collectRelatedPullRequests, runTreeRoot } from "./related-pull-requests.js";
|
|
@@ -31,18 +31,31 @@ export function mergeOrderVerdict(self, set) {
|
|
|
31
31
|
return { status: "in_progress", waitingFor };
|
|
32
32
|
return { status: "success", dependencies };
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
34
|
+
/** `repository#number`, as a link when the host has one: never a title or any other free text from the host. */
|
|
35
|
+
const prLink = (m) => {
|
|
36
|
+
const label = `${m.repository}#${m.number}`;
|
|
37
|
+
const url = pullRequestWebUrl(codeProviderOf(m.repository), m.repository, m.number);
|
|
38
|
+
return url ? `[${label}](${url})` : label;
|
|
39
|
+
};
|
|
37
40
|
const keyOf = (repository, number) => `${repository}#${number}`;
|
|
38
41
|
function safeRepository(value) {
|
|
39
42
|
try {
|
|
40
|
-
return
|
|
43
|
+
return normalizeHostedRepository(value);
|
|
41
44
|
}
|
|
42
45
|
catch {
|
|
43
46
|
return undefined;
|
|
44
47
|
}
|
|
45
48
|
}
|
|
49
|
+
const canPostCheck = (host) => host?.upsertNamedCheck !== undefined && host.pullRequestOrigin !== undefined;
|
|
50
|
+
/** The host that can post this repository's merge order check, if any. */
|
|
51
|
+
function checkHost(hosts, repository) {
|
|
52
|
+
const host = hosts?.[codeProviderOf(repository)];
|
|
53
|
+
return canPostCheck(host) ? host : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Whether any configured host can post the check; none means nothing to read or sync. */
|
|
56
|
+
function anyCheckHost(hosts) {
|
|
57
|
+
return Object.values(hosts ?? {}).some(canPostCheck);
|
|
58
|
+
}
|
|
46
59
|
function originState(origin) {
|
|
47
60
|
if (origin.merged)
|
|
48
61
|
return "merged";
|
|
@@ -109,8 +122,7 @@ export function mergeOrderCheckInput(self, verdict, steps, headSha, leadRunning
|
|
|
109
122
|
*/
|
|
110
123
|
export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
|
|
111
124
|
const log = deps.log ?? moduleLog;
|
|
112
|
-
|
|
113
|
-
if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
|
|
125
|
+
if (!anyCheckHost(deps.hosts))
|
|
114
126
|
return;
|
|
115
127
|
try {
|
|
116
128
|
const group = await collectRelatedPullRequests(deps.db, seedRunId);
|
|
@@ -154,9 +166,15 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
|
|
|
154
166
|
continue;
|
|
155
167
|
}
|
|
156
168
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
let origin;
|
|
170
|
+
// Reading only needs the read: a host that can't post checks still reports its pull requests.
|
|
171
|
+
const memberHost = deps.hosts?.[codeProviderOf(pr.repository)];
|
|
172
|
+
if (reused && key === keyOf(reused, opts.origin.number))
|
|
173
|
+
origin = await opts.origin.read();
|
|
174
|
+
else if (memberHost?.pullRequestOrigin)
|
|
175
|
+
origin = await memberHost.pullRequestOrigin(pr.repository, pr.number);
|
|
176
|
+
else
|
|
177
|
+
throw new Error("no_host_for_repository");
|
|
160
178
|
let headSha;
|
|
161
179
|
if (origin.markerRunId) {
|
|
162
180
|
const opener = await deps.db.codingRun.findUnique({
|
|
@@ -186,9 +204,12 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
|
|
|
186
204
|
const verdict = mergeOrderVerdict(self, set);
|
|
187
205
|
if (verdict.status === "skip")
|
|
188
206
|
continue;
|
|
207
|
+
const selfHost = checkHost(deps.hosts, self.repository);
|
|
208
|
+
if (!selfHost)
|
|
209
|
+
continue;
|
|
189
210
|
const input = mergeOrderCheckInput(self, verdict, steps, target.headSha, leadRunning);
|
|
190
211
|
try {
|
|
191
|
-
await
|
|
212
|
+
await selfHost.upsertNamedCheck(self.repository, input);
|
|
192
213
|
log.info({ seedRunId, repository: self.repository, number: self.number, status: verdict.status }, "merge order check posted");
|
|
193
214
|
}
|
|
194
215
|
catch (err) {
|
|
@@ -197,7 +218,7 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
|
|
|
197
218
|
}
|
|
198
219
|
if (input.status === "completed" && input.conclusion === "success") {
|
|
199
220
|
// The last earlier step merged: the pull request may now be ready for review (#287).
|
|
200
|
-
await maybeMarkReady({ db: deps.db, hosts: deps.hosts, log: deps.log }, self.repository, self.number);
|
|
221
|
+
await maybeMarkReady({ db: deps.db, hosts: deps.hosts, log: deps.log }, self.repository, self.number, codeProviderOf(self.repository));
|
|
201
222
|
}
|
|
202
223
|
}
|
|
203
224
|
}
|
|
@@ -212,8 +233,7 @@ export async function syncMergeOrderChecks(deps, seedRunId, opts = {}) {
|
|
|
212
233
|
export async function syncMergeOrderChecksAfterRun(deps, run) {
|
|
213
234
|
if (run.parentRunId !== null)
|
|
214
235
|
return;
|
|
215
|
-
|
|
216
|
-
if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
|
|
236
|
+
if (!anyCheckHost(deps.hosts))
|
|
217
237
|
return;
|
|
218
238
|
try {
|
|
219
239
|
if (!(await deps.db.run.findFirst({ where: { parentRunId: run.id }, select: { id: true } })))
|
|
@@ -250,12 +270,12 @@ async function orderedSeeds(db, repository, number) {
|
|
|
250
270
|
*/
|
|
251
271
|
export async function syncMergeOrderChecksForPullRequest(deps, pr, opts = {}) {
|
|
252
272
|
const log = deps.log ?? moduleLog;
|
|
253
|
-
const host = deps.hosts?.[CODING_CODE_PROVIDER];
|
|
254
|
-
if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
|
|
255
|
-
return;
|
|
256
273
|
const repository = safeRepository(pr.repository);
|
|
257
274
|
if (!repository || !Number.isSafeInteger(pr.number) || pr.number <= 0)
|
|
258
275
|
return;
|
|
276
|
+
// This pull request's own host need not post checks: its siblings' checks may depend on it.
|
|
277
|
+
if (!anyCheckHost(deps.hosts))
|
|
278
|
+
return;
|
|
259
279
|
let roots;
|
|
260
280
|
try {
|
|
261
281
|
roots = [];
|
|
@@ -297,8 +317,7 @@ export function resetMergeOrderSweepForTests() {
|
|
|
297
317
|
*/
|
|
298
318
|
export async function sweepMergeOrderChecks(deps, now = new Date()) {
|
|
299
319
|
const log = deps.log ?? moduleLog;
|
|
300
|
-
|
|
301
|
-
if (!host?.upsertNamedCheck || !host.pullRequestOrigin)
|
|
320
|
+
if (!anyCheckHost(deps.hosts))
|
|
302
321
|
return 0;
|
|
303
322
|
if (now.getTime() - lastSweepAt < MERGE_ORDER_SWEEP_INTERVAL_MS)
|
|
304
323
|
return 0;
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { Prisma } from "#prisma";
|
|
36
36
|
import { ChatError, } from "../providers/chat/types.js";
|
|
37
|
+
import { pullRequestWebUrl } from "../coding/code-host.js";
|
|
37
38
|
import { tryAcquireLease } from "./lease.js";
|
|
38
39
|
import { logger } from "./logger.js";
|
|
39
40
|
import { broadcasts, nextStatus, renderEvent, renderParent, senderName, } from "./notification-templates.js";
|
|
@@ -82,7 +83,7 @@ async function subjectOf(db, d) {
|
|
|
82
83
|
return { label: ev.workItemKey, title: item?.title ?? null, url: item?.url ?? null };
|
|
83
84
|
}
|
|
84
85
|
if (ev.repository && ev.prNumber != null) {
|
|
85
|
-
const url = ev.codeProvider
|
|
86
|
+
const url = pullRequestWebUrl(ev.codeProvider ?? "", ev.repository, ev.prNumber);
|
|
86
87
|
return { label: `${ev.repository}#${ev.prNumber}`, title: null, url };
|
|
87
88
|
}
|
|
88
89
|
if (ev.runId)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Low-power mode's idle rules: whether the coding proxy or native gateway has
|
|
3
3
|
* had work since `since`. A target is busy while any of its runs is pending
|
|
4
4
|
* (queued included) or running, or finished at or after `since`; the gateway
|
|
5
|
-
* is also busy while any gateway session is active.
|
|
5
|
+
* is also busy while any gateway session is active. The gateway's runs are
|
|
6
|
+
* sandboxed native runs plus coding runs with MCP tools: a coding run's MCP
|
|
7
|
+
* calls go coding proxy -> gateway, so it needs both. powerBlockers lists the
|
|
6
8
|
* run ids that keep a target busy right now (recent finishes do not count).
|
|
7
9
|
*/
|
|
8
10
|
import type { PrismaClient } from "#prisma";
|
|
@@ -10,5 +12,9 @@ import type { PowerTarget } from "./power.js";
|
|
|
10
12
|
export declare function proxyBusy(db: PrismaClient, since: Date): Promise<boolean>;
|
|
11
13
|
export declare function gatewayBusy(db: PrismaClient, since: Date): Promise<boolean>;
|
|
12
14
|
export declare function powerBusy(db: PrismaClient): (target: PowerTarget, since: Date) => Promise<boolean>;
|
|
13
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Run ids keeping `target` busy right now: active or queued runs (plus, for the gateway, active
|
|
17
|
+
* gateway sessions and active or queued coding runs with MCP tools). "Active" for a coding run is
|
|
18
|
+
* the proxy's rule: its run is pending or running.
|
|
19
|
+
*/
|
|
14
20
|
export declare function powerBlockers(db: PrismaClient, target: PowerTarget): Promise<string[]>;
|
package/dist/core/power-idle.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const ACTIVE = { in: ["pending", "running"] };
|
|
2
|
+
/** A coding run whose proxy session holds an MCP token: it has MCP tools, served through the gateway. */
|
|
3
|
+
const WITH_MCP_TOOLS = { proxySession: { is: { mcpTokenHash: { not: null } } } };
|
|
2
4
|
export async function proxyBusy(db, since) {
|
|
3
5
|
const n = await db.codingRun.count({
|
|
4
6
|
where: { run: { OR: [{ status: ACTIVE }, { finishedAt: { gte: since } }] } },
|
|
@@ -11,20 +13,32 @@ export async function gatewayBusy(db, since) {
|
|
|
11
13
|
});
|
|
12
14
|
if (runs > 0)
|
|
13
15
|
return true;
|
|
16
|
+
const mcpCodingRuns = await db.codingRun.count({
|
|
17
|
+
where: { ...WITH_MCP_TOOLS, run: { OR: [{ status: ACTIVE }, { finishedAt: { gte: since } }] } },
|
|
18
|
+
});
|
|
19
|
+
if (mcpCodingRuns > 0)
|
|
20
|
+
return true;
|
|
14
21
|
return (await db.nativeGatewaySession.count({ where: { status: "active" } })) > 0;
|
|
15
22
|
}
|
|
16
23
|
export function powerBusy(db) {
|
|
17
24
|
return (target, since) => (target === "proxy" ? proxyBusy(db, since) : gatewayBusy(db, since));
|
|
18
25
|
}
|
|
19
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Run ids keeping `target` busy right now: active or queued runs (plus, for the gateway, active
|
|
28
|
+
* gateway sessions and active or queued coding runs with MCP tools). "Active" for a coding run is
|
|
29
|
+
* the proxy's rule: its run is pending or running.
|
|
30
|
+
*/
|
|
20
31
|
export async function powerBlockers(db, target) {
|
|
21
32
|
if (target === "proxy") {
|
|
22
33
|
const rows = await db.codingRun.findMany({ where: { run: { status: ACTIVE } }, select: { runId: true } });
|
|
23
34
|
return [...new Set(rows.map((r) => r.runId))].sort();
|
|
24
35
|
}
|
|
25
|
-
const [runs, sessions] = await Promise.all([
|
|
36
|
+
const [runs, sessions, mcpCodingRuns] = await Promise.all([
|
|
26
37
|
db.run.findMany({ where: { nativeExecutionMode: "sandbox", status: ACTIVE }, select: { id: true } }),
|
|
27
38
|
db.nativeGatewaySession.findMany({ where: { status: "active" }, select: { runId: true } }),
|
|
39
|
+
db.codingRun.findMany({ where: { ...WITH_MCP_TOOLS, run: { status: ACTIVE } }, select: { runId: true } }),
|
|
28
40
|
]);
|
|
29
|
-
return [
|
|
41
|
+
return [
|
|
42
|
+
...new Set([...runs.map((r) => r.id), ...sessions.map((s) => s.runId), ...mcpCodingRuns.map((r) => r.runId)]),
|
|
43
|
+
].sort();
|
|
30
44
|
}
|
package/dist/core/power.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export type PowerTarget = "proxy" | "gateway";
|
|
|
4
4
|
export declare const LOW_POWER_WAKE_TIMEOUT = "low_power_wake_timeout";
|
|
5
5
|
export declare const LOW_POWER_SCALE_FORBIDDEN = "low_power_scale_forbidden";
|
|
6
6
|
export declare const LOW_POWER_DISABLED = "low_power_disabled";
|
|
7
|
+
/**
|
|
8
|
+
* A coding run with MCP tools could not wake the native gateway that serves them: the run fails
|
|
9
|
+
* before its worker launches (category mcp_gateway_unavailable).
|
|
10
|
+
*/
|
|
11
|
+
export declare const CODING_MCP_GATEWAY_UNAVAILABLE = "coding_mcp_gateway_unavailable";
|
|
7
12
|
/** ISO-8601 time the control plane last scaled the Deployment up from zero. */
|
|
8
13
|
export declare const POWER_WOKE_AT = "wardby.io/power-woke-at";
|
|
9
14
|
/**
|
package/dist/core/power.js
CHANGED
|
@@ -4,6 +4,11 @@ const powerLog = logger.child({ module: "power" });
|
|
|
4
4
|
export const LOW_POWER_WAKE_TIMEOUT = "low_power_wake_timeout";
|
|
5
5
|
export const LOW_POWER_SCALE_FORBIDDEN = "low_power_scale_forbidden";
|
|
6
6
|
export const LOW_POWER_DISABLED = "low_power_disabled";
|
|
7
|
+
/**
|
|
8
|
+
* A coding run with MCP tools could not wake the native gateway that serves them: the run fails
|
|
9
|
+
* before its worker launches (category mcp_gateway_unavailable).
|
|
10
|
+
*/
|
|
11
|
+
export const CODING_MCP_GATEWAY_UNAVAILABLE = "coding_mcp_gateway_unavailable";
|
|
7
12
|
/** ISO-8601 time the control plane last scaled the Deployment up from zero. */
|
|
8
13
|
export const POWER_WOKE_AT = "wardby.io/power-woke-at";
|
|
9
14
|
/**
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* brings every later follow-up tree that continued one of its pull requests.
|
|
24
24
|
*/
|
|
25
25
|
import { Prisma } from "#prisma";
|
|
26
|
-
import {
|
|
26
|
+
import { codeProviderOf, normalizeHostedRepository, pullRequestWebUrl } from "../coding/code-host.js";
|
|
27
27
|
import { issueReference, } from "../providers/issue-tracker/types.js";
|
|
28
28
|
import { renderRelatedSection } from "../providers/vcs/github.js";
|
|
29
29
|
import { pullRequestOutcome } from "./host-status.js";
|
|
@@ -91,7 +91,7 @@ export async function runTreeRoot(db, markerRunId, repository) {
|
|
|
91
91
|
}
|
|
92
92
|
function safeRepository(value) {
|
|
93
93
|
try {
|
|
94
|
-
return
|
|
94
|
+
return normalizeHostedRepository(value);
|
|
95
95
|
}
|
|
96
96
|
catch {
|
|
97
97
|
return undefined;
|
|
@@ -222,7 +222,8 @@ onlyIssueKey) {
|
|
|
222
222
|
if (issueFilter.size > 0) {
|
|
223
223
|
const recorded = await db.issuePullRequest.findMany({
|
|
224
224
|
where: {
|
|
225
|
-
|
|
225
|
+
// A local repository has no pull requests to record: everything here is on a code host.
|
|
226
|
+
codeProvider: { not: "local" },
|
|
226
227
|
OR: [...issueFilter.values()].map((i) => ({ issueProvider: i.provider, issueKey: i.key })),
|
|
227
228
|
},
|
|
228
229
|
select: { repository: true, number: true, createdAt: true, openedByRunId: true, state: true },
|
|
@@ -269,6 +270,7 @@ onlyIssueKey) {
|
|
|
269
270
|
...(roots.size > 0 ? { roots: [...roots].map(([id, root]) => ({ id, ...root })) } : {}),
|
|
270
271
|
};
|
|
271
272
|
}
|
|
273
|
+
const canEditRelatedSection = (host) => host?.replaceRelatedSection !== undefined && host.pullRequestOrigin !== undefined;
|
|
272
274
|
function issueFor(group, trackers) {
|
|
273
275
|
if (!group.issue)
|
|
274
276
|
return undefined;
|
|
@@ -283,8 +285,12 @@ function issueFor(group, trackers) {
|
|
|
283
285
|
* deployment recorded in that repository, are edited.
|
|
284
286
|
*/
|
|
285
287
|
export async function updateRelatedPullRequests(db, run, hosts, trackers) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
+
// A request's pull requests may span hosts: each is read and written through its own.
|
|
289
|
+
const editorFor = (repository) => {
|
|
290
|
+
const host = hosts?.[codeProviderOf(repository)];
|
|
291
|
+
return canEditRelatedSection(host) ? host : undefined;
|
|
292
|
+
};
|
|
293
|
+
if (!Object.values(hosts ?? {}).some(canEditRelatedSection))
|
|
288
294
|
return;
|
|
289
295
|
try {
|
|
290
296
|
// A run that delegated nothing has no request set of its own.
|
|
@@ -306,6 +312,10 @@ export async function updateRelatedPullRequests(db, run, hosts, trackers) {
|
|
|
306
312
|
continue;
|
|
307
313
|
}
|
|
308
314
|
try {
|
|
315
|
+
// Reading only needs the read: a host that can't edit the section still reports its pull requests.
|
|
316
|
+
const host = hosts?.[codeProviderOf(pr.repository)];
|
|
317
|
+
if (!host?.pullRequestOrigin)
|
|
318
|
+
throw new Error("no_host_for_repository");
|
|
309
319
|
const origin = await host.pullRequestOrigin(pr.repository, pr.number);
|
|
310
320
|
const state = origin.merged
|
|
311
321
|
? "merged"
|
|
@@ -358,6 +368,9 @@ export async function updateRelatedPullRequests(db, run, hosts, trackers) {
|
|
|
358
368
|
});
|
|
359
369
|
if (!block)
|
|
360
370
|
continue;
|
|
371
|
+
const host = editorFor(target.repository);
|
|
372
|
+
if (!host)
|
|
373
|
+
continue;
|
|
361
374
|
try {
|
|
362
375
|
const outcome = await host.replaceRelatedSection(target.repository, target.number, {
|
|
363
376
|
expectedMarkerRunId: target.markerRunId,
|
|
@@ -381,7 +394,7 @@ const SAFE_RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
|
381
394
|
export const SIBLING_GUIDANCE = "Continue the pull request you were asked about, and a listed open one only when the change requires it. " +
|
|
382
395
|
"Never open a new pull request in a repository that already has an open pull request listed here. Merged or " +
|
|
383
396
|
"closed pull requests are not listed: a change there needs a new pull request.";
|
|
384
|
-
const HINT_REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
397
|
+
const HINT_REPOSITORY = /^(?:[a-z][a-z0-9-]{1,30}:)?[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
385
398
|
/**
|
|
386
399
|
* The continuation hint for a ticket (a GitHub or Jira issue) that already has
|
|
387
400
|
* open wardby pull requests: one sentence per pull request (up to
|
|
@@ -396,9 +409,12 @@ export function openPullRequestHints(prs) {
|
|
|
396
409
|
Number.isSafeInteger(p.number) &&
|
|
397
410
|
p.number > 0)
|
|
398
411
|
.slice(0, MAX_SIBLING_HINTS)
|
|
399
|
-
.map((p) =>
|
|
400
|
-
|
|
401
|
-
`
|
|
412
|
+
.map((p) => {
|
|
413
|
+
const url = pullRequestWebUrl(codeProviderOf(p.repository), p.repository, p.number);
|
|
414
|
+
return (`This issue already has an open pull request wardby opened: ${p.repository}#${p.number}` +
|
|
415
|
+
`${url ? ` (${url})` : ""}. ` +
|
|
416
|
+
`To revise it, delegate with continuePriorRun set to exactly "${p.openedByRunId}".`);
|
|
417
|
+
});
|
|
402
418
|
return lines.length > 0 ? [...lines, SIBLING_GUIDANCE].join("\n\n") : "";
|
|
403
419
|
}
|
|
404
420
|
/**
|
|
@@ -436,7 +452,7 @@ export async function openSiblings(db, seedRunId, exclude) {
|
|
|
436
452
|
export async function openSiblingsForIssue(db, issue) {
|
|
437
453
|
try {
|
|
438
454
|
const newest = await db.issuePullRequest.findMany({
|
|
439
|
-
where: { issueProvider: issue.provider, issueKey: issue.key, codeProvider:
|
|
455
|
+
where: { issueProvider: issue.provider, issueKey: issue.key, codeProvider: { not: "local" } },
|
|
440
456
|
orderBy: { createdAt: "desc" },
|
|
441
457
|
take: 1,
|
|
442
458
|
select: { openedByRunId: true },
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a retention setting in whole days (1 to 3650) from the environment. An unset or blank value
|
|
3
|
+
* gives `defaultDays`; anything else out of range stops start-up, naming the variable.
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadRetentionDays(env: NodeJS.ProcessEnv, name: string, defaultDays: number): number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a retention setting in whole days (1 to 3650) from the environment. An unset or blank value
|
|
3
|
+
* gives `defaultDays`; anything else out of range stops start-up, naming the variable.
|
|
4
|
+
*/
|
|
5
|
+
export function loadRetentionDays(env, name, defaultDays) {
|
|
6
|
+
const raw = env[name]?.trim();
|
|
7
|
+
if (!raw)
|
|
8
|
+
return defaultDays;
|
|
9
|
+
const days = Number(raw);
|
|
10
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650) {
|
|
11
|
+
throw new Error(`${name} must be a whole number of days from 1 to 3650.`);
|
|
12
|
+
}
|
|
13
|
+
return days;
|
|
14
|
+
}
|