@wardby/cli 0.5.3 → 0.6.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 +33 -0
- package/README.md +25 -14
- package/dist/claude-coding-worker/context.d.ts +22 -0
- package/dist/claude-coding-worker/context.js +92 -0
- package/dist/claude-coding-worker/driver.d.ts +7 -1
- package/dist/claude-coding-worker/driver.js +38 -5
- package/dist/claude-coding-worker/sdk.js +13 -4
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -2
- package/dist/cli-native-mode.d.ts +9 -0
- package/dist/cli-native-mode.js +20 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +141 -5
- package/dist/coding/claude-context.d.ts +45 -0
- package/dist/coding/claude-context.js +434 -0
- package/dist/coding/profile.d.ts +12 -0
- package/dist/coding/profile.js +6 -0
- package/dist/coding/protocol.d.ts +104 -0
- package/dist/coding/protocol.js +87 -3
- package/dist/coding-worker/artifact.d.ts +8 -0
- package/dist/coding-worker/artifact.js +2 -2
- package/dist/coding-worker/driver.js +2 -0
- package/dist/coding-worker/sdk.d.ts +6 -0
- package/dist/coding-worker/sdk.js +1 -0
- package/dist/coding-worker/skills.d.ts +23 -0
- package/dist/coding-worker/skills.js +117 -0
- package/dist/coding-worker/types.d.ts +2 -0
- package/dist/config/github-allowed-accounts.d.ts +15 -0
- package/dist/config/github-allowed-accounts.js +36 -0
- package/dist/config/low-power.d.ts +12 -0
- package/dist/config/low-power.js +18 -0
- package/dist/config/mobile.d.ts +12 -0
- package/dist/config/mobile.js +18 -0
- package/dist/config/providers.d.ts +53 -0
- package/dist/config/providers.js +92 -6
- package/dist/core/db.d.ts +16 -6
- package/dist/core/db.js +12 -10
- package/dist/core/dispatch.d.ts +11 -0
- package/dist/core/dispatch.js +36 -1
- package/dist/core/engine-native.js +9 -5
- package/dist/core/host-events.d.ts +49 -3
- package/dist/core/host-events.js +289 -15
- package/dist/core/issue-bridge.js +42 -2
- package/dist/core/issue-events.js +12 -1
- package/dist/core/issue-status.js +14 -3
- package/dist/core/merge-order-check.d.ts +129 -0
- package/dist/core/merge-order-check.js +336 -0
- package/dist/core/notification-dispatcher.d.ts +70 -0
- package/dist/core/notification-dispatcher.js +368 -0
- package/dist/core/notification-templates.d.ts +25 -0
- package/dist/core/notification-templates.js +105 -0
- package/dist/core/notifications.d.ts +10 -0
- package/dist/core/notifications.js +38 -0
- package/dist/core/pg-auth.d.ts +22 -0
- package/dist/core/pg-auth.js +152 -0
- package/dist/core/power-composition.d.ts +16 -0
- package/dist/core/power-composition.js +37 -0
- package/dist/core/power-idle.d.ts +14 -0
- package/dist/core/power-idle.js +30 -0
- package/dist/core/power.d.ts +71 -0
- package/dist/core/power.js +202 -0
- package/dist/core/reconciler.d.ts +4 -2
- package/dist/core/reconciler.js +29 -5
- package/dist/core/related-pull-requests.d.ts +48 -4
- package/dist/core/related-pull-requests.js +113 -11
- package/dist/core/repo-access.d.ts +15 -0
- package/dist/core/repo-access.js +66 -0
- package/dist/core/review-fix.d.ts +37 -0
- package/dist/core/review-fix.js +207 -6
- package/dist/core/review-host-tools.d.ts +20 -0
- package/dist/core/review-host-tools.js +58 -2
- package/dist/core/runner.d.ts +336 -3
- package/dist/core/runner.js +894 -317
- package/dist/core/secret-broker-config.d.ts +103 -0
- package/dist/core/secret-broker-config.js +112 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +148 -40
- package/dist/core/workflow-event-recorder.d.ts +14 -0
- package/dist/core/workflow-event-recorder.js +120 -0
- package/dist/core/workflow-events.d.ts +82 -0
- package/dist/core/workflow-events.js +44 -0
- package/dist/core/workflow-run-events.d.ts +16 -0
- package/dist/core/workflow-run-events.js +40 -0
- package/dist/generated/prisma/browser.d.ts +58 -0
- package/dist/generated/prisma/client.d.ts +58 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +150 -0
- package/dist/generated/prisma/enums.d.ts +27 -0
- package/dist/generated/prisma/enums.js +23 -0
- package/dist/generated/prisma/internal/class.d.ts +88 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +772 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +135 -4
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +139 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -4
- package/dist/generated/prisma/models/Agent.d.ts +360 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +73 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +205 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1675 -0
- package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1517 -0
- package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
- package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
- package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
- package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
- package/dist/generated/prisma/models/NotificationChannel.js +1 -0
- package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
- package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
- package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
- package/dist/generated/prisma/models/NotificationThread.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +826 -1
- package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
- package/dist/generated/prisma/models/Secret.d.ts +37 -1
- package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
- package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
- package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
- package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
- package/dist/generated/prisma/models.d.ts +8 -0
- package/dist/help/build.js +4 -2
- package/dist/help/catalog.d.ts +1 -1
- package/dist/help/catalog.js +31 -6
- package/dist/help-index.json +1363 -176
- package/dist/import/create.js +17 -0
- package/dist/import/neutral-schema.d.ts +6 -2
- package/dist/import/neutral-schema.js +2 -0
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/auth/repo-authorization.js +14 -0
- package/dist/mcp/auth/resource-server.d.ts +2 -1
- package/dist/mcp/auth/resource-server.js +7 -2
- package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
- package/dist/mcp/auth/self-hosted/browser.js +19 -6
- package/dist/mcp/context.d.ts +6 -0
- package/dist/mcp/host-events/github-ingress.d.ts +23 -1
- package/dist/mcp/host-events/github-ingress.js +68 -0
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +59 -10
- package/dist/mcp/tools/agents.js +82 -11
- package/dist/mcp/tools/notification-channels.d.ts +2 -0
- package/dist/mcp/tools/notification-channels.js +293 -0
- package/dist/mcp/tools/power.d.ts +2 -0
- package/dist/mcp/tools/power.js +77 -0
- package/dist/mcp/tools/runs.d.ts +0 -6
- package/dist/mcp/tools/runs.js +7 -2
- package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
- package/dist/mcp/tools/secret-broker-compat.js +23 -0
- package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
- package/dist/mcp/tools/secret-elicitation-form.js +200 -10
- package/dist/mcp/tools/secret-elicitation-server.js +8 -1
- package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
- package/dist/mcp/tools/secret-elicitation.js +110 -12
- package/dist/mcp/tools/secrets.js +184 -7
- package/dist/mcp/tools/tools.d.ts +1 -1
- package/dist/mcp/tools/tools.js +22 -2
- package/dist/native-worker/composition.d.ts +23 -0
- package/dist/native-worker/composition.js +77 -0
- package/dist/native-worker/docker-isolation.d.ts +65 -0
- package/dist/native-worker/docker-isolation.js +183 -0
- package/dist/native-worker/docker-launcher.d.ts +74 -0
- package/dist/native-worker/docker-launcher.js +237 -0
- package/dist/native-worker/gateway-server.d.ts +39 -0
- package/dist/native-worker/gateway-server.js +315 -0
- package/dist/native-worker/gateway.d.ts +44 -0
- package/dist/native-worker/gateway.js +197 -0
- package/dist/native-worker/http-server.d.ts +22 -0
- package/dist/native-worker/http-server.js +109 -0
- package/dist/native-worker/http-transport.d.ts +17 -0
- package/dist/native-worker/http-transport.js +88 -0
- package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
- package/dist/native-worker/kubernetes-isolation.js +218 -0
- package/dist/native-worker/kubernetes-launcher.d.ts +102 -0
- package/dist/native-worker/kubernetes-launcher.js +335 -0
- package/dist/native-worker/launch.d.ts +45 -0
- package/dist/native-worker/launch.js +44 -0
- package/dist/native-worker/ledger.d.ts +109 -0
- package/dist/native-worker/ledger.js +192 -0
- package/dist/native-worker/loopback.d.ts +12 -0
- package/dist/native-worker/loopback.js +49 -0
- package/dist/native-worker/main.d.ts +7 -0
- package/dist/native-worker/main.js +74 -0
- package/dist/native-worker/protocol.d.ts +744 -0
- package/dist/native-worker/protocol.js +200 -0
- package/dist/native-worker/sandbox-executor.d.ts +68 -0
- package/dist/native-worker/sandbox-executor.js +210 -0
- package/dist/native-worker/stdio.d.ts +29 -0
- package/dist/native-worker/stdio.js +188 -0
- package/dist/native-worker/warm-delivery.d.ts +16 -0
- package/dist/native-worker/warm-delivery.js +33 -0
- package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
- package/dist/native-worker/warm-pool-ledger.js +73 -0
- package/dist/native-worker/warm-pool.d.ts +107 -0
- package/dist/native-worker/warm-pool.js +283 -0
- package/dist/native-worker/worker.d.ts +17 -0
- package/dist/native-worker/worker.js +115 -0
- package/dist/providers/auth/authorization-server.d.ts +3 -0
- package/dist/providers/auth/index.js +2 -0
- package/dist/providers/auth/self-hosted.d.ts +13 -1
- package/dist/providers/auth/self-hosted.js +54 -4
- package/dist/providers/chat/fake.d.ts +46 -0
- package/dist/providers/chat/fake.js +39 -0
- package/dist/providers/chat/index.d.ts +4 -0
- package/dist/providers/chat/index.js +8 -0
- package/dist/providers/chat/slack.d.ts +37 -0
- package/dist/providers/chat/slack.js +105 -0
- package/dist/providers/chat/types.d.ts +42 -0
- package/dist/providers/chat/types.js +19 -0
- package/dist/providers/engine/types.d.ts +19 -0
- package/dist/providers/engine/types.js +22 -0
- package/dist/providers/executor/composition.d.ts +24 -1
- package/dist/providers/executor/composition.js +39 -6
- package/dist/providers/executor/container.d.ts +24 -1
- package/dist/providers/executor/container.js +101 -11
- package/dist/providers/executor/dbos.d.ts +42 -0
- package/dist/providers/executor/dbos.js +118 -12
- package/dist/providers/executor/deferred.d.ts +56 -0
- package/dist/providers/executor/deferred.js +119 -0
- package/dist/providers/executor/routing.d.ts +37 -10
- package/dist/providers/executor/routing.js +73 -32
- package/dist/providers/executor/types.d.ts +12 -0
- package/dist/providers/jobs/docker.js +3 -3
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +21 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +42 -1
- package/dist/providers/jobs/kubernetes-api.d.ts +17 -1
- package/dist/providers/jobs/kubernetes-api.js +7 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
- package/dist/providers/jobs/kubernetes-client.js +52 -3
- package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
- package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
- package/dist/providers/jobs/kubernetes-isolation.d.ts +3 -1
- package/dist/providers/jobs/kubernetes-isolation.js +6 -0
- package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
- package/dist/providers/jobs/kubernetes-platform.js +38 -1
- package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
- package/dist/providers/jobs/kubernetes-preflight.js +4 -2
- package/dist/providers/jobs/kubernetes.d.ts +36 -1
- package/dist/providers/jobs/kubernetes.js +74 -4
- package/dist/providers/jobs/metadata-probes.d.ts +11 -0
- package/dist/providers/jobs/metadata-probes.js +13 -0
- package/dist/providers/jobs/types.d.ts +9 -0
- package/dist/providers/review-host/github.d.ts +11 -1
- package/dist/providers/review-host/github.js +50 -0
- package/dist/providers/review-host/types.d.ts +23 -0
- package/dist/providers/vcs/github.d.ts +32 -6
- package/dist/providers/vcs/github.js +74 -16
- package/dist/quickstart/coding-seed.d.ts +10 -3
- package/dist/quickstart/coding-seed.js +74 -3
- package/dist/quickstart/images.d.ts +2 -0
- package/dist/quickstart/images.js +7 -1
- package/dist/quickstart/reviewer-prompt.d.ts +1 -1
- package/dist/quickstart/reviewer-prompt.js +1 -1
- package/dist/quickstart-images.json +1 -1
- package/dist/sandbox/brokered-fetch.d.ts +23 -0
- package/dist/sandbox/brokered-fetch.js +37 -0
- package/dist/sandbox/host-functions.d.ts +34 -0
- package/dist/sandbox/host-functions.js +144 -80
- package/dist/sandbox/prelude.js +7 -1
- package/dist/sandbox/run-in-sandbox.d.ts +19 -0
- package/dist/sandbox/run-in-sandbox.js +28 -10
- package/dist/sandbox/safe-fetch.d.ts +2 -0
- package/dist/sandbox/safe-fetch.js +3 -1
- package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
- package/dist/sandbox/secret-broker-sigv4.js +49 -0
- package/dist/sandbox/secret-broker.d.ts +34 -0
- package/dist/sandbox/secret-broker.js +162 -0
- package/dist/sandbox/user-tool.d.ts +14 -0
- package/dist/sandbox/user-tool.js +32 -0
- package/dist/serve.js +18 -0
- package/dist/viewer/api-schema.d.ts +210 -14
- package/dist/viewer/api-schema.js +22 -0
- package/dist/viewer/build-schemas.js +4 -1
- package/dist/viewer/event-bus.d.ts +6 -0
- package/dist/viewer/event-bus.js +16 -7
- package/dist/viewer/graph.d.ts +5 -2
- package/dist/viewer/graph.js +7 -2
- package/dist/viewer/http.d.ts +0 -1
- package/dist/viewer/http.js +2 -2
- package/dist/viewer/infra.js +25 -2
- package/dist/viewer/limits.d.ts +4 -0
- package/dist/viewer/limits.js +4 -0
- package/dist/viewer/openapi.d.ts +15 -0
- package/dist/viewer/openapi.js +172 -0
- package/docs/README.md +7 -0
- package/docs/agent-recipes.md +57 -8
- package/docs/architecture-runtime.md +16 -1
- package/docs/code-review-agents.md +139 -5
- package/docs/coding-agent-setup.md +113 -5
- package/docs/coding-packages.md +4 -2
- package/docs/coding-services.md +3 -1
- package/docs/coding-worker-byo-images.md +198 -6
- package/docs/coding-worker-isolation.md +68 -14
- package/docs/getting-started-gke.md +225 -9
- package/docs/getting-started-identity-provider.md +21 -10
- package/docs/getting-started.md +5 -4
- package/docs/jira-agents.md +5 -1
- package/docs/native-sandbox.md +635 -0
- package/docs/release-verification.md +9 -0
- package/docs/security-deployment.md +113 -12
- package/docs/slack-notifications.md +252 -0
- package/docs/tool-secrets.md +252 -0
- package/docs/viewer-api.md +46 -5
- package/docs/viewer-api.openapi.yml +1404 -0
- package/help/admin-viewer.md +6 -2
- package/help/agent-recipes.md +27 -0
- package/help/architecture-agent.md +1 -1
- package/help/brokered-secrets.md +79 -0
- package/help/build-claude-code-image.md +131 -0
- package/help/build-worker-image.md +3 -6
- package/help/code-review-agents.md +68 -6
- package/help/deploy-gke.md +8 -1
- package/help/deployment-targets.md +25 -14
- package/help/errors/coding-provider-not-configured.md +2 -2
- package/help/errors/database-auth-invalid.md +28 -0
- package/help/errors/github-account-not-allowed.md +25 -0
- package/help/errors/low-power-disabled.md +29 -0
- package/help/errors/low-power-scale-forbidden.md +35 -0
- package/help/errors/low-power-wake-timeout.md +30 -0
- package/help/errors/mobile-disabled.md +31 -0
- package/help/errors/native-sandbox-capacity.md +22 -0
- package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
- package/help/errors/native-sandbox-docker-failed.md +20 -0
- package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
- package/help/errors/native-sandbox-image-not-pinned.md +19 -0
- package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
- package/help/errors/native-sandbox-network-unenforced.md +22 -0
- package/help/errors/native-sandbox-requires-catalog.md +31 -0
- package/help/errors/native-sandbox-unavailable.md +19 -0
- package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
- package/help/errors/native-sandbox-worker-exited.md +20 -0
- package/help/errors/native-sandbox-worker-lost.md +20 -0
- package/help/errors/native-sandbox-worker-unready.md +22 -0
- package/help/errors/secret-broker-body-invalid.md +26 -0
- package/help/errors/secret-broker-changed.md +20 -0
- package/help/errors/secret-broker-config-invalid.md +30 -0
- package/help/errors/secret-broker-conflict.md +27 -0
- package/help/errors/secret-broker-destination-denied.md +28 -0
- package/help/errors/secret-broker-value-invalid.md +28 -0
- package/help/errors/secret-brokered.md +24 -0
- package/help/errors/secret-not-brokered.md +25 -0
- package/help/errors/slack-auth-failed.md +48 -0
- package/help/errors/slack-channel-unreachable.md +39 -0
- package/help/errors/slack-not-configured.md +33 -0
- package/help/getting-started.md +4 -5
- package/help/github.md +18 -1
- package/help/identity-and-access.md +5 -1
- package/help/local-repositories.md +2 -3
- package/help/low-power-mode.md +125 -0
- package/help/merge-order-check.md +117 -0
- package/help/mobile-app.md +54 -0
- package/help/native-sandbox.md +136 -0
- package/help/observability.md +8 -0
- package/help/related-pull-requests.md +70 -9
- package/help/repo-instructions-and-skills.md +91 -0
- package/help/review-fix-rounds.md +10 -0
- package/help/slack-notifications.md +62 -0
- package/help/troubleshooting/coding-workers.md +21 -0
- package/package.json +11 -4
- package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
- package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
- package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
- package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
- package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
- package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
- package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
- package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
- package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
- package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
- package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
- package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
- package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
- package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
- package/prisma/schema.prisma +362 -91
package/dist/mcp/tools/agents.js
CHANGED
|
@@ -27,9 +27,12 @@ import { authorizeRepositoryForSet, canonicalLocalRepository, } from "../auth/re
|
|
|
27
27
|
import { McpError } from "../errors.js";
|
|
28
28
|
import { textResult } from "./text-result.js";
|
|
29
29
|
// workerImageRef is the BYO-arbitrary-image escape hatch (see
|
|
30
|
-
// resolveCodingWorkerImage in container.ts)
|
|
31
|
-
//
|
|
32
|
-
//
|
|
30
|
+
// resolveCodingWorkerImage/resolveCodingToolImage in container.ts). For a Codex agent it
|
|
31
|
+
// replaces the one container that runs the whole agent; for a Claude Code agent it replaces
|
|
32
|
+
// only the tool-runner container that runs the agent's commands, never the agent container
|
|
33
|
+
// (which always stays the deployment's standard Claude Code image). Either way, setting or
|
|
34
|
+
// changing it requires a step-up beyond agents:write, the same pattern make_owner below uses
|
|
35
|
+
// for its own sensitive, ownership-bypassing mutation.
|
|
33
36
|
function requireWorkerImageRefScope(ctx) {
|
|
34
37
|
requireScope(ctx, ctx.canonicalUri, "agents:admin");
|
|
35
38
|
}
|
|
@@ -54,6 +57,16 @@ const MAX_DELEGATIONS_PER_RUN = 20;
|
|
|
54
57
|
const PARALLEL_DELEGATIONS_DESCRIPTION = "Native agents with sub-agents: when true, the delegate_to_* calls the agent makes in one model turn run at the same time " +
|
|
55
58
|
"instead of one after another (other tool calls still run in order). The run tree still shares one budget, so later " +
|
|
56
59
|
"sub-agents get what earlier ones leave. Coding sub-agents still queue for CODING_MAX_CONCURRENT slots. Default false.";
|
|
60
|
+
/** Operator-facing spellings of Agent.nativeExecutionMode (the Prisma enum is `control_plane` | `sandbox`). */
|
|
61
|
+
const NATIVE_EXECUTION_MODES = ["control-plane", "sandbox"];
|
|
62
|
+
const toStoredMode = (mode) => mode === "sandbox" ? "sandbox" : "control_plane";
|
|
63
|
+
/** An agent row as returned to operators: the mode in its hyphenated spelling, null for coding agents. */
|
|
64
|
+
function withOperatorMode(agent) {
|
|
65
|
+
return {
|
|
66
|
+
...agent,
|
|
67
|
+
nativeExecutionMode: agent.kind !== "native" ? null : agent.nativeExecutionMode === "sandbox" ? "sandbox" : "control-plane",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
57
70
|
const agentFields = {
|
|
58
71
|
name: z.string().trim().min(1).max(MAX_AGENT_NAME_CHARS),
|
|
59
72
|
systemPrompt: z.string().min(1).max(MAX_SYSTEM_PROMPT_CHARS),
|
|
@@ -69,6 +82,7 @@ const agentFields = {
|
|
|
69
82
|
budgetGroupId: z.string().min(1).max(128),
|
|
70
83
|
memoryEnabled: z.boolean(),
|
|
71
84
|
effort: z.enum(LLM_EFFORT_LEVELS),
|
|
85
|
+
nativeExecutionMode: z.enum(NATIVE_EXECUTION_MODES),
|
|
72
86
|
defectProjectKey: z.string().regex(PROJECT_KEY, "must be an upper-case Jira project key"),
|
|
73
87
|
defectIssueType: z.string().trim().min(1).max(100),
|
|
74
88
|
};
|
|
@@ -102,6 +116,7 @@ const CreateAgentSchema = z
|
|
|
102
116
|
budgetGroupId: agentFields.budgetGroupId.optional(),
|
|
103
117
|
memoryEnabled: agentFields.memoryEnabled.default(false),
|
|
104
118
|
effort: agentFields.effort.optional(),
|
|
119
|
+
nativeExecutionMode: agentFields.nativeExecutionMode.optional(),
|
|
105
120
|
defectProjectKey: agentFields.defectProjectKey.optional(),
|
|
106
121
|
defectIssueType: agentFields.defectIssueType.optional(),
|
|
107
122
|
codingProfile: CodingProfileSchema.optional(),
|
|
@@ -158,6 +173,7 @@ const UpdateAgentSchema = z
|
|
|
158
173
|
budgetGroupId: agentFields.budgetGroupId.nullable().optional(),
|
|
159
174
|
memoryEnabled: agentFields.memoryEnabled.optional(),
|
|
160
175
|
effort: agentFields.effort.nullable().optional(),
|
|
176
|
+
nativeExecutionMode: agentFields.nativeExecutionMode.optional(),
|
|
161
177
|
defectProjectKey: agentFields.defectProjectKey.nullable().optional(),
|
|
162
178
|
defectIssueType: agentFields.defectIssueType.nullable().optional(),
|
|
163
179
|
codingProfile: CodingProfilePatchSchema.extend({
|
|
@@ -210,7 +226,10 @@ const profileJsonSchema = {
|
|
|
210
226
|
collectExclude: { type: "array", maxItems: 64, items: { type: "string" } },
|
|
211
227
|
toolchain: { type: "string", enum: ["node", "node-python"] },
|
|
212
228
|
toolchainVersion: { type: ["string", "null"] },
|
|
213
|
-
workerImageRef: {
|
|
229
|
+
workerImageRef: {
|
|
230
|
+
type: ["string", "null"],
|
|
231
|
+
description: "A digest-pinned custom image that this agent's commands run in, for your own languages and tools. Codex: build it FROM the coding-worker driver image; it runs the whole agent. Claude Code: build it FROM this release's wardby-claude-tool-runner image; it runs the agent's commands, while Claude Code itself stays on the standard image. Needs agents:admin. Null uses the toolchain's standard image.",
|
|
232
|
+
},
|
|
214
233
|
workspaceDiskMb: {
|
|
215
234
|
type: ["integer", "null"],
|
|
216
235
|
minimum: 64,
|
|
@@ -223,6 +242,14 @@ const profileJsonSchema = {
|
|
|
223
242
|
maximum: MAX_CODING_TURNS,
|
|
224
243
|
description: `Claude Code runs only: the most agent turns (model calls) a run may take before it stops with coding_turn_limit. Null uses the worker default (${DEFAULT_CLAUDE_MAX_TURNS}). The run's budget and timeout still apply. Codex runs have no turn limit.`,
|
|
225
244
|
},
|
|
245
|
+
repoSkills: {
|
|
246
|
+
type: "boolean",
|
|
247
|
+
description: "Load the repository's agent skills into runs (Codex: .agents/skills and .codex/skills; Claude Code: .claude/skills). Default true. Repository instructions (AGENTS.md for Codex, CLAUDE.md for Claude Code) load either way.",
|
|
248
|
+
},
|
|
249
|
+
claudeBareMode: {
|
|
250
|
+
type: "boolean",
|
|
251
|
+
description: "Claude Code runs only. Default true: Claude Code runs in bare mode and wardby adds the repository's CLAUDE.md and its skill list to the system prompt; the model reads a skill's SKILL.md with run_command. False: bare mode is off and Claude Code loads CLAUDE.md and skills itself (native Skill tool). Repository settings, hooks, and MCP config are never loaded either way.",
|
|
252
|
+
},
|
|
226
253
|
packageAllowlist: {
|
|
227
254
|
type: "object",
|
|
228
255
|
description: "Approved top-level packages per ecosystem (npm, pypi). A PyPI entry may name extras, e.g. psycopg[binary]. Needs packages:approve or agents:admin.",
|
|
@@ -285,6 +312,23 @@ function validateEffort(kind, model, effort) {
|
|
|
285
312
|
: "It accepts no effort setting; leave effort unset (or null)."));
|
|
286
313
|
}
|
|
287
314
|
}
|
|
315
|
+
const NATIVE_SANDBOX_MODE_DESCRIPTION = "Where a native agent's runs execute: control-plane (default, in the server process) or sandbox (an isolated container; needs the server's native sandbox configured).";
|
|
316
|
+
/**
|
|
317
|
+
* The mode only applies to native agents, and `sandbox` only works where the
|
|
318
|
+
* server composed a native sandbox executor; refuse it otherwise rather than
|
|
319
|
+
* store a mode whose every run would fail closed.
|
|
320
|
+
*/
|
|
321
|
+
function validateNativeExecutionMode(ctx, kind, mode) {
|
|
322
|
+
if (mode == null)
|
|
323
|
+
return;
|
|
324
|
+
if (kind !== "native") {
|
|
325
|
+
throw new McpError(400, "nativeExecutionMode is only valid for native agents; coding agents do not use it.");
|
|
326
|
+
}
|
|
327
|
+
if (mode === "sandbox" && ctx.providers.executor.supportsNativeSandbox?.() !== true) {
|
|
328
|
+
throw new McpError(400, "native_sandbox_unavailable: this server has no native sandbox configured (set NATIVE_SANDBOX_LAUNCHER), " +
|
|
329
|
+
'so an agent cannot use nativeExecutionMode "sandbox".');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
288
332
|
/**
|
|
289
333
|
* A model being set (create, or update with `model`) must be runnable here:
|
|
290
334
|
* in the catalog and enabled; for a native agent, its provider must also be
|
|
@@ -322,6 +366,8 @@ function storedProfile(profile) {
|
|
|
322
366
|
packageAllowlist: profile.packageAllowlist,
|
|
323
367
|
packagePolicy: profile.packagePolicy,
|
|
324
368
|
services: profile.services,
|
|
369
|
+
repoSkills: profile.repoSkills,
|
|
370
|
+
claudeBareMode: profile.claudeBareMode,
|
|
325
371
|
});
|
|
326
372
|
}
|
|
327
373
|
/** A service name an agent is allowed must exist in the catalog (any version); typos fail here, not at a run. */
|
|
@@ -366,6 +412,11 @@ export function registerAgentTools(mcp) {
|
|
|
366
412
|
enum: [...LLM_EFFORT_LEVELS],
|
|
367
413
|
description: "Reasoning effort for native agents. Unset uses the provider default.",
|
|
368
414
|
},
|
|
415
|
+
nativeExecutionMode: {
|
|
416
|
+
type: "string",
|
|
417
|
+
enum: [...NATIVE_EXECUTION_MODES],
|
|
418
|
+
description: NATIVE_SANDBOX_MODE_DESCRIPTION,
|
|
419
|
+
},
|
|
369
420
|
defectProjectKey: { type: "string", pattern: PROJECT_KEY.source, description: DEFECT_DESCRIPTION },
|
|
370
421
|
defectIssueType: { type: "string", minLength: 1, maxLength: 100, description: DEFECT_DESCRIPTION },
|
|
371
422
|
codingProfile: { ...profileJsonSchema, required: ["repository"] },
|
|
@@ -385,12 +436,17 @@ export function registerAgentTools(mcp) {
|
|
|
385
436
|
validateSchedule(args.schedule, args.timezone ?? "UTC");
|
|
386
437
|
requireModelAvailable(ctx, args.model, args.kind);
|
|
387
438
|
validateEffort(args.kind, args.model, args.effort);
|
|
439
|
+
validateNativeExecutionMode(ctx, args.kind, args.nativeExecutionMode);
|
|
388
440
|
if (args.codingProfile)
|
|
389
441
|
await requireCatalogServiceNames(ctx.db, args.codingProfile.services);
|
|
390
442
|
if (args.budgetGroupId) {
|
|
391
443
|
await requireReadableBudgetGroup(ctx.db, args.budgetGroupId, ctx.principal.id);
|
|
392
444
|
}
|
|
393
|
-
const { codingProfile, repositoryAdminOverride, ...
|
|
445
|
+
const { codingProfile, repositoryAdminOverride, nativeExecutionMode, ...agentFieldsData } = args;
|
|
446
|
+
const agentData = {
|
|
447
|
+
...agentFieldsData,
|
|
448
|
+
...(nativeExecutionMode ? { nativeExecutionMode: toStoredMode(nativeExecutionMode) } : {}),
|
|
449
|
+
};
|
|
394
450
|
if (repositoryAdminOverride === true && !codingProfile) {
|
|
395
451
|
throw new McpError(400, "repositoryAdminOverride only applies with a codingProfile repository.");
|
|
396
452
|
}
|
|
@@ -414,7 +470,7 @@ export function registerAgentTools(mcp) {
|
|
|
414
470
|
},
|
|
415
471
|
include: { codingProfile: true },
|
|
416
472
|
});
|
|
417
|
-
return textResult(agent);
|
|
473
|
+
return textResult(withOperatorMode(agent));
|
|
418
474
|
},
|
|
419
475
|
});
|
|
420
476
|
mcp.registerTool({
|
|
@@ -449,6 +505,11 @@ export function registerAgentTools(mcp) {
|
|
|
449
505
|
enum: [...LLM_EFFORT_LEVELS, null],
|
|
450
506
|
description: "Reasoning effort for native agents. Null clears it back to the provider default.",
|
|
451
507
|
},
|
|
508
|
+
nativeExecutionMode: {
|
|
509
|
+
type: "string",
|
|
510
|
+
enum: [...NATIVE_EXECUTION_MODES],
|
|
511
|
+
description: NATIVE_SANDBOX_MODE_DESCRIPTION,
|
|
512
|
+
},
|
|
452
513
|
defectProjectKey: {
|
|
453
514
|
type: ["string", "null"],
|
|
454
515
|
pattern: PROJECT_KEY.source,
|
|
@@ -585,6 +646,12 @@ export function registerAgentTools(mcp) {
|
|
|
585
646
|
if (args.model !== undefined || args.effort !== undefined || nextKind !== existing.kind) {
|
|
586
647
|
validateEffort(nextKind, args.model ?? existing.model, args.effort !== undefined ? args.effort : existing.effort);
|
|
587
648
|
}
|
|
649
|
+
if (args.nativeExecutionMode !== undefined) {
|
|
650
|
+
validateNativeExecutionMode(ctx, nextKind, args.nativeExecutionMode);
|
|
651
|
+
}
|
|
652
|
+
else if (nextKind === "coding" && existing.nativeExecutionMode === "sandbox") {
|
|
653
|
+
throw new McpError(400, 'nativeExecutionMode is only valid for native agents; set nativeExecutionMode to "control-plane" before changing this agent to coding.');
|
|
654
|
+
}
|
|
588
655
|
let nextProfile = null;
|
|
589
656
|
if (nextKind === "coding") {
|
|
590
657
|
const attachedTools = await tx.agentTool.count({ where: { agentId: args.id } });
|
|
@@ -614,7 +681,11 @@ export function registerAgentTools(mcp) {
|
|
|
614
681
|
if (nextKind === "coding" && nextSchedule && nextScheduleEnabled && !nextProfile?.defaultTask) {
|
|
615
682
|
throw new McpError(400, "A default task is required for an enabled coding-agent schedule.");
|
|
616
683
|
}
|
|
617
|
-
const { id, codingProfile: _profilePatch, repositoryAdminOverride: _override, ...
|
|
684
|
+
const { id, codingProfile: _profilePatch, repositoryAdminOverride: _override, nativeExecutionMode: modeArg, ...fieldUpdates } = args;
|
|
685
|
+
const updates = {
|
|
686
|
+
...fieldUpdates,
|
|
687
|
+
...(modeArg ? { nativeExecutionMode: toStoredMode(modeArg) } : {}),
|
|
688
|
+
};
|
|
618
689
|
const stamp = authorization ? profileStamp(authorization) : {};
|
|
619
690
|
const trace = debugTraceUntil === undefined ? {} : { debugTraceUntil };
|
|
620
691
|
const profileMutation = nextKind === "native"
|
|
@@ -641,7 +712,7 @@ export function registerAgentTools(mcp) {
|
|
|
641
712
|
by: ctx.principal.id,
|
|
642
713
|
}, "coding debug trace set");
|
|
643
714
|
}
|
|
644
|
-
return textResult(agent);
|
|
715
|
+
return textResult(withOperatorMode(agent));
|
|
645
716
|
},
|
|
646
717
|
});
|
|
647
718
|
mcp.registerTool({
|
|
@@ -651,7 +722,7 @@ export function registerAgentTools(mcp) {
|
|
|
651
722
|
handler: async (_args, ctx) => {
|
|
652
723
|
const [where, accessOf] = await Promise.all([readableAgentsWhere(ctx), agentAccessResolver(ctx)]);
|
|
653
724
|
const agents = await ctx.db.agent.findMany({ where, include: { codingProfile: true } });
|
|
654
|
-
return textResult(agents.map((agent) => ({ ...agent, access: accessOf(agent) })));
|
|
725
|
+
return textResult(agents.map((agent) => ({ ...withOperatorMode(agent), access: accessOf(agent) })));
|
|
655
726
|
},
|
|
656
727
|
});
|
|
657
728
|
mcp.registerTool({
|
|
@@ -671,7 +742,7 @@ export function registerAgentTools(mcp) {
|
|
|
671
742
|
// A7: a tool's code and schema are its owner's, whoever can read the
|
|
672
743
|
// agent (the agent's owner included). Capabilities stay visible: config.
|
|
673
744
|
return textResult({
|
|
674
|
-
...agent,
|
|
745
|
+
...withOperatorMode(agent),
|
|
675
746
|
access,
|
|
676
747
|
tools: agent.tools.map((attachment) => ({
|
|
677
748
|
...attachment,
|
|
@@ -870,7 +941,7 @@ export function registerAgentTools(mcp) {
|
|
|
870
941
|
}
|
|
871
942
|
}
|
|
872
943
|
return {
|
|
873
|
-
...updated,
|
|
944
|
+
...withOperatorMode(updated),
|
|
874
945
|
repositoryApprovalsRevoked: revoked,
|
|
875
946
|
bindingsRemoved,
|
|
876
947
|
toolCapabilitiesSuspended,
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationChannel CRUD — admin-approved links from a Slack channel to a
|
|
3
|
+
* Jira project or to an agent (native or coding), so wardby's workflow dispatcher
|
|
4
|
+
* (core/notification-dispatcher.ts) knows where to post a card's lifecycle:
|
|
5
|
+
* picked up, PR opened, review verdict, fix rounds, merge. Outbound only —
|
|
6
|
+
* wardby never reads a channel's history, and linking posts nothing.
|
|
7
|
+
*
|
|
8
|
+
* Same trust model as link_issue_project (src/mcp/tools/issue-projects.ts):
|
|
9
|
+
* wardby cannot verify a caller's own Slack workspace access (one bot token
|
|
10
|
+
* for the whole deployment, no per-user OAuth), so link/unlink/test need
|
|
11
|
+
* agents:admin with the admin role, and the approver is stamped on the
|
|
12
|
+
* link. Exactly one of projectKey/agentId is the link's subject (enforced
|
|
13
|
+
* again by a DB CHECK constraint); `events` narrows which of the six
|
|
14
|
+
* workflow event kinds post to it (empty/omitted = all); private Slack
|
|
15
|
+
* channels always need /invite @<app> before the bot can see or post to
|
|
16
|
+
* them. See docs/private/2026-10-08-slack-workflow-notifications-design.md §8.
|
|
17
|
+
*/
|
|
18
|
+
import { Prisma } from "#prisma";
|
|
19
|
+
import { requireAgentAccess } from "../auth/access.js";
|
|
20
|
+
import { requireScope } from "../auth/resource-server.js";
|
|
21
|
+
import { McpError } from "../errors.js";
|
|
22
|
+
import { PROJECT_KEY } from "../../providers/issue-tracker/types.js";
|
|
23
|
+
import { ChatError } from "../../providers/chat/types.js";
|
|
24
|
+
import { WORKFLOW_EVENT_KINDS } from "../../core/workflow-events.js";
|
|
25
|
+
import { textResult } from "./text-result.js";
|
|
26
|
+
const CHAT_PROVIDERS = ["slack"];
|
|
27
|
+
/** Slack channel id: C… (public) or G… (private), case-insensitive, normalized upper-case. */
|
|
28
|
+
const CHANNEL_ID = /^[CG][A-Z0-9]{2,20}$/i;
|
|
29
|
+
function normalizeProjectKey(projectKey) {
|
|
30
|
+
const key = projectKey.trim().toUpperCase();
|
|
31
|
+
if (!PROJECT_KEY.test(key))
|
|
32
|
+
throw new McpError(400, `invalid projectKey "${projectKey}"`);
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
function normalizeChannelId(channel) {
|
|
36
|
+
const id = channel.trim().toUpperCase();
|
|
37
|
+
if (!CHANNEL_ID.test(id)) {
|
|
38
|
+
throw new McpError(400, "Pass the channel id (C…); find it in the channel's details in Slack.");
|
|
39
|
+
}
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
export function registerNotificationChannelTools(mcp) {
|
|
43
|
+
mcp.registerTool({
|
|
44
|
+
name: "link_notification_channel",
|
|
45
|
+
scope: "agents:write",
|
|
46
|
+
description: "Links a Slack channel to a Jira project or to an agent (native or coding), so wardby posts that project's or " +
|
|
47
|
+
"agent's workflow updates there: a card picked up, a PR opened, a review verdict, fix rounds, and the merge (one thread per card). " +
|
|
48
|
+
"Outbound only — wardby never reads the channel's history, and nothing in Slack starts work. Admin-approved " +
|
|
49
|
+
"(agents:admin with the admin role), the same trust model as link_issue_project: wardby cannot verify a caller's " +
|
|
50
|
+
"own Slack workspace access, so an admin approves every link and is stamped as its authorizer. " +
|
|
51
|
+
"Exactly one of projectKey or agentId is the link's subject: a project link (with issueProvider, default jira) " +
|
|
52
|
+
"matches events for issues in that project; an agent link matches events for that agent's runs. " +
|
|
53
|
+
"channel is the Slack channel id (C… for public, G… for private), never a #name — find it in the channel's " +
|
|
54
|
+
"details in Slack. Private channels always need /invite @<app> before the bot can see or post to them. " +
|
|
55
|
+
"events narrows which of the six workflow event kinds post to this link; empty or omitted means all. " +
|
|
56
|
+
"includeCost appends the run's spend line to messages. Re-linking the same channel + subject replaces events and " +
|
|
57
|
+
"includeCost and clears any previously recorded delivery error — always send the full desired state.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
properties: {
|
|
62
|
+
provider: { type: "string", enum: [...CHAT_PROVIDERS] },
|
|
63
|
+
channel: { type: "string", minLength: 1, maxLength: 64 },
|
|
64
|
+
projectKey: { type: "string", minLength: 1, maxLength: 255 },
|
|
65
|
+
issueProvider: { type: "string", enum: ["jira"] },
|
|
66
|
+
agentId: { type: "string" },
|
|
67
|
+
events: { type: "array", items: { type: "string", enum: [...WORKFLOW_EVENT_KINDS] }, uniqueItems: true },
|
|
68
|
+
includeCost: { type: "boolean" },
|
|
69
|
+
},
|
|
70
|
+
required: ["channel"],
|
|
71
|
+
},
|
|
72
|
+
handler: async (args, ctx) => {
|
|
73
|
+
requireScope(ctx, ctx.canonicalUri, "agents:admin");
|
|
74
|
+
const provider = args.provider ?? "slack";
|
|
75
|
+
const chat = ctx.providers.chat?.[provider];
|
|
76
|
+
if (!chat) {
|
|
77
|
+
throw new McpError(400, "Slack is not configured (WARDBY_SLACK_BOT_TOKEN). See help article errors/slack-not-configured.");
|
|
78
|
+
}
|
|
79
|
+
const hasProject = args.projectKey !== undefined;
|
|
80
|
+
const hasAgent = args.agentId !== undefined;
|
|
81
|
+
if (hasProject === hasAgent) {
|
|
82
|
+
throw new McpError(400, "Give exactly one of projectKey or agentId.");
|
|
83
|
+
}
|
|
84
|
+
let issueProvider = null;
|
|
85
|
+
let projectKey = null;
|
|
86
|
+
let agentId = null;
|
|
87
|
+
if (hasProject) {
|
|
88
|
+
issueProvider = args.issueProvider ?? "jira";
|
|
89
|
+
projectKey = normalizeProjectKey(args.projectKey);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
agentId = args.agentId;
|
|
93
|
+
const agent = await ctx.db.agent.findUnique({ where: { id: agentId } });
|
|
94
|
+
if (!agent)
|
|
95
|
+
throw new McpError(404, `Agent "${agentId}" not found.`);
|
|
96
|
+
}
|
|
97
|
+
const channelId = normalizeChannelId(args.channel);
|
|
98
|
+
// `info` is undefined when channelInfo can't be validated at all
|
|
99
|
+
// (missing_scope: no channels:read/groups:read — accept the id
|
|
100
|
+
// unvalidated); any other auth failure (revoked or invalid token) is a
|
|
101
|
+
// real error the operator must fix before linking. `info` is
|
|
102
|
+
// null when the bot genuinely cannot see the channel (an error, not a
|
|
103
|
+
// validation gap) — kept out of the try/catch below so the 400 for
|
|
104
|
+
// "bot cannot see this channel" isn't routed through ChatError handling.
|
|
105
|
+
let info;
|
|
106
|
+
try {
|
|
107
|
+
info = await chat.channelInfo(channelId);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (!(err instanceof ChatError && err.code === "auth_failed"))
|
|
111
|
+
throw err;
|
|
112
|
+
if (err.slackError !== "missing_scope") {
|
|
113
|
+
throw new McpError(400, `Slack rejected the bot token (${err.slackError}). See help article errors/slack-auth-failed.`);
|
|
114
|
+
}
|
|
115
|
+
info = undefined;
|
|
116
|
+
}
|
|
117
|
+
if (info === null) {
|
|
118
|
+
throw new McpError(400, `The bot cannot see channel ${channelId}: invite it with /invite @<app> (private channels always need ` +
|
|
119
|
+
"this). See errors/slack-channel-unreachable.");
|
|
120
|
+
}
|
|
121
|
+
const channelName = info ? info.name : null;
|
|
122
|
+
const events = [...new Set(args.events ?? [])];
|
|
123
|
+
const includeCost = args.includeCost ?? false;
|
|
124
|
+
const fields = {
|
|
125
|
+
channelName,
|
|
126
|
+
events,
|
|
127
|
+
includeCost,
|
|
128
|
+
lastError: null,
|
|
129
|
+
lastErrorAt: null,
|
|
130
|
+
authorizedById: ctx.principal.id,
|
|
131
|
+
authorizedAt: new Date(),
|
|
132
|
+
};
|
|
133
|
+
// Can't use Prisma `upsert` here: Postgres treats NULLs as distinct
|
|
134
|
+
// within a unique index, and this link's unique keys
|
|
135
|
+
// (provider, channelId, issueProvider, projectKey) / (provider,
|
|
136
|
+
// channelId, agentId) each have two columns that are null for the
|
|
137
|
+
// other subject kind — so the where-unique input Prisma generates for
|
|
138
|
+
// this compound key never matches an existing row the way a fully
|
|
139
|
+
// non-null key would. Find the existing row explicitly instead, then
|
|
140
|
+
// update it by id or create.
|
|
141
|
+
const subjectWhere = hasProject
|
|
142
|
+
? { provider, channelId, issueProvider, projectKey }
|
|
143
|
+
: { provider, channelId, agentId };
|
|
144
|
+
const existing = await ctx.db.notificationChannel.findFirst({ where: subjectWhere });
|
|
145
|
+
let link;
|
|
146
|
+
if (existing) {
|
|
147
|
+
link = await ctx.db.notificationChannel.update({ where: { id: existing.id }, data: fields });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
try {
|
|
151
|
+
link = await ctx.db.notificationChannel.create({
|
|
152
|
+
data: { provider, channelId, issueProvider, projectKey, agentId, ...fields },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (!(err instanceof Prisma.PrismaClientKnownRequestError) || err.code !== "P2002")
|
|
157
|
+
throw err;
|
|
158
|
+
// Lost a create race: another concurrent link_notification_channel
|
|
159
|
+
// call for the same channel + subject committed first. Re-linking
|
|
160
|
+
// an existing link is normal, documented behavior (it replaces
|
|
161
|
+
// events/includeCost and clears lastError) — so re-query and
|
|
162
|
+
// update that row instead of surfacing the race as an error.
|
|
163
|
+
const raced = await ctx.db.notificationChannel.findFirst({ where: subjectWhere });
|
|
164
|
+
if (!raced)
|
|
165
|
+
throw err;
|
|
166
|
+
link = await ctx.db.notificationChannel.update({ where: { id: raced.id }, data: fields });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return textResult({ link });
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
mcp.registerTool({
|
|
173
|
+
name: "unlink_notification_channel",
|
|
174
|
+
scope: "agents:write",
|
|
175
|
+
description: "Removes a notification channel link. Admin only (agents:admin with the admin role).",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
properties: { id: { type: "string" } },
|
|
180
|
+
required: ["id"],
|
|
181
|
+
},
|
|
182
|
+
handler: async (args, ctx) => {
|
|
183
|
+
requireScope(ctx, ctx.canonicalUri, "agents:admin");
|
|
184
|
+
const { count } = await ctx.db.notificationChannel.deleteMany({ where: { id: args.id } });
|
|
185
|
+
return textResult({ unlinked: count > 0 });
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
mcp.registerTool({
|
|
189
|
+
name: "list_notification_channels",
|
|
190
|
+
scope: "agents:read",
|
|
191
|
+
description: "Lists notification channel links, each with its pending and failed delivery counts. Filter by projectKey, " +
|
|
192
|
+
"agentId, or channel (the Slack channel id). Listing an agent's own links needs read access to that agent; " +
|
|
193
|
+
"listing without agentId (every link, including project links) needs agents:admin with the admin role.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
additionalProperties: false,
|
|
197
|
+
properties: {
|
|
198
|
+
projectKey: { type: "string", minLength: 1, maxLength: 255 },
|
|
199
|
+
agentId: { type: "string" },
|
|
200
|
+
channel: { type: "string", minLength: 1, maxLength: 64 },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
handler: async (args, ctx) => {
|
|
204
|
+
if (args.agentId !== undefined) {
|
|
205
|
+
await requireAgentAccess(ctx, args.agentId, "read");
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
requireScope(ctx, ctx.canonicalUri, "agents:admin");
|
|
209
|
+
}
|
|
210
|
+
const where = {};
|
|
211
|
+
if (args.agentId !== undefined)
|
|
212
|
+
where.agentId = args.agentId;
|
|
213
|
+
if (args.projectKey !== undefined)
|
|
214
|
+
where.projectKey = normalizeProjectKey(args.projectKey);
|
|
215
|
+
if (args.channel !== undefined)
|
|
216
|
+
where.channelId = normalizeChannelId(args.channel);
|
|
217
|
+
const links = await ctx.db.notificationChannel.findMany({ where, orderBy: { createdAt: "asc" } });
|
|
218
|
+
// Counts only for the listed links' own (provider, channel) pairs, so a
|
|
219
|
+
// caller never learns about deliveries to channels it cannot list.
|
|
220
|
+
const pairKey = (r) => `${r.provider}\u0000${r.channelId}`;
|
|
221
|
+
const pairs = [
|
|
222
|
+
...new Map(links.map((l) => [pairKey(l), { provider: l.provider, channelId: l.channelId }])).values(),
|
|
223
|
+
];
|
|
224
|
+
const counts = pairs.length === 0
|
|
225
|
+
? []
|
|
226
|
+
: await ctx.db.notificationDelivery.groupBy({
|
|
227
|
+
by: ["provider", "channelId", "state"],
|
|
228
|
+
where: { state: { in: ["pending", "failed"] }, OR: pairs },
|
|
229
|
+
_count: true,
|
|
230
|
+
});
|
|
231
|
+
const byChannel = new Map();
|
|
232
|
+
for (const row of counts) {
|
|
233
|
+
const entry = byChannel.get(pairKey(row)) ?? { pending: 0, failed: 0 };
|
|
234
|
+
if (row.state === "pending")
|
|
235
|
+
entry.pending = row._count;
|
|
236
|
+
else if (row.state === "failed")
|
|
237
|
+
entry.failed = row._count;
|
|
238
|
+
byChannel.set(pairKey(row), entry);
|
|
239
|
+
}
|
|
240
|
+
const channels = links.map((l) => ({
|
|
241
|
+
...l,
|
|
242
|
+
pending: byChannel.get(pairKey(l))?.pending ?? 0,
|
|
243
|
+
failed: byChannel.get(pairKey(l))?.failed ?? 0,
|
|
244
|
+
}));
|
|
245
|
+
return textResult({ channels });
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
mcp.registerTool({
|
|
249
|
+
name: "test_notification_channel",
|
|
250
|
+
scope: "agents:write",
|
|
251
|
+
description: "Posts a one-off confirmation message to a linked channel, to prove the bot can write there before relying on it " +
|
|
252
|
+
"for workflow notifications (a private channel needs /invite @<app> first; a channel the bot can't reach " +
|
|
253
|
+
"surfaces its error immediately instead of silently queueing). Admin only (agents:admin with the admin role).",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: "object",
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
properties: { id: { type: "string" } },
|
|
258
|
+
required: ["id"],
|
|
259
|
+
},
|
|
260
|
+
handler: async (args, ctx) => {
|
|
261
|
+
requireScope(ctx, ctx.canonicalUri, "agents:admin");
|
|
262
|
+
const link = await ctx.db.notificationChannel.findUnique({ where: { id: args.id } });
|
|
263
|
+
if (!link)
|
|
264
|
+
throw new McpError(404, `Notification channel link "${args.id}" not found.`);
|
|
265
|
+
const chat = ctx.providers.chat?.[link.provider];
|
|
266
|
+
if (!chat) {
|
|
267
|
+
throw new McpError(400, "Slack is not configured (WARDBY_SLACK_BOT_TOKEN). See help article errors/slack-not-configured.");
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
await chat.postMessage(link.channelId, { text: "✅ wardby is connected to this channel." });
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (!(err instanceof ChatError))
|
|
274
|
+
throw err;
|
|
275
|
+
if (err.code === "channel_unreachable") {
|
|
276
|
+
await ctx.db.notificationChannel.update({
|
|
277
|
+
where: { id: link.id },
|
|
278
|
+
data: { lastError: err.slackError, lastErrorAt: new Date() },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
throw new McpError(400, `${err.code}: ${err.slackError}`);
|
|
282
|
+
}
|
|
283
|
+
// The bot can post here again: clear any recorded delivery error.
|
|
284
|
+
if (link.lastError !== null || link.lastErrorAt !== null) {
|
|
285
|
+
await ctx.db.notificationChannel.update({
|
|
286
|
+
where: { id: link.id },
|
|
287
|
+
data: { lastError: null, lastErrorAt: null },
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return textResult({ tested: true });
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-power mode over MCP: power_status reads the coding proxy's and native
|
|
3
|
+
* gateway's state, power_sleep scales them to zero now instead of waiting out
|
|
4
|
+
* the idle window. Both need power:manage, a privileged scope honoured only
|
|
5
|
+
* for the admin role (resource-server.ts). power_sleep never stops a run: a
|
|
6
|
+
* target with active or queued runs (or, for the gateway, an active gateway
|
|
7
|
+
* session) is refused and its run ids returned. Each power_sleep is audited
|
|
8
|
+
* (event power.sleep, outcome ok or error, naming the principal), including a
|
|
9
|
+
* failed or refused call, since a failure can follow a target already slept.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { logger } from "../../core/logger.js";
|
|
13
|
+
import { DISABLED_POWER, LOW_POWER_DISABLED } from "../../core/power.js";
|
|
14
|
+
import { McpError } from "../errors.js";
|
|
15
|
+
import { textResult } from "./text-result.js";
|
|
16
|
+
const powerToolLog = logger.child({ module: "mcp-power" });
|
|
17
|
+
const TARGETS = ["proxy", "gateway"];
|
|
18
|
+
const SleepSchema = z.object({ target: z.enum(TARGETS).optional() }).strict();
|
|
19
|
+
const StatusSchema = z.object({}).strict();
|
|
20
|
+
function parse(schema, label, value) {
|
|
21
|
+
const result = schema.safeParse(value ?? {});
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
const details = result.error.issues
|
|
24
|
+
.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`)
|
|
25
|
+
.join("; ");
|
|
26
|
+
throw new McpError(400, `Invalid ${label}: ${details}`);
|
|
27
|
+
}
|
|
28
|
+
return result.data;
|
|
29
|
+
}
|
|
30
|
+
export function registerPowerTools(mcp) {
|
|
31
|
+
mcp.registerTool({
|
|
32
|
+
name: "power_status",
|
|
33
|
+
scope: "power:manage",
|
|
34
|
+
description: "Admins (power:manage): low-power mode state. For the coding proxy and the native gateway: replicas, whether a ready endpoint exists, when it last woke (wokeAt), and blockers (run ids keeping it busy). Returns { enabled: false } when low-power mode is off.",
|
|
35
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
36
|
+
handler: async (rawArgs, ctx) => {
|
|
37
|
+
parse(StatusSchema, "power_status arguments", rawArgs);
|
|
38
|
+
return textResult(await (ctx.providers.power ?? DISABLED_POWER).status());
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
mcp.registerTool({
|
|
42
|
+
name: "power_sleep",
|
|
43
|
+
scope: "power:manage",
|
|
44
|
+
description: "Admins (power:manage): scale the coding proxy and/or native gateway to zero now, skipping low-power mode's idle window. It never stops a run: a target is refused while it has active or queued runs (for the gateway, also an active gateway session), and those run ids are returned under blocked. Recently finished runs do not block it. The next run wakes the target again. Fails (409 low_power_disabled) when low-power mode is off.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: {
|
|
49
|
+
target: {
|
|
50
|
+
type: "string",
|
|
51
|
+
enum: [...TARGETS],
|
|
52
|
+
description: "Sleep only this target. Omit to sleep both.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
handler: async (rawArgs, ctx) => {
|
|
57
|
+
const { target } = parse(SleepSchema, "power_sleep arguments", rawArgs);
|
|
58
|
+
const targets = target ? [target] : [...TARGETS];
|
|
59
|
+
const power = ctx.providers.power ?? DISABLED_POWER;
|
|
60
|
+
const audit = { event: "power.sleep", principal: ctx.principal.id, targets };
|
|
61
|
+
let result;
|
|
62
|
+
try {
|
|
63
|
+
result = await power.sleepNow(targets);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Audited before rethrowing: sleepNow may already have slept an earlier target.
|
|
67
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
68
|
+
powerToolLog.warn({ ...audit, outcome: "error", error }, "power_sleep");
|
|
69
|
+
if (err instanceof Error && err.message.startsWith(`${LOW_POWER_DISABLED}:`))
|
|
70
|
+
throw new McpError(409, err.message);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
powerToolLog.info({ ...audit, outcome: "ok", slept: result.slept, blocked: result.blocked }, "power_sleep");
|
|
74
|
+
return textResult(result);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
package/dist/mcp/tools/runs.d.ts
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
1
|
import type { WardbyMcpServer } from "../server.js";
|
|
2
|
-
/**
|
|
3
|
-
* Run visibility (resource-sharing grants spec §3.5, A6): a run's output is
|
|
4
|
-
* the triggerer's and the agent owner's, never every reader's. The owner
|
|
5
|
-
* (and the stdio operator) sees every run; anyone else only runs with
|
|
6
|
-
* Run.triggeredById equal to themselves.
|
|
7
|
-
*/
|
|
8
2
|
export declare function registerRunTools(mcp: WardbyMcpServer): void;
|
package/dist/mcp/tools/runs.js
CHANGED
|
@@ -10,6 +10,11 @@ import { textResult } from "./text-result.js";
|
|
|
10
10
|
* (and the stdio operator) sees every run; anyone else only runs with
|
|
11
11
|
* Run.triggeredById equal to themselves.
|
|
12
12
|
*/
|
|
13
|
+
/** A run row with its execution mode in the operator spelling agent tools use; null for coding runs. */
|
|
14
|
+
function withOperatorMode(run) {
|
|
15
|
+
const mode = run.nativeExecutionMode;
|
|
16
|
+
return { ...run, nativeExecutionMode: mode == null ? null : mode === "sandbox" ? "sandbox" : "control-plane" };
|
|
17
|
+
}
|
|
13
18
|
export function registerRunTools(mcp) {
|
|
14
19
|
mcp.registerTool({
|
|
15
20
|
name: "list_runs",
|
|
@@ -42,7 +47,7 @@ export function registerRunTools(mcp) {
|
|
|
42
47
|
const queuedAtByRun = new Map(queued.map((row) => [row.runId, row.queuedAt?.toISOString()]));
|
|
43
48
|
return textResult(runs.map((run) => {
|
|
44
49
|
const codingQueuedAt = queuedAtByRun.get(run.id);
|
|
45
|
-
return codingQueuedAt ? { ...run, codingQueuedAt } : run;
|
|
50
|
+
return codingQueuedAt ? { ...withOperatorMode(run), codingQueuedAt } : withOperatorMode(run);
|
|
46
51
|
}));
|
|
47
52
|
},
|
|
48
53
|
});
|
|
@@ -106,7 +111,7 @@ export function registerRunTools(mcp) {
|
|
|
106
111
|
// Which services the run started with ("postgres 16"); never their images or environments.
|
|
107
112
|
const services = codingRun ? storedServiceLabels(codingRun.services) : [];
|
|
108
113
|
return textResult({
|
|
109
|
-
...run,
|
|
114
|
+
...withOperatorMode(run),
|
|
110
115
|
...(codingResult ? { codingResult } : {}),
|
|
111
116
|
...(failureCategory ? { failureCategory } : {}),
|
|
112
117
|
...(diagnosticId ? { diagnosticId } : {}),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A static, best-effort check: a tool granted a brokered secret that still reads secrets with
|
|
3
|
+
* secrets.get() will fail at run time. Warns; never blocks (the scan can't see dynamic code).
|
|
4
|
+
*/
|
|
5
|
+
export declare function toolCallsSecretsGet(code: string): boolean;
|
|
6
|
+
export declare function brokerCompatibilityWarnings(tools: readonly {
|
|
7
|
+
name: string;
|
|
8
|
+
code: string;
|
|
9
|
+
allowedSecrets: readonly string[];
|
|
10
|
+
}[], brokeredNames: ReadonlySet<string>): string[];
|
|
11
|
+
/** An attachment's `allowedSecrets` JSON column as names (anything else is ignored). */
|
|
12
|
+
export declare function allowedSecretNames(json: unknown): string[];
|