@wardby/cli 0.5.3 → 0.7.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/deploy/local/docker-compose.quickstart-coding.yml +13 -0
- 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 +27 -3
- package/dist/claude-coding-worker/driver.js +161 -20
- package/dist/claude-coding-worker/main.js +34 -6
- package/dist/claude-coding-worker/sdk.js +25 -5
- package/dist/claude-coding-worker/tool-socket.d.ts +27 -0
- package/dist/claude-coding-worker/tool-socket.js +111 -0
- package/dist/claude-coding-worker/transcript.d.ts +18 -0
- package/dist/claude-coding-worker/transcript.js +77 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +6 -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 +161 -11
- package/dist/coding/claude-context.d.ts +45 -0
- package/dist/coding/claude-context.js +434 -0
- package/dist/coding/continuation-wording.d.ts +35 -0
- package/dist/coding/continuation-wording.js +73 -0
- package/dist/coding/linked-issue-note.d.ts +23 -0
- package/dist/coding/linked-issue-note.js +47 -0
- package/dist/coding/merge-wording.d.ts +28 -0
- package/dist/coding/merge-wording.js +78 -0
- package/dist/coding/pr-description-note.d.ts +9 -0
- package/dist/coding/pr-description-note.js +32 -0
- package/dist/coding/profile.d.ts +50 -118
- package/dist/coding/profile.js +51 -4
- package/dist/coding/protected-path-wording.d.ts +8 -0
- package/dist/coding/protected-path-wording.js +1 -1
- package/dist/coding/protocol.d.ts +292 -270
- package/dist/coding/protocol.js +322 -15
- package/dist/coding/redact.d.ts +3 -0
- package/dist/coding/redact.js +12 -0
- package/dist/coding/registry/allowlist.d.ts +12 -0
- package/dist/coding/registry/allowlist.js +14 -1
- package/dist/coding/registry/audit-mode.d.ts +153 -0
- package/dist/coding/registry/audit-mode.js +304 -0
- package/dist/coding/registry/lockfile-shared.d.ts +36 -0
- package/dist/coding/registry/lockfile-shared.js +128 -0
- package/dist/coding/registry/lockfiles.js +1 -1
- package/dist/coding/registry/npm-lockfile-formats.d.ts +11 -0
- package/dist/coding/registry/npm-lockfile-formats.js +41 -0
- package/dist/coding/registry/npm-lockfile.d.ts +8 -4
- package/dist/coding/registry/npm-lockfile.js +4 -6
- package/dist/coding/registry/npm.js +78 -8
- package/dist/coding/registry/pnpm-lockfile.d.ts +17 -0
- package/dist/coding/registry/pnpm-lockfile.js +189 -0
- package/dist/coding/registry/pypi-plan.d.ts +50 -0
- package/dist/coding/registry/pypi-plan.js +406 -0
- package/dist/coding/registry/pypi-requirements.d.ts +58 -0
- package/dist/coding/registry/pypi-requirements.js +155 -0
- package/dist/coding/registry/pypi.d.ts +3 -27
- package/dist/coding/registry/pypi.js +40 -154
- package/dist/coding/registry/refusal-hints.d.ts +20 -0
- package/dist/coding/registry/refusal-hints.js +61 -0
- package/dist/coding/registry/refusal-wording.d.ts +11 -0
- package/dist/coding/registry/refusal-wording.js +56 -0
- package/dist/coding/registry/report.d.ts +70 -8
- package/dist/coding/registry/report.js +112 -5
- package/dist/coding/registry/types.d.ts +58 -8
- package/dist/coding/registry/yarn-lockfile.d.ts +30 -0
- package/dist/coding/registry/yarn-lockfile.js +367 -0
- package/dist/coding/services/catalog.d.ts +37 -351
- package/dist/coding/transcript/claude-code.d.ts +13 -0
- package/dist/coding/transcript/claude-code.js +273 -0
- package/dist/coding/transcript/download-protocol.d.ts +30 -0
- package/dist/coding/transcript/download-protocol.js +18 -0
- package/dist/coding/transcript/format.d.ts +20 -0
- package/dist/coding/transcript/format.js +32 -0
- package/dist/coding/transcript/recorder.d.ts +51 -0
- package/dist/coding/transcript/recorder.js +225 -0
- package/dist/coding/transcript/resume/checkout.d.ts +31 -0
- package/dist/coding/transcript/resume/checkout.js +203 -0
- package/dist/coding/transcript/resume/command.d.ts +17 -0
- package/dist/coding/transcript/resume/command.js +176 -0
- package/dist/coding/transcript/resume/download.d.ts +20 -0
- package/dist/coding/transcript/resume/download.js +153 -0
- package/dist/coding/transcript/resume/launch.d.ts +38 -0
- package/dist/coding/transcript/resume/launch.js +128 -0
- package/dist/coding/transcript/resume/session-file.d.ts +13 -0
- package/dist/coding/transcript/resume/session-file.js +79 -0
- package/dist/coding/transcript/resume/stand-in.d.ts +11 -0
- package/dist/coding/transcript/resume/stand-in.js +49 -0
- package/dist/coding-worker/artifact.d.ts +16 -3
- package/dist/coding-worker/artifact.js +5 -3
- package/dist/coding-worker/debug-trace.d.ts +6 -5
- package/dist/coding-worker/debug-trace.js +13 -18
- package/dist/coding-worker/dependency-cache.d.mts +229 -0
- package/dist/coding-worker/dependency-cache.mjs +2123 -0
- package/dist/coding-worker/driver.d.ts +4 -3
- package/dist/coding-worker/driver.js +118 -38
- package/dist/coding-worker/errors.d.ts +1 -0
- package/dist/coding-worker/errors.js +32 -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/task-note.d.ts +11 -0
- package/dist/coding-worker/task-note.js +13 -0
- package/dist/coding-worker/types.d.ts +13 -0
- package/dist/config/dependency-cache.d.ts +23 -0
- package/dist/config/dependency-cache.js +32 -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 +65 -3
- package/dist/config/providers.js +147 -11
- package/dist/config/test-simulators.d.ts +82 -0
- package/dist/config/test-simulators.js +305 -0
- package/dist/core/async-context.d.ts +2 -0
- package/dist/core/async-context.js +12 -0
- package/dist/core/budget-events.d.ts +108 -0
- package/dist/core/budget-events.js +346 -0
- package/dist/core/budget-groups.d.ts +50 -1
- package/dist/core/budget-groups.js +236 -27
- package/dist/core/budget-stopped-head.d.ts +46 -0
- package/dist/core/budget-stopped-head.js +81 -0
- package/dist/core/budget-topup.d.ts +51 -0
- package/dist/core/budget-topup.js +156 -0
- package/dist/core/budget-wording.d.ts +16 -2
- package/dist/core/budget-wording.js +40 -2
- package/dist/core/coding-queue.d.ts +2 -1
- package/dist/core/coding-queue.js +7 -0
- package/dist/core/cron.d.ts +19 -1
- package/dist/core/cron.js +17 -0
- package/dist/core/datastores.js +1 -1
- package/dist/core/db.d.ts +16 -6
- package/dist/core/db.js +12 -10
- package/dist/core/deferred-coding.d.ts +14 -0
- package/dist/core/deferred-coding.js +62 -0
- package/dist/core/delegation-child.d.ts +25 -0
- package/dist/core/delegation-child.js +37 -0
- package/dist/core/dispatch.d.ts +53 -0
- package/dist/core/dispatch.js +167 -57
- package/dist/core/engine-native.js +10 -6
- package/dist/core/error-code.d.ts +38 -0
- package/dist/core/error-code.js +168 -0
- package/dist/core/grants.d.ts +19 -6
- package/dist/core/grants.js +55 -6
- package/dist/core/host-events.d.ts +67 -5
- package/dist/core/host-events.js +571 -37
- package/dist/core/host-status.d.ts +85 -3
- package/dist/core/host-status.js +298 -20
- package/dist/core/issue-bridge.d.ts +50 -6
- package/dist/core/issue-bridge.js +151 -20
- package/dist/core/issue-events.d.ts +1 -1
- package/dist/core/issue-events.js +16 -11
- package/dist/core/issue-status.d.ts +1 -1
- package/dist/core/issue-status.js +68 -29
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +14 -1
- package/dist/core/lost-run-children.d.ts +6 -0
- package/dist/core/lost-run-children.js +32 -0
- package/dist/core/mark-ready.d.ts +34 -0
- package/dist/core/mark-ready.js +152 -0
- package/dist/core/markdown-code.d.ts +8 -0
- package/dist/core/markdown-code.js +107 -0
- package/dist/core/memory-tools.js +7 -0
- package/dist/core/merge-order-check.d.ts +130 -0
- package/dist/core/merge-order-check.js +343 -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 +19 -0
- package/dist/core/notification-templates.js +139 -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/pull-request-state-sync.js +2 -2
- package/dist/core/reconciler.d.ts +8 -5
- package/dist/core/reconciler.js +37 -6
- package/dist/core/related-pull-requests.d.ts +59 -7
- package/dist/core/related-pull-requests.js +140 -25
- package/dist/core/reply-parts.d.ts +21 -0
- package/dist/core/reply-parts.js +73 -0
- package/dist/core/repo-access.d.ts +15 -0
- package/dist/core/repo-access.js +66 -0
- package/dist/core/review-fix.d.ts +38 -1
- package/dist/core/review-fix.js +209 -7
- package/dist/core/review-host-checks.d.ts +1 -1
- package/dist/core/review-host-tools.d.ts +23 -0
- package/dist/core/review-host-tools.js +156 -14
- package/dist/core/review-reads.d.ts +55 -0
- package/dist/core/review-reads.js +233 -0
- package/dist/core/run-status.d.ts +12 -0
- package/dist/core/run-status.js +12 -0
- package/dist/core/runner.d.ts +384 -5
- package/dist/core/runner.js +1244 -372
- package/dist/core/schedule-start.d.ts +12 -0
- package/dist/core/schedule-start.js +14 -0
- package/dist/core/secret-broker-config.d.ts +39 -0
- package/dist/core/secret-broker-config.js +112 -0
- package/dist/core/secret-guard.d.ts +46 -0
- package/dist/core/secret-guard.js +104 -0
- package/dist/core/secret-guarded-stores.d.ts +12 -0
- package/dist/core/secret-guarded-stores.js +41 -0
- package/dist/core/secret-spans.d.ts +50 -0
- package/dist/core/secret-spans.js +94 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +148 -40
- package/dist/core/stop-run.d.ts +46 -0
- package/dist/core/stop-run.js +61 -0
- 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 +94 -0
- package/dist/core/workflow-events.js +48 -0
- package/dist/core/workflow-run-events.d.ts +18 -0
- package/dist/core/workflow-run-events.js +75 -0
- package/dist/core/zod-json-schema.d.ts +4 -0
- package/dist/core/zod-json-schema.js +39 -0
- package/dist/core/zod3-window.d.ts +35 -0
- package/dist/core/zod3-window.js +124 -0
- package/dist/generated/prisma/browser.d.ts +92 -3
- package/dist/generated/prisma/client.d.ts +92 -3
- package/dist/generated/prisma/commonInputTypes.d.ts +364 -54
- package/dist/generated/prisma/enums.d.ts +37 -0
- package/dist/generated/prisma/enums.js +31 -0
- package/dist/generated/prisma/internal/class.d.ts +132 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +1332 -142
- package/dist/generated/prisma/internal/prismaNamespace.js +227 -5
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +234 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +227 -5
- package/dist/generated/prisma/models/Agent.d.ts +464 -7
- package/dist/generated/prisma/models/AgentRepository.d.ts +42 -1
- package/dist/generated/prisma/models/CodingAgentProfile.d.ts +301 -1
- package/dist/generated/prisma/models/CodingProxyRequest.d.ts +207 -3
- package/dist/generated/prisma/models/CodingProxySession.d.ts +114 -1
- package/dist/generated/prisma/models/CodingRun.d.ts +1841 -1
- package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
- package/dist/generated/prisma/models/DependencySnapshot.d.ts +1668 -0
- package/dist/generated/prisma/models/DependencySnapshot.js +1 -0
- package/dist/generated/prisma/models/IssuePullRequest.d.ts +6 -2
- package/dist/generated/prisma/models/LocalReview.d.ts +40 -1
- package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1713 -0
- package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
- package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1551 -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/RegistryAllowance.d.ts +41 -1
- package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +75 -1
- package/dist/generated/prisma/models/RegistryAuditRecord.d.ts +1364 -0
- package/dist/generated/prisma/models/RegistryAuditRecord.js +1 -0
- package/dist/generated/prisma/models/Run.d.ts +1619 -58
- package/dist/generated/prisma/models/RunBudgetEvent.d.ts +1211 -0
- package/dist/generated/prisma/models/RunBudgetEvent.js +1 -0
- package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
- package/dist/generated/prisma/models/RunTranscript.d.ts +1339 -0
- package/dist/generated/prisma/models/RunTranscript.js +1 -0
- 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 +12 -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 +2584 -146
- package/dist/import/create.js +49 -3
- package/dist/import/neutral-schema.d.ts +50 -277
- package/dist/import/neutral-schema.js +7 -4
- package/dist/knowledge/check.d.ts +1 -1
- package/dist/knowledge/cli.d.ts +11 -1
- package/dist/knowledge/cli.js +37 -4
- package/dist/knowledge/concept.d.ts +3 -19
- package/dist/knowledge/location.d.ts +31 -0
- package/dist/knowledge/location.js +78 -0
- package/dist/mcp/auth/access.d.ts +11 -2
- package/dist/mcp/auth/access.js +15 -1
- package/dist/mcp/auth/ownership.d.ts +7 -7
- 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/auth/self-hosted/credentials.d.ts +3 -3
- package/dist/mcp/auth/self-hosted/session.d.ts +2 -2
- package/dist/mcp/context.d.ts +6 -0
- package/dist/mcp/host-events/github-ingress.d.ts +25 -1
- package/dist/mcp/host-events/github-ingress.js +70 -1
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.js +117 -20
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +33 -14
- package/dist/mcp/tools/agents.d.ts +4 -0
- package/dist/mcp/tools/agents.js +277 -24
- package/dist/mcp/tools/datastore.js +17 -8
- package/dist/mcp/tools/grants.js +9 -3
- package/dist/mcp/tools/memory.js +11 -1
- package/dist/mcp/tools/notification-channels.d.ts +2 -0
- package/dist/mcp/tools/notification-channels.js +293 -0
- package/dist/mcp/tools/packages.d.ts +2 -0
- package/dist/mcp/tools/packages.js +195 -0
- package/dist/mcp/tools/power.d.ts +2 -0
- package/dist/mcp/tools/power.js +77 -0
- package/dist/mcp/tools/repositories.js +18 -5
- package/dist/mcp/tools/run-access.d.ts +16 -0
- package/dist/mcp/tools/run-access.js +25 -0
- package/dist/mcp/tools/run-transcript-download.d.ts +30 -0
- package/dist/mcp/tools/run-transcript-download.js +129 -0
- package/dist/mcp/tools/run-transcript.d.ts +19 -0
- package/dist/mcp/tools/run-transcript.js +70 -0
- package/dist/mcp/tools/runs.d.ts +0 -6
- package/dist/mcp/tools/runs.js +41 -14
- package/dist/mcp/tools/scheduling.js +3 -1
- 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 +205 -10
- package/dist/mcp/tools/secret-elicitation-server.d.ts +6 -0
- package/dist/mcp/tools/secret-elicitation-server.js +10 -54
- 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 +186 -9
- package/dist/mcp/tools/stop-run.d.ts +2 -0
- package/dist/mcp/tools/stop-run.js +82 -0
- package/dist/mcp/tools/subagents.js +2 -2
- package/dist/mcp/tools/tools.d.ts +2 -2
- package/dist/mcp/tools/tools.js +47 -11
- package/dist/mcp/transport/stdio-link-host.d.ts +20 -0
- package/dist/mcp/transport/stdio-link-host.js +67 -0
- package/dist/mcp/transport/streamable-http.d.ts +13 -0
- package/dist/mcp/transport/streamable-http.js +41 -1
- 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 +110 -0
- package/dist/native-worker/docker-launcher.js +297 -0
- package/dist/native-worker/gateway-server.d.ts +77 -0
- package/dist/native-worker/gateway-server.js +412 -0
- package/dist/native-worker/gateway.d.ts +70 -0
- package/dist/native-worker/gateway.js +263 -0
- package/dist/native-worker/http-server.d.ts +24 -0
- package/dist/native-worker/http-server.js +113 -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 +106 -0
- package/dist/native-worker/kubernetes-launcher.js +375 -0
- package/dist/native-worker/launch.d.ts +62 -0
- package/dist/native-worker/launch.js +49 -0
- package/dist/native-worker/ledger.d.ts +125 -0
- package/dist/native-worker/ledger.js +245 -0
- package/dist/native-worker/loopback.d.ts +12 -0
- package/dist/native-worker/loopback.js +44 -0
- package/dist/native-worker/main.d.ts +8 -0
- package/dist/native-worker/main.js +85 -0
- package/dist/native-worker/protocol.d.ts +296 -0
- package/dist/native-worker/protocol.js +246 -0
- package/dist/native-worker/sandbox-executor.d.ts +83 -0
- package/dist/native-worker/sandbox-executor.js +284 -0
- package/dist/native-worker/stderr-redaction.d.ts +77 -0
- package/dist/native-worker/stderr-redaction.js +237 -0
- package/dist/native-worker/stdio.d.ts +29 -0
- package/dist/native-worker/stdio.js +189 -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 +141 -0
- package/dist/observability/metrics.js +6 -1
- 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 +14 -2
- 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/coding-proxy/dependency-cache/eviction.d.ts +29 -0
- package/dist/providers/coding-proxy/dependency-cache/eviction.js +87 -0
- package/dist/providers/coding-proxy/dependency-cache/key.d.ts +33 -0
- package/dist/providers/coding-proxy/dependency-cache/key.js +86 -0
- package/dist/providers/coding-proxy/dependency-cache/model-request-seal.d.ts +18 -0
- package/dist/providers/coding-proxy/dependency-cache/model-request-seal.js +40 -0
- package/dist/providers/coding-proxy/dependency-cache/prisma-store.d.ts +25 -0
- package/dist/providers/coding-proxy/dependency-cache/prisma-store.js +131 -0
- package/dist/providers/coding-proxy/dependency-cache/service.d.ts +70 -0
- package/dist/providers/coding-proxy/dependency-cache/service.js +409 -0
- package/dist/providers/coding-proxy/dependency-cache/store.d.ts +85 -0
- package/dist/providers/coding-proxy/dependency-cache/store.js +95 -0
- package/dist/providers/coding-proxy/hold.d.ts +32 -0
- package/dist/providers/coding-proxy/hold.js +86 -0
- package/dist/providers/coding-proxy/interrupted.d.ts +70 -0
- package/dist/providers/coding-proxy/interrupted.js +104 -0
- package/dist/providers/coding-proxy/memory-ledger.d.ts +15 -2
- package/dist/providers/coding-proxy/memory-ledger.js +84 -8
- package/dist/providers/coding-proxy/metering.d.ts +16 -0
- package/dist/providers/coding-proxy/metering.js +27 -4
- package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -2
- package/dist/providers/coding-proxy/prisma-ledger.js +179 -67
- package/dist/providers/coding-proxy/proxy.d.ts +14 -1
- package/dist/providers/coding-proxy/proxy.js +166 -14
- package/dist/providers/coding-proxy/registry/audit.d.ts +53 -10
- package/dist/providers/coding-proxy/registry/audit.js +94 -17
- package/dist/providers/coding-proxy/registry/lockfile-parse-worker.d.ts +36 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse-worker.js +67 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse.d.ts +16 -0
- package/dist/providers/coding-proxy/registry/lockfile-parse.js +110 -0
- package/dist/providers/coding-proxy/registry/plan.d.ts +9 -0
- package/dist/providers/coding-proxy/registry/plan.js +201 -48
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +11 -5
- package/dist/providers/coding-proxy/registry/prisma-store.js +120 -17
- package/dist/providers/coding-proxy/registry/service.d.ts +97 -16
- package/dist/providers/coding-proxy/registry/service.js +579 -112
- package/dist/providers/coding-proxy/registry/store.d.ts +84 -9
- package/dist/providers/coding-proxy/registry/store.js +54 -1
- package/dist/providers/coding-proxy/runtime.d.ts +4 -0
- package/dist/providers/coding-proxy/runtime.js +78 -16
- package/dist/providers/coding-proxy/server.d.ts +6 -1
- package/dist/providers/coding-proxy/server.js +45 -1
- package/dist/providers/coding-proxy/types.d.ts +65 -1
- package/dist/providers/datastore/types.d.ts +5 -0
- package/dist/providers/engine/types.d.ts +36 -2
- package/dist/providers/engine/types.js +39 -0
- package/dist/providers/executor/composition.d.ts +31 -1
- package/dist/providers/executor/composition.js +81 -8
- package/dist/providers/executor/container.d.ts +385 -9
- package/dist/providers/executor/container.js +1234 -99
- package/dist/providers/executor/dbos-status.d.ts +31 -0
- package/dist/providers/executor/dbos-status.js +64 -0
- package/dist/providers/executor/dbos.d.ts +91 -0
- package/dist/providers/executor/dbos.js +319 -26
- package/dist/providers/executor/deferred.d.ts +44 -0
- package/dist/providers/executor/deferred.js +100 -0
- package/dist/providers/executor/in-process.d.ts +9 -1
- package/dist/providers/executor/in-process.js +14 -4
- package/dist/providers/executor/linked-issue-reader.d.ts +16 -0
- package/dist/providers/executor/linked-issue-reader.js +30 -0
- package/dist/providers/executor/routing.d.ts +39 -10
- package/dist/providers/executor/routing.js +77 -32
- package/dist/providers/executor/types.d.ts +25 -0
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/issue-tracker/types.d.ts +17 -0
- package/dist/providers/issue-tracker/types.js +39 -1
- package/dist/providers/jobs/claude-tool-setup.d.ts +4 -0
- package/dist/providers/jobs/claude-tool-setup.js +5 -2
- package/dist/providers/jobs/docker-isolation.d.ts +8 -0
- package/dist/providers/jobs/docker-isolation.js +17 -0
- package/dist/providers/jobs/docker.d.ts +1 -0
- package/dist/providers/jobs/docker.js +29 -4
- package/dist/providers/jobs/fake-kubernetes-api.d.ts +23 -2
- package/dist/providers/jobs/fake-kubernetes-api.js +45 -1
- package/dist/providers/jobs/kubernetes-api.d.ts +25 -1
- package/dist/providers/jobs/kubernetes-api.js +17 -0
- package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
- package/dist/providers/jobs/kubernetes-client.js +61 -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 +5 -1
- package/dist/providers/jobs/kubernetes-isolation.js +23 -1
- 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 +42 -2
- package/dist/providers/jobs/kubernetes.js +178 -19
- 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 +42 -0
- package/dist/providers/jobs/types.js +13 -1
- package/dist/providers/llm/anthropic.js +5 -1
- package/dist/providers/llm/bedrock.d.ts +0 -12
- package/dist/providers/llm/bedrock.js +4 -0
- package/dist/providers/llm/claude-messages.d.ts +3 -1
- package/dist/providers/llm/claude-messages.js +4 -1
- package/dist/providers/llm/claude-provider.d.ts +2 -2
- package/dist/providers/llm/claude-provider.js +2 -2
- package/dist/providers/llm/openai.d.ts +2 -2
- package/dist/providers/llm/openai.js +6 -2
- package/dist/providers/llm/routing.d.ts +2 -2
- package/dist/providers/llm/routing.js +2 -2
- package/dist/providers/llm/types.d.ts +12 -2
- package/dist/providers/review-host/github-events.d.ts +9 -1
- package/dist/providers/review-host/github-events.js +34 -12
- package/dist/providers/review-host/github.d.ts +39 -2
- package/dist/providers/review-host/github.js +274 -29
- package/dist/providers/review-host/local.d.ts +8 -4
- package/dist/providers/review-host/local.js +49 -31
- package/dist/providers/review-host/patch-pages.d.ts +41 -0
- package/dist/providers/review-host/patch-pages.js +176 -0
- package/dist/providers/review-host/review-format.d.ts +19 -1
- package/dist/providers/review-host/review-format.js +38 -7
- package/dist/providers/review-host/types.d.ts +157 -2
- package/dist/providers/review-host/types.js +8 -0
- package/dist/providers/storage/gcs.d.ts +63 -0
- package/dist/providers/storage/gcs.js +69 -0
- package/dist/providers/storage/index.d.ts +15 -0
- package/dist/providers/storage/index.js +14 -0
- package/dist/providers/storage/local.d.ts +24 -0
- package/dist/providers/storage/local.js +92 -0
- package/dist/providers/storage/meter.d.ts +14 -0
- package/dist/providers/storage/meter.js +34 -0
- package/dist/providers/storage/s3.d.ts +41 -0
- package/dist/providers/storage/s3.js +107 -0
- package/dist/providers/storage/types.d.ts +37 -3
- package/dist/providers/storage/types.js +21 -4
- package/dist/providers/vcs/git.d.ts +33 -2
- package/dist/providers/vcs/git.js +324 -22
- package/dist/providers/vcs/github-remote.d.ts +26 -3
- package/dist/providers/vcs/github-remote.js +124 -14
- package/dist/providers/vcs/github.d.ts +246 -12
- package/dist/providers/vcs/github.js +521 -61
- package/dist/providers/vcs/index.js +8 -1
- package/dist/providers/vcs/pr-description.d.ts +127 -0
- package/dist/providers/vcs/pr-description.js +270 -0
- package/dist/providers/vcs/remote.d.ts +33 -1
- package/dist/providers/vcs/routing.d.ts +4 -2
- package/dist/providers/vcs/routing.js +6 -0
- package/dist/providers/vcs/types.d.ts +80 -3
- package/dist/quickstart/coding-seed.d.ts +21 -3
- package/dist/quickstart/coding-seed.js +283 -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 +6 -6
- 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/generated/zod.bundle.js +62 -1
- package/dist/sandbox/generated/zod3.bundle.js +5 -0
- package/dist/sandbox/host-functions.d.ts +35 -0
- package/dist/sandbox/host-functions.js +150 -91
- 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 +161 -0
- package/dist/sandbox/tool-capabilities.d.ts +10 -30
- package/dist/sandbox/user-tool.d.ts +14 -0
- package/dist/sandbox/user-tool.js +32 -0
- package/dist/sandbox/zod-params.d.ts +37 -5
- package/dist/sandbox/zod-params.js +181 -26
- package/dist/serve.js +25 -0
- package/dist/viewer/api-schema.d.ts +372 -2454
- package/dist/viewer/api-schema.js +29 -0
- package/dist/viewer/build-schemas.js +10 -3
- 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 +170 -0
- package/dist/wardby-bin.js +10 -0
- package/docs/README.md +7 -0
- package/docs/agent-recipes.md +114 -27
- package/docs/architecture-runtime.md +16 -1
- package/docs/code-review-agents.md +689 -45
- package/docs/coding-agent-setup.md +602 -12
- package/docs/coding-packages.md +1037 -92
- package/docs/coding-services.md +30 -13
- package/docs/coding-worker-byo-images.md +218 -6
- package/docs/coding-worker-isolation.md +173 -44
- package/docs/getting-started-gke.md +332 -129
- package/docs/getting-started-identity-provider.md +31 -11
- package/docs/getting-started.md +14 -4
- package/docs/jira-agents.md +22 -9
- package/docs/knowledge/blob-not-found-semantics.md +41 -0
- package/docs/knowledge/bounded-file-descriptor.md +52 -0
- package/docs/knowledge/delegation-admission-slots.md +52 -0
- package/docs/knowledge/http-dispatcher-bootstrap.md +61 -0
- package/docs/knowledge/import-rename-target.md +52 -0
- package/docs/knowledge/index.md +57 -0
- package/docs/knowledge/log.md +9 -0
- package/docs/knowledge/mcp-cancelled-task-precedence.md +41 -0
- package/docs/knowledge/mcp-http-prompt-discovery.md +41 -0
- package/docs/knowledge/mcp-schema-module-boundary.md +41 -0
- package/docs/knowledge/nullable-tool-uniqueness.md +52 -0
- package/docs/knowledge/overview-cli.md +74 -0
- package/docs/knowledge/overview-coding-proxy.md +72 -0
- package/docs/knowledge/overview-coding.md +72 -0
- package/docs/knowledge/overview-config.md +60 -0
- package/docs/knowledge/overview-core.md +72 -0
- package/docs/knowledge/overview-deploy.md +83 -0
- package/docs/knowledge/overview-mcp.md +61 -0
- package/docs/knowledge/overview-prisma.md +72 -0
- package/docs/knowledge/overview-providers.md +89 -0
- package/docs/knowledge/overview-sandbox.md +83 -0
- package/docs/knowledge/overview-surfaces.md +83 -0
- package/docs/knowledge/private-datastore-shared-boundary.md +54 -0
- package/docs/knowledge/proxy-settlement-projections.md +41 -0
- package/docs/knowledge/run-pricing-first-writer.md +43 -0
- package/docs/knowledge/schedule-claim-transaction.md +41 -0
- package/docs/knowledge/secret-entry-replica-contract.md +52 -0
- package/docs/knowledge/terminal-run-first-writer.md +41 -0
- package/docs/knowledge/transactional-agent-authorization.md +52 -0
- package/docs/knowledge/viewer-notify-write-isolation.md +30 -0
- package/docs/knowledge/warm-input-publication.md +30 -0
- package/docs/knowledge/workspace-staging-failure.md +54 -0
- package/docs/knowledge.md +54 -14
- package/docs/native-sandbox.md +731 -0
- package/docs/observability.md +4 -0
- package/docs/release-verification.md +9 -0
- package/docs/security-deployment.md +146 -14
- package/docs/slack-notifications.md +268 -0
- package/docs/tool-secrets.md +284 -0
- package/docs/viewer-api.md +46 -5
- package/docs/viewer-api.openapi.yml +1404 -0
- package/help/admin-viewer.md +29 -3
- package/help/agent-recipes.md +27 -0
- package/help/architecture-agent.md +11 -6
- package/help/brokered-secrets.md +79 -0
- package/help/build-claude-code-image.md +131 -0
- package/help/build-worker-image.md +6 -7
- package/help/builder-agent.md +19 -6
- package/help/code-review-agents.md +243 -10
- package/help/coding-packages.md +156 -6
- package/help/cost-attribution.md +9 -5
- package/help/creating-agents.md +14 -1
- package/help/dependency-cache.md +143 -0
- package/help/deploy-gke.md +23 -9
- package/help/deployment-targets.md +25 -14
- package/help/errors/budget-exhausted.md +187 -0
- package/help/errors/budget-group-exhausted.md +5 -1
- package/help/errors/coding-provider-not-configured.md +2 -2
- package/help/errors/coding-stream-quota-exceeded.md +33 -0
- package/help/errors/coding-workspace-disk-exceeds-limit.md +39 -0
- package/help/errors/continuation-closed.md +9 -12
- package/help/errors/database-auth-invalid.md +28 -0
- package/help/errors/delegation-conflict.md +24 -0
- package/help/errors/file-list-truncated.md +45 -0
- package/help/errors/full-read-required.md +49 -0
- package/help/errors/github-account-not-allowed.md +29 -0
- package/help/errors/graph-incomplete.md +43 -0
- package/help/errors/graph-limit.md +43 -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/merge-conflict.md +49 -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-error.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-launch-abandoned.md +21 -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 +21 -0
- package/help/errors/native-sandbox-worker-lost.md +20 -0
- package/help/errors/native-sandbox-worker-unready.md +22 -0
- package/help/errors/package-not-allowed-pending.md +52 -0
- package/help/errors/package-not-allowed.md +51 -0
- package/help/errors/package-refusals.md +43 -0
- package/help/errors/patch-cursor-malformed.md +31 -0
- package/help/errors/run-lost.md +39 -0
- package/help/errors/run-transcript-download.md +44 -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-in-value.md +69 -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/errors/target-merge.md +40 -0
- package/help/fix-merge-conflicts.md +73 -0
- package/help/getting-started.md +4 -5
- package/help/github-app-setup.md +132 -0
- package/help/github.md +40 -1
- package/help/identity-and-access.md +7 -1
- package/help/jira.md +23 -6
- package/help/knowledge.md +16 -7
- package/help/local-repositories.md +3 -4
- package/help/low-power-mode.md +131 -0
- package/help/merge-order-check.md +122 -0
- package/help/mobile-app.md +54 -0
- package/help/native-capabilities.md +29 -1
- package/help/native-sandbox.md +150 -0
- package/help/observability.md +8 -0
- package/help/operating-agents.md +5 -0
- package/help/package-audit-mode.md +91 -0
- package/help/related-pull-requests.md +80 -16
- package/help/repo-instructions-and-skills.md +91 -0
- package/help/resume-runs-locally.md +131 -0
- package/help/review-fix-rounds.md +43 -1
- package/help/sharing-agents.md +59 -0
- package/help/slack-notifications.md +76 -0
- package/help/status-comments.md +31 -0
- package/help/stop-run.md +58 -0
- package/help/troubleshooting/budgets.md +28 -0
- package/help/troubleshooting/coding-workers.md +99 -7
- package/package.json +27 -13
- 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/migrations/20261016000000_registry_approved_accepted_advisories/migration.sql +3 -0
- package/prisma/migrations/20261016000001_mark_ready_when_passing/migration.sql +3 -0
- package/prisma/migrations/20261017000000_debug_trace_max_mb/migration.sql +4 -0
- package/prisma/migrations/20261018000000_dependency_cache/migration.sql +53 -0
- package/prisma/migrations/20261019000000_claude_max_output_tokens/migration.sql +5 -0
- package/prisma/migrations/20261021000000_pr_description_hash/migration.sql +4 -0
- package/prisma/migrations/20261022000000_dependency_snapshot_validation_metadata/migration.sql +4 -0
- package/prisma/migrations/20261023000000_package_audit_mode/migration.sql +27 -0
- package/prisma/migrations/20261024000000_budget_events/migration.sql +26 -0
- package/prisma/migrations/20261025000000_cache_aware_holds/migration.sql +9 -0
- package/prisma/migrations/20261026000000_budget_overrun/migration.sql +4 -0
- package/prisma/migrations/20261027000000_coding_run_request/migration.sql +1 -0
- package/prisma/migrations/20261028000000_native_gateway_review_reads/migration.sql +6 -0
- package/prisma/migrations/20261029000000_lead_survives_deploy/migration.sql +6 -0
- package/prisma/migrations/20261030000000_local_review_covered_sha/migration.sql +2 -0
- package/prisma/migrations/20261031000000_coding_run_budget_limit/migration.sql +9 -0
- package/prisma/migrations/20261101000000_run_transcripts/migration.sql +22 -0
- package/prisma/schema.prisma +631 -146
- package/dist/sandbox/generated/zod-to-json-schema.bundle.js +0 -5
package/dist/core/runner.js
CHANGED
|
@@ -16,17 +16,26 @@
|
|
|
16
16
|
* down) lives inside the engine now — there is exactly one place that
|
|
17
17
|
* reasons about cost, not one here plus one in the engine.
|
|
18
18
|
*/
|
|
19
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
19
20
|
import { z } from "zod";
|
|
20
|
-
import {
|
|
21
|
+
import { Prisma } from "#prisma";
|
|
22
|
+
import { RunOwnershipLostError, STEPS_CHANGED_REASON, StepReplayMismatchError, runStepInline, } from "../providers/engine/types.js";
|
|
21
23
|
import { isLlmEffort } from "../providers/llm/types.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
+
import { createPrivilegedHost } from "../sandbox/host-functions.js";
|
|
25
|
+
import { consoleRedactionValues } from "../sandbox/secret-broker.js";
|
|
26
|
+
import { runUserToolCall } from "../sandbox/user-tool.js";
|
|
27
|
+
import { logUnexpectedError, runSandboxedEngine, SandboxRunCancelledError, } from "../native-worker/gateway.js";
|
|
28
|
+
import { NATIVE_WORKER_PROTOCOL_VERSION } from "../native-worker/protocol.js";
|
|
24
29
|
import { asStringArray, asPrefixMap } from "../sandbox/tool-capabilities.js";
|
|
25
30
|
import { scopeDatastore } from "../providers/datastore/scoped.js";
|
|
26
31
|
import { buildSecretsAccessor, scopeSecretsAccessor } from "./secrets.js";
|
|
32
|
+
import { loadAgentSecretValues, maskNamedSecrets, POSTING_TOOL_NAMES, SECRET_CHECK_UNAVAILABLE_MESSAGE, WITHHELD_TEXT_NOTICE, } from "./secret-guard.js";
|
|
27
33
|
import { buildSharedDatastoreAccessor, scopeSharedDatastoreAccessor } from "./datastores.js";
|
|
28
34
|
import { effectiveBudgetForRun } from "./budget-groups.js";
|
|
35
|
+
import { stopChildrenOfLostRun } from "./lost-run-children.js";
|
|
29
36
|
import { createDelegationSiblings } from "./delegation-siblings.js";
|
|
37
|
+
import { findDelegationChild } from "./delegation-child.js";
|
|
38
|
+
import { DRIVABLE } from "./run-status.js";
|
|
30
39
|
import { createSerialGate } from "./serial-gate.js";
|
|
31
40
|
import { withRunHeartbeat } from "./run-heartbeat.js";
|
|
32
41
|
import { ContinuationRefusedError, dispatchRun } from "./dispatch.js";
|
|
@@ -36,6 +45,7 @@ import { DELEGATE_TOOL_PREFIX, duplicateToolNames } from "./tool-names.js";
|
|
|
36
45
|
import { SUBAGENT_MEMORY_GET_TOOL, PARENT_MEMORY_GET_TOOL, handleSubAgentMemoryGet, handleParentMemoryGet, } from "./subagent-memory-tools.js";
|
|
37
46
|
import { prisma as defaultDb } from "./db.js";
|
|
38
47
|
import { logger } from "./logger.js";
|
|
48
|
+
import { CHILD_RUN_FAILURE_CODES, declaredCodeOf } from "./error-code.js";
|
|
39
49
|
import { loadCodingConcurrencyConfig } from "../config/providers.js";
|
|
40
50
|
import { REVIEW_HOST_TOOL_DEFS, REVIEW_HOST_TOOL_NAMES, handleReviewHostTool, } from "./review-host-tools.js";
|
|
41
51
|
import { closeOpenHostCheck } from "./review-host-checks.js";
|
|
@@ -47,15 +57,21 @@ import { ISSUE_TRACKER_TOOL_DEFS, ISSUE_TRACKER_TOOL_NAMES, handleIssueTrackerTo
|
|
|
47
57
|
import { completeIssueStatus } from "./issue-status.js";
|
|
48
58
|
import { fileIssue } from "./issue-dedupe.js";
|
|
49
59
|
import { fileSelfDefect } from "./self-defects.js";
|
|
60
|
+
import { emitRunFinishedEvents } from "./workflow-run-events.js";
|
|
50
61
|
import { startReviewFixAfterReview } from "./review-fix.js";
|
|
51
62
|
import { updateRelatedPullRequests } from "./related-pull-requests.js";
|
|
63
|
+
import { syncMergeOrderChecksAfterRun } from "./merge-order-check.js";
|
|
64
|
+
import { markReadyAfterRun } from "./mark-ready.js";
|
|
65
|
+
import { startDeferredForRequest } from "./host-events.js";
|
|
52
66
|
import { recordNativeModelUsage } from "./model-usage.js";
|
|
53
67
|
import { pinNativeRunPricing } from "./run-pricing.js";
|
|
54
68
|
import { RoutingLlmProvider } from "../providers/llm/routing.js";
|
|
55
69
|
import { ModelUnavailableError } from "../providers/llm/catalog-types.js";
|
|
56
70
|
import { trackRun } from "./in-flight-runs.js";
|
|
71
|
+
import { ReviewReadLedger } from "./review-reads.js";
|
|
57
72
|
import { createRepoAccessGate, requiredLevel } from "./repo-access.js";
|
|
58
|
-
|
|
73
|
+
import { loadBudgetEvent, ownerBudgetSentence } from "./budget-events.js";
|
|
74
|
+
export const runnerLog = logger.child({ module: "runner" });
|
|
59
75
|
/**
|
|
60
76
|
* Sub-agent dispatch uses one synthetic tool per declared child, named by the
|
|
61
77
|
* boundName it's attached
|
|
@@ -115,20 +131,177 @@ function delegateToolDef(boundName, parallel) {
|
|
|
115
131
|
type: "string",
|
|
116
132
|
description: "Coding sub-agents only. The run id of a prior coding run whose branch/PR this dispatch should push a new commit onto, instead of opening a fresh one — use when the task is a revision to an existing PR (a plan/spec update, or a code-review follow-up), even across a different sub-agent than the one that opened it.",
|
|
117
133
|
},
|
|
134
|
+
mergeOrder: {
|
|
135
|
+
type: "integer",
|
|
136
|
+
minimum: 1,
|
|
137
|
+
maximum: 99,
|
|
138
|
+
description: "Coding sub-agents only. This repository's step in the merge order of the whole request (1 = merge first). " +
|
|
139
|
+
"Give dependencies lower numbers than the changes that use them (e.g. a service 1, the API that calls it 2, the client 3); " +
|
|
140
|
+
"equal numbers have no order between them. Set it whenever one change spans several repositories.",
|
|
141
|
+
},
|
|
118
142
|
},
|
|
119
143
|
required: ["task"],
|
|
120
144
|
additionalProperties: false,
|
|
121
145
|
},
|
|
122
146
|
};
|
|
123
147
|
}
|
|
148
|
+
/** Every mergeOrder validation failure (wrong type, non-integer, or out of range) names the same bound. */
|
|
149
|
+
const MERGE_ORDER_RANGE_MESSAGE = "mergeOrder must be an integer from 1 to 99.";
|
|
124
150
|
const DelegateArgs = z
|
|
125
151
|
.object({
|
|
126
152
|
task: z.string(),
|
|
127
153
|
datastoreRef: z.object({ name: z.string(), key: z.string() }).strict().optional(),
|
|
128
154
|
grantParentMemoryKeys: z.array(z.string()).optional(),
|
|
129
155
|
continuePriorRun: z.string().optional(),
|
|
156
|
+
mergeOrder: z
|
|
157
|
+
.number({ error: MERGE_ORDER_RANGE_MESSAGE })
|
|
158
|
+
.int(MERGE_ORDER_RANGE_MESSAGE)
|
|
159
|
+
.min(1, MERGE_ORDER_RANGE_MESSAGE)
|
|
160
|
+
.max(99, MERGE_ORDER_RANGE_MESSAGE)
|
|
161
|
+
.optional(),
|
|
130
162
|
})
|
|
131
163
|
.strict();
|
|
164
|
+
/** The tool result note for a native child's delegation when the caller passed mergeOrder: it does not apply. */
|
|
165
|
+
const MERGE_ORDER_IGNORED_NOTE = "mergeOrder applies only to coding sub-agents; ignored";
|
|
166
|
+
/** A delegate_to_* call's arguments, or the tool result refusing them. */
|
|
167
|
+
export function parseDelegateArgs(argsJson) {
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(argsJson || "{}");
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return {
|
|
174
|
+
refusal: JSON.stringify({
|
|
175
|
+
error: "invalid_arguments_json",
|
|
176
|
+
message: err instanceof Error ? err.message : String(err),
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
return { args: DelegateArgs.parse(parsed) };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
if (err instanceof z.ZodError) {
|
|
185
|
+
return {
|
|
186
|
+
refusal: JSON.stringify({
|
|
187
|
+
error: "validation_failed",
|
|
188
|
+
message: err.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "),
|
|
189
|
+
}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* At most `limit` dispatches per run (default one: a classifier deciding plan-vs-implement
|
|
197
|
+
* commits to exactly one child), and never the same child twice, so a retry can't leave two
|
|
198
|
+
* children racing on the same task. A delegation waiting for budget holds its place.
|
|
199
|
+
*/
|
|
200
|
+
export function delegationLimitRefusal(input) {
|
|
201
|
+
const { priorChildAgentIds, waitingChildAgentIds, childAgentId, boundName, limit } = input;
|
|
202
|
+
if (priorChildAgentIds.includes(childAgentId) || waitingChildAgentIds.includes(childAgentId)) {
|
|
203
|
+
return JSON.stringify({
|
|
204
|
+
error: "already_dispatched",
|
|
205
|
+
message: `This run already delegated to "${boundName}"; a run delegates to each sub-agent at most once.`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (priorChildAgentIds.length < limit && priorChildAgentIds.length + waitingChildAgentIds.length >= limit) {
|
|
209
|
+
// Only a waiting delegation (parallelDelegations) holds the remaining place.
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
error: "already_dispatched",
|
|
212
|
+
message: `This run reached its limit of ${limit} delegations: ${priorChildAgentIds.length} made and ${waitingChildAgentIds.length} waiting for budget.`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (priorChildAgentIds.length >= limit) {
|
|
216
|
+
return JSON.stringify({
|
|
217
|
+
error: "already_dispatched",
|
|
218
|
+
message: limit === 1
|
|
219
|
+
? "This run already delegated to a sub-agent; only one delegation is allowed per run."
|
|
220
|
+
: `This run already made ${limit} delegations, the most this agent allows per run.`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const DELEGATION_CHILD_SELECT = {
|
|
226
|
+
id: true,
|
|
227
|
+
kind: true,
|
|
228
|
+
budgetGroupId: true,
|
|
229
|
+
budgetUsd: true,
|
|
230
|
+
ownerId: true,
|
|
231
|
+
nativeExecutionMode: true,
|
|
232
|
+
codingProfile: { select: { allowWebhookTaskOverride: true, timeoutSec: true } },
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* The live authorization of one delegation, and the child agent as it is now. Owners are re-read
|
|
236
|
+
* on every delegation (resource-sharing grants spec §3.4.4, N1): the child must have the parent's
|
|
237
|
+
* current owner, or that owner must still hold execute on the child. A revoked grant or a
|
|
238
|
+
* make_owner transfer stops the very next call.
|
|
239
|
+
*/
|
|
240
|
+
export async function authorizeDelegation(db, parentAgentId, edge, boundName, args) {
|
|
241
|
+
const [parentNow, childAgent] = await Promise.all([
|
|
242
|
+
db.agent.findUnique({ where: { id: parentAgentId }, select: { ownerId: true } }),
|
|
243
|
+
db.agent.findUniqueOrThrow({ where: { id: edge.childAgentId }, select: DELEGATION_CHILD_SELECT }),
|
|
244
|
+
]);
|
|
245
|
+
const parentOwnerId = parentNow?.ownerId ?? null;
|
|
246
|
+
const childOwnerId = childAgent.ownerId ?? null;
|
|
247
|
+
if (!parentNow ||
|
|
248
|
+
!(await canDelegate(db, { ownerId: parentOwnerId }, { id: childAgent.id, ownerId: childOwnerId }))) {
|
|
249
|
+
return {
|
|
250
|
+
refusal: JSON.stringify({
|
|
251
|
+
error: "subagent_not_authorized",
|
|
252
|
+
message: `The "${boundName}" sub-agent belongs to another owner who has not given this agent's owner execute access to it.`,
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (parentOwnerId !== childOwnerId) {
|
|
257
|
+
// Across owners the edge carries execute and nothing more: no
|
|
258
|
+
// model-chosen memory grant, no continuation of another run's PR,
|
|
259
|
+
// and no task text the caller couldn't give the child directly --
|
|
260
|
+
// the trigger_agent rule (review I3): a coding child only with its
|
|
261
|
+
// owner's allowWebhookTaskOverride opt-in, a native child never
|
|
262
|
+
// (it runs its owner's fixed prompt; pass an empty task).
|
|
263
|
+
const refusal = (args.grantParentMemoryKeys?.length ?? 0) > 0
|
|
264
|
+
? "grantParentMemoryKeys is only allowed when the sub-agent has the same owner."
|
|
265
|
+
: args.continuePriorRun !== undefined
|
|
266
|
+
? "continuePriorRun is only allowed when the sub-agent has the same owner."
|
|
267
|
+
: childAgent.kind === "coding" && !childAgent.codingProfile?.allowWebhookTaskOverride
|
|
268
|
+
? "This coding sub-agent belongs to another owner and does not accept task text from others (allowWebhookTaskOverride)."
|
|
269
|
+
: childAgent.kind !== "coding" && (args.task.trim() !== "" || args.datastoreRef !== undefined)
|
|
270
|
+
? 'This sub-agent belongs to another owner and runs only its own instructions: delegate with task "" and no datastoreRef.'
|
|
271
|
+
: null;
|
|
272
|
+
if (refusal)
|
|
273
|
+
return { refusal: JSON.stringify({ error: "cross_owner_not_allowed", message: refusal }) };
|
|
274
|
+
}
|
|
275
|
+
if (args.continuePriorRun !== undefined && childAgent.kind !== "coding") {
|
|
276
|
+
return {
|
|
277
|
+
refusal: JSON.stringify({
|
|
278
|
+
error: "continuation_requires_coding_agent",
|
|
279
|
+
message: "continuePriorRun is only supported when the sub-agent is coding-kind.",
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return { childAgent };
|
|
284
|
+
}
|
|
285
|
+
/** A native child's task text: the delegated task, with any datastore reference as a note. */
|
|
286
|
+
export function delegationTaskOverride(args) {
|
|
287
|
+
return args.datastoreRef
|
|
288
|
+
? `${args.task}\n\n[Referenced datastore: name="${args.datastoreRef.name}", key="${args.datastoreRef.key}" — use your datastore tools to read it.]`
|
|
289
|
+
: args.task.trim() !== ""
|
|
290
|
+
? args.task
|
|
291
|
+
: undefined;
|
|
292
|
+
}
|
|
293
|
+
/** The tool result a native child's terminal run reports to its parent. `note` surfaces e.g. an ignored mergeOrder. */
|
|
294
|
+
export function nativeChildResult(run, note) {
|
|
295
|
+
return JSON.stringify({
|
|
296
|
+
status: run.status,
|
|
297
|
+
finalText: run.finalText,
|
|
298
|
+
costUsd: Number(run.costUsd),
|
|
299
|
+
tokensIn: run.tokensIn,
|
|
300
|
+
tokensOut: run.tokensOut,
|
|
301
|
+
...(run.error ? { error: run.error } : {}),
|
|
302
|
+
...(note ? { note } : {}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
132
305
|
/**
|
|
133
306
|
* The composed review hosts, or undefined when there are none (no GitHub App
|
|
134
307
|
* configured yields an empty registry). Undefined means the run never touches
|
|
@@ -161,17 +334,7 @@ function toIssueProjectLink(row) {
|
|
|
161
334
|
maxNewIssuesPerRun: row.maxNewIssuesPerRun ?? null,
|
|
162
335
|
};
|
|
163
336
|
}
|
|
164
|
-
|
|
165
|
-
* The two states a Run can still be driven out of. Every write `executeRun`
|
|
166
|
-
* makes is filtered on these: a durable executor can have two attempts of the
|
|
167
|
-
* same run in flight at once (a still-live original parked in a slow LLM step
|
|
168
|
-
* and an adopted attempt the reconciler resumed elsewhere), and whichever
|
|
169
|
-
* reaches a terminal state first must win. A conditional `updateMany` makes
|
|
170
|
-
* that a database-level CAS rather than a race — the loser's WHERE matches
|
|
171
|
-
* zero rows. It also stops a *later* attempt resurrecting a run the
|
|
172
|
-
* reconciler already reaped: a `lost` row stays `lost`.
|
|
173
|
-
*/
|
|
174
|
-
const DRIVABLE = ["pending", "running"];
|
|
337
|
+
// DRIVABLE (the two states a Run can still be driven out of) lives in run-status.ts.
|
|
175
338
|
/**
|
|
176
339
|
* Raised when a run's step boundary observed a cancellation (DbosExecutor's
|
|
177
340
|
* `stop`). `executeRun`'s backstop persists these as `cancelled` with the
|
|
@@ -183,6 +346,15 @@ export class RunCancelledError extends Error {
|
|
|
183
346
|
this.name = "RunCancelledError";
|
|
184
347
|
}
|
|
185
348
|
}
|
|
349
|
+
/** Re-exported beside RunCancelledError: thrown by a step runner when another attempt owns the run. */
|
|
350
|
+
export { RunOwnershipLostError };
|
|
351
|
+
/**
|
|
352
|
+
* The one log line for an attempt that lost ownership of its run. Warn, not info: each one also
|
|
353
|
+
* means a discarded step's spend and an owner that stalled long enough to be adopted.
|
|
354
|
+
*/
|
|
355
|
+
function logRunOwnershipLost(runId, err) {
|
|
356
|
+
runnerLog.warn({ runId, step: err.step, event: "run_ownership_lost", discardedUsage: err.discardedUsage }, "run_ownership_lost: another execution owns this run; stopping without a terminal write (this attempt's last step is discarded)");
|
|
357
|
+
}
|
|
186
358
|
/**
|
|
187
359
|
* Terminal write + read-back. The write is conditional (see DRIVABLE), so
|
|
188
360
|
* the returned row is the run's real state — this attempt's result if it won
|
|
@@ -196,6 +368,46 @@ async function finishRunClaimed(db, runId, data) {
|
|
|
196
368
|
const updated = await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data });
|
|
197
369
|
return { run: await db.run.findUniqueOrThrow({ where: { id: runId } }), claimed: updated.count > 0 };
|
|
198
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* A step runner made to notice a stop. Before each step this reads the run's own row: once anything
|
|
373
|
+
* else has made it terminal (stop_run, an MCP task's cancel, a parent run's stop, the reconciler),
|
|
374
|
+
* the run stops at this boundary with a RunCancelledError. The terminal row is never rewritten
|
|
375
|
+
* (finishRunClaimed matches zero rows), so the stop keeps its reason. Inline runs need it because
|
|
376
|
+
* nothing else interrupts their steps. DbosExecutor wraps its own step runner in it too: DBOS stops
|
|
377
|
+
* only a workflow it was told to cancel, and stop() can name only the workflow the run records.
|
|
378
|
+
*/
|
|
379
|
+
export function stopAwareStep(db, runId, inner = runStepInline, options = {}) {
|
|
380
|
+
const check = async (name, own) => {
|
|
381
|
+
const read = db.run.findUnique({ where: { id: runId }, select: { status: true, executionWorkflowId: true } });
|
|
382
|
+
const row = options.failOpen ? await read.catch(() => null) : await read;
|
|
383
|
+
if (row?.executionWorkflowId && own && options.ownership?.forkedFrom(row.executionWorkflowId, own)) {
|
|
384
|
+
throw new RunOwnershipLostError(name);
|
|
385
|
+
}
|
|
386
|
+
if (row && !DRIVABLE.includes(row.status)) {
|
|
387
|
+
throw new RunCancelledError(`Run ${row.status}: stopped before step "${name}".`);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
// Each check waits for the one before it, so steps reach `inner` in the order they were called,
|
|
391
|
+
// as they did without a read in front: a durable runner numbers a step when it is called (DBOS
|
|
392
|
+
// assigns its function id then), and a replay of concurrent delegations matches steps by number.
|
|
393
|
+
let previous = Promise.resolve();
|
|
394
|
+
return async (name, fn) => {
|
|
395
|
+
const own = options.ownership?.workflowId();
|
|
396
|
+
const checked = previous.then(() => check(name, own));
|
|
397
|
+
previous = checked.catch(() => undefined);
|
|
398
|
+
await checked;
|
|
399
|
+
return inner(name, fn);
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Why a sandbox-mode native run is not executed here. The runner only ever
|
|
404
|
+
* executes native runs in the control plane; a run whose snapshot says
|
|
405
|
+
* `sandbox` reaches it only when no native sandbox executor is composed in
|
|
406
|
+
* (RoutingExecutor sends it to the native executor), and must fail closed
|
|
407
|
+
* rather than run unisolated.
|
|
408
|
+
*/
|
|
409
|
+
export const NATIVE_SANDBOX_NOT_CONFIGURED = "native_sandbox_unavailable: This agent runs with nativeExecutionMode=sandbox, but this deployment has no native " +
|
|
410
|
+
"sandbox executor configured, so the run was not started. Switch the agent back to control-plane mode to run it here.";
|
|
199
411
|
/**
|
|
200
412
|
* Why a coding run cannot start in this process: runs reach the runner only
|
|
201
413
|
* when no container executor is composed in (JOB_LAUNCHER=local, e.g. every
|
|
@@ -210,12 +422,22 @@ const CODING_EXECUTOR_NOT_CONFIGURED = "Coding agents need a container executor,
|
|
|
210
422
|
* write the terminal row, and the queue timeout only fires on a drain tick.
|
|
211
423
|
*/
|
|
212
424
|
export const CODING_CHILD_WAIT_GRACE_SEC = 60;
|
|
425
|
+
/**
|
|
426
|
+
* How long a parent waits for a sandbox-mode native child before stopping it. Native runs have
|
|
427
|
+
* no run timeout of their own; until the sandbox executor brings a per-run deadline, this bounds
|
|
428
|
+
* a parent left waiting on a child that never finishes.
|
|
429
|
+
*/
|
|
430
|
+
export const SANDBOX_CHILD_WAIT_SEC = 60 * 60;
|
|
431
|
+
/** The leading snake_case code of an error message (`code: sentence`); messages can carry internals. */
|
|
213
432
|
/**
|
|
214
433
|
* What a coding sub-agent's run tells the parent run's model. A run refused
|
|
215
434
|
* over its services carries the host sentence (never the raw Run.error), so
|
|
216
|
-
* the parent can pass it on — for a mention, that is the requester.
|
|
435
|
+
* the parent can pass it on — for a mention, that is the requester. A failed
|
|
436
|
+
* run carries only its error code, so the parent can say why without seeing internals.
|
|
217
437
|
*/
|
|
218
|
-
export function codingChildResult(run, codingResult
|
|
438
|
+
export function codingChildResult(run, codingResult,
|
|
439
|
+
/** The child's recorded budget refusal (loadBudgetEvent), if any. */
|
|
440
|
+
budgetEvent) {
|
|
219
441
|
// Checked before the "refused" branch: a continuation whose PR closed
|
|
220
442
|
// before the run ever spent anything is stored as "refused" (a preflight
|
|
221
443
|
// refusal, container.ts), not "failed" -- but it is the same
|
|
@@ -226,6 +448,10 @@ export function codingChildResult(run, codingResult) {
|
|
|
226
448
|
: run.status === "refused"
|
|
227
449
|
? serviceRefusalSentence(run.error)
|
|
228
450
|
: null;
|
|
451
|
+
const errorCode = run.status === "failed" ? declaredCodeOf(run.error, CHILD_RUN_FAILURE_CODES) : undefined;
|
|
452
|
+
const budget = budgetEvent && (run.status === "budget_exhausted" || run.status === "refused")
|
|
453
|
+
? ownerBudgetSentence(budgetEvent.event, budgetEvent.agentName)
|
|
454
|
+
: undefined;
|
|
229
455
|
return JSON.stringify({
|
|
230
456
|
status: run.status,
|
|
231
457
|
finalText: run.finalText,
|
|
@@ -233,6 +459,8 @@ export function codingChildResult(run, codingResult) {
|
|
|
233
459
|
tokensIn: run.tokensIn,
|
|
234
460
|
tokensOut: run.tokensOut,
|
|
235
461
|
...(refusal ? { refusal } : {}),
|
|
462
|
+
...(errorCode ? { errorCode } : {}),
|
|
463
|
+
...(budget ? { budget } : {}),
|
|
236
464
|
...pullRequestOf(codingResult),
|
|
237
465
|
});
|
|
238
466
|
}
|
|
@@ -306,40 +534,27 @@ export async function createRun(db, agentName, trigger = "manual") {
|
|
|
306
534
|
if (agent.kind === "coding") {
|
|
307
535
|
throw new Error(CODING_EXECUTOR_NOT_CONFIGURED);
|
|
308
536
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
export async function executeRun(runId, providers, db = defaultDb, onText, step = runStepInline) {
|
|
313
|
-
// Counted so a shutdown waits for this run instead of abandoning it (core/in-flight-runs.ts).
|
|
314
|
-
return trackRun(runId, () => executeTrackedRun(runId, providers, db, onText, step));
|
|
315
|
-
}
|
|
316
|
-
async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
317
|
-
const existingRun = await db.run.findUnique({ where: { id: runId } });
|
|
318
|
-
if (!existingRun) {
|
|
319
|
-
throw new Error(`Unknown run "${runId}".`);
|
|
537
|
+
// Refused here, before any row: a foreground run executes inline, never in a sandbox.
|
|
538
|
+
if (agent.nativeExecutionMode === "sandbox") {
|
|
539
|
+
throw new Error(NATIVE_SANDBOX_NOT_CONFIGURED);
|
|
320
540
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const issueTrackers =
|
|
337
|
-
|
|
338
|
-
// or its budget group may have changed since first execution. The
|
|
339
|
-
// engine's control flow depends on budgetUsd and maxTurns, so they must
|
|
340
|
-
// be pinned to the values seen on first execution or the replay's step
|
|
341
|
-
// order diverges from the record.
|
|
342
|
-
const loadedOrUnavailable = await step("load", async () => {
|
|
541
|
+
return db.run.create({ data: { agentId: agent.id, trigger, nativeExecutionMode: agent.nativeExecutionMode } });
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* The trusted load of a native run: the agent, its attached tools and their
|
|
545
|
+
* consented capabilities, the pinned catalog entry, the effective budget,
|
|
546
|
+
* sub-agent edges, and repository/issue links. Shared by in-process execution
|
|
547
|
+
* and the native sandbox gateway, which hands the worker this result.
|
|
548
|
+
*
|
|
549
|
+
* Pinned in one checkpointed step: on replay after a crash, the agent row
|
|
550
|
+
* or its budget group may have changed since first execution. The
|
|
551
|
+
* engine's control flow depends on budgetUsd and maxTurns, so they must
|
|
552
|
+
* be pinned to the values seen on first execution or the replay's step
|
|
553
|
+
* order diverges from the record.
|
|
554
|
+
*/
|
|
555
|
+
export async function loadNativeRun(options) {
|
|
556
|
+
const { runId, existingRun, providers, db, step, reviewHosts, issueTrackers } = options;
|
|
557
|
+
return step("load", async () => {
|
|
343
558
|
const agent = await db.agent.findUnique({ where: { id: existingRun.agentId } });
|
|
344
559
|
if (!agent) {
|
|
345
560
|
throw new Error(`Run "${runId}" references missing agent "${existingRun.agentId}".`);
|
|
@@ -486,53 +701,181 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
486
701
|
(err instanceof Error && err.message.startsWith("model_unavailable:"))) {
|
|
487
702
|
return { unavailable: err.message };
|
|
488
703
|
}
|
|
704
|
+
// Thrown out of executeRun ahead of its backstop, so logged here instead.
|
|
705
|
+
if (err instanceof RunOwnershipLostError)
|
|
706
|
+
logRunOwnershipLost(runId, err);
|
|
489
707
|
throw err;
|
|
490
708
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Every value of the secrets `names` (readable or brokered; a SigV4 secret's key and token alone
|
|
712
|
+
* too, the same set the in-process host redacts), as a redaction list. Rejects when ANY read
|
|
713
|
+
* fails: a list missing one secret would let that secret through, so a caller treats a rejection
|
|
714
|
+
* as "no list" and fails closed. A secret with no value contributes nothing.
|
|
715
|
+
*/
|
|
716
|
+
export async function readRedactionValues(accessor, names) {
|
|
717
|
+
const values = await Promise.all(names.map((secret) => accessor.resolve
|
|
718
|
+
? accessor.resolve(secret).then((e) => (e ? consoleRedactionValues(e) : []))
|
|
719
|
+
: accessor.get(secret).then((v) => (v === undefined ? [] : [v]))));
|
|
720
|
+
return values.flat().filter((value) => typeof value === "string");
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* One attempt's tool surface for a native run. `builtinHandler(name)` is the
|
|
724
|
+
* single source of truth for which names are trusted built-ins (memory,
|
|
725
|
+
* repo_*, jira_*, sub-agent/parent memory, delegate_to_*) and returns undefined
|
|
726
|
+
* for everything else, which `runUserTool` runs in the WASM sandbox. Per-attempt
|
|
727
|
+
* state (the delegation gate, siblings, issue creation counters) lives here.
|
|
728
|
+
*/
|
|
729
|
+
export function createNativeRunTools(options) {
|
|
730
|
+
const { runId, existingRun, loaded, providers, db, reviewHosts, repoAccess, issueTrackers } = options;
|
|
731
|
+
const toolsByName = new Map(Object.entries(loaded.toolsByName));
|
|
732
|
+
const secretsAccessor = buildSecretsAccessor(loaded.agentId, providers.secrets, db);
|
|
733
|
+
const sharedDatastoreAccessor = buildSharedDatastoreAccessor(loaded.agentId, providers.datastore, db);
|
|
734
|
+
// jira_create_issue's per-run cap counter: shared by every tool call of this attempt (a resumed attempt
|
|
735
|
+
// starts a fresh one, floored by the run's recorded fingerprint creates).
|
|
736
|
+
const issueCreationCounters = new Map();
|
|
737
|
+
// What this attempt's review tools have read, for repo_publish_review's unread_files gate.
|
|
738
|
+
const reviewReads = options.reviewReads ?? new ReviewReadLedger();
|
|
739
|
+
// One per attempt: admits this run's delegations one at a time (see the delegate branch).
|
|
740
|
+
const delegationGate = createSerialGate();
|
|
741
|
+
// The children this attempt's delegations have running, and the sub-agents whose delegation is
|
|
742
|
+
// waiting for one of them to free budget (parallelDelegations): a waiting delegation keeps its
|
|
743
|
+
// place in the same-child and limit checks while the gate is open to the others.
|
|
744
|
+
const delegationSiblings = createDelegationSiblings();
|
|
745
|
+
const waitingDelegations = new Set();
|
|
746
|
+
/**
|
|
747
|
+
* Waits on a dispatched coding or sandbox-mode native child until it is terminal, its wait
|
|
748
|
+
* bound passes (it is then stopped), or this run is cancelled, and returns what the model sees.
|
|
749
|
+
* The bound is measured from the child's creation, so a re-executed delegate step (a resume, or
|
|
750
|
+
* a takeover under a new version) never extends how long this run waits for it.
|
|
751
|
+
*/
|
|
752
|
+
const awaitDelegatedChild = async (wait) => {
|
|
753
|
+
const { executor, childRunId, kind, boundName } = wait;
|
|
754
|
+
const startedAt = Date.now();
|
|
755
|
+
let boundMs;
|
|
756
|
+
if (kind === "coding") {
|
|
757
|
+
const codingRun = await db.codingRun.findUnique({
|
|
758
|
+
where: { runId: childRunId },
|
|
759
|
+
select: { timeoutSec: true },
|
|
760
|
+
});
|
|
761
|
+
const { queueTimeoutSec } = loadCodingConcurrencyConfig();
|
|
762
|
+
boundMs = (queueTimeoutSec + (codingRun?.timeoutSec ?? 0) + CODING_CHILD_WAIT_GRACE_SEC) * 1000;
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
boundMs = SANDBOX_CHILD_WAIT_SEC * 1000;
|
|
766
|
+
}
|
|
767
|
+
const elapsedMs = Math.max(0, Date.now() - wait.createdAtMs);
|
|
768
|
+
// Executor.start may resolve before the child is terminal, so wait on the row.
|
|
769
|
+
const waited = await waitForCodingChild({
|
|
770
|
+
db,
|
|
771
|
+
executor,
|
|
772
|
+
childRunId,
|
|
773
|
+
parentRunId: runId,
|
|
774
|
+
boundMs: Math.max(0, boundMs - elapsedMs),
|
|
775
|
+
});
|
|
776
|
+
wait.onSettled();
|
|
777
|
+
runnerLog.info({
|
|
778
|
+
runId,
|
|
779
|
+
childRunId,
|
|
780
|
+
boundName,
|
|
781
|
+
outcome: waited.kind === "terminal" ? waited.run.status : waited.kind,
|
|
782
|
+
durationMs: Date.now() - startedAt,
|
|
783
|
+
}, "delegation finished");
|
|
784
|
+
if (waited.kind === "timed_out") {
|
|
785
|
+
return JSON.stringify({
|
|
786
|
+
error: "subagent_wait_timed_out",
|
|
787
|
+
runId: childRunId,
|
|
788
|
+
message: kind === "coding"
|
|
789
|
+
? "The coding sub-agent did not finish within its queue timeout plus run timeout; it was stopped and produced no result."
|
|
790
|
+
: "The sandbox sub-agent did not finish in time; it was stopped and produced no result.",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
if (waited.kind === "parent_cancelled") {
|
|
794
|
+
return JSON.stringify({
|
|
795
|
+
error: "parent_cancelled",
|
|
796
|
+
runId: childRunId,
|
|
797
|
+
message: kind === "coding"
|
|
798
|
+
? "This run was cancelled while waiting for the coding sub-agent; the sub-agent was stopped."
|
|
799
|
+
: "This run was cancelled while waiting for the sandbox sub-agent; the sub-agent was stopped.",
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
if (kind === "coding") {
|
|
803
|
+
const stored = await db.codingRun
|
|
804
|
+
.findUnique({ where: { runId: childRunId }, select: { result: true } })
|
|
805
|
+
.catch((err) => {
|
|
806
|
+
runnerLog.warn({ err, childRunId }, "could not read the coding sub-run's result");
|
|
807
|
+
return null;
|
|
808
|
+
});
|
|
809
|
+
return codingChildResult(waited.run, stored?.result, await loadBudgetEvent(db, childRunId));
|
|
810
|
+
}
|
|
811
|
+
return nativeChildResult(waited.run, wait.mergeOrderNote);
|
|
502
812
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
813
|
+
/**
|
|
814
|
+
* A re-executed delegate call whose child already exists (findDelegationChild): wait on it
|
|
815
|
+
* again rather than refuse it as a duplicate. A control-plane native child ran inline in the
|
|
816
|
+
* attempt that died, so nothing drives it any more: it is stopped (or its result returned, if
|
|
817
|
+
* it had finished) and no new child is started.
|
|
818
|
+
*/
|
|
819
|
+
const reattachDelegatedChild = async (child, boundName, argsJson) => {
|
|
820
|
+
const row = await db.run.findUniqueOrThrow({ where: { id: child.id } });
|
|
821
|
+
const childAgentRow = await db.agent.findUnique({ where: { id: row.agentId }, select: { kind: true } });
|
|
822
|
+
const parsedArgs = parseDelegateArgs(argsJson);
|
|
823
|
+
const mergeOrderNote = "args" in parsedArgs && parsedArgs.args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined;
|
|
824
|
+
const coding = childAgentRow?.kind === "coding";
|
|
825
|
+
if (!coding && row.nativeExecutionMode !== "sandbox") {
|
|
826
|
+
if (!DRIVABLE.includes(row.status))
|
|
827
|
+
return nativeChildResult(row, mergeOrderNote);
|
|
828
|
+
await providers.executor?.stop(child.id, "parent resumed without it").catch((err) => {
|
|
829
|
+
runnerLog.warn({ err, childRunId: child.id, runId }, "failed to stop an interrupted sub-agent run");
|
|
830
|
+
});
|
|
831
|
+
return JSON.stringify({
|
|
832
|
+
error: "subagent_interrupted",
|
|
833
|
+
message: "The sub-agent was interrupted by a restart and cannot be resumed; no new sub-agent run was started.",
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (!providers.executor) {
|
|
837
|
+
return coding
|
|
838
|
+
? JSON.stringify({
|
|
839
|
+
error: "coding_dispatch_unavailable",
|
|
840
|
+
message: "This execution context has no Executor wired in, so a coding-kind sub-agent cannot be dispatched.",
|
|
841
|
+
})
|
|
842
|
+
: JSON.stringify({
|
|
843
|
+
error: "sandbox_dispatch_unavailable",
|
|
844
|
+
message: "This execution context has no Executor wired in, so a sandbox-mode sub-agent cannot be dispatched.",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
// Counted as a running sibling while it is waited on, as a fresh dispatch is.
|
|
848
|
+
const finishSibling = delegationSiblings.start();
|
|
849
|
+
try {
|
|
850
|
+
return await awaitDelegatedChild({
|
|
851
|
+
executor: providers.executor,
|
|
852
|
+
childRunId: child.id,
|
|
853
|
+
createdAtMs: child.createdAt.getTime(),
|
|
854
|
+
kind: coding ? "coding" : "native-sandbox",
|
|
855
|
+
boundName,
|
|
856
|
+
mergeOrderNote: coding ? undefined : mergeOrderNote,
|
|
857
|
+
onSettled: finishSibling,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
finishSibling();
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
const unguardedBuiltinHandler = (name) => {
|
|
865
|
+
if (loaded.memoryEnabled && MEMORY_TOOL_NAMES.has(name)) {
|
|
866
|
+
return async (argsJson) => {
|
|
528
867
|
return handleMemoryTool(name, argsJson, loaded.agentId, providers.memory);
|
|
529
|
-
}
|
|
530
|
-
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (REVIEW_HOST_TOOL_NAMES.has(name) && loaded.repositoryLinks.length > 0 && reviewHosts && repoAccess) {
|
|
871
|
+
return async (argsJson) => {
|
|
531
872
|
const check = await db.runHostCheck.findUnique({ where: { runId } });
|
|
532
873
|
return handleReviewHostTool(name, argsJson, {
|
|
533
874
|
agentId: loaded.agentId,
|
|
534
875
|
links: loaded.repositoryLinks,
|
|
535
876
|
hosts: reviewHosts,
|
|
877
|
+
reads: reviewReads,
|
|
878
|
+
db,
|
|
536
879
|
runCheck: check && !check.completedAt
|
|
537
880
|
? {
|
|
538
881
|
provider: check.provider,
|
|
@@ -581,11 +924,13 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
581
924
|
});
|
|
582
925
|
},
|
|
583
926
|
});
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
// `?? []`: a load step replayed from before these links were pinned has none.
|
|
930
|
+
// Each link is re-normalised too: one pinned before the allowlists existed lacks them.
|
|
931
|
+
const issueProjectLinks = (loaded.issueProjectLinks ?? []).map(toIssueProjectLink);
|
|
932
|
+
if (ISSUE_TRACKER_TOOL_NAMES.has(name) && issueProjectLinks.length > 0 && issueTrackers) {
|
|
933
|
+
return async (argsJson) => {
|
|
589
934
|
return handleIssueTrackerTool(name, argsJson, {
|
|
590
935
|
agentId: loaded.agentId,
|
|
591
936
|
links: issueProjectLinks,
|
|
@@ -605,14 +950,22 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
605
950
|
recordedCreates: (projectKey) => db.issueFingerprint.count({ where: { createdByRunId: runId, issueProvider: "jira", projectKey } }),
|
|
606
951
|
},
|
|
607
952
|
});
|
|
608
|
-
}
|
|
609
|
-
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
if (name === "subagent_memory_get") {
|
|
956
|
+
return async (argsJson) => {
|
|
610
957
|
return handleSubAgentMemoryGet(argsJson, loaded.agentId, db, providers.memory);
|
|
611
|
-
}
|
|
612
|
-
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (name === "parent_memory_get") {
|
|
961
|
+
return async (argsJson) => {
|
|
613
962
|
return handleParentMemoryGet(argsJson, runId, db, providers.memory);
|
|
614
|
-
}
|
|
615
|
-
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (name.startsWith(DELEGATE_TOOL_PREFIX)) {
|
|
966
|
+
return async (argsJson, toolCallId) => {
|
|
967
|
+
// An empty id (an adapter that had none) is no key: every such call would share it.
|
|
968
|
+
const callId = toolCallId || undefined;
|
|
616
969
|
const boundName = name.slice(DELEGATE_TOOL_PREFIX.length);
|
|
617
970
|
const edge = loaded.subAgentEdges.find((e) => e.boundName === boundName);
|
|
618
971
|
if (!edge) {
|
|
@@ -621,8 +974,36 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
621
974
|
message: `No sub-agent is bound to name "${boundName}".`,
|
|
622
975
|
});
|
|
623
976
|
}
|
|
977
|
+
// A re-executed delegate step (a resume, or a takeover under a new version) finds the
|
|
978
|
+
// child this very call created and waits on it again, instead of refusing it as a duplicate.
|
|
979
|
+
const existingChild = await findDelegationChild(db, runId, edge.childAgentId, callId);
|
|
980
|
+
if (existingChild) {
|
|
981
|
+
runnerLog.info({ runId, childRunId: existingChild.id, boundName }, "delegation re-attached");
|
|
982
|
+
return reattachDelegatedChild(existingChild, boundName, argsJson);
|
|
983
|
+
}
|
|
624
984
|
let release = await delegationGate.acquire();
|
|
625
985
|
let finishSibling = () => { };
|
|
986
|
+
/**
|
|
987
|
+
* A child create that lost the (parentRunId, parentCallId) unique key to another execution of
|
|
988
|
+
* this same delegate step (a stalled owner overlapping an adopted attempt): wait on the child
|
|
989
|
+
* that execution created, as a re-executed step does. A key held by no child of this agent
|
|
990
|
+
* (an id colliding with another sub-agent's call) is the model's to see, never a run failure.
|
|
991
|
+
*/
|
|
992
|
+
const afterDelegationKeyConflict = async (err) => {
|
|
993
|
+
if (!(err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002"))
|
|
994
|
+
throw err;
|
|
995
|
+
release();
|
|
996
|
+
const raced = await findDelegationChild(db, runId, edge.childAgentId, callId);
|
|
997
|
+
if (raced) {
|
|
998
|
+
runnerLog.info({ runId, childRunId: raced.id, boundName }, "delegation re-attached after a concurrent create");
|
|
999
|
+
return reattachDelegatedChild(raced, boundName, argsJson);
|
|
1000
|
+
}
|
|
1001
|
+
runnerLog.warn({ err, runId, boundName }, "delegation child create hit a unique key held by no child of this agent");
|
|
1002
|
+
return JSON.stringify({
|
|
1003
|
+
error: "delegation_conflict",
|
|
1004
|
+
message: "The sub-agent run could not be created: its call id is already used by another sub-agent run.",
|
|
1005
|
+
});
|
|
1006
|
+
};
|
|
626
1007
|
try {
|
|
627
1008
|
// At most maxDelegationsPerRun dispatches per run (default one: a
|
|
628
1009
|
// classifier deciding plan-vs-implement commits to exactly one child),
|
|
@@ -630,105 +1011,24 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
630
1011
|
// racing on the same task. Checked and the child row written under the
|
|
631
1012
|
// run's delegation gate: delegations started together in one turn
|
|
632
1013
|
// (parallelDelegations) are admitted one at a time.
|
|
633
|
-
const limit = loaded.maxDelegationsPerRun ?? 1;
|
|
634
1014
|
const priorDispatches = await db.run.findMany({ where: { parentRunId: { in: [runId] } } });
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
return
|
|
651
|
-
|
|
652
|
-
message: limit === 1
|
|
653
|
-
? "This run already delegated to a sub-agent; only one delegation is allowed per run."
|
|
654
|
-
: `This run already made ${limit} delegations, the most this agent allows per run.`,
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
let parsed;
|
|
658
|
-
try {
|
|
659
|
-
parsed = JSON.parse(argsJson || "{}");
|
|
660
|
-
}
|
|
661
|
-
catch (err) {
|
|
662
|
-
return JSON.stringify({
|
|
663
|
-
error: "invalid_arguments_json",
|
|
664
|
-
message: err instanceof Error ? err.message : String(err),
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
let args;
|
|
668
|
-
try {
|
|
669
|
-
args = DelegateArgs.parse(parsed);
|
|
670
|
-
}
|
|
671
|
-
catch (err) {
|
|
672
|
-
if (err instanceof z.ZodError) {
|
|
673
|
-
return JSON.stringify({
|
|
674
|
-
error: "validation_failed",
|
|
675
|
-
message: err.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "),
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
throw err;
|
|
679
|
-
}
|
|
680
|
-
// Owners are re-read live on every delegation (resource-sharing
|
|
681
|
-
// grants spec §3.4.4, N1): the child must have the parent's current
|
|
682
|
-
// owner, or that owner must still hold execute on the child. A
|
|
683
|
-
// revoked grant or a make_owner transfer stops the very next call.
|
|
684
|
-
const [parentNow, childAgent] = await Promise.all([
|
|
685
|
-
db.agent.findUnique({ where: { id: loaded.agentId }, select: { ownerId: true } }),
|
|
686
|
-
db.agent.findUniqueOrThrow({
|
|
687
|
-
where: { id: edge.childAgentId },
|
|
688
|
-
select: {
|
|
689
|
-
id: true,
|
|
690
|
-
kind: true,
|
|
691
|
-
budgetGroupId: true,
|
|
692
|
-
budgetUsd: true,
|
|
693
|
-
ownerId: true,
|
|
694
|
-
codingProfile: { select: { allowWebhookTaskOverride: true, timeoutSec: true } },
|
|
695
|
-
},
|
|
696
|
-
}),
|
|
697
|
-
]);
|
|
698
|
-
const parentOwnerId = parentNow?.ownerId ?? null;
|
|
699
|
-
const childOwnerId = childAgent.ownerId ?? null;
|
|
700
|
-
if (!parentNow ||
|
|
701
|
-
!(await canDelegate(db, { ownerId: parentOwnerId }, { id: childAgent.id, ownerId: childOwnerId }))) {
|
|
702
|
-
return JSON.stringify({
|
|
703
|
-
error: "subagent_not_authorized",
|
|
704
|
-
message: `The "${boundName}" sub-agent belongs to another owner who has not given this agent's owner execute access to it.`,
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
if (parentOwnerId !== childOwnerId) {
|
|
708
|
-
// Across owners the edge carries execute and nothing more: no
|
|
709
|
-
// model-chosen memory grant, no continuation of another run's PR,
|
|
710
|
-
// and no task text the caller couldn't give the child directly --
|
|
711
|
-
// the trigger_agent rule (review I3): a coding child only with its
|
|
712
|
-
// owner's allowWebhookTaskOverride opt-in, a native child never
|
|
713
|
-
// (it runs its owner's fixed prompt; pass an empty task).
|
|
714
|
-
const refusal = (args.grantParentMemoryKeys?.length ?? 0) > 0
|
|
715
|
-
? "grantParentMemoryKeys is only allowed when the sub-agent has the same owner."
|
|
716
|
-
: args.continuePriorRun !== undefined
|
|
717
|
-
? "continuePriorRun is only allowed when the sub-agent has the same owner."
|
|
718
|
-
: childAgent.kind === "coding" && !childAgent.codingProfile?.allowWebhookTaskOverride
|
|
719
|
-
? "This coding sub-agent belongs to another owner and does not accept task text from others (allowWebhookTaskOverride)."
|
|
720
|
-
: childAgent.kind !== "coding" && (args.task.trim() !== "" || args.datastoreRef !== undefined)
|
|
721
|
-
? 'This sub-agent belongs to another owner and runs only its own instructions: delegate with task "" and no datastoreRef.'
|
|
722
|
-
: null;
|
|
723
|
-
if (refusal)
|
|
724
|
-
return JSON.stringify({ error: "cross_owner_not_allowed", message: refusal });
|
|
725
|
-
}
|
|
726
|
-
if (args.continuePriorRun !== undefined && childAgent.kind !== "coding") {
|
|
727
|
-
return JSON.stringify({
|
|
728
|
-
error: "continuation_requires_coding_agent",
|
|
729
|
-
message: "continuePriorRun is only supported when the sub-agent is coding-kind.",
|
|
730
|
-
});
|
|
731
|
-
}
|
|
1015
|
+
const limitRefusal = delegationLimitRefusal({
|
|
1016
|
+
priorChildAgentIds: priorDispatches.map((prior) => prior.agentId),
|
|
1017
|
+
waitingChildAgentIds: [...waitingDelegations],
|
|
1018
|
+
childAgentId: edge.childAgentId,
|
|
1019
|
+
boundName,
|
|
1020
|
+
limit: loaded.maxDelegationsPerRun ?? 1,
|
|
1021
|
+
});
|
|
1022
|
+
if (limitRefusal)
|
|
1023
|
+
return limitRefusal;
|
|
1024
|
+
const parsedArgs = parseDelegateArgs(argsJson);
|
|
1025
|
+
if ("refusal" in parsedArgs)
|
|
1026
|
+
return parsedArgs.refusal;
|
|
1027
|
+
const { args } = parsedArgs;
|
|
1028
|
+
const authorized = await authorizeDelegation(db, loaded.agentId, edge, boundName, args);
|
|
1029
|
+
if ("refusal" in authorized)
|
|
1030
|
+
return authorized.refusal;
|
|
1031
|
+
const { childAgent } = authorized;
|
|
732
1032
|
// parallelDelegations: a child the budget would refuse (run_tree_exhausted or
|
|
733
1033
|
// budget_group_exhausted:*) while a sibling still runs waits for a sibling to finish and
|
|
734
1034
|
// free its hold, then tries again while siblings remain, up to the child's own wait bound
|
|
@@ -741,6 +1041,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
741
1041
|
while (delegationSiblings.inFlight > 0) {
|
|
742
1042
|
// Taken before the budget read, so a sibling that finishes during it still wakes this wait.
|
|
743
1043
|
const finishes = delegationSiblings.finishes;
|
|
1044
|
+
// What dispatch will reserve: a coding child's base too, its overrun is never reserved.
|
|
744
1045
|
const { exhaustedBy } = await effectiveBudgetForRun(db, childAgent, new Date(), runId);
|
|
745
1046
|
const remainingMs = deadline - Date.now();
|
|
746
1047
|
if (!exhaustedBy || remainingMs <= 0)
|
|
@@ -798,7 +1099,9 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
798
1099
|
trigger: "subagent",
|
|
799
1100
|
codingTask: args.task,
|
|
800
1101
|
continuesCodingRunId: args.continuePriorRun,
|
|
1102
|
+
mergeOrder: args.mergeOrder,
|
|
801
1103
|
parentRunId: runId,
|
|
1104
|
+
parentCallId: callId,
|
|
802
1105
|
// The child's result flows back into this run, which the
|
|
803
1106
|
// triggerer sees, so the child is visible to them too.
|
|
804
1107
|
triggeredById: existingRun.triggeredById,
|
|
@@ -818,7 +1121,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
818
1121
|
// a pull request another deployment opened, say) is the model's to
|
|
819
1122
|
// report, not a reason to fail the whole run.
|
|
820
1123
|
if (!(err instanceof ContinuationRefusedError))
|
|
821
|
-
|
|
1124
|
+
return await afterDelegationKeyConflict(err);
|
|
822
1125
|
return JSON.stringify({
|
|
823
1126
|
error: "continuation_refused",
|
|
824
1127
|
message: `${err.message} No sub-agent run was started. Do not open a new pull request in its place: ` +
|
|
@@ -829,65 +1132,91 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
829
1132
|
return JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." });
|
|
830
1133
|
}
|
|
831
1134
|
const childRunId = dispatched.run.id;
|
|
832
|
-
const startedAt = Date.now();
|
|
833
1135
|
runnerLog.info({ runId, childRunId, boundName, kind: "coding" }, "delegation started");
|
|
834
|
-
|
|
835
|
-
where: { runId: childRunId },
|
|
836
|
-
select: { timeoutSec: true },
|
|
837
|
-
});
|
|
838
|
-
const { queueTimeoutSec } = loadCodingConcurrencyConfig();
|
|
839
|
-
const waited = await waitForCodingChild({
|
|
840
|
-
db,
|
|
1136
|
+
return await awaitDelegatedChild({
|
|
841
1137
|
executor: providers.executor,
|
|
842
1138
|
childRunId,
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
});
|
|
846
|
-
finishSibling();
|
|
847
|
-
runnerLog.info({
|
|
848
|
-
runId,
|
|
849
|
-
childRunId,
|
|
1139
|
+
createdAtMs: dispatched.run.startedAt.getTime(),
|
|
1140
|
+
kind: "coding",
|
|
850
1141
|
boundName,
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
|
|
1142
|
+
mergeOrderNote: undefined,
|
|
1143
|
+
onSettled: finishSibling,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
const taskOverride = delegationTaskOverride(args);
|
|
1147
|
+
// mergeOrder is a coding-only concept (it orders CodingRuns within a merge); a native
|
|
1148
|
+
// child has no CodingRun to store it on, so it's never passed to dispatchRun/executeRun
|
|
1149
|
+
// below -- only surfaced back to the model as a note, so a lead that set it on the wrong
|
|
1150
|
+
// kind of sub-agent notices rather than silently losing it.
|
|
1151
|
+
const mergeOrderNote = args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined;
|
|
1152
|
+
if (childAgent.nativeExecutionMode === "sandbox") {
|
|
1153
|
+
// A sandbox-mode child must run where its snapshot says, so it goes through
|
|
1154
|
+
// dispatchRun and the Executor (RoutingExecutor sends it to the sandbox executor),
|
|
1155
|
+
// exactly like a coding child — never inline in this process.
|
|
1156
|
+
if (!providers.executor) {
|
|
855
1157
|
return JSON.stringify({
|
|
856
|
-
error: "
|
|
857
|
-
|
|
858
|
-
message: "The coding sub-agent did not finish within its queue timeout plus run timeout; it was stopped and produced no result.",
|
|
1158
|
+
error: "sandbox_dispatch_unavailable",
|
|
1159
|
+
message: "This execution context has no Executor wired in, so a sandbox-mode sub-agent cannot be dispatched.",
|
|
859
1160
|
});
|
|
860
1161
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1162
|
+
let dispatched;
|
|
1163
|
+
try {
|
|
1164
|
+
dispatched = await dispatchRun({
|
|
1165
|
+
db,
|
|
1166
|
+
executor: providers.executor,
|
|
1167
|
+
selfDefects: { db, issueTrackers },
|
|
1168
|
+
agentId: edge.childAgentId,
|
|
1169
|
+
trigger: "subagent",
|
|
1170
|
+
taskOverride,
|
|
1171
|
+
grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [],
|
|
1172
|
+
parentRunId: runId,
|
|
1173
|
+
parentCallId: callId,
|
|
1174
|
+
triggeredById: existingRun.triggeredById,
|
|
1175
|
+
awaitExecution: true,
|
|
1176
|
+
onPersisted: (persistedRun) => {
|
|
1177
|
+
if (persistedRun.status !== "refused" && persistedRun.status !== "failed") {
|
|
1178
|
+
finishSibling = delegationSiblings.start();
|
|
1179
|
+
}
|
|
1180
|
+
release();
|
|
1181
|
+
},
|
|
866
1182
|
});
|
|
867
1183
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
return
|
|
1184
|
+
catch (err) {
|
|
1185
|
+
return await afterDelegationKeyConflict(err);
|
|
1186
|
+
}
|
|
1187
|
+
if (!dispatched) {
|
|
1188
|
+
return JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." });
|
|
1189
|
+
}
|
|
1190
|
+
const childRunId = dispatched.run.id;
|
|
1191
|
+
runnerLog.info({ runId, childRunId, boundName, kind: "native-sandbox" }, "delegation started");
|
|
1192
|
+
return await awaitDelegatedChild({
|
|
1193
|
+
executor: providers.executor,
|
|
1194
|
+
childRunId,
|
|
1195
|
+
createdAtMs: dispatched.run.startedAt.getTime(),
|
|
1196
|
+
kind: "native-sandbox",
|
|
1197
|
+
boundName,
|
|
1198
|
+
mergeOrderNote,
|
|
1199
|
+
onSettled: finishSibling,
|
|
873
1200
|
});
|
|
874
|
-
return codingChildResult(waited.run, stored?.result);
|
|
875
1201
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
}
|
|
1202
|
+
let childRun;
|
|
1203
|
+
try {
|
|
1204
|
+
childRun = await db.run.create({
|
|
1205
|
+
data: {
|
|
1206
|
+
agentId: edge.childAgentId,
|
|
1207
|
+
trigger: "subagent",
|
|
1208
|
+
parentRunId: runId,
|
|
1209
|
+
parentCallId: callId,
|
|
1210
|
+
grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [],
|
|
1211
|
+
taskOverride,
|
|
1212
|
+
triggeredById: existingRun.triggeredById,
|
|
1213
|
+
nativeExecutionMode: childAgent.nativeExecutionMode,
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
catch (err) {
|
|
1218
|
+
return await afterDelegationKeyConflict(err);
|
|
1219
|
+
}
|
|
891
1220
|
finishSibling = delegationSiblings.start();
|
|
892
1221
|
// The child row exists: admit the next delegation while this child runs.
|
|
893
1222
|
release();
|
|
@@ -904,87 +1233,674 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
904
1233
|
outcome: childResult.status,
|
|
905
1234
|
durationMs: Date.now() - startedAt,
|
|
906
1235
|
}, "delegation finished");
|
|
907
|
-
return
|
|
908
|
-
status: childResult.status,
|
|
909
|
-
finalText: childResult.finalText,
|
|
910
|
-
costUsd: Number(childResult.costUsd),
|
|
911
|
-
tokensIn: childResult.tokensIn,
|
|
912
|
-
tokensOut: childResult.tokensOut,
|
|
913
|
-
});
|
|
1236
|
+
return nativeChildResult(childResult, mergeOrderNote);
|
|
914
1237
|
}
|
|
915
1238
|
finally {
|
|
916
1239
|
finishSibling();
|
|
917
1240
|
release();
|
|
918
1241
|
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
return undefined;
|
|
1245
|
+
};
|
|
1246
|
+
/**
|
|
1247
|
+
* The only route to a built-in tool, in both execution modes (the in-process loop and the native
|
|
1248
|
+
* gateway's builtin.call). A tool that posts model-written text to a host, and a delegate_to_*
|
|
1249
|
+
* call (its task text becomes the child run's stored task), has every attached secret's value
|
|
1250
|
+
* masked in its arguments first; when the secrets cannot be read the call is refused and
|
|
1251
|
+
* neither the host nor a child run is reached. (The native gateway serves delegate_to_* through
|
|
1252
|
+
* durableDelegate, which masks the same way.)
|
|
1253
|
+
*/
|
|
1254
|
+
const builtinHandler = (name) => {
|
|
1255
|
+
const handler = unguardedBuiltinHandler(name);
|
|
1256
|
+
if (!handler || !(POSTING_TOOL_NAMES.has(name) || name.startsWith(DELEGATE_TOOL_PREFIX)))
|
|
1257
|
+
return handler;
|
|
1258
|
+
return async (argsJson, toolCallId) => {
|
|
1259
|
+
const masked = await maskModelWrittenArgs({ db, secrets: providers.secrets, agentId: loaded.agentId, runId }, name, argsJson);
|
|
1260
|
+
if (masked === undefined) {
|
|
1261
|
+
return JSON.stringify({ error: "secret_check_unavailable", message: SECRET_CHECK_UNAVAILABLE_MESSAGE });
|
|
919
1262
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1263
|
+
return handler(masked, toolCallId);
|
|
1264
|
+
};
|
|
1265
|
+
};
|
|
1266
|
+
/**
|
|
1267
|
+
* The privileged host for one call of the user tool `name`, scoped to that attachment's
|
|
1268
|
+
* consented capabilities. Undefined for a name that is not an attached user tool. In-process it
|
|
1269
|
+
* serves the sandbox directly; the native sandbox gateway serves a worker's bridge calls with it.
|
|
1270
|
+
*/
|
|
1271
|
+
const scopedHost = (name, tool, signal, redactSecretValues) => createPrivilegedHost({
|
|
1272
|
+
redactSecretValues,
|
|
1273
|
+
agentId: loaded.agentId,
|
|
1274
|
+
datastore: scopeDatastore(providers.datastore, tool.allowedDatastorePrefixes),
|
|
1275
|
+
sharedDatastore: scopeSharedDatastoreAccessor(sharedDatastoreAccessor, tool.allowedSharedDatastorePrefixes),
|
|
1276
|
+
secrets: scopeSecretsAccessor(secretsAccessor, tool.allowedSecrets),
|
|
1277
|
+
allowedFetchHosts: tool.allowedHosts,
|
|
1278
|
+
fetchImpl: providers.sandboxFetch,
|
|
1279
|
+
logTag: name,
|
|
1280
|
+
signal,
|
|
1281
|
+
});
|
|
1282
|
+
const privilegedHostFor = (name, signal, redactSecretValues) => {
|
|
1283
|
+
const tool = toolsByName.get(name);
|
|
1284
|
+
return tool ? scopedHost(name, tool, signal, redactSecretValues) : undefined;
|
|
1285
|
+
};
|
|
1286
|
+
/**
|
|
1287
|
+
* Every secret value the user tool `name` may use (readable or brokered), for console redaction
|
|
1288
|
+
* by a stateless gateway. Rejects when any read fails (readRedactionValues).
|
|
1289
|
+
*/
|
|
1290
|
+
const readableSecretValues = async (name) => {
|
|
1291
|
+
const tool = toolsByName.get(name);
|
|
1292
|
+
if (!tool)
|
|
1293
|
+
return [];
|
|
1294
|
+
return readRedactionValues(scopeSecretsAccessor(secretsAccessor, tool.allowedSecrets), tool.allowedSecrets);
|
|
1295
|
+
};
|
|
1296
|
+
const runUserTool = async (name, argsJson) => {
|
|
1297
|
+
const tool = toolsByName.get(name);
|
|
1298
|
+
if (!tool) {
|
|
1299
|
+
return JSON.stringify({
|
|
1300
|
+
error: "unknown_tool",
|
|
1301
|
+
message: `No tool named "${name}" is attached to this agent.`,
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
return runUserToolCall(tool, argsJson, (signal) => scopedHost(name, tool, signal));
|
|
1305
|
+
};
|
|
1306
|
+
const runSandboxTool = async (name, argsJson, callId) => {
|
|
1307
|
+
const builtin = builtinHandler(name);
|
|
1308
|
+
return builtin ? builtin(argsJson, callId) : runUserTool(name, argsJson);
|
|
1309
|
+
};
|
|
1310
|
+
return { toolsByName, builtinHandler, privilegedHostFor, readableSecretValues, runUserTool, runSandboxTool };
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Live progress for observers (MCP get_run, the viewer). Absolute totals, so a DBOS replay
|
|
1314
|
+
* re-writing them is harmless. Written into the run's own cost columns on purpose: the run
|
|
1315
|
+
* tree's shared budget (computeRunTreeSpend) then counts a running parent's spend so far, as it
|
|
1316
|
+
* already does for coding runs, whose proxy ledger writes their totals live. Best-effort: a
|
|
1317
|
+
* failed write only delays what observers see, and finishRun writes the final totals anyway.
|
|
1318
|
+
* A cancelled run takes them too: a stop lands mid-step, and that step's spend is real, while the
|
|
1319
|
+
* cancelled run's terminal write (failNativeRun) records no totals of its own.
|
|
1320
|
+
*/
|
|
1321
|
+
export async function recordNativeRunProgress(db, runId, progress) {
|
|
1322
|
+
try {
|
|
1323
|
+
await db.run.updateMany({
|
|
1324
|
+
where: { id: runId, status: { in: ["running", "cancelled"] } },
|
|
1325
|
+
data: {
|
|
1326
|
+
turns: progress.turns,
|
|
1327
|
+
tokensIn: progress.usage.tokensIn,
|
|
1328
|
+
tokensOut: progress.usage.tokensOut,
|
|
1329
|
+
costUsd: progress.usage.costUsd,
|
|
1330
|
+
heartbeatAt: new Date(),
|
|
1331
|
+
},
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
catch (err) {
|
|
1335
|
+
runnerLog.warn({ err, runId }, "failed to record run progress");
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* `texts` with every value of every secret attached to the run's agent masked, for a terminal
|
|
1340
|
+
* write: the stored row is what get_run, the viewer, host and issue status comments and run
|
|
1341
|
+
* notifications all read. Fails closed: when the secrets cannot be read, every non-null text is
|
|
1342
|
+
* replaced by WITHHELD_TEXT_NOTICE (the log line carries no text and no error message). A pure
|
|
1343
|
+
* function of the stored secrets, so a durable replay writes the same row.
|
|
1344
|
+
*/
|
|
1345
|
+
async function maskRunTexts(ctx, texts) {
|
|
1346
|
+
const entries = Object.entries(texts);
|
|
1347
|
+
if (!entries.some(([, text]) => typeof text === "string" && text.length > 0))
|
|
1348
|
+
return texts;
|
|
1349
|
+
let mask;
|
|
1350
|
+
try {
|
|
1351
|
+
const row = await ctx.db.run.findUnique({ where: { id: ctx.runId }, select: { agentId: true } });
|
|
1352
|
+
if (!row)
|
|
1353
|
+
throw new Error("run not found");
|
|
1354
|
+
const named = await loadAgentSecretValues(ctx.db, ctx.providers.secrets, row.agentId);
|
|
1355
|
+
mask = (text) => maskNamedSecrets(text, named);
|
|
1356
|
+
}
|
|
1357
|
+
catch (err) {
|
|
1358
|
+
runnerLog.warn({
|
|
1359
|
+
runId: ctx.runId,
|
|
1360
|
+
event: "run_text_secret_check_failed",
|
|
1361
|
+
errorName: err instanceof Error ? err.name : typeof err,
|
|
1362
|
+
}, "could not read the agent's secrets to check the run's final text and error; storing the withheld notice");
|
|
1363
|
+
mask = () => WITHHELD_TEXT_NOTICE;
|
|
1364
|
+
}
|
|
1365
|
+
return Object.fromEntries(entries.map(([key, text]) => [key, typeof text === "string" && text.length > 0 ? mask(text) : text]));
|
|
1366
|
+
}
|
|
1367
|
+
/** Everything after the terminal write, shared by the normal and backstop paths. */
|
|
1368
|
+
async function settleFinishedNativeRun(ctx, finished, claimed) {
|
|
1369
|
+
const { runId, db, providers, reviewHosts, repoAccess, issueTrackers } = ctx;
|
|
1370
|
+
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
1371
|
+
await completeHostStatus(db, finished, reviewHosts);
|
|
1372
|
+
await completeIssueStatus(db, finished, issueTrackers);
|
|
1373
|
+
// After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
|
|
1374
|
+
if (claimed)
|
|
1375
|
+
await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
|
|
1376
|
+
// A tree root is the lead of a request: once it is terminal every coding run it delegated is
|
|
1377
|
+
// too, so the reviews of their pull requests that waited for it start now, after the related
|
|
1378
|
+
// sections above are current (#259). Not gated on `claimed`: release is claimed per row, and a
|
|
1379
|
+
// lead another finalizer ended (a cancellation) should not leave them to the sweep. A run that
|
|
1380
|
+
// delegated nothing has no rows: one indexed lookup. Never throws.
|
|
1381
|
+
if (finished.parentRunId === null && providers.executor && reviewHosts && repoAccess) {
|
|
1382
|
+
await startDeferredForRequest({ db, executor: providers.executor, hosts: reviewHosts, repoAccess, issueTrackers }, runId).catch((err) => runnerLog.warn({ err, runId }, "could not start the reviews waiting for this run"));
|
|
1383
|
+
}
|
|
1384
|
+
if (claimed && providers.executor && reviewHosts && repoAccess) {
|
|
1385
|
+
await startReviewFixAfterReview(runId, {
|
|
1386
|
+
db,
|
|
1387
|
+
executor: providers.executor,
|
|
1388
|
+
hosts: reviewHosts,
|
|
1389
|
+
repoAccess,
|
|
1390
|
+
issueTrackers,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
// Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
|
|
1394
|
+
// filed twice. Bounded and never throws.
|
|
1395
|
+
if (claimed)
|
|
1396
|
+
await fileSelfDefect(db, issueTrackers, finished);
|
|
1397
|
+
// Only the finalizer that made the row terminal notifies. Never throws.
|
|
1398
|
+
if (claimed)
|
|
1399
|
+
await emitRunFinishedEvents(db, finished);
|
|
1400
|
+
// A lead's end posts the `wardby merge order` check on each open pull request of its set,
|
|
1401
|
+
// once every delegated coding run has opened its PR (#261); last, after the release above (so
|
|
1402
|
+
// no review waits on it) and after the follow-ups and notifications (so none is delayed by its
|
|
1403
|
+
// host reads). Only for a tree root that delegated. Not gated on `claimed`: the sync is
|
|
1404
|
+
// idempotent (an unchanged check writes nothing). Never throws.
|
|
1405
|
+
await syncMergeOrderChecksAfterRun({ db, hosts: reviewHosts }, finished);
|
|
1406
|
+
// A reviewer run's end may be the last thing a pull request waited for: mark it ready for review
|
|
1407
|
+
// when its builder link opted in (#287). Last, so nothing above waits on its host reads; idempotent
|
|
1408
|
+
// (claimed once per pull request); no host reads unless a link opted in. Never throws.
|
|
1409
|
+
await markReadyAfterRun({ db, hosts: reviewHosts, log: runnerLog }, runId);
|
|
1410
|
+
return finished;
|
|
1411
|
+
}
|
|
1412
|
+
/** The terminal write for an engine result, then its usage, host/issue status, and follow-ups. */
|
|
1413
|
+
export async function finishNativeRun(ctx, model, result) {
|
|
1414
|
+
const texts = await maskRunTexts(ctx, { error: result.error ?? null, finalText: result.finalText || null });
|
|
1415
|
+
const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
|
|
1416
|
+
status: result.status,
|
|
1417
|
+
tokensIn: result.usage.tokensIn,
|
|
1418
|
+
tokensOut: result.usage.tokensOut,
|
|
1419
|
+
costUsd: result.usage.costUsd,
|
|
1420
|
+
error: texts.error,
|
|
1421
|
+
finalText: texts.finalText,
|
|
1422
|
+
turns: result.turns,
|
|
1423
|
+
finishedAt: new Date(),
|
|
1424
|
+
});
|
|
1425
|
+
// Per-model usage is written on the same path as Run.costUsd: any future mid-run cost write must write usage too.
|
|
1426
|
+
await recordNativeModelUsage(ctx.db, ctx.runId, model, result.usage);
|
|
1427
|
+
return settleFinishedNativeRun(ctx, finished, claimed);
|
|
1428
|
+
}
|
|
1429
|
+
/** The backstop terminal write for a run that threw. A cancellation is not a failure: it carries the operator's own reason. */
|
|
1430
|
+
export async function failNativeRun(ctx, err) {
|
|
1431
|
+
const { error } = await maskRunTexts(ctx, { error: err instanceof Error ? err.message : String(err) });
|
|
1432
|
+
const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
|
|
1433
|
+
status: err instanceof RunCancelledError || err instanceof SandboxRunCancelledError ? "cancelled" : "failed",
|
|
1434
|
+
error,
|
|
1435
|
+
finishedAt: new Date(),
|
|
1436
|
+
});
|
|
1437
|
+
return settleFinishedNativeRun(ctx, finished, claimed);
|
|
1438
|
+
}
|
|
1439
|
+
/** The terminal write for a run that cannot continue (it ends `lost` with `reason`), then the same settle path. */
|
|
1440
|
+
export async function loseNativeRun(ctx, reason) {
|
|
1441
|
+
const { error } = await maskRunTexts(ctx, { error: reason });
|
|
1442
|
+
const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
|
|
1443
|
+
status: "lost",
|
|
1444
|
+
error,
|
|
1445
|
+
finishedAt: new Date(),
|
|
1446
|
+
});
|
|
1447
|
+
if (claimed)
|
|
1448
|
+
await stopChildrenOfLostRun(ctx.db, ctx.providers.executor, ctx.runId);
|
|
1449
|
+
return settleFinishedNativeRun(ctx, finished, claimed);
|
|
1450
|
+
}
|
|
1451
|
+
/** Where a run stands for the native gateway: still drivable, cancelled (its task), or otherwise ended. */
|
|
1452
|
+
export async function runDrivability(db, runId) {
|
|
1453
|
+
const run = await db.run.findUnique({ where: { id: runId }, select: { status: true } });
|
|
1454
|
+
if (!run || !DRIVABLE.includes(run.status)) {
|
|
1455
|
+
return run?.status === "cancelled" ? "cancelled" : "ended";
|
|
1456
|
+
}
|
|
1457
|
+
const cancelledTask = await db.task.findFirst({ where: { runId, status: "cancelled" }, select: { id: true } });
|
|
1458
|
+
return cancelledTask ? "cancelled" : "drivable";
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* What a sandbox worker is given: the pinned agent and tool definitions, which names are built-ins
|
|
1462
|
+
* (served by the gateway), and each user tool's code and schema only — never its capabilities,
|
|
1463
|
+
* secret names, or values, which stay with the gateway's privileged host.
|
|
1464
|
+
*/
|
|
1465
|
+
export function sandboxWorkerInput(runId, loaded, tools) {
|
|
1466
|
+
if (!loaded.pricing) {
|
|
1467
|
+
throw new Error("native_sandbox_requires_catalog: a sandbox run needs its model's catalog entry, and none was recorded.");
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1470
|
+
v: NATIVE_WORKER_PROTOCOL_VERSION,
|
|
1471
|
+
runId,
|
|
1472
|
+
agent: loaded.agent,
|
|
1473
|
+
tools: loaded.tools,
|
|
1474
|
+
builtinTools: loaded.tools.map((tool) => tool.name).filter((name) => tools.builtinHandler(name) !== undefined),
|
|
1475
|
+
userTools: Object.fromEntries([...tools.toolsByName].map(([name, tool]) => [name, { code: tool.code, paramsZod: tool.paramsZod }])),
|
|
1476
|
+
runsConcurrently: loaded.parallelDelegations
|
|
1477
|
+
? loaded.tools.map((tool) => tool.name).filter((name) => name.startsWith(DELEGATE_TOOL_PREFIX))
|
|
1478
|
+
: [],
|
|
1479
|
+
pricing: { ...loaded.pricing.entry, efforts: [...loaded.pricing.entry.efforts] },
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* A tool call's model-written arguments with every secret value attached to the agent masked, or
|
|
1484
|
+
* undefined (logged by error name only) when the secrets cannot be read and the call must be refused.
|
|
1485
|
+
*/
|
|
1486
|
+
async function maskModelWrittenArgs(ctx, tool, argsJson) {
|
|
1487
|
+
try {
|
|
1488
|
+
const named = await loadAgentSecretValues(ctx.db, ctx.secrets, ctx.agentId);
|
|
1489
|
+
return maskNamedSecrets(argsJson, named);
|
|
1490
|
+
}
|
|
1491
|
+
catch (err) {
|
|
1492
|
+
runnerLog.warn({
|
|
1493
|
+
runId: ctx.runId,
|
|
1494
|
+
tool,
|
|
1495
|
+
event: "post_secret_check_failed",
|
|
1496
|
+
errorName: err instanceof Error ? err.name : typeof err,
|
|
1497
|
+
}, "could not read the agent's secrets to check a posted text; refusing the call");
|
|
1498
|
+
return undefined;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
/** Long-poll window: well under common proxy and load-balancer idle timeouts. */
|
|
1502
|
+
export const DELEGATION_POLL_WINDOW_MS = 20_000;
|
|
1503
|
+
/**
|
|
1504
|
+
* A delegate_to_* call from a sandboxed run, served by the native gateway. The in-process branch
|
|
1505
|
+
* holds its admission, budget wait, and child wait in one process's memory for the child's whole
|
|
1506
|
+
* life; this one keeps them on the call's ledger row, so the worker can call again with the same
|
|
1507
|
+
* callId — on any control-plane replica, before or after a restart — and continue where it was:
|
|
1508
|
+
*
|
|
1509
|
+
* - every child, native or coding, is a managed run dispatched through dispatchRun and the
|
|
1510
|
+
* Executor (never inline: an inline child dies with the replica, and the reconciler does not
|
|
1511
|
+
* reap unmanaged runs);
|
|
1512
|
+
* - admission re-checks the per-run delegation limit under a run-scoped advisory lock inside the
|
|
1513
|
+
* child's own transaction, so parallel delegations on different replicas cannot exceed it;
|
|
1514
|
+
* - a delegation the budget would refuse while sibling children still run is recorded as waiting
|
|
1515
|
+
* and re-evaluated on each call (parallelDelegations), up to the same bound as in process;
|
|
1516
|
+
* - each call waits on the child row up to `pollWindowMs`, then answers `pending`.
|
|
1517
|
+
*
|
|
1518
|
+
* Every refusal and result uses the same helpers, and so the same messages, as in process.
|
|
1519
|
+
*/
|
|
1520
|
+
export async function durableDelegate(ctx, callId, name, argsJson) {
|
|
1521
|
+
const { runId, existingRun, loaded, providers, db, ledger, sessionId } = ctx;
|
|
1522
|
+
const now = ctx.now ?? Date.now;
|
|
1523
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1524
|
+
const finish = async (result) => {
|
|
1525
|
+
await ledger.recordResult(sessionId, callId, result);
|
|
1526
|
+
return { result };
|
|
1527
|
+
};
|
|
1528
|
+
const claimed = await ledger.claim(sessionId, callId, "builtin.call");
|
|
1529
|
+
if (claimed.outcome === "done")
|
|
1530
|
+
return { result: String(claimed.result) };
|
|
1531
|
+
const state = await ledger.delegation(sessionId, callId);
|
|
1532
|
+
const startedAt = state?.createdAt.getTime() ?? now();
|
|
1533
|
+
const boundName = name.slice(DELEGATE_TOOL_PREFIX.length);
|
|
1534
|
+
const edge = loaded.subAgentEdges.find((e) => e.boundName === boundName);
|
|
1535
|
+
if (!edge) {
|
|
1536
|
+
return finish(JSON.stringify({ error: "no_such_subagent", message: `No sub-agent is bound to name "${boundName}".` }));
|
|
1537
|
+
}
|
|
1538
|
+
const executor = providers.executor;
|
|
1539
|
+
if (!executor) {
|
|
1540
|
+
return finish(JSON.stringify({
|
|
1541
|
+
error: "delegation_unavailable",
|
|
1542
|
+
message: "This execution context has no Executor wired in, so the sub-agent cannot be dispatched.",
|
|
1543
|
+
}));
|
|
1544
|
+
}
|
|
1545
|
+
const parsedArgs = parseDelegateArgs(argsJson);
|
|
1546
|
+
if ("refusal" in parsedArgs)
|
|
1547
|
+
return finish(parsedArgs.refusal);
|
|
1548
|
+
const { args } = parsedArgs;
|
|
1549
|
+
const authorized = await authorizeDelegation(db, loaded.agentId, edge, boundName, args);
|
|
1550
|
+
if ("refusal" in authorized)
|
|
1551
|
+
return finish(authorized.refusal);
|
|
1552
|
+
const { childAgent } = authorized;
|
|
1553
|
+
const coding = childAgent.kind === "coding";
|
|
1554
|
+
const { queueTimeoutSec } = loadCodingConcurrencyConfig();
|
|
1555
|
+
let childRunId = state?.childRunId ?? null;
|
|
1556
|
+
if (!childRunId && state?.childAgentId === edge.childAgentId) {
|
|
1557
|
+
// A replica that died between dispatching the child and recording it left the child row:
|
|
1558
|
+
// adopt it rather than refusing this very delegation as a duplicate.
|
|
1559
|
+
childRunId = (await findDelegationChild(db, runId, edge.childAgentId, callId))?.id ?? null;
|
|
1560
|
+
// Recorded, so later calls wait on it by id.
|
|
1561
|
+
if (childRunId) {
|
|
1562
|
+
await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId, childRunId });
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (!childRunId) {
|
|
1566
|
+
// About to create a child: its task is model-written and stored, so mask the parent's secret
|
|
1567
|
+
// values first. Only here, never on a poll of an existing child (a transient secret-read
|
|
1568
|
+
// failure must not orphan a running child). Recorded as the call's result, which also completes a waiting row; the model's retry comes with a new callId.
|
|
1569
|
+
const maskedArgsJson = await maskModelWrittenArgs({ db, secrets: providers.secrets, agentId: loaded.agentId, runId }, name, argsJson);
|
|
1570
|
+
if (maskedArgsJson === undefined) {
|
|
1571
|
+
return finish(JSON.stringify({ error: "secret_check_unavailable", message: SECRET_CHECK_UNAVAILABLE_MESSAGE }));
|
|
1572
|
+
}
|
|
1573
|
+
const maskedArgs = parseDelegateArgs(maskedArgsJson);
|
|
1574
|
+
if ("refusal" in maskedArgs)
|
|
1575
|
+
return finish(maskedArgs.refusal);
|
|
1576
|
+
const dispatchArgs = maskedArgs.args;
|
|
1577
|
+
const limit = loaded.maxDelegationsPerRun ?? 1;
|
|
1578
|
+
const refusalNow = async (reader) => {
|
|
1579
|
+
const prior = await reader.run.findMany({ where: { parentRunId: runId }, select: { agentId: true } });
|
|
1580
|
+
return delegationLimitRefusal({
|
|
1581
|
+
priorChildAgentIds: prior.map((p) => p.agentId),
|
|
1582
|
+
waitingChildAgentIds: await ledger.waitingChildAgentIds(sessionId, callId),
|
|
1583
|
+
childAgentId: edge.childAgentId,
|
|
1584
|
+
boundName,
|
|
1585
|
+
limit,
|
|
952
1586
|
});
|
|
953
|
-
if (!result.ok) {
|
|
954
|
-
return JSON.stringify({ error: result.errorKind, message: result.errorMessage });
|
|
955
|
-
}
|
|
956
|
-
return JSON.stringify(result.value);
|
|
957
1587
|
};
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
});
|
|
1588
|
+
const limitRefusal = await refusalNow(db);
|
|
1589
|
+
if (limitRefusal)
|
|
1590
|
+
return finish(limitRefusal);
|
|
1591
|
+
// parallelDelegations: a child the budget would refuse while a sibling still runs waits for a
|
|
1592
|
+
// sibling to finish and free its hold, up to the child's own wait bound. With no sibling
|
|
1593
|
+
// running it goes ahead and is refused at dispatch or at its first turn, as in process.
|
|
1594
|
+
if (loaded.parallelDelegations) {
|
|
1595
|
+
const siblings = await db.run.count({ where: { parentRunId: runId, status: { in: [...DRIVABLE] } } });
|
|
1596
|
+
if (siblings > 0) {
|
|
1597
|
+
const runTimeoutSec = coding ? (childAgent.codingProfile?.timeoutSec ?? 0) : 0;
|
|
1598
|
+
const boundMs = (queueTimeoutSec + runTimeoutSec + CODING_CHILD_WAIT_GRACE_SEC) * 1000;
|
|
1599
|
+
const { exhaustedBy } = await effectiveBudgetForRun(db, childAgent, new Date(), runId);
|
|
1600
|
+
if (exhaustedBy && now() - startedAt < boundMs) {
|
|
1601
|
+
await ledger.setDelegation(sessionId, callId, { status: "waiting_budget", childAgentId: edge.childAgentId });
|
|
1602
|
+
return { pending: true };
|
|
1603
|
+
}
|
|
975
1604
|
}
|
|
976
|
-
|
|
977
|
-
|
|
1605
|
+
}
|
|
1606
|
+
// Recorded before dispatch, so a replica dying in between leaves a row the next call adopts.
|
|
1607
|
+
await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId });
|
|
1608
|
+
let dispatched;
|
|
1609
|
+
let refusedUnderLock = null;
|
|
1610
|
+
try {
|
|
1611
|
+
dispatched = await dispatchRun({
|
|
1612
|
+
db,
|
|
1613
|
+
executor,
|
|
1614
|
+
selfDefects: { db, issueTrackers: ctx.issueTrackers },
|
|
1615
|
+
agentId: edge.childAgentId,
|
|
1616
|
+
trigger: "subagent",
|
|
1617
|
+
...(coding
|
|
1618
|
+
? {
|
|
1619
|
+
codingTask: dispatchArgs.task,
|
|
1620
|
+
continuesCodingRunId: dispatchArgs.continuePriorRun,
|
|
1621
|
+
mergeOrder: dispatchArgs.mergeOrder,
|
|
1622
|
+
}
|
|
1623
|
+
: {
|
|
1624
|
+
taskOverride: delegationTaskOverride(dispatchArgs),
|
|
1625
|
+
grantedParentMemoryKeys: dispatchArgs.grantParentMemoryKeys ?? [],
|
|
1626
|
+
}),
|
|
1627
|
+
parentRunId: runId,
|
|
1628
|
+
parentCallId: callId,
|
|
1629
|
+
// The child's result flows back into this run, which the triggerer sees, so the child is visible to them too.
|
|
1630
|
+
triggeredById: existingRun.triggeredById,
|
|
1631
|
+
beforePersist: async (tx) => {
|
|
1632
|
+
// One admission per parent at a time, across every replica.
|
|
1633
|
+
await tx.$queryRaw `SELECT 1 AS locked FROM (SELECT pg_advisory_xact_lock(hashtext(${runId}))) AS l`;
|
|
1634
|
+
refusedUnderLock = await refusalNow(tx);
|
|
1635
|
+
return refusedUnderLock === null;
|
|
1636
|
+
},
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
catch (err) {
|
|
1640
|
+
if (err instanceof ContinuationRefusedError) {
|
|
1641
|
+
return finish(JSON.stringify({
|
|
1642
|
+
error: "continuation_refused",
|
|
1643
|
+
message: `${err.message} No sub-agent run was started. Do not open a new pull request in its place: ` +
|
|
1644
|
+
"tell the requester that this wardby deployment cannot continue that pull request's branch.",
|
|
1645
|
+
}));
|
|
978
1646
|
}
|
|
979
|
-
|
|
1647
|
+
// Recorded as the call's result, so the model can report it and a retried callId never
|
|
1648
|
+
// dispatches twice. Only the leading code is exposed: messages can carry internals.
|
|
1649
|
+
const code = declaredCodeOf(err);
|
|
1650
|
+
const values = await ctx.secretValues?.().catch(() => undefined);
|
|
1651
|
+
logUnexpectedError(runnerLog, err, values, "delegation dispatch failed", { runId, boundName });
|
|
1652
|
+
return finish(JSON.stringify({
|
|
1653
|
+
error: "dispatch_failed",
|
|
1654
|
+
message: `The sub-agent run could not be created${code ? ` (${code})` : ""}.`,
|
|
1655
|
+
}));
|
|
1656
|
+
}
|
|
1657
|
+
if (!dispatched) {
|
|
1658
|
+
return finish(refusedUnderLock ??
|
|
1659
|
+
JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." }));
|
|
1660
|
+
}
|
|
1661
|
+
childRunId = dispatched.run.id;
|
|
1662
|
+
await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId, childRunId });
|
|
1663
|
+
runnerLog.info({ runId, childRunId, boundName, kind: childAgent.kind, durable: true }, "delegation started");
|
|
1664
|
+
}
|
|
1665
|
+
const codingRun = coding
|
|
1666
|
+
? await db.codingRun.findUnique({ where: { runId: childRunId }, select: { timeoutSec: true } })
|
|
1667
|
+
: null;
|
|
1668
|
+
const boundMs = coding
|
|
1669
|
+
? (queueTimeoutSec + (codingRun?.timeoutSec ?? 0) + CODING_CHILD_WAIT_GRACE_SEC) * 1000
|
|
1670
|
+
: SANDBOX_CHILD_WAIT_SEC * 1000;
|
|
1671
|
+
const windowEnd = now() + (ctx.pollWindowMs ?? DELEGATION_POLL_WINDOW_MS);
|
|
1672
|
+
for (;;) {
|
|
1673
|
+
const child = await db.run.findUniqueOrThrow({ where: { id: childRunId } });
|
|
1674
|
+
if (!DRIVABLE.includes(child.status)) {
|
|
1675
|
+
runnerLog.info({ runId, childRunId, boundName, outcome: child.status, durable: true }, "delegation finished");
|
|
1676
|
+
if (!coding) {
|
|
1677
|
+
return finish(nativeChildResult(child, args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined));
|
|
1678
|
+
}
|
|
1679
|
+
const stored = await db.codingRun
|
|
1680
|
+
.findUnique({ where: { runId: childRunId }, select: { result: true } })
|
|
1681
|
+
.catch(() => null);
|
|
1682
|
+
return finish(codingChildResult(child, stored?.result, await loadBudgetEvent(db, childRunId)));
|
|
1683
|
+
}
|
|
1684
|
+
if (now() - startedAt >= boundMs) {
|
|
1685
|
+
await executor.stop(childRunId, "sub-agent wait timed out").catch((err) => {
|
|
1686
|
+
runnerLog.warn({ err, childRunId, runId }, "failed to stop a timed-out sub-agent run");
|
|
1687
|
+
});
|
|
1688
|
+
return finish(JSON.stringify({
|
|
1689
|
+
error: "subagent_wait_timed_out",
|
|
1690
|
+
runId: childRunId,
|
|
1691
|
+
message: coding
|
|
1692
|
+
? "The coding sub-agent did not finish within its queue timeout plus run timeout; it was stopped and produced no result."
|
|
1693
|
+
: "The sub-agent did not finish in time; it was stopped and produced no result.",
|
|
1694
|
+
}));
|
|
1695
|
+
}
|
|
1696
|
+
const remaining = windowEnd - now();
|
|
1697
|
+
if (remaining <= 0)
|
|
1698
|
+
return { pending: true };
|
|
1699
|
+
await sleep(Math.min(ctx.pollIntervalMs ?? 1_000, remaining));
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
/** The integrations a native run uses, as executeRun derives them from its providers. */
|
|
1703
|
+
export function nativeRunIntegrations(providers, db) {
|
|
1704
|
+
const reviewHosts = configuredReviewHosts(providers.reviewHosts);
|
|
1705
|
+
const repoAccess = reviewHosts
|
|
1706
|
+
? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
|
|
1707
|
+
: undefined;
|
|
1708
|
+
return { reviewHosts, repoAccess, issueTrackers: configuredIssueTrackers(providers.issueTrackers) };
|
|
1709
|
+
}
|
|
1710
|
+
/** How long a sandbox run's gateway session (and so its capability) stays valid. */
|
|
1711
|
+
export const SANDBOX_RUN_MAX_SEC = SANDBOX_CHILD_WAIT_SEC;
|
|
1712
|
+
/**
|
|
1713
|
+
* Starts a sandbox-mode run's trusted half: loads and pins the run, marks it running, records its
|
|
1714
|
+
* gateway session (capability hash, deadline, budget, snapshot), and records `native-sandbox` as
|
|
1715
|
+
* its execution backend — all before any worker exists. The caller launches the worker with the
|
|
1716
|
+
* returned input, which carries the gateway URL and the one-time capability.
|
|
1717
|
+
*/
|
|
1718
|
+
export async function createSandboxSession(options) {
|
|
1719
|
+
const { runId, providers, db, ledger } = options;
|
|
1720
|
+
const now = options.now ?? new Date();
|
|
1721
|
+
const existingRun = await db.run.findUnique({ where: { id: runId } });
|
|
1722
|
+
if (!existingRun)
|
|
1723
|
+
throw new Error(`Unknown run "${runId}".`);
|
|
1724
|
+
if (!DRIVABLE.includes(existingRun.status))
|
|
1725
|
+
return { kind: "ended", run: existingRun };
|
|
1726
|
+
if (existingRun.nativeExecutionMode !== "sandbox")
|
|
1727
|
+
throw new Error(`Run "${runId}" is not a sandbox-mode run.`);
|
|
1728
|
+
const { reviewHosts, repoAccess, issueTrackers } = nativeRunIntegrations(providers, db);
|
|
1729
|
+
const finishContext = { runId, db, providers, reviewHosts, repoAccess, issueTrackers };
|
|
1730
|
+
const loadedOrUnavailable = await loadNativeRun({
|
|
1731
|
+
runId,
|
|
1732
|
+
existingRun,
|
|
1733
|
+
providers,
|
|
1734
|
+
db,
|
|
1735
|
+
step: runStepInline,
|
|
1736
|
+
reviewHosts,
|
|
1737
|
+
issueTrackers,
|
|
1738
|
+
});
|
|
1739
|
+
if ("unavailable" in loadedOrUnavailable) {
|
|
1740
|
+
return { kind: "ended", run: await failNativeRun(finishContext, new Error(loadedOrUnavailable.unavailable)) };
|
|
1741
|
+
}
|
|
1742
|
+
const loaded = loadedOrUnavailable;
|
|
1743
|
+
if (loaded.kind === "coding") {
|
|
1744
|
+
return { kind: "ended", run: await failNativeRun(finishContext, new Error(CODING_EXECUTOR_NOT_CONFIGURED)) };
|
|
1745
|
+
}
|
|
1746
|
+
const tools = createNativeRunTools({
|
|
1747
|
+
runId,
|
|
1748
|
+
existingRun,
|
|
1749
|
+
loaded,
|
|
1750
|
+
providers,
|
|
1751
|
+
db,
|
|
1752
|
+
reviewHosts,
|
|
1753
|
+
repoAccess,
|
|
1754
|
+
issueTrackers,
|
|
1755
|
+
});
|
|
1756
|
+
let input;
|
|
1757
|
+
try {
|
|
1758
|
+
input = sandboxWorkerInput(runId, loaded, tools);
|
|
1759
|
+
}
|
|
1760
|
+
catch (err) {
|
|
1761
|
+
return { kind: "ended", run: await failNativeRun(finishContext, err) };
|
|
1762
|
+
}
|
|
1763
|
+
await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data: { status: "running" } });
|
|
1764
|
+
const capability = randomBytes(32).toString("base64url");
|
|
1765
|
+
const snapshot = { input, loaded };
|
|
1766
|
+
const session = await ledger.createSession({
|
|
1767
|
+
runId,
|
|
1768
|
+
capabilityHash: sandboxCapabilityHash(capability),
|
|
1769
|
+
deadlineAt: new Date(now.getTime() + SANDBOX_RUN_MAX_SEC * 1000),
|
|
1770
|
+
budgetUsd: loaded.agent.budgetUsd,
|
|
1771
|
+
snapshot: snapshot,
|
|
1772
|
+
networkReadyAt: options.networkReady === false ? null : now,
|
|
1773
|
+
});
|
|
1774
|
+
// The handle exists before any worker does, so the reconciler routes a stale run to the sandbox executor.
|
|
1775
|
+
await db.run.updateMany({
|
|
1776
|
+
where: { id: runId, executionBackend: null },
|
|
1777
|
+
data: { executionBackend: NATIVE_SANDBOX_BACKEND },
|
|
1778
|
+
});
|
|
1779
|
+
return {
|
|
1780
|
+
kind: "started",
|
|
1781
|
+
sessionId: session.id,
|
|
1782
|
+
capability,
|
|
1783
|
+
input: { ...input, gateway: { url: options.gatewayUrl, capability } },
|
|
1784
|
+
secretValues: () => userToolSecretValues(tools, Object.keys(input.userTools)),
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Every secret value the sandbox run's user tools `names` may read (readable or brokered): what its
|
|
1789
|
+
* worker can be handed, so what the gateway checks its cause codes against and a launcher masks
|
|
1790
|
+
* from the worker's output tail. Rejects when any read fails (readRedactionValues).
|
|
1791
|
+
*/
|
|
1792
|
+
export async function userToolSecretValues(tools, names) {
|
|
1793
|
+
return (await Promise.all(names.map((name) => tools.readableSecretValues(name)))).flat();
|
|
1794
|
+
}
|
|
1795
|
+
/** Run.executionBackend for a sandbox-mode run whose worker is (or was) launched. */
|
|
1796
|
+
export const NATIVE_SANDBOX_BACKEND = "native-sandbox";
|
|
1797
|
+
/** The stored form of a sandbox capability: never the token itself. */
|
|
1798
|
+
export function sandboxCapabilityHash(capability) {
|
|
1799
|
+
return createHash("sha256").update(capability).digest("hex");
|
|
1800
|
+
}
|
|
1801
|
+
/** Drives an existing Run (created by `createRun` or the scheduler) to a terminal state. */
|
|
1802
|
+
export async function executeRun(runId, providers, db = defaultDb, onText, step = runStepInline) {
|
|
1803
|
+
// Counted so a shutdown waits for this run instead of abandoning it (core/in-flight-runs.ts).
|
|
1804
|
+
return trackRun(runId, () => executeTrackedRun(runId, providers, db, onText, step));
|
|
1805
|
+
}
|
|
1806
|
+
async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
1807
|
+
const existingRun = await db.run.findUnique({ where: { id: runId } });
|
|
1808
|
+
if (!existingRun) {
|
|
1809
|
+
throw new Error(`Unknown run "${runId}".`);
|
|
1810
|
+
}
|
|
1811
|
+
// Pre-flight guard. A run that already reached a terminal state must never
|
|
1812
|
+
// be re-driven: a durable workflow re-dispatched after the reconciler
|
|
1813
|
+
// reaped its row (rollback to EXECUTOR=in-process, then roll forward) would
|
|
1814
|
+
// otherwise re-spend the whole run against a `lost` row, and a duplicate
|
|
1815
|
+
// attempt of a finished run would spend a second time for a result no write
|
|
1816
|
+
// can land. Cheaper and clearer than letting it run and discarding the
|
|
1817
|
+
// result at the conditional write.
|
|
1818
|
+
if (!DRIVABLE.includes(existingRun.status)) {
|
|
1819
|
+
runnerLog.info({ runId, status: existingRun.status }, "skipping execution of an already-terminal run");
|
|
1820
|
+
return existingRun;
|
|
1821
|
+
}
|
|
1822
|
+
const reviewHosts = configuredReviewHosts(providers.reviewHosts);
|
|
1823
|
+
const repoAccess = reviewHosts
|
|
1824
|
+
? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
|
|
1825
|
+
: undefined;
|
|
1826
|
+
const issueTrackers = configuredIssueTrackers(providers.issueTrackers);
|
|
1827
|
+
// Inline execution checks for a stop before every step (stopAwareStep); a durable executor passes
|
|
1828
|
+
// its own step runner, which does its own check (DbosExecutor wraps it in stopAwareStep too).
|
|
1829
|
+
if (step === runStepInline)
|
|
1830
|
+
step = stopAwareStep(db, runId);
|
|
1831
|
+
// Fails a run that ends before its engine starts, closing what its trigger opened (the review
|
|
1832
|
+
// check, the host and issue status comments) exactly as the normal and catch paths below do.
|
|
1833
|
+
// Deliberately no self-defect: every caller is a configuration state, not this run's defect (an
|
|
1834
|
+
// admin disabled or removed the model, or this deployment has no coding or native sandbox
|
|
1835
|
+
// executor), and filing would open one defect per affected agent rather than describe a failure
|
|
1836
|
+
// of that agent.
|
|
1837
|
+
const finishEarly = async (reason) => {
|
|
1838
|
+
const { error } = await maskRunTexts({ runId, db, providers }, { error: reason });
|
|
1839
|
+
const finished = await finishRun(db, runId, { status: "failed", error, finishedAt: new Date() });
|
|
1840
|
+
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
1841
|
+
await completeHostStatus(db, finished, reviewHosts);
|
|
1842
|
+
await completeIssueStatus(db, finished, issueTrackers);
|
|
1843
|
+
await emitRunFinishedEvents(db, finished);
|
|
1844
|
+
return finished;
|
|
1845
|
+
};
|
|
1846
|
+
// Decided by the run's own snapshot, before any work or spend (no pricing pin, no engine): a
|
|
1847
|
+
// sandbox-mode run never executes in the control plane.
|
|
1848
|
+
const sandboxed = existingRun.nativeExecutionMode === "sandbox";
|
|
1849
|
+
if (sandboxed && !providers.nativeSandbox)
|
|
1850
|
+
return finishEarly(NATIVE_SANDBOX_NOT_CONFIGURED);
|
|
1851
|
+
const loadedOrUnavailable = await loadNativeRun({
|
|
1852
|
+
runId,
|
|
1853
|
+
existingRun,
|
|
1854
|
+
providers,
|
|
1855
|
+
db,
|
|
1856
|
+
step,
|
|
1857
|
+
reviewHosts,
|
|
1858
|
+
issueTrackers,
|
|
1859
|
+
});
|
|
1860
|
+
if ("unavailable" in loadedOrUnavailable)
|
|
1861
|
+
return finishEarly(loadedOrUnavailable.unavailable);
|
|
1862
|
+
const loaded = loadedOrUnavailable;
|
|
1863
|
+
if (loaded.kind === "coding")
|
|
1864
|
+
return finishEarly(CODING_EXECUTOR_NOT_CONFIGURED);
|
|
1865
|
+
// Conditional on DRIVABLE rather than on `pending`: an adopted attempt
|
|
1866
|
+
// legitimately finds the row already `running`, but a terminal row must
|
|
1867
|
+
// never be flipped back to `running`.
|
|
1868
|
+
await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data: { status: "running" } });
|
|
1869
|
+
const finishContext = { runId, db, providers, reviewHosts, repoAccess, issueTrackers };
|
|
1870
|
+
try {
|
|
1871
|
+
const tools = createNativeRunTools({
|
|
1872
|
+
runId,
|
|
1873
|
+
existingRun,
|
|
1874
|
+
loaded,
|
|
1875
|
+
providers,
|
|
1876
|
+
db,
|
|
1877
|
+
reviewHosts,
|
|
1878
|
+
repoAccess,
|
|
1879
|
+
issueTrackers,
|
|
1880
|
+
});
|
|
1881
|
+
const { runSandboxTool } = tools;
|
|
1882
|
+
const onProgress = (progress) => recordNativeRunProgress(db, runId, progress);
|
|
1883
|
+
const llm = loaded.pricing && providers.llm instanceof RoutingLlmProvider
|
|
1884
|
+
? providers.llm.forRun(loaded.pricing.entry)
|
|
1885
|
+
: providers.llm;
|
|
1886
|
+
if (sandboxed && providers.nativeSandbox) {
|
|
1887
|
+
// The turn loop runs in a worker; this process serves its calls from the same tools and LLM.
|
|
1888
|
+
const engineResult = await runSandboxedEngine({
|
|
1889
|
+
runId,
|
|
1890
|
+
input: sandboxWorkerInput(runId, loaded, tools),
|
|
1891
|
+
ctx: { providers: { llm }, onText, onProgress },
|
|
1892
|
+
builtinHandler: tools.builtinHandler,
|
|
1893
|
+
privilegedHostFor: tools.privilegedHostFor,
|
|
1894
|
+
readableSecretValues: tools.readableSecretValues,
|
|
1895
|
+
drivability: () => runDrivability(db, runId),
|
|
1896
|
+
launcher: providers.nativeSandbox,
|
|
1897
|
+
});
|
|
1898
|
+
return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
|
|
1899
|
+
}
|
|
980
1900
|
const engineResult = await providers.engine.run({
|
|
981
1901
|
agent: loaded.agent,
|
|
982
1902
|
tools: loaded.tools,
|
|
983
|
-
providers: {
|
|
984
|
-
llm: loaded.pricing && providers.llm instanceof RoutingLlmProvider
|
|
985
|
-
? providers.llm.forRun(loaded.pricing.entry)
|
|
986
|
-
: providers.llm,
|
|
987
|
-
},
|
|
1903
|
+
providers: { llm },
|
|
988
1904
|
runSandboxTool,
|
|
989
1905
|
// A replay recorded before this flag existed has none: its delegations stay sequential.
|
|
990
1906
|
...(loaded.parallelDelegations
|
|
@@ -994,70 +1910,26 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
|
|
|
994
1910
|
onProgress,
|
|
995
1911
|
step,
|
|
996
1912
|
});
|
|
997
|
-
|
|
998
|
-
status: engineResult.status,
|
|
999
|
-
tokensIn: engineResult.usage.tokensIn,
|
|
1000
|
-
tokensOut: engineResult.usage.tokensOut,
|
|
1001
|
-
costUsd: engineResult.usage.costUsd,
|
|
1002
|
-
error: engineResult.error ?? null,
|
|
1003
|
-
finalText: engineResult.finalText || null,
|
|
1004
|
-
turns: engineResult.turns,
|
|
1005
|
-
finishedAt: new Date(),
|
|
1006
|
-
});
|
|
1007
|
-
// Per-model usage is written on the same path as Run.costUsd: any future mid-run cost write must write usage too.
|
|
1008
|
-
await recordNativeModelUsage(db, runId, loaded.agent.model, engineResult.usage);
|
|
1009
|
-
await closeOpenHostCheck(db, finished, reviewHosts);
|
|
1010
|
-
await completeHostStatus(db, finished, reviewHosts);
|
|
1011
|
-
await completeIssueStatus(db, finished, issueTrackers);
|
|
1012
|
-
// After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
|
|
1013
|
-
if (claimed)
|
|
1014
|
-
await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
|
|
1015
|
-
if (claimed && providers.executor && reviewHosts && repoAccess) {
|
|
1016
|
-
await startReviewFixAfterReview(runId, {
|
|
1017
|
-
db,
|
|
1018
|
-
executor: providers.executor,
|
|
1019
|
-
hosts: reviewHosts,
|
|
1020
|
-
repoAccess,
|
|
1021
|
-
issueTrackers,
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
// Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
|
|
1025
|
-
// filed twice. Bounded and never throws.
|
|
1026
|
-
if (claimed)
|
|
1027
|
-
await fileSelfDefect(db, issueTrackers, finished);
|
|
1028
|
-
return finished;
|
|
1913
|
+
return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
|
|
1029
1914
|
}
|
|
1030
1915
|
catch (err) {
|
|
1031
1916
|
// Defensive backstop: the engine is expected to catch its own errors
|
|
1032
1917
|
// and return a "failed" EngineResult, but an unexpected throw here
|
|
1033
1918
|
// (a real bug, or tool-loading failing outside the per-tool try above)
|
|
1034
|
-
// must still never leave the run dangling in "running".
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
await completeHostStatus(db, finished, reviewHosts);
|
|
1043
|
-
await completeIssueStatus(db, finished, issueTrackers);
|
|
1044
|
-
// After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
|
|
1045
|
-
if (claimed)
|
|
1046
|
-
await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
|
|
1047
|
-
if (claimed && providers.executor && reviewHosts && repoAccess) {
|
|
1048
|
-
await startReviewFixAfterReview(runId, {
|
|
1049
|
-
db,
|
|
1050
|
-
executor: providers.executor,
|
|
1051
|
-
hosts: reviewHosts,
|
|
1052
|
-
repoAccess,
|
|
1053
|
-
issueTrackers,
|
|
1054
|
-
});
|
|
1919
|
+
// must still never leave the run dangling in "running".
|
|
1920
|
+
if (err instanceof RunOwnershipLostError) {
|
|
1921
|
+
// Another execution of this run recorded a step first and owns the run now. Write nothing
|
|
1922
|
+
// and fire no settle side effects: the winner makes the terminal write. Rethrown (not
|
|
1923
|
+
// returned) so the durable executor hands the conflict back to its SDK, which parks this
|
|
1924
|
+
// attempt instead of recording a workflow outcome under the winner.
|
|
1925
|
+
logRunOwnershipLost(runId, err);
|
|
1926
|
+
throw err;
|
|
1055
1927
|
}
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
1060
|
-
return
|
|
1928
|
+
// A replay that no longer matches its record (a run recorded by a release whose steps differ)
|
|
1929
|
+
// cannot continue: it ends lost, not failed, through the same settle path.
|
|
1930
|
+
if (err instanceof StepReplayMismatchError)
|
|
1931
|
+
return loseNativeRun(finishContext, STEPS_CHANGED_REASON);
|
|
1932
|
+
return failNativeRun(finishContext, err);
|
|
1061
1933
|
}
|
|
1062
1934
|
}
|
|
1063
1935
|
/**
|