@paperclipai/server 2026.817.0 → 2026.818.0-beta.1
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/dist/adapters/hermes-gateway-doc.d.ts +1 -1
- package/dist/adapters/hermes-gateway-doc.d.ts.map +1 -1
- package/dist/adapters/hermes-gateway-doc.js +1 -1
- package/dist/adapters/plugin-loader.d.ts.map +1 -1
- package/dist/adapters/plugin-loader.js +3 -2
- package/dist/adapters/plugin-loader.js.map +1 -1
- package/dist/agent-auth-jwt.d.ts.map +1 -1
- package/dist/agent-auth-jwt.js +8 -2
- package/dist/agent-auth-jwt.js.map +1 -1
- package/dist/app.d.ts +5 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +202 -15
- package/dist/app.js.map +1 -1
- package/dist/built-ins/agents/summarizer/AGENTS.md +2 -2
- package/dist/built-ins/agents/summarizer/routines/refresh-stale-summaries.md +1 -0
- package/dist/db-errors.d.ts +9 -0
- package/dist/db-errors.d.ts.map +1 -1
- package/dist/db-errors.js +19 -0
- package/dist/db-errors.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +162 -38
- package/dist/index.js.map +1 -1
- package/dist/lib/request-promise-memo.d.ts +6 -0
- package/dist/lib/request-promise-memo.d.ts.map +1 -0
- package/dist/lib/request-promise-memo.js +25 -0
- package/dist/lib/request-promise-memo.js.map +1 -0
- package/dist/lib/request-promise-memo.test.d.ts +2 -0
- package/dist/lib/request-promise-memo.test.d.ts.map +1 -0
- package/dist/lib/request-promise-memo.test.js +74 -0
- package/dist/lib/request-promise-memo.test.js.map +1 -0
- package/dist/middleware/auth.d.ts +13 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +92 -41
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/cloud-tenant-actor.test.js +26 -1
- package/dist/middleware/cloud-tenant-actor.test.js.map +1 -1
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/error-handler.js +11 -2
- package/dist/middleware/error-handler.js.map +1 -1
- package/dist/middleware/logger.d.ts.map +1 -1
- package/dist/middleware/logger.js +7 -33
- package/dist/middleware/logger.js.map +1 -1
- package/dist/middleware/private-hostname-guard.d.ts.map +1 -1
- package/dist/middleware/private-hostname-guard.js +11 -6
- package/dist/middleware/private-hostname-guard.js.map +1 -1
- package/dist/middleware/private-json-etag.d.ts +3 -0
- package/dist/middleware/private-json-etag.d.ts.map +1 -0
- package/dist/middleware/private-json-etag.js +37 -0
- package/dist/middleware/private-json-etag.js.map +1 -0
- package/dist/middleware/redact-sensitive.d.ts.map +1 -1
- package/dist/middleware/redact-sensitive.js +11 -0
- package/dist/middleware/redact-sensitive.js.map +1 -1
- package/dist/realtime/live-events-ws.d.ts +12 -0
- package/dist/realtime/live-events-ws.d.ts.map +1 -1
- package/dist/realtime/live-events-ws.js +20 -0
- package/dist/realtime/live-events-ws.js.map +1 -1
- package/dist/redaction.js +1 -1
- package/dist/redaction.js.map +1 -1
- package/dist/routes/access.d.ts +2 -0
- package/dist/routes/access.d.ts.map +1 -1
- package/dist/routes/access.js +21 -10
- package/dist/routes/access.js.map +1 -1
- package/dist/routes/agents.d.ts +42 -0
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +875 -8
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/companies.d.ts +5 -1
- package/dist/routes/companies.d.ts.map +1 -1
- package/dist/routes/companies.js +467 -30
- package/dist/routes/companies.js.map +1 -1
- package/dist/routes/environments.d.ts +12 -3
- package/dist/routes/environments.d.ts.map +1 -1
- package/dist/routes/environments.js +139 -23
- package/dist/routes/environments.js.map +1 -1
- package/dist/routes/execution-workspaces.d.ts.map +1 -1
- package/dist/routes/execution-workspaces.js +240 -70
- package/dist/routes/execution-workspaces.js.map +1 -1
- package/dist/routes/file-resources.d.ts +14 -1
- package/dist/routes/file-resources.d.ts.map +1 -1
- package/dist/routes/file-resources.js +182 -3
- package/dist/routes/file-resources.js.map +1 -1
- package/dist/routes/index.d.ts +2 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/instance-settings.d.ts.map +1 -1
- package/dist/routes/instance-settings.js +20 -1
- package/dist/routes/instance-settings.js.map +1 -1
- package/dist/routes/issues.d.ts.map +1 -1
- package/dist/routes/issues.js +750 -235
- package/dist/routes/issues.js.map +1 -1
- package/dist/routes/onboarding-seed.d.ts +27 -0
- package/dist/routes/onboarding-seed.d.ts.map +1 -0
- package/dist/routes/onboarding-seed.js +56 -0
- package/dist/routes/onboarding-seed.js.map +1 -0
- package/dist/routes/openapi.d.ts.map +1 -1
- package/dist/routes/openapi.js +316 -2
- package/dist/routes/openapi.js.map +1 -1
- package/dist/routes/secrets.d.ts.map +1 -1
- package/dist/routes/secrets.js +11 -1
- package/dist/routes/secrets.js.map +1 -1
- package/dist/routes/setup-token-route.test.d.ts +2 -0
- package/dist/routes/setup-token-route.test.d.ts.map +1 -0
- package/dist/routes/setup-token-route.test.js +896 -0
- package/dist/routes/setup-token-route.test.js.map +1 -0
- package/dist/routes/tool-access.d.ts.map +1 -1
- package/dist/routes/tool-access.js +87 -52
- package/dist/routes/tool-access.js.map +1 -1
- package/dist/routes/workspace-runtime-service-authz.d.ts +11 -1
- package/dist/routes/workspace-runtime-service-authz.d.ts.map +1 -1
- package/dist/routes/workspace-runtime-service-authz.js +18 -16
- package/dist/routes/workspace-runtime-service-authz.js.map +1 -1
- package/dist/services/access.d.ts.map +1 -1
- package/dist/services/activity.d.ts +1 -1
- package/dist/services/agents.d.ts +21 -0
- package/dist/services/agents.d.ts.map +1 -1
- package/dist/services/agents.js +121 -1
- package/dist/services/agents.js.map +1 -1
- package/dist/services/attention-resolver-audience.test.d.ts +2 -0
- package/dist/services/attention-resolver-audience.test.d.ts.map +1 -0
- package/dist/services/attention-resolver-audience.test.js +81 -0
- package/dist/services/attention-resolver-audience.test.js.map +1 -0
- package/dist/services/attention.d.ts +20 -1
- package/dist/services/attention.d.ts.map +1 -1
- package/dist/services/attention.js +45 -1
- package/dist/services/attention.js.map +1 -1
- package/dist/services/authorization.d.ts +2 -2
- package/dist/services/authorization.d.ts.map +1 -1
- package/dist/services/authorization.js +89 -46
- package/dist/services/authorization.js.map +1 -1
- package/dist/services/board-auth.d.ts.map +1 -1
- package/dist/services/board-auth.js +40 -1
- package/dist/services/board-auth.js.map +1 -1
- package/dist/services/board-auth.test.d.ts +2 -0
- package/dist/services/board-auth.test.d.ts.map +1 -0
- package/dist/services/board-auth.test.js +34 -0
- package/dist/services/board-auth.test.js.map +1 -0
- package/dist/services/built-in-agents.d.ts.map +1 -1
- package/dist/services/built-in-agents.js +20 -4
- package/dist/services/built-in-agents.js.map +1 -1
- package/dist/services/bundled-plugins.d.ts +11 -1
- package/dist/services/bundled-plugins.d.ts.map +1 -1
- package/dist/services/bundled-plugins.js +46 -1
- package/dist/services/bundled-plugins.js.map +1 -1
- package/dist/services/codex-device-login-reaper.d.ts +61 -0
- package/dist/services/codex-device-login-reaper.d.ts.map +1 -0
- package/dist/services/codex-device-login-reaper.js +219 -0
- package/dist/services/codex-device-login-reaper.js.map +1 -0
- package/dist/services/codex-device-login-service.d.ts +355 -0
- package/dist/services/codex-device-login-service.d.ts.map +1 -0
- package/dist/services/codex-device-login-service.js +775 -0
- package/dist/services/codex-device-login-service.js.map +1 -0
- package/dist/services/companies.d.ts.map +1 -1
- package/dist/services/companies.js +7 -0
- package/dist/services/companies.js.map +1 -1
- package/dist/services/company-export-readme.js +1 -1
- package/dist/services/company-export-readme.js.map +1 -1
- package/dist/services/company-import-transfers.d.ts +55 -0
- package/dist/services/company-import-transfers.d.ts.map +1 -0
- package/dist/services/company-import-transfers.js +151 -0
- package/dist/services/company-import-transfers.js.map +1 -0
- package/dist/services/company-portability.d.ts.map +1 -1
- package/dist/services/company-portability.js +128 -3
- package/dist/services/company-portability.js.map +1 -1
- package/dist/services/company-transfer-runs.d.ts +136 -0
- package/dist/services/company-transfer-runs.d.ts.map +1 -0
- package/dist/services/company-transfer-runs.js +243 -0
- package/dist/services/company-transfer-runs.js.map +1 -0
- package/dist/services/cross-issue-influence-limit.d.ts +7 -1
- package/dist/services/cross-issue-influence-limit.d.ts.map +1 -1
- package/dist/services/cross-issue-influence-limit.js.map +1 -1
- package/dist/services/environment-config.d.ts +15 -0
- package/dist/services/environment-config.d.ts.map +1 -1
- package/dist/services/environment-config.js +56 -10
- package/dist/services/environment-config.js.map +1 -1
- package/dist/services/environment-execution-target.d.ts.map +1 -1
- package/dist/services/environment-execution-target.js +61 -11
- package/dist/services/environment-execution-target.js.map +1 -1
- package/dist/services/environment-run-orchestrator.d.ts +16 -1
- package/dist/services/environment-run-orchestrator.d.ts.map +1 -1
- package/dist/services/environment-run-orchestrator.js +1 -1
- package/dist/services/environment-run-orchestrator.js.map +1 -1
- package/dist/services/environment-runtime.d.ts +178 -2
- package/dist/services/environment-runtime.d.ts.map +1 -1
- package/dist/services/environment-runtime.js +1035 -96
- package/dist/services/environment-runtime.js.map +1 -1
- package/dist/services/environments.d.ts +75 -0
- package/dist/services/environments.d.ts.map +1 -1
- package/dist/services/environments.js +199 -8
- package/dist/services/environments.js.map +1 -1
- package/dist/services/execution-allowlist.d.ts +16 -4
- package/dist/services/execution-allowlist.d.ts.map +1 -1
- package/dist/services/execution-allowlist.js +35 -16
- package/dist/services/execution-allowlist.js.map +1 -1
- package/dist/services/execution-allowlist.test.js +33 -0
- package/dist/services/execution-allowlist.test.js.map +1 -1
- package/dist/services/execution-workspace-policy.d.ts +14 -1
- package/dist/services/execution-workspace-policy.d.ts.map +1 -1
- package/dist/services/execution-workspace-policy.js +29 -12
- package/dist/services/execution-workspace-policy.js.map +1 -1
- package/dist/services/execution-workspaces.d.ts +89 -0
- package/dist/services/execution-workspaces.d.ts.map +1 -1
- package/dist/services/execution-workspaces.js +997 -125
- package/dist/services/execution-workspaces.js.map +1 -1
- package/dist/services/heartbeat-run-summary.d.ts +1 -0
- package/dist/services/heartbeat-run-summary.d.ts.map +1 -1
- package/dist/services/heartbeat-run-summary.js +19 -3
- package/dist/services/heartbeat-run-summary.js.map +1 -1
- package/dist/services/heartbeat.d.ts +18 -2
- package/dist/services/heartbeat.d.ts.map +1 -1
- package/dist/services/heartbeat.js +369 -16
- package/dist/services/heartbeat.js.map +1 -1
- package/dist/services/import-write-types.d.ts +12 -0
- package/dist/services/import-write-types.d.ts.map +1 -1
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/instance-settings.d.ts +10 -1
- package/dist/services/instance-settings.d.ts.map +1 -1
- package/dist/services/instance-settings.js +16 -9
- package/dist/services/instance-settings.js.map +1 -1
- package/dist/services/issue-review-policy.d.ts +4 -0
- package/dist/services/issue-review-policy.d.ts.map +1 -1
- package/dist/services/issue-review-policy.js +18 -7
- package/dist/services/issue-review-policy.js.map +1 -1
- package/dist/services/issue-thread-interaction-resolution.d.ts +92 -0
- package/dist/services/issue-thread-interaction-resolution.d.ts.map +1 -0
- package/dist/services/issue-thread-interaction-resolution.js +198 -0
- package/dist/services/issue-thread-interaction-resolution.js.map +1 -0
- package/dist/services/issue-thread-interaction-resolution.test.d.ts +2 -0
- package/dist/services/issue-thread-interaction-resolution.test.d.ts.map +1 -0
- package/dist/services/issue-thread-interaction-resolution.test.js +152 -0
- package/dist/services/issue-thread-interaction-resolution.test.js.map +1 -0
- package/dist/services/issue-thread-interactions.d.ts +24 -4
- package/dist/services/issue-thread-interactions.d.ts.map +1 -1
- package/dist/services/issue-thread-interactions.js +360 -145
- package/dist/services/issue-thread-interactions.js.map +1 -1
- package/dist/services/issue-thread-interactions.test.js +18 -11
- package/dist/services/issue-thread-interactions.test.js.map +1 -1
- package/dist/services/issues.d.ts +3 -0
- package/dist/services/issues.d.ts.map +1 -1
- package/dist/services/issues.js +157 -33
- package/dist/services/issues.js.map +1 -1
- package/dist/services/local-service-supervisor.d.ts +2 -0
- package/dist/services/local-service-supervisor.d.ts.map +1 -1
- package/dist/services/local-service-supervisor.js +53 -3
- package/dist/services/local-service-supervisor.js.map +1 -1
- package/dist/services/managed-environments.d.ts +4 -1
- package/dist/services/managed-environments.d.ts.map +1 -1
- package/dist/services/managed-environments.js +86 -2
- package/dist/services/managed-environments.js.map +1 -1
- package/dist/services/managed-environments.test.js +164 -0
- package/dist/services/managed-environments.test.js.map +1 -1
- package/dist/services/onboarding-greeting.d.ts +7 -0
- package/dist/services/onboarding-greeting.d.ts.map +1 -0
- package/dist/services/onboarding-greeting.js +27 -0
- package/dist/services/onboarding-greeting.js.map +1 -0
- package/dist/services/onboarding-greeting.test.d.ts +2 -0
- package/dist/services/onboarding-greeting.test.d.ts.map +1 -0
- package/dist/services/onboarding-greeting.test.js +32 -0
- package/dist/services/onboarding-greeting.test.js.map +1 -0
- package/dist/services/onboarding-seed.d.ts +57 -0
- package/dist/services/onboarding-seed.d.ts.map +1 -0
- package/dist/services/onboarding-seed.js +344 -0
- package/dist/services/onboarding-seed.js.map +1 -0
- package/dist/services/plan-review-context.d.ts +9 -1
- package/dist/services/plan-review-context.d.ts.map +1 -1
- package/dist/services/plan-review-context.js +192 -0
- package/dist/services/plan-review-context.js.map +1 -1
- package/dist/services/plugin-environment-driver.d.ts +46 -0
- package/dist/services/plugin-environment-driver.d.ts.map +1 -1
- package/dist/services/plugin-environment-driver.js +50 -0
- package/dist/services/plugin-environment-driver.js.map +1 -1
- package/dist/services/plugin-host-services.d.ts +1 -0
- package/dist/services/plugin-host-services.d.ts.map +1 -1
- package/dist/services/plugin-host-services.js +51 -28
- package/dist/services/plugin-host-services.js.map +1 -1
- package/dist/services/plugin-loader.d.ts +10 -1
- package/dist/services/plugin-loader.d.ts.map +1 -1
- package/dist/services/plugin-loader.js +43 -2
- package/dist/services/plugin-loader.js.map +1 -1
- package/dist/services/plugin-worker-manager.d.ts +61 -0
- package/dist/services/plugin-worker-manager.d.ts.map +1 -1
- package/dist/services/plugin-worker-manager.js +248 -1
- package/dist/services/plugin-worker-manager.js.map +1 -1
- package/dist/services/productivity-review.d.ts.map +1 -1
- package/dist/services/productivity-review.js +5 -0
- package/dist/services/productivity-review.js.map +1 -1
- package/dist/services/recovery/service.d.ts.map +1 -1
- package/dist/services/recovery/service.js +5 -1
- package/dist/services/recovery/service.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.d.ts +16 -1
- package/dist/services/recovery/successful-run-handoff.d.ts.map +1 -1
- package/dist/services/recovery/successful-run-handoff.js +32 -0
- package/dist/services/recovery/successful-run-handoff.js.map +1 -1
- package/dist/services/recovery/successful-run-handoff.test.js +51 -1
- package/dist/services/recovery/successful-run-handoff.test.js.map +1 -1
- package/dist/services/routines.d.ts.map +1 -1
- package/dist/services/routines.js +48 -4
- package/dist/services/routines.js.map +1 -1
- package/dist/services/runtime-exposure/broker-client.d.ts +88 -0
- package/dist/services/runtime-exposure/broker-client.d.ts.map +1 -0
- package/dist/services/runtime-exposure/broker-client.js +217 -0
- package/dist/services/runtime-exposure/broker-client.js.map +1 -0
- package/dist/services/runtime-exposure/broker-client.test.d.ts +2 -0
- package/dist/services/runtime-exposure/broker-client.test.d.ts.map +1 -0
- package/dist/services/runtime-exposure/broker-client.test.js +185 -0
- package/dist/services/runtime-exposure/broker-client.test.js.map +1 -0
- package/dist/services/runtime-exposure/exposure-manager.d.ts +135 -0
- package/dist/services/runtime-exposure/exposure-manager.d.ts.map +1 -0
- package/dist/services/runtime-exposure/exposure-manager.js +247 -0
- package/dist/services/runtime-exposure/exposure-manager.js.map +1 -0
- package/dist/services/runtime-exposure/exposure-manager.test.d.ts +2 -0
- package/dist/services/runtime-exposure/exposure-manager.test.d.ts.map +1 -0
- package/dist/services/runtime-exposure/exposure-manager.test.js +384 -0
- package/dist/services/runtime-exposure/exposure-manager.test.js.map +1 -0
- package/dist/services/runtime-exposure/loopback-listener.d.ts +33 -0
- package/dist/services/runtime-exposure/loopback-listener.d.ts.map +1 -0
- package/dist/services/runtime-exposure/loopback-listener.js +146 -0
- package/dist/services/runtime-exposure/loopback-listener.js.map +1 -0
- package/dist/services/runtime-exposure/loopback-listener.test.d.ts +2 -0
- package/dist/services/runtime-exposure/loopback-listener.test.d.ts.map +1 -0
- package/dist/services/runtime-exposure/loopback-listener.test.js +116 -0
- package/dist/services/runtime-exposure/loopback-listener.test.js.map +1 -0
- package/dist/services/runtime-exposure/port-pair.d.ts +47 -0
- package/dist/services/runtime-exposure/port-pair.d.ts.map +1 -0
- package/dist/services/runtime-exposure/port-pair.js +57 -0
- package/dist/services/runtime-exposure/port-pair.js.map +1 -0
- package/dist/services/runtime-exposure/port-pair.test.d.ts +2 -0
- package/dist/services/runtime-exposure/port-pair.test.d.ts.map +1 -0
- package/dist/services/runtime-exposure/port-pair.test.js +92 -0
- package/dist/services/runtime-exposure/port-pair.test.js.map +1 -0
- package/dist/services/runtime-exposure/port-reservation.d.ts +211 -0
- package/dist/services/runtime-exposure/port-reservation.d.ts.map +1 -0
- package/dist/services/runtime-exposure/port-reservation.js +380 -0
- package/dist/services/runtime-exposure/port-reservation.js.map +1 -0
- package/dist/services/runtime-exposure/port-reservation.test.d.ts +2 -0
- package/dist/services/runtime-exposure/port-reservation.test.d.ts.map +1 -0
- package/dist/services/runtime-exposure/port-reservation.test.js +456 -0
- package/dist/services/runtime-exposure/port-reservation.test.js.map +1 -0
- package/dist/services/runtime-exposure/tailscale-hostname.d.ts +11 -0
- package/dist/services/runtime-exposure/tailscale-hostname.d.ts.map +1 -0
- package/dist/services/runtime-exposure/tailscale-hostname.js +36 -0
- package/dist/services/runtime-exposure/tailscale-hostname.js.map +1 -0
- package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts +2 -0
- package/dist/services/runtime-exposure/tailscale-hostname.test.d.ts.map +1 -0
- package/dist/services/runtime-exposure/tailscale-hostname.test.js +14 -0
- package/dist/services/runtime-exposure/tailscale-hostname.test.js.map +1 -0
- package/dist/services/sandbox-orphan-cleanup-spool.d.ts +64 -0
- package/dist/services/sandbox-orphan-cleanup-spool.d.ts.map +1 -0
- package/dist/services/sandbox-orphan-cleanup-spool.js +134 -0
- package/dist/services/sandbox-orphan-cleanup-spool.js.map +1 -0
- package/dist/services/sandbox-provider-runtime.d.ts +15 -0
- package/dist/services/sandbox-provider-runtime.d.ts.map +1 -1
- package/dist/services/sandbox-provider-runtime.js +1 -0
- package/dist/services/sandbox-provider-runtime.js.map +1 -1
- package/dist/services/secrets.d.ts +103 -0
- package/dist/services/secrets.d.ts.map +1 -1
- package/dist/services/secrets.js +432 -10
- package/dist/services/secrets.js.map +1 -1
- package/dist/services/setup-token-session.d.ts +630 -0
- package/dist/services/setup-token-session.d.ts.map +1 -0
- package/dist/services/setup-token-session.js +1005 -0
- package/dist/services/setup-token-session.js.map +1 -0
- package/dist/services/setup-token-session.test.d.ts +2 -0
- package/dist/services/setup-token-session.test.d.ts.map +1 -0
- package/dist/services/setup-token-session.test.js +1143 -0
- package/dist/services/setup-token-session.test.js.map +1 -0
- package/dist/services/setup-token-transport-binding.d.ts +202 -0
- package/dist/services/setup-token-transport-binding.d.ts.map +1 -0
- package/dist/services/setup-token-transport-binding.js +386 -0
- package/dist/services/setup-token-transport-binding.js.map +1 -0
- package/dist/services/setup-token-transport-binding.test.d.ts +2 -0
- package/dist/services/setup-token-transport-binding.test.d.ts.map +1 -0
- package/dist/services/setup-token-transport-binding.test.js +761 -0
- package/dist/services/setup-token-transport-binding.test.js.map +1 -0
- package/dist/services/summary-slots.d.ts.map +1 -1
- package/dist/services/summary-slots.js +40 -6
- package/dist/services/summary-slots.js.map +1 -1
- package/dist/services/task-watchdogs.js +2 -2
- package/dist/services/task-watchdogs.js.map +1 -1
- package/dist/services/tool-access-policy.d.ts +1 -1
- package/dist/services/tool-access.d.ts.map +1 -1
- package/dist/services/tool-access.js +29 -11
- package/dist/services/tool-access.js.map +1 -1
- package/dist/services/tool-gateway.d.ts +1 -1
- package/dist/services/tool-gateway.d.ts.map +1 -1
- package/dist/services/tool-gateway.js +56 -5
- package/dist/services/tool-gateway.js.map +1 -1
- package/dist/services/workspace-file-resources.d.ts +10 -1
- package/dist/services/workspace-file-resources.d.ts.map +1 -1
- package/dist/services/workspace-file-resources.js +258 -6
- package/dist/services/workspace-file-resources.js.map +1 -1
- package/dist/services/workspace-git-operation-scheduler.d.ts +135 -0
- package/dist/services/workspace-git-operation-scheduler.d.ts.map +1 -0
- package/dist/services/workspace-git-operation-scheduler.js +655 -0
- package/dist/services/workspace-git-operation-scheduler.js.map +1 -0
- package/dist/services/workspace-git-operation-scheduler.test.d.ts +2 -0
- package/dist/services/workspace-git-operation-scheduler.test.d.ts.map +1 -0
- package/dist/services/workspace-git-operation-scheduler.test.js +367 -0
- package/dist/services/workspace-git-operation-scheduler.test.js.map +1 -0
- package/dist/services/workspace-operations.d.ts +60 -0
- package/dist/services/workspace-operations.d.ts.map +1 -1
- package/dist/services/workspace-operations.js +384 -19
- package/dist/services/workspace-operations.js.map +1 -1
- package/dist/services/workspace-runtime-exposure-backfill.test.d.ts +2 -0
- package/dist/services/workspace-runtime-exposure-backfill.test.d.ts.map +1 -0
- package/dist/services/workspace-runtime-exposure-backfill.test.js +149 -0
- package/dist/services/workspace-runtime-exposure-backfill.test.js.map +1 -0
- package/dist/services/workspace-runtime-exposure.test.d.ts +2 -0
- package/dist/services/workspace-runtime-exposure.test.d.ts.map +1 -0
- package/dist/services/workspace-runtime-exposure.test.js +517 -0
- package/dist/services/workspace-runtime-exposure.test.js.map +1 -0
- package/dist/services/workspace-runtime-leases.d.ts +90 -0
- package/dist/services/workspace-runtime-leases.d.ts.map +1 -0
- package/dist/services/workspace-runtime-leases.js +252 -0
- package/dist/services/workspace-runtime-leases.js.map +1 -0
- package/dist/services/workspace-runtime-read-model.d.ts +12 -0
- package/dist/services/workspace-runtime-read-model.d.ts.map +1 -1
- package/dist/services/workspace-runtime-read-model.test.js +3 -0
- package/dist/services/workspace-runtime-read-model.test.js.map +1 -1
- package/dist/services/workspace-runtime-ready-comment.test.js +1 -0
- package/dist/services/workspace-runtime-ready-comment.test.js.map +1 -1
- package/dist/services/workspace-runtime.d.ts +160 -2
- package/dist/services/workspace-runtime.d.ts.map +1 -1
- package/dist/services/workspace-runtime.js +1693 -141
- package/dist/services/workspace-runtime.js.map +1 -1
- package/dist/shutdown.d.ts +24 -0
- package/dist/shutdown.d.ts.map +1 -1
- package/dist/shutdown.js +37 -0
- package/dist/shutdown.js.map +1 -1
- package/dist/shutdown.test.js +113 -1
- package/dist/shutdown.test.js.map +1 -1
- package/dist/startup-banner.js +1 -1
- package/dist/startup-banner.js.map +1 -1
- package/dist/url-utils.d.ts +26 -0
- package/dist/url-utils.d.ts.map +1 -0
- package/dist/url-utils.js +59 -0
- package/dist/url-utils.js.map +1 -0
- package/dist/url-utils.test.d.ts +2 -0
- package/dist/url-utils.test.d.ts.map +1 -0
- package/dist/url-utils.test.js +55 -0
- package/dist/url-utils.test.js.map +1 -0
- package/dist/worktree-config.d.ts.map +1 -1
- package/dist/worktree-config.js +3 -17
- package/dist/worktree-config.js.map +1 -1
- package/package.json +20 -20
- package/skills/paperclip/SKILL.md +16 -11
- package/skills/paperclip/references/api-reference.md +8 -5
- package/skills/paperclip-board/SKILL.md +1 -1
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-wvSnvD6-.js → abnfDiagram-N423BO3Z-BvgoV-u5.js} +1 -1
- package/ui-dist/assets/{arc-Bx0V1JsN.js → arc-BInnUBUU.js} +1 -1
- package/ui-dist/assets/architectureDiagram-T3A2C74G-Dg4MziOm.js +36 -0
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-BROcLaUk.js → blockDiagram-VBNYF7ZC-D9kzAkLN.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV-D-V3kgju.js → c4Diagram-5PPSVZJV-BwzsgV_0.js} +1 -1
- package/ui-dist/assets/channel-qoSYbCn0.js +1 -0
- package/ui-dist/assets/{chunk-32BRIVSS-qillH3H6.js → chunk-2GRJ4B5K-D2EQ9vrl.js} +1 -1
- package/ui-dist/assets/{chunk-2Q5K7J3B-D19cVnii.js → chunk-2Q5K7J3B-DOA_8fSd.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-CLUswrZg.js → chunk-5RXB4S5H-CT5ukRiT.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-Cwlp02Lx.js → chunk-5VM5RSS4-D2gOlkyB.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-yKciyq6E.js → chunk-6Q2QTUOP-DUb6aG8c.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-DjJ75g0_.js → chunk-GF5L2VYU-BPabG3Cx.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-BjepyzEb.js → chunk-JWPE2WC7-scNfhlZz.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN-CiEmy8-R.js → chunk-KBJHAD2P-BcRAzRpq.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-ed7mQM9j.js → chunk-RYQCIY6F-BiZ0-EGh.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-BiI83zoO.js → chunk-XXDRQBXY-lJULxFIE.js} +1 -1
- package/ui-dist/assets/classDiagram-JCYQIIEL-B24u1REs.js +1 -0
- package/ui-dist/assets/classDiagram-v2-OCEON4UE-B24u1REs.js +1 -0
- package/ui-dist/assets/{cose-bilkent-JH36ORCC-DIdQf5ta.js → cose-bilkent-JH36ORCC-Do8pe8JB.js} +1 -1
- package/ui-dist/assets/{cynefin-VYW2F7L2-B2m9xxh8.js → cynefin-VYW2F7L2-B34jJY1x.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-CGVdB0kX.js → cynefinDiagram-MW4NZA55-DQ8QK8JK.js} +1 -1
- package/ui-dist/assets/cytoscape.esm-DShqj__B.js +321 -0
- package/ui-dist/assets/{dagre-VKFMJZFB-CskKiZwJ.js → dagre-VZM6K2ZE-B7lSKgxH.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-D2cn5eIN.js → diagram-7IWD3JNH-B3hLAFGE.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-oa5Zu0XG.js → diagram-B4RE2ZJO-CH9Ro9e0.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-fymti5Js.js → diagram-LBJQPF4R-DdPVMii7.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-BWF2ZCsw.js → diagram-Q27KOJAE-f5VXswjc.js} +1 -1
- package/ui-dist/assets/diagram-UB23O5K3-DGqxixsR.js +41 -0
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-CJ1d3D5q.js → ebnfDiagram-BXEA7PRR-C-1klg_w.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-BJCPa7kK.js → erDiagram-JOGREHBK-Dt4qK4gv.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-BWjWuz6r.js → flowDiagram-UKHOOZJN-9ZlrTuET.js} +5 -5
- package/ui-dist/assets/{ganttDiagram-NO4QXBWP-Dv1KKjoM.js → ganttDiagram-PKOTCBZU-CE3lyvFq.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-9X7vgL0A.js → gitGraphDiagram-DS77QQ5N-HH2fSgad.js} +1 -1
- package/ui-dist/assets/{index-CpaK48ok.js → index-B61ORkIb.js} +1 -1
- package/ui-dist/assets/index-BHiUiToL.css +1 -0
- package/ui-dist/assets/{index-D2YCrA-D.js → index-BY5ITKmG.js} +1 -1
- package/ui-dist/assets/{index-9JMDmXc5.js → index-BbAhzCq8.js} +1 -1
- package/ui-dist/assets/{index-C5qHatpp.js → index-Bc9g6q63.js} +1 -1
- package/ui-dist/assets/{index-DuI6e5mP.js → index-Bf7tyGxE.js} +1 -1
- package/ui-dist/assets/{index-BahRsj0s.js → index-BvFx-4KW.js} +1 -1
- package/ui-dist/assets/{index-DKlsEDV7.js → index-C1mpWCcn.js} +1 -1
- package/ui-dist/assets/{index-DIXaXZrW.js → index-CHXG4GPL.js} +1 -1
- package/ui-dist/assets/{index-DcfO6NoY.js → index-CLu3Tfpv.js} +1 -1
- package/ui-dist/assets/{index-Cfz6P0Qq.js → index-CQhaM6qd.js} +1 -1
- package/ui-dist/assets/{index-D9d3sl6K.js → index-CVTL3aAL.js} +1 -1
- package/ui-dist/assets/{index-BexA3NAl.js → index-CcRTJ93J.js} +1 -1
- package/ui-dist/assets/{index-CnBrDwGT.js → index-Cm6iEYPt.js} +247 -223
- package/ui-dist/assets/{index-ByGcu-MB.js → index-DQIrrn2L.js} +1 -1
- package/ui-dist/assets/{index-DKEzHO7c.js → index-DU4U1fuv.js} +1 -1
- package/ui-dist/assets/{index-pqJO8FsV.js → index-DV2FOgyD.js} +1 -1
- package/ui-dist/assets/{index-tDuieU3u.js → index-DceuCrr8.js} +1 -1
- package/ui-dist/assets/{index-CjN2lm_w.js → index-DlVBBF8o.js} +1 -1
- package/ui-dist/assets/{index-0yJtUsFh.js → index-DsdVneqe.js} +1 -1
- package/ui-dist/assets/{index-C2a4_IKU.js → index-DvFUXI-Q.js} +1 -1
- package/ui-dist/assets/{index-BTMiDMYv.js → index-FHzrJ7iQ.js} +1 -1
- package/ui-dist/assets/{index-cUQXJtLK.js → index-diFs0CNC.js} +1 -1
- package/ui-dist/assets/{index-C96LHV1A.js → index-hkanj4t6.js} +1 -1
- package/ui-dist/assets/{index-rDI1Q4r9.js → index-yRJnh4vx.js} +1 -1
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-CHIhutSb.js → infoDiagram-6WML65LV-2jai2fw-.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-ChaD7-5_.js → ishikawaDiagram-WSZJBQD7-CUmGhmEb.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-M5T2sMgn.js → journeyDiagram-NVQOT4AX-CGQuPDO1.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-B8H6R_Wc.js → kanban-definition-27J2QSJJ-zgEPcbHl.js} +1 -1
- package/ui-dist/assets/{linear-cpPKlVCq.js → linear-BVf5VtLS.js} +1 -1
- package/ui-dist/assets/mermaid.core-D2TFkGfI.js +308 -0
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-Bf6POzvm.js → mindmap-definition-FAOFIHXS-xoAQ0Lb_.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-BJbGktAV.js → pegDiagram-VL7TDLO6-CBR1PWLr.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-BmSN9Bio.js → pieDiagram-7S7Q4E2Y-BMPnzK_e.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-JUpqQCx_.js → quadrantDiagram-CIZ2JOQS-CPtVUPkR.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-B8k8T4-2.js → railroadDiagram-AXF67PYL-DC0lsZV1.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-b32heq7Q.js → requirementDiagram-LRYGKXZP-BF6k05SB.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DC3s-xgf.js → sankeyDiagram-W5VNT64P-CRN0ibiP.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-BsCFyv_E.js → sequenceDiagram-SI44F4Z6-CQtTiPWm.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-S7xeCud3.js → sizeCapture-X5ZJPWSS-BdjuGZd1.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-58O3ofoS.js → stateDiagram-OKZ733FA-B99onxyh.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-UEYNNEHI-bzFlYBZ1.js +1 -0
- package/ui-dist/assets/{swimlanes-5IMT3BWC-DFCVG0iF.js → swimlanes-SLNWSIFB-BIByPbrL.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-ULZ7WXOC-DgdkBnuK.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-C0c0SkQu.js → timeline-definition-Z64GVDOM-I1NgOcn9.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-B1cEx8GQ.js → vennDiagram-T6HMQDX7-D6t2Njcl.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-BQhDkOx9.js → wardleyDiagram-T6FBY63Y-BiYKm9gh.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-YSQp1N0i.js → xychartDiagram-ELKLHX3M-BhwXKWIa.js} +4 -4
- package/ui-dist/index.html +3 -3
- package/ui-dist/sw.js +8 -3
- package/ui-dist/assets/architectureDiagram-ZJ3FMSHR-BcJt8Ia1.js +0 -36
- package/ui-dist/assets/channel-ZRpjyNiZ.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-CCzzzCgk.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CCzzzCgk.js +0 -1
- package/ui-dist/assets/cytoscape.esm-D8joxN9f.js +0 -321
- package/ui-dist/assets/diagram-WEI45ONY-M3WOC2tK.js +0 -41
- package/ui-dist/assets/index-BqfqfoQX.css +0 -1
- package/ui-dist/assets/mermaid.core-MCazc_od.js +0 -308
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-_VY5dpDG.js +0 -1
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BJhUjfns.js +0 -8
|
@@ -1,16 +1,187 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { and, eq, inArray } from "drizzle-orm";
|
|
3
3
|
import { companySecrets, companySecretVersions, environmentLeases } from "@paperclipai/db";
|
|
4
|
+
import { resolveDeclaredSandboxCapabilities } from "@paperclipai/shared";
|
|
4
5
|
import { ensureSshWorkspaceReady } from "@paperclipai/adapter-utils/ssh";
|
|
5
6
|
import { getActiveStepContext, runWithRuntimeParent, } from "@paperclipai/adapter-utils/acpx-engine/startup-timing";
|
|
6
7
|
import { environmentService } from "./environments.js";
|
|
7
|
-
import { collectEnvironmentSecretRefs, parseEnvironmentDriverConfig, resolveEnvironmentDriverConfigForRuntime, stripSandboxProviderEnvelope, } from "./environment-config.js";
|
|
8
|
+
import { collectEnvironmentSecretRefs, parseEnvironmentDriverConfig, resolveEnvironmentDriverConfigForRuntime, resolveSandboxCleanupConfigSecrets, stripSandboxProviderEnvelope, } from "./environment-config.js";
|
|
8
9
|
import { createEffectiveRunConfigFingerprints, } from "./effective-run-config-fingerprints.js";
|
|
9
10
|
import { acquireSandboxProviderLease, destroySandboxProviderLease, findReusableSandboxProviderLeaseId, getSandboxProvider as getBuiltinSandboxProvider, isBuiltinSandboxProvider, releaseSandboxProviderLease, sandboxConfigFromLeaseMetadata, sandboxConfigFromLeaseMetadataLoose, } from "./sandbox-provider-runtime.js";
|
|
10
11
|
import { pluginRegistryService } from "./plugin-registry.js";
|
|
11
|
-
import { destroyPluginEnvironmentLease, executePluginEnvironmentCommand, realizePluginEnvironmentWorkspace, resolvePluginSandboxProviderDriverByKey, resolvePluginExecuteRpcTimeoutMs, resumePluginEnvironmentLease, } from "./plugin-environment-driver.js";
|
|
12
|
+
import { REUSABLE_LEASE_WORKER_METHODS, destroyPluginEnvironmentLease, executePluginEnvironmentCommand, realizePluginEnvironmentWorkspace, resolvePluginSandboxProviderDriverByKey, resolvePluginSandboxProviderDriverById, resolvePluginExecuteRpcTimeoutMs, resumePluginEnvironmentLease, } from "./plugin-environment-driver.js";
|
|
12
13
|
import { collectSecretRefPaths } from "./json-schema-secret-refs.js";
|
|
13
14
|
import { buildWorkspaceRealizationRecordFromDriverInput } from "./workspace-realization.js";
|
|
15
|
+
import { createSandboxOrphanCleanupSpool, } from "./sandbox-orphan-cleanup-spool.js";
|
|
16
|
+
import { logger } from "../middleware/logger.js";
|
|
17
|
+
// The constant error kind for the durable orphan-cleanup-write-failed log. The
|
|
18
|
+
// log never reads the caught exception, because the exception can carry a
|
|
19
|
+
// credential in its name, code, message, cause, or stack. So the log records
|
|
20
|
+
// this constant and the plain provider identifiers only.
|
|
21
|
+
const SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND = "sandbox_orphan_cleanup_write_failed";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Sandbox capability contract — one normalizer for both branches
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export const SANDBOX_CAPABILITY_KEYS = [
|
|
26
|
+
"reusableLeases",
|
|
27
|
+
"nativeSyncIn",
|
|
28
|
+
"nativeSyncOut",
|
|
29
|
+
"persistentProcessSessions",
|
|
30
|
+
"independentControlCommands",
|
|
31
|
+
"incrementalSessionOutput",
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* Opt-in capability keys. Most capabilities are a worker property: an absent
|
|
35
|
+
* declaration defers to the verified baseline and allows the capability. An
|
|
36
|
+
* opt-in capability is a behavioral guarantee that only some providers make, so
|
|
37
|
+
* an absent declaration denies it. A generic one-shot provider must not get an
|
|
38
|
+
* opt-in capability just because its worker verifies a shared verb such as
|
|
39
|
+
* `environmentExecute`. Only a provider that declares the key `true` (and still
|
|
40
|
+
* verifies the prerequisites and passes narrowing) gets the capability.
|
|
41
|
+
*/
|
|
42
|
+
const SANDBOX_CAPABILITY_OPT_IN_KEYS = new Set([
|
|
43
|
+
"incrementalSessionOutput",
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* Verified prerequisite mapping: the worker methods each capability requires.
|
|
47
|
+
*
|
|
48
|
+
* Each capability maps to a list of requirement groups. A group holds one or
|
|
49
|
+
* more verbs; the group is met when the runtime verified AT LEAST ONE verb in
|
|
50
|
+
* it. A capability verifies only when EVERY group is met.
|
|
51
|
+
*
|
|
52
|
+
* The verbs are the worker method names from the worker discovery list (the
|
|
53
|
+
* plugin worker reports them from `handleInitialize`). A built-in provider maps
|
|
54
|
+
* its own methods to the same verb names through
|
|
55
|
+
* {@link builtinSandboxProviderVerifiedMethods}, so both branches share this
|
|
56
|
+
* mapping and the one normalizer below.
|
|
57
|
+
*
|
|
58
|
+
* The mapping was audited against the worker discovery list and the runtime
|
|
59
|
+
* execution guards:
|
|
60
|
+
* - `nativeSyncIn`/`nativeSyncOut` require the matching sync verb; the native
|
|
61
|
+
* sync guard checks both verbs before it routes a lease to the native hook.
|
|
62
|
+
* - `reusableLeases` requires `environmentResumeLease` (reattach),
|
|
63
|
+
* `environmentReleaseLease` (end-of-run release), and `environmentDestroyLease`
|
|
64
|
+
* (stale-lease teardown). All three run on the reuse path: the runtime resumes
|
|
65
|
+
* or releases the lease, and it destroys the stale lease when a resume fails
|
|
66
|
+
* before it acquires a fresh lease.
|
|
67
|
+
* - `persistentProcessSessions` and `independentControlCommands` require
|
|
68
|
+
* `environmentExecute`; both run commands through it.
|
|
69
|
+
* - `incrementalSessionOutput` requires `environmentExecute` too, because the
|
|
70
|
+
* provider tails the session log through it. The verified verb is necessary
|
|
71
|
+
* but not sufficient: this key is opt-in, so the declaration is the real gate
|
|
72
|
+
* (see {@link SANDBOX_CAPABILITY_OPT_IN_KEYS}).
|
|
73
|
+
*/
|
|
74
|
+
const SANDBOX_CAPABILITY_PREREQUISITE_METHODS = {
|
|
75
|
+
// Reusable leases require ALL reuse verbs. Each verb is its own required
|
|
76
|
+
// group, so every one must be verified. The list function that publishes
|
|
77
|
+
// provider-level reusable support checks the same verbs; both read from
|
|
78
|
+
// `REUSABLE_LEASE_WORKER_METHODS`, so the runtime guard and the published
|
|
79
|
+
// value cannot drift.
|
|
80
|
+
reusableLeases: REUSABLE_LEASE_WORKER_METHODS.map((method) => [method]),
|
|
81
|
+
nativeSyncIn: [["environmentSyncIn"]],
|
|
82
|
+
nativeSyncOut: [["environmentSyncOut"]],
|
|
83
|
+
persistentProcessSessions: [["environmentExecute"]],
|
|
84
|
+
independentControlCommands: [["environmentExecute"]],
|
|
85
|
+
incrementalSessionOutput: [["environmentExecute"]],
|
|
86
|
+
};
|
|
87
|
+
function capabilityIsVerified(key, verifiedMethods) {
|
|
88
|
+
return SANDBOX_CAPABILITY_PREREQUISITE_METHODS[key].every((group) => group.some((verb) => verifiedMethods.has(verb)));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Map a built-in sandbox provider's own methods to the worker verb names the
|
|
92
|
+
* prerequisite mapping uses, so the built-in branch and the plug-in branch feed
|
|
93
|
+
* the SAME normalizer. A built-in provider has no native sync hooks, so it never
|
|
94
|
+
* verifies a sync verb. It verifies `environmentExecute` when it implements
|
|
95
|
+
* `execute`, and the reuse verbs only when it declares `supportsReusableLeases`.
|
|
96
|
+
* A built-in reusable provider destroys its own leases in-process, so it
|
|
97
|
+
* verifies `environmentDestroyLease` with the two reuse verbs.
|
|
98
|
+
*/
|
|
99
|
+
export function builtinSandboxProviderVerifiedMethods(provider) {
|
|
100
|
+
if (!provider)
|
|
101
|
+
return [];
|
|
102
|
+
const methods = [];
|
|
103
|
+
if (typeof provider.execute === "function") {
|
|
104
|
+
methods.push("environmentExecute");
|
|
105
|
+
}
|
|
106
|
+
if (provider.supportsReusableLeases === true) {
|
|
107
|
+
methods.push("environmentResumeLease", "environmentReleaseLease", "environmentDestroyLease");
|
|
108
|
+
}
|
|
109
|
+
return methods;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The one normalizer for the sandbox capability contract. It resolves the
|
|
113
|
+
* effective capability as verified ∩ declared ∩ narrowing.
|
|
114
|
+
*
|
|
115
|
+
* - `verifiedMethods` is the runtime's verified worker verb list (the plug-in
|
|
116
|
+
* worker's `supportedMethods`, or a built-in provider mapped through
|
|
117
|
+
* {@link builtinSandboxProviderVerifiedMethods}). A missing or empty list
|
|
118
|
+
* verifies nothing, so every capability resolves `false` (fail closed).
|
|
119
|
+
* - `declared` is the provider's declaration. An absent flag defers to the
|
|
120
|
+
* verified discovery baseline; it never grants a capability. A present flag
|
|
121
|
+
* can only remove a verified capability, never add one.
|
|
122
|
+
* - `narrowing` is the per-target restriction from the config or lease. An
|
|
123
|
+
* absent key applies no restriction; a `false` value removes the capability.
|
|
124
|
+
*
|
|
125
|
+
* A declaration never grants a capability the runtime did not verify, so a
|
|
126
|
+
* declared capability whose worker lacks a prerequisite verb resolves `false`.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveEffectiveSandboxCapabilities(input) {
|
|
129
|
+
const verifiedMethods = new Set(input.verifiedMethods ?? []);
|
|
130
|
+
const declared = input.declared ?? {};
|
|
131
|
+
const narrowing = input.narrowing ?? {};
|
|
132
|
+
const resolve = (key) => {
|
|
133
|
+
const verified = capabilityIsVerified(key, verifiedMethods);
|
|
134
|
+
// An absent declaration defers to the verified baseline for a worker-property
|
|
135
|
+
// capability (true = no extra restriction), but denies an opt-in capability
|
|
136
|
+
// (false). A present declaration can only narrow.
|
|
137
|
+
const declaredDefault = SANDBOX_CAPABILITY_OPT_IN_KEYS.has(key) ? false : true;
|
|
138
|
+
const declaredAllows = declared[key] ?? declaredDefault;
|
|
139
|
+
// An absent narrowing applies no restriction.
|
|
140
|
+
const narrowingAllows = narrowing[key] ?? true;
|
|
141
|
+
return verified && declaredAllows && narrowingAllows;
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
reusableLeases: resolve("reusableLeases"),
|
|
145
|
+
nativeSyncIn: resolve("nativeSyncIn"),
|
|
146
|
+
nativeSyncOut: resolve("nativeSyncOut"),
|
|
147
|
+
persistentProcessSessions: resolve("persistentProcessSessions"),
|
|
148
|
+
independentControlCommands: resolve("independentControlCommands"),
|
|
149
|
+
incrementalSessionOutput: resolve("incrementalSessionOutput"),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build the per-target narrowing for a sandbox lease. Narrowing removes a
|
|
154
|
+
* capability that the provider verified and declared but that this specific
|
|
155
|
+
* lease or config cannot use. Each source is grounded in existing runtime
|
|
156
|
+
* behavior:
|
|
157
|
+
* - `reusableLeases` follows this lease's resolved policy (an ephemeral lease
|
|
158
|
+
* never reuses).
|
|
159
|
+
* - a Kubernetes Job lease disables native sync (mirrors the native sync guard,
|
|
160
|
+
* which falls back for a `job` backend or a `nativeFileSyncUnsupported` lease).
|
|
161
|
+
* - `configResolutionFailed` marks that the runtime could not resolve the
|
|
162
|
+
* provider config. A provider whose config cannot be resolved is untrusted, so
|
|
163
|
+
* the runtime fails closed and narrows `persistentProcessSessions` and
|
|
164
|
+
* `incrementalSessionOutput` to false. An empty config alone does not fail
|
|
165
|
+
* closed; only a resolution error does.
|
|
166
|
+
*/
|
|
167
|
+
export function buildSandboxCapabilityNarrowing(input) {
|
|
168
|
+
const narrowing = {};
|
|
169
|
+
const metadata = input.leaseMetadata ?? {};
|
|
170
|
+
narrowing.reusableLeases = input.leasePolicy === "reuse_by_environment";
|
|
171
|
+
if (metadata.backend === "job" || metadata.nativeFileSyncUnsupported === true) {
|
|
172
|
+
narrowing.nativeSyncIn = false;
|
|
173
|
+
narrowing.nativeSyncOut = false;
|
|
174
|
+
}
|
|
175
|
+
if (input.configResolutionFailed === true) {
|
|
176
|
+
// The runtime could not resolve the provider config, so it fails closed and
|
|
177
|
+
// denies persistent process sessions and incremental session output. The
|
|
178
|
+
// session-output streaming gate reads `incrementalSessionOutput`, so it must
|
|
179
|
+
// narrow with the persistent-session gate to keep the fail-closed behavior.
|
|
180
|
+
narrowing.persistentProcessSessions = false;
|
|
181
|
+
narrowing.incrementalSessionOutput = false;
|
|
182
|
+
}
|
|
183
|
+
return narrowing;
|
|
184
|
+
}
|
|
14
185
|
export function buildEnvironmentLeaseContext(input) {
|
|
15
186
|
return {
|
|
16
187
|
executionWorkspaceId: input.persistedExecutionWorkspace?.id ?? null,
|
|
@@ -70,14 +241,51 @@ function resolvePluginSandboxRpcTimeoutMs(config) {
|
|
|
70
241
|
config,
|
|
71
242
|
});
|
|
72
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* The acquire provisioned a remote sandbox, the lease insert rejected it, the
|
|
246
|
+
* compensating teardown failed, and every retry of the durable pending-cleanup
|
|
247
|
+
* write also failed. A live sandbox now has no lease row and no cleanup record,
|
|
248
|
+
* so no sweep can find it. The acquire throws this error so the failure stays visible
|
|
249
|
+
* and never resolves to a clean rejection. The `cause` field holds the original
|
|
250
|
+
* lease insert rejection. The `cleanupWriteError` field holds the write failure.
|
|
251
|
+
*/
|
|
252
|
+
export class SandboxOrphanCleanupWriteError extends Error {
|
|
253
|
+
provider;
|
|
254
|
+
providerLeaseId;
|
|
255
|
+
cleanupWriteError;
|
|
256
|
+
constructor(input) {
|
|
257
|
+
super(`Sandbox provider "${input.provider}" leaked lease ` +
|
|
258
|
+
`"${input.providerLeaseId ?? "unknown"}": the acquire could not tear it down ` +
|
|
259
|
+
"and could not record a durable pending-cleanup row. The live sandbox has no " +
|
|
260
|
+
"durable cleanup state and needs a manual teardown.", { cause: input.cause });
|
|
261
|
+
this.name = "SandboxOrphanCleanupWriteError";
|
|
262
|
+
this.provider = input.provider;
|
|
263
|
+
this.providerLeaseId = input.providerLeaseId;
|
|
264
|
+
this.cleanupWriteError = input.cleanupWriteError;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
73
267
|
const DEFAULT_PLUGIN_SANDBOX_WORKER_READY_TIMEOUT_MS = 5_000;
|
|
74
268
|
const DEFAULT_PLUGIN_SANDBOX_WORKER_READY_POLL_MS = 100;
|
|
269
|
+
// The durable pending-cleanup write is the only automated way to find a leaked
|
|
270
|
+
// sandbox. A single insert can lose to a transient database fault (a dropped
|
|
271
|
+
// connection, a lock timeout, a serialization conflict). So the acquire retries
|
|
272
|
+
// the write a few times with a short backoff before it gives up. A later
|
|
273
|
+
// attempt can still land the durable row that a sweep finds.
|
|
274
|
+
const DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_ATTEMPTS = 3;
|
|
275
|
+
const DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_BACKOFF_MS = 100;
|
|
276
|
+
// A prolonged database outage after a failed teardown can buffer many orphan
|
|
277
|
+
// records in-process. The bound stops that buffer from growing without limit. A
|
|
278
|
+
// full buffer keeps the error log as the last durable handle for a new orphan.
|
|
279
|
+
const DEFAULT_DEFERRED_ORPHAN_CLEANUP_BUFFER_LIMIT = 256;
|
|
75
280
|
function delay(ms) {
|
|
76
281
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
282
|
}
|
|
78
283
|
function getLeaseDriverKey(lease, environment) {
|
|
79
284
|
const leaseDriver = typeof lease.metadata?.driver === "string" ? lease.metadata.driver : null;
|
|
80
|
-
|
|
285
|
+
// An orphan `pending_cleanup` row whose environment a delete removed keeps its
|
|
286
|
+
// driver in the metadata, so the sweep still finds the sandbox driver. Fall
|
|
287
|
+
// back to "sandbox" because only sandbox leases record orphan cleanup.
|
|
288
|
+
return leaseDriver ?? environment?.driver ?? "sandbox";
|
|
81
289
|
}
|
|
82
290
|
function toEnvironmentLeaseSnapshot(row) {
|
|
83
291
|
return {
|
|
@@ -391,7 +599,279 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
391
599
|
const pluginWorkerManager = options.pluginWorkerManager;
|
|
392
600
|
const pluginWorkerReadyTimeoutMs = options.pluginWorkerReadyTimeoutMs ?? DEFAULT_PLUGIN_SANDBOX_WORKER_READY_TIMEOUT_MS;
|
|
393
601
|
const pluginWorkerReadyPollMs = options.pluginWorkerReadyPollMs ?? DEFAULT_PLUGIN_SANDBOX_WORKER_READY_POLL_MS;
|
|
602
|
+
// The retry count is at least one attempt, so a zero or negative override
|
|
603
|
+
// never skips the durable write. The backoff is at least zero milliseconds.
|
|
604
|
+
const pendingCleanupWriteAttempts = Math.max(1, options.pendingCleanupWriteAttempts ?? DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_ATTEMPTS);
|
|
605
|
+
const pendingCleanupWriteBackoffMs = Math.max(0, options.pendingCleanupWriteBackoffMs ?? DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_BACKOFF_MS);
|
|
606
|
+
// The buffer holds at least one orphan, so a zero or negative override never
|
|
607
|
+
// drops every buffered record.
|
|
608
|
+
const deferredOrphanCleanupBufferLimit = Math.max(1, options.deferredOrphanCleanupBufferLimit ?? DEFAULT_DEFERRED_ORPHAN_CLEANUP_BUFFER_LIMIT);
|
|
394
609
|
const environmentsSvc = environmentService(db);
|
|
610
|
+
// A live sandbox whose teardown failed needs a durable `pending_cleanup` row,
|
|
611
|
+
// so a sweep can find and release it. When every synchronous write attempt
|
|
612
|
+
// fails, the database is down but the process still runs. So the driver keeps
|
|
613
|
+
// the orphan record in this in-process buffer, and a later cleanup sweep
|
|
614
|
+
// flushes it back to the database. The buffer closes the common window where
|
|
615
|
+
// the database recovers after the synchronous attempts but while the process
|
|
616
|
+
// still runs. The `pending_cleanup` failure reason matches the synchronous
|
|
617
|
+
// write, so a flushed row and a synchronous row are indistinguishable to the
|
|
618
|
+
// sweep.
|
|
619
|
+
const deferredOrphanCleanups = [];
|
|
620
|
+
const DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON = "acquire_rejected_teardown_failed";
|
|
621
|
+
// The in-process buffer alone loses an orphan on a restart. So the driver also
|
|
622
|
+
// persists each buffered orphan to this durable local spool. The spool keeps
|
|
623
|
+
// the record on the local disk, next to the other durable server state, so a
|
|
624
|
+
// restart still finds it. The first flush after a restart loads the spool back
|
|
625
|
+
// into the buffer, so the sweep re-inserts the durable row. The spool holds no
|
|
626
|
+
// secret value: it persists the same sanitized metadata the database row
|
|
627
|
+
// holds, so a restart-recovered record still authorizes the teardown but never
|
|
628
|
+
// exposes a credential. Only a crash during the outage before the durable
|
|
629
|
+
// spool write lands loses the record; the error log stays the last handle for
|
|
630
|
+
// that narrow case.
|
|
631
|
+
const orphanCleanupSpool = options.orphanCleanupSpool ?? createSandboxOrphanCleanupSpool(options.orphanCleanupSpoolDir);
|
|
632
|
+
// Add one orphan record to the in-process buffer. Report `false` only when the
|
|
633
|
+
// buffer is full, so the caller keeps the error log as the last durable handle.
|
|
634
|
+
const enqueueDeferredOrphanCleanup = (record) => {
|
|
635
|
+
// Dedup by the provider lease id, so a repeated failure for the same orphan
|
|
636
|
+
// never buffers it twice. Each acquire mints a unique provider lease id, so
|
|
637
|
+
// two distinct orphans never collide. Skip the dedup for a null lease id,
|
|
638
|
+
// because a null value carries no identity.
|
|
639
|
+
const alreadyBuffered = record.providerLeaseId !== null &&
|
|
640
|
+
deferredOrphanCleanups.some((entry) => entry.providerLeaseId === record.providerLeaseId);
|
|
641
|
+
if (alreadyBuffered)
|
|
642
|
+
return true;
|
|
643
|
+
if (deferredOrphanCleanups.length >= deferredOrphanCleanupBufferLimit)
|
|
644
|
+
return false;
|
|
645
|
+
deferredOrphanCleanups.push(record);
|
|
646
|
+
return true;
|
|
647
|
+
};
|
|
648
|
+
// Load the durable spool into the in-process buffer exactly once. The first
|
|
649
|
+
// flush after a restart runs this load, so the records a previous process
|
|
650
|
+
// could not land re-enter the flush path. A later flush sees the resolved
|
|
651
|
+
// promise and never reloads, so a record this process already buffered never
|
|
652
|
+
// doubles. The load runs before the flush splices the buffer, so a
|
|
653
|
+
// restart-recovered record flushes on the same tick.
|
|
654
|
+
let spoolLoadPromise = null;
|
|
655
|
+
const ensureSpoolLoaded = () => {
|
|
656
|
+
if (!spoolLoadPromise) {
|
|
657
|
+
spoolLoadPromise = (async () => {
|
|
658
|
+
const persisted = await orphanCleanupSpool.load();
|
|
659
|
+
for (const record of persisted) {
|
|
660
|
+
enqueueDeferredOrphanCleanup(record);
|
|
661
|
+
}
|
|
662
|
+
})();
|
|
663
|
+
}
|
|
664
|
+
return spoolLoadPromise;
|
|
665
|
+
};
|
|
666
|
+
const flushDeferredOrphanCleanups = async () => {
|
|
667
|
+
// Load the durable spool into the buffer first, so a restart-recovered record
|
|
668
|
+
// flushes on this tick. The load runs once, then a resolved promise returns.
|
|
669
|
+
await ensureSpoolLoaded();
|
|
670
|
+
if (deferredOrphanCleanups.length === 0) {
|
|
671
|
+
return { recovered: 0, pending: 0 };
|
|
672
|
+
}
|
|
673
|
+
// Take the whole batch synchronously before the first await, so a second
|
|
674
|
+
// concurrent flush sees an empty buffer and never re-inserts the same record.
|
|
675
|
+
const batch = deferredOrphanCleanups.splice(0, deferredOrphanCleanups.length);
|
|
676
|
+
let recovered = 0;
|
|
677
|
+
for (const record of batch) {
|
|
678
|
+
try {
|
|
679
|
+
await environmentsSvc.insertPendingCleanupLease({
|
|
680
|
+
companyId: record.companyId,
|
|
681
|
+
environmentId: record.environmentId,
|
|
682
|
+
executionWorkspaceId: record.executionWorkspaceId,
|
|
683
|
+
issueId: record.issueId,
|
|
684
|
+
heartbeatRunId: record.heartbeatRunId,
|
|
685
|
+
provider: record.provider,
|
|
686
|
+
providerLeaseId: record.providerLeaseId,
|
|
687
|
+
metadata: record.metadata,
|
|
688
|
+
failureReason: DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON,
|
|
689
|
+
});
|
|
690
|
+
// The durable row exists now, so the sweep finds and releases the orphan.
|
|
691
|
+
// Drop the spooled copy only after the row lands. A crash between the two
|
|
692
|
+
// leaves the spooled copy for a later flush, which re-inserts a duplicate
|
|
693
|
+
// the idempotent teardown handles. This order never loses the orphan.
|
|
694
|
+
await orphanCleanupSpool.remove(record);
|
|
695
|
+
recovered += 1;
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
// The database is still down. Re-queue the record for a later flush,
|
|
699
|
+
// unless the buffer filled again. The identifiers carry no secret; the
|
|
700
|
+
// caught write exception never enters the log, because it can hold a
|
|
701
|
+
// credential in its message, code, cause, or stack.
|
|
702
|
+
const requeued = enqueueDeferredOrphanCleanup(record);
|
|
703
|
+
logger.warn({
|
|
704
|
+
errorKind: SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND,
|
|
705
|
+
provider: record.provider,
|
|
706
|
+
providerLeaseId: record.providerLeaseId,
|
|
707
|
+
companyId: record.companyId,
|
|
708
|
+
environmentId: record.environmentId,
|
|
709
|
+
requeued,
|
|
710
|
+
}, requeued
|
|
711
|
+
? "deferred sandbox orphan cleanup flush failed; kept the orphan buffered for a later flush"
|
|
712
|
+
: "deferred sandbox orphan cleanup flush failed and the in-process buffer is full; live sandbox needs a manual teardown");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return { recovered, pending: deferredOrphanCleanups.length };
|
|
716
|
+
};
|
|
717
|
+
// Build the safe diagnostic fields for an orphan record. The provider
|
|
718
|
+
// identifiers are the only fields any diagnostic log emits, and they carry no
|
|
719
|
+
// secret. A caught write exception never enters a log: it can carry a
|
|
720
|
+
// credential in its name, code, message, cause, or stack.
|
|
721
|
+
const orphanDiagnosticFields = (record) => ({
|
|
722
|
+
errorKind: SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND,
|
|
723
|
+
provider: record.provider,
|
|
724
|
+
providerLeaseId: record.providerLeaseId,
|
|
725
|
+
companyId: record.companyId,
|
|
726
|
+
environmentId: record.environmentId,
|
|
727
|
+
executionWorkspaceId: record.executionWorkspaceId,
|
|
728
|
+
issueId: record.issueId,
|
|
729
|
+
heartbeatRunId: record.heartbeatRunId,
|
|
730
|
+
});
|
|
731
|
+
// Try to write the durable `pending_cleanup` row for an orphan sandbox, with a
|
|
732
|
+
// few retries. Report the new lease id on success, or a null lease id and the
|
|
733
|
+
// last write error on total failure. This function never buffers, never logs
|
|
734
|
+
// at error level, and never throws. The caller decides whether a total failure
|
|
735
|
+
// is fatal, because a later successful teardown removes the orphan and needs
|
|
736
|
+
// no durable row.
|
|
737
|
+
//
|
|
738
|
+
// The row carries the provider lease id and the same resolution metadata a
|
|
739
|
+
// normal lease holds, so a later cleanup sweep finds and releases the orphan.
|
|
740
|
+
// One atomic insert writes the row directly in the terminal `pending_cleanup`
|
|
741
|
+
// state, and the insert skips the company-binding assertion, so it records the
|
|
742
|
+
// orphan even for a foreign-bound environment. A transient database fault (a
|
|
743
|
+
// dropped connection, a lock timeout, a serialization conflict) can reject one
|
|
744
|
+
// insert but clear on the next, so the write retries a few times with a short
|
|
745
|
+
// backoff.
|
|
746
|
+
const tryWriteDurablePendingCleanup = async (record) => {
|
|
747
|
+
const diagnosticFields = orphanDiagnosticFields(record);
|
|
748
|
+
let lastCleanupWriteError;
|
|
749
|
+
for (let attempt = 1; attempt <= pendingCleanupWriteAttempts; attempt += 1) {
|
|
750
|
+
try {
|
|
751
|
+
const lease = await environmentsSvc.insertPendingCleanupLease({
|
|
752
|
+
companyId: record.companyId,
|
|
753
|
+
environmentId: record.environmentId,
|
|
754
|
+
executionWorkspaceId: record.executionWorkspaceId,
|
|
755
|
+
issueId: record.issueId,
|
|
756
|
+
heartbeatRunId: record.heartbeatRunId,
|
|
757
|
+
provider: record.provider,
|
|
758
|
+
providerLeaseId: record.providerLeaseId,
|
|
759
|
+
metadata: record.metadata,
|
|
760
|
+
failureReason: DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON,
|
|
761
|
+
});
|
|
762
|
+
// The durable row exists now, so a sweep can find and release the orphan.
|
|
763
|
+
return { leaseId: lease.id, lastError: undefined };
|
|
764
|
+
}
|
|
765
|
+
catch (cleanupWriteError) {
|
|
766
|
+
lastCleanupWriteError = cleanupWriteError;
|
|
767
|
+
if (attempt < pendingCleanupWriteAttempts) {
|
|
768
|
+
// The write failed, but attempts remain. Log the retry at warn level
|
|
769
|
+
// and back off, then try again. A later attempt can still land the
|
|
770
|
+
// durable row.
|
|
771
|
+
logger.warn({ ...diagnosticFields, attempt, maxAttempts: pendingCleanupWriteAttempts }, "sandbox orphan cleanup write failed; retrying the durable pending-cleanup write");
|
|
772
|
+
await delay(pendingCleanupWriteBackoffMs * attempt);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return { leaseId: null, lastError: lastCleanupWriteError };
|
|
777
|
+
};
|
|
778
|
+
// Release the durable `pending_cleanup` row after a successful inline teardown.
|
|
779
|
+
// The teardown removed the orphan sandbox, so the row is no longer needed. The
|
|
780
|
+
// release moves the row to the terminal `expired` state with a `success`
|
|
781
|
+
// cleanup status, the same state the sweep records for a successful teardown.
|
|
782
|
+
//
|
|
783
|
+
// The release is best-effort. If it fails, the row stays `pending_cleanup`, so
|
|
784
|
+
// a later sweep runs the idempotent teardown on the already-gone sandbox and
|
|
785
|
+
// releases the row itself. A failed release never loses or leaks the orphan.
|
|
786
|
+
const releaseCleanedUpOrphanRow = async (leaseId, diagnosticFields) => {
|
|
787
|
+
try {
|
|
788
|
+
await environmentsSvc.releaseLease(leaseId, "expired", {
|
|
789
|
+
cleanupStatus: "success",
|
|
790
|
+
failureReason: "acquire_rejected_teardown_succeeded",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
// The caught release exception never enters a log: it can carry a
|
|
795
|
+
// credential in its name, code, message, cause, or stack.
|
|
796
|
+
logger.warn(diagnosticFields, "could not release the pending-cleanup row after a successful orphan teardown; a later sweep reconciles the released row");
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
// Neither the durable write nor the inline teardown worked, so the orphan
|
|
800
|
+
// sandbox is live with no durable row. Persist the orphan to the durable spool
|
|
801
|
+
// and buffer it in-process, so a later cleanup-sweep flush lands the durable
|
|
802
|
+
// row. The spool survives a restart, so the flush recovers the orphan even
|
|
803
|
+
// after a crash. Then log the plain provider identifiers at error level, so an
|
|
804
|
+
// operator keeps a handle to tear the sandbox down by hand. Only a crash before
|
|
805
|
+
// the spool write lands loses the record, so the error log is the last handle
|
|
806
|
+
// for that narrow case. Throw `SandboxOrphanCleanupWriteError`, which the
|
|
807
|
+
// caller propagates. The error keeps the original insert rejection as its
|
|
808
|
+
// `cause`, so the real reason the acquire failed stays visible in the error
|
|
809
|
+
// chain.
|
|
810
|
+
const escalateUnwrittenOrphan = async (record, cause, cleanupWriteError) => {
|
|
811
|
+
const persisted = await orphanCleanupSpool.append(record);
|
|
812
|
+
const buffered = enqueueDeferredOrphanCleanup(record);
|
|
813
|
+
logger.error({
|
|
814
|
+
...orphanDiagnosticFields(record),
|
|
815
|
+
persisted,
|
|
816
|
+
buffered,
|
|
817
|
+
deferredBufferSize: deferredOrphanCleanups.length,
|
|
818
|
+
}, persisted
|
|
819
|
+
? "sandbox orphan cleanup write failed; persisted the orphan to the durable spool for a later cleanup-sweep flush"
|
|
820
|
+
: buffered
|
|
821
|
+
? "sandbox orphan cleanup write failed and the durable spool write failed; buffered the orphan in-process only for a later cleanup-sweep flush"
|
|
822
|
+
: "sandbox orphan cleanup write failed and neither the durable spool nor the in-process buffer kept the orphan; live sandbox needs a manual teardown");
|
|
823
|
+
throw new SandboxOrphanCleanupWriteError({
|
|
824
|
+
provider: record.provider,
|
|
825
|
+
providerLeaseId: record.providerLeaseId,
|
|
826
|
+
cause,
|
|
827
|
+
cleanupWriteError,
|
|
828
|
+
});
|
|
829
|
+
};
|
|
830
|
+
// Clean up an orphan sandbox after the conditional lease insert rejected it.
|
|
831
|
+
// The acquire provisioned a remote sandbox, then the insert rejected (a
|
|
832
|
+
// foreign-company binding), so no lease row tracks the live sandbox. This
|
|
833
|
+
// handler records the durable `pending_cleanup` row FIRST, before the inline
|
|
834
|
+
// teardown, then tears the sandbox down.
|
|
835
|
+
//
|
|
836
|
+
// The write goes first on purpose. The conditional insert just returned a
|
|
837
|
+
// definitive rejection, so the database is reachable at this instant. An
|
|
838
|
+
// inline teardown can run long, and the database can drop during it. So a
|
|
839
|
+
// write after the teardown can fail and leave the orphan only in memory, which
|
|
840
|
+
// a restart loses. With the write first, the durable row lands while the
|
|
841
|
+
// database is proven up, so a crash after it still leaves a row that a sweep
|
|
842
|
+
// finds and releases.
|
|
843
|
+
//
|
|
844
|
+
// The teardown then runs. On success the orphan is gone, so the handler
|
|
845
|
+
// releases the durable row. On a failed teardown the row stays for the sweep.
|
|
846
|
+
// If the durable write itself failed and the teardown also failed, the handler
|
|
847
|
+
// buffers the orphan and throws `SandboxOrphanCleanupWriteError`. A successful
|
|
848
|
+
// teardown after a failed write leaves no orphan, so it needs no error.
|
|
849
|
+
const cleanUpRejectedOrphanSandbox = async (input) => {
|
|
850
|
+
const durable = await tryWriteDurablePendingCleanup(input.record);
|
|
851
|
+
let teardownFailed = !input.canTeardown;
|
|
852
|
+
if (!teardownFailed) {
|
|
853
|
+
try {
|
|
854
|
+
await input.teardown();
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
teardownFailed = true;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (!teardownFailed) {
|
|
861
|
+
// The teardown removed the orphan, so drop the durable row if we wrote one.
|
|
862
|
+
if (durable.leaseId !== null) {
|
|
863
|
+
await releaseCleanedUpOrphanRow(durable.leaseId, orphanDiagnosticFields(input.record));
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
// The teardown failed, so the orphan sandbox is still live.
|
|
868
|
+
if (durable.leaseId !== null) {
|
|
869
|
+
// The durable row already tracks the orphan, so a sweep finds and releases
|
|
870
|
+
// it. The caller rethrows the original insert rejection.
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
await escalateUnwrittenOrphan(input.record, input.cause, durable.lastError);
|
|
874
|
+
};
|
|
395
875
|
// The run-time exec parent context, held per lease id. A plugin sandbox
|
|
396
876
|
// provider can open a persistent session on the first command and delete it
|
|
397
877
|
// on lease release. The session open runs inside `execute`, under the run
|
|
@@ -464,14 +944,14 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
464
944
|
config: sandboxConfigForLeaseMetadata(metadataConfig),
|
|
465
945
|
});
|
|
466
946
|
if (parsed.driver === "sandbox") {
|
|
467
|
-
return parsed.config;
|
|
947
|
+
return dropInternalPluginSandboxConfigKeys(parsed.config);
|
|
468
948
|
}
|
|
469
949
|
}
|
|
470
950
|
if (input.environment.driver === "sandbox") {
|
|
471
951
|
try {
|
|
472
952
|
const parsed = await resolveEnvironmentDriverConfigForRuntime(db, input.lease.companyId, input.environment);
|
|
473
953
|
if (parsed.driver === "sandbox" && parsed.config.provider === input.provider) {
|
|
474
|
-
return parsed.config;
|
|
954
|
+
return dropInternalPluginSandboxConfigKeys(parsed.config);
|
|
475
955
|
}
|
|
476
956
|
}
|
|
477
957
|
catch {
|
|
@@ -481,7 +961,7 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
481
961
|
}
|
|
482
962
|
return {
|
|
483
963
|
provider: input.provider,
|
|
484
|
-
...
|
|
964
|
+
...dropInternalPluginSandboxConfigKeys(input.lease.metadata),
|
|
485
965
|
};
|
|
486
966
|
}
|
|
487
967
|
async function cleanupObsoleteReusableSandboxLeases(input) {
|
|
@@ -559,7 +1039,20 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
559
1039
|
const workerConfig = stripSandboxProviderEnvelope(parsed.config);
|
|
560
1040
|
const storedConfig = storedParsed.config;
|
|
561
1041
|
const providerConfigForLease = sandboxConfigForLeaseMetadata(storedConfig);
|
|
562
|
-
|
|
1042
|
+
// Require the reusable-lease capability AND a worker that verifies the
|
|
1043
|
+
// reuse methods. The provider must first opt in through the declaration:
|
|
1044
|
+
// the nested `sandboxCapabilities.reusableLeases` wins over the legacy
|
|
1045
|
+
// `supportsReusableLeases` flag. The worker must also verify
|
|
1046
|
+
// `environmentResumeLease`, `environmentReleaseLease`, and
|
|
1047
|
+
// `environmentDestroyLease` (the reuse prerequisite verbs) before the
|
|
1048
|
+
// runtime resumes, releases, or tears down a reusable lease. A provider
|
|
1049
|
+
// that declares `reusableLeases` true but whose worker does not verify
|
|
1050
|
+
// all three methods fails closed and uses an ephemeral lease, so the
|
|
1051
|
+
// runtime never dispatches a resume, a release, or a destroy the worker
|
|
1052
|
+
// cannot serve, and it never strands a stale lease it cannot destroy.
|
|
1053
|
+
const declaredReusableLeases = resolveDeclaredSandboxCapabilities(pluginProvider.resolved.driver).reusableLeases === true;
|
|
1054
|
+
const pluginVerifiedMethods = new Set(pluginWorkerManager.getWorker(pluginProvider.resolved.plugin.id)?.supportedMethods ?? []);
|
|
1055
|
+
const supportsReusableLeases = declaredReusableLeases && capabilityIsVerified("reusableLeases", pluginVerifiedMethods);
|
|
563
1056
|
const leaseFingerprint = supportsReusableLeases &&
|
|
564
1057
|
parsed.config.reuseLease &&
|
|
565
1058
|
input.heartbeatRunId !== null &&
|
|
@@ -632,29 +1125,41 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
632
1125
|
: null;
|
|
633
1126
|
let providerLease = null;
|
|
634
1127
|
if (reusableLease?.providerLeaseId) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
:
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1128
|
+
// The `supportsReusableLeases` check above reads a snapshot of the
|
|
1129
|
+
// worker methods. The runtime then does asynchronous database work
|
|
1130
|
+
// (list, fingerprint, obsolete-lease cleanup) before this dispatch. A
|
|
1131
|
+
// worker restart in that window can drop `environmentResumeLease`
|
|
1132
|
+
// while the snapshot still marks the method verified. Re-check the
|
|
1133
|
+
// live worker here and fail closed when the method is absent: skip the
|
|
1134
|
+
// resume, destroy the stale reusable lease, and acquire a fresh lease
|
|
1135
|
+
// below. The runtime never dispatches a resume the live worker cannot
|
|
1136
|
+
// serve.
|
|
1137
|
+
const workerVerifiesResume = pluginWorkerVerifiesLifecycleMethod(pluginProvider.resolved.plugin.id, "environmentResumeLease");
|
|
1138
|
+
if (workerVerifiesResume) {
|
|
1139
|
+
try {
|
|
1140
|
+
const resumed = await pluginWorkerManager.call(pluginProvider.resolved.plugin.id, "environmentResumeLease", {
|
|
1141
|
+
driverKey: parsed.config.provider,
|
|
1142
|
+
companyId: input.companyId,
|
|
1143
|
+
environmentId: input.environment.id,
|
|
1144
|
+
issueId: input.issueId,
|
|
1145
|
+
config: workerConfig,
|
|
1146
|
+
providerLeaseId: reusableLease.providerLeaseId,
|
|
1147
|
+
leaseMetadata: reusableLease.metadata ?? undefined,
|
|
1148
|
+
}, resolvePluginSandboxRpcTimeoutMs(workerConfig));
|
|
1149
|
+
providerLease =
|
|
1150
|
+
typeof resumed.providerLeaseId === "string" && resumed.providerLeaseId.length > 0
|
|
1151
|
+
? resumed
|
|
1152
|
+
: null;
|
|
1153
|
+
}
|
|
1154
|
+
catch {
|
|
1155
|
+
providerLease = null;
|
|
1156
|
+
}
|
|
652
1157
|
}
|
|
653
1158
|
if (!providerLease) {
|
|
654
1159
|
await destroyReusableSandboxLease({
|
|
655
1160
|
environment: input.environment,
|
|
656
1161
|
lease: reusableLease,
|
|
657
|
-
failureReason: "resume_failed",
|
|
1162
|
+
failureReason: workerVerifiesResume ? "resume_failed" : "resume_capability_lost",
|
|
658
1163
|
});
|
|
659
1164
|
}
|
|
660
1165
|
}
|
|
@@ -679,6 +1184,11 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
679
1184
|
// environment's default adapter image (a pi agent then runs in the
|
|
680
1185
|
// opencode image and the harness binary is missing at exec time).
|
|
681
1186
|
adapterType: input.adapterType ?? undefined,
|
|
1187
|
+
// Forward a caller deadline so the provider configures a provider-side
|
|
1188
|
+
// expiry at or before it and returns the real provider expiry.
|
|
1189
|
+
...(requestedExpiresAtParam(input.requestedExpiresAt) !== undefined
|
|
1190
|
+
? { requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt) }
|
|
1191
|
+
: {}),
|
|
682
1192
|
}, resolvePluginSandboxRpcTimeoutMs(workerConfig));
|
|
683
1193
|
// Ad-hoc test leases are never publishable for reuse: storing them
|
|
684
1194
|
// as `reuse_by_environment` would let a concurrent heartbeat resume
|
|
@@ -703,28 +1213,76 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
703
1213
|
providerMetadata: sanitizedProviderMetadata,
|
|
704
1214
|
})
|
|
705
1215
|
: null;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1216
|
+
const pluginLeaseMetadata = {
|
|
1217
|
+
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
1218
|
+
driver: input.environment.driver,
|
|
1219
|
+
executionWorkspaceMode: input.executionWorkspaceMode,
|
|
1220
|
+
pluginId: pluginProvider.resolved.plugin.id,
|
|
1221
|
+
pluginKey: pluginProvider.resolved.plugin.pluginKey,
|
|
1222
|
+
sandboxProviderPlugin: true,
|
|
1223
|
+
...sandboxConfigForLeaseMetadata(storedConfig),
|
|
1224
|
+
...sanitizedProviderMetadata,
|
|
1225
|
+
...(reusableScope ? { reusableSandboxLease: reusableScope } : {}),
|
|
1226
|
+
};
|
|
1227
|
+
try {
|
|
1228
|
+
return await environmentsSvc.acquireLease({
|
|
1229
|
+
companyId: input.companyId,
|
|
1230
|
+
environmentId: input.environment.id,
|
|
1231
|
+
executionWorkspaceId: input.executionWorkspaceId,
|
|
1232
|
+
issueId: input.issueId,
|
|
1233
|
+
heartbeatRunId: input.heartbeatRunId,
|
|
1234
|
+
assertCompanyBinding: input.assertCompanyBinding,
|
|
1235
|
+
leasePolicy: resolvedLeasePolicy,
|
|
1236
|
+
provider: parsed.config.provider,
|
|
1237
|
+
providerLeaseId: acquiredLease.providerLeaseId,
|
|
1238
|
+
expiresAt: providerAttestedLeaseExpiry(input.requestedExpiresAt, acquiredLease.expiresAt ? new Date(acquiredLease.expiresAt) : undefined),
|
|
1239
|
+
metadata: pluginLeaseMetadata,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
catch (error) {
|
|
1243
|
+
// The conditional lease insert rejected, so no lease row exists. A
|
|
1244
|
+
// managed reconciliation can bind the environment to another company
|
|
1245
|
+
// between the route guard and this insert, so the insert fails closed
|
|
1246
|
+
// with `environment_company_mismatch`. This call already provisioned the
|
|
1247
|
+
// remote plugin sandbox above, so tear it down now. Without this step
|
|
1248
|
+
// the rejected insert leaks a live sandbox that no lease row tracks. The
|
|
1249
|
+
// Claude login runs on a plugin-backed sandbox, so this path is the one
|
|
1250
|
+
// the login uses. Do not tear down a reused sandbox that an earlier
|
|
1251
|
+
// lease still owns.
|
|
1252
|
+
if (!reusableLease || acquiredLease.providerLeaseId !== reusableLease.providerLeaseId) {
|
|
1253
|
+
// Record the durable pending-cleanup row before the teardown, then
|
|
1254
|
+
// tear the sandbox down. On a successful teardown the handler releases
|
|
1255
|
+
// the row. If the durable write and the teardown both fail, the
|
|
1256
|
+
// handler throws a `SandboxOrphanCleanupWriteError` that carries the
|
|
1257
|
+
// original rejection as its cause.
|
|
1258
|
+
await cleanUpRejectedOrphanSandbox({
|
|
1259
|
+
record: {
|
|
1260
|
+
companyId: input.companyId,
|
|
1261
|
+
environmentId: input.environment.id,
|
|
1262
|
+
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
1263
|
+
issueId: input.issueId ?? null,
|
|
1264
|
+
heartbeatRunId: input.heartbeatRunId ?? null,
|
|
1265
|
+
provider: parsed.config.provider,
|
|
1266
|
+
providerLeaseId: acquiredLease.providerLeaseId,
|
|
1267
|
+
metadata: pluginLeaseMetadata,
|
|
1268
|
+
},
|
|
1269
|
+
cause: error,
|
|
1270
|
+
canTeardown: pluginWorkerManager.isRunning(pluginProvider.resolved.plugin.id),
|
|
1271
|
+
teardown: async () => {
|
|
1272
|
+
await pluginWorkerManager.call(pluginProvider.resolved.plugin.id, "environmentDestroyLease", {
|
|
1273
|
+
driverKey: parsed.config.provider,
|
|
1274
|
+
companyId: input.companyId,
|
|
1275
|
+
environmentId: input.environment.id,
|
|
1276
|
+
issueId: input.issueId,
|
|
1277
|
+
config: workerConfig,
|
|
1278
|
+
providerLeaseId: acquiredLease.providerLeaseId,
|
|
1279
|
+
leaseMetadata: acquiredLease.metadata ?? undefined,
|
|
1280
|
+
}, resolvePluginSandboxRpcTimeoutMs(workerConfig));
|
|
1281
|
+
},
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
throw error;
|
|
1285
|
+
}
|
|
728
1286
|
}
|
|
729
1287
|
// Built-in sandbox provider path. Same guard as the plugin-backed path:
|
|
730
1288
|
// ad-hoc tests (heartbeatRunId === null) must never resume an existing
|
|
@@ -732,7 +1290,11 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
732
1290
|
// heartbeat run that shares it. Filter to reusable policies and statuses
|
|
733
1291
|
// so non-reusable, cleanup-pending, or terminal rows can never be matched.
|
|
734
1292
|
const builtinSandboxProvider = getBuiltinSandboxProvider(parsed.config.provider);
|
|
735
|
-
|
|
1293
|
+
// Resolve the DECLARED reusable-lease capability through the same contract
|
|
1294
|
+
// as the plugin path, so the nested capability wins over the legacy flag.
|
|
1295
|
+
const supportsReusableLeases = resolveDeclaredSandboxCapabilities({
|
|
1296
|
+
supportsReusableLeases: builtinSandboxProvider?.supportsReusableLeases,
|
|
1297
|
+
}).reusableLeases === true;
|
|
736
1298
|
const providerConfigForLease = sandboxConfigForLeaseMetadata(parsed.config);
|
|
737
1299
|
const leaseFingerprint = supportsReusableLeases &&
|
|
738
1300
|
parsed.config.reuseLease &&
|
|
@@ -802,6 +1364,9 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
802
1364
|
agentId: input.agentId,
|
|
803
1365
|
executionWorkspaceId: input.executionWorkspaceId,
|
|
804
1366
|
reusableProviderLeaseId,
|
|
1367
|
+
// Forward a caller deadline so the provider configures a provider-side
|
|
1368
|
+
// expiry at or before it and returns the real provider expiry.
|
|
1369
|
+
requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt),
|
|
805
1370
|
});
|
|
806
1371
|
}
|
|
807
1372
|
catch (error) {
|
|
@@ -839,23 +1404,65 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
839
1404
|
providerMetadata: providerLease.metadata,
|
|
840
1405
|
})
|
|
841
1406
|
: null;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1407
|
+
const builtinLeaseMetadata = {
|
|
1408
|
+
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
1409
|
+
driver: input.environment.driver,
|
|
1410
|
+
executionWorkspaceMode: input.executionWorkspaceMode,
|
|
1411
|
+
...providerLease.metadata,
|
|
1412
|
+
...(reusableScope ? { reusableSandboxLease: reusableScope } : {}),
|
|
1413
|
+
};
|
|
1414
|
+
try {
|
|
1415
|
+
return await environmentsSvc.acquireLease({
|
|
1416
|
+
companyId: input.companyId,
|
|
1417
|
+
environmentId: input.environment.id,
|
|
1418
|
+
executionWorkspaceId: input.executionWorkspaceId,
|
|
1419
|
+
issueId: input.issueId,
|
|
1420
|
+
heartbeatRunId: input.heartbeatRunId,
|
|
1421
|
+
assertCompanyBinding: input.assertCompanyBinding,
|
|
1422
|
+
leasePolicy: resolvedLeasePolicy,
|
|
1423
|
+
provider: parsed.config.provider,
|
|
1424
|
+
providerLeaseId: providerLease.providerLeaseId,
|
|
1425
|
+
expiresAt: providerAttestedLeaseExpiry(input.requestedExpiresAt, providerLease.expiresAt ? new Date(providerLease.expiresAt) : undefined),
|
|
1426
|
+
metadata: builtinLeaseMetadata,
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
// The conditional lease insert rejected, so no lease row exists. A managed
|
|
1431
|
+
// reconciliation can bind the environment to another company between the
|
|
1432
|
+
// route guard and this insert, so the insert fails closed with
|
|
1433
|
+
// `environment_company_mismatch`. This call already provisioned the remote
|
|
1434
|
+
// sandbox above, so release it now. Without this teardown the rejected
|
|
1435
|
+
// insert leaks a live sandbox that no lease row tracks. Do not tear down a
|
|
1436
|
+
// reused sandbox that an earlier lease still owns.
|
|
1437
|
+
if (!reusableLease || providerLease.providerLeaseId !== reusableLease.providerLeaseId) {
|
|
1438
|
+
// Record the durable pending-cleanup row before the teardown, then
|
|
1439
|
+
// release the remote sandbox. On a successful teardown the handler
|
|
1440
|
+
// releases the row. If the durable write and the teardown both fail,
|
|
1441
|
+
// the handler throws a `SandboxOrphanCleanupWriteError` that carries the
|
|
1442
|
+
// original rejection as its cause.
|
|
1443
|
+
await cleanUpRejectedOrphanSandbox({
|
|
1444
|
+
record: {
|
|
1445
|
+
companyId: input.companyId,
|
|
1446
|
+
environmentId: input.environment.id,
|
|
1447
|
+
executionWorkspaceId: input.executionWorkspaceId ?? null,
|
|
1448
|
+
issueId: input.issueId ?? null,
|
|
1449
|
+
heartbeatRunId: input.heartbeatRunId ?? null,
|
|
1450
|
+
provider: parsed.config.provider,
|
|
1451
|
+
providerLeaseId: providerLease.providerLeaseId,
|
|
1452
|
+
metadata: builtinLeaseMetadata,
|
|
1453
|
+
},
|
|
1454
|
+
cause: error,
|
|
1455
|
+
canTeardown: true,
|
|
1456
|
+
teardown: async () => {
|
|
1457
|
+
await destroySandboxProviderLease({
|
|
1458
|
+
config: parsed.config,
|
|
1459
|
+
providerLeaseId: providerLease.providerLeaseId,
|
|
1460
|
+
});
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
throw error;
|
|
1465
|
+
}
|
|
859
1466
|
},
|
|
860
1467
|
async releaseRunLease(input) {
|
|
861
1468
|
if (input.status === "expired" && input.lease.leasePolicy === "reuse_by_environment") {
|
|
@@ -906,6 +1513,116 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
906
1513
|
cleanupStatus,
|
|
907
1514
|
});
|
|
908
1515
|
},
|
|
1516
|
+
async retryPendingSandboxTeardown(input) {
|
|
1517
|
+
// Resolve the teardown from the immutable orphan lease row, not from the
|
|
1518
|
+
// current environment. The row keeps the provider, the provider lease id,
|
|
1519
|
+
// and the sandbox config in its metadata. A provider change re-points the
|
|
1520
|
+
// environment, and an environment delete removes it, but neither must
|
|
1521
|
+
// strand this teardown. So the retry reads the recorded provider and the
|
|
1522
|
+
// recorded config, and never the current environment provider.
|
|
1523
|
+
const recordedProvider = input.lease.provider ??
|
|
1524
|
+
(typeof input.lease.metadata?.provider === "string" ? input.lease.metadata.provider : null);
|
|
1525
|
+
if (!recordedProvider) {
|
|
1526
|
+
throw new Error(`Pending-cleanup lease "${input.lease.id}" has no recorded provider for teardown.`);
|
|
1527
|
+
}
|
|
1528
|
+
// Build the config from the lease metadata under the orphan's company
|
|
1529
|
+
// scope, so the retry resolves the same connection secrets the failed
|
|
1530
|
+
// acquire used. The recorded config is the sole source of truth here: a
|
|
1531
|
+
// delete removed the environment, and a provider change re-points it, so
|
|
1532
|
+
// the retry never reads the current environment config.
|
|
1533
|
+
const metadataConfig = sandboxConfigFromLeaseMetadataLoose(input.lease);
|
|
1534
|
+
if (!metadataConfig || metadataConfig.provider !== recordedProvider) {
|
|
1535
|
+
throw new Error(`Pending-cleanup lease "${input.lease.id}" has no recorded config for provider "${recordedProvider}".`);
|
|
1536
|
+
}
|
|
1537
|
+
// Plugin-backed provider path. The Claude login runs on a plugin-backed
|
|
1538
|
+
// sandbox, so this path tears down the login orphan.
|
|
1539
|
+
if (!isBuiltinSandboxProvider(recordedProvider)) {
|
|
1540
|
+
if (!pluginWorkerManager) {
|
|
1541
|
+
throw new Error(`Sandbox provider "${recordedProvider}" needs a plugin worker manager for cleanup, but none is available.`);
|
|
1542
|
+
}
|
|
1543
|
+
const pluginProvider = await resolveSandboxProviderPlugin({ provider: recordedProvider });
|
|
1544
|
+
if (pluginProvider.state !== "running") {
|
|
1545
|
+
throw new Error(`Sandbox provider plugin for "${recordedProvider}" is ${pluginProvider.state}, so the cleanup teardown cannot run yet.`);
|
|
1546
|
+
}
|
|
1547
|
+
// Resolve the recorded secret refs through the durable orphan record,
|
|
1548
|
+
// not the environment binding. A delete removed the binding, or a
|
|
1549
|
+
// provider change replaced it, so environment-bound resolution would
|
|
1550
|
+
// reject the recorded API-key ref and strand the teardown forever.
|
|
1551
|
+
const config = await resolveSandboxCleanupConfigSecrets(db, input.lease.companyId, metadataConfig, { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId });
|
|
1552
|
+
const workerConfig = stripSandboxProviderEnvelope(config);
|
|
1553
|
+
await pluginWorkerManager.call(pluginProvider.resolved.plugin.id, "environmentDestroyLease", {
|
|
1554
|
+
driverKey: recordedProvider,
|
|
1555
|
+
companyId: input.lease.companyId,
|
|
1556
|
+
// The provider teardown keys on the provider lease id. The
|
|
1557
|
+
// environment id is only context, and it is empty when a delete
|
|
1558
|
+
// removed the environment before this orphan was recorded.
|
|
1559
|
+
environmentId: input.lease.environmentId ?? input.environment?.id ?? "",
|
|
1560
|
+
issueId: input.lease.issueId,
|
|
1561
|
+
config: workerConfig,
|
|
1562
|
+
providerLeaseId: input.lease.providerLeaseId,
|
|
1563
|
+
leaseMetadata: input.lease.metadata ?? undefined,
|
|
1564
|
+
}, resolvePluginSandboxRpcTimeoutMs(workerConfig));
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
// Built-in provider path. Resolve the recorded config secrets through the
|
|
1568
|
+
// durable orphan record, not the environment binding, for the same reason
|
|
1569
|
+
// as the plugin path above. The teardown targets the recorded provider,
|
|
1570
|
+
// never the current environment provider.
|
|
1571
|
+
const cleanupConfig = await resolveSandboxCleanupConfigSecrets(db, input.lease.companyId, metadataConfig, { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId });
|
|
1572
|
+
await destroySandboxProviderLease({
|
|
1573
|
+
config: cleanupConfig,
|
|
1574
|
+
providerLeaseId: input.lease.providerLeaseId,
|
|
1575
|
+
});
|
|
1576
|
+
},
|
|
1577
|
+
async isPendingCleanupWorkerReady(input) {
|
|
1578
|
+
// Resolve the recorded provider the same way `retryPendingSandboxTeardown`
|
|
1579
|
+
// does, so the probe reads the same target the teardown uses.
|
|
1580
|
+
const recordedProvider = input.lease.provider ??
|
|
1581
|
+
(typeof input.lease.metadata?.provider === "string" ? input.lease.metadata.provider : null);
|
|
1582
|
+
// A missing provider is a permanent misconfiguration. Report ready, so the
|
|
1583
|
+
// teardown runs, throws, and counts toward the cap.
|
|
1584
|
+
if (!recordedProvider)
|
|
1585
|
+
return true;
|
|
1586
|
+
// A built-in provider has no plugin worker, so it is always ready.
|
|
1587
|
+
if (isBuiltinSandboxProvider(recordedProvider))
|
|
1588
|
+
return true;
|
|
1589
|
+
// No worker manager is a permanent condition here. Report ready, so the
|
|
1590
|
+
// teardown runs, throws its own "no worker manager" error, and counts
|
|
1591
|
+
// toward the cap.
|
|
1592
|
+
if (!pluginWorkerManager)
|
|
1593
|
+
return true;
|
|
1594
|
+
// Resolve the installed plugin without a wait. A plugin reload or a plugin
|
|
1595
|
+
// reinstall can remove the plugin row for a short window, so a missing
|
|
1596
|
+
// plugin is a transient condition, not a permanent one. Report not ready,
|
|
1597
|
+
// so the sweep skips the lease without a claim, and a later sweep retries
|
|
1598
|
+
// after the plugin returns. A teardown while the plugin is missing only
|
|
1599
|
+
// throws and burns a finite attempt, so a long reload could exhaust the
|
|
1600
|
+
// retries and strand the sandbox. A permanent uninstall also cannot tear
|
|
1601
|
+
// the sandbox down, because there is no worker to call, so the preserved
|
|
1602
|
+
// pending_cleanup row still holds the durable cleanup state for later.
|
|
1603
|
+
const installed = await resolvePluginSandboxProviderDriverByKey({
|
|
1604
|
+
db,
|
|
1605
|
+
driverKey: recordedProvider,
|
|
1606
|
+
workerManager: pluginWorkerManager,
|
|
1607
|
+
requireRunning: false,
|
|
1608
|
+
});
|
|
1609
|
+
if (!installed)
|
|
1610
|
+
return false;
|
|
1611
|
+
// The plugin is installed but not ready yet. A plugin reload or a plugin
|
|
1612
|
+
// reinstall moves the plugin through this state, so it is a transient
|
|
1613
|
+
// window, not a permanent condition. Report not ready, so the sweep skips
|
|
1614
|
+
// the lease without a claim, and a later sweep retries after the plugin
|
|
1615
|
+
// becomes ready. A teardown here only throws "is not_ready" and burns a
|
|
1616
|
+
// finite attempt, so a long reload could exhaust the retries and strand the
|
|
1617
|
+
// sandbox.
|
|
1618
|
+
if (installed.plugin.status !== "ready")
|
|
1619
|
+
return false;
|
|
1620
|
+
// The plugin is installed and ready, so gate on the live worker. A running
|
|
1621
|
+
// worker is ready. A down worker is the transient restart window, so report
|
|
1622
|
+
// not ready and let a later sweep retry after the worker recovers.
|
|
1623
|
+
return pluginWorkerManager.isRunning(installed.plugin.id);
|
|
1624
|
+
},
|
|
1625
|
+
flushDeferredOrphanCleanups,
|
|
909
1626
|
async realizeWorkspace(input) {
|
|
910
1627
|
// Resolve the realized cwd and any provider metadata first, then build ONE
|
|
911
1628
|
// workspace-realization record and wrap it the SAME way for every driver. A
|
|
@@ -1077,6 +1794,81 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
1077
1794
|
async syncOut(input) {
|
|
1078
1795
|
return await callPluginEnvironmentSync("environmentSyncOut", input);
|
|
1079
1796
|
},
|
|
1797
|
+
async effectiveSandboxCapabilities(input) {
|
|
1798
|
+
const metadata = input.lease.metadata ?? {};
|
|
1799
|
+
const providerKey = readString(metadata.provider) ??
|
|
1800
|
+
(input.environment.driver === "sandbox"
|
|
1801
|
+
? readString(parseEnvironmentDriverConfig(input.environment).config.provider)
|
|
1802
|
+
: null);
|
|
1803
|
+
if (!providerKey) {
|
|
1804
|
+
return resolveEffectiveSandboxCapabilities({ verifiedMethods: [], declared: null, narrowing: null });
|
|
1805
|
+
}
|
|
1806
|
+
let declared = null;
|
|
1807
|
+
let verifiedMethods = [];
|
|
1808
|
+
let configResolutionFailed = false;
|
|
1809
|
+
if (metadata.sandboxProviderPlugin) {
|
|
1810
|
+
const pluginId = readString(metadata.pluginId);
|
|
1811
|
+
// Read the declaration from the exact plugin that acquired the lease,
|
|
1812
|
+
// not the first installed plugin with this driver key. A driver key is
|
|
1813
|
+
// only unique inside one manifest, so two plugins can share it. The
|
|
1814
|
+
// by-key resolver could intersect this lease's verified methods with a
|
|
1815
|
+
// different plugin's declaration. This resolver fails closed: it returns
|
|
1816
|
+
// null when the pinned plugin id is absent, or when that exact plugin no
|
|
1817
|
+
// longer declares this provider key with the `sandbox_provider` kind.
|
|
1818
|
+
const resolvedDriver = pluginId
|
|
1819
|
+
? await resolvePluginSandboxProviderDriverById({
|
|
1820
|
+
db,
|
|
1821
|
+
pluginId,
|
|
1822
|
+
driverKey: providerKey,
|
|
1823
|
+
})
|
|
1824
|
+
: null;
|
|
1825
|
+
if (!pluginId || !resolvedDriver) {
|
|
1826
|
+
// Exact-plugin identity failure. The lease pins a plugin id, but the
|
|
1827
|
+
// id is missing, that plugin is absent, or it no longer declares this
|
|
1828
|
+
// provider key. Fail closed: resolve every effective capability to
|
|
1829
|
+
// false, no matter what methods a stale or running worker still
|
|
1830
|
+
// advertises. Do not read the worker methods here; passing them would
|
|
1831
|
+
// let an identity-less lease keep a verified baseline. This differs
|
|
1832
|
+
// from a valid plugin whose manifest merely omits
|
|
1833
|
+
// `sandboxCapabilities`: that case keeps `declared` null below and
|
|
1834
|
+
// defers to verified worker discovery.
|
|
1835
|
+
return resolveEffectiveSandboxCapabilities({
|
|
1836
|
+
verifiedMethods: [],
|
|
1837
|
+
declared: null,
|
|
1838
|
+
narrowing: null,
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
verifiedMethods = pluginWorkerManager?.getWorker(pluginId)?.supportedMethods ?? [];
|
|
1842
|
+
declared = resolveDeclaredSandboxCapabilities(resolvedDriver.driver);
|
|
1843
|
+
try {
|
|
1844
|
+
// Resolve the provider config to confirm it is readable. A provider
|
|
1845
|
+
// whose config cannot be resolved is untrusted, so a resolution error
|
|
1846
|
+
// fails closed on persistent process sessions below. The resolved
|
|
1847
|
+
// value itself is no longer read for the narrowing decision.
|
|
1848
|
+
await resolvePluginSandboxRuntimeConfig({
|
|
1849
|
+
environment: input.environment,
|
|
1850
|
+
lease: input.lease,
|
|
1851
|
+
provider: providerKey,
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
catch {
|
|
1855
|
+
configResolutionFailed = true;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
else {
|
|
1859
|
+
const builtin = getBuiltinSandboxProvider(providerKey);
|
|
1860
|
+
verifiedMethods = builtinSandboxProviderVerifiedMethods(builtin);
|
|
1861
|
+
declared = resolveDeclaredSandboxCapabilities({
|
|
1862
|
+
supportsReusableLeases: builtin?.supportsReusableLeases,
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
const narrowing = buildSandboxCapabilityNarrowing({
|
|
1866
|
+
leasePolicy: input.lease.leasePolicy,
|
|
1867
|
+
leaseMetadata: metadata,
|
|
1868
|
+
configResolutionFailed,
|
|
1869
|
+
});
|
|
1870
|
+
return resolveEffectiveSandboxCapabilities({ verifiedMethods, declared, narrowing });
|
|
1871
|
+
},
|
|
1080
1872
|
async destroyRunLease(input) {
|
|
1081
1873
|
return await destroyReusableSandboxLease({
|
|
1082
1874
|
environment: input.environment,
|
|
@@ -1085,12 +1877,29 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
1085
1877
|
});
|
|
1086
1878
|
},
|
|
1087
1879
|
};
|
|
1880
|
+
/**
|
|
1881
|
+
* Verify that the live plugin worker still advertises a reusable-lease
|
|
1882
|
+
* lifecycle method before the runtime dispatches that RPC. The worker reports
|
|
1883
|
+
* `supportedMethods` from its discovery list on every start. A worker restart
|
|
1884
|
+
* can drop a lifecycle method a reusable lease was created under. The runtime
|
|
1885
|
+
* must not dispatch a lifecycle RPC the live worker does not advertise. It
|
|
1886
|
+
* fails closed when the worker is absent or the method is stale, so a lease
|
|
1887
|
+
* that a worker can no longer clean up goes to the cleanup reaper instead of
|
|
1888
|
+
* a doomed RPC.
|
|
1889
|
+
*/
|
|
1890
|
+
function pluginWorkerVerifiesLifecycleMethod(pluginId, method) {
|
|
1891
|
+
const advertised = pluginWorkerManager?.getWorker(pluginId)?.supportedMethods ?? [];
|
|
1892
|
+
return advertised.includes(method);
|
|
1893
|
+
}
|
|
1088
1894
|
async function releasePluginBackedSandboxLease(input) {
|
|
1089
1895
|
const metadata = input.lease.metadata ?? {};
|
|
1090
1896
|
const pluginId = readString(metadata.pluginId);
|
|
1091
1897
|
const providerKey = readString(metadata.provider);
|
|
1092
1898
|
let cleanupStatus = "success";
|
|
1093
|
-
if (pluginId &&
|
|
1899
|
+
if (pluginId &&
|
|
1900
|
+
providerKey &&
|
|
1901
|
+
pluginWorkerManager?.isRunning(pluginId) &&
|
|
1902
|
+
pluginWorkerVerifiesLifecycleMethod(pluginId, "environmentReleaseLease")) {
|
|
1094
1903
|
try {
|
|
1095
1904
|
const config = await resolvePluginSandboxRuntimeConfig({
|
|
1096
1905
|
environment: input.environment,
|
|
@@ -1114,11 +1923,24 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
1114
1923
|
else {
|
|
1115
1924
|
cleanupStatus = "failed";
|
|
1116
1925
|
}
|
|
1117
|
-
|
|
1926
|
+
// A failed release verification leaves the provider resource active. The
|
|
1927
|
+
// cleanup reaper retries only `pending_cleanup` leases, so route a failed
|
|
1928
|
+
// release into that retry flow. A `retain_on_failure` lease keeps the
|
|
1929
|
+
// resource on purpose for reuse, so it stays `retained` and never enters the
|
|
1930
|
+
// reaper, which would destroy the resource the retain policy wants to keep.
|
|
1931
|
+
const retained = input.lease.leasePolicy === "retain_on_failure" && input.status === "failed";
|
|
1932
|
+
const releaseStatus = retained
|
|
1118
1933
|
? "retained"
|
|
1119
|
-
:
|
|
1934
|
+
: cleanupStatus === "failed"
|
|
1935
|
+
? "pending_cleanup"
|
|
1936
|
+
: input.status;
|
|
1937
|
+
const failureReason = input.status === "failed"
|
|
1938
|
+
? "adapter_or_run_failure"
|
|
1939
|
+
: cleanupStatus === "failed"
|
|
1940
|
+
? "release_cleanup_failed"
|
|
1941
|
+
: undefined;
|
|
1120
1942
|
return await environmentsSvc.releaseLease(input.lease.id, releaseStatus, {
|
|
1121
|
-
failureReason
|
|
1943
|
+
failureReason,
|
|
1122
1944
|
cleanupStatus,
|
|
1123
1945
|
});
|
|
1124
1946
|
}
|
|
@@ -1129,7 +1951,10 @@ function createSandboxEnvironmentDriver(db, options = {}) {
|
|
|
1129
1951
|
if (metadata.sandboxProviderPlugin) {
|
|
1130
1952
|
const pluginId = readString(metadata.pluginId);
|
|
1131
1953
|
const providerKey = readString(metadata.provider);
|
|
1132
|
-
if (!pluginId ||
|
|
1954
|
+
if (!pluginId ||
|
|
1955
|
+
!providerKey ||
|
|
1956
|
+
!pluginWorkerManager?.isRunning(pluginId) ||
|
|
1957
|
+
!pluginWorkerVerifiesLifecycleMethod(pluginId, "environmentDestroyLease")) {
|
|
1133
1958
|
cleanupStatus = "failed";
|
|
1134
1959
|
}
|
|
1135
1960
|
else {
|
|
@@ -1184,24 +2009,66 @@ function parseExpiresAt(value) {
|
|
|
1184
2009
|
const parsed = new Date(value);
|
|
1185
2010
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
1186
2011
|
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Resolves the persisted lease expiry for a caller-requested deadline. It records
|
|
2014
|
+
* ONLY a provider-attested expiry as evidence of provider enforcement. It never
|
|
2015
|
+
* synthesizes the requested deadline onto the lease row: a database-only expiry
|
|
2016
|
+
* does not stop a remote sandbox after a server crash, so it must not stand in for
|
|
2017
|
+
* a provider-side bound. It returns the real provider expiry when the caller sets
|
|
2018
|
+
* a deadline (the caller then verifies the expiry bounds the deadline and fails
|
|
2019
|
+
* closed when it does not). It returns null when the caller sets a deadline and the
|
|
2020
|
+
* provider grants no expiry. It returns the provider expiry unchanged when the
|
|
2021
|
+
* caller requests no deadline, so all other callers keep the current behavior. It
|
|
2022
|
+
* ignores an invalid requested deadline.
|
|
2023
|
+
*/
|
|
2024
|
+
function providerAttestedLeaseExpiry(requestedExpiresAt, providerExpiresAt) {
|
|
2025
|
+
const requested = requestedExpiresAt instanceof Date && !Number.isNaN(requestedExpiresAt.getTime())
|
|
2026
|
+
? requestedExpiresAt
|
|
2027
|
+
: null;
|
|
2028
|
+
if (!requested)
|
|
2029
|
+
return providerExpiresAt;
|
|
2030
|
+
return providerExpiresAt ?? null;
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Converts a caller-requested deadline to the ISO 8601 string a provider acquire
|
|
2034
|
+
* RPC carries. It returns undefined for an absent or invalid deadline, so a
|
|
2035
|
+
* generic caller without a deadline sends no requested expiry and keeps the
|
|
2036
|
+
* current provider behavior.
|
|
2037
|
+
*/
|
|
2038
|
+
function requestedExpiresAtParam(requestedExpiresAt) {
|
|
2039
|
+
return requestedExpiresAt instanceof Date && !Number.isNaN(requestedExpiresAt.getTime())
|
|
2040
|
+
? requestedExpiresAt.toISOString()
|
|
2041
|
+
: undefined;
|
|
2042
|
+
}
|
|
1187
2043
|
function pluginDriverProviderKey(config) {
|
|
1188
2044
|
return `${config.pluginKey}:${config.driverKey}`;
|
|
1189
2045
|
}
|
|
1190
2046
|
function readString(value) {
|
|
1191
2047
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1192
2048
|
}
|
|
2049
|
+
// Keys the runtime stores in the lease metadata that are not part of the
|
|
2050
|
+
// provider driver config. Some are host-internal control fields. `remoteCwd` is
|
|
2051
|
+
// a per-lease runtime value. The host reads `remoteCwd` from the lease metadata
|
|
2052
|
+
// directly, so the worker never needs it as config. Drop every key here before
|
|
2053
|
+
// the runtime sends a config to a lifecycle RPC.
|
|
1193
2054
|
const INTERNAL_PLUGIN_SANDBOX_CONFIG_KEYS = new Set([
|
|
1194
2055
|
"driver",
|
|
1195
2056
|
"executionWorkspaceMode",
|
|
1196
2057
|
"pluginId",
|
|
1197
2058
|
"pluginKey",
|
|
1198
2059
|
"providerMetadata",
|
|
2060
|
+
"remoteCwd",
|
|
1199
2061
|
"shellCommand",
|
|
1200
2062
|
"sandboxProviderPlugin",
|
|
1201
2063
|
]);
|
|
1202
|
-
|
|
2064
|
+
// Drop the host-internal and per-lease runtime keys from a sandbox config
|
|
2065
|
+
// record. The runtime stores these keys in the lease metadata and in some
|
|
2066
|
+
// resolved configs, but the plugin worker must receive only the provider driver
|
|
2067
|
+
// config. Use this on every config the runtime sends to a lifecycle RPC, so no
|
|
2068
|
+
// host-internal field reaches the worker.
|
|
2069
|
+
function dropInternalPluginSandboxConfigKeys(config) {
|
|
1203
2070
|
const sanitized = {};
|
|
1204
|
-
for (const [key, value] of Object.entries(
|
|
2071
|
+
for (const [key, value] of Object.entries(config ?? {})) {
|
|
1205
2072
|
if (INTERNAL_PLUGIN_SANDBOX_CONFIG_KEYS.has(key))
|
|
1206
2073
|
continue;
|
|
1207
2074
|
sanitized[key] = value;
|
|
@@ -1308,6 +2175,11 @@ function createPluginEnvironmentDriver(db, workerManager) {
|
|
|
1308
2175
|
executionWorkspaceId: input.executionWorkspaceId ?? undefined,
|
|
1309
2176
|
adapterType: input.adapterType ?? undefined,
|
|
1310
2177
|
executionWorkspaceSettings: input.executionWorkspaceSettings,
|
|
2178
|
+
// Forward a caller deadline so the provider configures a provider-side
|
|
2179
|
+
// expiry at or before it and returns the real provider expiry.
|
|
2180
|
+
...(requestedExpiresAtParam(input.requestedExpiresAt) !== undefined
|
|
2181
|
+
? { requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt) }
|
|
2182
|
+
: {}),
|
|
1311
2183
|
});
|
|
1312
2184
|
return await environmentsSvc.acquireLease({
|
|
1313
2185
|
companyId: input.companyId,
|
|
@@ -1318,7 +2190,7 @@ function createPluginEnvironmentDriver(db, workerManager) {
|
|
|
1318
2190
|
leasePolicy: "ephemeral",
|
|
1319
2191
|
provider: `plugin:${parsed.config.pluginKey}:${parsed.config.driverKey}`,
|
|
1320
2192
|
providerLeaseId: providerLease.providerLeaseId,
|
|
1321
|
-
expiresAt: parseExpiresAt(providerLease.expiresAt),
|
|
2193
|
+
expiresAt: providerAttestedLeaseExpiry(input.requestedExpiresAt, parseExpiresAt(providerLease.expiresAt)),
|
|
1322
2194
|
metadata: {
|
|
1323
2195
|
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
1324
2196
|
providerMetadata: providerLease.metadata ?? {},
|
|
@@ -1466,6 +2338,11 @@ export function environmentRuntimeService(db, options = {}) {
|
|
|
1466
2338
|
pluginWorkerManager: options.pluginWorkerManager,
|
|
1467
2339
|
pluginWorkerReadyTimeoutMs: options.pluginWorkerReadyTimeoutMs,
|
|
1468
2340
|
pluginWorkerReadyPollMs: options.pluginWorkerReadyPollMs,
|
|
2341
|
+
pendingCleanupWriteAttempts: options.pendingCleanupWriteAttempts,
|
|
2342
|
+
pendingCleanupWriteBackoffMs: options.pendingCleanupWriteBackoffMs,
|
|
2343
|
+
deferredOrphanCleanupBufferLimit: options.deferredOrphanCleanupBufferLimit,
|
|
2344
|
+
orphanCleanupSpool: options.orphanCleanupSpool,
|
|
2345
|
+
orphanCleanupSpoolDir: options.orphanCleanupSpoolDir,
|
|
1469
2346
|
}),
|
|
1470
2347
|
...(options.pluginWorkerManager
|
|
1471
2348
|
? [createPluginEnvironmentDriver(db, options.pluginWorkerManager)]
|
|
@@ -1512,6 +2389,8 @@ export function environmentRuntimeService(db, options = {}) {
|
|
|
1512
2389
|
executionWorkspaceSettings: input.executionWorkspaceSettings ?? null,
|
|
1513
2390
|
adapterType: input.adapterType ?? null,
|
|
1514
2391
|
applyCustomImageTemplate: input.applyCustomImageTemplate ?? false,
|
|
2392
|
+
requestedExpiresAt: input.requestedExpiresAt ?? null,
|
|
2393
|
+
assertCompanyBinding: input.assertCompanyBinding,
|
|
1515
2394
|
});
|
|
1516
2395
|
return {
|
|
1517
2396
|
environment: input.environment,
|
|
@@ -1519,7 +2398,7 @@ export function environmentRuntimeService(db, options = {}) {
|
|
|
1519
2398
|
leaseContext,
|
|
1520
2399
|
};
|
|
1521
2400
|
},
|
|
1522
|
-
async releaseRunLeases(heartbeatRunId, status = "released") {
|
|
2401
|
+
async releaseRunLeases(heartbeatRunId, status = "released", onLeaseReleaseError) {
|
|
1523
2402
|
const leaseRows = await db
|
|
1524
2403
|
.select()
|
|
1525
2404
|
.from(environmentLeases)
|
|
@@ -1527,33 +2406,87 @@ export function environmentRuntimeService(db, options = {}) {
|
|
|
1527
2406
|
if (leaseRows.length === 0) {
|
|
1528
2407
|
return [];
|
|
1529
2408
|
}
|
|
2409
|
+
// Release each lease in its own try/catch. One driver error must not stop
|
|
2410
|
+
// the release of the later leases. The caller records each lease-specific
|
|
2411
|
+
// error through `onLeaseReleaseError` for its log path. Keep the order
|
|
2412
|
+
// serial.
|
|
1530
2413
|
const released = [];
|
|
1531
2414
|
for (const leaseRow of leaseRows) {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2415
|
+
try {
|
|
2416
|
+
const environment = leaseRow.environmentId
|
|
2417
|
+
? await environmentsSvc.getById(leaseRow.environmentId)
|
|
2418
|
+
: null;
|
|
2419
|
+
if (!environment)
|
|
2420
|
+
continue;
|
|
2421
|
+
const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow);
|
|
2422
|
+
const driver = getDriver(getLeaseDriverKey(leaseSnapshot, environment));
|
|
2423
|
+
const lease = driver
|
|
2424
|
+
? await driver.releaseRunLease({
|
|
2425
|
+
environment,
|
|
2426
|
+
lease: leaseSnapshot,
|
|
2427
|
+
status,
|
|
2428
|
+
})
|
|
2429
|
+
: await environmentsSvc.releaseLease(leaseRow.id, status);
|
|
2430
|
+
if (!lease)
|
|
2431
|
+
continue;
|
|
2432
|
+
released.push({
|
|
1539
2433
|
environment,
|
|
1540
|
-
lease
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
executionWorkspaceId: lease.executionWorkspaceId,
|
|
1551
|
-
executionWorkspaceMode: lease.metadata?.executionWorkspaceMode ?? null,
|
|
1552
|
-
},
|
|
1553
|
-
});
|
|
2434
|
+
lease,
|
|
2435
|
+
leaseContext: {
|
|
2436
|
+
executionWorkspaceId: lease.executionWorkspaceId,
|
|
2437
|
+
executionWorkspaceMode: lease.metadata?.executionWorkspaceMode ?? null,
|
|
2438
|
+
},
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
catch (error) {
|
|
2442
|
+
onLeaseReleaseError?.(leaseRow.id, error);
|
|
2443
|
+
}
|
|
1554
2444
|
}
|
|
1555
2445
|
return released;
|
|
1556
2446
|
},
|
|
2447
|
+
// Tear an orphan ephemeral sandbox down from its recorded provider config.
|
|
2448
|
+
// A failed acquire records the orphan as a `pending_cleanup` lease. The row
|
|
2449
|
+
// keeps the provider, the provider lease id, and the sandbox config, so this
|
|
2450
|
+
// teardown runs without the environment row and accepts a null environment.
|
|
2451
|
+
// The teardown resolves the recorded secret refs through the durable orphan
|
|
2452
|
+
// record, so a deleted or foreign-bound environment never strands it. The
|
|
2453
|
+
// caller owns the lease release; this dispatcher only runs the provider
|
|
2454
|
+
// teardown and throws when the driver teardown throws.
|
|
2455
|
+
async retryPendingSandboxTeardown(input) {
|
|
2456
|
+
const driver = requireDriverKey(getLeaseDriverKey(input.lease, input.environment));
|
|
2457
|
+
if (!driver.retryPendingSandboxTeardown) {
|
|
2458
|
+
throw new Error(`Environment driver "${driver.driver}" does not support orphan sandbox teardown.`);
|
|
2459
|
+
}
|
|
2460
|
+
await driver.retryPendingSandboxTeardown(input);
|
|
2461
|
+
},
|
|
2462
|
+
// Report whether the provider worker can run an orphan teardown now. The
|
|
2463
|
+
// cleanup sweep calls this before it claims a finite retry attempt, so a
|
|
2464
|
+
// briefly-down plugin worker never burns an attempt. This dispatcher never
|
|
2465
|
+
// throws: an unregistered driver, or a driver with no probe, reports ready,
|
|
2466
|
+
// so the teardown still runs and its own failure counts toward the cap.
|
|
2467
|
+
async isPendingCleanupWorkerReady(input) {
|
|
2468
|
+
const driver = getDriver(getLeaseDriverKey(input.lease, input.environment));
|
|
2469
|
+
if (!driver?.isPendingCleanupWorkerReady)
|
|
2470
|
+
return true;
|
|
2471
|
+
return driver.isPendingCleanupWorkerReady(input);
|
|
2472
|
+
},
|
|
2473
|
+
// Flush the in-process orphan-cleanup buffers of every driver that keeps one.
|
|
2474
|
+
// The cleanup sweep calls this each tick, so a buffered orphan lands a durable
|
|
2475
|
+
// `pending_cleanup` row once the database recovers, and the same sweep tears
|
|
2476
|
+
// it down. The dispatcher sums the recovered and pending counts across all
|
|
2477
|
+
// drivers. A driver with no buffer contributes nothing.
|
|
2478
|
+
async flushDeferredOrphanCleanups() {
|
|
2479
|
+
let recovered = 0;
|
|
2480
|
+
let pending = 0;
|
|
2481
|
+
for (const driver of drivers.values()) {
|
|
2482
|
+
if (!driver.flushDeferredOrphanCleanups)
|
|
2483
|
+
continue;
|
|
2484
|
+
const result = await driver.flushDeferredOrphanCleanups();
|
|
2485
|
+
recovered += result.recovered;
|
|
2486
|
+
pending += result.pending;
|
|
2487
|
+
}
|
|
2488
|
+
return { recovered, pending };
|
|
2489
|
+
},
|
|
1557
2490
|
async destroyReusableSandboxLeases(input) {
|
|
1558
2491
|
const scopeConditions = [
|
|
1559
2492
|
input.issueId ? eq(environmentLeases.issueId, input.issueId) : undefined,
|
|
@@ -1567,7 +2500,9 @@ export function environmentRuntimeService(db, options = {}) {
|
|
|
1567
2500
|
.where(and(eq(environmentLeases.companyId, input.companyId), eq(environmentLeases.leasePolicy, "reuse_by_environment"), inArray(environmentLeases.status, ["active", "released", "retained", "pending_cleanup"]), ...scopeConditions));
|
|
1568
2501
|
const destroyed = [];
|
|
1569
2502
|
for (const leaseRow of leaseRows) {
|
|
1570
|
-
const environment =
|
|
2503
|
+
const environment = leaseRow.environmentId
|
|
2504
|
+
? await environmentsSvc.getById(leaseRow.environmentId)
|
|
2505
|
+
: null;
|
|
1571
2506
|
if (!environment)
|
|
1572
2507
|
continue;
|
|
1573
2508
|
const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow);
|
|
@@ -1627,6 +2562,10 @@ export function environmentRuntimeService(db, options = {}) {
|
|
|
1627
2562
|
const driver = getDriver(getLeaseDriverKey(input.lease, input.environment));
|
|
1628
2563
|
return driver?.supportsSync?.(input) ?? false;
|
|
1629
2564
|
},
|
|
2565
|
+
async effectiveSandboxCapabilities(input) {
|
|
2566
|
+
const driver = getDriver(getLeaseDriverKey(input.lease, input.environment));
|
|
2567
|
+
return (await driver?.effectiveSandboxCapabilities?.(input)) ?? null;
|
|
2568
|
+
},
|
|
1630
2569
|
async syncIn(input) {
|
|
1631
2570
|
const driver = requireDriverKey(getLeaseDriverKey(input.lease, input.environment));
|
|
1632
2571
|
if (!driver.syncIn) {
|