@capybara-hq/kernel 0.0.0-stage → 0.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +20 -2
- package/bootstrap.json +22 -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 +1125 -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 +223 -0
- package/dist/config/schema.js +1151 -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 +422 -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 +150 -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 +210 -0
- package/dist/errors.js +212 -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 +789 -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 +149 -0
- package/dist/extensions/bootstrap.js +441 -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 +170 -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 +67 -0
- package/dist/index.js +65 -0
- package/dist/kernel.d.ts +256 -0
- package/dist/kernel.js +1326 -0
- package/dist/log.d.ts +48 -0
- package/dist/log.js +229 -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 +271 -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 +147 -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 +805 -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 +550 -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 +114 -0
- package/dist/tools/builtins/sandbox/common.js +304 -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 +67 -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,1333 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { EXTENSION_POINTS, parseExtensionPoint, parseSkillMd, SKILL_ENTRY_FILE, } from "@capybara-hq/plugin-sdk";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
import { normalizeToolRef } from "../agents/definition.js";
|
|
5
|
+
import { putTemplateVersionIn, revertTemplateVersionIn, softDeleteTemplateIn, } from "../agents/templates.js";
|
|
6
|
+
import { ApiError } from "../errors.js";
|
|
7
|
+
import { isExtensionEnabled } from "../extensions/enabled.js";
|
|
8
|
+
import { audit } from "../identity/audit.js";
|
|
9
|
+
import { lockOrg } from "../orgs/org-lock.js";
|
|
10
|
+
import { packagePath } from "../packages/tgz.js";
|
|
11
|
+
import { skillConflictError, skillConflicts, } from "../skills/skills.service.js";
|
|
12
|
+
import { lockOrgForActions, readCatalogAgentTemplates, readCatalogFiles, readCatalogTemplates, requiredActions, skillOf, templateConnectorInput, templatePluginProblem, } from "./org-catalog.service.js";
|
|
13
|
+
import { assertItemInstallable, disabledPluginItems, pluginItemDisabled, } from "./requests.service.js";
|
|
14
|
+
import { upgradeInfo } from "./upgrade-targets.js";
|
|
15
|
+
import { maxSatisfying, satisfiesRange } from "./versions.js";
|
|
16
|
+
/** How deep plugin dependencies may nest (the root plugin's dependencies are level 1). */
|
|
17
|
+
export const DEPENDENCY_DEPTH_LIMIT = 10;
|
|
18
|
+
/** A failed install step, carried up through the dependency installs to the root install. */
|
|
19
|
+
export class InstallFailure extends Error {
|
|
20
|
+
step;
|
|
21
|
+
cause;
|
|
22
|
+
dependency;
|
|
23
|
+
rolledBack;
|
|
24
|
+
constructor(step, cause,
|
|
25
|
+
/** The dependency path when a dependency failed (outermost first). */
|
|
26
|
+
dependency, rolledBack) {
|
|
27
|
+
super("plugin install failed");
|
|
28
|
+
this.step = step;
|
|
29
|
+
this.cause = cause;
|
|
30
|
+
this.dependency = dependency;
|
|
31
|
+
this.rolledBack = rolledBack;
|
|
32
|
+
}
|
|
33
|
+
get code() {
|
|
34
|
+
return this.cause instanceof ApiError ? this.cause.code : "internal";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
38
|
+
const ALL_ACTIONS = [
|
|
39
|
+
"org.extensions.manage",
|
|
40
|
+
"connectors.manage",
|
|
41
|
+
"skills.manage",
|
|
42
|
+
"agents.manage",
|
|
43
|
+
];
|
|
44
|
+
const installNotFound = () => new ApiError("plugin.install_not_found", "Plugin install not found");
|
|
45
|
+
const fail = (path, message) => new ApiError("validation_failed", "Request validation failed", [{ path, message }]);
|
|
46
|
+
const isUniqueViolation = (e) => typeof e === "object" && e !== null && e.code === "23505";
|
|
47
|
+
const unresolved = (name, range, why) => new ApiError("catalog.dependency_unresolved", `Dependency ${name}@${range} cannot be resolved: ${why}`, [{ dependency: name, range }]);
|
|
48
|
+
const dependencyDisabled = (name) => new ApiError("catalog.dependency_disabled", `Dependency ${name} is disabled by the enterprise admin, so this plugin cannot be installed`, [{ dependency: name }]);
|
|
49
|
+
/**
|
|
50
|
+
* The dependency `d` resolved against the catalog: the highest imported version (of the live item
|
|
51
|
+
* with that name) satisfying its range. No such item or version -> `catalog.dependency_unresolved`;
|
|
52
|
+
* the item disabled -> `catalog.dependency_disabled` (the enterprise admin's ban is not bypassed,
|
|
53
|
+
* spec §10).
|
|
54
|
+
*/
|
|
55
|
+
async function resolveOne(db, d) {
|
|
56
|
+
const item = await db
|
|
57
|
+
.selectFrom("catalog_items")
|
|
58
|
+
.select(["id", "policy"])
|
|
59
|
+
.where("name", "=", d.name)
|
|
60
|
+
.where("deleted_at", "is", null)
|
|
61
|
+
.executeTakeFirst();
|
|
62
|
+
if (!item) {
|
|
63
|
+
throw unresolved(d.name, d.version, "it is not in the enterprise catalog; ask the enterprise admin to import it");
|
|
64
|
+
}
|
|
65
|
+
if (item.policy === "disabled")
|
|
66
|
+
throw dependencyDisabled(d.name);
|
|
67
|
+
const versions = await db
|
|
68
|
+
.selectFrom("catalog_item_versions")
|
|
69
|
+
.select(["version", "manifest", "imported_at"])
|
|
70
|
+
.where("item_id", "=", item.id)
|
|
71
|
+
.execute();
|
|
72
|
+
const v = maxSatisfying(versions.map((x) => ({ ...x, importedAt: x.imported_at })), d.version);
|
|
73
|
+
if (!v) {
|
|
74
|
+
throw unresolved(d.name, d.version, "no imported version satisfies the range; ask the enterprise admin to import one");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
name: d.name,
|
|
78
|
+
range: d.version,
|
|
79
|
+
itemId: item.id,
|
|
80
|
+
version: v.version,
|
|
81
|
+
manifest: v.manifest,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolves a plugin's dependencies against the catalog: for each one the highest imported version
|
|
86
|
+
* of the live item with that name satisfying its range. The whole dependency tree is checked: a
|
|
87
|
+
* dependency nothing satisfies -> `catalog.dependency_unresolved`; a disabled one ->
|
|
88
|
+
* `catalog.dependency_disabled`; a cycle (`a` -> `b` -> `a`, by name) or nesting deeper than
|
|
89
|
+
* {@link DEPENDENCY_DEPTH_LIMIT} -> `catalog.dependency_cycle`. Returns the direct dependencies.
|
|
90
|
+
*/
|
|
91
|
+
export async function resolveDependencies(db, manifest) {
|
|
92
|
+
// Explored subtrees by name@version: their height and the names in them (a memo hit whose names
|
|
93
|
+
// meet the current path is a cycle)
|
|
94
|
+
const memo = new Map();
|
|
95
|
+
const direct = [];
|
|
96
|
+
const tooDeep = () => new ApiError("catalog.dependency_cycle", `Plugin dependencies nest more than ${DEPENDENCY_DEPTH_LIMIT} levels deep`);
|
|
97
|
+
const cycle = (path, name) => new ApiError("catalog.dependency_cycle", `Plugin dependencies form a cycle: ${[...path.slice(path.indexOf(name)), name].join(" -> ")}`, [{ cycle: [...path.slice(path.indexOf(name)), name] }]);
|
|
98
|
+
const visit = async (m, path) => {
|
|
99
|
+
let height = 0;
|
|
100
|
+
const names = new Set();
|
|
101
|
+
for (const d of m.dependencies ?? []) {
|
|
102
|
+
if (path.includes(d.name))
|
|
103
|
+
throw cycle(path, d.name);
|
|
104
|
+
if (path.length > DEPENDENCY_DEPTH_LIMIT)
|
|
105
|
+
throw tooDeep();
|
|
106
|
+
const r = await resolveOne(db, d);
|
|
107
|
+
if (path.length === 1) {
|
|
108
|
+
direct.push({ name: r.name, range: r.range, itemId: r.itemId, version: r.version });
|
|
109
|
+
}
|
|
110
|
+
const key = `${r.name}@${r.version}`;
|
|
111
|
+
let sub = memo.get(key);
|
|
112
|
+
if (sub) {
|
|
113
|
+
const hit = path.find((p) => sub?.names.has(p));
|
|
114
|
+
if (hit !== undefined)
|
|
115
|
+
throw cycle(path, hit);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
sub = await visit(r.manifest, [...path, d.name]);
|
|
119
|
+
memo.set(key, sub);
|
|
120
|
+
}
|
|
121
|
+
if (path.length + sub.height > DEPENDENCY_DEPTH_LIMIT)
|
|
122
|
+
throw tooDeep();
|
|
123
|
+
height = Math.max(height, sub.height + 1);
|
|
124
|
+
names.add(d.name);
|
|
125
|
+
for (const n of sub.names)
|
|
126
|
+
names.add(n);
|
|
127
|
+
}
|
|
128
|
+
return { height, names };
|
|
129
|
+
};
|
|
130
|
+
await visit(manifest, [manifest.name]);
|
|
131
|
+
return direct;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The input a template needs that the org did not give: a static credential (the org's for an
|
|
135
|
+
* org-owned template; for a personal one the installer's own, stored as their personal credential,
|
|
136
|
+
* since creating the connector lists its tools with it), or the OAuth client of a
|
|
137
|
+
* client-credentials / manual-client template. Undefined when nothing is missing.
|
|
138
|
+
* `personalCredential: false` keeps a personal static template optional (an existing connector
|
|
139
|
+
* whose template's auth changed: its members keep giving their own).
|
|
140
|
+
*/
|
|
141
|
+
export function missingTemplateInput(template, given, o = {}) {
|
|
142
|
+
const auth = template.auth;
|
|
143
|
+
if (auth.type === "oauth_client" || (auth.type === "oauth_code" && auth.client === "manual")) {
|
|
144
|
+
return given?.client === undefined ? "client" : undefined;
|
|
145
|
+
}
|
|
146
|
+
if (auth.type === "none" || auth.type === "oauth_code")
|
|
147
|
+
return undefined;
|
|
148
|
+
if (auth.ownership !== "org" && o.personalCredential === false)
|
|
149
|
+
return undefined;
|
|
150
|
+
return given?.credential === undefined ? "credential" : undefined;
|
|
151
|
+
}
|
|
152
|
+
/** `e` with its validation paths under `at` (input errors of a template's connector). */
|
|
153
|
+
export function under(e, at) {
|
|
154
|
+
if (!(e instanceof ApiError) || e.code !== "validation_failed" || !Array.isArray(e.details)) {
|
|
155
|
+
return e;
|
|
156
|
+
}
|
|
157
|
+
return new ApiError(e.code, e.message, e.details.map((d) => typeof d === "object" && d !== null && typeof d.path === "string"
|
|
158
|
+
? { ...d, path: [at, d.path].filter(Boolean).join(".") }
|
|
159
|
+
: d));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Org install and uninstall of plugins (spec §10, C31). Before anything is written the install is
|
|
163
|
+
* planned for the whole dependency tree ({@link resolveDependencies}; the extension deployed, the
|
|
164
|
+
* template input valid, else a 4xx). Install, in order, per plugin:
|
|
165
|
+
* 1. dependencies, depth first: each one not yet installed in the org gets its own nested
|
|
166
|
+
* `plugin_installs` row, recorded as a `plugin` component of the parent (created=true); one
|
|
167
|
+
* already installed is recorded created=false;
|
|
168
|
+
* 2. the plugin's extension, enabled in the org (`catalog.not_deployed` when the deployment does
|
|
169
|
+
* not run it; already enabled is recorded created=false);
|
|
170
|
+
* 3. connectors from the templates (credentials / clients go to secrets, default policies are
|
|
171
|
+
* recorded `policy_source 'template'`);
|
|
172
|
+
* 4. the skills (an org skill of the same name from the same catalog item gets a new version,
|
|
173
|
+
* recorded created=false with skill_version; one of other provenance, the org's own or another
|
|
174
|
+
* item's, refuses the install while planning with `plugin.skill_conflict`, spec §9.2).
|
|
175
|
+
* Approval follows the installed plugin's policy only (spec §10): dependency components are
|
|
176
|
+
* re-checked against the root item's policy and their own item's disabled state. Each component is
|
|
177
|
+
* recorded in `plugin_install_components` as soon as it exists; if any step fails, the components
|
|
178
|
+
* this install created (nested dependency installs included) are undone in reverse order, the
|
|
179
|
+
* install is marked failed (error holds only the step and the error code) and 502
|
|
180
|
+
* `plugin.install_failed` is thrown. Uninstall removes only components with created=true that
|
|
181
|
+
* nothing else uses, and reports what it kept and why. No in-process state; the org row lock
|
|
182
|
+
* serializes writes within one org. In-place upgrades are run by `PluginUpgrader`
|
|
183
|
+
* (plugin-upgrade.ts), which reuses the planning, install and ledger methods here (the ones without
|
|
184
|
+
* `private` are internal to the catalog, not an API).
|
|
185
|
+
*/
|
|
186
|
+
export class PluginInstaller {
|
|
187
|
+
d;
|
|
188
|
+
constructor(d) {
|
|
189
|
+
this.d = d;
|
|
190
|
+
}
|
|
191
|
+
/** The installer's dependencies (shared with `PluginUpgrader`). */
|
|
192
|
+
get deps() {
|
|
193
|
+
return this.d;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The checks an install runs before it creates anything, without the org's input (for the org
|
|
197
|
+
* catalog to run before a request is created): dependency resolution, the extensions deployed and
|
|
198
|
+
* the template connectors available. Returns the org actions the whole install needs.
|
|
199
|
+
*/
|
|
200
|
+
async preflight(orgId, actor, itemId, version) {
|
|
201
|
+
const plan = await this.plan(orgId, actor, itemId, version, null, "", undefined);
|
|
202
|
+
await this.assertNoSkillConflicts(orgId, plan);
|
|
203
|
+
return { actions: planActions(plan) };
|
|
204
|
+
}
|
|
205
|
+
async install(orgId, actor, itemId, version, input, opts = {}) {
|
|
206
|
+
const plan = await this.plan(orgId, actor, itemId, version, input, "", undefined);
|
|
207
|
+
await this.assertNoSkillConflicts(orgId, plan);
|
|
208
|
+
let installId;
|
|
209
|
+
try {
|
|
210
|
+
installId = await this.execute(orgId, actor, plan, opts, {
|
|
211
|
+
actions: planActions(plan),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
if (!(e instanceof InstallFailure))
|
|
216
|
+
throw e;
|
|
217
|
+
// A dependency another install is still installing: retryable, a 409 as it is
|
|
218
|
+
if (e.cause instanceof ApiError && e.cause.code === "plugin.install_in_progress") {
|
|
219
|
+
throw e.cause;
|
|
220
|
+
}
|
|
221
|
+
const dep = e.dependency.length > 0 ? ` in dependency ${e.dependency.join(" > ")}` : "";
|
|
222
|
+
throw new ApiError("plugin.install_failed", `Installing the plugin failed at step ${e.step}${dep} (${e.code}); the components it created were removed`, [
|
|
223
|
+
{
|
|
224
|
+
step: e.step,
|
|
225
|
+
code: e.code,
|
|
226
|
+
...(e.dependency.length > 0 && { dependency: e.dependency.join(" > ") }),
|
|
227
|
+
...(e.cause instanceof ApiError && { message: e.cause.message }),
|
|
228
|
+
...(e.cause instanceof ApiError &&
|
|
229
|
+
e.cause.details !== undefined && { causeDetails: e.cause.details }),
|
|
230
|
+
rolledBack: e.rolledBack,
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
const [view] = await this.list(orgId, installId);
|
|
235
|
+
if (!view)
|
|
236
|
+
throw installNotFound();
|
|
237
|
+
return view;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Refuses a plan whose skills would overwrite a skill of other provenance (spec §9.2, Decision
|
|
241
|
+
* 8) with 409 `plugin.skill_conflict`, before anything is written: for the plugin and each
|
|
242
|
+
* dependency it installs, an org skill of the same name that is the org's own or another item's,
|
|
243
|
+
* and a name two plugins of the same plan both bring (the one installed first is the source).
|
|
244
|
+
*/
|
|
245
|
+
async assertNoSkillConflicts(orgId, plan) {
|
|
246
|
+
const conflicts = await planSkillConflicts(this.d.db, orgId, plan);
|
|
247
|
+
if (conflicts.length > 0)
|
|
248
|
+
throw skillConflictError(conflicts);
|
|
249
|
+
}
|
|
250
|
+
/** The connector checks of templates against what this replica loads right now. */
|
|
251
|
+
templateDeps() {
|
|
252
|
+
const versions = new Map(this.d.host.manifests().map((m) => [m.name, m.version]));
|
|
253
|
+
return { findConnector: this.d.findConnector, pluginVersion: (n) => versions.get(n) };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Plans the install of one plugin version and, depth first, of its dependencies not yet
|
|
257
|
+
* installed in the org. `input` null skips the input checks (preflight); dependencies always get
|
|
258
|
+
* an empty input. `at` prefixes validation paths (`dependencies.<name>.` for a dependency);
|
|
259
|
+
* `approvedBy` is the root item for a dependency.
|
|
260
|
+
*/
|
|
261
|
+
async plan(orgId, actor, itemId, version, input, at, approvedBy) {
|
|
262
|
+
if (!UUID_RE.test(itemId))
|
|
263
|
+
throw new ApiError("catalog.item_not_found", "Plugin not found");
|
|
264
|
+
const v = await this.d.db
|
|
265
|
+
.selectFrom("catalog_item_versions as v")
|
|
266
|
+
.innerJoin("catalog_items as i", "i.id", "v.item_id")
|
|
267
|
+
.select([
|
|
268
|
+
"i.name",
|
|
269
|
+
"v.manifest",
|
|
270
|
+
"v.object_prefix",
|
|
271
|
+
"v.files",
|
|
272
|
+
"v.version",
|
|
273
|
+
"v.source_format",
|
|
274
|
+
])
|
|
275
|
+
.where("v.item_id", "=", itemId)
|
|
276
|
+
.where("v.version", "=", version)
|
|
277
|
+
.where("i.deleted_at", "is", null)
|
|
278
|
+
.executeTakeFirst();
|
|
279
|
+
if (!v)
|
|
280
|
+
throw new ApiError("catalog.item_not_found", "Plugin version not found");
|
|
281
|
+
const manifest = v.manifest;
|
|
282
|
+
const deps = await resolveDependencies(this.d.db, manifest);
|
|
283
|
+
const extension = await this.planExtension(orgId, manifest);
|
|
284
|
+
const templates = await readCatalogTemplates(this.d.storage, v);
|
|
285
|
+
const skillNames = await catalogSkillNames(this.d.storage, v, manifest);
|
|
286
|
+
const agentTemplates = await readCatalogAgentTemplates(this.d.storage, v);
|
|
287
|
+
const connectors = new Map();
|
|
288
|
+
if (input === null) {
|
|
289
|
+
for (const ct of templates) {
|
|
290
|
+
const problem = templatePluginProblem(this.templateDeps(), ct.file.template);
|
|
291
|
+
if (problem !== undefined)
|
|
292
|
+
throw new ApiError("catalog.not_deployed", problem);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.checkInput(manifest, templates, input, at);
|
|
297
|
+
for (const ct of templates) {
|
|
298
|
+
connectors.set(ct.id, this.planConnector(actor, { itemId, version, name: manifest.name }, ct, input, at, approvedBy));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const dependencies = [];
|
|
302
|
+
for (const dep of deps) {
|
|
303
|
+
if (await this.liveDependency(this.d.db, orgId, dep)) {
|
|
304
|
+
dependencies.push({ dep, plan: null });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
dependencies.push({
|
|
308
|
+
dep,
|
|
309
|
+
plan: await this.plan(orgId, actor, dep.itemId, dep.version, input === null ? null : {}, `${at}dependencies.${dep.name}.`, approvedBy ?? itemId),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
itemId,
|
|
314
|
+
name: v.name,
|
|
315
|
+
version,
|
|
316
|
+
manifest,
|
|
317
|
+
row: v,
|
|
318
|
+
sourceFormat: v.source_format,
|
|
319
|
+
extension,
|
|
320
|
+
extensionConfig: input?.extensionConfig,
|
|
321
|
+
templates,
|
|
322
|
+
connectors,
|
|
323
|
+
skillNames,
|
|
324
|
+
agentTemplates,
|
|
325
|
+
dependencies,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The connector input of one template from the org's input (C30), checked without a connection:
|
|
330
|
+
* the template's connector extension deployed, the input valid, the credential / OAuth client it
|
|
331
|
+
* needs given (else 400; a dependency's says to install it on its own first).
|
|
332
|
+
*/
|
|
333
|
+
planConnector(actor, plugin, ct, input, at, approvedBy) {
|
|
334
|
+
const path = `${at}connectors.${ct.id}`;
|
|
335
|
+
const given = input.connectors?.[ct.id];
|
|
336
|
+
const ci = templateConnectorInput(this.templateDeps(), ct.file.template, {
|
|
337
|
+
itemId: plugin.itemId,
|
|
338
|
+
version: plugin.version,
|
|
339
|
+
templateId: ct.id,
|
|
340
|
+
...(approvedBy !== undefined && { approvedBy }),
|
|
341
|
+
}, given, ct.file.displayName.en, path);
|
|
342
|
+
const missing = missingTemplateInput(ct.file.template, given);
|
|
343
|
+
if (missing !== undefined) {
|
|
344
|
+
const personal = missing === "credential" &&
|
|
345
|
+
ct.file.template.auth.ownership !== "org";
|
|
346
|
+
const needs = missing === "client"
|
|
347
|
+
? "an OAuth client"
|
|
348
|
+
: personal
|
|
349
|
+
? "the installer's own credential"
|
|
350
|
+
: "an org credential";
|
|
351
|
+
throw fail(`${path}.${missing}`, approvedBy !== undefined
|
|
352
|
+
? `Dependency ${plugin.name} has a connector template that needs ${needs}; install ${plugin.name} on its own first`
|
|
353
|
+
: personal
|
|
354
|
+
? "This connector template uses each member's own credential; give yours (it lists the tools and is stored as your personal credential)"
|
|
355
|
+
: `This connector template needs ${needs}`);
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
this.d.connectors.checkCreateInput(actor, ci);
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
throw under(e, path);
|
|
362
|
+
}
|
|
363
|
+
return ci;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The org's live install of a dependency's item (installing or installed), or undefined. One at a
|
|
367
|
+
* version outside the dependency's range -> `catalog.dependency_unresolved` (org upgrades arrive in
|
|
368
|
+
* phase A).
|
|
369
|
+
*/
|
|
370
|
+
async liveDependency(db, orgId, dep) {
|
|
371
|
+
const live = await this.findLiveDependency(db, orgId, dep, false);
|
|
372
|
+
if (!live)
|
|
373
|
+
return undefined;
|
|
374
|
+
if (!satisfiesRange(live.version, dep.range)) {
|
|
375
|
+
throw unresolved(dep.name, dep.range, `this organization has version ${live.version} installed; uninstall it to install a matching version`);
|
|
376
|
+
}
|
|
377
|
+
return live.id;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The org's live (not failed) install of a dependency's item; `lock` takes it FOR SHARE (the
|
|
381
|
+
* caller holds the org row lock).
|
|
382
|
+
*/
|
|
383
|
+
async findLiveDependency(db, orgId, dep, lock) {
|
|
384
|
+
let q = db
|
|
385
|
+
.selectFrom("plugin_installs")
|
|
386
|
+
.select(["id", "version", "status"])
|
|
387
|
+
.where("org_id", "=", orgId)
|
|
388
|
+
.where("item_id", "=", dep.itemId)
|
|
389
|
+
.where("status", "<>", "failed");
|
|
390
|
+
if (lock)
|
|
391
|
+
q = q.forShare();
|
|
392
|
+
return q.executeTakeFirst();
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Records an install of `dep` as a `plugin` component of the install, checked and written in one
|
|
396
|
+
* transaction under the org row lock, so a concurrent uninstall or a sibling install's rollback
|
|
397
|
+
* cannot leave the component pointing at a removed install. `created` false: the org's existing
|
|
398
|
+
* install, if any (returns its id, undefined when there is none); it must be `installed`
|
|
399
|
+
* (`installing` -> 409 `plugin.install_in_progress`, retry later) and in range. `created` true:
|
|
400
|
+
* `child`, the dependency install this install just created, must still exist.
|
|
401
|
+
*/
|
|
402
|
+
async recordDependency(orgId, dep, record, child) {
|
|
403
|
+
return this.d.db.transaction().execute(async (t) => {
|
|
404
|
+
await lockOrg(t, orgId);
|
|
405
|
+
const live = await this.findLiveDependency(t, orgId, dep, true);
|
|
406
|
+
if (child !== undefined) {
|
|
407
|
+
if (live?.id !== child || live.status !== "installed") {
|
|
408
|
+
throw unresolved(dep.name, dep.range, "its install in this organization was removed meanwhile; try again");
|
|
409
|
+
}
|
|
410
|
+
await record({ kind: "plugin", ref: child, created: true, skillVersion: null }, t);
|
|
411
|
+
return child;
|
|
412
|
+
}
|
|
413
|
+
if (!live)
|
|
414
|
+
return undefined;
|
|
415
|
+
if (!satisfiesRange(live.version, dep.range)) {
|
|
416
|
+
throw unresolved(dep.name, dep.range, `this organization has version ${live.version} installed; uninstall it to install a matching version`);
|
|
417
|
+
}
|
|
418
|
+
if (live.status !== "installed") {
|
|
419
|
+
throw new ApiError("plugin.install_in_progress", `Dependency ${dep.name} is being installed by another install in this organization; try again when it finishes. An install stuck for longer than catalog.staleInstallMinutes can be removed by uninstalling it`);
|
|
420
|
+
}
|
|
421
|
+
await record({ kind: "plugin", ref: live.id, created: false, skillVersion: null }, t);
|
|
422
|
+
return live.id;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Runs a plan: the install row, then the steps (see the class comment). Throws
|
|
427
|
+
* {@link InstallFailure} once the row exists (after rolling back); errors before it propagate
|
|
428
|
+
* as they are. Returns the install id.
|
|
429
|
+
*/
|
|
430
|
+
async execute(orgId, actor, plan, opts, ctx) {
|
|
431
|
+
const { itemId, version, manifest } = plan;
|
|
432
|
+
// The install row (role and item policy re-checked under the lock; one live install per org
|
|
433
|
+
// and item)
|
|
434
|
+
const installId = randomUUID();
|
|
435
|
+
await this.d.db.transaction().execute(async (t) => {
|
|
436
|
+
await lockOrgForActions(t, orgId, actor, ctx.actions);
|
|
437
|
+
if (ctx.approvedBy !== undefined) {
|
|
438
|
+
// A dependency disabled (or removed) since it was planned
|
|
439
|
+
const own = await t
|
|
440
|
+
.selectFrom("catalog_items")
|
|
441
|
+
.select(["policy", "deleted_at"])
|
|
442
|
+
.where("id", "=", itemId)
|
|
443
|
+
.forShare()
|
|
444
|
+
.executeTakeFirst();
|
|
445
|
+
if (!own || own.deleted_at !== null) {
|
|
446
|
+
throw unresolved(plan.name, plan.version, "it was removed from the enterprise catalog");
|
|
447
|
+
}
|
|
448
|
+
if (own.policy === "disabled")
|
|
449
|
+
throw dependencyDisabled(plan.name);
|
|
450
|
+
}
|
|
451
|
+
await assertItemInstallable(t, orgId, itemId, ctx.approvedBy ?? itemId);
|
|
452
|
+
const live = await t
|
|
453
|
+
.selectFrom("plugin_installs")
|
|
454
|
+
.select("id")
|
|
455
|
+
.where("org_id", "=", orgId)
|
|
456
|
+
.where("item_id", "=", itemId)
|
|
457
|
+
.where("status", "<>", "failed")
|
|
458
|
+
.executeTakeFirst();
|
|
459
|
+
if (live)
|
|
460
|
+
throw alreadyInstalled();
|
|
461
|
+
try {
|
|
462
|
+
await t
|
|
463
|
+
.insertInto("plugin_installs")
|
|
464
|
+
.values({
|
|
465
|
+
id: installId,
|
|
466
|
+
org_id: orgId,
|
|
467
|
+
item_id: itemId,
|
|
468
|
+
version,
|
|
469
|
+
status: "installing",
|
|
470
|
+
installed_by: actor,
|
|
471
|
+
})
|
|
472
|
+
.execute();
|
|
473
|
+
}
|
|
474
|
+
catch (e) {
|
|
475
|
+
if (isUniqueViolation(e))
|
|
476
|
+
throw alreadyInstalled();
|
|
477
|
+
throw e;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
const done = [];
|
|
481
|
+
// Records a component in the given transaction (the install row already cleaned up -> the FK
|
|
482
|
+
// fails, the install aborts and rolls back)
|
|
483
|
+
const record = async (c, t) => {
|
|
484
|
+
// Into `done` first: a component that exists is rolled back even when recording it fails
|
|
485
|
+
// (enabling an extension and recording it share one transaction and fail together)
|
|
486
|
+
done.push(c);
|
|
487
|
+
await (t ?? this.d.db)
|
|
488
|
+
.insertInto("plugin_install_components")
|
|
489
|
+
.values({
|
|
490
|
+
install_id: installId,
|
|
491
|
+
kind: c.kind,
|
|
492
|
+
ref: c.ref,
|
|
493
|
+
created: c.created,
|
|
494
|
+
skill_version: c.skillVersion,
|
|
495
|
+
template_id: c.templateId ?? null,
|
|
496
|
+
})
|
|
497
|
+
.onConflict((oc) => oc.columns(["install_id", "kind", "ref"]).doNothing())
|
|
498
|
+
.execute();
|
|
499
|
+
};
|
|
500
|
+
const approvedBy = ctx.approvedBy ?? itemId;
|
|
501
|
+
const nested = ctx.approvedBy !== undefined;
|
|
502
|
+
let step = "dependencies";
|
|
503
|
+
try {
|
|
504
|
+
for (const { dep, plan: depPlan } of plan.dependencies) {
|
|
505
|
+
// Installed already (before, or by an earlier dependency): recorded, not reinstalled
|
|
506
|
+
if ((await this.recordDependency(orgId, dep, record)) !== undefined)
|
|
507
|
+
continue;
|
|
508
|
+
if (depPlan === null) {
|
|
509
|
+
throw unresolved(dep.name, dep.range, "its install in this organization was removed meanwhile; try again");
|
|
510
|
+
}
|
|
511
|
+
let child;
|
|
512
|
+
try {
|
|
513
|
+
child = await this.execute(orgId, actor, depPlan, opts, {
|
|
514
|
+
actions: requiredActions(depPlan.manifest),
|
|
515
|
+
approvedBy,
|
|
516
|
+
parent: installId,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
if (e instanceof InstallFailure) {
|
|
521
|
+
throw new InstallFailure(e.step, e.cause, [dep.name, ...e.dependency], e.rolledBack);
|
|
522
|
+
}
|
|
523
|
+
// Another install created it meanwhile: use that one
|
|
524
|
+
if (e instanceof ApiError &&
|
|
525
|
+
e.code === "plugin.already_installed" &&
|
|
526
|
+
(await this.recordDependency(orgId, dep, record)) !== undefined) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
throw new InstallFailure("dependencies", e, [dep.name], true);
|
|
530
|
+
}
|
|
531
|
+
await this.recordDependency(orgId, dep, record, child);
|
|
532
|
+
}
|
|
533
|
+
step = "extension";
|
|
534
|
+
if (plan.extension !== null) {
|
|
535
|
+
const p = plan.extension;
|
|
536
|
+
// "Enabled before" is decided under the org row lock (already enabled: put writes nothing,
|
|
537
|
+
// the config is untouched); the component is recorded in the same transaction
|
|
538
|
+
await this.d.orgConfigs.put(orgId, p, {
|
|
539
|
+
enabled: true,
|
|
540
|
+
...(plan.extensionConfig !== undefined && { config: plan.extensionConfig }),
|
|
541
|
+
}, actor, opts.actorTokenId, {
|
|
542
|
+
viaItem: itemId,
|
|
543
|
+
...(nested && { approvedBy }),
|
|
544
|
+
ifDisabled: { skipped: false },
|
|
545
|
+
inTransaction: (t, skipped) => record({ kind: "extension", ref: p, created: !skipped, skillVersion: null }, t),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
step = "connectors";
|
|
549
|
+
for (const ct of plan.templates) {
|
|
550
|
+
const ci = plan.connectors.get(ct.id);
|
|
551
|
+
if (!ci)
|
|
552
|
+
throw new ApiError("internal", `Connector template ${ct.id} was not planned`);
|
|
553
|
+
const r = await this.d.connectors.create(orgId, actor, ci, opts.actorTokenId);
|
|
554
|
+
await record({
|
|
555
|
+
kind: "connector",
|
|
556
|
+
ref: r.connector.id,
|
|
557
|
+
created: true,
|
|
558
|
+
skillVersion: null,
|
|
559
|
+
templateId: ct.id,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
step = "agent_templates";
|
|
563
|
+
for (const a of plan.agentTemplates) {
|
|
564
|
+
// Created (or a new version of the org's live template of this plugin and name) and
|
|
565
|
+
// recorded in one transaction, under the org row lock
|
|
566
|
+
await this.d.db.transaction().execute(async (t) => {
|
|
567
|
+
await lockOrg(t, orgId);
|
|
568
|
+
const r = await putTemplateVersionIn(t, {
|
|
569
|
+
orgId,
|
|
570
|
+
plugin: plan.name,
|
|
571
|
+
catalogItemId: itemId,
|
|
572
|
+
catalogVersion: version,
|
|
573
|
+
sourceFormat: plan.sourceFormat,
|
|
574
|
+
}, a.template);
|
|
575
|
+
await record({
|
|
576
|
+
kind: "agent_template",
|
|
577
|
+
ref: r.id,
|
|
578
|
+
created: r.created,
|
|
579
|
+
skillVersion: !r.created && r.changed ? r.version : null,
|
|
580
|
+
}, t);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
step = "skills";
|
|
584
|
+
for (const s of manifest.skills ?? []) {
|
|
585
|
+
const files = await readCatalogFiles(this.d.storage, plan.row.object_prefix, plan.row.files, packagePath(s.path));
|
|
586
|
+
const r = await this.d.skills.createVersionFromFiles(orgId, actor, { ...skillOf(files), files }, {
|
|
587
|
+
catalog: {
|
|
588
|
+
itemId,
|
|
589
|
+
version,
|
|
590
|
+
...(nested && { approvedBy }),
|
|
591
|
+
format: plan.sourceFormat,
|
|
592
|
+
suggestPinned: s.pinned === true,
|
|
593
|
+
},
|
|
594
|
+
...opts,
|
|
595
|
+
});
|
|
596
|
+
await record({
|
|
597
|
+
kind: "skill",
|
|
598
|
+
ref: r.skill.id,
|
|
599
|
+
created: r.created,
|
|
600
|
+
skillVersion: r.created ? null : r.version,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
await this.d.db.transaction().execute(async (t) => {
|
|
604
|
+
await lockOrg(t, orgId);
|
|
605
|
+
const updated = await t
|
|
606
|
+
.updateTable("plugin_installs")
|
|
607
|
+
.set({ status: "installed" })
|
|
608
|
+
.where("id", "=", installId)
|
|
609
|
+
.where("status", "=", "installing")
|
|
610
|
+
.executeTakeFirst();
|
|
611
|
+
// The install row was already cleaned up as stale: do not audit plugin.org_installed, roll
|
|
612
|
+
// back
|
|
613
|
+
if (updated.numUpdatedRows === 0n) {
|
|
614
|
+
throw new ApiError("plugin.install_not_found", "The install record was cleaned up as stale while the install was running");
|
|
615
|
+
}
|
|
616
|
+
await this.d.requests.resolveByInstall(t, orgId, itemId, actor, opts);
|
|
617
|
+
await audit(t, {
|
|
618
|
+
actorAccountId: actor,
|
|
619
|
+
...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
|
|
620
|
+
orgId,
|
|
621
|
+
action: "plugin.org_installed",
|
|
622
|
+
target: installId,
|
|
623
|
+
detail: {
|
|
624
|
+
itemId,
|
|
625
|
+
name: plan.name,
|
|
626
|
+
version,
|
|
627
|
+
components: done,
|
|
628
|
+
...(ctx.parent !== undefined && { dependencyOf: ctx.parent }),
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
return installId;
|
|
633
|
+
}
|
|
634
|
+
catch (e) {
|
|
635
|
+
const failure = e instanceof InstallFailure ? e : undefined;
|
|
636
|
+
const code = failure ? failure.code : e instanceof ApiError ? e.code : "internal";
|
|
637
|
+
const rollbackErrors = await this.rollback(orgId, actor, installId, done, opts);
|
|
638
|
+
await this.d.db.transaction().execute(async (t) => {
|
|
639
|
+
await t
|
|
640
|
+
.updateTable("plugin_installs")
|
|
641
|
+
.set({ status: "failed", error: JSON.stringify({ step, code }) })
|
|
642
|
+
.where("id", "=", installId)
|
|
643
|
+
.execute();
|
|
644
|
+
await audit(t, {
|
|
645
|
+
actorAccountId: actor,
|
|
646
|
+
...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
|
|
647
|
+
orgId,
|
|
648
|
+
action: "plugin.org_install_failed",
|
|
649
|
+
target: installId,
|
|
650
|
+
detail: {
|
|
651
|
+
itemId,
|
|
652
|
+
name: plan.name,
|
|
653
|
+
version,
|
|
654
|
+
step,
|
|
655
|
+
code,
|
|
656
|
+
rolledBack: done,
|
|
657
|
+
rollbackErrors,
|
|
658
|
+
...(ctx.parent !== undefined && { dependencyOf: ctx.parent }),
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
throw new InstallFailure(failure ? failure.step : step, failure ? failure.cause : e, failure ? failure.dependency : [], rollbackErrors.length === 0 && (failure?.rolledBack ?? true));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* The extension to enable: the plugin's own, when it has one. Not loaded on this replica ->
|
|
667
|
+
* `catalog.not_deployed`; not yet enabled in this org while its own catalog item is disabled ->
|
|
668
|
+
* `catalog.item_disabled` (a disabled item is a master switch, Ruling M-5); already enabled is
|
|
669
|
+
* unaffected.
|
|
670
|
+
*/
|
|
671
|
+
async planExtension(orgId, manifest) {
|
|
672
|
+
if (!manifest.extension)
|
|
673
|
+
return null;
|
|
674
|
+
const name = manifest.name;
|
|
675
|
+
if (!this.d.host.manifests().some((m) => m.name === name)) {
|
|
676
|
+
throw new ApiError("catalog.not_deployed", `The extension of plugin ${name} is not deployed; ask the enterprise admin to install and enable it`);
|
|
677
|
+
}
|
|
678
|
+
const enabled = await isExtensionEnabled(this.d.db, orgId, name);
|
|
679
|
+
if (!enabled && (await disabledPluginItems(this.d.db, [name])).length > 0) {
|
|
680
|
+
throw pluginItemDisabled([name]);
|
|
681
|
+
}
|
|
682
|
+
return name;
|
|
683
|
+
}
|
|
684
|
+
/** The input only names the plugin's template ids, and an extension config needs an extension. */
|
|
685
|
+
checkInput(manifest, templates, input, at) {
|
|
686
|
+
const ids = new Set(templates.map((c) => c.id));
|
|
687
|
+
for (const k of Object.keys(input.connectors ?? {})) {
|
|
688
|
+
if (!ids.has(k))
|
|
689
|
+
throw fail(`${at}connectors.${k}`, "No such connector template in this plugin");
|
|
690
|
+
}
|
|
691
|
+
if (input.extensionConfig !== undefined && !manifest.extension) {
|
|
692
|
+
throw fail(`${at}extensionConfig`, "This plugin has no extension");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Undoes the components this install created, in reverse order (the role is not re-checked: it
|
|
697
|
+
* undoes this install's own changes); returns the ones that failed. The component rows of undone
|
|
698
|
+
* (or already gone) components are deleted in the same transaction, so cleaning up the failed
|
|
699
|
+
* install later only handles what failed to undo or was kept, and does not undo again an extension
|
|
700
|
+
* an org admin re-enabled by hand or a skill version a user uploaded since.
|
|
701
|
+
*/
|
|
702
|
+
async rollback(orgId, actor, installId, done, opts) {
|
|
703
|
+
const errors = [];
|
|
704
|
+
for (const c of [...done].reverse()) {
|
|
705
|
+
if (!c.created && c.skillVersion === null)
|
|
706
|
+
continue;
|
|
707
|
+
try {
|
|
708
|
+
const disabled = [];
|
|
709
|
+
await this.d.db.transaction().execute(async (t) => {
|
|
710
|
+
await lockOrg(t, orgId);
|
|
711
|
+
if (c.kind === "plugin") {
|
|
712
|
+
// A dependency install this install created: uninstalled by the usual rules
|
|
713
|
+
const sub = { removed: [], kept: [] };
|
|
714
|
+
await this.uninstallIn(t, orgId, actor, c.ref, {
|
|
715
|
+
report: sub,
|
|
716
|
+
removing: new Set([installId]),
|
|
717
|
+
from: installId,
|
|
718
|
+
authorize: false,
|
|
719
|
+
reason: "plugin_install_rollback",
|
|
720
|
+
opts,
|
|
721
|
+
});
|
|
722
|
+
disabled.push(...sub.removed.filter((r) => r.kind === "extension").map((r) => r.ref));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
// Another enabled extension depends on it (only it provides what that one needs in this
|
|
726
|
+
// org): kept (its component row too), so the other does not lose its dependency
|
|
727
|
+
if (c.kind === "extension" &&
|
|
728
|
+
(await this.d.orgConfigs.enabledDependents(t, orgId, c.ref)).length > 0) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const removed = await this.removeComponent(t, orgId, actor, c, "plugin_install_rollback");
|
|
732
|
+
if (removed && c.kind === "extension")
|
|
733
|
+
disabled.push(c.ref);
|
|
734
|
+
}
|
|
735
|
+
await t
|
|
736
|
+
.deleteFrom("plugin_install_components")
|
|
737
|
+
.where("install_id", "=", installId)
|
|
738
|
+
.where("kind", "=", c.kind)
|
|
739
|
+
.where("ref", "=", c.ref)
|
|
740
|
+
.execute();
|
|
741
|
+
});
|
|
742
|
+
for (const p of disabled)
|
|
743
|
+
await this.d.orgConfigs.afterDisable(orgId, p);
|
|
744
|
+
}
|
|
745
|
+
catch (e) {
|
|
746
|
+
errors.push({ kind: c.kind, ref: c.ref });
|
|
747
|
+
this.d.log?.warn("plugin install rollback step failed", {
|
|
748
|
+
orgId,
|
|
749
|
+
kind: c.kind,
|
|
750
|
+
ref: c.ref,
|
|
751
|
+
error: e instanceof Error ? e.message : String(e),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return errors;
|
|
756
|
+
}
|
|
757
|
+
/** Removes one component (one already gone is no error); returns whether anything changed. */
|
|
758
|
+
async removeComponent(t, orgId, actor, c, reason) {
|
|
759
|
+
if (c.kind === "extension") {
|
|
760
|
+
return this.d.orgConfigs.disableIn(t, orgId, c.ref, actor, reason);
|
|
761
|
+
}
|
|
762
|
+
if (c.kind === "connector") {
|
|
763
|
+
const live = await t
|
|
764
|
+
.selectFrom("connectors")
|
|
765
|
+
.select("id")
|
|
766
|
+
.where("id", "=", c.ref)
|
|
767
|
+
.where("org_id", "=", orgId)
|
|
768
|
+
.where("deleted_at", "is", null)
|
|
769
|
+
.executeTakeFirst();
|
|
770
|
+
if (!live)
|
|
771
|
+
return false;
|
|
772
|
+
await this.d.connectors.removeIn(t, orgId, c.ref, actor, undefined, reason);
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
if (c.kind === "plugin")
|
|
776
|
+
return false;
|
|
777
|
+
if (c.kind === "agent_template") {
|
|
778
|
+
if (c.created)
|
|
779
|
+
return softDeleteTemplateIn(t, orgId, c.ref);
|
|
780
|
+
if (c.skillVersion !== null) {
|
|
781
|
+
return revertTemplateVersionIn(t, orgId, c.ref, c.skillVersion);
|
|
782
|
+
}
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
if (c.created)
|
|
786
|
+
return this.d.skills.softDeleteIn(t, orgId, c.ref, actor, reason);
|
|
787
|
+
if (c.skillVersion !== null) {
|
|
788
|
+
return this.d.skills.revertVersionIn(t, orgId, c.ref, c.skillVersion, actor, reason);
|
|
789
|
+
}
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Uninstall (the plugin's component actions, see `requiredActions`, re-checked under the org row
|
|
794
|
+
* lock, one transaction). Removes only components with created=true:
|
|
795
|
+
* - a skill is kept when an agent selects it (used_by_agents) or another plugin install in this
|
|
796
|
+
* org uses it too (used_by_other_install);
|
|
797
|
+
* - a connector is kept when an agent references it (used_by_agents);
|
|
798
|
+
* - an extension is kept when another plugin install uses it too (used_by_other_install), a
|
|
799
|
+
* connector still uses a connector it provides (used_by_connectors), or another enabled
|
|
800
|
+
* extension depends on it (used_by_plugins);
|
|
801
|
+
* - a dependency install (`plugin`) is uninstalled by these same rules, after the plugin's own
|
|
802
|
+
* components; it is kept when another install outside this uninstall depends on it
|
|
803
|
+
* (used_by_other_install);
|
|
804
|
+
* - components that existed before are kept (pre_existing).
|
|
805
|
+
* When kept for used_by_other_install, ownership passes to the oldest remaining install (its
|
|
806
|
+
* component row becomes created=true), so the uninstall order doesn't change the end result.
|
|
807
|
+
* Uninstalling a dependency another install still uses removes nothing and reports it kept
|
|
808
|
+
* (`plugin`, used_by_other_install). Deletes the install rows and audits `plugin.org_uninstalled`
|
|
809
|
+
* per install (detail has removed / kept). Can be uninstalled: installed; failed (already rolled
|
|
810
|
+
* back: clears the row and leftovers); installing for longer than `catalog.staleInstallMinutes`
|
|
811
|
+
* (a replica crashed midway) - the last two are cleaned up as a rollback (versions added to
|
|
812
|
+
* existing skills are reverted too). Still installing -> `plugin.install_in_progress`.
|
|
813
|
+
*/
|
|
814
|
+
async uninstall(orgId, actor, installId, opts = {}) {
|
|
815
|
+
if (!UUID_RE.test(installId))
|
|
816
|
+
throw installNotFound();
|
|
817
|
+
const report = { removed: [], kept: [] };
|
|
818
|
+
await this.d.db.transaction().execute(async (t) => {
|
|
819
|
+
const outcome = await this.uninstallIn(t, orgId, actor, installId, {
|
|
820
|
+
report,
|
|
821
|
+
removing: new Set(),
|
|
822
|
+
authorize: true,
|
|
823
|
+
opts,
|
|
824
|
+
});
|
|
825
|
+
if (outcome === "missing")
|
|
826
|
+
throw installNotFound();
|
|
827
|
+
if (outcome === "kept") {
|
|
828
|
+
report.kept.push({ kind: "plugin", ref: installId, reason: "used_by_other_install" });
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
for (const r of report.removed) {
|
|
832
|
+
if (r.kind === "extension")
|
|
833
|
+
await this.d.orgConfigs.afterDisable(orgId, r.ref);
|
|
834
|
+
}
|
|
835
|
+
return report;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* The oldest other live install (not in `removing`) that has component `kind` / `ref`, or
|
|
839
|
+
* undefined.
|
|
840
|
+
*/
|
|
841
|
+
async heir(t, orgId, removing, kind, ref) {
|
|
842
|
+
let q = t
|
|
843
|
+
.selectFrom("plugin_install_components as c")
|
|
844
|
+
.innerJoin("plugin_installs as a", "a.id", "c.install_id")
|
|
845
|
+
.select("a.id")
|
|
846
|
+
.where("a.org_id", "=", orgId)
|
|
847
|
+
.where("a.status", "in", ["installing", "installed", "upgrading"])
|
|
848
|
+
.where("c.kind", "=", kind)
|
|
849
|
+
.where("c.ref", "=", ref);
|
|
850
|
+
if (removing.size > 0)
|
|
851
|
+
q = q.where("a.id", "not in", [...removing]);
|
|
852
|
+
const row = await q.orderBy("a.installed_at").orderBy("a.id").executeTakeFirst();
|
|
853
|
+
return row?.id;
|
|
854
|
+
}
|
|
855
|
+
/** Passes ownership of a component to `heir` (its component row becomes created=true). */
|
|
856
|
+
async handOver(t, heir, kind, ref) {
|
|
857
|
+
await t
|
|
858
|
+
.updateTable("plugin_install_components")
|
|
859
|
+
.set({ created: true })
|
|
860
|
+
.where("install_id", "=", heir)
|
|
861
|
+
.where("kind", "=", kind)
|
|
862
|
+
.where("ref", "=", ref)
|
|
863
|
+
.execute();
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Uninstalls one install in the caller's transaction (see {@link uninstall}); dependency installs
|
|
867
|
+
* it created recurse. Its own components' outcomes go into `ctx.report` (and its audit); the
|
|
868
|
+
* outcome of the install itself is returned for the caller to report: `missing`, `kept` (another
|
|
869
|
+
* install outside this uninstall depends on it) or `removed`.
|
|
870
|
+
*/
|
|
871
|
+
async uninstallIn(t, orgId, actor, installId, ctx) {
|
|
872
|
+
const { removing } = ctx;
|
|
873
|
+
if (removing.has(installId))
|
|
874
|
+
return "removed";
|
|
875
|
+
// This install's own slice of the report (its audit), also appended to the shared report
|
|
876
|
+
const mine = { removed: [], kept: [] };
|
|
877
|
+
const addRemoved = (x) => {
|
|
878
|
+
mine.removed.push(x);
|
|
879
|
+
ctx.report.removed.push(x);
|
|
880
|
+
};
|
|
881
|
+
const addKept = (x) => {
|
|
882
|
+
mine.kept.push(x);
|
|
883
|
+
ctx.report.kept.push(x);
|
|
884
|
+
};
|
|
885
|
+
const staleMinutes = this.d.staleInstallMinutes();
|
|
886
|
+
if (ctx.authorize) {
|
|
887
|
+
const target = await t
|
|
888
|
+
.selectFrom("plugin_installs as a")
|
|
889
|
+
.innerJoin("catalog_item_versions as v", (j) => j.onRef("v.item_id", "=", "a.item_id").onRef("v.version", "=", "a.version"))
|
|
890
|
+
.select("v.manifest")
|
|
891
|
+
.where("a.id", "=", installId)
|
|
892
|
+
.where("a.org_id", "=", orgId)
|
|
893
|
+
.executeTakeFirst();
|
|
894
|
+
await lockOrgForActions(t, orgId, actor, target ? requiredActions(target.manifest) : ALL_ACTIONS);
|
|
895
|
+
}
|
|
896
|
+
const row = await t
|
|
897
|
+
.selectFrom("plugin_installs as a")
|
|
898
|
+
.innerJoin("catalog_items as i", "i.id", "a.item_id")
|
|
899
|
+
.select([
|
|
900
|
+
"a.id",
|
|
901
|
+
"a.item_id",
|
|
902
|
+
"a.version",
|
|
903
|
+
"a.status",
|
|
904
|
+
"i.name",
|
|
905
|
+
sql `a.installed_at < now() - make_interval(mins => ${staleMinutes})`.as("stale"),
|
|
906
|
+
sql `coalesce((a.upgrade->>'startedAt')::timestamptz < now() - make_interval(mins => ${staleMinutes}), false)`.as("upgradeStale"),
|
|
907
|
+
"a.upgrade",
|
|
908
|
+
])
|
|
909
|
+
.where("a.id", "=", installId)
|
|
910
|
+
.where("a.org_id", "=", orgId)
|
|
911
|
+
.forUpdate("a")
|
|
912
|
+
.executeTakeFirst();
|
|
913
|
+
if (!row)
|
|
914
|
+
return "missing";
|
|
915
|
+
// Another install outside this uninstall depends on it: kept (a dependency uninstall hands
|
|
916
|
+
// ownership over)
|
|
917
|
+
const user = await this.heir(t, orgId, new Set([...removing, installId]), "plugin", installId);
|
|
918
|
+
if (user !== undefined) {
|
|
919
|
+
if (ctx.from !== undefined)
|
|
920
|
+
await this.handOver(t, user, "plugin", installId);
|
|
921
|
+
return "kept";
|
|
922
|
+
}
|
|
923
|
+
if (row.status === "uninstalling" ||
|
|
924
|
+
(row.status === "installing" && !row.stale) ||
|
|
925
|
+
(row.status === "upgrading" && !row.upgradeStale)) {
|
|
926
|
+
throw new ApiError("plugin.install_in_progress", `This plugin install is still in progress; it can be cleaned up after ${staleMinutes} minutes`);
|
|
927
|
+
}
|
|
928
|
+
if (row.status === "upgrading") {
|
|
929
|
+
// An upgrade interrupted midway (a replica crashed): undone first, back at the old version
|
|
930
|
+
const undone = await this.undoUpgradeIn(t, orgId, actor, installId, journalEntries(row.upgrade), "plugin_upgrade_cleanup");
|
|
931
|
+
for (const x of undone)
|
|
932
|
+
addRemoved(x);
|
|
933
|
+
}
|
|
934
|
+
removing.add(installId);
|
|
935
|
+
// failed (already rolled back) and a stale installing (a replica crashed midway) are cleaned up
|
|
936
|
+
// as a rollback
|
|
937
|
+
const rollbackMode = row.status !== "installed" && row.status !== "upgrading";
|
|
938
|
+
const comps = await t
|
|
939
|
+
.selectFrom("plugin_install_components")
|
|
940
|
+
.select(["kind", "ref", "created", "skill_version"])
|
|
941
|
+
.where("install_id", "=", installId)
|
|
942
|
+
.orderBy("kind")
|
|
943
|
+
.orderBy("ref")
|
|
944
|
+
.execute();
|
|
945
|
+
// Skills and agent templates -> connectors -> extensions (in reverse requires order: dependents before providers)
|
|
946
|
+
// -> dependency installs (created ones first)
|
|
947
|
+
const extensionOrder = this.uninstallExtensionOrder(comps.filter((c) => c.kind === "extension").map((c) => c.ref));
|
|
948
|
+
const rank = (c) => c.kind === "skill" || c.kind === "agent_template"
|
|
949
|
+
? 0
|
|
950
|
+
: c.kind === "connector"
|
|
951
|
+
? 1
|
|
952
|
+
: c.kind === "extension"
|
|
953
|
+
? 2 + extensionOrder.indexOf(c.ref)
|
|
954
|
+
: 1000 + (c.created ? 0 : 1);
|
|
955
|
+
comps.sort((a, b) => rank(a) - rank(b));
|
|
956
|
+
const agentRefs = await this.agentRefs(t, orgId);
|
|
957
|
+
for (const c of comps) {
|
|
958
|
+
const keep = (reason) => addKept({ kind: c.kind, ref: c.ref, reason });
|
|
959
|
+
let created = c.created;
|
|
960
|
+
if (c.kind === "plugin") {
|
|
961
|
+
// Re-read: an earlier dependency uninstall may have handed it over, or removed it
|
|
962
|
+
const now = await t
|
|
963
|
+
.selectFrom("plugin_install_components")
|
|
964
|
+
.select("created")
|
|
965
|
+
.where("install_id", "=", installId)
|
|
966
|
+
.where("kind", "=", "plugin")
|
|
967
|
+
.where("ref", "=", c.ref)
|
|
968
|
+
.executeTakeFirst();
|
|
969
|
+
const live = await t
|
|
970
|
+
.selectFrom("plugin_installs")
|
|
971
|
+
.select("id")
|
|
972
|
+
.where("id", "=", c.ref)
|
|
973
|
+
.where("org_id", "=", orgId)
|
|
974
|
+
.executeTakeFirst();
|
|
975
|
+
if (!now || !live)
|
|
976
|
+
continue;
|
|
977
|
+
created = now.created;
|
|
978
|
+
}
|
|
979
|
+
const comp = { kind: c.kind, ref: c.ref, created, skillVersion: c.skill_version };
|
|
980
|
+
if (!created) {
|
|
981
|
+
// Rollback cleanup: a version this install added to an existing skill is reverted; other
|
|
982
|
+
// pre-existing components are kept
|
|
983
|
+
if (rollbackMode &&
|
|
984
|
+
(c.kind === "skill" || c.kind === "agent_template") &&
|
|
985
|
+
c.skill_version !== null &&
|
|
986
|
+
(await this.removeComponent(t, orgId, actor, comp, "plugin_install_cleanup"))) {
|
|
987
|
+
addRemoved({ kind: c.kind, ref: c.ref });
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
keep("pre_existing");
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (c.kind === "plugin") {
|
|
994
|
+
const outcome = await this.uninstallIn(t, orgId, actor, c.ref, { ...ctx, from: installId });
|
|
995
|
+
if (outcome === "removed")
|
|
996
|
+
addRemoved({ kind: "plugin", ref: c.ref });
|
|
997
|
+
else if (outcome === "kept")
|
|
998
|
+
keep("used_by_other_install");
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const reason = rollbackMode ? "plugin_install_cleanup" : (ctx.reason ?? "plugin_uninstall");
|
|
1002
|
+
const outcome = await this.decideRemovalIn(t, orgId, actor, comp, removing, agentRefs, reason);
|
|
1003
|
+
if (outcome === "removed")
|
|
1004
|
+
addRemoved({ kind: c.kind, ref: c.ref });
|
|
1005
|
+
else if (outcome !== "gone")
|
|
1006
|
+
keep(outcome);
|
|
1007
|
+
}
|
|
1008
|
+
await t.deleteFrom("plugin_installs").where("id", "=", installId).execute();
|
|
1009
|
+
await audit(t, {
|
|
1010
|
+
actorAccountId: actor,
|
|
1011
|
+
...(ctx.opts.actorTokenId !== undefined && { actorTokenId: ctx.opts.actorTokenId }),
|
|
1012
|
+
orgId,
|
|
1013
|
+
action: "plugin.org_uninstalled",
|
|
1014
|
+
target: installId,
|
|
1015
|
+
detail: {
|
|
1016
|
+
itemId: row.item_id,
|
|
1017
|
+
name: row.name,
|
|
1018
|
+
version: row.version,
|
|
1019
|
+
status: row.status,
|
|
1020
|
+
removed: mine.removed,
|
|
1021
|
+
kept: mine.kept,
|
|
1022
|
+
...(ctx.from !== undefined && { dependencyOf: ctx.from }),
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
return "removed";
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* The uninstall rules for one component an install created (a skill, connector or extension;
|
|
1029
|
+
* dependency installs recurse through {@link uninstallIn}), in the caller's transaction: kept
|
|
1030
|
+
* when an agent uses the skill / connector (`used_by_agents`), when another live install outside
|
|
1031
|
+
* `removing` has it (`used_by_other_install`, ownership handed over to the oldest one, so the
|
|
1032
|
+
* uninstall order doesn't change the result), when a connector still uses the extension's
|
|
1033
|
+
* connector kind (`used_by_connectors`) or another enabled extension depends on it
|
|
1034
|
+
* (`used_by_plugins`); otherwise removed (`removed`, or `gone` when it no longer existed). Used
|
|
1035
|
+
* by uninstall and by an upgrade's removals.
|
|
1036
|
+
*/
|
|
1037
|
+
async decideRemovalIn(t, orgId, actor, c, removing, agentRefs, reason) {
|
|
1038
|
+
if (c.kind === "skill" && agentRefs.skills.has(c.ref))
|
|
1039
|
+
return "used_by_agents";
|
|
1040
|
+
if (c.kind === "agent_template" && agentRefs.templates.has(c.ref))
|
|
1041
|
+
return "used_by_agents";
|
|
1042
|
+
if (c.kind === "connector" && agentRefs.connectors.has(c.ref))
|
|
1043
|
+
return "used_by_agents";
|
|
1044
|
+
if (c.kind !== "connector") {
|
|
1045
|
+
const heir = await this.heir(t, orgId, removing, c.kind, c.ref);
|
|
1046
|
+
if (heir !== undefined) {
|
|
1047
|
+
await this.handOver(t, heir, c.kind, c.ref);
|
|
1048
|
+
return "used_by_other_install";
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (c.kind === "extension") {
|
|
1052
|
+
const kinds = this.connectorKindsOf(c.ref);
|
|
1053
|
+
if (kinds.length > 0) {
|
|
1054
|
+
const used = await t
|
|
1055
|
+
.selectFrom("connectors")
|
|
1056
|
+
.select("id")
|
|
1057
|
+
.where("org_id", "=", orgId)
|
|
1058
|
+
.where("deleted_at", "is", null)
|
|
1059
|
+
.where("connector", "in", kinds)
|
|
1060
|
+
.executeTakeFirst();
|
|
1061
|
+
if (used)
|
|
1062
|
+
return "used_by_connectors";
|
|
1063
|
+
}
|
|
1064
|
+
if ((await this.d.orgConfigs.enabledDependents(t, orgId, c.ref)).length > 0) {
|
|
1065
|
+
return "used_by_plugins";
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return (await this.removeComponent(t, orgId, actor, c, reason)) ? "removed" : "gone";
|
|
1069
|
+
}
|
|
1070
|
+
/** The order extensions are uninstalled in: the reverse of the install (requires) order; extensions not loaded on this replica last. */
|
|
1071
|
+
uninstallExtensionOrder(refs) {
|
|
1072
|
+
const loaded = new Map(this.d.host.manifests().map((m) => [m.name, m]));
|
|
1073
|
+
const known = refs.filter((r) => loaded.has(r)).map((r) => loaded.get(r));
|
|
1074
|
+
return [...orderByRequires(known).reverse(), ...refs.filter((r) => !loaded.has(r))];
|
|
1075
|
+
}
|
|
1076
|
+
/** The org's plugin installs (failed ones included), newest first; `installId` picks one. */
|
|
1077
|
+
async list(orgId, installId) {
|
|
1078
|
+
let q = this.d.db
|
|
1079
|
+
.selectFrom("plugin_installs as a")
|
|
1080
|
+
.innerJoin("catalog_items as i", "i.id", "a.item_id")
|
|
1081
|
+
.select([
|
|
1082
|
+
"a.id",
|
|
1083
|
+
"a.item_id",
|
|
1084
|
+
"i.name",
|
|
1085
|
+
"a.version",
|
|
1086
|
+
"a.status",
|
|
1087
|
+
"a.installed_by",
|
|
1088
|
+
"a.installed_at",
|
|
1089
|
+
"a.error",
|
|
1090
|
+
])
|
|
1091
|
+
.where("a.org_id", "=", orgId);
|
|
1092
|
+
if (installId !== undefined)
|
|
1093
|
+
q = q.where("a.id", "=", installId);
|
|
1094
|
+
const rows = await q.orderBy("a.installed_at", "desc").orderBy("a.id").execute();
|
|
1095
|
+
if (rows.length === 0)
|
|
1096
|
+
return [];
|
|
1097
|
+
const info = await upgradeInfo(this.d.db, this.d.host, rows.map((r) => ({ itemId: r.item_id, name: r.name, version: r.version })));
|
|
1098
|
+
const comps = await this.d.db
|
|
1099
|
+
.selectFrom("plugin_install_components")
|
|
1100
|
+
.select(["install_id", "kind", "ref", "created", "skill_version", "dropped"])
|
|
1101
|
+
.where("install_id", "in", rows.map((r) => r.id))
|
|
1102
|
+
.execute();
|
|
1103
|
+
return rows.map((r) => ({
|
|
1104
|
+
id: r.id,
|
|
1105
|
+
itemId: r.item_id,
|
|
1106
|
+
name: r.name,
|
|
1107
|
+
version: r.version,
|
|
1108
|
+
status: r.status,
|
|
1109
|
+
installedBy: r.installed_by,
|
|
1110
|
+
installedAt: r.installed_at.toISOString(),
|
|
1111
|
+
error: r.error ?? null,
|
|
1112
|
+
components: comps
|
|
1113
|
+
.filter((c) => c.install_id === r.id)
|
|
1114
|
+
.map((c) => ({
|
|
1115
|
+
kind: c.kind,
|
|
1116
|
+
ref: c.ref,
|
|
1117
|
+
created: c.created,
|
|
1118
|
+
skillVersion: c.skill_version,
|
|
1119
|
+
dropped: c.dropped,
|
|
1120
|
+
})),
|
|
1121
|
+
upgradeAvailable: info.get(`${r.item_id}@${r.version}`)?.upgradeAvailable ?? null,
|
|
1122
|
+
deploymentPending: info.get(`${r.item_id}@${r.version}`)?.deploymentPending ?? false,
|
|
1123
|
+
}));
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Undoes the steps of an upgrade in the caller's transaction (the caller holds the org row lock),
|
|
1127
|
+
* in reverse order: components it created are removed (a dependency install by the uninstall
|
|
1128
|
+
* rules, an extension another enabled extension depends on is kept), the ledger rows it added
|
|
1129
|
+
* are deleted, and versions it added to skills are reverted (`revertVersionIn` reverts exactly
|
|
1130
|
+
* one version, and an upgrade adds at most one per skill). Returns the components removed (an
|
|
1131
|
+
* extension among them needs `afterDisable` once committed). Used when an upgrade fails and when
|
|
1132
|
+
* an interrupted one is cleaned up.
|
|
1133
|
+
*/
|
|
1134
|
+
async undoUpgradeIn(t, orgId, actor, installId, steps, reason) {
|
|
1135
|
+
const removed = [];
|
|
1136
|
+
const dropRow = (kind, ref) => t
|
|
1137
|
+
.deleteFrom("plugin_install_components")
|
|
1138
|
+
.where("install_id", "=", installId)
|
|
1139
|
+
.where("kind", "=", kind)
|
|
1140
|
+
.where("ref", "=", ref)
|
|
1141
|
+
.execute();
|
|
1142
|
+
for (const s of [...steps].reverse()) {
|
|
1143
|
+
if (s.op === "skill_version") {
|
|
1144
|
+
await this.d.skills.revertVersionIn(t, orgId, s.ref, s.version, actor, reason);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (s.op === "template_version") {
|
|
1148
|
+
await revertTemplateVersionIn(t, orgId, s.ref, s.version);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (s.op === "recorded") {
|
|
1152
|
+
await dropRow(s.kind, s.ref);
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
if (s.kind === "plugin") {
|
|
1156
|
+
const sub = { removed: [], kept: [] };
|
|
1157
|
+
await dropRow(s.kind, s.ref);
|
|
1158
|
+
const outcome = await this.uninstallIn(t, orgId, actor, s.ref, {
|
|
1159
|
+
report: sub,
|
|
1160
|
+
removing: new Set([installId]),
|
|
1161
|
+
from: installId,
|
|
1162
|
+
authorize: false,
|
|
1163
|
+
reason,
|
|
1164
|
+
opts: {},
|
|
1165
|
+
});
|
|
1166
|
+
removed.push(...sub.removed.filter((r) => r.kind === "extension"));
|
|
1167
|
+
if (outcome === "removed")
|
|
1168
|
+
removed.push({ kind: "plugin", ref: s.ref });
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
if (s.kind === "extension" &&
|
|
1172
|
+
(await this.d.orgConfigs.enabledDependents(t, orgId, s.ref)).length > 0) {
|
|
1173
|
+
// Another enabled extension needs it: kept, and stays this install's component
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const c = { kind: s.kind, ref: s.ref, created: true, skillVersion: null };
|
|
1177
|
+
if (await this.removeComponent(t, orgId, actor, c, reason)) {
|
|
1178
|
+
removed.push({ kind: s.kind, ref: s.ref });
|
|
1179
|
+
}
|
|
1180
|
+
await dropRow(s.kind, s.ref);
|
|
1181
|
+
}
|
|
1182
|
+
return removed;
|
|
1183
|
+
}
|
|
1184
|
+
/** The connector kinds an extension provides (the `connector@1` implementations loaded on this replica). */
|
|
1185
|
+
connectorKindsOf(plugin) {
|
|
1186
|
+
return this.d.host
|
|
1187
|
+
.list("connector")
|
|
1188
|
+
.filter((e) => e.plugin === plugin)
|
|
1189
|
+
.map((e) => e.impl?.kind)
|
|
1190
|
+
.filter((k) => typeof k === "string");
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* The connectors, org skills and agent templates (`subagents.templates`) referenced by the
|
|
1194
|
+
* current definitions of the org's live agents.
|
|
1195
|
+
*/
|
|
1196
|
+
async agentRefs(t, orgId) {
|
|
1197
|
+
const rows = await t
|
|
1198
|
+
.selectFrom("agents as a")
|
|
1199
|
+
.innerJoin("agent_versions as v", (j) => j.onRef("v.agent_id", "=", "a.id").onRef("v.version", "=", "a.current_version"))
|
|
1200
|
+
.select("v.definition")
|
|
1201
|
+
.where("a.org_id", "=", orgId)
|
|
1202
|
+
.where("a.deleted_at", "is", null)
|
|
1203
|
+
.execute();
|
|
1204
|
+
const connectors = new Set();
|
|
1205
|
+
const skills = new Set();
|
|
1206
|
+
const templates = new Set();
|
|
1207
|
+
for (const r of rows) {
|
|
1208
|
+
const def = r.definition;
|
|
1209
|
+
for (const ref of def?.tools ?? []) {
|
|
1210
|
+
const n = normalizeToolRef(ref);
|
|
1211
|
+
if (n?.kind === "connector" && typeof n.connectorId === "string") {
|
|
1212
|
+
connectors.add(n.connectorId);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
for (const s of def?.skills?.orgSkills ?? [])
|
|
1216
|
+
if (typeof s === "string")
|
|
1217
|
+
skills.add(s);
|
|
1218
|
+
for (const x of def?.subagents?.templates ?? [])
|
|
1219
|
+
if (typeof x === "string")
|
|
1220
|
+
templates.add(x);
|
|
1221
|
+
}
|
|
1222
|
+
return { connectors, skills, templates };
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The skill names of a catalog version, read from each skill's SKILL.md in storage (the names are
|
|
1227
|
+
* not in plugin.json). Used by install planning and the upgrade preview.
|
|
1228
|
+
*/
|
|
1229
|
+
export async function catalogSkillNames(storage, row, manifest) {
|
|
1230
|
+
const out = [];
|
|
1231
|
+
for (const s of manifest.skills ?? []) {
|
|
1232
|
+
const dir = packagePath(s.path);
|
|
1233
|
+
const entry = `${dir}/${SKILL_ENTRY_FILE}`;
|
|
1234
|
+
const files = await readCatalogFiles(storage, row.object_prefix, row.files.filter((f) => f.path === entry), dir);
|
|
1235
|
+
const file = files[0];
|
|
1236
|
+
const parsed = file
|
|
1237
|
+
? parseSkillMd(new TextDecoder().decode(file.data), { invocationKeys: false })
|
|
1238
|
+
: undefined;
|
|
1239
|
+
if (!parsed || "errors" in parsed) {
|
|
1240
|
+
throw new ApiError("internal", `Stored skill ${dir} has no valid ${SKILL_ENTRY_FILE}`);
|
|
1241
|
+
}
|
|
1242
|
+
out.push(parsed.name);
|
|
1243
|
+
}
|
|
1244
|
+
return out;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* The skill conflicts of a plan, depth first in install order (dependencies before the plugin).
|
|
1248
|
+
* Exported for the upgrade preview.
|
|
1249
|
+
*/
|
|
1250
|
+
export async function planSkillConflicts(db, orgId, plan) {
|
|
1251
|
+
const out = [];
|
|
1252
|
+
const seen = new Set();
|
|
1253
|
+
/** Skill name -> the plugin of this plan that brings it first. */
|
|
1254
|
+
const planned = new Map();
|
|
1255
|
+
const visit = async (p) => {
|
|
1256
|
+
for (const d of p.dependencies)
|
|
1257
|
+
if (d.plan)
|
|
1258
|
+
await visit(d.plan);
|
|
1259
|
+
for (const c of await skillConflicts(db, orgId, p.itemId, p.skillNames)) {
|
|
1260
|
+
if (!seen.has(c.skill))
|
|
1261
|
+
out.push(c);
|
|
1262
|
+
seen.add(c.skill);
|
|
1263
|
+
}
|
|
1264
|
+
for (const skill of p.skillNames) {
|
|
1265
|
+
const first = planned.get(skill);
|
|
1266
|
+
if (first === undefined)
|
|
1267
|
+
planned.set(skill, p);
|
|
1268
|
+
else if (first.itemId !== p.itemId && !seen.has(skill)) {
|
|
1269
|
+
out.push({ skill, source: { kind: "plugin", name: first.name, version: first.version } });
|
|
1270
|
+
seen.add(skill);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
await visit(plan);
|
|
1275
|
+
return out;
|
|
1276
|
+
}
|
|
1277
|
+
const COMPONENT_KINDS = new Set([
|
|
1278
|
+
"extension",
|
|
1279
|
+
"connector",
|
|
1280
|
+
"skill",
|
|
1281
|
+
"plugin",
|
|
1282
|
+
"agent_template",
|
|
1283
|
+
]);
|
|
1284
|
+
/** The journaled steps of `plugin_installs.upgrade` (malformed entries are skipped). */
|
|
1285
|
+
export function journalEntries(upgrade) {
|
|
1286
|
+
const done = upgrade?.done;
|
|
1287
|
+
if (!Array.isArray(done))
|
|
1288
|
+
return [];
|
|
1289
|
+
const out = [];
|
|
1290
|
+
for (const e of done) {
|
|
1291
|
+
if (typeof e !== "object" || e === null || typeof e.ref !== "string")
|
|
1292
|
+
continue;
|
|
1293
|
+
if ((e.op === "skill_version" || e.op === "template_version") &&
|
|
1294
|
+
typeof e.version === "number") {
|
|
1295
|
+
out.push({ op: e.op, ref: e.ref, version: e.version });
|
|
1296
|
+
}
|
|
1297
|
+
else if ((e.op === "created" || e.op === "recorded") &&
|
|
1298
|
+
typeof e.kind === "string" &&
|
|
1299
|
+
COMPONENT_KINDS.has(e.kind)) {
|
|
1300
|
+
out.push({ op: e.op, kind: e.kind, ref: e.ref });
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
/** The org actions a plan needs: the union over the plugins it installs (spec §10). */
|
|
1306
|
+
export function planActions(plan) {
|
|
1307
|
+
const out = new Set(requiredActions(plan.manifest));
|
|
1308
|
+
for (const d of plan.dependencies) {
|
|
1309
|
+
if (d.plan)
|
|
1310
|
+
for (const a of planActions(d.plan))
|
|
1311
|
+
out.add(a);
|
|
1312
|
+
}
|
|
1313
|
+
return [...out];
|
|
1314
|
+
}
|
|
1315
|
+
const alreadyInstalled = () => new ApiError("plugin.already_installed", "This plugin is already installed in this organization; uninstall it before installing again");
|
|
1316
|
+
/**
|
|
1317
|
+
* Orders extensions by `requires`: when an org-scoped capability an extension needs is provided by
|
|
1318
|
+
* another extension in the set, the provider comes first; a cycle keeps the given order.
|
|
1319
|
+
*/
|
|
1320
|
+
function orderByRequires(set) {
|
|
1321
|
+
const orgScoped = (ref) => EXTENSION_POINTS[parseExtensionPoint(ref).name]?.orgScoped === true;
|
|
1322
|
+
const placed = [];
|
|
1323
|
+
const rest = [...set];
|
|
1324
|
+
while (rest.length > 0) {
|
|
1325
|
+
const idx = rest.findIndex((m) => (m.requires ?? []).filter(orgScoped).every((ref) => {
|
|
1326
|
+
const providers = rest.filter((o) => o.name !== m.name && o.provides.includes(ref));
|
|
1327
|
+
return providers.length === 0;
|
|
1328
|
+
}));
|
|
1329
|
+
const [next] = rest.splice(idx === -1 ? 0 : idx, 1);
|
|
1330
|
+
placed.push(next.name);
|
|
1331
|
+
}
|
|
1332
|
+
return placed;
|
|
1333
|
+
}
|