@capybara-hq/kernel 0.0.0-stage → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +20 -2
- package/bootstrap.json +26 -0
- package/dist/admin/admin.controller.d.ts +139 -0
- package/dist/admin/admin.controller.js +238 -0
- package/dist/admin/extensions.controller.d.ts +100 -0
- package/dist/admin/extensions.controller.js +226 -0
- package/dist/admin/setup.controller.d.ts +36 -0
- package/dist/admin/setup.controller.js +106 -0
- package/dist/admin/setup.service.d.ts +47 -0
- package/dist/admin/setup.service.js +90 -0
- package/dist/agents/agents.controller.d.ts +205 -0
- package/dist/agents/agents.controller.js +144 -0
- package/dist/agents/agents.service.d.ts +177 -0
- package/dist/agents/agents.service.js +677 -0
- package/dist/agents/definition.d.ts +198 -0
- package/dist/agents/definition.js +152 -0
- package/dist/agents/models.controller.d.ts +13 -0
- package/dist/agents/models.controller.js +31 -0
- package/dist/agents/templates.controller.d.ts +15 -0
- package/dist/agents/templates.controller.js +37 -0
- package/dist/agents/templates.d.ts +83 -0
- package/dist/agents/templates.js +173 -0
- package/dist/catalog/admin-catalog.controller.d.ts +143 -0
- package/dist/catalog/admin-catalog.controller.js +245 -0
- package/dist/catalog/catalog.service.d.ts +430 -0
- package/dist/catalog/catalog.service.js +882 -0
- package/dist/catalog/claude/agents.d.ts +27 -0
- package/dist/catalog/claude/agents.js +270 -0
- package/dist/catalog/claude/commands.d.ts +50 -0
- package/dist/catalog/claude/commands.js +179 -0
- package/dist/catalog/claude/manifest.d.ts +69 -0
- package/dist/catalog/claude/manifest.js +134 -0
- package/dist/catalog/claude/marketplace.d.ts +35 -0
- package/dist/catalog/claude/marketplace.js +194 -0
- package/dist/catalog/claude/mcp.d.ts +33 -0
- package/dist/catalog/claude/mcp.js +226 -0
- package/dist/catalog/claude/report.d.ts +86 -0
- package/dist/catalog/claude/report.js +121 -0
- package/dist/catalog/claude/translate.d.ts +56 -0
- package/dist/catalog/claude/translate.js +857 -0
- package/dist/catalog/cli.d.ts +46 -0
- package/dist/catalog/cli.js +211 -0
- package/dist/catalog/git-source.d.ts +127 -0
- package/dist/catalog/git-source.js +520 -0
- package/dist/catalog/marketplace-fetch.d.ts +99 -0
- package/dist/catalog/marketplace-fetch.js +203 -0
- package/dist/catalog/marketplaces.controller.d.ts +97 -0
- package/dist/catalog/marketplaces.controller.js +197 -0
- package/dist/catalog/marketplaces.service.d.ts +340 -0
- package/dist/catalog/marketplaces.service.js +1217 -0
- package/dist/catalog/org-catalog.controller.d.ts +128 -0
- package/dist/catalog/org-catalog.controller.js +277 -0
- package/dist/catalog/org-catalog.service.d.ts +246 -0
- package/dist/catalog/org-catalog.service.js +625 -0
- package/dist/catalog/plugin-import.d.ts +436 -0
- package/dist/catalog/plugin-import.js +1069 -0
- package/dist/catalog/plugin-install.d.ts +447 -0
- package/dist/catalog/plugin-install.js +1333 -0
- package/dist/catalog/plugin-package.d.ts +143 -0
- package/dist/catalog/plugin-package.js +382 -0
- package/dist/catalog/plugin-upgrade.d.ts +222 -0
- package/dist/catalog/plugin-upgrade.js +1219 -0
- package/dist/catalog/requests.service.d.ts +162 -0
- package/dist/catalog/requests.service.js +504 -0
- package/dist/catalog/schema-path.d.ts +7 -0
- package/dist/catalog/schema-path.js +63 -0
- package/dist/catalog/upgrade-targets.d.ts +59 -0
- package/dist/catalog/upgrade-targets.js +90 -0
- package/dist/catalog/versions.d.ts +31 -0
- package/dist/catalog/versions.js +44 -0
- package/dist/channels/actions.d.ts +37 -0
- package/dist/channels/actions.js +121 -0
- package/dist/channels/cards.d.ts +44 -0
- package/dist/channels/cards.js +156 -0
- package/dist/channels/connections.controller.d.ts +59 -0
- package/dist/channels/connections.controller.js +115 -0
- package/dist/channels/connections.service.d.ts +187 -0
- package/dist/channels/connections.service.js +534 -0
- package/dist/channels/features.d.ts +12 -0
- package/dist/channels/features.js +22 -0
- package/dist/channels/inbound.d.ts +120 -0
- package/dist/channels/inbound.js +332 -0
- package/dist/channels/outbound.d.ts +140 -0
- package/dist/channels/outbound.js +1126 -0
- package/dist/channels/phrases.d.ts +100 -0
- package/dist/channels/phrases.js +81 -0
- package/dist/channels/supervisor.d.ts +182 -0
- package/dist/channels/supervisor.js +625 -0
- package/dist/channels/threads.d.ts +29 -0
- package/dist/channels/threads.js +104 -0
- package/dist/channels/webhook-url.d.ts +16 -0
- package/dist/channels/webhook-url.js +34 -0
- package/dist/channels/webhook.controller.d.ts +19 -0
- package/dist/channels/webhook.controller.js +72 -0
- package/dist/channels/webhook.d.ts +73 -0
- package/dist/channels/webhook.js +274 -0
- package/dist/config/docs.d.ts +9 -0
- package/dist/config/docs.js +85 -0
- package/dist/config/env.d.ts +6 -0
- package/dist/config/env.js +73 -0
- package/dist/config/fields.d.ts +42 -0
- package/dist/config/fields.js +214 -0
- package/dist/config/file-source.d.ts +3 -0
- package/dist/config/file-source.js +31 -0
- package/dist/config/load.d.ts +20 -0
- package/dist/config/load.js +98 -0
- package/dist/config/nacos-source.d.ts +31 -0
- package/dist/config/nacos-source.js +229 -0
- package/dist/config/schema.d.ts +237 -0
- package/dist/config/schema.js +1192 -0
- package/dist/config/sources.d.ts +18 -0
- package/dist/config/sources.js +59 -0
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.js +7 -0
- package/dist/conversations/artifacts.d.ts +52 -0
- package/dist/conversations/artifacts.js +137 -0
- package/dist/conversations/attachments.controller.d.ts +28 -0
- package/dist/conversations/attachments.controller.js +96 -0
- package/dist/conversations/attachments.service.d.ts +180 -0
- package/dist/conversations/attachments.service.js +578 -0
- package/dist/conversations/conversations.controller.d.ts +288 -0
- package/dist/conversations/conversations.controller.js +607 -0
- package/dist/conversations/conversations.service.d.ts +321 -0
- package/dist/conversations/conversations.service.js +754 -0
- package/dist/conversations/event-log.d.ts +122 -0
- package/dist/conversations/event-log.js +259 -0
- package/dist/conversations/events.d.ts +856 -0
- package/dist/conversations/events.js +639 -0
- package/dist/conversations/files.d.ts +92 -0
- package/dist/conversations/files.js +213 -0
- package/dist/conversations/outputs-read.d.ts +34 -0
- package/dist/conversations/outputs-read.js +37 -0
- package/dist/conversations/queue.d.ts +169 -0
- package/dist/conversations/queue.js +527 -0
- package/dist/conversations/sandbox-status.d.ts +21 -0
- package/dist/conversations/sandbox-status.js +24 -0
- package/dist/conversations/selection.d.ts +46 -0
- package/dist/conversations/selection.js +75 -0
- package/dist/conversations/titles.d.ts +93 -0
- package/dist/conversations/titles.js +423 -0
- package/dist/ctx/bus.d.ts +16 -0
- package/dist/ctx/bus.js +101 -0
- package/dist/ctx/create-ctx.d.ts +109 -0
- package/dist/ctx/create-ctx.js +592 -0
- package/dist/ctx/kv.d.ts +4 -0
- package/dist/ctx/kv.js +31 -0
- package/dist/ctx/lock.d.ts +4 -0
- package/dist/ctx/lock.js +39 -0
- package/dist/ctx/namespace.d.ts +6 -0
- package/dist/ctx/namespace.js +12 -0
- package/dist/ctx/queue.d.ts +25 -0
- package/dist/ctx/queue.js +151 -0
- package/dist/ctx/secrets.d.ts +45 -0
- package/dist/ctx/secrets.js +175 -0
- package/dist/db/kernel-schema.d.ts +1122 -0
- package/dist/db/kernel-schema.js +5 -0
- package/dist/db/migrator.d.ts +28 -0
- package/dist/db/migrator.js +140 -0
- package/dist/db/pool.d.ts +21 -0
- package/dist/db/pool.js +28 -0
- package/dist/db/sql-statements.d.ts +34 -0
- package/dist/db/sql-statements.js +168 -0
- package/dist/errors.d.ts +211 -0
- package/dist/errors.js +213 -0
- package/dist/evals/budget.d.ts +61 -0
- package/dist/evals/budget.js +181 -0
- package/dist/evals/diff.d.ts +55 -0
- package/dist/evals/diff.js +133 -0
- package/dist/evals/judge.d.ts +53 -0
- package/dist/evals/judge.js +149 -0
- package/dist/evals/plan.d.ts +47 -0
- package/dist/evals/plan.js +77 -0
- package/dist/evals/reconstruct.d.ts +137 -0
- package/dist/evals/reconstruct.js +322 -0
- package/dist/evals/recorded-tools.d.ts +54 -0
- package/dist/evals/recorded-tools.js +167 -0
- package/dist/evals/run-replay.d.ts +86 -0
- package/dist/evals/run-replay.js +502 -0
- package/dist/evals/runs.controller.d.ts +100 -0
- package/dist/evals/runs.controller.js +199 -0
- package/dist/evals/runs.service.d.ts +275 -0
- package/dist/evals/runs.service.js +836 -0
- package/dist/evals/sets.controller.d.ts +61 -0
- package/dist/evals/sets.controller.js +183 -0
- package/dist/evals/sets.service.d.ts +128 -0
- package/dist/evals/sets.service.js +558 -0
- package/dist/evals/snapshot.d.ts +76 -0
- package/dist/evals/snapshot.js +207 -0
- package/dist/evals/step-replay.d.ts +113 -0
- package/dist/evals/step-replay.js +181 -0
- package/dist/evals/trajectory.controller.d.ts +41 -0
- package/dist/evals/trajectory.controller.js +88 -0
- package/dist/evals/trajectory.d.ts +144 -0
- package/dist/evals/trajectory.js +338 -0
- package/dist/evals/trajectory.service.d.ts +108 -0
- package/dist/evals/trajectory.service.js +323 -0
- package/dist/evals/worker.d.ts +101 -0
- package/dist/evals/worker.js +790 -0
- package/dist/extensions/admin-ops.d.ts +105 -0
- package/dist/extensions/admin-ops.js +353 -0
- package/dist/extensions/admin-view.d.ts +79 -0
- package/dist/extensions/admin-view.js +327 -0
- package/dist/extensions/approvals.d.ts +119 -0
- package/dist/extensions/approvals.js +205 -0
- package/dist/extensions/blobs.d.ts +45 -0
- package/dist/extensions/blobs.js +160 -0
- package/dist/extensions/bootstrap.d.ts +176 -0
- package/dist/extensions/bootstrap.js +535 -0
- package/dist/extensions/cli.d.ts +61 -0
- package/dist/extensions/cli.js +165 -0
- package/dist/extensions/config-json-schema.d.ts +9 -0
- package/dist/extensions/config-json-schema.js +73 -0
- package/dist/extensions/deployment-config.d.ts +138 -0
- package/dist/extensions/deployment-config.js +394 -0
- package/dist/extensions/enabled.d.ts +31 -0
- package/dist/extensions/enabled.js +55 -0
- package/dist/extensions/hooks.d.ts +161 -0
- package/dist/extensions/hooks.js +425 -0
- package/dist/extensions/host.d.ts +577 -0
- package/dist/extensions/host.js +2071 -0
- package/dist/extensions/inflight.d.ts +69 -0
- package/dist/extensions/inflight.js +277 -0
- package/dist/extensions/installs.d.ts +166 -0
- package/dist/extensions/installs.js +253 -0
- package/dist/extensions/instance.d.ts +167 -0
- package/dist/extensions/instance.js +329 -0
- package/dist/extensions/ledger.d.ts +30 -0
- package/dist/extensions/ledger.js +50 -0
- package/dist/extensions/lifecycle.d.ts +80 -0
- package/dist/extensions/lifecycle.js +152 -0
- package/dist/extensions/migrate-lock.d.ts +51 -0
- package/dist/extensions/migrate-lock.js +100 -0
- package/dist/extensions/org-config.controller.d.ts +24 -0
- package/dist/extensions/org-config.controller.js +59 -0
- package/dist/extensions/org-config.d.ts +169 -0
- package/dist/extensions/org-config.js +520 -0
- package/dist/extensions/package.d.ts +115 -0
- package/dist/extensions/package.js +246 -0
- package/dist/extensions/permission-set.d.ts +117 -0
- package/dist/extensions/permission-set.js +184 -0
- package/dist/extensions/permission-view.d.ts +194 -0
- package/dist/extensions/permission-view.js +284 -0
- package/dist/extensions/registry.d.ts +24 -0
- package/dist/extensions/registry.js +56 -0
- package/dist/extensions/resolve.d.ts +12 -0
- package/dist/extensions/resolve.js +71 -0
- package/dist/extensions/sources.d.ts +69 -0
- package/dist/extensions/sources.js +120 -0
- package/dist/extensions/sync.d.ts +40 -0
- package/dist/extensions/sync.js +242 -0
- package/dist/files/previewers.controller.d.ts +46 -0
- package/dist/files/previewers.controller.js +105 -0
- package/dist/files/previewers.d.ts +100 -0
- package/dist/files/previewers.js +250 -0
- package/dist/http/api.module.d.ts +114 -0
- package/dist/http/api.module.js +448 -0
- package/dist/http/app.d.ts +77 -0
- package/dist/http/app.js +220 -0
- package/dist/http/error.filter.d.ts +21 -0
- package/dist/http/error.filter.js +85 -0
- package/dist/http/gateway.module.d.ts +15 -0
- package/dist/http/gateway.module.js +13 -0
- package/dist/http/health.controller.d.ts +26 -0
- package/dist/http/health.controller.js +79 -0
- package/dist/http/nest-logger.d.ts +20 -0
- package/dist/http/nest-logger.js +28 -0
- package/dist/http/plugin-dispatch.d.ts +68 -0
- package/dist/http/plugin-dispatch.js +200 -0
- package/dist/http/tokens.d.ts +131 -0
- package/dist/http/tokens.js +131 -0
- package/dist/http/zod.pipe.d.ts +8 -0
- package/dist/http/zod.pipe.js +14 -0
- package/dist/identity/accounts.d.ts +93 -0
- package/dist/identity/accounts.js +367 -0
- package/dist/identity/api-tokens.d.ts +73 -0
- package/dist/identity/api-tokens.js +197 -0
- package/dist/identity/audit.d.ts +23 -0
- package/dist/identity/audit.js +41 -0
- package/dist/identity/auth.controller.d.ts +40 -0
- package/dist/identity/auth.controller.js +130 -0
- package/dist/identity/auth.guard.d.ts +41 -0
- package/dist/identity/auth.guard.js +162 -0
- package/dist/identity/binding.d.ts +47 -0
- package/dist/identity/binding.js +84 -0
- package/dist/identity/cookies.d.ts +16 -0
- package/dist/identity/cookies.js +32 -0
- package/dist/identity/credential-check.d.ts +17 -0
- package/dist/identity/credential-check.js +24 -0
- package/dist/identity/enterprise-admin.d.ts +14 -0
- package/dist/identity/enterprise-admin.js +27 -0
- package/dist/identity/external.d.ts +106 -0
- package/dist/identity/external.js +337 -0
- package/dist/identity/hash.d.ts +2 -0
- package/dist/identity/hash.js +3 -0
- package/dist/identity/login.d.ts +6 -0
- package/dist/identity/login.js +20 -0
- package/dist/identity/me.controller.d.ts +61 -0
- package/dist/identity/me.controller.js +159 -0
- package/dist/identity/passwords.d.ts +6 -0
- package/dist/identity/passwords.js +37 -0
- package/dist/identity/principal.d.ts +56 -0
- package/dist/identity/principal.js +38 -0
- package/dist/identity/rate-limit.d.ts +55 -0
- package/dist/identity/rate-limit.js +121 -0
- package/dist/identity/sessions.d.ts +84 -0
- package/dist/identity/sessions.js +306 -0
- package/dist/identity/settings.d.ts +121 -0
- package/dist/identity/settings.js +248 -0
- package/dist/identity/sso.controller.d.ts +59 -0
- package/dist/identity/sso.controller.js +192 -0
- package/dist/identity/sso.d.ts +83 -0
- package/dist/identity/sso.js +202 -0
- package/dist/inbox/inbox.service.d.ts +43 -0
- package/dist/inbox/inbox.service.js +57 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +67 -0
- package/dist/kernel.d.ts +259 -0
- package/dist/kernel.js +1326 -0
- package/dist/log-config.d.ts +9 -0
- package/dist/log-config.js +27 -0
- package/dist/log-context.d.ts +32 -0
- package/dist/log-context.js +40 -0
- package/dist/log-loki.d.ts +34 -0
- package/dist/log-loki.js +145 -0
- package/dist/log.d.ts +79 -0
- package/dist/log.js +303 -0
- package/dist/models/prices.d.ts +53 -0
- package/dist/models/prices.js +121 -0
- package/dist/models/purposes.controller.d.ts +19 -0
- package/dist/models/purposes.controller.js +56 -0
- package/dist/models/purposes.d.ts +140 -0
- package/dist/models/purposes.js +267 -0
- package/dist/net/proxy.d.ts +44 -0
- package/dist/net/proxy.js +162 -0
- package/dist/net/public-address.d.ts +60 -0
- package/dist/net/public-address.js +272 -0
- package/dist/net/safe-fetch.d.ts +38 -0
- package/dist/net/safe-fetch.js +140 -0
- package/dist/orgs/directory-sync.controller.d.ts +14 -0
- package/dist/orgs/directory-sync.controller.js +40 -0
- package/dist/orgs/directory-sync.d.ts +166 -0
- package/dist/orgs/directory-sync.js +558 -0
- package/dist/orgs/invitations.controller.d.ts +58 -0
- package/dist/orgs/invitations.controller.js +137 -0
- package/dist/orgs/invitations.service.d.ts +80 -0
- package/dist/orgs/invitations.service.js +217 -0
- package/dist/orgs/members.service.d.ts +63 -0
- package/dist/orgs/members.service.js +234 -0
- package/dist/orgs/offboarding.d.ts +141 -0
- package/dist/orgs/offboarding.js +398 -0
- package/dist/orgs/org-lock.d.ts +32 -0
- package/dist/orgs/org-lock.js +46 -0
- package/dist/orgs/org.guard.d.ts +35 -0
- package/dist/orgs/org.guard.js +95 -0
- package/dist/orgs/orgs.controller.d.ts +78 -0
- package/dist/orgs/orgs.controller.js +192 -0
- package/dist/orgs/orgs.service.d.ts +61 -0
- package/dist/orgs/orgs.service.js +209 -0
- package/dist/orgs/roles.d.ts +13 -0
- package/dist/orgs/roles.js +58 -0
- package/dist/orgs/settings.controller.d.ts +20 -0
- package/dist/orgs/settings.controller.js +53 -0
- package/dist/orgs/settings.service.d.ts +120 -0
- package/dist/orgs/settings.service.js +178 -0
- package/dist/packages/npm.d.ts +50 -0
- package/dist/packages/npm.js +218 -0
- package/dist/packages/provenance.d.ts +47 -0
- package/dist/packages/provenance.js +147 -0
- package/dist/packages/tgz.d.ts +76 -0
- package/dist/packages/tgz.js +225 -0
- package/dist/runtime/abort.d.ts +32 -0
- package/dist/runtime/abort.js +71 -0
- package/dist/runtime/checkpointer.d.ts +32 -0
- package/dist/runtime/checkpointer.js +243 -0
- package/dist/runtime/context/build.d.ts +254 -0
- package/dist/runtime/context/build.js +693 -0
- package/dist/runtime/context/claude-tools.d.ts +11 -0
- package/dist/runtime/context/claude-tools.js +24 -0
- package/dist/runtime/context/compaction.d.ts +26 -0
- package/dist/runtime/context/compaction.js +195 -0
- package/dist/runtime/context/pairing.d.ts +11 -0
- package/dist/runtime/context/pairing.js +82 -0
- package/dist/runtime/context/reminders.d.ts +5 -0
- package/dist/runtime/context/reminders.js +11 -0
- package/dist/runtime/context/skills.d.ts +105 -0
- package/dist/runtime/context/skills.js +283 -0
- package/dist/runtime/context/tokens.d.ts +19 -0
- package/dist/runtime/context/tokens.js +104 -0
- package/dist/runtime/conversation-lock.d.ts +40 -0
- package/dist/runtime/conversation-lock.js +97 -0
- package/dist/runtime/hook-calls.d.ts +84 -0
- package/dist/runtime/hook-calls.js +136 -0
- package/dist/runtime/langgraph-loop.d.ts +52 -0
- package/dist/runtime/langgraph-loop.js +247 -0
- package/dist/runtime/loop.d.ts +93 -0
- package/dist/runtime/loop.js +7 -0
- package/dist/runtime/maintenance.d.ts +80 -0
- package/dist/runtime/maintenance.js +148 -0
- package/dist/runtime/media-cache.d.ts +21 -0
- package/dist/runtime/media-cache.js +51 -0
- package/dist/runtime/model-call.d.ts +176 -0
- package/dist/runtime/model-call.js +1049 -0
- package/dist/runtime/providers.d.ts +86 -0
- package/dist/runtime/providers.js +224 -0
- package/dist/runtime/quota.d.ts +29 -0
- package/dist/runtime/quota.js +70 -0
- package/dist/runtime/run-finish.d.ts +45 -0
- package/dist/runtime/run-finish.js +69 -0
- package/dist/runtime/run-queue.d.ts +121 -0
- package/dist/runtime/run-queue.js +493 -0
- package/dist/runtime/run-worker.d.ts +138 -0
- package/dist/runtime/run-worker.js +806 -0
- package/dist/runtime/stream.controller.d.ts +34 -0
- package/dist/runtime/stream.controller.js +122 -0
- package/dist/runtime/stream.d.ts +63 -0
- package/dist/runtime/stream.js +262 -0
- package/dist/runtime/subagents/child.d.ts +80 -0
- package/dist/runtime/subagents/child.js +225 -0
- package/dist/runtime/subagents/definition.d.ts +103 -0
- package/dist/runtime/subagents/definition.js +392 -0
- package/dist/runtime/subagents/dispatch.d.ts +69 -0
- package/dist/runtime/subagents/dispatch.js +502 -0
- package/dist/runtime/subagents/limits.d.ts +23 -0
- package/dist/runtime/subagents/limits.js +34 -0
- package/dist/runtime/subagents/org-agents.d.ts +50 -0
- package/dist/runtime/subagents/org-agents.js +70 -0
- package/dist/runtime/subagents/preamble.d.ts +7 -0
- package/dist/runtime/subagents/preamble.js +11 -0
- package/dist/runtime/subagents/progress.d.ts +26 -0
- package/dist/runtime/subagents/progress.js +36 -0
- package/dist/runtime/subagents/roster.d.ts +55 -0
- package/dist/runtime/subagents/roster.js +111 -0
- package/dist/runtime/subagents/skill-fork.d.ts +123 -0
- package/dist/runtime/subagents/skill-fork.js +259 -0
- package/dist/runtime/usage-report.d.ts +163 -0
- package/dist/runtime/usage-report.js +551 -0
- package/dist/runtime/waits.d.ts +233 -0
- package/dist/runtime/waits.js +883 -0
- package/dist/sandbox/leases.d.ts +169 -0
- package/dist/sandbox/leases.js +515 -0
- package/dist/sandbox/lifecycle.d.ts +21 -0
- package/dist/sandbox/lifecycle.js +68 -0
- package/dist/sandbox/sandbox.service.d.ts +249 -0
- package/dist/sandbox/sandbox.service.js +908 -0
- package/dist/skills/catalog.d.ts +149 -0
- package/dist/skills/catalog.js +405 -0
- package/dist/skills/changes.controller.d.ts +56 -0
- package/dist/skills/changes.controller.js +136 -0
- package/dist/skills/changes.service.d.ts +175 -0
- package/dist/skills/changes.service.js +805 -0
- package/dist/skills/invocable.controller.d.ts +23 -0
- package/dist/skills/invocable.controller.js +50 -0
- package/dist/skills/invocation-keys.d.ts +23 -0
- package/dist/skills/invocation-keys.js +44 -0
- package/dist/skills/invoke.d.ts +135 -0
- package/dist/skills/invoke.js +286 -0
- package/dist/skills/package.d.ts +87 -0
- package/dist/skills/package.js +227 -0
- package/dist/skills/skills.controller.d.ts +40 -0
- package/dist/skills/skills.controller.js +110 -0
- package/dist/skills/skills.service.d.ts +192 -0
- package/dist/skills/skills.service.js +567 -0
- package/dist/storage/prefix.d.ts +36 -0
- package/dist/storage/prefix.js +104 -0
- package/dist/storage/purge.d.ts +27 -0
- package/dist/storage/purge.js +75 -0
- package/dist/storage/storage.service.d.ts +44 -0
- package/dist/storage/storage.service.js +145 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +13 -0
- package/dist/tools/approvals.controller.d.ts +53 -0
- package/dist/tools/approvals.controller.js +128 -0
- package/dist/tools/approvals.service.d.ts +172 -0
- package/dist/tools/approvals.service.js +568 -0
- package/dist/tools/breaker.d.ts +28 -0
- package/dist/tools/breaker.js +72 -0
- package/dist/tools/builtins/agent.d.ts +81 -0
- package/dist/tools/builtins/agent.js +223 -0
- package/dist/tools/builtins/ask-user.d.ts +28 -0
- package/dist/tools/builtins/ask-user.js +139 -0
- package/dist/tools/builtins/read-tool-output.d.ts +18 -0
- package/dist/tools/builtins/read-tool-output.js +108 -0
- package/dist/tools/builtins/sandbox/bash.d.ts +16 -0
- package/dist/tools/builtins/sandbox/bash.js +137 -0
- package/dist/tools/builtins/sandbox/common.d.ts +116 -0
- package/dist/tools/builtins/sandbox/common.js +306 -0
- package/dist/tools/builtins/sandbox/deliver.d.ts +5 -0
- package/dist/tools/builtins/sandbox/deliver.js +180 -0
- package/dist/tools/builtins/sandbox/edit.d.ts +4 -0
- package/dist/tools/builtins/sandbox/edit.js +98 -0
- package/dist/tools/builtins/sandbox/glob-match.d.ts +25 -0
- package/dist/tools/builtins/sandbox/glob-match.js +240 -0
- package/dist/tools/builtins/sandbox/glob.d.ts +4 -0
- package/dist/tools/builtins/sandbox/glob.js +104 -0
- package/dist/tools/builtins/sandbox/grep.d.ts +9 -0
- package/dist/tools/builtins/sandbox/grep.js +133 -0
- package/dist/tools/builtins/sandbox/index.d.ts +12 -0
- package/dist/tools/builtins/sandbox/index.js +27 -0
- package/dist/tools/builtins/sandbox/names.d.ts +7 -0
- package/dist/tools/builtins/sandbox/names.js +20 -0
- package/dist/tools/builtins/sandbox/read.d.ts +8 -0
- package/dist/tools/builtins/sandbox/read.js +121 -0
- package/dist/tools/builtins/sandbox/write.d.ts +4 -0
- package/dist/tools/builtins/sandbox/write.js +88 -0
- package/dist/tools/builtins/skill-load.d.ts +53 -0
- package/dist/tools/builtins/skill-load.js +446 -0
- package/dist/tools/builtins/skill-write.d.ts +59 -0
- package/dist/tools/builtins/skill-write.js +244 -0
- package/dist/tools/builtins/tool-search.d.ts +31 -0
- package/dist/tools/builtins/tool-search.js +171 -0
- package/dist/tools/connector-content.controller.d.ts +34 -0
- package/dist/tools/connector-content.controller.js +78 -0
- package/dist/tools/connector-content.d.ts +104 -0
- package/dist/tools/connector-content.js +267 -0
- package/dist/tools/connectors.controller.d.ts +210 -0
- package/dist/tools/connectors.controller.js +234 -0
- package/dist/tools/connectors.service.d.ts +303 -0
- package/dist/tools/connectors.service.js +1102 -0
- package/dist/tools/credentials.d.ts +139 -0
- package/dist/tools/credentials.js +351 -0
- package/dist/tools/drift.d.ts +141 -0
- package/dist/tools/drift.js +539 -0
- package/dist/tools/elicitation.d.ts +81 -0
- package/dist/tools/elicitation.js +266 -0
- package/dist/tools/endpoint.d.ts +18 -0
- package/dist/tools/endpoint.js +115 -0
- package/dist/tools/info.d.ts +67 -0
- package/dist/tools/info.js +113 -0
- package/dist/tools/naming.d.ts +27 -0
- package/dist/tools/naming.js +47 -0
- package/dist/tools/oauth/discovery.d.ts +65 -0
- package/dist/tools/oauth/discovery.js +307 -0
- package/dist/tools/oauth/flow.d.ts +63 -0
- package/dist/tools/oauth/flow.js +237 -0
- package/dist/tools/oauth/oauth.controller.d.ts +31 -0
- package/dist/tools/oauth/oauth.controller.js +95 -0
- package/dist/tools/oauth/tokens.d.ts +111 -0
- package/dist/tools/oauth/tokens.js +258 -0
- package/dist/tools/outputs.d.ts +61 -0
- package/dist/tools/outputs.js +156 -0
- package/dist/tools/pipeline.d.ts +322 -0
- package/dist/tools/pipeline.js +1236 -0
- package/dist/tools/policy.d.ts +60 -0
- package/dist/tools/policy.js +236 -0
- package/dist/tools/questions.controller.d.ts +24 -0
- package/dist/tools/questions.controller.js +58 -0
- package/dist/tools/questions.service.d.ts +103 -0
- package/dist/tools/questions.service.js +295 -0
- package/dist/tools/redact.d.ts +20 -0
- package/dist/tools/redact.js +60 -0
- package/dist/tools/registry.d.ts +246 -0
- package/dist/tools/registry.js +481 -0
- package/dist/tools/resources.d.ts +16 -0
- package/dist/tools/resources.js +76 -0
- package/dist/tools/specs.d.ts +14 -0
- package/dist/tools/specs.js +31 -0
- package/dist/tools/validate.d.ts +20 -0
- package/dist/tools/validate.js +183 -0
- package/dist/tools/view.d.ts +43 -0
- package/dist/tools/view.js +225 -0
- package/dist/ui/embed-token.d.ts +31 -0
- package/dist/ui/embed-token.js +132 -0
- package/dist/ui/registry.d.ts +58 -0
- package/dist/ui/registry.js +90 -0
- package/dist/ui/ui-stream.d.ts +37 -0
- package/dist/ui/ui-stream.js +162 -0
- package/dist/ui/views.controller.d.ts +81 -0
- package/dist/ui/views.controller.js +261 -0
- package/dist/ui/views.d.ts +155 -0
- package/dist/ui/views.js +306 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +6 -0
- package/package.json +69 -3
- package/src/migrations/0001_init.sql +8 -0
- package/src/migrations/0002_secrets.sql +8 -0
- package/src/migrations/0003_identity.sql +117 -0
- package/src/migrations/0004_plugin_org_config_nullable.sql +3 -0
- package/src/migrations/0005_agents.sql +27 -0
- package/src/migrations/0006_conversations.sql +37 -0
- package/src/migrations/0007_checkpoints.sql +26 -0
- package/src/migrations/0008_runs_trigger_seq.sql +3 -0
- package/src/migrations/0009_runs_active_unique.sql +4 -0
- package/src/migrations/0010_tools.sql +125 -0
- package/src/migrations/0011_approvals_spec.sql +10 -0
- package/src/migrations/0012_run_waits.sql +24 -0
- package/src/migrations/0013_app_tools_source.sql +4 -0
- package/src/migrations/0014_tool_specs.sql +8 -0
- package/src/migrations/0015_tool_drift.sql +20 -0
- package/src/migrations/0016_plugin_permissions.sql +11 -0
- package/src/migrations/0017_identity_sources.sql +6 -0
- package/src/migrations/0018_channels.sql +74 -0
- package/src/migrations/0019_identity_trust.sql +17 -0
- package/src/migrations/0020_channel_card_lookup.sql +10 -0
- package/src/migrations/0021_org_storage_purge.sql +9 -0
- package/src/migrations/0022_attachments.sql +29 -0
- package/src/migrations/0023_sandboxes.sql +37 -0
- package/src/migrations/0024_skills.sql +46 -0
- package/src/migrations/0025_skills_catalog_idx.sql +6 -0
- package/src/migrations/0026_skill_changes.sql +34 -0
- package/src/migrations/0027_model_prices.sql +18 -0
- package/src/migrations/0028_evals.sql +105 -0
- package/src/migrations/0029_model_purposes.sql +18 -0
- package/src/migrations/0030_plugin_installs.sql +73 -0
- package/src/migrations/0031_plugin_snapshot_source.sql +6 -0
- package/src/migrations/0032_plugin_deployment_configs.sql +10 -0
- package/src/migrations/0033_rename_apps_to_connectors.sql +25 -0
- package/src/migrations/0034_directory.sql +49 -0
- package/src/migrations/0035_catalogs.sql +40 -0
- package/src/migrations/0036_app_import_staging.sql +17 -0
- package/src/migrations/0037_directory_installs.sql +92 -0
- package/src/migrations/0038_channel_inbound_mode.sql +6 -0
- package/src/migrations/0039_channel_card_pending.sql +8 -0
- package/src/migrations/0040_usage_report_sweep.sql +10 -0
- package/src/migrations/0041_rename_plugins_to_extensions.sql +38 -0
- package/src/migrations/0042_rename_directory_to_catalog.sql +80 -0
- package/src/migrations/0043_rename_app_installs_to_plugin_installs.sql +20 -0
- package/src/migrations/0044_catalog_one_kind.sql +90 -0
- package/src/migrations/0045_plugin_dependencies.sql +9 -0
- package/src/migrations/0046_git_sources.sql +25 -0
- package/src/migrations/0047_marketplace_entry_heads.sql +7 -0
- package/src/migrations/0048_marketplace_entry_trees.sql +8 -0
- package/src/migrations/0049_plugin_upgrades.sql +37 -0
- package/src/migrations/0050_bootstrap.sql +53 -0
- package/src/migrations/0051_claude_compat.sql +26 -0
- package/src/migrations/0052_conversations_account_agent_idx.sql +5 -0
- package/src/migrations/0053_subagents.sql +73 -0
- package/src/migrations/0054_subagent_wait_expiry.sql +7 -0
- package/src/migrations/0055_subagent_parent_indexes.sql +10 -0
- package/src/migrations/0056_approval_reason.sql +3 -0
- package/src/migrations/0057_conversation_titles.sql +11 -0
- package/src/migrations/0058_conversation_selection.sql +4 -0
- package/src/migrations/0059_conversation_queue.sql +22 -0
- package/src/migrations/0060_file_previewers.sql +10 -0
- package/src/migrations/0061_queue_selection.sql +5 -0
- package/src/migrations/0062_queued_skill.sql +8 -0
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { PLUGIN_MANIFEST_FILE, parseSkillMd, SDK_VERSION, SKILL_ENTRY_FILE, } from "@capybara-hq/plugin-sdk";
|
|
3
|
+
import { sql } from "kysely";
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
import { ApiError } from "../errors.js";
|
|
6
|
+
import { writeSnapshot } from "../extensions/approvals.js";
|
|
7
|
+
import { unrunMigrations } from "../extensions/migrate-lock.js";
|
|
8
|
+
import { ExtensionPackageError, permissionSetOfPackage, readExtensionPackage, } from "../extensions/package.js";
|
|
9
|
+
import { diffPermissions, permissionKeys, WARNINGS_KEY_PREFIX, } from "../extensions/permission-set.js";
|
|
10
|
+
import { reviewHash as extensionReviewHash, migrationWarnings, permissionGroups, permissionKeyItems, } from "../extensions/permission-view.js";
|
|
11
|
+
import { audit } from "../identity/audit.js";
|
|
12
|
+
import { lockEnterpriseAdmins, lockEnterpriseAdminsAndAuthorize, } from "../identity/enterprise-admin.js";
|
|
13
|
+
import { packPackageTgz } from "../packages/tgz.js";
|
|
14
|
+
import { checkSkillFiles, SkillArchiveError } from "../skills/package.js";
|
|
15
|
+
import { endpointCredentialIssues } from "../tools/endpoint.js";
|
|
16
|
+
import { canonicalJson } from "../tools/registry.js";
|
|
17
|
+
import { assertPluginNameOwner, catalogObjectPrefix, itemView, permissionsMismatch, precheckCatalogImport, putCatalogFiles, templateProblems, writeCatalogVersion, } from "./catalog.service.js";
|
|
18
|
+
import { CLAUDE_PLUGIN_FILE, claudeContentDigest, } from "./claude/manifest.js";
|
|
19
|
+
import { CLAUDE_MCP_CONNECTOR } from "./claude/mcp.js";
|
|
20
|
+
import { item as reportItem, sortItems, } from "./claude/report.js";
|
|
21
|
+
import { isClaudePluginTree, translateClaudePlugin } from "./claude/translate.js";
|
|
22
|
+
import { PluginPackageError, packagePath, readPluginFiles, readPluginPackage, } from "./plugin-package.js";
|
|
23
|
+
import { maxSatisfying, satisfiesRange } from "./versions.js";
|
|
24
|
+
/**
|
|
25
|
+
* Enterprise import of a plugin (package ecosystem spec §4.3): one review for every plugin, whatever
|
|
26
|
+
* it contains (an extension, skills, connector templates, dependencies), confirmed once.
|
|
27
|
+
*
|
|
28
|
+
* prepare: the tgz is always hostile input, and **nothing is written until every check passes**
|
|
29
|
+
* (F-AD): `readPluginPackage` (plugin.json, package.json, listed / scanned skills and templates,
|
|
30
|
+
* icons; a pre-0.1.0 tgz is `catalog.legacy_format`), the extension's module graph and migrations
|
|
31
|
+
* (`readExtensionPackage`), the extension's deployment state (config-declared names are
|
|
32
|
+
* refused; name ownership; a downgrade to a version with unrun migrations is refused), each skill by
|
|
33
|
+
* the skill package rules, each connector template against the connector loaded on this replica (or
|
|
34
|
+
* statically when the connector is provided by the plugin's own extension or by a dependency), the
|
|
35
|
+
* catalog name / version conflicts and the actor still an enterprise admin. Dependencies are only
|
|
36
|
+
* resolved against the imported catalog versions for the review (an unresolved one does not block
|
|
37
|
+
* the import, spec §4.1); they are never fetched. Only then is the extension handed to
|
|
38
|
+
* `ExtensionInstalls.ingest` (source `catalog`), the skill / template / icon files written to object
|
|
39
|
+
* storage, a staging row written (24 hours) and `catalog.plugin_staged` audited.
|
|
40
|
+
*
|
|
41
|
+
* confirm: `reviewHash` must match the staging; in one transaction it deletes the staging row
|
|
42
|
+
* (confirm works once), locks the extension name and recomputes its deployment state (installed /
|
|
43
|
+
* upgraded / swapped after the review -> `plugin.review_stale`), writes the extension's permission
|
|
44
|
+
* snapshot when it is installed by this import (or upgraded with `deployExtension`; source web, audit
|
|
45
|
+
* `extension.permissions_approved` carrying the plugin), and creates the item or adds the version
|
|
46
|
+
* (`manifest` = the normalized plugin.json, `extension_sha256`, the files under the staging prefix).
|
|
47
|
+
* After the commit, with `deployExtension`, the plugin's own extension is installed on the
|
|
48
|
+
* deployment and enabled (`ExtensionAdminOps.install` + `enable`; each replica reconciles, no
|
|
49
|
+
* restart); a failure is returned, not thrown, and an upgrade that was not installed gets its old
|
|
50
|
+
* snapshot back.
|
|
51
|
+
*
|
|
52
|
+
* Expired staging is cleaned by the maintenance job `app-staging-cleanup`
|
|
53
|
+
* ({@link PluginImport.cleanupExpired}): it deletes the staging row and its object prefix; a stored
|
|
54
|
+
* extension package stays in the content-addressed `extension_packages` (the same as importing the
|
|
55
|
+
* same tgz again; without an install record referencing it it is never loaded).
|
|
56
|
+
*/
|
|
57
|
+
/** How long a staged import is kept. */
|
|
58
|
+
export const PLUGIN_STAGING_TTL_MS = 24 * 3600_000;
|
|
59
|
+
/** Staging rows cleaned per batch. */
|
|
60
|
+
const CLEANUP_BATCH = 100;
|
|
61
|
+
/** Root license files, in the order they are looked for. */
|
|
62
|
+
const LICENSE_FILES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE"];
|
|
63
|
+
/** The largest license text stored (bytes); a longer one is omitted (`licenseTruncated`). */
|
|
64
|
+
export const LICENSE_TEXT_MAX_BYTES = 65536;
|
|
65
|
+
/** The provenance of every import that is not from npm. */
|
|
66
|
+
const NOT_NPM = { provenance: { checked: false, reason: "not an npm package" } };
|
|
67
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
68
|
+
const notFound = () => new ApiError("plugin.staging_not_found", "This plugin import was not found or has expired (staged imports are kept for 24 hours); prepare it again");
|
|
69
|
+
const invalid = (problems) => new ApiError("catalog.invalid_manifest", `Invalid plugin package: ${problems[0] ?? "unknown problem"}`, problems.slice(0, 50));
|
|
70
|
+
const mismatch = (what) => new ApiError("marketplace.package_mismatch", `The downloaded package does not match the marketplace entry: ${what}`);
|
|
71
|
+
/**
|
|
72
|
+
* The API error of a plugin package that cannot be read in a catalog path: the pre-0.1.0 format ->
|
|
73
|
+
* `catalog.legacy_format`, anything else -> `catalog.invalid_manifest` (issues as details).
|
|
74
|
+
*/
|
|
75
|
+
export function catalogPackageError(e) {
|
|
76
|
+
const legacy = e.code === "catalog.legacy_format" || e.code === "extension.legacy_format";
|
|
77
|
+
if (legacy)
|
|
78
|
+
return new ApiError("catalog.legacy_format", e.message, e.issues);
|
|
79
|
+
return invalid(e.issues);
|
|
80
|
+
}
|
|
81
|
+
/** Runs a package read, mapping its errors with {@link catalogPackageError}. */
|
|
82
|
+
async function readingPackage(f) {
|
|
83
|
+
try {
|
|
84
|
+
return await f();
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (e instanceof PluginPackageError || e instanceof ExtensionPackageError) {
|
|
88
|
+
throw catalogPackageError(e);
|
|
89
|
+
}
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const fileOf = (path, data) => ({
|
|
94
|
+
path,
|
|
95
|
+
size: data.byteLength,
|
|
96
|
+
sha256: createHash("sha256").update(data).digest("hex"),
|
|
97
|
+
data,
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* The enterprise-admin re-check under the lock. `actor` null is the CLI (the operator of the
|
|
101
|
+
* deployment: no account to re-check, audits carry no actor), like `ExtensionInstalls.ingest`.
|
|
102
|
+
*/
|
|
103
|
+
const authorizeActor = (trx, actor) => actor === null ? lockEnterpriseAdmins(trx) : lockEnterpriseAdminsAndAuthorize(trx, actor);
|
|
104
|
+
export class PluginImport {
|
|
105
|
+
d;
|
|
106
|
+
constructor(d) {
|
|
107
|
+
this.d = d;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Reads, checks and stages a plugin (see the file header). Returns the staging id and review.
|
|
111
|
+
* `fallback` (a marketplace archive): a tgz without plugin.json may be skills only, named,
|
|
112
|
+
* versioned and published by it (the version defaulting to `0.0.0-sha256.h<12>`, spec §5.3).
|
|
113
|
+
*/
|
|
114
|
+
async prepare(actor, bytes, source, opts = {}) {
|
|
115
|
+
const maxBytes = this.d.config().packages.maxBytes;
|
|
116
|
+
const pkg = await readingPackage(() => readPluginPackage(bytes, {
|
|
117
|
+
maxBytes,
|
|
118
|
+
hostSdkVersion: SDK_VERSION,
|
|
119
|
+
...(opts.fallback !== undefined && { fallback: opts.fallback }),
|
|
120
|
+
}));
|
|
121
|
+
const { fallback: _, ...rest } = opts;
|
|
122
|
+
return this.prepareContents(actor, pkg, { bytes, size: bytes.byteLength }, source, rest);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Checks and stages a plugin read from a git commit (`files` relative to the plugin root; see
|
|
126
|
+
* `git-source.ts`): the same checks as a tgz ({@link readPluginFiles}), with package.json
|
|
127
|
+
* optional (spec §4.1) and a skills-only tree without plugin.json named by `fallback` (Decision
|
|
128
|
+
* 3). A plugin with an extension needs package.json (its peerDependencies ranges are checked as
|
|
129
|
+
* for a tgz) and its build output; its files are packed into a deterministic tgz, which is what
|
|
130
|
+
* the package store keeps and the deployment installs. Returns what {@link prepare} returns.
|
|
131
|
+
*/
|
|
132
|
+
async prepareGit(actor, files, source, opts) {
|
|
133
|
+
const { fallback: _, claude, ...rest } = opts;
|
|
134
|
+
if (!files.has(PLUGIN_MANIFEST_FILE) && isClaudePluginTree(files, claude)) {
|
|
135
|
+
// A Claude Code plugin (spec §8): translated, then read like any git plugin; the content
|
|
136
|
+
// digest is over the files as read and the entry's components, never the generated tree
|
|
137
|
+
// (Review Focus 5, claudeContentDigest)
|
|
138
|
+
const t = await readingPackage(async () => translateClaudePlugin(files, {
|
|
139
|
+
fallbackName: opts.fallback.name,
|
|
140
|
+
fallbackVersion: opts.fallback.version,
|
|
141
|
+
fallbackPublisher: opts.fallback.publisher,
|
|
142
|
+
...(opts.fallback.entryVersion !== undefined && {
|
|
143
|
+
entryVersion: opts.fallback.entryVersion,
|
|
144
|
+
}),
|
|
145
|
+
...(claude !== undefined && { overlay: claude }),
|
|
146
|
+
allowInsecureLoopback: this.d.config().marketplaces.allowInsecureLoopback,
|
|
147
|
+
}));
|
|
148
|
+
const translated = await readingPackage(async () => readPluginFiles(t.tree, {
|
|
149
|
+
hostSdkVersion: SDK_VERSION,
|
|
150
|
+
requirePackageJson: false,
|
|
151
|
+
invocationKeys: false,
|
|
152
|
+
}));
|
|
153
|
+
const contents = {
|
|
154
|
+
...translated,
|
|
155
|
+
contentDigest: claudeContentDigest(files, claude, t.translatorRevision),
|
|
156
|
+
};
|
|
157
|
+
let size = 0;
|
|
158
|
+
for (const data of files.values())
|
|
159
|
+
size += data.byteLength;
|
|
160
|
+
return this.prepareContents(actor, contents, { size }, source, {
|
|
161
|
+
...rest,
|
|
162
|
+
claude: t.report,
|
|
163
|
+
...(t.repository !== undefined && { repository: t.repository }),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const contents = await readingPackage(async () => readPluginFiles(files, {
|
|
167
|
+
hostSdkVersion: SDK_VERSION,
|
|
168
|
+
requirePackageJson: false,
|
|
169
|
+
fallbackName: opts.fallback.name,
|
|
170
|
+
fallbackVersion: opts.fallback.version,
|
|
171
|
+
fallbackPublisher: opts.fallback.publisher,
|
|
172
|
+
}));
|
|
173
|
+
const next = {
|
|
174
|
+
...rest,
|
|
175
|
+
// both manifests: the native one is read (spec §4.1) and the review says so
|
|
176
|
+
...(files.has(PLUGIN_MANIFEST_FILE) &&
|
|
177
|
+
files.has(CLAUDE_PLUGIN_FILE) && { claudeManifestIgnored: true }),
|
|
178
|
+
};
|
|
179
|
+
if (contents.manifest.extension !== undefined) {
|
|
180
|
+
if (!files.has("package.json")) {
|
|
181
|
+
throw invalid([
|
|
182
|
+
"package.json not found: a plugin with an extension needs package.json with peerDependencies (@capybara-hq/plugin-sdk, zod), which are checked against this host",
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
return this.prepare(actor, await packPackageTgz(files), source, next);
|
|
186
|
+
}
|
|
187
|
+
let size = 0;
|
|
188
|
+
for (const data of files.values())
|
|
189
|
+
size += data.byteLength;
|
|
190
|
+
return this.prepareContents(actor, contents, { size }, source, next);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The checks and writes of prepare after the package was read. `tgz.bytes` is the package tgz
|
|
194
|
+
* (required with an extension: it is stored in the package store); `size` is recorded.
|
|
195
|
+
*/
|
|
196
|
+
async prepareContents(actor, pkg, tgz, source, opts) {
|
|
197
|
+
const maxBytes = this.d.config().packages.maxBytes;
|
|
198
|
+
// 1. Every check (nothing written)
|
|
199
|
+
const m = pkg.manifest;
|
|
200
|
+
const packageName = packageNameOf(pkg);
|
|
201
|
+
const format = opts.claude !== undefined ? "claude-code" : "capybara";
|
|
202
|
+
this.checkExpected(pkg, opts.expect, format);
|
|
203
|
+
const bytes = tgz.bytes;
|
|
204
|
+
if (m.extension && bytes === undefined) {
|
|
205
|
+
throw new Error("an extension plugin is prepared from its package tgz");
|
|
206
|
+
}
|
|
207
|
+
const ext = m.extension && bytes !== undefined
|
|
208
|
+
? await readingPackage(() => readExtensionPackage(bytes, { maxBytes, hostSdkVersion: SDK_VERSION }))
|
|
209
|
+
: undefined;
|
|
210
|
+
const content = this.checkContent(pkg, format);
|
|
211
|
+
const { existing, version } = await precheckCatalogImport(this.d.db, m.name, {
|
|
212
|
+
packageName,
|
|
213
|
+
version: m.version,
|
|
214
|
+
contentDigest: pkg.contentDigest,
|
|
215
|
+
...(source.integrity !== undefined && { integrity: source.integrity }),
|
|
216
|
+
...(source.detail !== undefined && { detail: source.detail }),
|
|
217
|
+
...(source.marketplaceId !== undefined && { marketplaceId: source.marketplaceId }),
|
|
218
|
+
});
|
|
219
|
+
// The catalog name space is the extension name space: a declarative plugin cannot take a
|
|
220
|
+
// config-declared or another package's extension name either
|
|
221
|
+
this.assertNotDeclared(m.name);
|
|
222
|
+
await assertPluginNameOwner(this.d.db, m.name, packageName);
|
|
223
|
+
let extension;
|
|
224
|
+
if (ext) {
|
|
225
|
+
extension = await this.reviewExtension(ext, await this.extensionState(m.name, packageName));
|
|
226
|
+
await assertDowngradable(this.d.db, extension, ext.migrationSql);
|
|
227
|
+
}
|
|
228
|
+
if (content.files.length > 0)
|
|
229
|
+
this.d.storage.assertAvailable();
|
|
230
|
+
const src = {
|
|
231
|
+
kind: source.kind,
|
|
232
|
+
ref: source.ref,
|
|
233
|
+
...(source.integrity !== undefined && { integrity: source.integrity }),
|
|
234
|
+
...(source.marketplaceId !== undefined && { marketplaceId: source.marketplaceId }),
|
|
235
|
+
...(source.detail !== undefined && { detail: source.detail }),
|
|
236
|
+
};
|
|
237
|
+
const dependencies = await this.resolveDependencies(m);
|
|
238
|
+
let translation;
|
|
239
|
+
if (opts.claude !== undefined) {
|
|
240
|
+
const added = [
|
|
241
|
+
...dependencies
|
|
242
|
+
.filter((d) => d.resolved === null)
|
|
243
|
+
.map((d) => reportItem("dependency_not_imported", "plugin", d.name, {
|
|
244
|
+
detail: { version: d.range },
|
|
245
|
+
})),
|
|
246
|
+
...content.notLoaded.map((id) => reportItem("connector_not_loaded", "mcpServer", id, {
|
|
247
|
+
path: content.templatePaths.get(id),
|
|
248
|
+
detail: { connector: CLAUDE_MCP_CONNECTOR.plugin },
|
|
249
|
+
})),
|
|
250
|
+
];
|
|
251
|
+
translation = { ...opts.claude, items: sortItems([...opts.claude.items, ...added]) };
|
|
252
|
+
}
|
|
253
|
+
const license = licenseOf(pkg.files);
|
|
254
|
+
const review = {
|
|
255
|
+
name: m.name,
|
|
256
|
+
format,
|
|
257
|
+
...(translation !== undefined && { translation }),
|
|
258
|
+
...(opts.claudeManifestIgnored === true && { claudeManifestIgnored: true }),
|
|
259
|
+
...(opts.repository !== undefined && { repository: opts.repository }),
|
|
260
|
+
...(license.text !== undefined && { licenseText: license.text }),
|
|
261
|
+
...(license.truncated && { licenseTruncated: true }),
|
|
262
|
+
version,
|
|
263
|
+
...(version !== m.version && { pluginVersion: m.version }),
|
|
264
|
+
contentDigest: pkg.contentDigest,
|
|
265
|
+
packageName,
|
|
266
|
+
publisher: m.publisher,
|
|
267
|
+
description: m.description,
|
|
268
|
+
...(m.displayName !== undefined && { displayName: m.displayName }),
|
|
269
|
+
...(m.license !== undefined && { license: m.license }),
|
|
270
|
+
...(m.homepage !== undefined && { homepage: m.homepage }),
|
|
271
|
+
...(extension && { extension }),
|
|
272
|
+
skills: content.skills,
|
|
273
|
+
connectorTemplates: content.templates,
|
|
274
|
+
...(content.agentTemplates.length > 0 && { agentTemplates: content.agentTemplates }),
|
|
275
|
+
dependencies,
|
|
276
|
+
source: src,
|
|
277
|
+
provenance: (opts.provenance ?? NOT_NPM).provenance,
|
|
278
|
+
...(opts.provenance?.warning !== undefined && { provenanceWarning: opts.provenance.warning }),
|
|
279
|
+
sensitive: extension?.sensitive === true,
|
|
280
|
+
reviewHash: "",
|
|
281
|
+
};
|
|
282
|
+
review.reviewHash = pluginReviewHash(review, content.files);
|
|
283
|
+
// Re-check under the lock that the actor is still an enterprise admin before writing
|
|
284
|
+
await this.d.db.transaction().execute((trx) => authorizeActor(trx, actor));
|
|
285
|
+
// 2. Write: the extension into the package store, files into object storage, the staging row
|
|
286
|
+
const stagingId = randomUUID();
|
|
287
|
+
const itemId = existing?.id ?? randomUUID();
|
|
288
|
+
if (ext) {
|
|
289
|
+
const bootstrap = src.kind === "bootstrap";
|
|
290
|
+
await this.d.installs.ingest(actor, bytes, bootstrap
|
|
291
|
+
? { source: "bootstrap", ref: src.ref }
|
|
292
|
+
: {
|
|
293
|
+
source: "catalog",
|
|
294
|
+
ref: src.marketplaceId !== undefined ? `catalog:${src.ref}` : `${itemId}@${version}`,
|
|
295
|
+
}, {
|
|
296
|
+
...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
|
|
297
|
+
...(bootstrap && { store: "pg" }),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
let objectPrefix = null;
|
|
301
|
+
if (content.files.length > 0) {
|
|
302
|
+
objectPrefix = catalogObjectPrefix(itemId, version, stagingId);
|
|
303
|
+
await putCatalogFiles(this.d.storage, objectPrefix, content.files);
|
|
304
|
+
}
|
|
305
|
+
const keys = ext
|
|
306
|
+
? permissionKeys(permissionSetOfPackage(ext))
|
|
307
|
+
.filter((k) => !k.startsWith(WARNINGS_KEY_PREFIX))
|
|
308
|
+
.sort()
|
|
309
|
+
: [];
|
|
310
|
+
const mismatch = permissionsMismatch(opts.expect?.marketplace?.permissions, ext ? keys : undefined);
|
|
311
|
+
const staged = {
|
|
312
|
+
itemId,
|
|
313
|
+
packageName,
|
|
314
|
+
version,
|
|
315
|
+
contentDigest: pkg.contentDigest,
|
|
316
|
+
size: tgz.size,
|
|
317
|
+
manifest: normalizedManifest(pkg),
|
|
318
|
+
files: content.files.map(({ path, size, sha256 }) => ({ path, size, sha256 })),
|
|
319
|
+
...(mismatch !== null && { permissionsMismatch: mismatch }),
|
|
320
|
+
sourceFormat: format,
|
|
321
|
+
...(translation !== undefined && { translation }),
|
|
322
|
+
...(license.text !== undefined && { licenseText: license.text }),
|
|
323
|
+
...(license.truncated && { licenseTruncated: true }),
|
|
324
|
+
};
|
|
325
|
+
await this.d.db.transaction().execute(async (trx) => {
|
|
326
|
+
await authorizeActor(trx, actor);
|
|
327
|
+
await trx
|
|
328
|
+
.insertInto("plugin_import_staging")
|
|
329
|
+
.values({
|
|
330
|
+
id: stagingId,
|
|
331
|
+
review: JSON.stringify(review),
|
|
332
|
+
manifest: JSON.stringify(staged),
|
|
333
|
+
object_prefix: objectPrefix,
|
|
334
|
+
source: JSON.stringify(src),
|
|
335
|
+
created_by: actor,
|
|
336
|
+
expires_at: new Date(Date.now() + PLUGIN_STAGING_TTL_MS),
|
|
337
|
+
})
|
|
338
|
+
.execute();
|
|
339
|
+
await audit(trx, {
|
|
340
|
+
...(actor !== null && { actorAccountId: actor }),
|
|
341
|
+
...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
|
|
342
|
+
action: "catalog.plugin_staged",
|
|
343
|
+
target: stagingId,
|
|
344
|
+
detail: {
|
|
345
|
+
stagingId,
|
|
346
|
+
name: m.name,
|
|
347
|
+
version,
|
|
348
|
+
source: src.kind,
|
|
349
|
+
sourceRef: src.ref,
|
|
350
|
+
...(src.integrity !== undefined && { integrity: src.integrity }),
|
|
351
|
+
...(src.marketplaceId !== undefined && { marketplaceId: src.marketplaceId }),
|
|
352
|
+
reviewHash: review.reviewHash,
|
|
353
|
+
...(extension && {
|
|
354
|
+
extension: { sha256: extension.sha256, action: extension.action },
|
|
355
|
+
}),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
return { stagingId, review, permissionKeys: keys, permissionsMismatch: mismatch };
|
|
360
|
+
}
|
|
361
|
+
/** Fetches a plugin from the configured npm registry (verified by `dist.integrity`), then prepares it. */
|
|
362
|
+
async prepareNpm(actor, input, opts = {}) {
|
|
363
|
+
const got = await this.d.installs.fetchNpm(input);
|
|
364
|
+
const provenance = await this.d.installs.fetchProvenance({
|
|
365
|
+
package: input.package,
|
|
366
|
+
version: got.version,
|
|
367
|
+
integrity: got.integrity,
|
|
368
|
+
});
|
|
369
|
+
const r = await this.prepare(actor, got.bytes, {
|
|
370
|
+
kind: "npm",
|
|
371
|
+
ref: `${input.package}@${got.version}`,
|
|
372
|
+
integrity: got.integrity,
|
|
373
|
+
detail: { registry: new URL(this.d.config().packages.npmRegistry).origin },
|
|
374
|
+
}, { ...opts, expect: { packageName: input.package }, provenance: { provenance } });
|
|
375
|
+
return { stagingId: r.stagingId, review: r.review };
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* A staged import's review, as prepare returned it, with a marketplace import's permission
|
|
379
|
+
* mismatch when there is one (the import page loads `?staging=<id>` with it). Unknown, expired or
|
|
380
|
+
* already confirmed -> 404 `plugin.staging_not_found`.
|
|
381
|
+
*/
|
|
382
|
+
async staged(stagingId) {
|
|
383
|
+
if (!UUID_RE.test(stagingId))
|
|
384
|
+
throw notFound();
|
|
385
|
+
const row = await this.d.db
|
|
386
|
+
.selectFrom("plugin_import_staging")
|
|
387
|
+
.select(["id", "review", "manifest"])
|
|
388
|
+
.where("id", "=", stagingId)
|
|
389
|
+
.where("expires_at", ">", sql `now()`)
|
|
390
|
+
.executeTakeFirst();
|
|
391
|
+
if (!row)
|
|
392
|
+
throw notFound();
|
|
393
|
+
const mismatch = row.manifest.permissionsMismatch;
|
|
394
|
+
return {
|
|
395
|
+
stagingId: row.id,
|
|
396
|
+
review: row.review,
|
|
397
|
+
...(mismatch !== undefined && mismatch !== null && { permissionsMismatch: mismatch }),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Confirms a staged import (see the file header). Returns the committed catalog item and, with
|
|
402
|
+
* `deployExtension`, the extension's deploy result (a failed deploy is returned, not thrown).
|
|
403
|
+
*/
|
|
404
|
+
async confirm(actor, stagingId, input, opts = {}) {
|
|
405
|
+
if (!UUID_RE.test(stagingId))
|
|
406
|
+
throw notFound();
|
|
407
|
+
const row = await this.d.db
|
|
408
|
+
.selectFrom("plugin_import_staging")
|
|
409
|
+
.selectAll()
|
|
410
|
+
.where("id", "=", stagingId)
|
|
411
|
+
.where("expires_at", ">", sql `now()`)
|
|
412
|
+
.executeTakeFirst();
|
|
413
|
+
if (!row)
|
|
414
|
+
throw notFound();
|
|
415
|
+
const review = row.review;
|
|
416
|
+
const staged = row.manifest;
|
|
417
|
+
const src = row.source;
|
|
418
|
+
const stale = () => new ApiError("plugin.review_stale", `The review of plugin ${review.name} ${review.version} is out of date (it changed, or its extension on this deployment changed since it was prepared); review it again`);
|
|
419
|
+
if (input.reviewHash !== review.reviewHash)
|
|
420
|
+
throw stale();
|
|
421
|
+
const manifest = withPinned(staged.manifest, review, input.pinnedSuggestions);
|
|
422
|
+
const reviewed = review.extension;
|
|
423
|
+
// Read the stored package outside the transaction (unless it was already installed as is)
|
|
424
|
+
const stored = reviewed && reviewed.action !== "keep" ? await this.readStored(reviewed.sha256) : undefined;
|
|
425
|
+
const committed = await this.d.db.transaction().execute(async (trx) => {
|
|
426
|
+
await authorizeActor(trx, actor);
|
|
427
|
+
const used = await trx
|
|
428
|
+
.deleteFrom("plugin_import_staging")
|
|
429
|
+
.where("id", "=", stagingId)
|
|
430
|
+
.where("expires_at", ">", sql `now()`)
|
|
431
|
+
.returning("id")
|
|
432
|
+
.executeTakeFirst();
|
|
433
|
+
if (!used)
|
|
434
|
+
throw notFound();
|
|
435
|
+
let now;
|
|
436
|
+
let previous;
|
|
437
|
+
if (reviewed) {
|
|
438
|
+
// The same per-name advisory lock as ExtensionAdminOps.install; under it the action is
|
|
439
|
+
// recomputed from the current deployment state (installed / upgraded / swapped since the
|
|
440
|
+
// review -> stale)
|
|
441
|
+
await sql `SELECT pg_advisory_xact_lock(hashtext(${`capybara.plugin-install:${reviewed.name}`}))`.execute(trx);
|
|
442
|
+
now = {
|
|
443
|
+
...reviewed,
|
|
444
|
+
...actionOf(await this.extensionState(reviewed.name, staged.packageName, trx), reviewed.sha256),
|
|
445
|
+
};
|
|
446
|
+
if (pluginReviewHash({ ...review, extension: now }, staged.files) !== review.reviewHash) {
|
|
447
|
+
throw stale();
|
|
448
|
+
}
|
|
449
|
+
// A foreseeable deploy failure (a downgrade to a version with unrun migrations) is refused
|
|
450
|
+
// before anything is written
|
|
451
|
+
if (input.deployExtension && now.action === "upgrade" && stored) {
|
|
452
|
+
await assertDowngradable(trx, now, stored.migrationSql);
|
|
453
|
+
}
|
|
454
|
+
if (now.action === "install" || (now.action === "upgrade" && input.deployExtension)) {
|
|
455
|
+
previous = await this.approve(trx, actor, review, staged, now, stored, opts);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const m = manifest;
|
|
459
|
+
const itemId = await writeCatalogVersion(trx, {
|
|
460
|
+
actor,
|
|
461
|
+
...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
|
|
462
|
+
name: m.name,
|
|
463
|
+
packageName: staged.packageName,
|
|
464
|
+
version: staged.version,
|
|
465
|
+
contentDigest: staged.contentDigest,
|
|
466
|
+
manifest: m,
|
|
467
|
+
listing: {
|
|
468
|
+
publisher: m.publisher,
|
|
469
|
+
description: m.description,
|
|
470
|
+
displayName: m.displayName ?? null,
|
|
471
|
+
},
|
|
472
|
+
newItemId: staged.itemId,
|
|
473
|
+
source: {
|
|
474
|
+
source: src.kind,
|
|
475
|
+
ref: src.ref,
|
|
476
|
+
...(src.integrity !== undefined && { integrity: src.integrity }),
|
|
477
|
+
...(src.detail !== undefined && { detail: src.detail }),
|
|
478
|
+
},
|
|
479
|
+
...(src.marketplaceId !== undefined && { marketplaceId: src.marketplaceId }),
|
|
480
|
+
objectPrefix: row.object_prefix,
|
|
481
|
+
extensionSha256: reviewed?.sha256 ?? null,
|
|
482
|
+
files: staged.files,
|
|
483
|
+
size: staged.size,
|
|
484
|
+
sourceFormat: staged.sourceFormat ?? "capybara",
|
|
485
|
+
translation: staged.translation ?? null,
|
|
486
|
+
licenseText: staged.licenseText ?? null,
|
|
487
|
+
licenseTruncated: staged.licenseTruncated === true,
|
|
488
|
+
auditDetail: {
|
|
489
|
+
stagingId,
|
|
490
|
+
deployExtension: input.deployExtension,
|
|
491
|
+
suggestedPinned: pinnedNames(manifest, review),
|
|
492
|
+
...(now && { extension: { name: now.name, version: now.version, action: now.action } }),
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
return { itemId, now, previous };
|
|
496
|
+
});
|
|
497
|
+
const { now } = committed;
|
|
498
|
+
const deploy = input.deployExtension && now && now.action !== "keep"
|
|
499
|
+
? await this.deploy(actor, review, committed.itemId, now, committed.previous, opts, input.enable ?? true)
|
|
500
|
+
: null;
|
|
501
|
+
return { item: await this.itemView(committed.itemId), deploy };
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Writes the extension's permission snapshot for the version being imported (source web; cli
|
|
505
|
+
* without an actor; bootstrap for the bootstrap step), after
|
|
506
|
+
* registering the name owner like ExtensionAdminOps.install (Ruling P3). Returns the snapshot it
|
|
507
|
+
* overwrote (null when there was none).
|
|
508
|
+
*/
|
|
509
|
+
async approve(trx, actor, review, staged, p, pkg, opts) {
|
|
510
|
+
const set = permissionSetOfPackage(pkg);
|
|
511
|
+
await sql `SELECT pg_advisory_xact_lock(hashtext(${`capybara.plugin-approve:${p.name}`}))`.execute(trx);
|
|
512
|
+
const snapRow = await trx
|
|
513
|
+
.selectFrom("extension_permission_snapshots")
|
|
514
|
+
.selectAll()
|
|
515
|
+
.where("plugin", "=", p.name)
|
|
516
|
+
.forUpdate()
|
|
517
|
+
.executeTakeFirst();
|
|
518
|
+
await trx
|
|
519
|
+
.insertInto("extension_name_owners")
|
|
520
|
+
.values({ name: p.name, package_name: staged.packageName })
|
|
521
|
+
.onConflict((oc) => oc.column("name").doNothing())
|
|
522
|
+
.execute();
|
|
523
|
+
await assertPluginNameOwner(trx, p.name, staged.packageName);
|
|
524
|
+
const { added, removed } = diffPermissions(snapRow?.permissions, set);
|
|
525
|
+
await writeSnapshot(trx, p.name, p.version, set, {
|
|
526
|
+
source: review.source.kind === "bootstrap" ? "bootstrap" : actor === null ? "cli" : "web",
|
|
527
|
+
approvedBy: actor,
|
|
528
|
+
...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
|
|
529
|
+
audit: {
|
|
530
|
+
added,
|
|
531
|
+
removed,
|
|
532
|
+
sha256: p.sha256,
|
|
533
|
+
app: { name: review.name, version: review.version },
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
return snapRow
|
|
537
|
+
? {
|
|
538
|
+
version: snapRow.version,
|
|
539
|
+
permissions: snapRow.permissions,
|
|
540
|
+
approvedBy: snapRow.approved_by,
|
|
541
|
+
approvedAt: snapRow.approved_at,
|
|
542
|
+
source: snapRow.source,
|
|
543
|
+
}
|
|
544
|
+
: null;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Deploys the plugin's extension after the commit: `install` (a new install is disabled, a version
|
|
548
|
+
* change keeps the desired state) then `enable`. Nothing is thrown; the result says what happened.
|
|
549
|
+
* When an upgrade was not installed, the snapshot overwritten at confirm is restored (the old
|
|
550
|
+
* version keeps running, F-O; audit `extension.permissions_restored`). Retry through the extension
|
|
551
|
+
* admin (the package is already in the package store) with the sha256 from the result.
|
|
552
|
+
*/
|
|
553
|
+
async deploy(actor, review, itemId, p, previous, opts, enable) {
|
|
554
|
+
const base = {
|
|
555
|
+
name: p.name,
|
|
556
|
+
action: p.action,
|
|
557
|
+
version: p.version,
|
|
558
|
+
sha256: p.sha256,
|
|
559
|
+
};
|
|
560
|
+
const failure = (e) => e instanceof ApiError
|
|
561
|
+
? { code: e.code, message: e.message }
|
|
562
|
+
: { code: "internal", message: "Internal error" };
|
|
563
|
+
try {
|
|
564
|
+
await this.d.ops.install(p.name, p.sha256, actor, opts.actorTokenId, {
|
|
565
|
+
catalogItemId: itemId,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
catch (e) {
|
|
569
|
+
this.d.log.warn("installing a plugin's extension on the deployment failed", {
|
|
570
|
+
plugin: review.name,
|
|
571
|
+
err: e,
|
|
572
|
+
});
|
|
573
|
+
const result = { ...base, result: "failed", ...failure(e) };
|
|
574
|
+
if (p.action === "upgrade" && previous !== undefined) {
|
|
575
|
+
try {
|
|
576
|
+
if (!(await this.restoreSnapshot(actor, review, p, previous, opts))) {
|
|
577
|
+
result.note =
|
|
578
|
+
"snapshot_not_restored: the permission approval changed after the import was confirmed, so it was left as it is";
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
this.d.log.error("cannot restore the permission snapshot of a plugin's extension", {
|
|
583
|
+
plugin: p.name,
|
|
584
|
+
err,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
if (!enable)
|
|
591
|
+
return { ...base, result: "installed" };
|
|
592
|
+
try {
|
|
593
|
+
await this.d.ops.enable(p.name, actor, opts.actorTokenId);
|
|
594
|
+
return { ...base, result: "enabled" };
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
this.d.log.warn("enabling a plugin's extension failed", { plugin: p.name, err: e });
|
|
598
|
+
return { ...base, result: "failed", ...failure(e) };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Restores the snapshot overwritten at confirm (deletes it when there was none before), audits
|
|
603
|
+
* `extension.permissions_restored`. Compare-and-write (row locked): restores only while the
|
|
604
|
+
* snapshot is still the one written at confirm (this version, approved by this actor); if someone
|
|
605
|
+
* approved or changed it meanwhile it is left alone and false is returned.
|
|
606
|
+
*/
|
|
607
|
+
async restoreSnapshot(actor, review, p, prev, opts) {
|
|
608
|
+
return this.d.db.transaction().execute(async (trx) => {
|
|
609
|
+
await sql `SELECT pg_advisory_xact_lock(hashtext(${`capybara.plugin-approve:${p.name}`}))`.execute(trx);
|
|
610
|
+
const current = await trx
|
|
611
|
+
.selectFrom("extension_permission_snapshots")
|
|
612
|
+
.select(["version", "approved_by"])
|
|
613
|
+
.where("plugin", "=", p.name)
|
|
614
|
+
.forUpdate()
|
|
615
|
+
.executeTakeFirst();
|
|
616
|
+
if (current?.version !== p.version || current.approved_by !== actor)
|
|
617
|
+
return false;
|
|
618
|
+
if (prev) {
|
|
619
|
+
await trx
|
|
620
|
+
.updateTable("extension_permission_snapshots")
|
|
621
|
+
.set({
|
|
622
|
+
version: prev.version,
|
|
623
|
+
permissions: JSON.stringify(prev.permissions),
|
|
624
|
+
approved_by: prev.approvedBy,
|
|
625
|
+
approved_at: prev.approvedAt,
|
|
626
|
+
source: prev.source,
|
|
627
|
+
})
|
|
628
|
+
.where("plugin", "=", p.name)
|
|
629
|
+
.execute();
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
await trx
|
|
633
|
+
.deleteFrom("extension_permission_snapshots")
|
|
634
|
+
.where("plugin", "=", p.name)
|
|
635
|
+
.execute();
|
|
636
|
+
}
|
|
637
|
+
await audit(trx, {
|
|
638
|
+
...(actor !== null && { actorAccountId: actor }),
|
|
639
|
+
...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
|
|
640
|
+
action: "extension.permissions_restored",
|
|
641
|
+
target: p.name,
|
|
642
|
+
detail: {
|
|
643
|
+
plugin: p.name,
|
|
644
|
+
restoredVersion: prev?.version ?? null,
|
|
645
|
+
reason: "plugin_deploy_failed",
|
|
646
|
+
app: { name: review.name, version: review.version },
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
return true;
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Deletes expired staging rows and their object prefixes (maintenance job `app-staging-cleanup`).
|
|
654
|
+
* Row by row: objects first, then the row (a failed object delete keeps the row for the next
|
|
655
|
+
* round); concurrent replicas skip each other's rows (`SKIP LOCKED`). Returns the rows deleted.
|
|
656
|
+
*/
|
|
657
|
+
async cleanupExpired() {
|
|
658
|
+
let total = 0;
|
|
659
|
+
for (;;) {
|
|
660
|
+
const batch = await this.d.db
|
|
661
|
+
.selectFrom("plugin_import_staging")
|
|
662
|
+
.select(["id", "object_prefix"])
|
|
663
|
+
.where("expires_at", "<=", sql `now()`)
|
|
664
|
+
.orderBy("expires_at")
|
|
665
|
+
.limit(CLEANUP_BATCH)
|
|
666
|
+
.execute();
|
|
667
|
+
if (batch.length === 0)
|
|
668
|
+
return total;
|
|
669
|
+
let progressed = 0;
|
|
670
|
+
for (const r of batch) {
|
|
671
|
+
try {
|
|
672
|
+
const done = await this.d.db.transaction().execute(async (trx) => {
|
|
673
|
+
const locked = await trx
|
|
674
|
+
.selectFrom("plugin_import_staging")
|
|
675
|
+
.select("id")
|
|
676
|
+
.where("id", "=", r.id)
|
|
677
|
+
.where("expires_at", "<=", sql `now()`)
|
|
678
|
+
.forUpdate()
|
|
679
|
+
.skipLocked()
|
|
680
|
+
.executeTakeFirst();
|
|
681
|
+
if (!locked)
|
|
682
|
+
return false;
|
|
683
|
+
if (r.object_prefix)
|
|
684
|
+
await this.d.storage.deployment().deletePrefix(r.object_prefix);
|
|
685
|
+
await trx.deleteFrom("plugin_import_staging").where("id", "=", r.id).execute();
|
|
686
|
+
return true;
|
|
687
|
+
});
|
|
688
|
+
if (done) {
|
|
689
|
+
total++;
|
|
690
|
+
progressed++;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
this.d.log.warn("cannot clean up an expired plugin import", { stagingId: r.id, err });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (progressed === 0 || batch.length < CLEANUP_BATCH)
|
|
698
|
+
return total;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// ---------------------------------------------------------------------------------------------
|
|
702
|
+
/** npm / marketplace imports: the package must be the one that was asked for. */
|
|
703
|
+
checkExpected(pkg, expect, format = "capybara") {
|
|
704
|
+
const packageName = packageNameOf(pkg);
|
|
705
|
+
if (expect?.packageName !== undefined && packageName !== expect.packageName) {
|
|
706
|
+
throw invalid([
|
|
707
|
+
`package.json name "${packageName}" does not match the requested npm package ${expect.packageName}`,
|
|
708
|
+
]);
|
|
709
|
+
}
|
|
710
|
+
const e = expect?.marketplace;
|
|
711
|
+
if (!e)
|
|
712
|
+
return;
|
|
713
|
+
if (e.packageName !== undefined && packageName !== e.packageName) {
|
|
714
|
+
throw mismatch(`package.json name is "${packageName}", the entry says ${e.packageName}`);
|
|
715
|
+
}
|
|
716
|
+
if (pkg.manifest.name !== e.name) {
|
|
717
|
+
throw mismatch(`the plugin is named "${pkg.manifest.name}", the entry says ${e.name}`);
|
|
718
|
+
}
|
|
719
|
+
// Claude Code's rule: plugin.json's version wins over the entry's (spec §5.3 order)
|
|
720
|
+
if (e.version !== null && format === "capybara" && pkg.manifest.version !== e.version) {
|
|
721
|
+
throw mismatch(`the plugin's version is ${pkg.manifest.version}, the entry says ${e.version}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/** A config-declared extension's name cannot be taken by an imported plugin. */
|
|
725
|
+
assertNotDeclared(name) {
|
|
726
|
+
const declared = this.d.declared().get(name);
|
|
727
|
+
if (declared === "config") {
|
|
728
|
+
throw new ApiError("extension.config_declared", `Extension ${name} is declared in the startup config (extensions); remove it there before importing a plugin with this name`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Skills (by the skill package rules), connector templates (against the loaded connector) and
|
|
733
|
+
* icons; returns the files to store and the review's skills and templates.
|
|
734
|
+
*/
|
|
735
|
+
checkContent(pkg, format = "capybara") {
|
|
736
|
+
const m = pkg.manifest;
|
|
737
|
+
const notLoaded = [];
|
|
738
|
+
const templatePaths = new Map();
|
|
739
|
+
const pinned = new Map((m.skills ?? []).map((s) => [packagePath(s.path), s.pinned === true]));
|
|
740
|
+
const problems = [];
|
|
741
|
+
const stored = new Map();
|
|
742
|
+
const keep = (path) => {
|
|
743
|
+
const data = pkg.files.get(path);
|
|
744
|
+
if (data !== undefined && !stored.has(path))
|
|
745
|
+
stored.set(path, fileOf(path, data));
|
|
746
|
+
};
|
|
747
|
+
if (m.icon !== undefined)
|
|
748
|
+
keep(packagePath(m.icon));
|
|
749
|
+
const skills = [];
|
|
750
|
+
for (const s of pkg.skills) {
|
|
751
|
+
const files = s.files.map((f) => fileOf(f, pkg.files.get(`${s.dir}/${f}`)));
|
|
752
|
+
try {
|
|
753
|
+
// A translated Claude Code plugin keeps its SKILL.md files as written: their invocation
|
|
754
|
+
// keys are read leniently (the translation report lists the ones ignored)
|
|
755
|
+
const invocationKeys = format !== "claude-code";
|
|
756
|
+
const r = checkSkillFiles(files, { invocationKeys });
|
|
757
|
+
const md = parseSkillMd(new TextDecoder().decode(pkg.files.get(`${s.dir}/${SKILL_ENTRY_FILE}`)), { invocationKeys });
|
|
758
|
+
skills.push({
|
|
759
|
+
name: r.name,
|
|
760
|
+
description: r.description,
|
|
761
|
+
body: "errors" in md ? "" : md.body,
|
|
762
|
+
path: `./${s.dir}`,
|
|
763
|
+
files: files.length,
|
|
764
|
+
suggestPinned: pinned.get(s.dir) === true,
|
|
765
|
+
});
|
|
766
|
+
for (const f of s.files)
|
|
767
|
+
keep(`${s.dir}/${f}`);
|
|
768
|
+
}
|
|
769
|
+
catch (e) {
|
|
770
|
+
if (!(e instanceof SkillArchiveError))
|
|
771
|
+
throw e;
|
|
772
|
+
for (const p of e.problems)
|
|
773
|
+
problems.push(`${s.dir}: ${p}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const local = new Set([
|
|
777
|
+
...(m.extension ? [m.name] : []),
|
|
778
|
+
...(m.dependencies ?? []).map((d) => d.name),
|
|
779
|
+
]);
|
|
780
|
+
const templates = pkg.connectorTemplates.map((c) => {
|
|
781
|
+
const t = c.file.template;
|
|
782
|
+
const at = `${c.path}: template`;
|
|
783
|
+
// The result depends on which connectors the replica serving this request has loaded right
|
|
784
|
+
// now: it is the import-time gate, not the last word (an org install checks again against the
|
|
785
|
+
// connector of the moment)
|
|
786
|
+
const loaded = this.d.findConnector(t.plugin, t.connector);
|
|
787
|
+
if (loaded) {
|
|
788
|
+
problems.push(...templateProblems(loaded, t, at));
|
|
789
|
+
const v = this.d.pluginVersion?.(t.plugin);
|
|
790
|
+
if (t.pluginVersion !== undefined &&
|
|
791
|
+
v !== undefined &&
|
|
792
|
+
!satisfiesRange(v, t.pluginVersion)) {
|
|
793
|
+
problems.push(`${at}.pluginVersion: extension ${t.plugin} is loaded at version ${v}, which does not satisfy ${t.pluginVersion}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else if (local.has(t.plugin) ||
|
|
797
|
+
this.d.connectorsLoaded === false ||
|
|
798
|
+
(format === "claude-code" && t.plugin === CLAUDE_MCP_CONNECTOR.plugin)) {
|
|
799
|
+
// The connector comes with this plugin or a dependency (or this process loads no
|
|
800
|
+
// connectors, or a translated Claude template targets connector-mcp, which may not be
|
|
801
|
+
// loaded: Decisions 2): static checks only now
|
|
802
|
+
for (const leak of endpointCredentialIssues(t.endpoint)) {
|
|
803
|
+
problems.push(`${at}.${leak.path}: ${leak.message}`);
|
|
804
|
+
}
|
|
805
|
+
if (format === "claude-code")
|
|
806
|
+
notLoaded.push(c.id);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
problems.push(`${at}.connector: extension ${t.plugin} providing connector "${t.connector}" is not loaded in this deployment and is neither this plugin's extension nor a dependency`);
|
|
810
|
+
}
|
|
811
|
+
keep(c.path);
|
|
812
|
+
templatePaths.set(c.id, c.path);
|
|
813
|
+
if (c.file.icon !== undefined)
|
|
814
|
+
keep(packagePath(c.file.icon));
|
|
815
|
+
return {
|
|
816
|
+
id: c.id,
|
|
817
|
+
displayName: c.file.displayName,
|
|
818
|
+
description: c.file.description ?? "",
|
|
819
|
+
plugin: t.plugin,
|
|
820
|
+
connector: t.connector,
|
|
821
|
+
endpoint: t.endpoint,
|
|
822
|
+
auth: t.auth,
|
|
823
|
+
editableEndpoint: t.editableEndpoint ?? [],
|
|
824
|
+
endpointChecked: loaded !== undefined,
|
|
825
|
+
};
|
|
826
|
+
});
|
|
827
|
+
const agentTemplates = pkg.agentTemplates.map((a) => {
|
|
828
|
+
keep(a.path);
|
|
829
|
+
const { name, description, instructions, model, ...rest } = a.template;
|
|
830
|
+
return { name, path: a.path, description, instructions, model: model ?? "inherit", ...rest };
|
|
831
|
+
});
|
|
832
|
+
if (problems.length > 0)
|
|
833
|
+
throw invalid(problems);
|
|
834
|
+
return {
|
|
835
|
+
files: [...stored.values()],
|
|
836
|
+
templates,
|
|
837
|
+
skills,
|
|
838
|
+
agentTemplates,
|
|
839
|
+
notLoaded,
|
|
840
|
+
templatePaths,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* The highest imported, non-deleted catalog version satisfying each dependency's range (null when
|
|
845
|
+
* none does).
|
|
846
|
+
*/
|
|
847
|
+
async resolveDependencies(m) {
|
|
848
|
+
const deps = m.dependencies ?? [];
|
|
849
|
+
if (deps.length === 0)
|
|
850
|
+
return [];
|
|
851
|
+
const rows = await this.d.db
|
|
852
|
+
.selectFrom("catalog_items as i")
|
|
853
|
+
.innerJoin("catalog_item_versions as v", "v.item_id", "i.id")
|
|
854
|
+
.select(["i.name", "v.version", "v.imported_at"])
|
|
855
|
+
.where("i.deleted_at", "is", null)
|
|
856
|
+
.where("i.name", "in", deps.map((d) => d.name))
|
|
857
|
+
.execute();
|
|
858
|
+
return deps.map((d) => ({
|
|
859
|
+
name: d.name,
|
|
860
|
+
range: d.version,
|
|
861
|
+
resolved: maxSatisfying(rows
|
|
862
|
+
.filter((r) => r.name === d.name)
|
|
863
|
+
.map((r) => ({ version: r.version, importedAt: r.imported_at })), d.version)?.version ?? null,
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* The extension's state on the deployment; installed from another npm package ->
|
|
868
|
+
* `extension.name_conflict`.
|
|
869
|
+
*/
|
|
870
|
+
async extensionState(name, packageName, db = this.d.db) {
|
|
871
|
+
const r = await db
|
|
872
|
+
.selectFrom("extension_installs")
|
|
873
|
+
.innerJoin("extension_packages", "extension_packages.sha256", "extension_installs.sha256")
|
|
874
|
+
.select([
|
|
875
|
+
"extension_installs.version",
|
|
876
|
+
"extension_installs.sha256",
|
|
877
|
+
"extension_packages.package_name",
|
|
878
|
+
])
|
|
879
|
+
.where("extension_installs.name", "=", name)
|
|
880
|
+
.executeTakeFirst();
|
|
881
|
+
if (r && r.package_name !== packageName) {
|
|
882
|
+
throw new ApiError("extension.name_conflict", `Extension ${name} is already installed from package ${r.package_name}; package ${packageName} cannot use it`);
|
|
883
|
+
}
|
|
884
|
+
return r
|
|
885
|
+
? { kind: "installed", version: r.version, sha256: r.sha256, packageName: r.package_name }
|
|
886
|
+
: { kind: "absent" };
|
|
887
|
+
}
|
|
888
|
+
/** The review of the extension: permissions, the change against the snapshot, warnings, action. */
|
|
889
|
+
async reviewExtension(pkg, state) {
|
|
890
|
+
const name = pkg.section.name;
|
|
891
|
+
const snap = await this.d.db
|
|
892
|
+
.selectFrom("extension_permission_snapshots")
|
|
893
|
+
.select("permissions")
|
|
894
|
+
.where("plugin", "=", name)
|
|
895
|
+
.executeTakeFirst();
|
|
896
|
+
const set = permissionSetOfPackage(pkg);
|
|
897
|
+
const warnings = migrationWarnings(pkg.migrationSql, name);
|
|
898
|
+
const { added, removed } = diffPermissions(snap?.permissions, set);
|
|
899
|
+
const permissions = permissionGroups(set);
|
|
900
|
+
return {
|
|
901
|
+
name,
|
|
902
|
+
version: pkg.section.version,
|
|
903
|
+
sha256: pkg.sha256,
|
|
904
|
+
...actionOf(state, pkg.sha256),
|
|
905
|
+
permissions,
|
|
906
|
+
added,
|
|
907
|
+
addedItems: permissionKeyItems(added),
|
|
908
|
+
removed,
|
|
909
|
+
warnings,
|
|
910
|
+
reviewHash: extensionReviewHash(set, warnings),
|
|
911
|
+
sensitive: permissions.some((g) => g.sensitive) || warnings.length > 0,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
async readStored(sha256) {
|
|
915
|
+
const bytes = await this.d.blobs.get(sha256);
|
|
916
|
+
return readingPackage(() => readExtensionPackage(bytes, {
|
|
917
|
+
maxBytes: this.d.config().packages.maxBytes,
|
|
918
|
+
hostSdkVersion: SDK_VERSION,
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
async itemView(itemId) {
|
|
922
|
+
const item = await this.d.db
|
|
923
|
+
.selectFrom("catalog_items")
|
|
924
|
+
.selectAll()
|
|
925
|
+
.where("id", "=", itemId)
|
|
926
|
+
.executeTakeFirstOrThrow();
|
|
927
|
+
const versions = await this.d.db
|
|
928
|
+
.selectFrom("catalog_item_versions")
|
|
929
|
+
.select(["version", "source", "imported_at"])
|
|
930
|
+
.where("item_id", "=", itemId)
|
|
931
|
+
.execute();
|
|
932
|
+
return itemView(item, versions);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* plugin.json as stored on the version: skills and connector templates listed explicitly (what was
|
|
937
|
+
* scanned when the manifest omitted them), `pinned` kept from the listing.
|
|
938
|
+
*/
|
|
939
|
+
function normalizedManifest(pkg) {
|
|
940
|
+
const pinned = new Map((pkg.manifest.skills ?? []).map((s) => [packagePath(s.path), s.pinned]));
|
|
941
|
+
const { agents: _, ...manifest } = pkg.manifest;
|
|
942
|
+
return {
|
|
943
|
+
...manifest,
|
|
944
|
+
skills: pkg.skills.map((s) => {
|
|
945
|
+
const p = pinned.get(s.dir);
|
|
946
|
+
return { path: `./${s.dir}`, ...(p !== undefined && { pinned: p }) };
|
|
947
|
+
}),
|
|
948
|
+
connectorTemplates: pkg.connectorTemplates.map((t) => ({ path: `./${t.path}` })),
|
|
949
|
+
// listed only when there are any, so the stored manifests of other plugins stay as they were
|
|
950
|
+
...(pkg.agentTemplates.length > 0 && {
|
|
951
|
+
agents: pkg.agentTemplates.map((a) => ({ path: `./${a.path}` })),
|
|
952
|
+
}),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
/** package.json `name`, or null without one (a git plugin; never the string "undefined"). */
|
|
956
|
+
function packageNameOf(pkg) {
|
|
957
|
+
const name = pkg.packageJson.name;
|
|
958
|
+
return typeof name === "string" ? name : null;
|
|
959
|
+
}
|
|
960
|
+
/** The extension's deployment state -> action and current version. */
|
|
961
|
+
function actionOf(s, sha256) {
|
|
962
|
+
if (s.kind === "installed") {
|
|
963
|
+
return {
|
|
964
|
+
action: s.sha256 === sha256 ? "keep" : "upgrade",
|
|
965
|
+
deployed: true,
|
|
966
|
+
currentVersion: s.version,
|
|
967
|
+
currentSha256: s.sha256,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
return { action: "install", deployed: false, currentVersion: null, currentSha256: null };
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* The review digest (first 32 hex of a sha256): name, version and content digest, the extension (sha256, action,
|
|
974
|
+
* current version / package, its own review digest), every template field, each skill's name / path
|
|
975
|
+
* / file count, the dependency resolution, the sha256 of every stored file, the format, the
|
|
976
|
+
* translation report's items, the sha256 of the license text and the two review flags
|
|
977
|
+
* (`claudeManifestIgnored`, `licenseTruncated`), and the agent templates when there are any.
|
|
978
|
+
* Exported for tests. `repository` is display only.
|
|
979
|
+
*/
|
|
980
|
+
export function pluginReviewHash(r, files) {
|
|
981
|
+
const e = r.extension;
|
|
982
|
+
const lic = r.licenseText;
|
|
983
|
+
const canonical = JSON.stringify([
|
|
984
|
+
r.name,
|
|
985
|
+
r.version,
|
|
986
|
+
r.contentDigest,
|
|
987
|
+
e ? [e.name, e.sha256, e.action, e.currentVersion, e.currentSha256, e.reviewHash] : null,
|
|
988
|
+
r.connectorTemplates.map((t) => [
|
|
989
|
+
t.id,
|
|
990
|
+
t.plugin,
|
|
991
|
+
t.connector,
|
|
992
|
+
t.endpoint,
|
|
993
|
+
t.auth,
|
|
994
|
+
t.editableEndpoint,
|
|
995
|
+
]),
|
|
996
|
+
r.skills.map((s) => [s.name, s.path, s.files]),
|
|
997
|
+
r.dependencies.map((d) => [d.name, d.range, d.resolved]),
|
|
998
|
+
[...files].sort((a, b) => a.path.localeCompare(b.path)).map((f) => [f.path, f.sha256]),
|
|
999
|
+
// phase B: the format, the translation report (in report order) and the license text
|
|
1000
|
+
r.format,
|
|
1001
|
+
r.translation?.items.map((i) => [i.group, i.component, i.name, i.path, i.code, i.detail]) ??
|
|
1002
|
+
null,
|
|
1003
|
+
lic !== undefined ? createHash("sha256").update(lic).digest("hex") : null,
|
|
1004
|
+
r.claudeManifestIgnored === true,
|
|
1005
|
+
r.licenseTruncated === true,
|
|
1006
|
+
// phase C: agent templates, only when there are any (earlier digests stay the same)
|
|
1007
|
+
...((r.agentTemplates ?? []).length > 0
|
|
1008
|
+
? [(r.agentTemplates ?? []).map((a) => [a.path, canonicalJson(a)])]
|
|
1009
|
+
: []),
|
|
1010
|
+
]);
|
|
1011
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* A version change that is a downgrade (the imported version below the installed one) whose
|
|
1015
|
+
* version has migrations not run on this deployment -> `extension.downgrade_unsupported` (the same
|
|
1016
|
+
* rule as `ExtensionAdminOps.install`: extension migrations are expand-only, a downgrade can only go
|
|
1017
|
+
* back to a version whose migrations have all run).
|
|
1018
|
+
*/
|
|
1019
|
+
async function assertDowngradable(db, p, migrationSql) {
|
|
1020
|
+
if (p.action !== "upgrade" || p.currentVersion === null)
|
|
1021
|
+
return;
|
|
1022
|
+
if (!semver.valid(p.currentVersion) || !semver.lt(p.version, p.currentVersion))
|
|
1023
|
+
return;
|
|
1024
|
+
const unrun = await unrunMigrations(db, p.name, migrationSql);
|
|
1025
|
+
if (unrun.length > 0) {
|
|
1026
|
+
throw new ApiError("extension.downgrade_unsupported", `The plugin's extension ${p.name} ${p.version} is older than the installed ${p.currentVersion}, and that version has migrations that were never run here (${unrun.join(", ")}); extension migrations only expand, so this downgrade is not possible`, unrun.map((file) => ({ file })));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/** The root license text (spec §8.4; any plugin): the first of {@link LICENSE_FILES}, UTF-8. */
|
|
1030
|
+
function licenseOf(files) {
|
|
1031
|
+
for (const f of LICENSE_FILES) {
|
|
1032
|
+
const data = files.get(f);
|
|
1033
|
+
if (data === undefined)
|
|
1034
|
+
continue;
|
|
1035
|
+
if (data.byteLength > LICENSE_TEXT_MAX_BYTES)
|
|
1036
|
+
return { truncated: true };
|
|
1037
|
+
try {
|
|
1038
|
+
return { text: new TextDecoder("utf-8", { fatal: true }).decode(data), truncated: false };
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
return { truncated: true };
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return { truncated: false };
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* The manifest with the confirm's suggested pinning: `names` (each a skill of the review, else
|
|
1048
|
+
* `validation_failed`) marks those skills `pinned: true` and clears the flag on the others;
|
|
1049
|
+
* undefined keeps the plugin's flags.
|
|
1050
|
+
*/
|
|
1051
|
+
function withPinned(m, review, names) {
|
|
1052
|
+
if (names === undefined)
|
|
1053
|
+
return m;
|
|
1054
|
+
const byName = new Map(review.skills.map((s) => [s.name, s.path]));
|
|
1055
|
+
const unknown = names.filter((n) => !byName.has(n));
|
|
1056
|
+
if (unknown.length > 0) {
|
|
1057
|
+
throw new ApiError("validation_failed", `pinnedSuggestions: ${unknown.map((n) => JSON.stringify(n)).join(", ")} ${unknown.length === 1 ? "is not a skill" : "are not skills"} of this plugin`);
|
|
1058
|
+
}
|
|
1059
|
+
const paths = new Set(names.map((n) => byName.get(n)));
|
|
1060
|
+
return {
|
|
1061
|
+
...m,
|
|
1062
|
+
skills: (m.skills ?? []).map(({ pinned: _, ...s }) => paths.has(s.path) ? { ...s, pinned: true } : s),
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/** Names of the skills the stored manifest suggests pinning (for the audit). */
|
|
1066
|
+
function pinnedNames(m, review) {
|
|
1067
|
+
const byPath = new Map(review.skills.map((s) => [s.path, s.name]));
|
|
1068
|
+
return (m.skills ?? []).filter((s) => s.pinned === true).map((s) => byPath.get(s.path) ?? s.path);
|
|
1069
|
+
}
|