@capybara-hq/kernel 0.0.0-stage → 0.1.0-rc.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +20 -2
- package/bootstrap.json +26 -0
- package/dist/admin/admin.controller.d.ts +139 -0
- package/dist/admin/admin.controller.js +238 -0
- package/dist/admin/extensions.controller.d.ts +100 -0
- package/dist/admin/extensions.controller.js +226 -0
- package/dist/admin/setup.controller.d.ts +36 -0
- package/dist/admin/setup.controller.js +106 -0
- package/dist/admin/setup.service.d.ts +47 -0
- package/dist/admin/setup.service.js +90 -0
- package/dist/agents/agents.controller.d.ts +205 -0
- package/dist/agents/agents.controller.js +144 -0
- package/dist/agents/agents.service.d.ts +177 -0
- package/dist/agents/agents.service.js +677 -0
- package/dist/agents/definition.d.ts +198 -0
- package/dist/agents/definition.js +152 -0
- package/dist/agents/models.controller.d.ts +13 -0
- package/dist/agents/models.controller.js +31 -0
- package/dist/agents/templates.controller.d.ts +15 -0
- package/dist/agents/templates.controller.js +37 -0
- package/dist/agents/templates.d.ts +83 -0
- package/dist/agents/templates.js +173 -0
- package/dist/catalog/admin-catalog.controller.d.ts +143 -0
- package/dist/catalog/admin-catalog.controller.js +245 -0
- package/dist/catalog/catalog.service.d.ts +430 -0
- package/dist/catalog/catalog.service.js +882 -0
- package/dist/catalog/claude/agents.d.ts +27 -0
- package/dist/catalog/claude/agents.js +270 -0
- package/dist/catalog/claude/commands.d.ts +50 -0
- package/dist/catalog/claude/commands.js +179 -0
- package/dist/catalog/claude/manifest.d.ts +69 -0
- package/dist/catalog/claude/manifest.js +134 -0
- package/dist/catalog/claude/marketplace.d.ts +35 -0
- package/dist/catalog/claude/marketplace.js +194 -0
- package/dist/catalog/claude/mcp.d.ts +33 -0
- package/dist/catalog/claude/mcp.js +226 -0
- package/dist/catalog/claude/report.d.ts +86 -0
- package/dist/catalog/claude/report.js +121 -0
- package/dist/catalog/claude/translate.d.ts +56 -0
- package/dist/catalog/claude/translate.js +857 -0
- package/dist/catalog/cli.d.ts +46 -0
- package/dist/catalog/cli.js +211 -0
- package/dist/catalog/git-source.d.ts +127 -0
- package/dist/catalog/git-source.js +520 -0
- package/dist/catalog/marketplace-fetch.d.ts +99 -0
- package/dist/catalog/marketplace-fetch.js +203 -0
- package/dist/catalog/marketplaces.controller.d.ts +97 -0
- package/dist/catalog/marketplaces.controller.js +197 -0
- package/dist/catalog/marketplaces.service.d.ts +340 -0
- package/dist/catalog/marketplaces.service.js +1217 -0
- package/dist/catalog/org-catalog.controller.d.ts +128 -0
- package/dist/catalog/org-catalog.controller.js +277 -0
- package/dist/catalog/org-catalog.service.d.ts +246 -0
- package/dist/catalog/org-catalog.service.js +625 -0
- package/dist/catalog/plugin-import.d.ts +436 -0
- package/dist/catalog/plugin-import.js +1069 -0
- package/dist/catalog/plugin-install.d.ts +447 -0
- package/dist/catalog/plugin-install.js +1333 -0
- package/dist/catalog/plugin-package.d.ts +143 -0
- package/dist/catalog/plugin-package.js +382 -0
- package/dist/catalog/plugin-upgrade.d.ts +222 -0
- package/dist/catalog/plugin-upgrade.js +1219 -0
- package/dist/catalog/requests.service.d.ts +162 -0
- package/dist/catalog/requests.service.js +504 -0
- package/dist/catalog/schema-path.d.ts +7 -0
- package/dist/catalog/schema-path.js +63 -0
- package/dist/catalog/upgrade-targets.d.ts +59 -0
- package/dist/catalog/upgrade-targets.js +90 -0
- package/dist/catalog/versions.d.ts +31 -0
- package/dist/catalog/versions.js +44 -0
- package/dist/channels/actions.d.ts +37 -0
- package/dist/channels/actions.js +121 -0
- package/dist/channels/cards.d.ts +44 -0
- package/dist/channels/cards.js +156 -0
- package/dist/channels/connections.controller.d.ts +59 -0
- package/dist/channels/connections.controller.js +115 -0
- package/dist/channels/connections.service.d.ts +187 -0
- package/dist/channels/connections.service.js +534 -0
- package/dist/channels/features.d.ts +12 -0
- package/dist/channels/features.js +22 -0
- package/dist/channels/inbound.d.ts +120 -0
- package/dist/channels/inbound.js +332 -0
- package/dist/channels/outbound.d.ts +140 -0
- package/dist/channels/outbound.js +1126 -0
- package/dist/channels/phrases.d.ts +100 -0
- package/dist/channels/phrases.js +81 -0
- package/dist/channels/supervisor.d.ts +182 -0
- package/dist/channels/supervisor.js +625 -0
- package/dist/channels/threads.d.ts +29 -0
- package/dist/channels/threads.js +104 -0
- package/dist/channels/webhook-url.d.ts +16 -0
- package/dist/channels/webhook-url.js +34 -0
- package/dist/channels/webhook.controller.d.ts +19 -0
- package/dist/channels/webhook.controller.js +72 -0
- package/dist/channels/webhook.d.ts +73 -0
- package/dist/channels/webhook.js +274 -0
- package/dist/config/docs.d.ts +9 -0
- package/dist/config/docs.js +85 -0
- package/dist/config/env.d.ts +6 -0
- package/dist/config/env.js +73 -0
- package/dist/config/fields.d.ts +42 -0
- package/dist/config/fields.js +214 -0
- package/dist/config/file-source.d.ts +3 -0
- package/dist/config/file-source.js +31 -0
- package/dist/config/load.d.ts +20 -0
- package/dist/config/load.js +98 -0
- package/dist/config/nacos-source.d.ts +31 -0
- package/dist/config/nacos-source.js +229 -0
- package/dist/config/schema.d.ts +237 -0
- package/dist/config/schema.js +1192 -0
- package/dist/config/sources.d.ts +18 -0
- package/dist/config/sources.js +59 -0
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.js +7 -0
- package/dist/conversations/artifacts.d.ts +52 -0
- package/dist/conversations/artifacts.js +137 -0
- package/dist/conversations/attachments.controller.d.ts +28 -0
- package/dist/conversations/attachments.controller.js +96 -0
- package/dist/conversations/attachments.service.d.ts +180 -0
- package/dist/conversations/attachments.service.js +578 -0
- package/dist/conversations/conversations.controller.d.ts +288 -0
- package/dist/conversations/conversations.controller.js +607 -0
- package/dist/conversations/conversations.service.d.ts +321 -0
- package/dist/conversations/conversations.service.js +754 -0
- package/dist/conversations/event-log.d.ts +122 -0
- package/dist/conversations/event-log.js +259 -0
- package/dist/conversations/events.d.ts +856 -0
- package/dist/conversations/events.js +639 -0
- package/dist/conversations/files.d.ts +92 -0
- package/dist/conversations/files.js +213 -0
- package/dist/conversations/outputs-read.d.ts +34 -0
- package/dist/conversations/outputs-read.js +37 -0
- package/dist/conversations/queue.d.ts +169 -0
- package/dist/conversations/queue.js +527 -0
- package/dist/conversations/sandbox-status.d.ts +21 -0
- package/dist/conversations/sandbox-status.js +24 -0
- package/dist/conversations/selection.d.ts +46 -0
- package/dist/conversations/selection.js +75 -0
- package/dist/conversations/titles.d.ts +93 -0
- package/dist/conversations/titles.js +423 -0
- package/dist/ctx/bus.d.ts +16 -0
- package/dist/ctx/bus.js +101 -0
- package/dist/ctx/create-ctx.d.ts +109 -0
- package/dist/ctx/create-ctx.js +592 -0
- package/dist/ctx/kv.d.ts +4 -0
- package/dist/ctx/kv.js +31 -0
- package/dist/ctx/lock.d.ts +4 -0
- package/dist/ctx/lock.js +39 -0
- package/dist/ctx/namespace.d.ts +6 -0
- package/dist/ctx/namespace.js +12 -0
- package/dist/ctx/queue.d.ts +25 -0
- package/dist/ctx/queue.js +151 -0
- package/dist/ctx/secrets.d.ts +45 -0
- package/dist/ctx/secrets.js +175 -0
- package/dist/db/kernel-schema.d.ts +1122 -0
- package/dist/db/kernel-schema.js +5 -0
- package/dist/db/migrator.d.ts +28 -0
- package/dist/db/migrator.js +140 -0
- package/dist/db/pool.d.ts +21 -0
- package/dist/db/pool.js +28 -0
- package/dist/db/sql-statements.d.ts +34 -0
- package/dist/db/sql-statements.js +168 -0
- package/dist/errors.d.ts +211 -0
- package/dist/errors.js +213 -0
- package/dist/evals/budget.d.ts +61 -0
- package/dist/evals/budget.js +181 -0
- package/dist/evals/diff.d.ts +55 -0
- package/dist/evals/diff.js +133 -0
- package/dist/evals/judge.d.ts +53 -0
- package/dist/evals/judge.js +149 -0
- package/dist/evals/plan.d.ts +47 -0
- package/dist/evals/plan.js +77 -0
- package/dist/evals/reconstruct.d.ts +137 -0
- package/dist/evals/reconstruct.js +322 -0
- package/dist/evals/recorded-tools.d.ts +54 -0
- package/dist/evals/recorded-tools.js +167 -0
- package/dist/evals/run-replay.d.ts +86 -0
- package/dist/evals/run-replay.js +502 -0
- package/dist/evals/runs.controller.d.ts +100 -0
- package/dist/evals/runs.controller.js +199 -0
- package/dist/evals/runs.service.d.ts +275 -0
- package/dist/evals/runs.service.js +836 -0
- package/dist/evals/sets.controller.d.ts +61 -0
- package/dist/evals/sets.controller.js +183 -0
- package/dist/evals/sets.service.d.ts +128 -0
- package/dist/evals/sets.service.js +558 -0
- package/dist/evals/snapshot.d.ts +76 -0
- package/dist/evals/snapshot.js +207 -0
- package/dist/evals/step-replay.d.ts +113 -0
- package/dist/evals/step-replay.js +181 -0
- package/dist/evals/trajectory.controller.d.ts +41 -0
- package/dist/evals/trajectory.controller.js +88 -0
- package/dist/evals/trajectory.d.ts +144 -0
- package/dist/evals/trajectory.js +338 -0
- package/dist/evals/trajectory.service.d.ts +108 -0
- package/dist/evals/trajectory.service.js +323 -0
- package/dist/evals/worker.d.ts +101 -0
- package/dist/evals/worker.js +790 -0
- package/dist/extensions/admin-ops.d.ts +105 -0
- package/dist/extensions/admin-ops.js +353 -0
- package/dist/extensions/admin-view.d.ts +79 -0
- package/dist/extensions/admin-view.js +327 -0
- package/dist/extensions/approvals.d.ts +119 -0
- package/dist/extensions/approvals.js +205 -0
- package/dist/extensions/blobs.d.ts +45 -0
- package/dist/extensions/blobs.js +160 -0
- package/dist/extensions/bootstrap.d.ts +176 -0
- package/dist/extensions/bootstrap.js +535 -0
- package/dist/extensions/cli.d.ts +61 -0
- package/dist/extensions/cli.js +165 -0
- package/dist/extensions/config-json-schema.d.ts +9 -0
- package/dist/extensions/config-json-schema.js +73 -0
- package/dist/extensions/deployment-config.d.ts +138 -0
- package/dist/extensions/deployment-config.js +394 -0
- package/dist/extensions/enabled.d.ts +31 -0
- package/dist/extensions/enabled.js +55 -0
- package/dist/extensions/hooks.d.ts +161 -0
- package/dist/extensions/hooks.js +425 -0
- package/dist/extensions/host.d.ts +577 -0
- package/dist/extensions/host.js +2071 -0
- package/dist/extensions/inflight.d.ts +69 -0
- package/dist/extensions/inflight.js +277 -0
- package/dist/extensions/installs.d.ts +166 -0
- package/dist/extensions/installs.js +253 -0
- package/dist/extensions/instance.d.ts +167 -0
- package/dist/extensions/instance.js +329 -0
- package/dist/extensions/ledger.d.ts +30 -0
- package/dist/extensions/ledger.js +50 -0
- package/dist/extensions/lifecycle.d.ts +80 -0
- package/dist/extensions/lifecycle.js +152 -0
- package/dist/extensions/migrate-lock.d.ts +51 -0
- package/dist/extensions/migrate-lock.js +100 -0
- package/dist/extensions/org-config.controller.d.ts +24 -0
- package/dist/extensions/org-config.controller.js +59 -0
- package/dist/extensions/org-config.d.ts +169 -0
- package/dist/extensions/org-config.js +520 -0
- package/dist/extensions/package.d.ts +115 -0
- package/dist/extensions/package.js +246 -0
- package/dist/extensions/permission-set.d.ts +117 -0
- package/dist/extensions/permission-set.js +184 -0
- package/dist/extensions/permission-view.d.ts +194 -0
- package/dist/extensions/permission-view.js +284 -0
- package/dist/extensions/registry.d.ts +24 -0
- package/dist/extensions/registry.js +56 -0
- package/dist/extensions/resolve.d.ts +12 -0
- package/dist/extensions/resolve.js +71 -0
- package/dist/extensions/sources.d.ts +69 -0
- package/dist/extensions/sources.js +120 -0
- package/dist/extensions/sync.d.ts +40 -0
- package/dist/extensions/sync.js +242 -0
- package/dist/files/previewers.controller.d.ts +46 -0
- package/dist/files/previewers.controller.js +105 -0
- package/dist/files/previewers.d.ts +100 -0
- package/dist/files/previewers.js +250 -0
- package/dist/http/api.module.d.ts +114 -0
- package/dist/http/api.module.js +448 -0
- package/dist/http/app.d.ts +77 -0
- package/dist/http/app.js +220 -0
- package/dist/http/error.filter.d.ts +21 -0
- package/dist/http/error.filter.js +85 -0
- package/dist/http/gateway.module.d.ts +15 -0
- package/dist/http/gateway.module.js +13 -0
- package/dist/http/health.controller.d.ts +26 -0
- package/dist/http/health.controller.js +79 -0
- package/dist/http/nest-logger.d.ts +20 -0
- package/dist/http/nest-logger.js +28 -0
- package/dist/http/plugin-dispatch.d.ts +68 -0
- package/dist/http/plugin-dispatch.js +200 -0
- package/dist/http/tokens.d.ts +131 -0
- package/dist/http/tokens.js +131 -0
- package/dist/http/zod.pipe.d.ts +8 -0
- package/dist/http/zod.pipe.js +14 -0
- package/dist/identity/accounts.d.ts +93 -0
- package/dist/identity/accounts.js +367 -0
- package/dist/identity/api-tokens.d.ts +73 -0
- package/dist/identity/api-tokens.js +197 -0
- package/dist/identity/audit.d.ts +23 -0
- package/dist/identity/audit.js +41 -0
- package/dist/identity/auth.controller.d.ts +40 -0
- package/dist/identity/auth.controller.js +130 -0
- package/dist/identity/auth.guard.d.ts +41 -0
- package/dist/identity/auth.guard.js +162 -0
- package/dist/identity/binding.d.ts +47 -0
- package/dist/identity/binding.js +84 -0
- package/dist/identity/cookies.d.ts +16 -0
- package/dist/identity/cookies.js +32 -0
- package/dist/identity/credential-check.d.ts +17 -0
- package/dist/identity/credential-check.js +24 -0
- package/dist/identity/enterprise-admin.d.ts +14 -0
- package/dist/identity/enterprise-admin.js +27 -0
- package/dist/identity/external.d.ts +106 -0
- package/dist/identity/external.js +337 -0
- package/dist/identity/hash.d.ts +2 -0
- package/dist/identity/hash.js +3 -0
- package/dist/identity/login.d.ts +6 -0
- package/dist/identity/login.js +20 -0
- package/dist/identity/me.controller.d.ts +61 -0
- package/dist/identity/me.controller.js +159 -0
- package/dist/identity/passwords.d.ts +6 -0
- package/dist/identity/passwords.js +37 -0
- package/dist/identity/principal.d.ts +56 -0
- package/dist/identity/principal.js +38 -0
- package/dist/identity/rate-limit.d.ts +55 -0
- package/dist/identity/rate-limit.js +121 -0
- package/dist/identity/sessions.d.ts +84 -0
- package/dist/identity/sessions.js +306 -0
- package/dist/identity/settings.d.ts +121 -0
- package/dist/identity/settings.js +248 -0
- package/dist/identity/sso.controller.d.ts +59 -0
- package/dist/identity/sso.controller.js +192 -0
- package/dist/identity/sso.d.ts +83 -0
- package/dist/identity/sso.js +202 -0
- package/dist/inbox/inbox.service.d.ts +43 -0
- package/dist/inbox/inbox.service.js +57 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +67 -0
- package/dist/kernel.d.ts +259 -0
- package/dist/kernel.js +1326 -0
- package/dist/log-config.d.ts +9 -0
- package/dist/log-config.js +27 -0
- package/dist/log-context.d.ts +32 -0
- package/dist/log-context.js +40 -0
- package/dist/log-loki.d.ts +34 -0
- package/dist/log-loki.js +145 -0
- package/dist/log.d.ts +79 -0
- package/dist/log.js +303 -0
- package/dist/models/prices.d.ts +53 -0
- package/dist/models/prices.js +121 -0
- package/dist/models/purposes.controller.d.ts +19 -0
- package/dist/models/purposes.controller.js +56 -0
- package/dist/models/purposes.d.ts +140 -0
- package/dist/models/purposes.js +267 -0
- package/dist/net/proxy.d.ts +44 -0
- package/dist/net/proxy.js +162 -0
- package/dist/net/public-address.d.ts +60 -0
- package/dist/net/public-address.js +272 -0
- package/dist/net/safe-fetch.d.ts +38 -0
- package/dist/net/safe-fetch.js +140 -0
- package/dist/orgs/directory-sync.controller.d.ts +14 -0
- package/dist/orgs/directory-sync.controller.js +40 -0
- package/dist/orgs/directory-sync.d.ts +166 -0
- package/dist/orgs/directory-sync.js +558 -0
- package/dist/orgs/invitations.controller.d.ts +58 -0
- package/dist/orgs/invitations.controller.js +137 -0
- package/dist/orgs/invitations.service.d.ts +80 -0
- package/dist/orgs/invitations.service.js +217 -0
- package/dist/orgs/members.service.d.ts +63 -0
- package/dist/orgs/members.service.js +234 -0
- package/dist/orgs/offboarding.d.ts +141 -0
- package/dist/orgs/offboarding.js +398 -0
- package/dist/orgs/org-lock.d.ts +32 -0
- package/dist/orgs/org-lock.js +46 -0
- package/dist/orgs/org.guard.d.ts +35 -0
- package/dist/orgs/org.guard.js +95 -0
- package/dist/orgs/orgs.controller.d.ts +78 -0
- package/dist/orgs/orgs.controller.js +192 -0
- package/dist/orgs/orgs.service.d.ts +61 -0
- package/dist/orgs/orgs.service.js +209 -0
- package/dist/orgs/roles.d.ts +13 -0
- package/dist/orgs/roles.js +58 -0
- package/dist/orgs/settings.controller.d.ts +20 -0
- package/dist/orgs/settings.controller.js +53 -0
- package/dist/orgs/settings.service.d.ts +120 -0
- package/dist/orgs/settings.service.js +178 -0
- package/dist/packages/npm.d.ts +50 -0
- package/dist/packages/npm.js +218 -0
- package/dist/packages/provenance.d.ts +47 -0
- package/dist/packages/provenance.js +147 -0
- package/dist/packages/tgz.d.ts +76 -0
- package/dist/packages/tgz.js +225 -0
- package/dist/runtime/abort.d.ts +32 -0
- package/dist/runtime/abort.js +71 -0
- package/dist/runtime/checkpointer.d.ts +32 -0
- package/dist/runtime/checkpointer.js +243 -0
- package/dist/runtime/context/build.d.ts +254 -0
- package/dist/runtime/context/build.js +693 -0
- package/dist/runtime/context/claude-tools.d.ts +11 -0
- package/dist/runtime/context/claude-tools.js +24 -0
- package/dist/runtime/context/compaction.d.ts +26 -0
- package/dist/runtime/context/compaction.js +195 -0
- package/dist/runtime/context/pairing.d.ts +11 -0
- package/dist/runtime/context/pairing.js +82 -0
- package/dist/runtime/context/reminders.d.ts +5 -0
- package/dist/runtime/context/reminders.js +11 -0
- package/dist/runtime/context/skills.d.ts +105 -0
- package/dist/runtime/context/skills.js +283 -0
- package/dist/runtime/context/tokens.d.ts +19 -0
- package/dist/runtime/context/tokens.js +104 -0
- package/dist/runtime/conversation-lock.d.ts +40 -0
- package/dist/runtime/conversation-lock.js +97 -0
- package/dist/runtime/hook-calls.d.ts +84 -0
- package/dist/runtime/hook-calls.js +136 -0
- package/dist/runtime/langgraph-loop.d.ts +52 -0
- package/dist/runtime/langgraph-loop.js +247 -0
- package/dist/runtime/loop.d.ts +93 -0
- package/dist/runtime/loop.js +7 -0
- package/dist/runtime/maintenance.d.ts +80 -0
- package/dist/runtime/maintenance.js +148 -0
- package/dist/runtime/media-cache.d.ts +21 -0
- package/dist/runtime/media-cache.js +51 -0
- package/dist/runtime/model-call.d.ts +176 -0
- package/dist/runtime/model-call.js +1049 -0
- package/dist/runtime/providers.d.ts +86 -0
- package/dist/runtime/providers.js +224 -0
- package/dist/runtime/quota.d.ts +29 -0
- package/dist/runtime/quota.js +70 -0
- package/dist/runtime/run-finish.d.ts +45 -0
- package/dist/runtime/run-finish.js +69 -0
- package/dist/runtime/run-queue.d.ts +121 -0
- package/dist/runtime/run-queue.js +493 -0
- package/dist/runtime/run-worker.d.ts +138 -0
- package/dist/runtime/run-worker.js +806 -0
- package/dist/runtime/stream.controller.d.ts +34 -0
- package/dist/runtime/stream.controller.js +122 -0
- package/dist/runtime/stream.d.ts +63 -0
- package/dist/runtime/stream.js +262 -0
- package/dist/runtime/subagents/child.d.ts +80 -0
- package/dist/runtime/subagents/child.js +225 -0
- package/dist/runtime/subagents/definition.d.ts +103 -0
- package/dist/runtime/subagents/definition.js +392 -0
- package/dist/runtime/subagents/dispatch.d.ts +69 -0
- package/dist/runtime/subagents/dispatch.js +502 -0
- package/dist/runtime/subagents/limits.d.ts +23 -0
- package/dist/runtime/subagents/limits.js +34 -0
- package/dist/runtime/subagents/org-agents.d.ts +50 -0
- package/dist/runtime/subagents/org-agents.js +70 -0
- package/dist/runtime/subagents/preamble.d.ts +7 -0
- package/dist/runtime/subagents/preamble.js +11 -0
- package/dist/runtime/subagents/progress.d.ts +26 -0
- package/dist/runtime/subagents/progress.js +36 -0
- package/dist/runtime/subagents/roster.d.ts +55 -0
- package/dist/runtime/subagents/roster.js +111 -0
- package/dist/runtime/subagents/skill-fork.d.ts +123 -0
- package/dist/runtime/subagents/skill-fork.js +259 -0
- package/dist/runtime/usage-report.d.ts +163 -0
- package/dist/runtime/usage-report.js +551 -0
- package/dist/runtime/waits.d.ts +233 -0
- package/dist/runtime/waits.js +883 -0
- package/dist/sandbox/leases.d.ts +169 -0
- package/dist/sandbox/leases.js +515 -0
- package/dist/sandbox/lifecycle.d.ts +21 -0
- package/dist/sandbox/lifecycle.js +68 -0
- package/dist/sandbox/sandbox.service.d.ts +249 -0
- package/dist/sandbox/sandbox.service.js +908 -0
- package/dist/skills/catalog.d.ts +149 -0
- package/dist/skills/catalog.js +405 -0
- package/dist/skills/changes.controller.d.ts +56 -0
- package/dist/skills/changes.controller.js +136 -0
- package/dist/skills/changes.service.d.ts +175 -0
- package/dist/skills/changes.service.js +805 -0
- package/dist/skills/invocable.controller.d.ts +23 -0
- package/dist/skills/invocable.controller.js +50 -0
- package/dist/skills/invocation-keys.d.ts +23 -0
- package/dist/skills/invocation-keys.js +44 -0
- package/dist/skills/invoke.d.ts +135 -0
- package/dist/skills/invoke.js +286 -0
- package/dist/skills/package.d.ts +87 -0
- package/dist/skills/package.js +227 -0
- package/dist/skills/skills.controller.d.ts +40 -0
- package/dist/skills/skills.controller.js +110 -0
- package/dist/skills/skills.service.d.ts +192 -0
- package/dist/skills/skills.service.js +567 -0
- package/dist/storage/prefix.d.ts +36 -0
- package/dist/storage/prefix.js +104 -0
- package/dist/storage/purge.d.ts +27 -0
- package/dist/storage/purge.js +75 -0
- package/dist/storage/storage.service.d.ts +44 -0
- package/dist/storage/storage.service.js +145 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +13 -0
- package/dist/tools/approvals.controller.d.ts +53 -0
- package/dist/tools/approvals.controller.js +128 -0
- package/dist/tools/approvals.service.d.ts +172 -0
- package/dist/tools/approvals.service.js +568 -0
- package/dist/tools/breaker.d.ts +28 -0
- package/dist/tools/breaker.js +72 -0
- package/dist/tools/builtins/agent.d.ts +81 -0
- package/dist/tools/builtins/agent.js +223 -0
- package/dist/tools/builtins/ask-user.d.ts +28 -0
- package/dist/tools/builtins/ask-user.js +139 -0
- package/dist/tools/builtins/read-tool-output.d.ts +18 -0
- package/dist/tools/builtins/read-tool-output.js +108 -0
- package/dist/tools/builtins/sandbox/bash.d.ts +16 -0
- package/dist/tools/builtins/sandbox/bash.js +137 -0
- package/dist/tools/builtins/sandbox/common.d.ts +116 -0
- package/dist/tools/builtins/sandbox/common.js +306 -0
- package/dist/tools/builtins/sandbox/deliver.d.ts +5 -0
- package/dist/tools/builtins/sandbox/deliver.js +180 -0
- package/dist/tools/builtins/sandbox/edit.d.ts +4 -0
- package/dist/tools/builtins/sandbox/edit.js +98 -0
- package/dist/tools/builtins/sandbox/glob-match.d.ts +25 -0
- package/dist/tools/builtins/sandbox/glob-match.js +240 -0
- package/dist/tools/builtins/sandbox/glob.d.ts +4 -0
- package/dist/tools/builtins/sandbox/glob.js +104 -0
- package/dist/tools/builtins/sandbox/grep.d.ts +9 -0
- package/dist/tools/builtins/sandbox/grep.js +133 -0
- package/dist/tools/builtins/sandbox/index.d.ts +12 -0
- package/dist/tools/builtins/sandbox/index.js +27 -0
- package/dist/tools/builtins/sandbox/names.d.ts +7 -0
- package/dist/tools/builtins/sandbox/names.js +20 -0
- package/dist/tools/builtins/sandbox/read.d.ts +8 -0
- package/dist/tools/builtins/sandbox/read.js +121 -0
- package/dist/tools/builtins/sandbox/write.d.ts +4 -0
- package/dist/tools/builtins/sandbox/write.js +88 -0
- package/dist/tools/builtins/skill-load.d.ts +53 -0
- package/dist/tools/builtins/skill-load.js +446 -0
- package/dist/tools/builtins/skill-write.d.ts +59 -0
- package/dist/tools/builtins/skill-write.js +244 -0
- package/dist/tools/builtins/tool-search.d.ts +31 -0
- package/dist/tools/builtins/tool-search.js +171 -0
- package/dist/tools/connector-content.controller.d.ts +34 -0
- package/dist/tools/connector-content.controller.js +78 -0
- package/dist/tools/connector-content.d.ts +104 -0
- package/dist/tools/connector-content.js +267 -0
- package/dist/tools/connectors.controller.d.ts +210 -0
- package/dist/tools/connectors.controller.js +234 -0
- package/dist/tools/connectors.service.d.ts +303 -0
- package/dist/tools/connectors.service.js +1102 -0
- package/dist/tools/credentials.d.ts +139 -0
- package/dist/tools/credentials.js +351 -0
- package/dist/tools/drift.d.ts +141 -0
- package/dist/tools/drift.js +539 -0
- package/dist/tools/elicitation.d.ts +81 -0
- package/dist/tools/elicitation.js +266 -0
- package/dist/tools/endpoint.d.ts +18 -0
- package/dist/tools/endpoint.js +115 -0
- package/dist/tools/info.d.ts +67 -0
- package/dist/tools/info.js +113 -0
- package/dist/tools/naming.d.ts +27 -0
- package/dist/tools/naming.js +47 -0
- package/dist/tools/oauth/discovery.d.ts +65 -0
- package/dist/tools/oauth/discovery.js +307 -0
- package/dist/tools/oauth/flow.d.ts +63 -0
- package/dist/tools/oauth/flow.js +237 -0
- package/dist/tools/oauth/oauth.controller.d.ts +31 -0
- package/dist/tools/oauth/oauth.controller.js +95 -0
- package/dist/tools/oauth/tokens.d.ts +111 -0
- package/dist/tools/oauth/tokens.js +258 -0
- package/dist/tools/outputs.d.ts +61 -0
- package/dist/tools/outputs.js +156 -0
- package/dist/tools/pipeline.d.ts +322 -0
- package/dist/tools/pipeline.js +1236 -0
- package/dist/tools/policy.d.ts +60 -0
- package/dist/tools/policy.js +236 -0
- package/dist/tools/questions.controller.d.ts +24 -0
- package/dist/tools/questions.controller.js +58 -0
- package/dist/tools/questions.service.d.ts +103 -0
- package/dist/tools/questions.service.js +295 -0
- package/dist/tools/redact.d.ts +20 -0
- package/dist/tools/redact.js +60 -0
- package/dist/tools/registry.d.ts +246 -0
- package/dist/tools/registry.js +481 -0
- package/dist/tools/resources.d.ts +16 -0
- package/dist/tools/resources.js +76 -0
- package/dist/tools/specs.d.ts +14 -0
- package/dist/tools/specs.js +31 -0
- package/dist/tools/validate.d.ts +20 -0
- package/dist/tools/validate.js +183 -0
- package/dist/tools/view.d.ts +43 -0
- package/dist/tools/view.js +225 -0
- package/dist/ui/embed-token.d.ts +31 -0
- package/dist/ui/embed-token.js +132 -0
- package/dist/ui/registry.d.ts +58 -0
- package/dist/ui/registry.js +90 -0
- package/dist/ui/ui-stream.d.ts +37 -0
- package/dist/ui/ui-stream.js +162 -0
- package/dist/ui/views.controller.d.ts +81 -0
- package/dist/ui/views.controller.js +261 -0
- package/dist/ui/views.d.ts +155 -0
- package/dist/ui/views.js +306 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +6 -0
- package/package.json +69 -3
- package/src/migrations/0001_init.sql +8 -0
- package/src/migrations/0002_secrets.sql +8 -0
- package/src/migrations/0003_identity.sql +117 -0
- package/src/migrations/0004_plugin_org_config_nullable.sql +3 -0
- package/src/migrations/0005_agents.sql +27 -0
- package/src/migrations/0006_conversations.sql +37 -0
- package/src/migrations/0007_checkpoints.sql +26 -0
- package/src/migrations/0008_runs_trigger_seq.sql +3 -0
- package/src/migrations/0009_runs_active_unique.sql +4 -0
- package/src/migrations/0010_tools.sql +125 -0
- package/src/migrations/0011_approvals_spec.sql +10 -0
- package/src/migrations/0012_run_waits.sql +24 -0
- package/src/migrations/0013_app_tools_source.sql +4 -0
- package/src/migrations/0014_tool_specs.sql +8 -0
- package/src/migrations/0015_tool_drift.sql +20 -0
- package/src/migrations/0016_plugin_permissions.sql +11 -0
- package/src/migrations/0017_identity_sources.sql +6 -0
- package/src/migrations/0018_channels.sql +74 -0
- package/src/migrations/0019_identity_trust.sql +17 -0
- package/src/migrations/0020_channel_card_lookup.sql +10 -0
- package/src/migrations/0021_org_storage_purge.sql +9 -0
- package/src/migrations/0022_attachments.sql +29 -0
- package/src/migrations/0023_sandboxes.sql +37 -0
- package/src/migrations/0024_skills.sql +46 -0
- package/src/migrations/0025_skills_catalog_idx.sql +6 -0
- package/src/migrations/0026_skill_changes.sql +34 -0
- package/src/migrations/0027_model_prices.sql +18 -0
- package/src/migrations/0028_evals.sql +105 -0
- package/src/migrations/0029_model_purposes.sql +18 -0
- package/src/migrations/0030_plugin_installs.sql +73 -0
- package/src/migrations/0031_plugin_snapshot_source.sql +6 -0
- package/src/migrations/0032_plugin_deployment_configs.sql +10 -0
- package/src/migrations/0033_rename_apps_to_connectors.sql +25 -0
- package/src/migrations/0034_directory.sql +49 -0
- package/src/migrations/0035_catalogs.sql +40 -0
- package/src/migrations/0036_app_import_staging.sql +17 -0
- package/src/migrations/0037_directory_installs.sql +92 -0
- package/src/migrations/0038_channel_inbound_mode.sql +6 -0
- package/src/migrations/0039_channel_card_pending.sql +8 -0
- package/src/migrations/0040_usage_report_sweep.sql +10 -0
- package/src/migrations/0041_rename_plugins_to_extensions.sql +38 -0
- package/src/migrations/0042_rename_directory_to_catalog.sql +80 -0
- package/src/migrations/0043_rename_app_installs_to_plugin_installs.sql +20 -0
- package/src/migrations/0044_catalog_one_kind.sql +90 -0
- package/src/migrations/0045_plugin_dependencies.sql +9 -0
- package/src/migrations/0046_git_sources.sql +25 -0
- package/src/migrations/0047_marketplace_entry_heads.sql +7 -0
- package/src/migrations/0048_marketplace_entry_trees.sql +8 -0
- package/src/migrations/0049_plugin_upgrades.sql +37 -0
- package/src/migrations/0050_bootstrap.sql +53 -0
- package/src/migrations/0051_claude_compat.sql +26 -0
- package/src/migrations/0052_conversations_account_agent_idx.sql +5 -0
- package/src/migrations/0053_subagents.sql +73 -0
- package/src/migrations/0054_subagent_wait_expiry.sql +7 -0
- package/src/migrations/0055_subagent_parent_indexes.sql +10 -0
- package/src/migrations/0056_approval_reason.sql +3 -0
- package/src/migrations/0057_conversation_titles.sql +11 -0
- package/src/migrations/0058_conversation_selection.sql +4 -0
- package/src/migrations/0059_conversation_queue.sql +22 -0
- package/src/migrations/0060_file_previewers.sql +10 -0
- package/src/migrations/0061_queue_selection.sql +5 -0
- package/src/migrations/0062_queued_skill.sql +8 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { Logger } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import type { Redis } from "ioredis";
|
|
3
|
+
import { ApiError } from "../errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shared and exclusive sandbox leases (sub-agents spec §10.3): the Redis protocol that lets several
|
|
6
|
+
* holders use one conversation's running sandbox concurrently (shared leases: commands, file reads
|
|
7
|
+
* and writes) while lifecycle changes (create, resume, rebuild, suspend, destroy) stay alone
|
|
8
|
+
* (exclusive leases). Keys, conversation = the sandbox conversation:
|
|
9
|
+
*
|
|
10
|
+
* - `capk:sandbox:<conv>`: the lock key kept from before (rollout, §10.8): an exclusive holder's
|
|
11
|
+
* token, or `shared` while shared leases exist. An older replica's `SET NX` therefore never gets
|
|
12
|
+
* in while shared leases live, and its token blocks new shared leases.
|
|
13
|
+
* - `capk:sandbox-users:<conv>`: sorted set of shared lease tokens scored by their expiry (ms, Redis
|
|
14
|
+
* server clock).
|
|
15
|
+
* - `capk:sandbox-intent:<conv>`: an exclusive waiter's token (writer preference); while it exists
|
|
16
|
+
* no new shared lease is granted.
|
|
17
|
+
* - `capk:sandbox-holders:<conv>`: hash token → expiry of holders that may have commands running
|
|
18
|
+
* (the per-holder replacement of the single busy marker `capk:sandbox-busy:<conv>`, still read).
|
|
19
|
+
* - `capk:sandbox-path:<conv>:<sha256hex(path)>`: a short per-path lease around the kernel's own
|
|
20
|
+
* read-modify-write of one file.
|
|
21
|
+
*
|
|
22
|
+
* Every script purges expired shared leases first, and every key carries a TTL. A holder renews
|
|
23
|
+
* every TTL / 3 (a timer for a lease this process holds) and treats "no successful renewal for
|
|
24
|
+
* 2/3 TTL" as lost: its `signal` aborts with {@link SandboxLockLostError}.
|
|
25
|
+
*/
|
|
26
|
+
/** Prefix of the lock key `capk:sandbox:<conversationId>`. */
|
|
27
|
+
export declare const SANDBOX_LOCK_PREFIX = "capk:sandbox:";
|
|
28
|
+
/** TTL of a lease (ms): renewed every TTL / 3 while held. */
|
|
29
|
+
export declare const SANDBOX_LOCK_TTL_MS = 60000;
|
|
30
|
+
/**
|
|
31
|
+
* Prefix of the single "sandbox in use" marker of kernels before phase C,
|
|
32
|
+
* `capk:sandbox-busy:<conversationId>`: present while such a holder ran commands. Found by an
|
|
33
|
+
* exclusive acquirer, it means that holder crashed and its commands may still run.
|
|
34
|
+
*
|
|
35
|
+
* Holders of this kernel write it too, with the value {@link BUSY_V2}, while any holder record
|
|
36
|
+
* exists (spec §10.8): this kernel ignores that value, and a replica from before phase C that
|
|
37
|
+
* takes the lock after a new holder crashed sees a marker and kills everything, which is safe
|
|
38
|
+
* because it only gets the lock when no shared lease is live.
|
|
39
|
+
*/
|
|
40
|
+
export declare const SANDBOX_BUSY_PREFIX = "capk:sandbox-busy:";
|
|
41
|
+
/** Value of the busy marker written by holders of this kernel (ignored by it). */
|
|
42
|
+
export declare const BUSY_V2 = "v2";
|
|
43
|
+
/** The sandbox of this conversation is busy: waited longer than `sandbox.lockWaitSeconds` (409). */
|
|
44
|
+
export declare const sandboxBusy: () => ApiError;
|
|
45
|
+
/** A lease was lost while held, or the record changed under it (generation mismatch). */
|
|
46
|
+
export declare class SandboxLockLostError extends Error {
|
|
47
|
+
/** `lock`: the lease was lost (renewal failed or none succeeded in time); `generation`: the record's fence changed. */
|
|
48
|
+
readonly reason: "lock" | "generation";
|
|
49
|
+
constructor(conversationId: string, reason?: "lock" | "generation");
|
|
50
|
+
}
|
|
51
|
+
/** Another holder kept writing the same file longer than the path lease wait (model-visible text). */
|
|
52
|
+
export declare class SandboxPathBusyError extends Error {
|
|
53
|
+
constructor();
|
|
54
|
+
}
|
|
55
|
+
export type LeaseKind = "shared" | "exclusive";
|
|
56
|
+
/** A lease this process holds. */
|
|
57
|
+
export interface SandboxLease {
|
|
58
|
+
readonly kind: LeaseKind;
|
|
59
|
+
readonly token: string;
|
|
60
|
+
readonly conversationId: string;
|
|
61
|
+
/** Aborts with {@link SandboxLockLostError} when the lease is lost. */
|
|
62
|
+
readonly signal: AbortSignal;
|
|
63
|
+
/** Renews once now; false when the lease is lost (the signal then aborts). */
|
|
64
|
+
renew(): Promise<boolean>;
|
|
65
|
+
/** Stops renewing and releases the lease (only this holder's). */
|
|
66
|
+
release(): Promise<void>;
|
|
67
|
+
/** exclusive → shared in one Lua step (same token, same signal); throws if not exclusive. */
|
|
68
|
+
downgrade(): Promise<SandboxLease>;
|
|
69
|
+
}
|
|
70
|
+
export interface SandboxLeasesDeps {
|
|
71
|
+
redis: Redis;
|
|
72
|
+
log: Pick<Logger, "warn">;
|
|
73
|
+
/** Lease TTL (ms), default {@link SANDBOX_LOCK_TTL_MS}. */
|
|
74
|
+
ttlMs?: number;
|
|
75
|
+
/** Poll interval while waiting (ms, jittered up to 2×), default 100. */
|
|
76
|
+
pollMs?: number;
|
|
77
|
+
/** Path lease TTL (ms), default 30 s; tests make it shorter. */
|
|
78
|
+
pathTtlMs?: number;
|
|
79
|
+
}
|
|
80
|
+
/** A path lease this process holds. */
|
|
81
|
+
export interface SandboxPathLease {
|
|
82
|
+
/** Aborts with {@link SandboxLockLostError} when a renewal finds the lease gone. */
|
|
83
|
+
readonly signal: AbortSignal;
|
|
84
|
+
release(): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
/** The lease protocol; no state in process beyond the leases this process holds. */
|
|
87
|
+
export declare class SandboxLeases {
|
|
88
|
+
private readonly redis;
|
|
89
|
+
private readonly log;
|
|
90
|
+
private readonly ttlMs;
|
|
91
|
+
private readonly pollMs;
|
|
92
|
+
private readonly pathTtlMs;
|
|
93
|
+
constructor(d: SandboxLeasesDeps);
|
|
94
|
+
/**
|
|
95
|
+
* TTL of an intent (ms): 4 × the poll interval (a jittered wait is up to 2 × the interval plus
|
|
96
|
+
* round trips), at least {@link INTENT_TTL_FLOOR_MS} so event-loop lag cannot let it lapse.
|
|
97
|
+
*/
|
|
98
|
+
private get intentTtlMs();
|
|
99
|
+
private keys;
|
|
100
|
+
private wait;
|
|
101
|
+
/**
|
|
102
|
+
* A shared lease (`cap` holders at most), polling until one is free; `sandbox.busy` after
|
|
103
|
+
* `capMs`, the signal's reason when it aborts. Not granted while an exclusive lease, an older
|
|
104
|
+
* replica's lock token or another caller's intent exists.
|
|
105
|
+
*/
|
|
106
|
+
acquireShared(conv: string, cap: number, signal: AbortSignal, capMs: number): Promise<SandboxLease>;
|
|
107
|
+
/**
|
|
108
|
+
* An exclusive lease, polling until no other lease exists; `sandbox.busy` after `capMs` (no cap
|
|
109
|
+
* when omitted), the signal's reason when it aborts. With `intent` the waiter sets the intent
|
|
110
|
+
* (refreshed while it waits) so new shared leases stop being granted; it is cleared on success
|
|
111
|
+
* and when the wait ends otherwise.
|
|
112
|
+
*/
|
|
113
|
+
acquireExclusive(conv: string, signal: AbortSignal, capMs?: number, o?: {
|
|
114
|
+
intent?: boolean;
|
|
115
|
+
}): Promise<SandboxLease>;
|
|
116
|
+
/**
|
|
117
|
+
* One try at an exclusive lease; null when any lease exists. Without `o` (sweeps) it sets no
|
|
118
|
+
* intent. With `o.token` the lease (and, when `o.intent`, the intent set on failure) carries that
|
|
119
|
+
* token, so one waiter can alternate between this and {@link tryShared} with the same token.
|
|
120
|
+
*/
|
|
121
|
+
tryExclusive(conv: string, o?: {
|
|
122
|
+
token?: string;
|
|
123
|
+
intent?: boolean;
|
|
124
|
+
}): Promise<SandboxLease | null>;
|
|
125
|
+
/**
|
|
126
|
+
* One try at a shared lease (`cap` holders at most); null when it is not granted. `token` is the
|
|
127
|
+
* lease token; a waiter that set an intent with the same token is let through its own intent
|
|
128
|
+
* (it should then {@link clearIntent}).
|
|
129
|
+
*/
|
|
130
|
+
tryShared(conv: string, cap: number, token?: string): Promise<SandboxLease | null>;
|
|
131
|
+
/** Deletes the conversation's intent when it is `token`'s. */
|
|
132
|
+
clearIntent(conv: string, token: string): Promise<void>;
|
|
133
|
+
/** Whether the single busy marker of a kernel before phase C is present (v2 does not count). */
|
|
134
|
+
legacyMarker(conv: string): Promise<boolean>;
|
|
135
|
+
/** Live shared leases of the conversation. */
|
|
136
|
+
sharedCount(conv: string): Promise<number>;
|
|
137
|
+
/**
|
|
138
|
+
* Records (or refreshes) a holder that may run commands (before its provider calls); `ttlMs`
|
|
139
|
+
* bounds the record. Also keeps the v2 busy marker for older replicas ({@link BUSY_V2}).
|
|
140
|
+
*/
|
|
141
|
+
registerHolder(conv: string, token: string, ttlMs: number): Promise<void>;
|
|
142
|
+
/** Deletes a holder record (after its provider calls); the v2 marker goes with the last one. */
|
|
143
|
+
unregisterHolder(conv: string, token: string): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Tags of holders without a live lease, plus whether an older kernel's single busy marker was
|
|
146
|
+
* present. Atomically claims them (removes the fields) so only one caller kills. That marker is
|
|
147
|
+
* claimed only when `exclusiveToken` is the caller's exclusive lease (no shared lease live).
|
|
148
|
+
*/
|
|
149
|
+
claimDeadHolders(conv: string, exclusiveToken?: string): Promise<{
|
|
150
|
+
tags: string[];
|
|
151
|
+
legacyMarker: boolean;
|
|
152
|
+
}>;
|
|
153
|
+
/**
|
|
154
|
+
* A short lease on one path of the sandbox (the kernel's own read-modify-write of a file); waits
|
|
155
|
+
* up to `capMs`, then throws {@link SandboxPathBusyError}. Renewed while held; a renewal that
|
|
156
|
+
* finds it gone warns and aborts its `signal`.
|
|
157
|
+
*/
|
|
158
|
+
pathLease(conv: string, path: string, signal: AbortSignal, capMs: number): Promise<SandboxPathLease>;
|
|
159
|
+
/**
|
|
160
|
+
* Wraps a just-acquired lease: renews every TTL / 3 and remembers the last successful renewal;
|
|
161
|
+
* the lease is lost when a renewal reports it gone or none succeeded for 2/3 TTL (leaving margin
|
|
162
|
+
* so the abort takes effect before the key expires).
|
|
163
|
+
*/
|
|
164
|
+
private hold;
|
|
165
|
+
private lose;
|
|
166
|
+
private renew;
|
|
167
|
+
/** A lease view of `s` as `kind`; a view stops working once the lease changes kind. */
|
|
168
|
+
private view;
|
|
169
|
+
}
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { ApiError } from "../errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shared and exclusive sandbox leases (sub-agents spec §10.3): the Redis protocol that lets several
|
|
5
|
+
* holders use one conversation's running sandbox concurrently (shared leases: commands, file reads
|
|
6
|
+
* and writes) while lifecycle changes (create, resume, rebuild, suspend, destroy) stay alone
|
|
7
|
+
* (exclusive leases). Keys, conversation = the sandbox conversation:
|
|
8
|
+
*
|
|
9
|
+
* - `capk:sandbox:<conv>`: the lock key kept from before (rollout, §10.8): an exclusive holder's
|
|
10
|
+
* token, or `shared` while shared leases exist. An older replica's `SET NX` therefore never gets
|
|
11
|
+
* in while shared leases live, and its token blocks new shared leases.
|
|
12
|
+
* - `capk:sandbox-users:<conv>`: sorted set of shared lease tokens scored by their expiry (ms, Redis
|
|
13
|
+
* server clock).
|
|
14
|
+
* - `capk:sandbox-intent:<conv>`: an exclusive waiter's token (writer preference); while it exists
|
|
15
|
+
* no new shared lease is granted.
|
|
16
|
+
* - `capk:sandbox-holders:<conv>`: hash token → expiry of holders that may have commands running
|
|
17
|
+
* (the per-holder replacement of the single busy marker `capk:sandbox-busy:<conv>`, still read).
|
|
18
|
+
* - `capk:sandbox-path:<conv>:<sha256hex(path)>`: a short per-path lease around the kernel's own
|
|
19
|
+
* read-modify-write of one file.
|
|
20
|
+
*
|
|
21
|
+
* Every script purges expired shared leases first, and every key carries a TTL. A holder renews
|
|
22
|
+
* every TTL / 3 (a timer for a lease this process holds) and treats "no successful renewal for
|
|
23
|
+
* 2/3 TTL" as lost: its `signal` aborts with {@link SandboxLockLostError}.
|
|
24
|
+
*/
|
|
25
|
+
/** Prefix of the lock key `capk:sandbox:<conversationId>`. */
|
|
26
|
+
export const SANDBOX_LOCK_PREFIX = "capk:sandbox:";
|
|
27
|
+
/** TTL of a lease (ms): renewed every TTL / 3 while held. */
|
|
28
|
+
export const SANDBOX_LOCK_TTL_MS = 60_000;
|
|
29
|
+
/**
|
|
30
|
+
* Prefix of the single "sandbox in use" marker of kernels before phase C,
|
|
31
|
+
* `capk:sandbox-busy:<conversationId>`: present while such a holder ran commands. Found by an
|
|
32
|
+
* exclusive acquirer, it means that holder crashed and its commands may still run.
|
|
33
|
+
*
|
|
34
|
+
* Holders of this kernel write it too, with the value {@link BUSY_V2}, while any holder record
|
|
35
|
+
* exists (spec §10.8): this kernel ignores that value, and a replica from before phase C that
|
|
36
|
+
* takes the lock after a new holder crashed sees a marker and kills everything, which is safe
|
|
37
|
+
* because it only gets the lock when no shared lease is live.
|
|
38
|
+
*/
|
|
39
|
+
export const SANDBOX_BUSY_PREFIX = "capk:sandbox-busy:";
|
|
40
|
+
/** Value of the busy marker written by holders of this kernel (ignored by it). */
|
|
41
|
+
export const BUSY_V2 = "v2";
|
|
42
|
+
const USERS_PREFIX = "capk:sandbox-users:";
|
|
43
|
+
const INTENT_PREFIX = "capk:sandbox-intent:";
|
|
44
|
+
const HOLDERS_PREFIX = "capk:sandbox-holders:";
|
|
45
|
+
const PATH_PREFIX = "capk:sandbox-path:";
|
|
46
|
+
/** TTL of a path lease (ms); renewed while held. */
|
|
47
|
+
const PATH_LEASE_TTL_MS = 30_000;
|
|
48
|
+
/** Shortest life of an exclusive waiter's intent (ms): outlasts event-loop lag between polls. */
|
|
49
|
+
const INTENT_TTL_FLOOR_MS = 2000;
|
|
50
|
+
/** The sandbox of this conversation is busy: waited longer than `sandbox.lockWaitSeconds` (409). */
|
|
51
|
+
export const sandboxBusy = () => new ApiError("sandbox.busy", "The sandbox for this conversation is busy with another command; wait for it to finish and try again");
|
|
52
|
+
/** A lease was lost while held, or the record changed under it (generation mismatch). */
|
|
53
|
+
export class SandboxLockLostError extends Error {
|
|
54
|
+
/** `lock`: the lease was lost (renewal failed or none succeeded in time); `generation`: the record's fence changed. */
|
|
55
|
+
reason;
|
|
56
|
+
constructor(conversationId, reason = "lock") {
|
|
57
|
+
super(reason === "lock"
|
|
58
|
+
? `sandbox lock lost for conversation ${conversationId}`
|
|
59
|
+
: `sandbox state of conversation ${conversationId} changed concurrently (generation mismatch)`);
|
|
60
|
+
this.name = "SandboxLockLostError";
|
|
61
|
+
this.reason = reason;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Another holder kept writing the same file longer than the path lease wait (model-visible text). */
|
|
65
|
+
export class SandboxPathBusyError extends Error {
|
|
66
|
+
constructor() {
|
|
67
|
+
super("another sub-agent is writing this file; try again");
|
|
68
|
+
this.name = "SandboxPathBusyError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const NOW = `local t = redis.call('TIME'); local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)`;
|
|
72
|
+
/** Drops expired shared leases; drops a `shared` lock value no live shared lease backs. */
|
|
73
|
+
const PURGE = `
|
|
74
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now)
|
|
75
|
+
local cur = redis.call('GET', KEYS[1])
|
|
76
|
+
if cur == 'shared' and redis.call('ZCARD', KEYS[2]) == 0 then
|
|
77
|
+
redis.call('DEL', KEYS[1])
|
|
78
|
+
cur = false
|
|
79
|
+
end`;
|
|
80
|
+
/**
|
|
81
|
+
* Keeps the users set and the `shared` lock value alive for at least `ttl` without shortening a
|
|
82
|
+
* longer TTL another holder set (holders may use different TTLs).
|
|
83
|
+
*/
|
|
84
|
+
const KEEP_SHARED = `
|
|
85
|
+
if redis.call('PTTL', KEYS[2]) < ttl then redis.call('PEXPIRE', KEYS[2], ttl) end
|
|
86
|
+
local lt = redis.call('PTTL', KEYS[1])
|
|
87
|
+
if redis.call('GET', KEYS[1]) ~= 'shared' or lt < ttl then
|
|
88
|
+
redis.call('SET', KEYS[1], 'shared', 'PX', math.max(ttl, lt))
|
|
89
|
+
end`;
|
|
90
|
+
/** KEYS lock, users, intent; ARGV token, cap, ttl. */
|
|
91
|
+
const ACQUIRE_SHARED = `${NOW}
|
|
92
|
+
${PURGE}
|
|
93
|
+
if cur and cur ~= 'shared' then return 0 end
|
|
94
|
+
local it = redis.call('GET', KEYS[3])
|
|
95
|
+
if it and it ~= ARGV[1] then return 0 end
|
|
96
|
+
if redis.call('ZCARD', KEYS[2]) >= tonumber(ARGV[2]) then return 0 end
|
|
97
|
+
local ttl = tonumber(ARGV[3])
|
|
98
|
+
redis.call('ZADD', KEYS[2], now + ttl, ARGV[1])
|
|
99
|
+
${KEEP_SHARED}
|
|
100
|
+
return 1`;
|
|
101
|
+
/** KEYS lock, users, intent; ARGV token, ttl, setIntent (0/1), intentTtl. */
|
|
102
|
+
const ACQUIRE_EXCLUSIVE = `${NOW}
|
|
103
|
+
${PURGE}
|
|
104
|
+
if cur or redis.call('ZCARD', KEYS[2]) > 0 then
|
|
105
|
+
if ARGV[3] == '1' then
|
|
106
|
+
local it = redis.call('GET', KEYS[3])
|
|
107
|
+
if (not it) or it == ARGV[1] then
|
|
108
|
+
redis.call('SET', KEYS[3], ARGV[1], 'PX', tonumber(ARGV[4]))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
return 0
|
|
112
|
+
end
|
|
113
|
+
redis.call('SET', KEYS[1], ARGV[1], 'PX', tonumber(ARGV[2]))
|
|
114
|
+
if redis.call('GET', KEYS[3]) == ARGV[1] then redis.call('DEL', KEYS[3]) end
|
|
115
|
+
return 1`;
|
|
116
|
+
/** KEYS lock, users; ARGV token, ttl, kind. Shared: lost when the member is gone or the lock holds a token. */
|
|
117
|
+
const RENEW = `${NOW}
|
|
118
|
+
local ttl = tonumber(ARGV[2])
|
|
119
|
+
if ARGV[3] == 'exclusive' then
|
|
120
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
121
|
+
redis.call('PEXPIRE', KEYS[1], ttl)
|
|
122
|
+
return 1
|
|
123
|
+
end
|
|
124
|
+
return 0
|
|
125
|
+
end
|
|
126
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now)
|
|
127
|
+
if not redis.call('ZSCORE', KEYS[2], ARGV[1]) then return 0 end
|
|
128
|
+
local cur = redis.call('GET', KEYS[1])
|
|
129
|
+
if cur and cur ~= 'shared' then
|
|
130
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
131
|
+
return 0
|
|
132
|
+
end
|
|
133
|
+
redis.call('ZADD', KEYS[2], 'XX', now + ttl, ARGV[1])
|
|
134
|
+
${KEEP_SHARED}
|
|
135
|
+
return 1`;
|
|
136
|
+
/** KEYS lock, users; ARGV token, kind. */
|
|
137
|
+
const RELEASE = `${NOW}
|
|
138
|
+
if ARGV[2] == 'exclusive' then
|
|
139
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) end
|
|
140
|
+
return 1
|
|
141
|
+
end
|
|
142
|
+
redis.call('ZREM', KEYS[2], ARGV[1])
|
|
143
|
+
${PURGE}
|
|
144
|
+
return 1`;
|
|
145
|
+
/** KEYS lock, users; ARGV token, ttl. */
|
|
146
|
+
const DOWNGRADE = `${NOW}
|
|
147
|
+
if redis.call('GET', KEYS[1]) ~= ARGV[1] then return 0 end
|
|
148
|
+
local ttl = tonumber(ARGV[2])
|
|
149
|
+
redis.call('ZADD', KEYS[2], now + ttl, ARGV[1])
|
|
150
|
+
${KEEP_SHARED}
|
|
151
|
+
return 1`;
|
|
152
|
+
/** KEYS lock, users; returns the live shared count. */
|
|
153
|
+
const COUNT = `${NOW}
|
|
154
|
+
${PURGE}
|
|
155
|
+
return redis.call('ZCARD', KEYS[2])`;
|
|
156
|
+
/**
|
|
157
|
+
* KEYS holders, busy; ARGV token, ttl. Also writes the busy marker as v2 (kept for at least `ttl`)
|
|
158
|
+
* unless an older kernel's holder owns it.
|
|
159
|
+
*/
|
|
160
|
+
const REGISTER = `${NOW}
|
|
161
|
+
local ttl = tonumber(ARGV[2])
|
|
162
|
+
redis.call('HSET', KEYS[1], ARGV[1], now + ttl)
|
|
163
|
+
if redis.call('PTTL', KEYS[1]) < ttl then redis.call('PEXPIRE', KEYS[1], ttl) end
|
|
164
|
+
local b = redis.call('GET', KEYS[2])
|
|
165
|
+
if (not b) or (b == '${BUSY_V2}' and redis.call('PTTL', KEYS[2]) < ttl) then
|
|
166
|
+
redis.call('SET', KEYS[2], '${BUSY_V2}', 'PX', ttl)
|
|
167
|
+
end
|
|
168
|
+
return 1`;
|
|
169
|
+
/** Deletes the v2 busy marker (key `busy`) once no holder record (hash `holders`) is left. */
|
|
170
|
+
const dropV2 = (holders, busy) => `
|
|
171
|
+
if redis.call('HLEN', ${holders}) == 0 and redis.call('GET', ${busy}) == '${BUSY_V2}' then
|
|
172
|
+
redis.call('DEL', ${busy})
|
|
173
|
+
end`;
|
|
174
|
+
/** KEYS holders, busy; ARGV token. */
|
|
175
|
+
const UNREGISTER = `
|
|
176
|
+
redis.call('HDEL', KEYS[1], ARGV[1])
|
|
177
|
+
${dropV2("KEYS[1]", "KEYS[2]")}
|
|
178
|
+
return 1`;
|
|
179
|
+
/**
|
|
180
|
+
* KEYS lock, users, holders, busy; ARGV the caller's exclusive token (or ''). Removes and returns
|
|
181
|
+
* the holder fields without a live lease (expired fields are dropped without being returned: the
|
|
182
|
+
* in-sandbox time limit ended their commands); claims an older kernel's busy marker only for the
|
|
183
|
+
* caller holding the exclusive lease (`cur == token`, so no shared lease is live and killing
|
|
184
|
+
* everything cannot hit a live sibling). The v2 marker is never claimed; it goes with the last
|
|
185
|
+
* record. Returns { legacy, tag… }.
|
|
186
|
+
*/
|
|
187
|
+
const CLAIM = `${NOW}
|
|
188
|
+
${PURGE}
|
|
189
|
+
local out = {0}
|
|
190
|
+
local f = redis.call('HGETALL', KEYS[3])
|
|
191
|
+
for i = 1, #f, 2 do
|
|
192
|
+
local tok = f[i]
|
|
193
|
+
if tonumber(f[i + 1]) <= now then
|
|
194
|
+
redis.call('HDEL', KEYS[3], tok)
|
|
195
|
+
elseif tok ~= cur and not redis.call('ZSCORE', KEYS[2], tok) then
|
|
196
|
+
redis.call('HDEL', KEYS[3], tok)
|
|
197
|
+
table.insert(out, tok)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
${dropV2("KEYS[3]", "KEYS[4]")}
|
|
201
|
+
local b = redis.call('GET', KEYS[4])
|
|
202
|
+
if ARGV[1] ~= '' and cur == ARGV[1] and b and b ~= '${BUSY_V2}' then
|
|
203
|
+
redis.call('DEL', KEYS[4])
|
|
204
|
+
out[1] = 1
|
|
205
|
+
end
|
|
206
|
+
return out`;
|
|
207
|
+
/** Compare-and-delete / compare-and-expire of a single-valued key. */
|
|
208
|
+
const DEL_IF = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0`;
|
|
209
|
+
const EXPIRE_IF = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0`;
|
|
210
|
+
const abortReason = (signal) => signal.reason ?? new DOMException("This operation was aborted", "AbortError");
|
|
211
|
+
function sleep(ms, signal) {
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
if (signal.aborted) {
|
|
214
|
+
reject(abortReason(signal));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const onAbort = () => {
|
|
218
|
+
clearTimeout(t);
|
|
219
|
+
reject(abortReason(signal));
|
|
220
|
+
};
|
|
221
|
+
const t = setTimeout(() => {
|
|
222
|
+
signal.removeEventListener("abort", onAbort);
|
|
223
|
+
resolve();
|
|
224
|
+
}, ms);
|
|
225
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** The lease protocol; no state in process beyond the leases this process holds. */
|
|
229
|
+
export class SandboxLeases {
|
|
230
|
+
redis;
|
|
231
|
+
log;
|
|
232
|
+
ttlMs;
|
|
233
|
+
pollMs;
|
|
234
|
+
pathTtlMs;
|
|
235
|
+
constructor(d) {
|
|
236
|
+
this.redis = d.redis;
|
|
237
|
+
this.log = d.log;
|
|
238
|
+
this.ttlMs = d.ttlMs ?? SANDBOX_LOCK_TTL_MS;
|
|
239
|
+
this.pollMs = d.pollMs ?? 100;
|
|
240
|
+
this.pathTtlMs = d.pathTtlMs ?? PATH_LEASE_TTL_MS;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* TTL of an intent (ms): 4 × the poll interval (a jittered wait is up to 2 × the interval plus
|
|
244
|
+
* round trips), at least {@link INTENT_TTL_FLOOR_MS} so event-loop lag cannot let it lapse.
|
|
245
|
+
*/
|
|
246
|
+
get intentTtlMs() {
|
|
247
|
+
return Math.max(this.pollMs * 4, INTENT_TTL_FLOOR_MS);
|
|
248
|
+
}
|
|
249
|
+
keys(conv) {
|
|
250
|
+
return [`${SANDBOX_LOCK_PREFIX}${conv}`, `${USERS_PREFIX}${conv}`, `${INTENT_PREFIX}${conv}`];
|
|
251
|
+
}
|
|
252
|
+
wait(signal) {
|
|
253
|
+
return sleep(this.pollMs + Math.floor(Math.random() * this.pollMs), signal);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A shared lease (`cap` holders at most), polling until one is free; `sandbox.busy` after
|
|
257
|
+
* `capMs`, the signal's reason when it aborts. Not granted while an exclusive lease, an older
|
|
258
|
+
* replica's lock token or another caller's intent exists.
|
|
259
|
+
*/
|
|
260
|
+
async acquireShared(conv, cap, signal, capMs) {
|
|
261
|
+
const token = randomUUID();
|
|
262
|
+
const deadline = Date.now() + capMs;
|
|
263
|
+
for (;;) {
|
|
264
|
+
if (signal.aborted)
|
|
265
|
+
throw abortReason(signal);
|
|
266
|
+
const ok = await this.redis.eval(ACQUIRE_SHARED, 3, ...this.keys(conv), token, cap, this.ttlMs);
|
|
267
|
+
if (Number(ok) === 1)
|
|
268
|
+
return this.hold(conv, token, "shared");
|
|
269
|
+
if (Date.now() + this.pollMs > deadline)
|
|
270
|
+
throw sandboxBusy();
|
|
271
|
+
await this.wait(signal);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* An exclusive lease, polling until no other lease exists; `sandbox.busy` after `capMs` (no cap
|
|
276
|
+
* when omitted), the signal's reason when it aborts. With `intent` the waiter sets the intent
|
|
277
|
+
* (refreshed while it waits) so new shared leases stop being granted; it is cleared on success
|
|
278
|
+
* and when the wait ends otherwise.
|
|
279
|
+
*/
|
|
280
|
+
async acquireExclusive(conv, signal, capMs, o = {}) {
|
|
281
|
+
const token = randomUUID();
|
|
282
|
+
const deadline = capMs === undefined ? Number.POSITIVE_INFINITY : Date.now() + capMs;
|
|
283
|
+
const keys = this.keys(conv);
|
|
284
|
+
try {
|
|
285
|
+
for (;;) {
|
|
286
|
+
if (signal.aborted)
|
|
287
|
+
throw abortReason(signal);
|
|
288
|
+
const ok = await this.redis.eval(ACQUIRE_EXCLUSIVE, 3, ...keys, token, this.ttlMs, o.intent ? 1 : 0, this.intentTtlMs);
|
|
289
|
+
if (Number(ok) === 1)
|
|
290
|
+
return this.hold(conv, token, "exclusive");
|
|
291
|
+
if (Date.now() + this.pollMs > deadline)
|
|
292
|
+
throw sandboxBusy();
|
|
293
|
+
await this.wait(signal);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
if (o.intent) {
|
|
298
|
+
await this.redis
|
|
299
|
+
.eval(DEL_IF, 1, keys[2], token)
|
|
300
|
+
.catch((err) => this.log.warn("cannot clear sandbox intent", { conversationId: conv, err }));
|
|
301
|
+
}
|
|
302
|
+
throw e;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* One try at an exclusive lease; null when any lease exists. Without `o` (sweeps) it sets no
|
|
307
|
+
* intent. With `o.token` the lease (and, when `o.intent`, the intent set on failure) carries that
|
|
308
|
+
* token, so one waiter can alternate between this and {@link tryShared} with the same token.
|
|
309
|
+
*/
|
|
310
|
+
async tryExclusive(conv, o = {}) {
|
|
311
|
+
const token = o.token ?? randomUUID();
|
|
312
|
+
const ok = await this.redis.eval(ACQUIRE_EXCLUSIVE, 3, ...this.keys(conv), token, this.ttlMs, o.intent ? 1 : 0, this.intentTtlMs);
|
|
313
|
+
return Number(ok) === 1 ? this.hold(conv, token, "exclusive") : null;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* One try at a shared lease (`cap` holders at most); null when it is not granted. `token` is the
|
|
317
|
+
* lease token; a waiter that set an intent with the same token is let through its own intent
|
|
318
|
+
* (it should then {@link clearIntent}).
|
|
319
|
+
*/
|
|
320
|
+
async tryShared(conv, cap, token = randomUUID()) {
|
|
321
|
+
const ok = await this.redis.eval(ACQUIRE_SHARED, 3, ...this.keys(conv), token, cap, this.ttlMs);
|
|
322
|
+
return Number(ok) === 1 ? this.hold(conv, token, "shared") : null;
|
|
323
|
+
}
|
|
324
|
+
/** Deletes the conversation's intent when it is `token`'s. */
|
|
325
|
+
async clearIntent(conv, token) {
|
|
326
|
+
await this.redis.eval(DEL_IF, 1, this.keys(conv)[2], token);
|
|
327
|
+
}
|
|
328
|
+
/** Whether the single busy marker of a kernel before phase C is present (v2 does not count). */
|
|
329
|
+
async legacyMarker(conv) {
|
|
330
|
+
const v = await this.redis.get(`${SANDBOX_BUSY_PREFIX}${conv}`);
|
|
331
|
+
return v !== null && v !== BUSY_V2;
|
|
332
|
+
}
|
|
333
|
+
/** Live shared leases of the conversation. */
|
|
334
|
+
async sharedCount(conv) {
|
|
335
|
+
const [lock, users] = this.keys(conv);
|
|
336
|
+
return Number(await this.redis.eval(COUNT, 2, lock, users));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Records (or refreshes) a holder that may run commands (before its provider calls); `ttlMs`
|
|
340
|
+
* bounds the record. Also keeps the v2 busy marker for older replicas ({@link BUSY_V2}).
|
|
341
|
+
*/
|
|
342
|
+
async registerHolder(conv, token, ttlMs) {
|
|
343
|
+
await this.redis.eval(REGISTER, 2, `${HOLDERS_PREFIX}${conv}`, `${SANDBOX_BUSY_PREFIX}${conv}`, token, ttlMs);
|
|
344
|
+
}
|
|
345
|
+
/** Deletes a holder record (after its provider calls); the v2 marker goes with the last one. */
|
|
346
|
+
async unregisterHolder(conv, token) {
|
|
347
|
+
await this.redis.eval(UNREGISTER, 2, `${HOLDERS_PREFIX}${conv}`, `${SANDBOX_BUSY_PREFIX}${conv}`, token);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Tags of holders without a live lease, plus whether an older kernel's single busy marker was
|
|
351
|
+
* present. Atomically claims them (removes the fields) so only one caller kills. That marker is
|
|
352
|
+
* claimed only when `exclusiveToken` is the caller's exclusive lease (no shared lease live).
|
|
353
|
+
*/
|
|
354
|
+
async claimDeadHolders(conv, exclusiveToken) {
|
|
355
|
+
const [lock, users] = this.keys(conv);
|
|
356
|
+
const r = (await this.redis.eval(CLAIM, 4, lock, users, `${HOLDERS_PREFIX}${conv}`, `${SANDBOX_BUSY_PREFIX}${conv}`, exclusiveToken ?? ""));
|
|
357
|
+
const [legacy, ...tags] = r;
|
|
358
|
+
return { tags, legacyMarker: Number(legacy) === 1 };
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* A short lease on one path of the sandbox (the kernel's own read-modify-write of a file); waits
|
|
362
|
+
* up to `capMs`, then throws {@link SandboxPathBusyError}. Renewed while held; a renewal that
|
|
363
|
+
* finds it gone warns and aborts its `signal`.
|
|
364
|
+
*/
|
|
365
|
+
async pathLease(conv, path, signal, capMs) {
|
|
366
|
+
const key = `${PATH_PREFIX}${conv}:${createHash("sha256").update(path).digest("hex")}`;
|
|
367
|
+
const token = randomUUID();
|
|
368
|
+
const ttl = this.pathTtlMs;
|
|
369
|
+
const deadline = Date.now() + capMs;
|
|
370
|
+
for (;;) {
|
|
371
|
+
if (signal.aborted)
|
|
372
|
+
throw abortReason(signal);
|
|
373
|
+
if ((await this.redis.set(key, token, "PX", ttl, "NX")) === "OK")
|
|
374
|
+
break;
|
|
375
|
+
if (Date.now() + this.pollMs > deadline)
|
|
376
|
+
throw new SandboxPathBusyError();
|
|
377
|
+
await this.wait(signal);
|
|
378
|
+
}
|
|
379
|
+
const ctl = new AbortController();
|
|
380
|
+
const timer = setInterval(() => {
|
|
381
|
+
this.redis
|
|
382
|
+
.eval(EXPIRE_IF, 1, key, token, ttl)
|
|
383
|
+
.then((ok) => {
|
|
384
|
+
if (Number(ok) === 1 || ctl.signal.aborted)
|
|
385
|
+
return;
|
|
386
|
+
this.log.warn("sandbox path lease lost", { conversationId: conv });
|
|
387
|
+
ctl.abort(new SandboxLockLostError(conv));
|
|
388
|
+
clearInterval(timer);
|
|
389
|
+
})
|
|
390
|
+
.catch((err) => this.log.warn("sandbox path lease renew failed", { conversationId: conv, err }));
|
|
391
|
+
}, Math.max(1, Math.floor(ttl / 3)));
|
|
392
|
+
timer.unref();
|
|
393
|
+
return {
|
|
394
|
+
signal: ctl.signal,
|
|
395
|
+
release: async () => {
|
|
396
|
+
clearInterval(timer);
|
|
397
|
+
await this.redis.eval(DEL_IF, 1, key, token);
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Wraps a just-acquired lease: renews every TTL / 3 and remembers the last successful renewal;
|
|
403
|
+
* the lease is lost when a renewal reports it gone or none succeeded for 2/3 TTL (leaving margin
|
|
404
|
+
* so the abort takes effect before the key expires).
|
|
405
|
+
*/
|
|
406
|
+
hold(conv, token, kind) {
|
|
407
|
+
const ctl = new AbortController();
|
|
408
|
+
const staleMs = Math.floor((this.ttlMs * 2) / 3);
|
|
409
|
+
let inFlight = false;
|
|
410
|
+
const s = {
|
|
411
|
+
kind,
|
|
412
|
+
token,
|
|
413
|
+
conv,
|
|
414
|
+
ctl,
|
|
415
|
+
lastOk: Date.now(),
|
|
416
|
+
released: false,
|
|
417
|
+
stop: () => {
|
|
418
|
+
clearInterval(timer);
|
|
419
|
+
clearInterval(watchdog);
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
const stale = () => Date.now() - s.lastOk > staleMs;
|
|
423
|
+
const tick = () => {
|
|
424
|
+
if (stale())
|
|
425
|
+
this.lose(s);
|
|
426
|
+
if (ctl.signal.aborted) {
|
|
427
|
+
s.stop();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (inFlight)
|
|
431
|
+
return;
|
|
432
|
+
inFlight = true;
|
|
433
|
+
void this.renew(s).finally(() => {
|
|
434
|
+
inFlight = false;
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
const timer = setInterval(tick, Math.max(1, Math.floor(this.ttlMs / 3)));
|
|
438
|
+
timer.unref();
|
|
439
|
+
// a finer check (at most every 5 s): notices a renewal that hangs
|
|
440
|
+
const watchdog = setInterval(() => {
|
|
441
|
+
if (stale())
|
|
442
|
+
this.lose(s);
|
|
443
|
+
if (ctl.signal.aborted)
|
|
444
|
+
s.stop();
|
|
445
|
+
}, Math.min(5000, Math.max(1, Math.floor(this.ttlMs / 6))));
|
|
446
|
+
watchdog.unref();
|
|
447
|
+
return this.view(s, kind);
|
|
448
|
+
}
|
|
449
|
+
lose(s) {
|
|
450
|
+
if (s.ctl.signal.aborted)
|
|
451
|
+
return;
|
|
452
|
+
this.log.warn("sandbox lease lost", { conversationId: s.conv, kind: s.kind });
|
|
453
|
+
s.ctl.abort(new SandboxLockLostError(s.conv));
|
|
454
|
+
s.stop();
|
|
455
|
+
}
|
|
456
|
+
async renew(s) {
|
|
457
|
+
if (s.ctl.signal.aborted)
|
|
458
|
+
return false;
|
|
459
|
+
const [lock, users] = this.keys(s.conv);
|
|
460
|
+
// Redis computes the new expiry when it runs the script: freshness counts from the send
|
|
461
|
+
const sent = Date.now();
|
|
462
|
+
try {
|
|
463
|
+
const ok = Number(await this.redis.eval(RENEW, 2, lock, users, s.token, this.ttlMs, s.kind));
|
|
464
|
+
if (ok === 1)
|
|
465
|
+
s.lastOk = Math.max(s.lastOk, sent);
|
|
466
|
+
else
|
|
467
|
+
this.lose(s);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
this.log.warn("sandbox lease renew failed", { conversationId: s.conv, err });
|
|
471
|
+
}
|
|
472
|
+
if (Date.now() - s.lastOk > Math.floor((this.ttlMs * 2) / 3))
|
|
473
|
+
this.lose(s);
|
|
474
|
+
return !s.ctl.signal.aborted;
|
|
475
|
+
}
|
|
476
|
+
/** A lease view of `s` as `kind`; a view stops working once the lease changes kind. */
|
|
477
|
+
view(s, kind) {
|
|
478
|
+
const [lock, users] = this.keys(s.conv);
|
|
479
|
+
const current = () => s.kind === kind;
|
|
480
|
+
return {
|
|
481
|
+
kind,
|
|
482
|
+
token: s.token,
|
|
483
|
+
conversationId: s.conv,
|
|
484
|
+
signal: s.ctl.signal,
|
|
485
|
+
renew: () => (current() ? this.renew(s) : Promise.resolve(false)),
|
|
486
|
+
// Any view releases the lease in its current kind (a caller that kept only the exclusive
|
|
487
|
+
// view after a downgrade must not leak the shared lease, which would renew forever)
|
|
488
|
+
release: async () => {
|
|
489
|
+
if (s.released)
|
|
490
|
+
return;
|
|
491
|
+
s.released = true;
|
|
492
|
+
s.stop();
|
|
493
|
+
await this.redis
|
|
494
|
+
.eval(RELEASE, 2, lock, users, s.token, s.kind)
|
|
495
|
+
.catch((err) => this.log.warn("cannot release sandbox lease", { conversationId: s.conv, err }));
|
|
496
|
+
},
|
|
497
|
+
downgrade: async () => {
|
|
498
|
+
if (!current() || s.kind !== "exclusive" || s.released) {
|
|
499
|
+
throw new Error("sandbox lease is not exclusive");
|
|
500
|
+
}
|
|
501
|
+
if (s.ctl.signal.aborted)
|
|
502
|
+
throw abortReason(s.ctl.signal);
|
|
503
|
+
const sent = Date.now();
|
|
504
|
+
const ok = Number(await this.redis.eval(DOWNGRADE, 2, lock, users, s.token, this.ttlMs));
|
|
505
|
+
if (ok !== 1) {
|
|
506
|
+
this.lose(s);
|
|
507
|
+
throw new SandboxLockLostError(s.conv);
|
|
508
|
+
}
|
|
509
|
+
s.kind = "shared";
|
|
510
|
+
s.lastOk = Math.max(s.lastOk, sent);
|
|
511
|
+
return this.view(s, "shared");
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|