@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,2071 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { EXTENSION_POINT_CARDINALITY, parseExtensionPoint, SDK_VERSION, } from "@capybara-hq/plugin-sdk";
|
|
7
|
+
import { changedPaths, configFields, getPath, secretValues, setPath, unreachableSecrets, } from "../config/fields.js";
|
|
8
|
+
import { pluginSchemaName, queryEnabledPlugins, } from "../ctx/create-ctx.js";
|
|
9
|
+
import { kernelDb } from "../db/kernel-schema.js";
|
|
10
|
+
import { migrate } from "../db/migrator.js";
|
|
11
|
+
import { createDb } from "../db/pool.js";
|
|
12
|
+
import { packagePath } from "../packages/tgz.js";
|
|
13
|
+
import { ExtensionApprovals, planApprovals, withPreApproved, } from "./approvals.js";
|
|
14
|
+
import { ExtensionBlobIntegrityError } from "./blobs.js";
|
|
15
|
+
import { DEFAULT_HOOK_TIMEOUT_MS, HookRegistry } from "./hooks.js";
|
|
16
|
+
import { trackCalls } from "./inflight.js";
|
|
17
|
+
import { DEFAULT_DISPOSE_TIMEOUT_MS, ImportTracker, importExtensionModule, ManifestMismatchError, matchesSection, setupInstance, warnUpgrades, } from "./instance.js";
|
|
18
|
+
import { toIssues } from "./org-config.js";
|
|
19
|
+
import { permissionSetOfSection, readMigrationSql } from "./package.js";
|
|
20
|
+
import { blockingKeys, computePermissionSet, UI_VIEW_KEY_PREFIX, } from "./permission-set.js";
|
|
21
|
+
import { ExtensionRegistry } from "./registry.js";
|
|
22
|
+
import { ExtensionResolveError, resolveExtensions } from "./resolve.js";
|
|
23
|
+
import { PackageShaMismatchError, packageRootOf } from "./sync.js";
|
|
24
|
+
/** 键排序后的 JSON(对象键顺序不影响结果);undefined 字段与 JSON.stringify 一样省略。 */
|
|
25
|
+
const canonicalJson = (v) => {
|
|
26
|
+
if (Array.isArray(v))
|
|
27
|
+
return `[${v.map((x) => (x === undefined ? "null" : canonicalJson(x))).join(",")}]`;
|
|
28
|
+
if (v !== null && typeof v === "object") {
|
|
29
|
+
const entries = Object.entries(v)
|
|
30
|
+
.filter(([, x]) => x !== undefined)
|
|
31
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
32
|
+
return `{${entries.map(([k, x]) => `${JSON.stringify(k)}:${canonicalJson(x)}`).join(",")}}`;
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(v) ?? "null";
|
|
35
|
+
};
|
|
36
|
+
/** 遮蔽 secret 字段后参与摘要的固定占位值。 */
|
|
37
|
+
const SECRET_PLACEHOLDER = "<secret>";
|
|
38
|
+
/**
|
|
39
|
+
* 插件部署配置(校验后)中非密钥部分的摘要:先把 schema 里所有 secret 叶子(`configFields` 可达路径,
|
|
40
|
+
* 加载时 `assertSecretsReachable` 已保证密钥都在这些路径上)换成固定占位值,再取 sha256(键排序 JSON) 前 16 位十六进制。
|
|
41
|
+
* 给需要「配置变了就失效」的缓存键用(如模型列表)。密钥不参与摘要:不加盐的短摘要写进 Redis 键名后,
|
|
42
|
+
* 低熵密钥可被离线猜测、泄露的密钥可被确认;代价是轮换密钥不会让缓存失效(缓存内容本来就不该依赖密钥)。
|
|
43
|
+
* 没有部署配置 schema 时无从得知哪些是密钥,摘要不按配置区分。同一份配置在各副本上得到同一摘要。
|
|
44
|
+
* 函数自己按 schema 遮蔽,调用方不必(也不能忘记)先打码。
|
|
45
|
+
*/
|
|
46
|
+
export function deploymentConfigHash(schema, config) {
|
|
47
|
+
let redacted = null;
|
|
48
|
+
if (schema) {
|
|
49
|
+
redacted = structuredClone(config);
|
|
50
|
+
if (redacted !== null && typeof redacted === "object") {
|
|
51
|
+
for (const f of configFields(schema)) {
|
|
52
|
+
if (f.meta?.secret && getPath(redacted, f.path) !== undefined) {
|
|
53
|
+
setPath(redacted, f.path, SECRET_PLACEHOLDER);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return createHash("sha256").update(canonicalJson(redacted)).digest("hex").slice(0, 16);
|
|
59
|
+
}
|
|
60
|
+
const CLOSED_DURING_LOAD = "plugin host closed during load";
|
|
61
|
+
/** 撤销实例时在途调用的默认排空时限(`extensionHost.drainSeconds` 的默认值)。 */
|
|
62
|
+
const DEFAULT_DRAIN_MS = 30_000;
|
|
63
|
+
/** 运行中重新加载时 setup 的默认时限(`extensionHost.reloadSetupTimeoutSeconds` 的默认值)。 */
|
|
64
|
+
const DEFAULT_RELOAD_SETUP_TIMEOUT_MS = 60_000;
|
|
65
|
+
/** 停机时等进行中的 unload / reload 的上限(之后直接关闭 setup 中的 ctx、撤销其余实例)。 */
|
|
66
|
+
const CLOSE_LIFECYCLE_WAIT_MS = 2_000;
|
|
67
|
+
/** 部署已知插件名(配置声明 / 安装记录)缓存的有效期:路由闸门 503 / 404 用,跨副本靠它收敛。 */
|
|
68
|
+
const KNOWN_NAMES_TTL_MS = 5_000;
|
|
69
|
+
/**
|
|
70
|
+
* 第 2 步里 `requires` 只能由安装记录满足的配置声明插件(C13 的遗留问题):先挑出来,等安装记录加载完再解析。
|
|
71
|
+
* 反复剔除直到不动点(剔除一个插件也拿掉了它提供的能力)。只有 `deferrable` 里的插件会被挑出。
|
|
72
|
+
*/
|
|
73
|
+
function splitDeferred(manifests, deferrable) {
|
|
74
|
+
const deferred = new Set();
|
|
75
|
+
for (let changed = true; changed;) {
|
|
76
|
+
changed = false;
|
|
77
|
+
const provided = new Set(manifests.filter((m) => !deferred.has(m.name)).flatMap((m) => m.provides));
|
|
78
|
+
for (const m of manifests) {
|
|
79
|
+
if (deferred.has(m.name) || !deferrable.has(m.name))
|
|
80
|
+
continue;
|
|
81
|
+
if ((m.requires ?? []).some((c) => !provided.has(c))) {
|
|
82
|
+
deferred.add(m.name);
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
ready: manifests.filter((m) => !deferred.has(m.name)),
|
|
89
|
+
deferred: manifests.filter((m) => deferred.has(m.name)),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const errMsg = (e) => (e instanceof Error ? e.message : String(e));
|
|
93
|
+
/** 把 specifier 解析成模块文件 URL;失败时错误点名 specifier 与 resolveFrom。 */
|
|
94
|
+
function toFileUrl(spec, resolveFrom) {
|
|
95
|
+
try {
|
|
96
|
+
if (spec.startsWith("file:"))
|
|
97
|
+
return spec;
|
|
98
|
+
if (isAbsolute(spec))
|
|
99
|
+
return pathToFileURL(spec).href;
|
|
100
|
+
const file = createRequire(join(resolveFrom, "noop.js")).resolve(spec);
|
|
101
|
+
return pathToFileURL(file).href;
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
throw new Error(`cannot resolve plugin "${spec}" from "${resolveFrom}": ${errMsg(e)}`, {
|
|
105
|
+
cause: e,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** 按顺序导入各 specifier 的插件模块(不排序);同时给出模块所在目录。 */
|
|
110
|
+
async function importEntries(specifiers, resolveFrom) {
|
|
111
|
+
const entries = [];
|
|
112
|
+
for (const spec of specifiers) {
|
|
113
|
+
const url = toFileUrl(spec, resolveFrom);
|
|
114
|
+
entries.push({
|
|
115
|
+
manifest: await importExtensionModule(url, spec, resolveFrom),
|
|
116
|
+
dir: dirname(fileURLToPath(url)),
|
|
117
|
+
url,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return entries;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The `name` (npm package name) of the nearest `package.json` above the module's directory; undefined
|
|
124
|
+
* when there is none. Config-declared extensions register their name ownership with it
|
|
125
|
+
* (`extension_name_owners`, I4).
|
|
126
|
+
*/
|
|
127
|
+
async function packageNameOf(dir) {
|
|
128
|
+
for (let d = resolvePath(dir);;) {
|
|
129
|
+
try {
|
|
130
|
+
const pj = JSON.parse(await readFile(join(d, "package.json"), "utf8"));
|
|
131
|
+
if (typeof pj.name === "string" && pj.name !== "")
|
|
132
|
+
return pj.name;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// 没有或读不了:继续往上
|
|
136
|
+
}
|
|
137
|
+
const up = dirname(d);
|
|
138
|
+
if (up === d)
|
|
139
|
+
return undefined;
|
|
140
|
+
d = up;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** 计算各插件的权限集合(按插件名索引)。 */
|
|
144
|
+
async function permissionSets(ordered, dirs) {
|
|
145
|
+
const sets = new Map();
|
|
146
|
+
for (const m of ordered) {
|
|
147
|
+
sets.set(m.name, await computePermissionSet(m, dirs.get(m.name)));
|
|
148
|
+
}
|
|
149
|
+
return sets;
|
|
150
|
+
}
|
|
151
|
+
/** 待批准插件的 warn 日志:点名插件、原因与新增项(依赖原因写明阻塞的能力与提供方)。 */
|
|
152
|
+
function warnPending(log, p) {
|
|
153
|
+
if (p.reason === "dependency") {
|
|
154
|
+
const missing = (p.blockedBy ?? []).some((b) => b.providers.length === 0);
|
|
155
|
+
log.warn(missing
|
|
156
|
+
? "extension not loaded: an extension point it needs has no provider (e.g. permissions.storage needs an object-store@1 extension such as object-store-s3)"
|
|
157
|
+
: "extension not loaded: a required extension point is only provided by extensions awaiting permission approval", { plugin: p.plugin, version: p.version, blockedBy: p.blockedBy });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
log.warn("extension not loaded: permissions awaiting approval (an enterprise admin approves it in the admin console, or run `capybara extensions review`, then `capybara extensions approve <name>`; running replicas load it without a restart)", { plugin: p.plugin, version: p.version, reason: p.reason, added: p.added, removed: p.removed });
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 跑单个插件的迁移(`migrationsDir` 为迁移目录的绝对路径;没有迁移时为 undefined);返回已应用的文件,
|
|
164
|
+
* 无迁移时返回 undefined。迁移器在会话上持有按 schema 的咨询锁:多副本同时启动时只有一个在执行,其余等它结束后
|
|
165
|
+
* 发现都已应用。
|
|
166
|
+
*/
|
|
167
|
+
async function migratePlugin(dbUrl, manifest, migrationsDir, connectTimeoutMs, signal) {
|
|
168
|
+
if (!migrationsDir)
|
|
169
|
+
return undefined;
|
|
170
|
+
const schema = pluginSchemaName(manifest.name);
|
|
171
|
+
try {
|
|
172
|
+
const { applied } = await migrate(dbUrl, { schema, dir: migrationsDir }, { connectTimeoutMs, signal });
|
|
173
|
+
return { schema, applied };
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
throw new Error(`plugin ${manifest.name}: migration failed: ${errMsg(e)}`, { cause: e });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 插件两级配置 schema 中的每个 `secret()` 都必须标在对象路径的字段上(见 `unreachableSecrets`),
|
|
181
|
+
* 否则内核无法把它单独加密存储 / 打码、会按明文保存与回显——此时拒绝加载并点名插件与路径。
|
|
182
|
+
*/
|
|
183
|
+
export function assertSecretsReachable(manifest) {
|
|
184
|
+
for (const level of ["deployment", "org"]) {
|
|
185
|
+
const schema = manifest.config?.[level];
|
|
186
|
+
if (!schema)
|
|
187
|
+
continue;
|
|
188
|
+
const bad = unreachableSecrets(schema);
|
|
189
|
+
if (bad.length > 0) {
|
|
190
|
+
throw new Error(`plugin ${manifest.name}: config.${level}: ${bad.map((p) => `secret() at "${p}"`).join(", ")} is not on an object field path; secret() inside union / intersection / transform / pipe / lazy / array / record / tuple is not supported — mark the whole enclosing object field secret instead`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** 按模块解析的插件(配置声明)的迁移目录:清单 `migrations` 相对模块所在目录。 */
|
|
195
|
+
const migrationsDirOf = (m, dir) => m.migrations ? resolvePath(dir, m.migrations) : undefined;
|
|
196
|
+
/**
|
|
197
|
+
* 由审阅过的静态清单拼出解析依赖、判定权限闸门用的清单(没有 setup 与配置 schema,不能用来加载)。
|
|
198
|
+
* 闸门放行后导入的真实模块必须与静态清单一致({@link matchesSection}),两者在依赖解析上等价。
|
|
199
|
+
*/
|
|
200
|
+
function manifestOfSection(m) {
|
|
201
|
+
return {
|
|
202
|
+
name: m.name,
|
|
203
|
+
version: m.version,
|
|
204
|
+
sdk: m.sdk,
|
|
205
|
+
provides: m.provides,
|
|
206
|
+
requires: m.requires,
|
|
207
|
+
optional: m.optional,
|
|
208
|
+
permissions: m.permissions,
|
|
209
|
+
...(m.ui !== undefined && { ui: m.ui }),
|
|
210
|
+
setup() {
|
|
211
|
+
throw new Error(`plugin ${m.name}: static manifest cannot be set up`);
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** 内核消费的提供方能力:调用计入提供方实例的在途计数(撤销 / 升级时排空,C17)。 */
|
|
216
|
+
const TRACKED_EXTENSION_POINTS = {
|
|
217
|
+
connector: { sessions: ["open"] },
|
|
218
|
+
sandbox: {},
|
|
219
|
+
"model-provider": {},
|
|
220
|
+
};
|
|
221
|
+
/** 插件宿主:解析、校验配置、迁移、setup,并登记插件提供的能力与路由。 */
|
|
222
|
+
export class ExtensionHost {
|
|
223
|
+
deps;
|
|
224
|
+
extensionConfig;
|
|
225
|
+
/**
|
|
226
|
+
* Only resolves extensions and runs migrations in dependency order: no deployment config check, no
|
|
227
|
+
* setup, no Redis (for `capybara migrate`). Like `load()` it checks secret markers first
|
|
228
|
+
* (`assertSecretsReachable`); if any extension has a problem, none is migrated.
|
|
229
|
+
* Returns every extension that declares migrations, with the files applied this time.
|
|
230
|
+
* The same permission snapshot gate as `load()`: extensions pending approval (and those whose
|
|
231
|
+
* `requires` only they satisfy) are not migrated and are reported through `onPending`; needs the
|
|
232
|
+
* kernel migrations to have run (reads `extension_permission_snapshots`). With `autoApprove` all are
|
|
233
|
+
* migrated and no snapshot is written (`load()` writes snapshots).
|
|
234
|
+
*/
|
|
235
|
+
static async migrateOnly(specifiers, opts) {
|
|
236
|
+
const from = resolvePath(opts.resolveFrom ?? process.cwd());
|
|
237
|
+
const entries = await importEntries(specifiers, from);
|
|
238
|
+
const dirs = new Map(entries.map((e) => [e.manifest.name, e.dir]));
|
|
239
|
+
const ordered = resolveExtensions(entries.map((e) => e.manifest), SDK_VERSION);
|
|
240
|
+
for (const manifest of ordered)
|
|
241
|
+
assertSecretsReachable(manifest);
|
|
242
|
+
let toMigrate = ordered;
|
|
243
|
+
if (!opts.autoApprove && ordered.length > 0) {
|
|
244
|
+
const sets = await permissionSets(ordered, dirs);
|
|
245
|
+
const raw = createDb(opts.dbUrl, 1, () => { }, { connectTimeoutMs: opts.connectTimeoutMs });
|
|
246
|
+
let decisions;
|
|
247
|
+
try {
|
|
248
|
+
decisions = planApprovals(ordered, sets, await new ExtensionApprovals(kernelDb(raw)).all());
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await raw.destroy();
|
|
252
|
+
}
|
|
253
|
+
const pending = decisions.flatMap((d) => (d.action === "pending" ? [d.pending] : []));
|
|
254
|
+
if (pending.length > 0)
|
|
255
|
+
opts.onPending?.(pending);
|
|
256
|
+
const skip = new Set(pending.map((p) => p.plugin));
|
|
257
|
+
toMigrate = ordered.filter((m) => !skip.has(m.name));
|
|
258
|
+
}
|
|
259
|
+
const out = [];
|
|
260
|
+
for (const manifest of toMigrate) {
|
|
261
|
+
const r = await migratePlugin(opts.dbUrl, manifest, migrationsDirOf(manifest, dirs.get(manifest.name)), opts.connectTimeoutMs);
|
|
262
|
+
if (r)
|
|
263
|
+
out.push({ plugin: manifest.name, ...r });
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The names of the config-declared extensions (`specifiers`, the startup config `extensions`
|
|
269
|
+
* list): imports their modules only (the same modules `load()` imports next, from Node's module
|
|
270
|
+
* cache). The kernel passes them to the bootstrap step before loading, so a bootstrap entry of
|
|
271
|
+
* the same name is kept and gets no install record (config wins, C15).
|
|
272
|
+
*/
|
|
273
|
+
static async declaredNames(specifiers, opts = {}) {
|
|
274
|
+
const entries = await importEntries(specifiers, resolvePath(opts.resolveFrom ?? process.cwd()));
|
|
275
|
+
return new Set(entries.map((e) => e.manifest.name));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 只导入、校验并按依赖排序插件,计算各自的权限集合;不迁移、不调用 setup、不连数据库与 Redis
|
|
279
|
+
* (供 `capybara extensions review / approve`)。返回依赖拓扑序。
|
|
280
|
+
*/
|
|
281
|
+
static async inspect(specifiers, opts = {}) {
|
|
282
|
+
const from = resolvePath(opts.resolveFrom ?? process.cwd());
|
|
283
|
+
const entries = await importEntries(specifiers, from);
|
|
284
|
+
const dirs = new Map(entries.map((e) => [e.manifest.name, e.dir]));
|
|
285
|
+
const ordered = resolveExtensions(entries.map((e) => e.manifest), SDK_VERSION);
|
|
286
|
+
for (const manifest of ordered)
|
|
287
|
+
assertSecretsReachable(manifest);
|
|
288
|
+
const sets = await permissionSets(ordered, dirs);
|
|
289
|
+
return ordered.map((manifest) => ({
|
|
290
|
+
source: "config",
|
|
291
|
+
manifest,
|
|
292
|
+
dir: dirs.get(manifest.name),
|
|
293
|
+
permissions: sets.get(manifest.name),
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
/** 本进程生效的插件实例(C17):消费方每次实时取,不缓存实现对象。 */
|
|
297
|
+
registry = new ExtensionRegistry();
|
|
298
|
+
/**
|
|
299
|
+
* M-1: plugin → mount points where deployment state expects `policy` hooks. A per-replica copy of PG
|
|
300
|
+
* deployment state (like the registry), rebuilt on every load / converge, never written elsewhere.
|
|
301
|
+
*/
|
|
302
|
+
policyExpected = new Map();
|
|
303
|
+
/**
|
|
304
|
+
* Runs the hooks of this process's active plugin instances (resolved from the registry on every
|
|
305
|
+
* call, so an upgrade fires only the new generation; calls go through each instance's in-flight
|
|
306
|
+
* tracker and are drained on revocation). Organization enablement is {@link enabledPlugins}.
|
|
307
|
+
*/
|
|
308
|
+
hooks;
|
|
309
|
+
/** 加载成功过的插件的加载参数(同进程重新加载用;撤销后保留,`unavailable` 据此判断)。 */
|
|
310
|
+
specs = new Map();
|
|
311
|
+
/** 因必需能力没了而随提供方卸下的插件:提供方回来后重新加载。 */
|
|
312
|
+
unloadedForDependency = new Set();
|
|
313
|
+
/** 每个插件已产生的实例代数。 */
|
|
314
|
+
generations = new Map();
|
|
315
|
+
/** 本进程导入过的模块入口(不同入口数超过阈值时告警:旧代码留在内存直到重启)。 */
|
|
316
|
+
imports = new ImportTracker();
|
|
317
|
+
/** 串行化 unload / reload(同一时刻只做一件,依赖方的连锁处理不交错)。 */
|
|
318
|
+
lifecycle = Promise.resolve();
|
|
319
|
+
drainMs;
|
|
320
|
+
disposeTimeoutMs;
|
|
321
|
+
reloadSetupTimeoutMs;
|
|
322
|
+
installedNames;
|
|
323
|
+
/** 安装记录里的插件名缓存(TTL + 注册表变化时作废)。 */
|
|
324
|
+
installedCache;
|
|
325
|
+
/** 本次因权限待批准(或依赖待批准插件)而未加载的插件。 */
|
|
326
|
+
pendingPlugins = [];
|
|
327
|
+
/**
|
|
328
|
+
* 待批准(或运行中加载失败)的配置声明插件的加载参数:{@link activatePending} / {@link converge} 据此重试。
|
|
329
|
+
* 安装记录不在这里:每次 converge 都按 PG 期望状态重新判定。
|
|
330
|
+
*/
|
|
331
|
+
pendingActivations = new Map();
|
|
332
|
+
/** 安装记录准备好的候选(按插件名;sha 变了就作废)。 */
|
|
333
|
+
prepared = new Map();
|
|
334
|
+
/** 闸门放行后导入过的安装记录真实清单(按插件名;部署配置 schema 用,含待配置而未加载的)。 */
|
|
335
|
+
importedManifests = new Map();
|
|
336
|
+
/** 本副本写过状态行的安装记录插件(停用 / 卸载后把未加载的那行标为 unloaded)。 */
|
|
337
|
+
installRows = new Map();
|
|
338
|
+
/** 启用的安装记录按静态清单声明提供的能力(最近一次 {@link applyInstalls};待定插件的 blockedBy 用)。 */
|
|
339
|
+
installProvides = new Map();
|
|
340
|
+
/** 已记过 error 日志的加载失败(插件名 → `<sha>:<原因>`;加载成功后清掉)。 */
|
|
341
|
+
loggedFailures = new Map();
|
|
342
|
+
/** 已告警过的「安装记录与配置声明重名」(每个名字只告警一次)。 */
|
|
343
|
+
warnedConflicts = new Set();
|
|
344
|
+
/** 升级时被替换、后台排空中的旧实例(close() 等它们,最多 lifecycleWaitMs)。 */
|
|
345
|
+
retiring = new Map();
|
|
346
|
+
/** 配置声明插件的清单与权限集合(含未加载的;后台列表与网页批准用)。 */
|
|
347
|
+
declaredInfo = new Map();
|
|
348
|
+
/** 导入过的插件名 → 来源与包(含未加载的;重名被跳过的一方不在这里)。 */
|
|
349
|
+
sources = new Map();
|
|
350
|
+
didLoad = false;
|
|
351
|
+
/** load() 的安装记录入口({@link converge} 据此按 PG 期望状态收敛)。 */
|
|
352
|
+
installLoader;
|
|
353
|
+
closed = false;
|
|
354
|
+
/** close() 时中止进行中的插件迁移。 */
|
|
355
|
+
abort = new AbortController();
|
|
356
|
+
/** 进行中的插件迁移(close() 等它结束,保证迁移连接不比宿主活得久)。 */
|
|
357
|
+
migrating;
|
|
358
|
+
/** 已创建、尚未登记(setup 进行中)的 ctx:close() 时立即关闭,不等 setup 返回。 */
|
|
359
|
+
settingUp;
|
|
360
|
+
resolveFrom;
|
|
361
|
+
onSecrets;
|
|
362
|
+
autoApprove;
|
|
363
|
+
status;
|
|
364
|
+
storedConfig;
|
|
365
|
+
/** 配置声明插件的部署配置最近一次判定是否合法(后台列表用;没判定过时按启动配置校验)。 */
|
|
366
|
+
configValidity = new Map();
|
|
367
|
+
/** 已告警过的「部署配置不合法」(插件名 → 说明;每个周期都会重新判定,同一说明只告警一次)。 */
|
|
368
|
+
configWarned = new Map();
|
|
369
|
+
/**
|
|
370
|
+
* @param opts.resolveFrom The directory bare package names are resolved from, default
|
|
371
|
+
* `process.cwd()`; a relative path is made absolute against the cwd at construction.
|
|
372
|
+
* Absolute paths and `file:` URL extension specifiers are used as they are;
|
|
373
|
+
* bare package names are resolved with `createRequire(<resolveFrom>/noop.js).resolve`, so an
|
|
374
|
+
* extension package's exports must expose the `default` (or `require`) condition.
|
|
375
|
+
* @param opts.autoApprove Approve extension permissions automatically (config
|
|
376
|
+
* `extensionPermissions.autoApprove`, development and trials only): extensions needing approval write a
|
|
377
|
+
* snapshot with source auto and load as usual. Default false: extensions pending approval do not
|
|
378
|
+
* load (see {@link pending}).
|
|
379
|
+
* @param opts.status This replica's load status table (`extension_replica_status`): every
|
|
380
|
+
* extension's load result writes a row.
|
|
381
|
+
*/
|
|
382
|
+
constructor(deps, extensionConfig = {}, opts = {}) {
|
|
383
|
+
this.deps = deps;
|
|
384
|
+
this.extensionConfig = extensionConfig;
|
|
385
|
+
this.storedConfig = opts.deploymentConfig;
|
|
386
|
+
this.drainMs = opts.drainMs ?? (() => DEFAULT_DRAIN_MS);
|
|
387
|
+
this.disposeTimeoutMs = opts.disposeTimeoutMs ?? (() => DEFAULT_DISPOSE_TIMEOUT_MS);
|
|
388
|
+
this.reloadSetupTimeoutMs =
|
|
389
|
+
opts.reloadSetupTimeoutMs ?? (() => DEFAULT_RELOAD_SETUP_TIMEOUT_MS);
|
|
390
|
+
this.installedNames = opts.installedNames;
|
|
391
|
+
const hookTimeoutMs = opts.hookTimeoutMs ?? (() => DEFAULT_HOOK_TIMEOUT_MS);
|
|
392
|
+
this.hooks = new HookRegistry({
|
|
393
|
+
instances: () => this.registry.all(),
|
|
394
|
+
current: (plugin) => this.registry.current(plugin),
|
|
395
|
+
expectedPolicy: (mount) => this.expectedPolicy(mount),
|
|
396
|
+
enabledPlugins: (orgId) => this.enabledPlugins(orgId),
|
|
397
|
+
redis: deps.redis,
|
|
398
|
+
config: () => ({ hooks: { timeoutMs: hookTimeoutMs() } }),
|
|
399
|
+
log: deps.log,
|
|
400
|
+
});
|
|
401
|
+
this.registry.onChange(() => {
|
|
402
|
+
this.installedCache = undefined;
|
|
403
|
+
});
|
|
404
|
+
this.onSecrets = opts.onSecrets;
|
|
405
|
+
this.autoApprove = opts.autoApprove === true;
|
|
406
|
+
this.resolveFrom = resolvePath(opts.resolveFrom ?? process.cwd());
|
|
407
|
+
this.status = opts.status;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Imports each extension module (default export) and initializes them in dependency order; may be
|
|
411
|
+
* called only once. An array is the same as `{ config: [...] }`.
|
|
412
|
+
*
|
|
413
|
+
* Order (C15 / C13; the bootstrap plugins that replaced builtin extensions are install records):
|
|
414
|
+
* 1. Config-declared extensions: not loaded while pending approval; an invalid deployment config is
|
|
415
|
+
* recorded `pending` (reason config, C10) and the kernel starts as usual; a failed
|
|
416
|
+
* migration or setup throws and the kernel fails to start.
|
|
417
|
+
* When an extension point in `requires` did not load because its provider is pending
|
|
418
|
+
* configuration or similar, it is recorded pending (dependency) and converge loads it once the
|
|
419
|
+
* provider has loaded.
|
|
420
|
+
* Deployment config = the startup config (`extensionConfig`) plus the fields stored in PG (constructor
|
|
421
|
+
* parameter `deploymentConfig`, C10).
|
|
422
|
+
* A config-declared extension whose `requires` can
|
|
423
|
+
* only be satisfied by an install record is set aside and resolved and loaded after step 2
|
|
424
|
+
* together with the loaded extensions (step 3; still unsatisfied -> it throws as before).
|
|
425
|
+
* 2. Install records (`plan.installs`): synced to local disk, the permission set computed from the
|
|
426
|
+
* reviewed static manifest and migration SQL, dependencies resolved together with the existing
|
|
427
|
+
* extensions, then the same permission gate in topological order; only after passing is the module
|
|
428
|
+
* imported and the static manifest checked (name and `capybara.manifest` must both match,
|
|
429
|
+
* otherwise failed / manifest_mismatch), so no extension code runs before approval; migration
|
|
430
|
+
* directories come only from the reviewed `capybara.migrations`; a failure at any step only marks
|
|
431
|
+
* that extension failed (or pending), the rest go on. On a name clash with a
|
|
432
|
+
* config-declared extension the latter wins, with a warning.
|
|
433
|
+
* Every extension's result is written to this replica's `extension_replica_status` (when `status`
|
|
434
|
+
* was given at construction).
|
|
435
|
+
* Instances that loaded are registered in the registry (C17): from then on {@link unload} /
|
|
436
|
+
* {@link reload} revoke or reload them in this process.
|
|
437
|
+
*
|
|
438
|
+
* When a config-declared extension's (trusted) setup never returns, load() (and so the
|
|
439
|
+
* kernel's `start()`) hangs forever: the kernel sets no timeout for them; `close()` / the kernel's
|
|
440
|
+
* `stop()` still close that extension's ctx right away and free its resources. Install records are
|
|
441
|
+
* not trusted: at startup their setup is also limited by `extensionHost.reloadSetupTimeoutSeconds`
|
|
442
|
+
* (timeout -> failed / setup_failed) and migrations by `extensionHost.migrationTimeoutSeconds` (see
|
|
443
|
+
* `ensureMigrated`).
|
|
444
|
+
* When the host is `close()`d during load: running migrations are aborted, the ctx in setup is closed
|
|
445
|
+
* right away (its queue's define / add then throw `queue is closed`), and load() throws
|
|
446
|
+
* `plugin host closed during load` at the next checkpoint.
|
|
447
|
+
*/
|
|
448
|
+
async load(plan) {
|
|
449
|
+
if (this.closed)
|
|
450
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
451
|
+
if (this.didLoad)
|
|
452
|
+
throw new Error("plugin host already loaded; load() may be called once");
|
|
453
|
+
this.didLoad = true;
|
|
454
|
+
const p = Array.isArray(plan) ? { config: plan } : plan;
|
|
455
|
+
this.installLoader = p.installs;
|
|
456
|
+
const entries = await importEntries(p.config ?? [], this.resolveFrom);
|
|
457
|
+
await this.claimNames(entries);
|
|
458
|
+
const dirs = new Map(entries.map((e) => [e.manifest.name, e.dir]));
|
|
459
|
+
const urls = new Map(entries.map((e) => [e.manifest.name, e.url]));
|
|
460
|
+
// 有安装记录时:配置声明的插件若 requires 只能由安装记录满足,先挑出来,等安装记录加载完再解析与加载
|
|
461
|
+
const { ready, deferred } = p.installs
|
|
462
|
+
? splitDeferred(entries.map((e) => e.manifest), new Set(entries.map((e) => e.manifest.name)))
|
|
463
|
+
: { ready: entries.map((e) => e.manifest), deferred: [] };
|
|
464
|
+
const ordered = resolveExtensions(ready, SDK_VERSION);
|
|
465
|
+
for (const e of entries)
|
|
466
|
+
this.sources.set(e.manifest.name, { source: "config", sha256: null });
|
|
467
|
+
for (const e of entries)
|
|
468
|
+
assertSecretsReachable(e.manifest);
|
|
469
|
+
const sets = await permissionSets(ordered, dirs);
|
|
470
|
+
this.noteDeclared(ordered, sets);
|
|
471
|
+
const approvals = new ExtensionApprovals(kernelDb(this.deps.db));
|
|
472
|
+
if (this.autoApprove) {
|
|
473
|
+
this.deps.log.warn("extensionPermissions.autoApprove is ON: plugin permissions are approved automatically without review; never enable this in production");
|
|
474
|
+
}
|
|
475
|
+
const snapshots = ordered.length > 0 || p.installs ? await approvals.all() : new Map();
|
|
476
|
+
const decisions = new Map(planApprovals(ordered, sets, snapshots, {
|
|
477
|
+
autoApprove: this.autoApprove,
|
|
478
|
+
}).map((d) => [d.plugin, d]));
|
|
479
|
+
for (const manifest of ordered) {
|
|
480
|
+
await this.activate(approvals, {
|
|
481
|
+
manifest,
|
|
482
|
+
source: this.sourceOf(manifest.name),
|
|
483
|
+
sha256: null,
|
|
484
|
+
entryUrl: urls.get(manifest.name),
|
|
485
|
+
migrationsDir: migrationsDirOf(manifest, dirs.get(manifest.name)),
|
|
486
|
+
set: sets.get(manifest.name),
|
|
487
|
+
decision: this.blockedByUnloaded(manifest, decisions.get(manifest.name)),
|
|
488
|
+
fatal: true,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
let installProviders = new Map();
|
|
492
|
+
let desired;
|
|
493
|
+
if (p.installs) {
|
|
494
|
+
// 与运行中的 reconcile 走同一条路径({@link converge});启动时不做依赖方连锁
|
|
495
|
+
try {
|
|
496
|
+
desired = await p.installs.desired();
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
this.deps.log.error("cannot read extension install records; installed extensions are not loaded", {
|
|
500
|
+
err: e,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (desired) {
|
|
504
|
+
// Packages kept in PG first: the object store serving the others may be one of them (a
|
|
505
|
+
// bootstrap plugin, e.g. object-store-s3). Nothing from install records is loaded yet, so
|
|
506
|
+
// the first pass unloads nothing; the second skips what the first loaded.
|
|
507
|
+
const inPg = desired.filter((d) => d.blobStore === "pg");
|
|
508
|
+
if (inPg.length > 0 && inPg.length < desired.length) {
|
|
509
|
+
await this.applyInstalls(p.installs, inPg, true);
|
|
510
|
+
}
|
|
511
|
+
installProviders = await this.applyInstalls(p.installs, desired, true);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (deferred.length > 0) {
|
|
515
|
+
await this.loadDeferred(deferred, dirs, urls, snapshots, approvals, installProviders);
|
|
516
|
+
}
|
|
517
|
+
await this.refreshPolicyExpectations(p.installs ? desired : []);
|
|
518
|
+
const orphans = Object.keys(this.extensionConfig).filter((k) => !this.sources.has(k));
|
|
519
|
+
if (orphans.length > 0) {
|
|
520
|
+
this.deps.log.warn("extensionConfig has keys for extensions that are not loaded", {
|
|
521
|
+
keys: orphans,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Config-declared extensions also take their name (I4, an addition to Ruling P3): the
|
|
527
|
+
* name is inserted into `extension_name_owners` with its npm package name (left alone when present).
|
|
528
|
+
* After it is removed from the config list, no other npm package can install an extension of the
|
|
529
|
+
* same name and take over its data, secrets and approvals. A failed write only warns.
|
|
530
|
+
*/
|
|
531
|
+
async claimNames(entries) {
|
|
532
|
+
const rows = [];
|
|
533
|
+
for (const e of entries) {
|
|
534
|
+
const pkg = await packageNameOf(e.dir);
|
|
535
|
+
if (pkg === undefined) {
|
|
536
|
+
this.deps.log.warn("plugin module has no package.json name; its plugin name is not claimed", {
|
|
537
|
+
plugin: e.manifest.name,
|
|
538
|
+
});
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
rows.push({ name: e.manifest.name, package_name: pkg });
|
|
542
|
+
}
|
|
543
|
+
if (rows.length === 0)
|
|
544
|
+
return;
|
|
545
|
+
try {
|
|
546
|
+
const kdb = kernelDb(this.deps.db);
|
|
547
|
+
await kdb
|
|
548
|
+
.insertInto("extension_name_owners")
|
|
549
|
+
.values(rows)
|
|
550
|
+
.onConflict((oc) => oc.column("name").doNothing())
|
|
551
|
+
.execute();
|
|
552
|
+
const owners = await kdb
|
|
553
|
+
.selectFrom("extension_name_owners")
|
|
554
|
+
.select(["name", "package_name"])
|
|
555
|
+
.where("name", "in", rows.map((r) => r.name))
|
|
556
|
+
.execute();
|
|
557
|
+
for (const o of owners) {
|
|
558
|
+
const mine = rows.find((r) => r.name === o.name);
|
|
559
|
+
if (mine && mine.package_name !== o.package_name) {
|
|
560
|
+
this.deps.log.warn("plugin name is owned by a different npm package; the data, secrets and approval kept under this name came from that package", { plugin: o.name, owner: o.package_name, package: mine.package_name });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
this.deps.log.warn("cannot record plugin name owners", { err });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* 启动时按拓扑序加载:权限闸门放行、但 `requires` 的能力此刻没有生效的提供方(提供方待配置等没加载成)→
|
|
570
|
+
* 改为待定(dependency,blockedBy 点名待定的提供方),提供方加载后由 {@link converge} 一并加载。
|
|
571
|
+
*/
|
|
572
|
+
blockedByUnloaded(manifest, decision) {
|
|
573
|
+
if (decision.action === "pending")
|
|
574
|
+
return decision;
|
|
575
|
+
const provided = new Set(this.registry.all().flatMap((i) => [...i.provided.keys()]));
|
|
576
|
+
const missing = (manifest.requires ?? []).filter((c) => !provided.has(c));
|
|
577
|
+
if (missing.length === 0)
|
|
578
|
+
return decision;
|
|
579
|
+
return {
|
|
580
|
+
plugin: manifest.name,
|
|
581
|
+
action: "pending",
|
|
582
|
+
pending: {
|
|
583
|
+
plugin: manifest.name,
|
|
584
|
+
version: manifest.version,
|
|
585
|
+
added: decision.added,
|
|
586
|
+
removed: decision.removed,
|
|
587
|
+
reason: "dependency",
|
|
588
|
+
blockedBy: missing.map((extensionPoint) => ({
|
|
589
|
+
extensionPoint,
|
|
590
|
+
providers: [...this.pendingActivations.values()]
|
|
591
|
+
.filter((p) => p.manifest.provides.includes(extensionPoint))
|
|
592
|
+
.map((p) => p.manifest.name),
|
|
593
|
+
})),
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 插件此刻的部署配置(未校验):启动配置补上 PG 里保存的字段(C10,见构造参数 `deploymentConfig`)。
|
|
599
|
+
*/
|
|
600
|
+
async rawConfigOf(name, schema) {
|
|
601
|
+
const fileValue = this.extensionConfig[name];
|
|
602
|
+
if (!this.storedConfig)
|
|
603
|
+
return fileValue ?? {};
|
|
604
|
+
return this.storedConfig(name, fileValue, schema);
|
|
605
|
+
}
|
|
606
|
+
/** 把一份已校验部署配置里的密钥值交给 onSecrets(日志打码)。 */
|
|
607
|
+
reportSecrets(schema, config) {
|
|
608
|
+
if (!schema || !this.onSecrets)
|
|
609
|
+
return;
|
|
610
|
+
const values = secretValues(schema, config);
|
|
611
|
+
if (values.length > 0)
|
|
612
|
+
this.onSecrets(values);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* 安装记录收敛到 PG 期望状态(启动第 3 步与运行中的 {@link converge} 共用,C16);`desired` 为期望启用的记录。
|
|
616
|
+
* 1. 不再期望启用的(停用 / 卸载):卸下(排空在途调用),未加载但写过状态行的标为 unloaded;
|
|
617
|
+
* 2. 期望启用、本副本已在目标包上的:不动;
|
|
618
|
+
* 3. 其余:准备候选(同步 → 按静态清单与迁移 SQL 算权限集合;失败记 failed:`sync_failed` / `sha_mismatch`),
|
|
619
|
+
* 与生效的插件一起按依赖拓扑序逐个处理:权限闸门(新增权限待批准 → pending / approval,旧实例照常)、
|
|
620
|
+
* 必需能力(此刻没有生效的提供方 → pending / dependency)、导入模块并核对静态清单(`import_failed` /
|
|
621
|
+
* `manifest_mismatch`;闸门放行之前不导入)、部署配置、
|
|
622
|
+
* 持锁迁移(busy → pending / migration)、setup,成功后登记新实例(先加载新、再撤销旧:旧实例在后台排空后撤销);
|
|
623
|
+
* 任何一步失败旧实例保留,记 failed(`migration_failed` / `setup_failed`)。
|
|
624
|
+
* 4. 运行中(非启动):对变了的能力做依赖方连锁(本轮刚加载的除外),此前因依赖卸下的插件随之补回。
|
|
625
|
+
* 返回启用的安装记录在静态清单里声明提供的能力(启动第 4 步据此判断待定还是缺提供方)。
|
|
626
|
+
*/
|
|
627
|
+
async applyInstalls(loader, desired, startup) {
|
|
628
|
+
const declared = this.declared();
|
|
629
|
+
const installProviders = new Map();
|
|
630
|
+
const wanted = new Map();
|
|
631
|
+
for (const d of desired) {
|
|
632
|
+
const taken = declared.get(d.name);
|
|
633
|
+
if (taken) {
|
|
634
|
+
if (!this.warnedConflicts.has(d.name)) {
|
|
635
|
+
this.warnedConflicts.add(d.name);
|
|
636
|
+
this.deps.log.warn("plugin is both installed and declared in the extensions config list; using the config plugin", { plugin: d.name, installedVersion: d.version });
|
|
637
|
+
// 该名字的状态行属于生效的一方,不覆盖;在它的 detail 里记下冲突(后台能看到)
|
|
638
|
+
await this.noteStatus(d.name, `name_conflict: install record ${d.version} (${d.sha256.slice(0, 16)}) ignored; the ${taken} plugin of the same name is used`);
|
|
639
|
+
}
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
wanted.set(d.name, d);
|
|
643
|
+
// 按审阅过的静态清单(不论随后加载成败)
|
|
644
|
+
for (const cap of d.section.manifest.provides) {
|
|
645
|
+
installProviders.set(cap, [...(installProviders.get(cap) ?? []), d.name]);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
this.installProvides = installProviders;
|
|
649
|
+
const drainMs = this.drainMs();
|
|
650
|
+
const changed = [];
|
|
651
|
+
// 1. 不再期望启用:卸下;未加载但写过状态行的标为 unloaded
|
|
652
|
+
for (const inst of this.registry.all()) {
|
|
653
|
+
if (inst.source !== "install" || wanted.has(inst.name))
|
|
654
|
+
continue;
|
|
655
|
+
const gone = await this.doUnload(inst.name, drainMs, null);
|
|
656
|
+
if (gone)
|
|
657
|
+
changed.push(...gone.manifest.provides);
|
|
658
|
+
}
|
|
659
|
+
for (const [name, row] of [...this.installRows]) {
|
|
660
|
+
if (wanted.has(name) || this.registry.current(name))
|
|
661
|
+
continue;
|
|
662
|
+
this.dropPending(name);
|
|
663
|
+
this.prepared.delete(name);
|
|
664
|
+
this.importedManifests.delete(name);
|
|
665
|
+
await this.writeStatus({
|
|
666
|
+
plugin: name,
|
|
667
|
+
version: row.version,
|
|
668
|
+
sha256: row.sha256,
|
|
669
|
+
source: "install",
|
|
670
|
+
state: "unloaded",
|
|
671
|
+
reason: null,
|
|
672
|
+
detail: null,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
// 2 / 3. 需要换包或尚未加载的:准备候选
|
|
676
|
+
const candidates = [];
|
|
677
|
+
for (const d of wanted.values()) {
|
|
678
|
+
if (this.closed)
|
|
679
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
680
|
+
if (this.registry.current(d.name)?.sha256 === d.sha256)
|
|
681
|
+
continue;
|
|
682
|
+
const c = await this.prepare(loader, d);
|
|
683
|
+
if (c)
|
|
684
|
+
candidates.push(c);
|
|
685
|
+
}
|
|
686
|
+
// 与生效的插件一起解析依赖:被替换的旧实例在新的被接受之前仍算提供方;解析不过的反复重试直到没有新的能加进来
|
|
687
|
+
const accepted = [];
|
|
688
|
+
const base = () => {
|
|
689
|
+
const replaced = new Set(accepted.map((a) => a.d.name));
|
|
690
|
+
return this.registry
|
|
691
|
+
.all()
|
|
692
|
+
.filter((i) => !replaced.has(i.name))
|
|
693
|
+
.map((i) => i.manifest);
|
|
694
|
+
};
|
|
695
|
+
const tryResolve = (c) => resolveExtensions([
|
|
696
|
+
...base().filter((m) => m.name !== c.d.name),
|
|
697
|
+
...accepted.map((a) => a.manifest),
|
|
698
|
+
c.manifest,
|
|
699
|
+
], SDK_VERSION);
|
|
700
|
+
let remaining = candidates;
|
|
701
|
+
for (let progress = true; progress && remaining.length > 0;) {
|
|
702
|
+
progress = false;
|
|
703
|
+
const next = [];
|
|
704
|
+
for (const c of remaining) {
|
|
705
|
+
try {
|
|
706
|
+
tryResolve(c);
|
|
707
|
+
accepted.push(c);
|
|
708
|
+
progress = true;
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
next.push(c);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
remaining = next;
|
|
715
|
+
}
|
|
716
|
+
for (const c of remaining) {
|
|
717
|
+
let detail = "cannot resolve the plugin's dependencies";
|
|
718
|
+
try {
|
|
719
|
+
tryResolve(c);
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
detail = errMsg(e);
|
|
723
|
+
}
|
|
724
|
+
this.notePending({
|
|
725
|
+
plugin: c.d.name,
|
|
726
|
+
version: c.d.version,
|
|
727
|
+
added: [],
|
|
728
|
+
removed: [],
|
|
729
|
+
reason: "dependency",
|
|
730
|
+
blockedBy: (c.manifest.requires ?? []).map((extensionPoint) => ({
|
|
731
|
+
extensionPoint,
|
|
732
|
+
providers: (installProviders.get(extensionPoint) ?? []).filter((n) => n !== c.d.name),
|
|
733
|
+
})),
|
|
734
|
+
});
|
|
735
|
+
await this.writeStatus({
|
|
736
|
+
plugin: c.d.name,
|
|
737
|
+
version: c.d.version,
|
|
738
|
+
sha256: c.d.sha256,
|
|
739
|
+
source: "install",
|
|
740
|
+
state: "pending",
|
|
741
|
+
reason: "dependency",
|
|
742
|
+
detail,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
if (accepted.length === 0) {
|
|
746
|
+
if (!startup && changed.length > 0)
|
|
747
|
+
await this.cascade(changed, drainMs);
|
|
748
|
+
return installProviders;
|
|
749
|
+
}
|
|
750
|
+
for (const c of accepted)
|
|
751
|
+
this.sources.set(c.d.name, { source: "install", sha256: c.d.sha256 });
|
|
752
|
+
// 4. 按拓扑序逐个:权限闸门 → 配置 → 迁移 → setup → 登记(替换旧实例)
|
|
753
|
+
const order = resolveExtensions([...base(), ...accepted.map((a) => a.manifest)], SDK_VERSION);
|
|
754
|
+
const byName = new Map(accepted.map((c) => [c.d.name, c]));
|
|
755
|
+
const approvals = new ExtensionApprovals(kernelDb(this.deps.db));
|
|
756
|
+
const snapshots = await approvals.all();
|
|
757
|
+
const activated = new Set();
|
|
758
|
+
for (const m of order) {
|
|
759
|
+
const c = byName.get(m.name);
|
|
760
|
+
if (!c)
|
|
761
|
+
continue;
|
|
762
|
+
if (this.closed)
|
|
763
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
764
|
+
const a = {
|
|
765
|
+
manifest: c.manifest,
|
|
766
|
+
source: "install",
|
|
767
|
+
sha256: c.d.sha256,
|
|
768
|
+
entryUrl: c.entryUrl,
|
|
769
|
+
migrationsDir: c.migrationsDir,
|
|
770
|
+
set: c.set,
|
|
771
|
+
decision: { plugin: m.name, action: "pending" },
|
|
772
|
+
fatal: false,
|
|
773
|
+
migrate: () => loader.migrate(c.d, c.migrationsDir, this.abort.signal),
|
|
774
|
+
importModule: () => this.importInstalled(c),
|
|
775
|
+
};
|
|
776
|
+
a.decision = this.decidePending(m.name, a, snapshots, installProviders);
|
|
777
|
+
const old = this.registry.current(m.name);
|
|
778
|
+
// 安装记录不受信任:启动时 setup 同样限时(I3),挂住的 setup 不卡住内核启动;配置声明的启动时不限
|
|
779
|
+
const inst = await this.activate(approvals, a, this.reloadSetupTimeoutMs());
|
|
780
|
+
if (inst) {
|
|
781
|
+
activated.add(m.name);
|
|
782
|
+
changed.push(...inst.manifest.provides, ...(old?.manifest.provides ?? []));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (!startup && changed.length > 0)
|
|
786
|
+
await this.cascade(changed, drainMs, activated);
|
|
787
|
+
return installProviders;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* The load candidate of an install record: sync it locally, then compute the permission set from
|
|
791
|
+
* the reviewed static manifest and the package's migration SQL (the same algorithm as
|
|
792
|
+
* `permissionSetOfPackage` at review; the migrations directory comes only from
|
|
793
|
+
* `extension.migrations`). Does not import the module: that happens after the permission gate
|
|
794
|
+
* ({@link importInstalled}). A failure is recorded as failed (`sync_failed` / `sha_mismatch`, the
|
|
795
|
+
* detail is the error message only, e.g. the legacy-format message for a pre-0.1.0 package) and
|
|
796
|
+
* returns undefined. The result for a package never changes, so successes are cached by sha (a
|
|
797
|
+
* reconcile cycle does not unpack again).
|
|
798
|
+
*/
|
|
799
|
+
async prepare(loader, d) {
|
|
800
|
+
const hit = this.prepared.get(d.name);
|
|
801
|
+
if (hit && hit.d.sha256 === d.sha256)
|
|
802
|
+
return { ...hit, d };
|
|
803
|
+
this.prepared.delete(d.name);
|
|
804
|
+
// 换了包:上一个包导入的真实清单作废(部署配置 schema 不能再按旧版本给)
|
|
805
|
+
if (this.importedManifests.get(d.name)?.sha256 !== d.sha256)
|
|
806
|
+
this.importedManifests.delete(d.name);
|
|
807
|
+
const fail = async (reason, e) => {
|
|
808
|
+
this.logFailure(d.name, d.sha256, reason, e, { version: d.version });
|
|
809
|
+
await this.writeStatus({
|
|
810
|
+
plugin: d.name,
|
|
811
|
+
version: d.version,
|
|
812
|
+
sha256: d.sha256,
|
|
813
|
+
source: "install",
|
|
814
|
+
state: "failed",
|
|
815
|
+
reason,
|
|
816
|
+
detail: errMsg(e),
|
|
817
|
+
});
|
|
818
|
+
return undefined;
|
|
819
|
+
};
|
|
820
|
+
let url;
|
|
821
|
+
let root;
|
|
822
|
+
let set;
|
|
823
|
+
try {
|
|
824
|
+
url = await loader.sync(d);
|
|
825
|
+
root = packageRootOf(url, packagePath(d.section.entry));
|
|
826
|
+
set = permissionSetOfSection(d.section, await readMigrationSql(root, d.section));
|
|
827
|
+
}
|
|
828
|
+
catch (e) {
|
|
829
|
+
const mismatch = e instanceof ExtensionBlobIntegrityError || e instanceof PackageShaMismatchError;
|
|
830
|
+
return fail(mismatch ? "sha_mismatch" : "sync_failed", e);
|
|
831
|
+
}
|
|
832
|
+
const migrations = d.section.migrations;
|
|
833
|
+
const c = {
|
|
834
|
+
d,
|
|
835
|
+
manifest: manifestOfSection(d.section.manifest),
|
|
836
|
+
set,
|
|
837
|
+
entryUrl: url,
|
|
838
|
+
root,
|
|
839
|
+
migrationsDir: migrations === undefined ? undefined : resolvePath(root, packagePath(migrations)),
|
|
840
|
+
};
|
|
841
|
+
this.prepared.set(d.name, c);
|
|
842
|
+
return c;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* 权限闸门放行后导入安装记录的真实模块(模块顶层代码此时才执行)并核对:密钥标记、与审阅过的静态清单一致
|
|
846
|
+
* (名字、版本、能力、权限、界面、配置字段;不一致抛 {@link ManifestMismatchError})。同一入口 URL 由 Node 模块缓存,
|
|
847
|
+
* 重复调用不重复执行顶层代码。导入限 `extensionHost.reloadSetupTimeoutSeconds`(与 setup 同一时限):顶层代码
|
|
848
|
+
* 永不结束时记 failed / import_failed,不卡住启动与 reconcile(挂住的模块留在内存里,下次导入同样超时)。
|
|
849
|
+
*/
|
|
850
|
+
async importInstalled(c) {
|
|
851
|
+
const ms = this.reloadSetupTimeoutMs();
|
|
852
|
+
let timer;
|
|
853
|
+
const importing = importExtensionModule(c.entryUrl, c.d.name, c.root);
|
|
854
|
+
importing.catch(() => { });
|
|
855
|
+
const manifest = await Promise.race([
|
|
856
|
+
importing,
|
|
857
|
+
new Promise((_, reject) => {
|
|
858
|
+
timer = setTimeout(() => reject(new Error(`plugin ${c.d.name}: importing its module did not finish within ${Math.round(ms / 1000)}s (extensionHost.reloadSetupTimeoutSeconds); its top-level code may never finish`)), ms);
|
|
859
|
+
}),
|
|
860
|
+
]).finally(() => clearTimeout(timer));
|
|
861
|
+
assertSecretsReachable(manifest);
|
|
862
|
+
if (!matchesSection(manifest, c.d.name, c.d.section.manifest)) {
|
|
863
|
+
throw new ManifestMismatchError(c.d.name, manifest.name);
|
|
864
|
+
}
|
|
865
|
+
this.importedManifests.set(c.d.name, { sha256: c.d.sha256, manifest });
|
|
866
|
+
return manifest;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* 安装记录加载失败的 error 日志:每个周期都会重试,同一个包、同一个原因只记一次(状态行每次照写)。
|
|
870
|
+
*/
|
|
871
|
+
logFailure(plugin, sha256, reason, err, fields) {
|
|
872
|
+
const key = `${sha256}:${reason}`;
|
|
873
|
+
if (this.loggedFailures.get(plugin) === key)
|
|
874
|
+
return;
|
|
875
|
+
this.loggedFailures.set(plugin, key);
|
|
876
|
+
this.deps.log.error("installed extension not loaded", {
|
|
877
|
+
plugin,
|
|
878
|
+
sha256,
|
|
879
|
+
reason,
|
|
880
|
+
err,
|
|
881
|
+
...fields,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
/** 记下(替换同名的)待定插件,供 {@link pending} 列出。 */
|
|
885
|
+
notePending(p) {
|
|
886
|
+
const at = this.pendingPlugins.findIndex((x) => x.plugin === p.plugin);
|
|
887
|
+
if (at >= 0)
|
|
888
|
+
this.pendingPlugins[at] = p;
|
|
889
|
+
else
|
|
890
|
+
this.pendingPlugins.push(p);
|
|
891
|
+
}
|
|
892
|
+
/** 从待定清单里去掉。 */
|
|
893
|
+
dropPending(plugin) {
|
|
894
|
+
const at = this.pendingPlugins.findIndex((x) => x.plugin === plugin);
|
|
895
|
+
if (at >= 0)
|
|
896
|
+
this.pendingPlugins.splice(at, 1);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* 第 4 步:第 2 步挑出来的配置声明插件(`requires` 只能由安装记录满足)。与已加载的插件一起解析依赖:
|
|
900
|
+
* - 满足的按拓扑序走同一道权限闸门后加载(出错直接抛,与其他配置声明插件一致);
|
|
901
|
+
* - 缺的能力有启用的安装记录声明提供、只是它没加载成(待批准 / 失败)→ 待定(pending,原因 dependency),
|
|
902
|
+
* 与「配置声明插件依赖待批准的配置声明插件」一致,内核照常启动;
|
|
903
|
+
* - 缺的能力没有任何提供方 → 与此前一样抛错(内核启动失败)。
|
|
904
|
+
*/
|
|
905
|
+
async loadDeferred(deferred, dirs, urls, snapshots, approvals, installProviders) {
|
|
906
|
+
if (this.closed)
|
|
907
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
908
|
+
const loaded = this.registry.all();
|
|
909
|
+
const { ready, deferred: blocked } = splitDeferred([...loaded.map((i) => i.manifest), ...deferred], new Set(deferred.map((m) => m.name)));
|
|
910
|
+
const deferredSets = await permissionSets(deferred, dirs);
|
|
911
|
+
this.noteDeclared(deferred, deferredSets);
|
|
912
|
+
const sets = new Map([...loaded.map((i) => [i.name, i.permissions]), ...deferredSets]);
|
|
913
|
+
const blockedProvides = new Set(blocked.flatMap((m) => m.provides));
|
|
914
|
+
const readyProvides = new Set(ready.flatMap((m) => m.provides));
|
|
915
|
+
for (const m of blocked) {
|
|
916
|
+
const missing = (m.requires ?? []).filter((c) => !readyProvides.has(c));
|
|
917
|
+
if (missing.some((c) => !installProviders.has(c) && !blockedProvides.has(c))) {
|
|
918
|
+
// No provider at all: throw as before (resolveExtensions names the extension and the extension point)
|
|
919
|
+
resolveExtensions([...loaded.map((i) => i.manifest), ...deferred], SDK_VERSION);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
for (const m of blocked) {
|
|
923
|
+
const blockedBy = (m.requires ?? [])
|
|
924
|
+
.filter((c) => !readyProvides.has(c))
|
|
925
|
+
.map((extensionPoint) => ({
|
|
926
|
+
extensionPoint,
|
|
927
|
+
providers: [
|
|
928
|
+
...(installProviders.get(extensionPoint) ?? []),
|
|
929
|
+
...blocked.filter((b) => b.provides.includes(extensionPoint)).map((b) => b.name),
|
|
930
|
+
],
|
|
931
|
+
}));
|
|
932
|
+
await this.activate(approvals, {
|
|
933
|
+
manifest: m,
|
|
934
|
+
source: "config",
|
|
935
|
+
sha256: null,
|
|
936
|
+
entryUrl: urls.get(m.name),
|
|
937
|
+
migrationsDir: migrationsDirOf(m, dirs.get(m.name)),
|
|
938
|
+
set: sets.get(m.name),
|
|
939
|
+
decision: {
|
|
940
|
+
plugin: m.name,
|
|
941
|
+
action: "pending",
|
|
942
|
+
pending: {
|
|
943
|
+
plugin: m.name,
|
|
944
|
+
version: m.version,
|
|
945
|
+
added: [],
|
|
946
|
+
removed: [],
|
|
947
|
+
reason: "dependency",
|
|
948
|
+
blockedBy,
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
fatal: true,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
const loadable = deferred.filter((m) => !blocked.includes(m));
|
|
955
|
+
if (loadable.length === 0)
|
|
956
|
+
return;
|
|
957
|
+
const all = resolveExtensions([...loaded.map((i) => i.manifest), ...loadable], SDK_VERSION);
|
|
958
|
+
const decisions = new Map(planApprovals(all, sets, withPreApproved(snapshots, loaded.map((i) => i.manifest), sets), { autoApprove: this.autoApprove }).map((d) => [d.plugin, d]));
|
|
959
|
+
const names = new Set(loadable.map((m) => m.name));
|
|
960
|
+
for (const manifest of all) {
|
|
961
|
+
if (!names.has(manifest.name))
|
|
962
|
+
continue;
|
|
963
|
+
await this.activate(approvals, {
|
|
964
|
+
manifest,
|
|
965
|
+
source: "config",
|
|
966
|
+
sha256: null,
|
|
967
|
+
entryUrl: urls.get(manifest.name),
|
|
968
|
+
migrationsDir: migrationsDirOf(manifest, dirs.get(manifest.name)),
|
|
969
|
+
set: sets.get(manifest.name),
|
|
970
|
+
// 同一批里的提供方可能刚记为待配置:依赖方改为待定,不让启动失败
|
|
971
|
+
decision: this.blockedByUnloaded(manifest, decisions.get(manifest.name)),
|
|
972
|
+
fatal: true,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/** 给已有状态行补说明;失败只记 warn。 */
|
|
977
|
+
async noteStatus(plugin, detail) {
|
|
978
|
+
if (!this.status)
|
|
979
|
+
return;
|
|
980
|
+
try {
|
|
981
|
+
await this.status.note(plugin, detail);
|
|
982
|
+
}
|
|
983
|
+
catch (e) {
|
|
984
|
+
this.deps.log.warn("cannot write plugin replica status", { plugin, err: e });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* 写本副本的加载状态;失败只记 warn(状态是给后台看的,不影响加载)。这一行的版本没加载成(failed / pending)
|
|
989
|
+
* 而本副本仍有别的版本在跑时,记下那个版本(`servingVersion`,后台同时显示「新版失败、旧版仍在服务」)。
|
|
990
|
+
*/
|
|
991
|
+
async writeStatus(s) {
|
|
992
|
+
if (s.servingVersion === undefined && (s.state === "failed" || s.state === "pending")) {
|
|
993
|
+
const cur = this.registry.current(s.plugin);
|
|
994
|
+
s = {
|
|
995
|
+
...s,
|
|
996
|
+
servingVersion: cur && (cur.sha256 !== s.sha256 || cur.version !== s.version) ? cur.version : null,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
if (s.source === "install") {
|
|
1000
|
+
if (s.state === "unloaded")
|
|
1001
|
+
this.installRows.delete(s.plugin);
|
|
1002
|
+
else
|
|
1003
|
+
this.installRows.set(s.plugin, { version: s.version, sha256: s.sha256 });
|
|
1004
|
+
}
|
|
1005
|
+
if (!this.status)
|
|
1006
|
+
return;
|
|
1007
|
+
try {
|
|
1008
|
+
await this.status.write(s);
|
|
1009
|
+
}
|
|
1010
|
+
catch (e) {
|
|
1011
|
+
this.deps.log.warn("cannot write plugin replica status", { plugin: s.plugin, err: e });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* 加载一个插件:权限闸门结论、部署配置、迁移、setup、登记。`fatal` 时(配置声明)出错直接抛;
|
|
1016
|
+
* 否则(安装记录)记 failed 后返回。部署配置不合法时(任何来源)只记 pending(reason config),不抛(C10)。
|
|
1017
|
+
* 已有同名实例时(升级):新实例登记后旧实例才撤销(后台排空在途调用);新实例没加载成时旧实例保持不变。
|
|
1018
|
+
* 返回新登记的实例(没有加载时返回 undefined)。
|
|
1019
|
+
*/
|
|
1020
|
+
async activate(approvals, a, setupTimeoutMs) {
|
|
1021
|
+
const { decision } = a;
|
|
1022
|
+
let manifest = a.manifest;
|
|
1023
|
+
const name = manifest.name;
|
|
1024
|
+
const base = { plugin: name, version: manifest.version, sha256: a.sha256, source: a.source };
|
|
1025
|
+
const failSoft = async (reason, e) => {
|
|
1026
|
+
this.logFailure(name, a.sha256, reason, e, { version: manifest.version });
|
|
1027
|
+
await this.writeStatus({ ...base, state: "failed", reason, detail: errMsg(e) });
|
|
1028
|
+
return undefined;
|
|
1029
|
+
};
|
|
1030
|
+
// 安装记录:闸门放行之后才导入真实模块(I1:批准之前不执行插件代码);待批准时只有静态清单,不读部署配置
|
|
1031
|
+
if (a.importModule && decision.action !== "pending") {
|
|
1032
|
+
try {
|
|
1033
|
+
manifest = await a.importModule();
|
|
1034
|
+
}
|
|
1035
|
+
catch (e) {
|
|
1036
|
+
if (this.closed)
|
|
1037
|
+
throw new Error(CLOSED_DURING_LOAD, { cause: e });
|
|
1038
|
+
return failSoft(e instanceof ManifestMismatchError ? "manifest_mismatch" : "import_failed", e);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const schema = manifest.config?.deployment;
|
|
1042
|
+
// 安装记录每次 converge 都按 PG 重新判定,不进 pendingActivations
|
|
1043
|
+
const retryLater = () => {
|
|
1044
|
+
if (a.source !== "install")
|
|
1045
|
+
this.pendingActivations.set(name, a);
|
|
1046
|
+
};
|
|
1047
|
+
const readConfig = !(a.importModule && decision.action === "pending");
|
|
1048
|
+
let raw;
|
|
1049
|
+
try {
|
|
1050
|
+
if (readConfig)
|
|
1051
|
+
raw = await this.rawConfigOf(name, schema);
|
|
1052
|
+
}
|
|
1053
|
+
catch (e) {
|
|
1054
|
+
if (this.closed)
|
|
1055
|
+
throw new Error(CLOSED_DURING_LOAD, { cause: e });
|
|
1056
|
+
// 读不到 PG 里保存的部署配置:按待配置处理,下次 reconcile 再试
|
|
1057
|
+
this.deps.log.error("cannot read the stored deployment config of plugin", {
|
|
1058
|
+
plugin: name,
|
|
1059
|
+
err: e,
|
|
1060
|
+
});
|
|
1061
|
+
retryLater();
|
|
1062
|
+
await this.writeStatus({
|
|
1063
|
+
...base,
|
|
1064
|
+
state: "pending",
|
|
1065
|
+
reason: "config",
|
|
1066
|
+
detail: "cannot read the stored deployment config",
|
|
1067
|
+
});
|
|
1068
|
+
return undefined;
|
|
1069
|
+
}
|
|
1070
|
+
let config = raw;
|
|
1071
|
+
const parsed = readConfig ? schema?.safeParse(raw) : undefined;
|
|
1072
|
+
if (a.source !== "install")
|
|
1073
|
+
this.configValidity.set(name, parsed?.success ?? true);
|
|
1074
|
+
if (decision.action === "pending") {
|
|
1075
|
+
// 待批准:不校验配置、不迁移、不 setup;配置合法时仍登记其中的密钥,避免日志泄露
|
|
1076
|
+
if (parsed?.success)
|
|
1077
|
+
this.reportSecrets(schema, parsed.data);
|
|
1078
|
+
const before = this.pendingPlugins.find((p) => p.plugin === name);
|
|
1079
|
+
this.notePending(decision.pending);
|
|
1080
|
+
retryLater();
|
|
1081
|
+
// 每个周期都会重新判定:同一版本、同一原因只告警一次
|
|
1082
|
+
if (before?.version !== decision.pending.version ||
|
|
1083
|
+
before.reason !== decision.pending.reason) {
|
|
1084
|
+
warnPending(this.deps.log, decision.pending);
|
|
1085
|
+
}
|
|
1086
|
+
const reason = decision.pending.reason === "dependency" ? "dependency" : "approval";
|
|
1087
|
+
await this.writeStatus({ ...base, state: "pending", reason, detail: null });
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
this.dropPending(name);
|
|
1091
|
+
// 权限放行在部署配置之前记下(autoApprove 的快照不因待配置而缺失,后台不会把待配置显示成待批准)
|
|
1092
|
+
if (decision.changed) {
|
|
1093
|
+
if (decision.autoApprove) {
|
|
1094
|
+
await approvals.approve(name, manifest.version, a.set, { source: "auto" });
|
|
1095
|
+
this.deps.log.warn("plugin permissions auto-approved (extensionPermissions.autoApprove)", {
|
|
1096
|
+
plugin: name,
|
|
1097
|
+
version: manifest.version,
|
|
1098
|
+
added: decision.added,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
// 权限变少、不变(版本变了)或只多出视图(ui.view:*,不卡加载):直接加载,快照跟上当前清单。
|
|
1103
|
+
// 补进视图不写审计(与 autoApprove 一样,Ruling A4),只记日志
|
|
1104
|
+
const updated = await approvals.refresh(name, manifest.version, a.set);
|
|
1105
|
+
const merged = decision.added.filter((k) => k.startsWith(UI_VIEW_KEY_PREFIX));
|
|
1106
|
+
if (updated && merged.length > 0 && blockingKeys(decision.added).length === 0) {
|
|
1107
|
+
this.deps.log.info("plugin ui views merged into the permission snapshot", {
|
|
1108
|
+
plugin: name,
|
|
1109
|
+
version: manifest.version,
|
|
1110
|
+
merged,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (schema && parsed) {
|
|
1116
|
+
if (!parsed.success) {
|
|
1117
|
+
// 只列路径与按 issue 类型生成的说明(不用 zod 的 message:自定义消息可能拼入输入值)
|
|
1118
|
+
const detail = toIssues(parsed.error)
|
|
1119
|
+
.map((i) => `${i.path || "<root>"}: ${i.message}`)
|
|
1120
|
+
.join("; ");
|
|
1121
|
+
// 待配置(C10):配置声明与安装记录都一样,内核照常启动;配置改好后下一次 reconcile 加载
|
|
1122
|
+
if (this.configWarned.get(name) !== detail) {
|
|
1123
|
+
this.configWarned.set(name, detail);
|
|
1124
|
+
this.deps.log.warn("extension not loaded: its deployment config is missing or invalid (set it in the admin console or the startup config; it loads without a restart)", { plugin: name, source: a.source, detail });
|
|
1125
|
+
}
|
|
1126
|
+
retryLater();
|
|
1127
|
+
await this.writeStatus({ ...base, state: "pending", reason: "config", detail });
|
|
1128
|
+
return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
this.configWarned.delete(name);
|
|
1131
|
+
config = parsed.data;
|
|
1132
|
+
this.reportSecrets(schema, config);
|
|
1133
|
+
}
|
|
1134
|
+
if (this.closed)
|
|
1135
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
1136
|
+
// 安装记录:一个副本持锁迁移(C16),别的副本正在迁移时待定、旧实例照常;配置声明:按 schema 锁直接迁移
|
|
1137
|
+
const migrating = a.migrate
|
|
1138
|
+
? a.migrate()
|
|
1139
|
+
: migratePlugin(this.deps.dbUrl, manifest, a.migrationsDir, this.deps.connectTimeoutMs, this.abort.signal);
|
|
1140
|
+
this.migrating = migrating;
|
|
1141
|
+
try {
|
|
1142
|
+
if ((await migrating) === "busy") {
|
|
1143
|
+
await this.writeStatus({
|
|
1144
|
+
...base,
|
|
1145
|
+
state: "pending",
|
|
1146
|
+
reason: "migration",
|
|
1147
|
+
detail: "another replica is running this package's migrations",
|
|
1148
|
+
});
|
|
1149
|
+
return undefined;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
catch (e) {
|
|
1153
|
+
if (this.closed)
|
|
1154
|
+
throw new Error(CLOSED_DURING_LOAD, { cause: e });
|
|
1155
|
+
if (a.fatal)
|
|
1156
|
+
throw e;
|
|
1157
|
+
return failSoft("migration_failed", e);
|
|
1158
|
+
}
|
|
1159
|
+
finally {
|
|
1160
|
+
this.migrating = undefined;
|
|
1161
|
+
}
|
|
1162
|
+
if (this.closed)
|
|
1163
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
1164
|
+
const spec = {
|
|
1165
|
+
manifest,
|
|
1166
|
+
entryUrl: a.entryUrl,
|
|
1167
|
+
source: a.source,
|
|
1168
|
+
sha256: a.sha256,
|
|
1169
|
+
config,
|
|
1170
|
+
configHash: deploymentConfigHash(schema, config),
|
|
1171
|
+
permissions: a.set,
|
|
1172
|
+
};
|
|
1173
|
+
let inst;
|
|
1174
|
+
try {
|
|
1175
|
+
inst = await this.setup(spec, setupTimeoutMs);
|
|
1176
|
+
}
|
|
1177
|
+
catch (e) {
|
|
1178
|
+
if (this.closed)
|
|
1179
|
+
throw new Error(CLOSED_DURING_LOAD, { cause: e });
|
|
1180
|
+
if (a.fatal)
|
|
1181
|
+
throw e;
|
|
1182
|
+
return failSoft("setup_failed", e);
|
|
1183
|
+
}
|
|
1184
|
+
if (this.closed) {
|
|
1185
|
+
await inst.dispose({ drainMs: 0 }).catch((err) => {
|
|
1186
|
+
this.deps.log.warn("plugin context close failed", { plugin: name, err });
|
|
1187
|
+
});
|
|
1188
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
1189
|
+
}
|
|
1190
|
+
// 升级:替换加载参数(之后的依赖方连锁与 reload 用新版本),先登记新实例、再撤销旧实例
|
|
1191
|
+
this.specs.set(name, spec);
|
|
1192
|
+
this.unloadedForDependency.delete(name);
|
|
1193
|
+
const old = this.registry.activate(inst);
|
|
1194
|
+
this.loggedFailures.delete(name);
|
|
1195
|
+
await this.writeStatus({ ...base, state: "loaded", reason: null, detail: null });
|
|
1196
|
+
if (old)
|
|
1197
|
+
this.retire(old);
|
|
1198
|
+
return inst;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* 后台撤销被替换的旧实例(排空在途调用,最多 `extensionHost.drainSeconds`):不挡住本副本后续的生命周期操作。
|
|
1202
|
+
* close() 等它们(最多 lifecycleWaitMs)。
|
|
1203
|
+
*/
|
|
1204
|
+
retire(old) {
|
|
1205
|
+
const p = this.disposeQuietly(old, this.drainMs()).finally(() => {
|
|
1206
|
+
this.retiring.delete(old);
|
|
1207
|
+
this.deps.log.info("previous plugin instance retired", {
|
|
1208
|
+
plugin: old.name,
|
|
1209
|
+
generation: old.generation,
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
this.retiring.set(old, p);
|
|
1213
|
+
}
|
|
1214
|
+
/** 记下配置声明插件的清单与权限集合(安装记录不在这里)。 */
|
|
1215
|
+
noteDeclared(manifests, sets) {
|
|
1216
|
+
for (const m of manifests) {
|
|
1217
|
+
const source = this.sourceOf(m.name);
|
|
1218
|
+
if (source !== "config")
|
|
1219
|
+
continue;
|
|
1220
|
+
this.declaredInfo.set(m.name, {
|
|
1221
|
+
source,
|
|
1222
|
+
manifest: m,
|
|
1223
|
+
set: sets.get(m.name),
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* 配置声明的插件(含待批准、待配置而未加载的):权限集合({@link computePermissionSet})、
|
|
1229
|
+
* 部署配置此刻是否合法(按清单的 deployment schema 校验启动配置)。按导入顺序。
|
|
1230
|
+
*/
|
|
1231
|
+
declaredPlugins() {
|
|
1232
|
+
return [...this.declaredInfo].map(([name, d]) => {
|
|
1233
|
+
const schema = d.manifest.config?.deployment;
|
|
1234
|
+
return {
|
|
1235
|
+
name,
|
|
1236
|
+
source: d.source,
|
|
1237
|
+
version: d.manifest.version,
|
|
1238
|
+
permissions: d.set,
|
|
1239
|
+
configurable: schema !== undefined,
|
|
1240
|
+
configValid: this.configValidity.get(name) ??
|
|
1241
|
+
(schema ? schema.safeParse(this.extensionConfig[name] ?? {}).success : true),
|
|
1242
|
+
};
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* 在本副本加载此刻能加载的待定配置声明插件(批准之后,或运行中加载失败后的重试),见 {@link converge}。
|
|
1247
|
+
* 与 unload / reload 串行。返回本次加载的插件名。
|
|
1248
|
+
*/
|
|
1249
|
+
activatePending() {
|
|
1250
|
+
return this.serial(() => this.activatePendingNow());
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* 本副本收敛到部署的期望状态(T14 reconcile 的一步,与 unload / reload 串行):
|
|
1254
|
+
* 1. 待定的配置声明插件(批准后、或运行中加载失败的)按此刻的快照与生效的提供方重试;
|
|
1255
|
+
* 2. 安装记录按 PG 期望状态收敛(见 {@link applyInstalls}:停用 / 卸载的卸下,换包的先加载新再撤销旧,
|
|
1256
|
+
* 失败的每次重试),之后做依赖方连锁;
|
|
1257
|
+
* 3. 再试一次第 1 步(安装记录提供的能力可能放行了依赖它的配置声明插件)。
|
|
1258
|
+
* 读不到安装记录时只做第 1 步并抛错(调用方记日志,下一个周期再来)。
|
|
1259
|
+
*/
|
|
1260
|
+
converge() {
|
|
1261
|
+
return this.serial(async () => {
|
|
1262
|
+
await this.activatePendingNow();
|
|
1263
|
+
const loader = this.installLoader;
|
|
1264
|
+
if (!loader)
|
|
1265
|
+
return;
|
|
1266
|
+
const desired = await loader.desired();
|
|
1267
|
+
// N-2: drop plugins no longer desired before applyInstalls unloads them (the drain can take
|
|
1268
|
+
// extensionHost.drainSeconds; meanwhile a disabled policy plugin must not fail calls closed).
|
|
1269
|
+
// Shrink only: plugins that are newly expected are added after they had their chance to load.
|
|
1270
|
+
await this.refreshPolicyExpectations(desired, { shrinkOnly: true });
|
|
1271
|
+
await this.applyInstalls(loader, desired, false);
|
|
1272
|
+
if (this.pendingActivations.size > 0)
|
|
1273
|
+
await this.activatePendingNow();
|
|
1274
|
+
await this.refreshPolicyExpectations(desired);
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Refreshes {@link policyExpected} from deployment state (M-1):
|
|
1279
|
+
* config-declared plugins and enabled installs (`desired`) from their approved permission snapshot.
|
|
1280
|
+
* `desired` undefined (install records unreadable) keeps the previous install entries.
|
|
1281
|
+
* `shrinkOnly`: only remove entries (a plugin no longer desired or approved), never add new ones.
|
|
1282
|
+
*/
|
|
1283
|
+
async refreshPolicyExpectations(desired, opts = {}) {
|
|
1284
|
+
const snapshots = await new ExtensionApprovals(kernelDb(this.deps.db)).all();
|
|
1285
|
+
const next = new Map();
|
|
1286
|
+
const add = (name, set) => {
|
|
1287
|
+
const mounts = set?.permissions?.policyHooks ?? [];
|
|
1288
|
+
if (mounts.length > 0)
|
|
1289
|
+
next.set(name, [...mounts]);
|
|
1290
|
+
};
|
|
1291
|
+
for (const name of this.declaredInfo.keys()) {
|
|
1292
|
+
add(name, snapshots.get(name)?.permissions);
|
|
1293
|
+
}
|
|
1294
|
+
const declared = this.declared();
|
|
1295
|
+
if (desired === undefined) {
|
|
1296
|
+
for (const [name, mounts] of this.policyExpected) {
|
|
1297
|
+
if (!this.declaredInfo.has(name))
|
|
1298
|
+
next.set(name, mounts);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
for (const d of desired) {
|
|
1303
|
+
if (!declared.has(d.name))
|
|
1304
|
+
add(d.name, snapshots.get(d.name)?.permissions);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (opts.shrinkOnly) {
|
|
1308
|
+
for (const name of [...next.keys()]) {
|
|
1309
|
+
if (!this.policyExpected.has(name))
|
|
1310
|
+
next.delete(name);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
this.policyExpected = next;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Plugins that deployment state expects to run `policy` hooks on the mount point (M-1): enabled and
|
|
1317
|
+
* approved, with the mount in `permissions.policyHooks`. In-process, no I/O (see
|
|
1318
|
+
* {@link refreshPolicyExpectations}); the hook registry fails calls closed for those not loaded here.
|
|
1319
|
+
*/
|
|
1320
|
+
expectedPolicy(mount) {
|
|
1321
|
+
return [...this.policyExpected].flatMap(([name, mounts]) => mounts.includes(mount) ? [name] : []);
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* 待定的配置声明插件({@link pendingActivations}):重读快照,按启动时同一套判定({@link planApprovals},
|
|
1325
|
+
* 另加「必需能力此刻要有生效的提供方」)逐个处理;能加载的走与启动相同的加载流程(部署配置、迁移、setup,
|
|
1326
|
+
* setup 时限 `extensionHost.reloadSetupTimeoutSeconds`),失败只记 failed、留着下次重试(T13 → T14 交接);
|
|
1327
|
+
* 仍被依赖阻塞的改记 pending / dependency。反复直到不动点(加载一个可能放行依赖它的),
|
|
1328
|
+
* 最后对新提供的能力做依赖方连锁(与 {@link reload} 相同)。
|
|
1329
|
+
*/
|
|
1330
|
+
async activatePendingNow() {
|
|
1331
|
+
if (this.pendingActivations.size === 0)
|
|
1332
|
+
return [];
|
|
1333
|
+
const approvals = new ExtensionApprovals(kernelDb(this.deps.db));
|
|
1334
|
+
const snapshots = await approvals.all();
|
|
1335
|
+
const activated = [];
|
|
1336
|
+
/** 本次已经试过且失败的:同一次不再重试(下一次 converge 再试)。 */
|
|
1337
|
+
const failedNow = new Set();
|
|
1338
|
+
for (let progress = true; progress;) {
|
|
1339
|
+
progress = false;
|
|
1340
|
+
for (const [name, a] of [...this.pendingActivations]) {
|
|
1341
|
+
if (this.closed)
|
|
1342
|
+
throw new Error("plugin host is closed");
|
|
1343
|
+
if (failedNow.has(name))
|
|
1344
|
+
continue;
|
|
1345
|
+
const decision = this.decidePending(name, a, snapshots);
|
|
1346
|
+
if (decision.action === "pending") {
|
|
1347
|
+
await this.stillPending(a, decision.pending);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
this.pendingActivations.delete(name);
|
|
1351
|
+
this.dropPending(name);
|
|
1352
|
+
try {
|
|
1353
|
+
await this.activate(approvals, { ...a, decision, fatal: false }, this.reloadSetupTimeoutMs());
|
|
1354
|
+
}
|
|
1355
|
+
catch (e) {
|
|
1356
|
+
if (this.closed)
|
|
1357
|
+
throw e;
|
|
1358
|
+
// 配置声明的插件迁移 / setup 出错时 activate 抛错(启动时内核失败);运行中只记 failed
|
|
1359
|
+
this.deps.log.error("approved plugin not loaded", { plugin: name, err: e });
|
|
1360
|
+
await this.writeStatus({
|
|
1361
|
+
plugin: name,
|
|
1362
|
+
version: a.manifest.version,
|
|
1363
|
+
sha256: a.sha256,
|
|
1364
|
+
source: a.source,
|
|
1365
|
+
state: "failed",
|
|
1366
|
+
reason: "setup_failed",
|
|
1367
|
+
detail: errMsg(e),
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
if (this.registry.current(name)) {
|
|
1371
|
+
activated.push(name);
|
|
1372
|
+
progress = true;
|
|
1373
|
+
this.deps.log.info("approved plugin loaded", { plugin: name });
|
|
1374
|
+
}
|
|
1375
|
+
else {
|
|
1376
|
+
// 加载失败(迁移、setup)或待配置:留着,下一次 converge 重试(同一次不再试)
|
|
1377
|
+
if (!this.pendingActivations.has(name))
|
|
1378
|
+
this.pendingActivations.set(name, a);
|
|
1379
|
+
failedNow.add(name);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (activated.length > 0) {
|
|
1384
|
+
await this.cascade(activated.flatMap((n) => this.registry.current(n)?.manifest.provides ?? []), this.drainMs(), new Set(activated));
|
|
1385
|
+
}
|
|
1386
|
+
return activated;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* 待批准插件此刻的判定:已生效的(同名实例除外:升级时按新权限集合比对快照)视为已批准;必需能力没有生效的
|
|
1390
|
+
* 提供方时仍待定(dependency)。`providers` 给出能力 → 声明提供它但没生效的安装记录(填 blockedBy)。
|
|
1391
|
+
*/
|
|
1392
|
+
decidePending(name, a, snapshots, providers = this.installProvides) {
|
|
1393
|
+
const loaded = this.registry.all().filter((i) => i.name !== name);
|
|
1394
|
+
const manifests = loaded.map((i) => i.manifest);
|
|
1395
|
+
const sets = new Map([
|
|
1396
|
+
...loaded.map((i) => [i.name, i.permissions]),
|
|
1397
|
+
[name, a.set],
|
|
1398
|
+
]);
|
|
1399
|
+
const decision = planApprovals([...manifests, a.manifest], sets, withPreApproved(snapshots, manifests, sets), { autoApprove: this.autoApprove }).find((d) => d.plugin === name);
|
|
1400
|
+
if (decision.action === "pending")
|
|
1401
|
+
return decision;
|
|
1402
|
+
const provided = new Set(loaded.flatMap((i) => [...i.provided.keys()]));
|
|
1403
|
+
const missing = (a.manifest.requires ?? []).filter((c) => !provided.has(c));
|
|
1404
|
+
if (missing.length === 0)
|
|
1405
|
+
return decision;
|
|
1406
|
+
return {
|
|
1407
|
+
plugin: name,
|
|
1408
|
+
action: "pending",
|
|
1409
|
+
pending: {
|
|
1410
|
+
plugin: name,
|
|
1411
|
+
version: a.manifest.version,
|
|
1412
|
+
added: decision.added,
|
|
1413
|
+
removed: decision.removed,
|
|
1414
|
+
reason: "dependency",
|
|
1415
|
+
blockedBy: missing.map((extensionPoint) => ({
|
|
1416
|
+
extensionPoint,
|
|
1417
|
+
providers: [
|
|
1418
|
+
...new Set([
|
|
1419
|
+
...[...this.pendingActivations.values()]
|
|
1420
|
+
.filter((p) => p.manifest.provides.includes(extensionPoint))
|
|
1421
|
+
.map((p) => p.manifest.name),
|
|
1422
|
+
...(providers.get(extensionPoint) ?? []).filter((n) => n !== name),
|
|
1423
|
+
]),
|
|
1424
|
+
],
|
|
1425
|
+
})),
|
|
1426
|
+
},
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
/** 仍不能加载的待批准插件:原因变了(如已批准、只剩依赖阻塞)时更新待批准清单与副本状态。 */
|
|
1430
|
+
async stillPending(a, p) {
|
|
1431
|
+
const at = this.pendingPlugins.findIndex((x) => x.plugin === p.plugin);
|
|
1432
|
+
const before = at >= 0 ? this.pendingPlugins[at] : undefined;
|
|
1433
|
+
if (at >= 0)
|
|
1434
|
+
this.pendingPlugins[at] = p;
|
|
1435
|
+
else
|
|
1436
|
+
this.pendingPlugins.push(p);
|
|
1437
|
+
if (before?.reason === p.reason)
|
|
1438
|
+
return;
|
|
1439
|
+
const reason = p.reason === "dependency" ? "dependency" : "approval";
|
|
1440
|
+
await this.writeStatus({
|
|
1441
|
+
plugin: p.plugin,
|
|
1442
|
+
version: a.manifest.version,
|
|
1443
|
+
sha256: a.sha256,
|
|
1444
|
+
source: a.source,
|
|
1445
|
+
state: "pending",
|
|
1446
|
+
reason,
|
|
1447
|
+
detail: null,
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
/** 插件的来源(导入过的;未知返回 undefined)。 */
|
|
1451
|
+
sourceOf(plugin) {
|
|
1452
|
+
return this.sources.get(plugin)?.source;
|
|
1453
|
+
}
|
|
1454
|
+
/** 配置声明的插件名 → 来源(安装包不能与它们重名)。 */
|
|
1455
|
+
declared() {
|
|
1456
|
+
return new Map([...this.sources].flatMap(([n, s]) => s.source === "install" ? [] : [[n, s.source]]));
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Extensions enabled in the org: `extension_org_configs.enabled` plus every required bootstrap
|
|
1460
|
+
* plugin (see `enabled.ts`); queried every time, not cached.
|
|
1461
|
+
* Cost (M-4): hooks call this on every run, so one PG query per model attempt and per tool call.
|
|
1462
|
+
* Acceptable for now; if a cache is ever added, give it a short TTL so an org disabling a plugin
|
|
1463
|
+
* takes effect quickly.
|
|
1464
|
+
*/
|
|
1465
|
+
async enabledPlugins(orgId) {
|
|
1466
|
+
return queryEnabledPlugins(this.deps.db, orgId);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* The access behind an extension's `ctx.extensionPoints`: only active instances in the registry
|
|
1470
|
+
* (dependencies load in topological order, so the depended-on are ready first); org enablement is
|
|
1471
|
+
* read from `extension_org_configs` every time, not cached. Resolved live on every call: once a
|
|
1472
|
+
* provider is revoked it is no longer returned.
|
|
1473
|
+
*/
|
|
1474
|
+
extensionPointAccess = {
|
|
1475
|
+
providers: (ref) => this.registry
|
|
1476
|
+
.all()
|
|
1477
|
+
.flatMap((i) => i.provided.has(ref) ? [{ plugin: i.name, impl: i.provided.get(ref) }] : []),
|
|
1478
|
+
enabledPlugins: (orgId) => this.enabledPlugins(orgId),
|
|
1479
|
+
};
|
|
1480
|
+
get loadDeps() {
|
|
1481
|
+
return {
|
|
1482
|
+
kernel: this.deps,
|
|
1483
|
+
extensionPointAccess: this.extensionPointAccess,
|
|
1484
|
+
nextGeneration: (name) => {
|
|
1485
|
+
const g = (this.generations.get(name) ?? 0) + 1;
|
|
1486
|
+
this.generations.set(name, g);
|
|
1487
|
+
return g;
|
|
1488
|
+
},
|
|
1489
|
+
imports: this.imports,
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* 按加载参数建一个新实例(setup 期间的 ctx 记在 `settingUp`,close() 时立即关闭)。
|
|
1494
|
+
* `timeoutMs`(运行中重新加载):setup 超时或宿主被关闭时关闭这个 ctx 并抛错;迟到完成的实例随即撤销。
|
|
1495
|
+
* 配置声明的插件启动时不设时限(见 {@link load})。
|
|
1496
|
+
*/
|
|
1497
|
+
async setup(spec, timeoutMs) {
|
|
1498
|
+
if (this.imports.record(spec.manifest.name, spec.entryUrl)) {
|
|
1499
|
+
warnUpgrades(this.deps.log, this.imports);
|
|
1500
|
+
}
|
|
1501
|
+
let ctx;
|
|
1502
|
+
const p = setupInstance(this.loadDeps, spec.manifest, {
|
|
1503
|
+
entryUrl: spec.entryUrl,
|
|
1504
|
+
source: spec.source,
|
|
1505
|
+
sha256: spec.sha256,
|
|
1506
|
+
config: spec.config,
|
|
1507
|
+
configHash: spec.configHash,
|
|
1508
|
+
permissions: spec.permissions,
|
|
1509
|
+
onCtx: (c) => {
|
|
1510
|
+
if (c)
|
|
1511
|
+
ctx = c;
|
|
1512
|
+
if (c || this.settingUp === ctx)
|
|
1513
|
+
this.settingUp = c;
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
1516
|
+
if (timeoutMs === undefined)
|
|
1517
|
+
return p;
|
|
1518
|
+
let timer;
|
|
1519
|
+
let onClose = () => { };
|
|
1520
|
+
const stop = new Promise((resolve) => {
|
|
1521
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
1522
|
+
onClose = () => resolve("closed");
|
|
1523
|
+
if (this.abort.signal.aborted)
|
|
1524
|
+
onClose();
|
|
1525
|
+
else
|
|
1526
|
+
this.abort.signal.addEventListener("abort", onClose, { once: true });
|
|
1527
|
+
});
|
|
1528
|
+
try {
|
|
1529
|
+
const r = await Promise.race([p.then((inst) => ({ inst })), stop]);
|
|
1530
|
+
if (typeof r === "object")
|
|
1531
|
+
return r.inst;
|
|
1532
|
+
// setup 迟迟不返回:关闭它的 ctx(此后它的登记立即撤销),迟到完成的实例直接撤销
|
|
1533
|
+
p.then((inst) => this.disposeQuietly(inst, 0), () => { });
|
|
1534
|
+
if (this.settingUp === ctx)
|
|
1535
|
+
this.settingUp = undefined;
|
|
1536
|
+
await ctx?.close().catch((err) => {
|
|
1537
|
+
this.deps.log.warn("plugin context close failed", { plugin: spec.manifest.name, err });
|
|
1538
|
+
});
|
|
1539
|
+
if (r === "closed")
|
|
1540
|
+
throw new Error(CLOSED_DURING_LOAD);
|
|
1541
|
+
throw new Error(`plugin ${spec.manifest.name}: setup did not finish within ${timeoutMs} ms during reload; the previous instance is kept`);
|
|
1542
|
+
}
|
|
1543
|
+
finally {
|
|
1544
|
+
clearTimeout(timer);
|
|
1545
|
+
this.abort.signal.removeEventListener("abort", onClose);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
/** 取能力实现:单选能力无需 providerName,多选能力必须指定。 */
|
|
1549
|
+
get(capability, providerName) {
|
|
1550
|
+
const providers = this.registry.all().filter((i) => i.provided.has(capability));
|
|
1551
|
+
if (providers.length === 0)
|
|
1552
|
+
throw new Error(`extension point ${capability} has no provider`);
|
|
1553
|
+
const single = EXTENSION_POINT_CARDINALITY[parseExtensionPoint(capability).name] === "single";
|
|
1554
|
+
let chosen;
|
|
1555
|
+
if (providerName !== undefined) {
|
|
1556
|
+
chosen = providers.find((p) => p.name === providerName);
|
|
1557
|
+
if (!chosen) {
|
|
1558
|
+
throw new Error(`extension point ${capability} is not provided by plugin ${providerName}`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
else if (single) {
|
|
1562
|
+
chosen = providers[0];
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
// 多选能力即使只有一个提供者也要显式指定,避免新增提供者后行为悄悄变化
|
|
1566
|
+
throw new Error(`extension point ${capability} is multi-provider; specify a provider name (available: ${providers.map((p) => p.name).join(", ")})`);
|
|
1567
|
+
}
|
|
1568
|
+
return chosen.provided.get(capability);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* 列出提供某能力(任意主版本)的所有插件及实现;`capability` 为完整引用(含主版本),
|
|
1572
|
+
* 同一插件提供多个主版本时各占一项,由调用方按主版本选择。
|
|
1573
|
+
* 每项带插件版本与部署配置摘要({@link deploymentConfigHash}),供调用方做「插件升级 / 改配置即失效」的缓存键。
|
|
1574
|
+
* 实时取自注册表:调用方不要缓存实现对象(插件撤销 / 升级后旧实现不再有效)。
|
|
1575
|
+
*/
|
|
1576
|
+
list(extensionPointName) {
|
|
1577
|
+
const out = [];
|
|
1578
|
+
const tracked = TRACKED_EXTENSION_POINTS[extensionPointName];
|
|
1579
|
+
for (const i of this.registry.all()) {
|
|
1580
|
+
for (const [cap, impl] of i.provided) {
|
|
1581
|
+
if (parseExtensionPoint(cap).name === extensionPointName) {
|
|
1582
|
+
out.push({
|
|
1583
|
+
plugin: i.name,
|
|
1584
|
+
capability: cap,
|
|
1585
|
+
// 内核消费的提供方调用计入实例在途计数:停用 / 升级时先排空、到时限中止(C17)
|
|
1586
|
+
impl: tracked && typeof impl === "object" && impl !== null
|
|
1587
|
+
? trackCalls(impl, i.inflight, tracked)
|
|
1588
|
+
: impl,
|
|
1589
|
+
version: i.version,
|
|
1590
|
+
configHash: i.configHash,
|
|
1591
|
+
inflight: i.inflight,
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return out;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* 已加载插件的部署配置(已按清单 schema 校验,含密钥),供内核调用该插件的能力时作为 `config` 传回
|
|
1600
|
+
* (如 `identity-source@1` 的登录配置);未加载返回 undefined。不得回显到响应或日志。
|
|
1601
|
+
*/
|
|
1602
|
+
deploymentConfig(plugin) {
|
|
1603
|
+
return this.registry.current(plugin)?.config;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* 本副本拿得到的插件清单里的部署级 schema(生效实例 → 配置声明 → 安装记录准备好的候选);
|
|
1607
|
+
* 都没有返回 undefined(如未加载的安装记录:调用方改用包内静态清单)。
|
|
1608
|
+
*/
|
|
1609
|
+
deploymentSchema(plugin) {
|
|
1610
|
+
const m = this.registry.current(plugin)?.manifest ??
|
|
1611
|
+
this.declaredInfo.get(plugin)?.manifest ??
|
|
1612
|
+
this.importedManifests.get(plugin)?.manifest;
|
|
1613
|
+
return m && { version: m.version, schema: m.config?.deployment };
|
|
1614
|
+
}
|
|
1615
|
+
/** 在本副本生效、声明了部署级配置的插件。 */
|
|
1616
|
+
configurablePlugins() {
|
|
1617
|
+
return this.registry
|
|
1618
|
+
.all()
|
|
1619
|
+
.filter((i) => i.manifest.config?.deployment !== undefined)
|
|
1620
|
+
.map((i) => i.name);
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* 部署配置保存后(`kernel.plugin-deployment-config`)本副本重读并校验(C10;与 unload / reload 串行):
|
|
1624
|
+
* - 插件没在本副本生效 → `not_loaded`(配置声明的刷新后台的合法性判定;待配置的由 reconcile 加载);
|
|
1625
|
+
* - 不合法 → `invalid`:保持当前值,告警;
|
|
1626
|
+
* - 没变 → `unchanged`;只改了 hot 字段 → `hot`:就地替换,插件经 `ctx.config` 读到新值,不重新 setup;
|
|
1627
|
+
* - 改了非 hot 字段 → `reload`:用新配置重新加载实例(先新后旧,排空在途调用),依赖方连锁;
|
|
1628
|
+
* 重新加载失败旧实例保留(`reload_failed`,记 error)。
|
|
1629
|
+
*/
|
|
1630
|
+
applyDeploymentConfig(plugin) {
|
|
1631
|
+
return this.serial(async () => {
|
|
1632
|
+
const inst = this.registry.current(plugin);
|
|
1633
|
+
const spec = this.specs.get(plugin);
|
|
1634
|
+
if (!inst || !spec) {
|
|
1635
|
+
const declared = this.declaredInfo.get(plugin);
|
|
1636
|
+
const schema = declared?.manifest.config?.deployment;
|
|
1637
|
+
if (schema) {
|
|
1638
|
+
try {
|
|
1639
|
+
const raw = await this.rawConfigOf(plugin, schema);
|
|
1640
|
+
this.configValidity.set(plugin, schema.safeParse(raw).success);
|
|
1641
|
+
}
|
|
1642
|
+
catch (err) {
|
|
1643
|
+
this.deps.log.warn("cannot read the stored deployment config of plugin", {
|
|
1644
|
+
plugin,
|
|
1645
|
+
err,
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return "not_loaded";
|
|
1650
|
+
}
|
|
1651
|
+
const schema = inst.manifest.config?.deployment;
|
|
1652
|
+
if (!schema)
|
|
1653
|
+
return "unchanged";
|
|
1654
|
+
let raw;
|
|
1655
|
+
try {
|
|
1656
|
+
raw = await this.rawConfigOf(plugin, schema);
|
|
1657
|
+
}
|
|
1658
|
+
catch (err) {
|
|
1659
|
+
this.deps.log.error("cannot read the stored deployment config of plugin; keeping its current config", { plugin, err });
|
|
1660
|
+
return "invalid";
|
|
1661
|
+
}
|
|
1662
|
+
const r = schema.safeParse(raw);
|
|
1663
|
+
if (!r.success) {
|
|
1664
|
+
this.deps.log.warn("plugin deployment config changed but is invalid on this replica; keeping the current config", {
|
|
1665
|
+
plugin,
|
|
1666
|
+
detail: toIssues(r.error)
|
|
1667
|
+
.map((i) => `${i.path || "<root>"}: ${i.message}`)
|
|
1668
|
+
.join("; "),
|
|
1669
|
+
});
|
|
1670
|
+
return "invalid";
|
|
1671
|
+
}
|
|
1672
|
+
const next = r.data;
|
|
1673
|
+
if (inst.source !== "install")
|
|
1674
|
+
this.configValidity.set(plugin, true);
|
|
1675
|
+
const changed = changedPaths(schema, inst.config, next);
|
|
1676
|
+
if (changed.length === 0)
|
|
1677
|
+
return "unchanged";
|
|
1678
|
+
this.reportSecrets(schema, next);
|
|
1679
|
+
const hot = new Set(configFields(schema)
|
|
1680
|
+
.filter((f) => f.meta?.hot)
|
|
1681
|
+
.map((f) => f.path));
|
|
1682
|
+
const configHash = deploymentConfigHash(schema, next);
|
|
1683
|
+
// 之后的重新加载(依赖方连锁等)都用新配置;重新加载失败时恢复原来的(旧实例的配置)
|
|
1684
|
+
this.specs.set(plugin, { ...spec, config: next, configHash });
|
|
1685
|
+
if (changed.every((p) => hot.has(p))) {
|
|
1686
|
+
inst.updateConfig(next, configHash);
|
|
1687
|
+
this.deps.log.info("plugin deployment config updated in place (hot fields)", {
|
|
1688
|
+
plugin,
|
|
1689
|
+
changed,
|
|
1690
|
+
});
|
|
1691
|
+
return "hot";
|
|
1692
|
+
}
|
|
1693
|
+
const drainMs = this.drainMs();
|
|
1694
|
+
let fresh;
|
|
1695
|
+
try {
|
|
1696
|
+
fresh = await this.doReload(plugin, drainMs);
|
|
1697
|
+
}
|
|
1698
|
+
catch (err) {
|
|
1699
|
+
// 旧实例保留,加载参数也回到旧实例的(否则依赖方连锁 / reload 会用失败的配置重建);
|
|
1700
|
+
// 配置仍与 PG 不同,下次消息或周期 reconcile 再试
|
|
1701
|
+
if (this.specs.get(plugin)?.config === next)
|
|
1702
|
+
this.specs.set(plugin, spec);
|
|
1703
|
+
if (this.closed)
|
|
1704
|
+
throw err;
|
|
1705
|
+
this.deps.log.error("plugin reload after a deployment config change failed; it keeps its previous instance", { plugin, changed, err });
|
|
1706
|
+
return "reload_failed";
|
|
1707
|
+
}
|
|
1708
|
+
await this.cascade(fresh.manifest.provides, drainMs);
|
|
1709
|
+
this.deps.log.info("plugin reloaded after a deployment config change", { plugin, changed });
|
|
1710
|
+
return "reload";
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
/** 本次未加载、等待批准权限的插件(含因 `requires` 只能由它们满足而未加载的插件),按依赖拓扑序。 */
|
|
1714
|
+
pending() {
|
|
1715
|
+
return structuredClone(this.pendingPlugins);
|
|
1716
|
+
}
|
|
1717
|
+
/** 已加载插件的清单(按加载顺序,即依赖拓扑序)。 */
|
|
1718
|
+
manifests() {
|
|
1719
|
+
return this.registry.all().map((i) => i.manifest);
|
|
1720
|
+
}
|
|
1721
|
+
/** 已加载插件的权限集合(加载时计算,清单不可变);未加载返回 undefined。 */
|
|
1722
|
+
permissionSet(plugin) {
|
|
1723
|
+
return this.registry.current(plugin)?.permissions;
|
|
1724
|
+
}
|
|
1725
|
+
/** 生效的插件实例;未加载 / 已撤销返回 undefined。 */
|
|
1726
|
+
current(plugin) {
|
|
1727
|
+
return this.registry.current(plugin);
|
|
1728
|
+
}
|
|
1729
|
+
/** 生效的插件实例(按加载顺序)。 */
|
|
1730
|
+
instances() {
|
|
1731
|
+
return this.registry.all();
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Route gate: the extension belongs to this deployment (config-declared, or an install
|
|
1735
|
+
* record in PG) but is not active on this replica right now -> true, and its `/plugins/<name>/*` and
|
|
1736
|
+
* `/plugin-ui/<name>/*` answer 503 `extension.unavailable` (the routes stay mounted and only gate);
|
|
1737
|
+
* active, or a completely unknown name -> false (unknown is a plain 404). Looks only at what the
|
|
1738
|
+
* deployment knows, not at this process's load history. Install record names are cached for
|
|
1739
|
+
* {@link KNOWN_NAMES_TTL_MS} and invalidated on registry changes; when they cannot be read the name
|
|
1740
|
+
* counts as unknown (404).
|
|
1741
|
+
*/
|
|
1742
|
+
async unavailable(plugin) {
|
|
1743
|
+
if (this.registry.current(plugin))
|
|
1744
|
+
return false;
|
|
1745
|
+
// 配置声明(含待批准、配置不合法的);安装记录以 PG 为准(卸载后的名字回到 404)
|
|
1746
|
+
const source = this.sources.get(plugin)?.source;
|
|
1747
|
+
if (source === "config")
|
|
1748
|
+
return true;
|
|
1749
|
+
return (await this.installed()).has(plugin);
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* 作废路由闸门的安装记录名字缓存(收到 `kernel.plugins.changed` 时各副本调用:卸载 / 新装后不等 TTL)。
|
|
1753
|
+
*/
|
|
1754
|
+
invalidateKnownNames() {
|
|
1755
|
+
this.installedCache = undefined;
|
|
1756
|
+
}
|
|
1757
|
+
/** 安装记录里的插件名(缓存,见 {@link unavailable})。 */
|
|
1758
|
+
installed() {
|
|
1759
|
+
const load = this.installedNames;
|
|
1760
|
+
if (!load)
|
|
1761
|
+
return Promise.resolve(new Set());
|
|
1762
|
+
const now = Date.now();
|
|
1763
|
+
if (!this.installedCache || now - this.installedCache.at > KNOWN_NAMES_TTL_MS) {
|
|
1764
|
+
const names = load()
|
|
1765
|
+
.then((xs) => new Set(xs))
|
|
1766
|
+
.catch((err) => {
|
|
1767
|
+
this.deps.log.warn("cannot read plugin install records for the route gate", { err });
|
|
1768
|
+
this.installedCache = undefined;
|
|
1769
|
+
return new Set();
|
|
1770
|
+
});
|
|
1771
|
+
this.installedCache = { at: now, names };
|
|
1772
|
+
}
|
|
1773
|
+
return this.installedCache.names;
|
|
1774
|
+
}
|
|
1775
|
+
/** Subscribes to registry changes (activated / replaced / removed, the argument is the extension name): this process only, see {@link ExtensionRegistry.onChange}. */
|
|
1776
|
+
onChange(listener) {
|
|
1777
|
+
return this.registry.onChange(listener);
|
|
1778
|
+
}
|
|
1779
|
+
/** 插件路由表(给 HTTP 的插件转发用):实时路由、注册表版本、闸门与变化通知(中止该插件进行中的请求)。 */
|
|
1780
|
+
routeTable() {
|
|
1781
|
+
return {
|
|
1782
|
+
routes: () => this.routes(),
|
|
1783
|
+
revision: () => this.registry.revision(),
|
|
1784
|
+
unavailable: (plugin) => this.unavailable(plugin),
|
|
1785
|
+
onChange: (listener) => this.registry.onChange(listener),
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
/** 所有插件注册的 HTTP 路由(路径已带 `/plugins/<name>` 前缀)。 */
|
|
1789
|
+
routes() {
|
|
1790
|
+
return this.registry.all().flatMap((i) => [...i.routes]);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* 所有插件经 `ctx.tools.register` 注册的工具(按加载顺序、注册顺序),附带实例的在途计数:
|
|
1794
|
+
* 执行时经 `inflight.run` 调用 handler,实例撤销时排空 / 中止。
|
|
1795
|
+
*/
|
|
1796
|
+
tools() {
|
|
1797
|
+
return this.registry.all().flatMap((i) => i.tools.map((t) => ({
|
|
1798
|
+
plugin: i.name,
|
|
1799
|
+
spec: t.spec,
|
|
1800
|
+
handler: t.handler,
|
|
1801
|
+
inflight: i.inflight,
|
|
1802
|
+
})));
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* 已加载插件清单里的界面贡献(按加载顺序、清单顺序),附带 setup 期间注册的视图 render / 动作处理函数。
|
|
1806
|
+
* 是否在某组织启用由调用方另查(每次查询,不缓存)。
|
|
1807
|
+
*/
|
|
1808
|
+
uiContributions() {
|
|
1809
|
+
return this.registry.all().flatMap((i) => (i.manifest.ui ?? []).map((c) => {
|
|
1810
|
+
const render = c.kind === "view" ? i.uiViews.get(c.id) : undefined;
|
|
1811
|
+
const action = c.kind === "view" ? i.uiActions.get(c.id) : undefined;
|
|
1812
|
+
return {
|
|
1813
|
+
plugin: i.name,
|
|
1814
|
+
contribution: c,
|
|
1815
|
+
...(render !== undefined && { render }),
|
|
1816
|
+
...(action !== undefined && { action }),
|
|
1817
|
+
};
|
|
1818
|
+
}));
|
|
1819
|
+
}
|
|
1820
|
+
/** 串行执行生命周期操作(unload / reload 互不交错);宿主关闭后拒绝。 */
|
|
1821
|
+
serial(fn) {
|
|
1822
|
+
const guarded = () => {
|
|
1823
|
+
if (this.closed)
|
|
1824
|
+
return Promise.reject(new Error("plugin host is closed"));
|
|
1825
|
+
return fn();
|
|
1826
|
+
};
|
|
1827
|
+
const run = this.lifecycle.then(guarded, guarded);
|
|
1828
|
+
this.lifecycle = run.catch(() => { });
|
|
1829
|
+
return run;
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* 撤销一个插件(本副本,C17):从注册表摘下(此后新请求取不到它)、排空在途调用(最多 `drainMs`,
|
|
1833
|
+
* 默认 `extensionHost.drainSeconds`)、逐项撤销登记、调插件 `dispose`(最多 `extensionHost.disposeTimeoutSeconds`)。连锁:
|
|
1834
|
+
* - `requires` 它提供的能力、且该能力已没有其他提供方的插件随之卸下(状态 `unloaded`、原因 `dependency`),
|
|
1835
|
+
* 提供方回来后({@link reload})自动重新加载;
|
|
1836
|
+
* - 其余 `requires` / `optional` 它提供的能力的插件(含间接依赖)在本副本按拓扑序各重新加载一次。
|
|
1837
|
+
* 返回卸下的插件名(含连锁卸下的);插件不在注册表时返回空数组。宿主关闭后拒绝。
|
|
1838
|
+
*/
|
|
1839
|
+
unload(plugin, opts = {}) {
|
|
1840
|
+
return this.serial(async () => {
|
|
1841
|
+
const drainMs = opts.drainMs ?? this.drainMs();
|
|
1842
|
+
const inst = await this.doUnload(plugin, drainMs, null);
|
|
1843
|
+
if (!inst)
|
|
1844
|
+
return [];
|
|
1845
|
+
return [plugin, ...(await this.cascade(inst.manifest.provides, drainMs))];
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* 用同一份加载参数(同一模块入口、部署配置与权限)新建一个实例并原子替换(同进程重新加载 / 撤销后重新启用):
|
|
1850
|
+
* 新实例 setup 成功后才登记(setup 最多 `extensionHost.reloadSetupTimeoutSeconds`),旧实例随后撤销(排空在途调用);
|
|
1851
|
+
* setup 失败或超时时旧实例保持不变并抛错。依赖它的插件在本副本按拓扑序各重新加载一次;此前因它卸下的依赖方重新加载。
|
|
1852
|
+
* 同一入口的重新加载不新增模块代码;升级到新入口(T14)的模块代码留在内存直到进程重启,不同入口数超过阈值时告警。
|
|
1853
|
+
* 宿主关闭后拒绝。
|
|
1854
|
+
*/
|
|
1855
|
+
reload(plugin, opts = {}) {
|
|
1856
|
+
return this.serial(async () => {
|
|
1857
|
+
const drainMs = opts.drainMs ?? this.drainMs();
|
|
1858
|
+
const inst = await this.doReload(plugin, drainMs);
|
|
1859
|
+
await this.cascade(inst.manifest.provides, drainMs);
|
|
1860
|
+
return inst;
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
/** 摘下并撤销一个实例(不做连锁);不在注册表时返回 undefined。 */
|
|
1864
|
+
async doUnload(plugin, drainMs, reason) {
|
|
1865
|
+
if (this.closed)
|
|
1866
|
+
throw new Error("plugin host is closed");
|
|
1867
|
+
const inst = this.registry.deactivate(plugin);
|
|
1868
|
+
if (!inst)
|
|
1869
|
+
return undefined;
|
|
1870
|
+
if (reason === "dependency")
|
|
1871
|
+
this.unloadedForDependency.add(plugin);
|
|
1872
|
+
else
|
|
1873
|
+
this.unloadedForDependency.delete(plugin);
|
|
1874
|
+
await this.disposeQuietly(inst, drainMs);
|
|
1875
|
+
await this.writeStatus({
|
|
1876
|
+
plugin,
|
|
1877
|
+
version: inst.version,
|
|
1878
|
+
sha256: inst.sha256,
|
|
1879
|
+
source: inst.source,
|
|
1880
|
+
state: "unloaded",
|
|
1881
|
+
reason,
|
|
1882
|
+
detail: null,
|
|
1883
|
+
});
|
|
1884
|
+
this.deps.log.info("plugin unloaded", { plugin, generation: inst.generation, reason });
|
|
1885
|
+
return inst;
|
|
1886
|
+
}
|
|
1887
|
+
/** 新建实例并原子替换、撤销旧实例(不做连锁)。 */
|
|
1888
|
+
async doReload(plugin, drainMs) {
|
|
1889
|
+
if (this.closed)
|
|
1890
|
+
throw new Error("plugin host is closed");
|
|
1891
|
+
const spec = this.specs.get(plugin);
|
|
1892
|
+
if (!spec)
|
|
1893
|
+
throw new Error(`plugin ${plugin} has never been loaded in this process`);
|
|
1894
|
+
const available = new Set(this.registry
|
|
1895
|
+
.all()
|
|
1896
|
+
.filter((i) => i.name !== plugin)
|
|
1897
|
+
.flatMap((i) => [...i.provided.keys()]));
|
|
1898
|
+
const missing = (spec.manifest.requires ?? []).filter((cap) => !available.has(cap));
|
|
1899
|
+
if (missing.length > 0) {
|
|
1900
|
+
throw new ExtensionResolveError(`plugin ${plugin}: required extension point ${missing.join(", ")} has no provider`);
|
|
1901
|
+
}
|
|
1902
|
+
const inst = await this.setup(spec, this.reloadSetupTimeoutMs());
|
|
1903
|
+
if (this.closed) {
|
|
1904
|
+
await this.disposeQuietly(inst, 0);
|
|
1905
|
+
throw new Error("plugin host is closed");
|
|
1906
|
+
}
|
|
1907
|
+
this.unloadedForDependency.delete(plugin);
|
|
1908
|
+
const old = this.registry.activate(inst);
|
|
1909
|
+
await this.writeStatus({
|
|
1910
|
+
plugin,
|
|
1911
|
+
version: inst.version,
|
|
1912
|
+
sha256: inst.sha256,
|
|
1913
|
+
source: inst.source,
|
|
1914
|
+
state: "loaded",
|
|
1915
|
+
reason: null,
|
|
1916
|
+
detail: null,
|
|
1917
|
+
});
|
|
1918
|
+
if (old)
|
|
1919
|
+
await this.disposeQuietly(old, drainMs);
|
|
1920
|
+
this.deps.log.info("plugin reloaded", { plugin, generation: inst.generation });
|
|
1921
|
+
return inst;
|
|
1922
|
+
}
|
|
1923
|
+
/** 生效实例按依赖拓扑排序(解析不了时退回注册表顺序)。 */
|
|
1924
|
+
topoOrder() {
|
|
1925
|
+
const all = this.registry.all();
|
|
1926
|
+
try {
|
|
1927
|
+
const order = resolveExtensions(all.map((i) => i.manifest), SDK_VERSION).map((m) => m.name);
|
|
1928
|
+
return order.flatMap((n) => {
|
|
1929
|
+
const i = this.registry.current(n);
|
|
1930
|
+
return i ? [i] : [];
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
catch {
|
|
1934
|
+
return all;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* 提供方实例变了(撤销 / 替换)之后的连锁:先一次性收集受影响的插件(`requires` / `optional` 这些能力的,
|
|
1939
|
+
* 以及间接依赖它们的——菱形依赖只收一次),再按拓扑序逐个处理:必需能力已无提供方的卸下(dependency),
|
|
1940
|
+
* 其余重新加载一次。最后把此前因依赖卸下、现在能满足的插件加回来。返回连锁卸下的插件名。
|
|
1941
|
+
* `skip`:本轮刚加载的插件(已按新的提供方 setup,不再重新加载)。
|
|
1942
|
+
*/
|
|
1943
|
+
async cascade(caps, drainMs, skip = new Set()) {
|
|
1944
|
+
const removed = [];
|
|
1945
|
+
const order = this.topoOrder();
|
|
1946
|
+
const changing = new Set(caps);
|
|
1947
|
+
const affected = [];
|
|
1948
|
+
for (const i of order) {
|
|
1949
|
+
const deps = [...(i.manifest.requires ?? []), ...(i.manifest.optional ?? [])];
|
|
1950
|
+
if (!deps.some((c) => changing.has(c)))
|
|
1951
|
+
continue;
|
|
1952
|
+
if (skip.has(i.name))
|
|
1953
|
+
continue;
|
|
1954
|
+
affected.push(i.name);
|
|
1955
|
+
for (const c of i.manifest.provides)
|
|
1956
|
+
changing.add(c);
|
|
1957
|
+
}
|
|
1958
|
+
const providedNow = () => new Set(this.registry.all().flatMap((i) => [...i.provided.keys()]));
|
|
1959
|
+
for (const name of affected) {
|
|
1960
|
+
const i = this.registry.current(name);
|
|
1961
|
+
if (!i)
|
|
1962
|
+
continue;
|
|
1963
|
+
if ((i.manifest.requires ?? []).some((c) => !providedNow().has(c))) {
|
|
1964
|
+
if (await this.doUnload(name, drainMs, "dependency"))
|
|
1965
|
+
removed.push(name);
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1968
|
+
try {
|
|
1969
|
+
await this.doReload(name, drainMs);
|
|
1970
|
+
}
|
|
1971
|
+
catch (err) {
|
|
1972
|
+
if (this.closed)
|
|
1973
|
+
throw err;
|
|
1974
|
+
this.deps.log.error("dependent plugin reload failed; it keeps its previous instance", {
|
|
1975
|
+
plugin: name,
|
|
1976
|
+
err,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
// 此前因依赖卸下的:必需能力都有提供方了就加回来(加回来的可能又满足别的,反复直到不动点)
|
|
1981
|
+
for (let progress = true; progress;) {
|
|
1982
|
+
progress = false;
|
|
1983
|
+
for (const name of [...this.unloadedForDependency]) {
|
|
1984
|
+
const spec = this.specs.get(name);
|
|
1985
|
+
if (!spec || this.registry.current(name)) {
|
|
1986
|
+
this.unloadedForDependency.delete(name);
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
const available = providedNow();
|
|
1990
|
+
if (!(spec.manifest.requires ?? []).every((c) => available.has(c)))
|
|
1991
|
+
continue;
|
|
1992
|
+
try {
|
|
1993
|
+
await this.doReload(name, drainMs);
|
|
1994
|
+
progress = true;
|
|
1995
|
+
}
|
|
1996
|
+
catch (err) {
|
|
1997
|
+
if (this.closed)
|
|
1998
|
+
throw err;
|
|
1999
|
+
this.deps.log.error("plugin reload after its dependency came back failed", {
|
|
2000
|
+
plugin: name,
|
|
2001
|
+
err,
|
|
2002
|
+
});
|
|
2003
|
+
this.unloadedForDependency.delete(name);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return removed;
|
|
2008
|
+
}
|
|
2009
|
+
/** 撤销实例;失败只记 error(已从注册表摘下,不影响其余)。 */
|
|
2010
|
+
async disposeQuietly(inst, drainMs) {
|
|
2011
|
+
try {
|
|
2012
|
+
await inst.dispose({ drainMs, disposeTimeoutMs: this.disposeTimeoutMs() });
|
|
2013
|
+
}
|
|
2014
|
+
catch (err) {
|
|
2015
|
+
this.deps.log.error("plugin dispose failed", { plugin: inst.name, err });
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* 按加载顺序(拓扑序)逆序串行撤销各插件:依赖方先于被依赖方。停机路径:不排空在途调用(运行此时已停),
|
|
2020
|
+
* 队列 worker 等进行中的任务结束。调用后宿主进入 closed 状态(幂等),此后 unload / reload 拒绝。
|
|
2021
|
+
* 进行中的 `load()`:迁移被中止并等其连接结束,setup 中的 ctx 被立即关闭(不等 setup 返回),load() 随后在检查点放弃。
|
|
2022
|
+
* 进行中的 unload / reload 最多等 `lifecycleWaitMs`(默认 2s;卡住的 setup / dispose 不拖住停机),
|
|
2023
|
+
* 之后关闭 setup 中的 ctx、撤销其余实例。单个插件关闭失败记 error 后继续关闭其余插件。
|
|
2024
|
+
*/
|
|
2025
|
+
async close(opts = {}) {
|
|
2026
|
+
this.closed = true;
|
|
2027
|
+
this.abort.abort();
|
|
2028
|
+
await this.migrating?.catch(() => { });
|
|
2029
|
+
let timer;
|
|
2030
|
+
await Promise.race([
|
|
2031
|
+
this.lifecycle.catch(() => { }),
|
|
2032
|
+
new Promise((resolve) => {
|
|
2033
|
+
timer = setTimeout(resolve, opts.lifecycleWaitMs ?? CLOSE_LIFECYCLE_WAIT_MS);
|
|
2034
|
+
}),
|
|
2035
|
+
]);
|
|
2036
|
+
clearTimeout(timer);
|
|
2037
|
+
// 升级时被替换、后台排空中的旧实例:同样最多等 lifecycleWaitMs,之后强制撤销(中止在途调用、不再排空),
|
|
2038
|
+
// 赶在共用资源(Redis / PG)关闭之前
|
|
2039
|
+
if (this.retiring.size > 0) {
|
|
2040
|
+
await Promise.race([
|
|
2041
|
+
Promise.allSettled([...this.retiring.values()]),
|
|
2042
|
+
new Promise((resolve) => {
|
|
2043
|
+
timer = setTimeout(resolve, opts.lifecycleWaitMs ?? CLOSE_LIFECYCLE_WAIT_MS);
|
|
2044
|
+
}),
|
|
2045
|
+
]);
|
|
2046
|
+
clearTimeout(timer);
|
|
2047
|
+
await Promise.allSettled([...this.retiring.keys()].map((old) => old.hurry().catch((err) => {
|
|
2048
|
+
this.deps.log.warn("plugin close failed", { plugin: old.name, err });
|
|
2049
|
+
})));
|
|
2050
|
+
}
|
|
2051
|
+
const settingUp = this.settingUp;
|
|
2052
|
+
this.settingUp = undefined;
|
|
2053
|
+
await settingUp?.close().catch((e) => {
|
|
2054
|
+
this.deps.log.error("plugin close failed", { err: e });
|
|
2055
|
+
});
|
|
2056
|
+
for (const inst of this.registry.all().reverse()) {
|
|
2057
|
+
this.registry.deactivate(inst.name);
|
|
2058
|
+
try {
|
|
2059
|
+
await inst.dispose({
|
|
2060
|
+
drainMs: 0,
|
|
2061
|
+
waitQueueJobs: true,
|
|
2062
|
+
disposeTimeoutMs: this.disposeTimeoutMs(),
|
|
2063
|
+
});
|
|
2064
|
+
this.deps.log.info("plugin closed", { plugin: inst.name });
|
|
2065
|
+
}
|
|
2066
|
+
catch (e) {
|
|
2067
|
+
this.deps.log.error("plugin close failed", { plugin: inst.name, err: e });
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|