@capybara-hq/kernel 0.0.0-stage → 0.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +20 -2
- package/bootstrap.json +22 -0
- package/dist/admin/admin.controller.d.ts +139 -0
- package/dist/admin/admin.controller.js +238 -0
- package/dist/admin/extensions.controller.d.ts +100 -0
- package/dist/admin/extensions.controller.js +226 -0
- package/dist/admin/setup.controller.d.ts +36 -0
- package/dist/admin/setup.controller.js +106 -0
- package/dist/admin/setup.service.d.ts +47 -0
- package/dist/admin/setup.service.js +90 -0
- package/dist/agents/agents.controller.d.ts +205 -0
- package/dist/agents/agents.controller.js +144 -0
- package/dist/agents/agents.service.d.ts +177 -0
- package/dist/agents/agents.service.js +677 -0
- package/dist/agents/definition.d.ts +198 -0
- package/dist/agents/definition.js +152 -0
- package/dist/agents/models.controller.d.ts +13 -0
- package/dist/agents/models.controller.js +31 -0
- package/dist/agents/templates.controller.d.ts +15 -0
- package/dist/agents/templates.controller.js +37 -0
- package/dist/agents/templates.d.ts +83 -0
- package/dist/agents/templates.js +173 -0
- package/dist/catalog/admin-catalog.controller.d.ts +143 -0
- package/dist/catalog/admin-catalog.controller.js +245 -0
- package/dist/catalog/catalog.service.d.ts +430 -0
- package/dist/catalog/catalog.service.js +882 -0
- package/dist/catalog/claude/agents.d.ts +27 -0
- package/dist/catalog/claude/agents.js +270 -0
- package/dist/catalog/claude/commands.d.ts +50 -0
- package/dist/catalog/claude/commands.js +179 -0
- package/dist/catalog/claude/manifest.d.ts +69 -0
- package/dist/catalog/claude/manifest.js +134 -0
- package/dist/catalog/claude/marketplace.d.ts +35 -0
- package/dist/catalog/claude/marketplace.js +194 -0
- package/dist/catalog/claude/mcp.d.ts +33 -0
- package/dist/catalog/claude/mcp.js +226 -0
- package/dist/catalog/claude/report.d.ts +86 -0
- package/dist/catalog/claude/report.js +121 -0
- package/dist/catalog/claude/translate.d.ts +56 -0
- package/dist/catalog/claude/translate.js +857 -0
- package/dist/catalog/cli.d.ts +46 -0
- package/dist/catalog/cli.js +211 -0
- package/dist/catalog/git-source.d.ts +127 -0
- package/dist/catalog/git-source.js +520 -0
- package/dist/catalog/marketplace-fetch.d.ts +99 -0
- package/dist/catalog/marketplace-fetch.js +203 -0
- package/dist/catalog/marketplaces.controller.d.ts +97 -0
- package/dist/catalog/marketplaces.controller.js +197 -0
- package/dist/catalog/marketplaces.service.d.ts +340 -0
- package/dist/catalog/marketplaces.service.js +1217 -0
- package/dist/catalog/org-catalog.controller.d.ts +128 -0
- package/dist/catalog/org-catalog.controller.js +277 -0
- package/dist/catalog/org-catalog.service.d.ts +246 -0
- package/dist/catalog/org-catalog.service.js +625 -0
- package/dist/catalog/plugin-import.d.ts +436 -0
- package/dist/catalog/plugin-import.js +1069 -0
- package/dist/catalog/plugin-install.d.ts +447 -0
- package/dist/catalog/plugin-install.js +1333 -0
- package/dist/catalog/plugin-package.d.ts +143 -0
- package/dist/catalog/plugin-package.js +382 -0
- package/dist/catalog/plugin-upgrade.d.ts +222 -0
- package/dist/catalog/plugin-upgrade.js +1219 -0
- package/dist/catalog/requests.service.d.ts +162 -0
- package/dist/catalog/requests.service.js +504 -0
- package/dist/catalog/schema-path.d.ts +7 -0
- package/dist/catalog/schema-path.js +63 -0
- package/dist/catalog/upgrade-targets.d.ts +59 -0
- package/dist/catalog/upgrade-targets.js +90 -0
- package/dist/catalog/versions.d.ts +31 -0
- package/dist/catalog/versions.js +44 -0
- package/dist/channels/actions.d.ts +37 -0
- package/dist/channels/actions.js +121 -0
- package/dist/channels/cards.d.ts +44 -0
- package/dist/channels/cards.js +156 -0
- package/dist/channels/connections.controller.d.ts +59 -0
- package/dist/channels/connections.controller.js +115 -0
- package/dist/channels/connections.service.d.ts +187 -0
- package/dist/channels/connections.service.js +534 -0
- package/dist/channels/features.d.ts +12 -0
- package/dist/channels/features.js +22 -0
- package/dist/channels/inbound.d.ts +120 -0
- package/dist/channels/inbound.js +332 -0
- package/dist/channels/outbound.d.ts +140 -0
- package/dist/channels/outbound.js +1125 -0
- package/dist/channels/phrases.d.ts +100 -0
- package/dist/channels/phrases.js +81 -0
- package/dist/channels/supervisor.d.ts +182 -0
- package/dist/channels/supervisor.js +625 -0
- package/dist/channels/threads.d.ts +29 -0
- package/dist/channels/threads.js +104 -0
- package/dist/channels/webhook-url.d.ts +16 -0
- package/dist/channels/webhook-url.js +34 -0
- package/dist/channels/webhook.controller.d.ts +19 -0
- package/dist/channels/webhook.controller.js +72 -0
- package/dist/channels/webhook.d.ts +73 -0
- package/dist/channels/webhook.js +274 -0
- package/dist/config/docs.d.ts +9 -0
- package/dist/config/docs.js +85 -0
- package/dist/config/env.d.ts +6 -0
- package/dist/config/env.js +73 -0
- package/dist/config/fields.d.ts +42 -0
- package/dist/config/fields.js +214 -0
- package/dist/config/file-source.d.ts +3 -0
- package/dist/config/file-source.js +31 -0
- package/dist/config/load.d.ts +20 -0
- package/dist/config/load.js +98 -0
- package/dist/config/nacos-source.d.ts +31 -0
- package/dist/config/nacos-source.js +229 -0
- package/dist/config/schema.d.ts +223 -0
- package/dist/config/schema.js +1151 -0
- package/dist/config/sources.d.ts +18 -0
- package/dist/config/sources.js +59 -0
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.js +7 -0
- package/dist/conversations/artifacts.d.ts +52 -0
- package/dist/conversations/artifacts.js +137 -0
- package/dist/conversations/attachments.controller.d.ts +28 -0
- package/dist/conversations/attachments.controller.js +96 -0
- package/dist/conversations/attachments.service.d.ts +180 -0
- package/dist/conversations/attachments.service.js +578 -0
- package/dist/conversations/conversations.controller.d.ts +288 -0
- package/dist/conversations/conversations.controller.js +607 -0
- package/dist/conversations/conversations.service.d.ts +321 -0
- package/dist/conversations/conversations.service.js +754 -0
- package/dist/conversations/event-log.d.ts +122 -0
- package/dist/conversations/event-log.js +259 -0
- package/dist/conversations/events.d.ts +856 -0
- package/dist/conversations/events.js +639 -0
- package/dist/conversations/files.d.ts +92 -0
- package/dist/conversations/files.js +213 -0
- package/dist/conversations/outputs-read.d.ts +34 -0
- package/dist/conversations/outputs-read.js +37 -0
- package/dist/conversations/queue.d.ts +169 -0
- package/dist/conversations/queue.js +527 -0
- package/dist/conversations/sandbox-status.d.ts +21 -0
- package/dist/conversations/sandbox-status.js +24 -0
- package/dist/conversations/selection.d.ts +46 -0
- package/dist/conversations/selection.js +75 -0
- package/dist/conversations/titles.d.ts +93 -0
- package/dist/conversations/titles.js +422 -0
- package/dist/ctx/bus.d.ts +16 -0
- package/dist/ctx/bus.js +101 -0
- package/dist/ctx/create-ctx.d.ts +109 -0
- package/dist/ctx/create-ctx.js +592 -0
- package/dist/ctx/kv.d.ts +4 -0
- package/dist/ctx/kv.js +31 -0
- package/dist/ctx/lock.d.ts +4 -0
- package/dist/ctx/lock.js +39 -0
- package/dist/ctx/namespace.d.ts +6 -0
- package/dist/ctx/namespace.js +12 -0
- package/dist/ctx/queue.d.ts +25 -0
- package/dist/ctx/queue.js +150 -0
- package/dist/ctx/secrets.d.ts +45 -0
- package/dist/ctx/secrets.js +175 -0
- package/dist/db/kernel-schema.d.ts +1122 -0
- package/dist/db/kernel-schema.js +5 -0
- package/dist/db/migrator.d.ts +28 -0
- package/dist/db/migrator.js +140 -0
- package/dist/db/pool.d.ts +21 -0
- package/dist/db/pool.js +28 -0
- package/dist/db/sql-statements.d.ts +34 -0
- package/dist/db/sql-statements.js +168 -0
- package/dist/errors.d.ts +210 -0
- package/dist/errors.js +212 -0
- package/dist/evals/budget.d.ts +61 -0
- package/dist/evals/budget.js +181 -0
- package/dist/evals/diff.d.ts +55 -0
- package/dist/evals/diff.js +133 -0
- package/dist/evals/judge.d.ts +53 -0
- package/dist/evals/judge.js +149 -0
- package/dist/evals/plan.d.ts +47 -0
- package/dist/evals/plan.js +77 -0
- package/dist/evals/reconstruct.d.ts +137 -0
- package/dist/evals/reconstruct.js +322 -0
- package/dist/evals/recorded-tools.d.ts +54 -0
- package/dist/evals/recorded-tools.js +167 -0
- package/dist/evals/run-replay.d.ts +86 -0
- package/dist/evals/run-replay.js +502 -0
- package/dist/evals/runs.controller.d.ts +100 -0
- package/dist/evals/runs.controller.js +199 -0
- package/dist/evals/runs.service.d.ts +275 -0
- package/dist/evals/runs.service.js +836 -0
- package/dist/evals/sets.controller.d.ts +61 -0
- package/dist/evals/sets.controller.js +183 -0
- package/dist/evals/sets.service.d.ts +128 -0
- package/dist/evals/sets.service.js +558 -0
- package/dist/evals/snapshot.d.ts +76 -0
- package/dist/evals/snapshot.js +207 -0
- package/dist/evals/step-replay.d.ts +113 -0
- package/dist/evals/step-replay.js +181 -0
- package/dist/evals/trajectory.controller.d.ts +41 -0
- package/dist/evals/trajectory.controller.js +88 -0
- package/dist/evals/trajectory.d.ts +144 -0
- package/dist/evals/trajectory.js +338 -0
- package/dist/evals/trajectory.service.d.ts +108 -0
- package/dist/evals/trajectory.service.js +323 -0
- package/dist/evals/worker.d.ts +101 -0
- package/dist/evals/worker.js +789 -0
- package/dist/extensions/admin-ops.d.ts +105 -0
- package/dist/extensions/admin-ops.js +353 -0
- package/dist/extensions/admin-view.d.ts +79 -0
- package/dist/extensions/admin-view.js +327 -0
- package/dist/extensions/approvals.d.ts +119 -0
- package/dist/extensions/approvals.js +205 -0
- package/dist/extensions/blobs.d.ts +45 -0
- package/dist/extensions/blobs.js +160 -0
- package/dist/extensions/bootstrap.d.ts +149 -0
- package/dist/extensions/bootstrap.js +441 -0
- package/dist/extensions/cli.d.ts +61 -0
- package/dist/extensions/cli.js +165 -0
- package/dist/extensions/config-json-schema.d.ts +9 -0
- package/dist/extensions/config-json-schema.js +73 -0
- package/dist/extensions/deployment-config.d.ts +138 -0
- package/dist/extensions/deployment-config.js +394 -0
- package/dist/extensions/enabled.d.ts +31 -0
- package/dist/extensions/enabled.js +55 -0
- package/dist/extensions/hooks.d.ts +161 -0
- package/dist/extensions/hooks.js +425 -0
- package/dist/extensions/host.d.ts +577 -0
- package/dist/extensions/host.js +2071 -0
- package/dist/extensions/inflight.d.ts +69 -0
- package/dist/extensions/inflight.js +277 -0
- package/dist/extensions/installs.d.ts +166 -0
- package/dist/extensions/installs.js +253 -0
- package/dist/extensions/instance.d.ts +167 -0
- package/dist/extensions/instance.js +329 -0
- package/dist/extensions/ledger.d.ts +30 -0
- package/dist/extensions/ledger.js +50 -0
- package/dist/extensions/lifecycle.d.ts +80 -0
- package/dist/extensions/lifecycle.js +152 -0
- package/dist/extensions/migrate-lock.d.ts +51 -0
- package/dist/extensions/migrate-lock.js +100 -0
- package/dist/extensions/org-config.controller.d.ts +24 -0
- package/dist/extensions/org-config.controller.js +59 -0
- package/dist/extensions/org-config.d.ts +169 -0
- package/dist/extensions/org-config.js +520 -0
- package/dist/extensions/package.d.ts +115 -0
- package/dist/extensions/package.js +246 -0
- package/dist/extensions/permission-set.d.ts +117 -0
- package/dist/extensions/permission-set.js +184 -0
- package/dist/extensions/permission-view.d.ts +194 -0
- package/dist/extensions/permission-view.js +284 -0
- package/dist/extensions/registry.d.ts +24 -0
- package/dist/extensions/registry.js +56 -0
- package/dist/extensions/resolve.d.ts +12 -0
- package/dist/extensions/resolve.js +71 -0
- package/dist/extensions/sources.d.ts +69 -0
- package/dist/extensions/sources.js +120 -0
- package/dist/extensions/sync.d.ts +40 -0
- package/dist/extensions/sync.js +242 -0
- package/dist/files/previewers.controller.d.ts +46 -0
- package/dist/files/previewers.controller.js +105 -0
- package/dist/files/previewers.d.ts +100 -0
- package/dist/files/previewers.js +250 -0
- package/dist/http/api.module.d.ts +114 -0
- package/dist/http/api.module.js +448 -0
- package/dist/http/app.d.ts +77 -0
- package/dist/http/app.js +170 -0
- package/dist/http/error.filter.d.ts +21 -0
- package/dist/http/error.filter.js +85 -0
- package/dist/http/gateway.module.d.ts +15 -0
- package/dist/http/gateway.module.js +13 -0
- package/dist/http/health.controller.d.ts +26 -0
- package/dist/http/health.controller.js +79 -0
- package/dist/http/nest-logger.d.ts +20 -0
- package/dist/http/nest-logger.js +28 -0
- package/dist/http/plugin-dispatch.d.ts +68 -0
- package/dist/http/plugin-dispatch.js +200 -0
- package/dist/http/tokens.d.ts +131 -0
- package/dist/http/tokens.js +131 -0
- package/dist/http/zod.pipe.d.ts +8 -0
- package/dist/http/zod.pipe.js +14 -0
- package/dist/identity/accounts.d.ts +93 -0
- package/dist/identity/accounts.js +367 -0
- package/dist/identity/api-tokens.d.ts +73 -0
- package/dist/identity/api-tokens.js +197 -0
- package/dist/identity/audit.d.ts +23 -0
- package/dist/identity/audit.js +41 -0
- package/dist/identity/auth.controller.d.ts +40 -0
- package/dist/identity/auth.controller.js +130 -0
- package/dist/identity/auth.guard.d.ts +41 -0
- package/dist/identity/auth.guard.js +162 -0
- package/dist/identity/binding.d.ts +47 -0
- package/dist/identity/binding.js +84 -0
- package/dist/identity/cookies.d.ts +16 -0
- package/dist/identity/cookies.js +32 -0
- package/dist/identity/credential-check.d.ts +17 -0
- package/dist/identity/credential-check.js +24 -0
- package/dist/identity/enterprise-admin.d.ts +14 -0
- package/dist/identity/enterprise-admin.js +27 -0
- package/dist/identity/external.d.ts +106 -0
- package/dist/identity/external.js +337 -0
- package/dist/identity/hash.d.ts +2 -0
- package/dist/identity/hash.js +3 -0
- package/dist/identity/login.d.ts +6 -0
- package/dist/identity/login.js +20 -0
- package/dist/identity/me.controller.d.ts +61 -0
- package/dist/identity/me.controller.js +159 -0
- package/dist/identity/passwords.d.ts +6 -0
- package/dist/identity/passwords.js +37 -0
- package/dist/identity/principal.d.ts +56 -0
- package/dist/identity/principal.js +38 -0
- package/dist/identity/rate-limit.d.ts +55 -0
- package/dist/identity/rate-limit.js +121 -0
- package/dist/identity/sessions.d.ts +84 -0
- package/dist/identity/sessions.js +306 -0
- package/dist/identity/settings.d.ts +121 -0
- package/dist/identity/settings.js +248 -0
- package/dist/identity/sso.controller.d.ts +59 -0
- package/dist/identity/sso.controller.js +192 -0
- package/dist/identity/sso.d.ts +83 -0
- package/dist/identity/sso.js +202 -0
- package/dist/inbox/inbox.service.d.ts +43 -0
- package/dist/inbox/inbox.service.js +57 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +65 -0
- package/dist/kernel.d.ts +256 -0
- package/dist/kernel.js +1326 -0
- package/dist/log.d.ts +48 -0
- package/dist/log.js +229 -0
- package/dist/models/prices.d.ts +53 -0
- package/dist/models/prices.js +121 -0
- package/dist/models/purposes.controller.d.ts +19 -0
- package/dist/models/purposes.controller.js +56 -0
- package/dist/models/purposes.d.ts +140 -0
- package/dist/models/purposes.js +267 -0
- package/dist/net/proxy.d.ts +44 -0
- package/dist/net/proxy.js +162 -0
- package/dist/net/public-address.d.ts +60 -0
- package/dist/net/public-address.js +271 -0
- package/dist/net/safe-fetch.d.ts +38 -0
- package/dist/net/safe-fetch.js +140 -0
- package/dist/orgs/directory-sync.controller.d.ts +14 -0
- package/dist/orgs/directory-sync.controller.js +40 -0
- package/dist/orgs/directory-sync.d.ts +166 -0
- package/dist/orgs/directory-sync.js +558 -0
- package/dist/orgs/invitations.controller.d.ts +58 -0
- package/dist/orgs/invitations.controller.js +137 -0
- package/dist/orgs/invitations.service.d.ts +80 -0
- package/dist/orgs/invitations.service.js +217 -0
- package/dist/orgs/members.service.d.ts +63 -0
- package/dist/orgs/members.service.js +234 -0
- package/dist/orgs/offboarding.d.ts +141 -0
- package/dist/orgs/offboarding.js +398 -0
- package/dist/orgs/org-lock.d.ts +32 -0
- package/dist/orgs/org-lock.js +46 -0
- package/dist/orgs/org.guard.d.ts +35 -0
- package/dist/orgs/org.guard.js +95 -0
- package/dist/orgs/orgs.controller.d.ts +78 -0
- package/dist/orgs/orgs.controller.js +192 -0
- package/dist/orgs/orgs.service.d.ts +61 -0
- package/dist/orgs/orgs.service.js +209 -0
- package/dist/orgs/roles.d.ts +13 -0
- package/dist/orgs/roles.js +58 -0
- package/dist/orgs/settings.controller.d.ts +20 -0
- package/dist/orgs/settings.controller.js +53 -0
- package/dist/orgs/settings.service.d.ts +120 -0
- package/dist/orgs/settings.service.js +178 -0
- package/dist/packages/npm.d.ts +50 -0
- package/dist/packages/npm.js +218 -0
- package/dist/packages/provenance.d.ts +47 -0
- package/dist/packages/provenance.js +147 -0
- package/dist/packages/tgz.d.ts +76 -0
- package/dist/packages/tgz.js +225 -0
- package/dist/runtime/abort.d.ts +32 -0
- package/dist/runtime/abort.js +71 -0
- package/dist/runtime/checkpointer.d.ts +32 -0
- package/dist/runtime/checkpointer.js +243 -0
- package/dist/runtime/context/build.d.ts +254 -0
- package/dist/runtime/context/build.js +693 -0
- package/dist/runtime/context/claude-tools.d.ts +11 -0
- package/dist/runtime/context/claude-tools.js +24 -0
- package/dist/runtime/context/compaction.d.ts +26 -0
- package/dist/runtime/context/compaction.js +195 -0
- package/dist/runtime/context/pairing.d.ts +11 -0
- package/dist/runtime/context/pairing.js +82 -0
- package/dist/runtime/context/reminders.d.ts +5 -0
- package/dist/runtime/context/reminders.js +11 -0
- package/dist/runtime/context/skills.d.ts +105 -0
- package/dist/runtime/context/skills.js +283 -0
- package/dist/runtime/context/tokens.d.ts +19 -0
- package/dist/runtime/context/tokens.js +104 -0
- package/dist/runtime/conversation-lock.d.ts +40 -0
- package/dist/runtime/conversation-lock.js +97 -0
- package/dist/runtime/hook-calls.d.ts +84 -0
- package/dist/runtime/hook-calls.js +136 -0
- package/dist/runtime/langgraph-loop.d.ts +52 -0
- package/dist/runtime/langgraph-loop.js +247 -0
- package/dist/runtime/loop.d.ts +93 -0
- package/dist/runtime/loop.js +7 -0
- package/dist/runtime/maintenance.d.ts +80 -0
- package/dist/runtime/maintenance.js +147 -0
- package/dist/runtime/media-cache.d.ts +21 -0
- package/dist/runtime/media-cache.js +51 -0
- package/dist/runtime/model-call.d.ts +176 -0
- package/dist/runtime/model-call.js +1049 -0
- package/dist/runtime/providers.d.ts +86 -0
- package/dist/runtime/providers.js +224 -0
- package/dist/runtime/quota.d.ts +29 -0
- package/dist/runtime/quota.js +70 -0
- package/dist/runtime/run-finish.d.ts +45 -0
- package/dist/runtime/run-finish.js +69 -0
- package/dist/runtime/run-queue.d.ts +121 -0
- package/dist/runtime/run-queue.js +493 -0
- package/dist/runtime/run-worker.d.ts +138 -0
- package/dist/runtime/run-worker.js +805 -0
- package/dist/runtime/stream.controller.d.ts +34 -0
- package/dist/runtime/stream.controller.js +122 -0
- package/dist/runtime/stream.d.ts +63 -0
- package/dist/runtime/stream.js +262 -0
- package/dist/runtime/subagents/child.d.ts +80 -0
- package/dist/runtime/subagents/child.js +225 -0
- package/dist/runtime/subagents/definition.d.ts +103 -0
- package/dist/runtime/subagents/definition.js +392 -0
- package/dist/runtime/subagents/dispatch.d.ts +69 -0
- package/dist/runtime/subagents/dispatch.js +502 -0
- package/dist/runtime/subagents/limits.d.ts +23 -0
- package/dist/runtime/subagents/limits.js +34 -0
- package/dist/runtime/subagents/org-agents.d.ts +50 -0
- package/dist/runtime/subagents/org-agents.js +70 -0
- package/dist/runtime/subagents/preamble.d.ts +7 -0
- package/dist/runtime/subagents/preamble.js +11 -0
- package/dist/runtime/subagents/progress.d.ts +26 -0
- package/dist/runtime/subagents/progress.js +36 -0
- package/dist/runtime/subagents/roster.d.ts +55 -0
- package/dist/runtime/subagents/roster.js +111 -0
- package/dist/runtime/subagents/skill-fork.d.ts +123 -0
- package/dist/runtime/subagents/skill-fork.js +259 -0
- package/dist/runtime/usage-report.d.ts +163 -0
- package/dist/runtime/usage-report.js +550 -0
- package/dist/runtime/waits.d.ts +233 -0
- package/dist/runtime/waits.js +883 -0
- package/dist/sandbox/leases.d.ts +169 -0
- package/dist/sandbox/leases.js +515 -0
- package/dist/sandbox/lifecycle.d.ts +21 -0
- package/dist/sandbox/lifecycle.js +68 -0
- package/dist/sandbox/sandbox.service.d.ts +249 -0
- package/dist/sandbox/sandbox.service.js +908 -0
- package/dist/skills/catalog.d.ts +149 -0
- package/dist/skills/catalog.js +405 -0
- package/dist/skills/changes.controller.d.ts +56 -0
- package/dist/skills/changes.controller.js +136 -0
- package/dist/skills/changes.service.d.ts +175 -0
- package/dist/skills/changes.service.js +805 -0
- package/dist/skills/invocable.controller.d.ts +23 -0
- package/dist/skills/invocable.controller.js +50 -0
- package/dist/skills/invocation-keys.d.ts +23 -0
- package/dist/skills/invocation-keys.js +44 -0
- package/dist/skills/invoke.d.ts +135 -0
- package/dist/skills/invoke.js +286 -0
- package/dist/skills/package.d.ts +87 -0
- package/dist/skills/package.js +227 -0
- package/dist/skills/skills.controller.d.ts +40 -0
- package/dist/skills/skills.controller.js +110 -0
- package/dist/skills/skills.service.d.ts +192 -0
- package/dist/skills/skills.service.js +567 -0
- package/dist/storage/prefix.d.ts +36 -0
- package/dist/storage/prefix.js +104 -0
- package/dist/storage/purge.d.ts +27 -0
- package/dist/storage/purge.js +75 -0
- package/dist/storage/storage.service.d.ts +44 -0
- package/dist/storage/storage.service.js +145 -0
- package/dist/text.d.ts +13 -0
- package/dist/text.js +13 -0
- package/dist/tools/approvals.controller.d.ts +53 -0
- package/dist/tools/approvals.controller.js +128 -0
- package/dist/tools/approvals.service.d.ts +172 -0
- package/dist/tools/approvals.service.js +568 -0
- package/dist/tools/breaker.d.ts +28 -0
- package/dist/tools/breaker.js +72 -0
- package/dist/tools/builtins/agent.d.ts +81 -0
- package/dist/tools/builtins/agent.js +223 -0
- package/dist/tools/builtins/ask-user.d.ts +28 -0
- package/dist/tools/builtins/ask-user.js +139 -0
- package/dist/tools/builtins/read-tool-output.d.ts +18 -0
- package/dist/tools/builtins/read-tool-output.js +108 -0
- package/dist/tools/builtins/sandbox/bash.d.ts +16 -0
- package/dist/tools/builtins/sandbox/bash.js +137 -0
- package/dist/tools/builtins/sandbox/common.d.ts +114 -0
- package/dist/tools/builtins/sandbox/common.js +304 -0
- package/dist/tools/builtins/sandbox/deliver.d.ts +5 -0
- package/dist/tools/builtins/sandbox/deliver.js +180 -0
- package/dist/tools/builtins/sandbox/edit.d.ts +4 -0
- package/dist/tools/builtins/sandbox/edit.js +98 -0
- package/dist/tools/builtins/sandbox/glob-match.d.ts +25 -0
- package/dist/tools/builtins/sandbox/glob-match.js +240 -0
- package/dist/tools/builtins/sandbox/glob.d.ts +4 -0
- package/dist/tools/builtins/sandbox/glob.js +104 -0
- package/dist/tools/builtins/sandbox/grep.d.ts +9 -0
- package/dist/tools/builtins/sandbox/grep.js +133 -0
- package/dist/tools/builtins/sandbox/index.d.ts +12 -0
- package/dist/tools/builtins/sandbox/index.js +27 -0
- package/dist/tools/builtins/sandbox/names.d.ts +7 -0
- package/dist/tools/builtins/sandbox/names.js +20 -0
- package/dist/tools/builtins/sandbox/read.d.ts +8 -0
- package/dist/tools/builtins/sandbox/read.js +121 -0
- package/dist/tools/builtins/sandbox/write.d.ts +4 -0
- package/dist/tools/builtins/sandbox/write.js +88 -0
- package/dist/tools/builtins/skill-load.d.ts +53 -0
- package/dist/tools/builtins/skill-load.js +446 -0
- package/dist/tools/builtins/skill-write.d.ts +59 -0
- package/dist/tools/builtins/skill-write.js +244 -0
- package/dist/tools/builtins/tool-search.d.ts +31 -0
- package/dist/tools/builtins/tool-search.js +171 -0
- package/dist/tools/connector-content.controller.d.ts +34 -0
- package/dist/tools/connector-content.controller.js +78 -0
- package/dist/tools/connector-content.d.ts +104 -0
- package/dist/tools/connector-content.js +267 -0
- package/dist/tools/connectors.controller.d.ts +210 -0
- package/dist/tools/connectors.controller.js +234 -0
- package/dist/tools/connectors.service.d.ts +303 -0
- package/dist/tools/connectors.service.js +1102 -0
- package/dist/tools/credentials.d.ts +139 -0
- package/dist/tools/credentials.js +351 -0
- package/dist/tools/drift.d.ts +141 -0
- package/dist/tools/drift.js +539 -0
- package/dist/tools/elicitation.d.ts +81 -0
- package/dist/tools/elicitation.js +266 -0
- package/dist/tools/endpoint.d.ts +18 -0
- package/dist/tools/endpoint.js +115 -0
- package/dist/tools/info.d.ts +67 -0
- package/dist/tools/info.js +113 -0
- package/dist/tools/naming.d.ts +27 -0
- package/dist/tools/naming.js +47 -0
- package/dist/tools/oauth/discovery.d.ts +65 -0
- package/dist/tools/oauth/discovery.js +307 -0
- package/dist/tools/oauth/flow.d.ts +63 -0
- package/dist/tools/oauth/flow.js +237 -0
- package/dist/tools/oauth/oauth.controller.d.ts +31 -0
- package/dist/tools/oauth/oauth.controller.js +95 -0
- package/dist/tools/oauth/tokens.d.ts +111 -0
- package/dist/tools/oauth/tokens.js +258 -0
- package/dist/tools/outputs.d.ts +61 -0
- package/dist/tools/outputs.js +156 -0
- package/dist/tools/pipeline.d.ts +322 -0
- package/dist/tools/pipeline.js +1236 -0
- package/dist/tools/policy.d.ts +60 -0
- package/dist/tools/policy.js +236 -0
- package/dist/tools/questions.controller.d.ts +24 -0
- package/dist/tools/questions.controller.js +58 -0
- package/dist/tools/questions.service.d.ts +103 -0
- package/dist/tools/questions.service.js +295 -0
- package/dist/tools/redact.d.ts +20 -0
- package/dist/tools/redact.js +60 -0
- package/dist/tools/registry.d.ts +246 -0
- package/dist/tools/registry.js +481 -0
- package/dist/tools/resources.d.ts +16 -0
- package/dist/tools/resources.js +76 -0
- package/dist/tools/specs.d.ts +14 -0
- package/dist/tools/specs.js +31 -0
- package/dist/tools/validate.d.ts +20 -0
- package/dist/tools/validate.js +183 -0
- package/dist/tools/view.d.ts +43 -0
- package/dist/tools/view.js +225 -0
- package/dist/ui/embed-token.d.ts +31 -0
- package/dist/ui/embed-token.js +132 -0
- package/dist/ui/registry.d.ts +58 -0
- package/dist/ui/registry.js +90 -0
- package/dist/ui/ui-stream.d.ts +37 -0
- package/dist/ui/ui-stream.js +162 -0
- package/dist/ui/views.controller.d.ts +81 -0
- package/dist/ui/views.controller.js +261 -0
- package/dist/ui/views.d.ts +155 -0
- package/dist/ui/views.js +306 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +6 -0
- package/package.json +67 -3
- package/src/migrations/0001_init.sql +8 -0
- package/src/migrations/0002_secrets.sql +8 -0
- package/src/migrations/0003_identity.sql +117 -0
- package/src/migrations/0004_plugin_org_config_nullable.sql +3 -0
- package/src/migrations/0005_agents.sql +27 -0
- package/src/migrations/0006_conversations.sql +37 -0
- package/src/migrations/0007_checkpoints.sql +26 -0
- package/src/migrations/0008_runs_trigger_seq.sql +3 -0
- package/src/migrations/0009_runs_active_unique.sql +4 -0
- package/src/migrations/0010_tools.sql +125 -0
- package/src/migrations/0011_approvals_spec.sql +10 -0
- package/src/migrations/0012_run_waits.sql +24 -0
- package/src/migrations/0013_app_tools_source.sql +4 -0
- package/src/migrations/0014_tool_specs.sql +8 -0
- package/src/migrations/0015_tool_drift.sql +20 -0
- package/src/migrations/0016_plugin_permissions.sql +11 -0
- package/src/migrations/0017_identity_sources.sql +6 -0
- package/src/migrations/0018_channels.sql +74 -0
- package/src/migrations/0019_identity_trust.sql +17 -0
- package/src/migrations/0020_channel_card_lookup.sql +10 -0
- package/src/migrations/0021_org_storage_purge.sql +9 -0
- package/src/migrations/0022_attachments.sql +29 -0
- package/src/migrations/0023_sandboxes.sql +37 -0
- package/src/migrations/0024_skills.sql +46 -0
- package/src/migrations/0025_skills_catalog_idx.sql +6 -0
- package/src/migrations/0026_skill_changes.sql +34 -0
- package/src/migrations/0027_model_prices.sql +18 -0
- package/src/migrations/0028_evals.sql +105 -0
- package/src/migrations/0029_model_purposes.sql +18 -0
- package/src/migrations/0030_plugin_installs.sql +73 -0
- package/src/migrations/0031_plugin_snapshot_source.sql +6 -0
- package/src/migrations/0032_plugin_deployment_configs.sql +10 -0
- package/src/migrations/0033_rename_apps_to_connectors.sql +25 -0
- package/src/migrations/0034_directory.sql +49 -0
- package/src/migrations/0035_catalogs.sql +40 -0
- package/src/migrations/0036_app_import_staging.sql +17 -0
- package/src/migrations/0037_directory_installs.sql +92 -0
- package/src/migrations/0038_channel_inbound_mode.sql +6 -0
- package/src/migrations/0039_channel_card_pending.sql +8 -0
- package/src/migrations/0040_usage_report_sweep.sql +10 -0
- package/src/migrations/0041_rename_plugins_to_extensions.sql +38 -0
- package/src/migrations/0042_rename_directory_to_catalog.sql +80 -0
- package/src/migrations/0043_rename_app_installs_to_plugin_installs.sql +20 -0
- package/src/migrations/0044_catalog_one_kind.sql +90 -0
- package/src/migrations/0045_plugin_dependencies.sql +9 -0
- package/src/migrations/0046_git_sources.sql +25 -0
- package/src/migrations/0047_marketplace_entry_heads.sql +7 -0
- package/src/migrations/0048_marketplace_entry_trees.sql +8 -0
- package/src/migrations/0049_plugin_upgrades.sql +37 -0
- package/src/migrations/0050_bootstrap.sql +53 -0
- package/src/migrations/0051_claude_compat.sql +26 -0
- package/src/migrations/0052_conversations_account_agent_idx.sql +5 -0
- package/src/migrations/0053_subagents.sql +73 -0
- package/src/migrations/0054_subagent_wait_expiry.sql +7 -0
- package/src/migrations/0055_subagent_parent_indexes.sql +10 -0
- package/src/migrations/0056_approval_reason.sql +3 -0
- package/src/migrations/0057_conversation_titles.sql +11 -0
- package/src/migrations/0058_conversation_selection.sql +4 -0
- package/src/migrations/0059_conversation_queue.sql +22 -0
- package/src/migrations/0060_file_previewers.sql +10 -0
- package/src/migrations/0061_queue_selection.sql +5 -0
- package/src/migrations/0062_queued_skill.sql +8 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
/**
|
|
3
|
+
* Which extensions an organization has enabled. Two sources, and every reader goes through these
|
|
4
|
+
* helpers so none of them misses one:
|
|
5
|
+
* - `extension_org_configs.enabled` (the org's own switch);
|
|
6
|
+
* - every required bootstrap plugin (`extension_installs.required`, package ecosystem spec §7):
|
|
7
|
+
* enabled in every org, existing and future, without an org config row, whatever such a row says.
|
|
8
|
+
* Neither checks that the extension is loaded on this replica or enabled on the deployment; callers
|
|
9
|
+
* that need that check it themselves, as before. `db` may be a transaction.
|
|
10
|
+
*/
|
|
11
|
+
/** The names of the extensions enabled in `orgId` (a subquery with one column, `plugin`). */
|
|
12
|
+
export function enabledExtensionsQuery(db, orgId) {
|
|
13
|
+
return db
|
|
14
|
+
.selectFrom("extension_org_configs as eoc")
|
|
15
|
+
.select("eoc.plugin as plugin")
|
|
16
|
+
.where("eoc.org_id", "=", orgId)
|
|
17
|
+
.where("eoc.enabled", "=", true)
|
|
18
|
+
.union(db
|
|
19
|
+
.selectFrom("extension_installs as ri")
|
|
20
|
+
.select("ri.name as plugin")
|
|
21
|
+
.where("ri.required", "=", true));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every (org, extension) pair that is enabled, for queries that join it (columns `org_id`,
|
|
25
|
+
* `plugin`): `.innerJoin(enabledExtensionsByOrg(db).as("en"), (j) => j.onRef("en.org_id", …)…)`.
|
|
26
|
+
* Soft-deleted orgs are included, as their org config rows are; callers filter `orgs.deleted_at`.
|
|
27
|
+
*/
|
|
28
|
+
export function enabledExtensionsByOrg(db) {
|
|
29
|
+
return db
|
|
30
|
+
.selectFrom("extension_org_configs as eoc")
|
|
31
|
+
.select(["eoc.org_id as org_id", "eoc.plugin as plugin"])
|
|
32
|
+
.where("eoc.enabled", "=", true)
|
|
33
|
+
.union(db
|
|
34
|
+
.selectFrom("orgs as ro")
|
|
35
|
+
.innerJoin("extension_installs as ri", (j) => j.on("ri.required", "=", true))
|
|
36
|
+
.select(["ro.id as org_id", "ri.name as plugin"]));
|
|
37
|
+
}
|
|
38
|
+
/** Whether `plugin` is enabled in `orgId` (see the file header). */
|
|
39
|
+
export async function isExtensionEnabled(db, orgId, plugin) {
|
|
40
|
+
const row = await db
|
|
41
|
+
.selectFrom(enabledExtensionsQuery(db, orgId).as("en"))
|
|
42
|
+
.select(sql `1`.as("one"))
|
|
43
|
+
.where("en.plugin", "=", plugin)
|
|
44
|
+
.executeTakeFirst();
|
|
45
|
+
return row !== undefined;
|
|
46
|
+
}
|
|
47
|
+
/** The names of the required bootstrap plugins (installed with `required`). */
|
|
48
|
+
export async function requiredExtensions(db) {
|
|
49
|
+
const rows = await db
|
|
50
|
+
.selectFrom("extension_installs")
|
|
51
|
+
.select("name")
|
|
52
|
+
.where("required", "=", true)
|
|
53
|
+
.execute();
|
|
54
|
+
return new Set(rows.map((r) => r.name));
|
|
55
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type AvailableHook, type HookHandler, type HookOptions, type HookPayloads, type Logger, type PolicyHook } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import type { Redis } from "ioredis";
|
|
3
|
+
import type { KernelConfig } from "../config/schema.js";
|
|
4
|
+
import { type InflightTracker } from "./inflight.js";
|
|
5
|
+
/** Default of `hooks.timeoutMs` (used when a host is built without kernel config, e.g. in tests). */
|
|
6
|
+
export declare const DEFAULT_HOOK_TIMEOUT_MS = 5000;
|
|
7
|
+
/**
|
|
8
|
+
* Longest plugin-authored deny reason kept, in UTF-16 code units (the `hook_denied` event's limit;
|
|
9
|
+
* the model-visible denial quotes it).
|
|
10
|
+
*/
|
|
11
|
+
export declare const HOOK_REASON_MAX_CHARS = 500;
|
|
12
|
+
/** In-flight observe calls allowed per plugin on one replica; overflow is dropped with a warning. */
|
|
13
|
+
export declare const OBSERVE_INFLIGHT_MAX = 64;
|
|
14
|
+
/** Reason used when a plugin denies with an empty (or whitespace-only) reason. */
|
|
15
|
+
export declare const EMPTY_DENY_REASON = "(no reason given)";
|
|
16
|
+
/**
|
|
17
|
+
* Result of running the policy hooks of a mount point. `reason` is model-visible: the plugin's own
|
|
18
|
+
* (sanitized) text for `deny`, a generic sentence for `hook_error` / `timeout`. `detail` is the raw
|
|
19
|
+
* error for logs and audit only, never for the model.
|
|
20
|
+
*/
|
|
21
|
+
export type HookOutcome = {
|
|
22
|
+
allowed: true;
|
|
23
|
+
} | {
|
|
24
|
+
allowed: false;
|
|
25
|
+
plugin: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
cause: "deny" | "hook_error" | "timeout";
|
|
28
|
+
detail?: string;
|
|
29
|
+
/**
|
|
30
|
+
* R10: the hooks could not run at all (organization lookup or payload copy failed); `plugin` is
|
|
31
|
+
* only the first policy plugin on the mount, not the cause.
|
|
32
|
+
*/
|
|
33
|
+
preflight?: true;
|
|
34
|
+
/**
|
|
35
|
+
* M-1: `plugin` declares a policy hook on this mount (`permissions.policyHooks`), is enabled and
|
|
36
|
+
* approved, but is not loaded on this replica: the call fails closed without running any hook.
|
|
37
|
+
*/
|
|
38
|
+
notLoaded?: true;
|
|
39
|
+
};
|
|
40
|
+
/** A hook a plugin instance registered through `ctx.hooks.on` (kept on the instance, like tools). */
|
|
41
|
+
export interface RegisteredHook {
|
|
42
|
+
mount: AvailableHook;
|
|
43
|
+
handler: HookHandler<AvailableHook>;
|
|
44
|
+
mode: HookOptions["mode"];
|
|
45
|
+
}
|
|
46
|
+
/** What the registry needs from a plugin instance (satisfied by `ExtensionInstance`). */
|
|
47
|
+
export interface HookSource {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly hooks: readonly RegisteredHook[];
|
|
50
|
+
readonly inflight: InflightTracker;
|
|
51
|
+
}
|
|
52
|
+
/** Error text that never throws itself (a hostile `message` getter or `toString`). */
|
|
53
|
+
export declare function errMsg(e: unknown): string;
|
|
54
|
+
/**
|
|
55
|
+
* Sanitizes a plugin-authored deny reason for the model: control characters become spaces,
|
|
56
|
+
* whitespace collapses, and the text is clipped to {@link HOOK_REASON_MAX_CHARS} UTF-16 code units
|
|
57
|
+
* (never splitting a surrogate pair). An empty result becomes {@link EMPTY_DENY_REASON}.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sanitizeDenyReason(raw: string): string;
|
|
60
|
+
/** Clips to at most `max` UTF-16 code units, ending in "…", without splitting a surrogate pair. */
|
|
61
|
+
export declare function clipText(s: string, max: number): string;
|
|
62
|
+
/** Deep-freezes a structured clone in place (typed arrays cannot be frozen and are left as they are). */
|
|
63
|
+
export declare function deepFreeze<T>(v: T): T;
|
|
64
|
+
/** Validates a registration; throws a plain Error (permission checks happen in the ctx). */
|
|
65
|
+
export declare function checkHookRegistration(plugin: string, mount: string, handler: unknown, opts: HookOptions | undefined): void;
|
|
66
|
+
/** Longest raw error kept in `HookOutcome.detail` (characters). */
|
|
67
|
+
export declare const HOOK_DETAIL_MAX_CHARS = 2048;
|
|
68
|
+
/**
|
|
69
|
+
* Runs the hooks of this process's **active** plugin instances (one registry per replica, like
|
|
70
|
+
* tools). Hooks live on each instance (`ctx.hooks.on` records them in the instance ledger), and the
|
|
71
|
+
* registry resolves them from the host's active instances on every call: after an upgrade only the
|
|
72
|
+
* new generation fires, and unloading or disabling an instance stops its hooks. Every hook call runs
|
|
73
|
+
* through the instance's `inflight.run`, so revocation drains it (up to `extensionHost.drainSeconds`).
|
|
74
|
+
*
|
|
75
|
+
* {@link run} (Ruling R11):
|
|
76
|
+
* 1. If the caller's signal is aborted, throw its reason (an abort is never a deny).
|
|
77
|
+
* 2. Resolve the organization's enabled plugins (every run, not cached) and make one deep-frozen
|
|
78
|
+
* `structuredClone` of the payload. If either throws (Ruling R10): with any policy hook on the
|
|
79
|
+
* mount, deny with a generic reason (`hook_error`); with only observe hooks, allow and warn.
|
|
80
|
+
* If a plugin the organization enabled is expected to run policy hooks on the mount (deployment
|
|
81
|
+
* state, `expectedPolicy`) but is not loaded on this replica, deny with a generic reason
|
|
82
|
+
* (`hook_error`, `notLoaded`) without running any policy hook (M-1: fail closed). Observe hooks
|
|
83
|
+
* of plugins that are not loaded are simply missing.
|
|
84
|
+
* 3. Policy hooks run serially by plugin name (registration order within a plugin), each limited
|
|
85
|
+
* by `hooks.timeoutMs` (read on every run, hot). `{ deny }`, an error or a timeout stops the chain
|
|
86
|
+
* and denies (`deny` / `hook_error` / `timeout`). A policy hook cut off by its instance being
|
|
87
|
+
* revoked (`ExtensionUnloadedError`) reruns once on the plugin's current generation, if one is active
|
|
88
|
+
* and enabled; otherwise the call is denied.
|
|
89
|
+
* 4. Observe hooks (resolved again from the active instances) are then dispatched concurrently, fire-and-forget, whether or not the call was
|
|
90
|
+
* denied (never awaited on the call path). At most {@link OBSERVE_INFLIGHT_MAX} run per plugin on
|
|
91
|
+
* this replica; overflow is dropped. Their verdicts are ignored; errors, timeouts and drops are
|
|
92
|
+
* logged at most once per plugin and mount per minute (Redis counter with a TTL, shared by
|
|
93
|
+
* replicas).
|
|
94
|
+
*
|
|
95
|
+
* The in-flight observe counter is per replica on purpose (it bounds this process's work); it is not
|
|
96
|
+
* cross-request state anybody reads back. Callers fire hooks only for chat conversations, never in
|
|
97
|
+
* eval replays, and skip building the payload when {@link has} is false.
|
|
98
|
+
*/
|
|
99
|
+
export declare class HookRegistry {
|
|
100
|
+
private readonly d;
|
|
101
|
+
/** In-flight observe calls per plugin on this replica (see {@link OBSERVE_INFLIGHT_MAX}). */
|
|
102
|
+
private readonly observing;
|
|
103
|
+
constructor(d: {
|
|
104
|
+
/** The host's active instances (read on every call). */
|
|
105
|
+
instances(): readonly HookSource[];
|
|
106
|
+
/** The active instance of a plugin (the current generation), if any. */
|
|
107
|
+
current(plugin: string): HookSource | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Plugins that deployment state expects to run policy hooks on a mount point (M-1): enabled and
|
|
110
|
+
* approved, with `permissions.policyHooks` listing it in the approved snapshot. In-process (refreshed by the host on every reconcile), so asking costs no
|
|
111
|
+
* I/O. Absent: none.
|
|
112
|
+
*/
|
|
113
|
+
expectedPolicy?(mount: PolicyHook): readonly string[];
|
|
114
|
+
enabledPlugins(orgId: string): Promise<Set<string>>;
|
|
115
|
+
redis: Redis;
|
|
116
|
+
config: () => {
|
|
117
|
+
hooks: Pick<KernelConfig["hooks"], "timeoutMs">;
|
|
118
|
+
};
|
|
119
|
+
log: Logger;
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* Whether calls on the mount point need {@link run}: an active instance has a hook on it, or a plugin
|
|
123
|
+
* expected to run policy hooks on it is not loaded here (M-1). In-process only (regardless of
|
|
124
|
+
* organization), so callers skip building payloads and log lookups when it is false.
|
|
125
|
+
*/
|
|
126
|
+
has(mount: AvailableHook): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Plugins expected to run policy hooks on the mount (deployment state) that have no active instance
|
|
129
|
+
* on this replica, sorted by name. A loaded plugin that registered no policy hook is not listed.
|
|
130
|
+
*/
|
|
131
|
+
private unloadedPolicy;
|
|
132
|
+
/**
|
|
133
|
+
* `usage.report` handlers by plugin name (delivered by the usage queue, not by {@link run}); each
|
|
134
|
+
* call runs through the instance's `inflight.run`.
|
|
135
|
+
*/
|
|
136
|
+
handlers(mount: "usage.report"): {
|
|
137
|
+
plugin: string;
|
|
138
|
+
handler: HookHandler<"usage.report">;
|
|
139
|
+
}[];
|
|
140
|
+
/** Runs the hooks of a mount point (see the class comment). */
|
|
141
|
+
run<K extends Exclude<AvailableHook, "usage.report">>(mount: K, payload: HookPayloads[K], signal: AbortSignal): Promise<HookOutcome>;
|
|
142
|
+
/**
|
|
143
|
+
* The M-1 denial: `missing` (sorted) are enabled policy plugins that are not loaded on this replica.
|
|
144
|
+
* The model-visible reason is generic; the detail and a throttled warning name the plugin for the
|
|
145
|
+
* admin.
|
|
146
|
+
*/
|
|
147
|
+
private notLoadedDenial;
|
|
148
|
+
/** Active entries of a mount point, sorted by plugin name (stable: registration order within). */
|
|
149
|
+
private entries;
|
|
150
|
+
/** One plugin's policy hooks in registration order; reruns once on a newer generation if revoked. */
|
|
151
|
+
private runPolicies;
|
|
152
|
+
/** Fire-and-forget observe call through the instance's in-flight tracker, capped per plugin. */
|
|
153
|
+
private dispatchObserve;
|
|
154
|
+
/**
|
|
155
|
+
* Calls one handler with its own signal (aborted on timeout or when `s` aborts). Rejects with
|
|
156
|
+
* `s.reason` when `s` aborts first, so callers can tell an abort from a hook failure.
|
|
157
|
+
*/
|
|
158
|
+
private call;
|
|
159
|
+
/** Logs a warning at most once per plugin, mount and kind per minute (counter in Redis, TTL). */
|
|
160
|
+
private warnOnce;
|
|
161
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { HOOKS_AVAILABLE, POLICY_HOOKS, } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { ExtensionUnloadedError } from "./inflight.js";
|
|
3
|
+
/** Default of `hooks.timeoutMs` (used when a host is built without kernel config, e.g. in tests). */
|
|
4
|
+
export const DEFAULT_HOOK_TIMEOUT_MS = 5000;
|
|
5
|
+
/**
|
|
6
|
+
* Longest plugin-authored deny reason kept, in UTF-16 code units (the `hook_denied` event's limit;
|
|
7
|
+
* the model-visible denial quotes it).
|
|
8
|
+
*/
|
|
9
|
+
export const HOOK_REASON_MAX_CHARS = 500;
|
|
10
|
+
/** In-flight observe calls allowed per plugin on one replica; overflow is dropped with a warning. */
|
|
11
|
+
export const OBSERVE_INFLIGHT_MAX = 64;
|
|
12
|
+
/** Reason used when a plugin denies with an empty (or whitespace-only) reason. */
|
|
13
|
+
export const EMPTY_DENY_REASON = "(no reason given)";
|
|
14
|
+
/** Warnings about a plugin's hooks are logged at most once per plugin, mount and kind in this window. */
|
|
15
|
+
const WARN_WINDOW_MS = 60_000;
|
|
16
|
+
const AVAILABLE = new Set(HOOKS_AVAILABLE);
|
|
17
|
+
const POLICY = new Set(POLICY_HOOKS);
|
|
18
|
+
/** Error text that never throws itself (a hostile `message` getter or `toString`). */
|
|
19
|
+
export function errMsg(e) {
|
|
20
|
+
try {
|
|
21
|
+
return e instanceof Error ? String(e.message) : String(e);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return "unprintable error";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control characters is the point
|
|
28
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
29
|
+
/**
|
|
30
|
+
* Sanitizes a plugin-authored deny reason for the model: control characters become spaces,
|
|
31
|
+
* whitespace collapses, and the text is clipped to {@link HOOK_REASON_MAX_CHARS} UTF-16 code units
|
|
32
|
+
* (never splitting a surrogate pair). An empty result becomes {@link EMPTY_DENY_REASON}.
|
|
33
|
+
*/
|
|
34
|
+
export function sanitizeDenyReason(raw) {
|
|
35
|
+
const s = raw.replace(CONTROL_RE, " ").replace(/\s+/g, " ").trim();
|
|
36
|
+
if (s === "")
|
|
37
|
+
return EMPTY_DENY_REASON;
|
|
38
|
+
return clipText(s, HOOK_REASON_MAX_CHARS);
|
|
39
|
+
}
|
|
40
|
+
/** Clips to at most `max` UTF-16 code units, ending in "…", without splitting a surrogate pair. */
|
|
41
|
+
export function clipText(s, max) {
|
|
42
|
+
if (s.length <= max)
|
|
43
|
+
return s;
|
|
44
|
+
let out = "";
|
|
45
|
+
for (const cp of s) {
|
|
46
|
+
if (out.length + cp.length > max - 1)
|
|
47
|
+
break;
|
|
48
|
+
out += cp;
|
|
49
|
+
}
|
|
50
|
+
return `${out}…`;
|
|
51
|
+
}
|
|
52
|
+
const failedReason = (plugin) => `The ${plugin} policy check failed`;
|
|
53
|
+
const unavailableReason = (plugin) => `The ${plugin} policy check is unavailable`;
|
|
54
|
+
const timeoutReason = (plugin) => `The ${plugin} policy check did not respond in time`;
|
|
55
|
+
/** Deep-freezes a structured clone in place (typed arrays cannot be frozen and are left as they are). */
|
|
56
|
+
export function deepFreeze(v) {
|
|
57
|
+
if (typeof v !== "object" || v === null || ArrayBuffer.isView(v) || Object.isFrozen(v))
|
|
58
|
+
return v;
|
|
59
|
+
for (const k of Reflect.ownKeys(v))
|
|
60
|
+
deepFreeze(v[k]);
|
|
61
|
+
return Object.freeze(v);
|
|
62
|
+
}
|
|
63
|
+
/** Validates a registration; throws a plain Error (permission checks happen in the ctx). */
|
|
64
|
+
export function checkHookRegistration(plugin, mount, handler, opts) {
|
|
65
|
+
if (!AVAILABLE.has(mount)) {
|
|
66
|
+
throw new Error(`plugin ${plugin}: hook "${mount}" is not available (available: ${HOOKS_AVAILABLE.join(", ")})`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof handler !== "function") {
|
|
69
|
+
throw new Error(`plugin ${plugin}: hook "${mount}" handler must be a function`);
|
|
70
|
+
}
|
|
71
|
+
const mode = opts?.mode;
|
|
72
|
+
if (mode !== "observe" && mode !== "policy") {
|
|
73
|
+
throw new Error(`plugin ${plugin}: hook "${mount}" needs mode "observe" or "policy"`);
|
|
74
|
+
}
|
|
75
|
+
if (mode === "policy" && !POLICY.has(mount)) {
|
|
76
|
+
throw new Error(`plugin ${plugin}: hook "${mount}" cannot use mode "policy" (policy hooks: ${POLICY_HOOKS.join(", ")})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A promise that rejects with the signal's reason once it aborts (never resolves otherwise), and a
|
|
81
|
+
* cleanup that removes its listener (call it in a `finally`: callers reuse one signal for many runs).
|
|
82
|
+
*/
|
|
83
|
+
const abortOf = (s) => {
|
|
84
|
+
let onAbort = () => { };
|
|
85
|
+
const aborted = new Promise((_, reject) => {
|
|
86
|
+
onAbort = () => reject(s.reason);
|
|
87
|
+
if (s.aborted)
|
|
88
|
+
onAbort();
|
|
89
|
+
else
|
|
90
|
+
s.addEventListener("abort", onAbort, { once: true });
|
|
91
|
+
});
|
|
92
|
+
aborted.catch(() => { });
|
|
93
|
+
return { aborted, cleanup: () => s.removeEventListener("abort", onAbort) };
|
|
94
|
+
};
|
|
95
|
+
/** Longest raw error kept in `HookOutcome.detail` (characters). */
|
|
96
|
+
export const HOOK_DETAIL_MAX_CHARS = 2048;
|
|
97
|
+
const clipDetail = (s) => clipText(s, HOOK_DETAIL_MAX_CHARS);
|
|
98
|
+
/**
|
|
99
|
+
* Runs the hooks of this process's **active** plugin instances (one registry per replica, like
|
|
100
|
+
* tools). Hooks live on each instance (`ctx.hooks.on` records them in the instance ledger), and the
|
|
101
|
+
* registry resolves them from the host's active instances on every call: after an upgrade only the
|
|
102
|
+
* new generation fires, and unloading or disabling an instance stops its hooks. Every hook call runs
|
|
103
|
+
* through the instance's `inflight.run`, so revocation drains it (up to `extensionHost.drainSeconds`).
|
|
104
|
+
*
|
|
105
|
+
* {@link run} (Ruling R11):
|
|
106
|
+
* 1. If the caller's signal is aborted, throw its reason (an abort is never a deny).
|
|
107
|
+
* 2. Resolve the organization's enabled plugins (every run, not cached) and make one deep-frozen
|
|
108
|
+
* `structuredClone` of the payload. If either throws (Ruling R10): with any policy hook on the
|
|
109
|
+
* mount, deny with a generic reason (`hook_error`); with only observe hooks, allow and warn.
|
|
110
|
+
* If a plugin the organization enabled is expected to run policy hooks on the mount (deployment
|
|
111
|
+
* state, `expectedPolicy`) but is not loaded on this replica, deny with a generic reason
|
|
112
|
+
* (`hook_error`, `notLoaded`) without running any policy hook (M-1: fail closed). Observe hooks
|
|
113
|
+
* of plugins that are not loaded are simply missing.
|
|
114
|
+
* 3. Policy hooks run serially by plugin name (registration order within a plugin), each limited
|
|
115
|
+
* by `hooks.timeoutMs` (read on every run, hot). `{ deny }`, an error or a timeout stops the chain
|
|
116
|
+
* and denies (`deny` / `hook_error` / `timeout`). A policy hook cut off by its instance being
|
|
117
|
+
* revoked (`ExtensionUnloadedError`) reruns once on the plugin's current generation, if one is active
|
|
118
|
+
* and enabled; otherwise the call is denied.
|
|
119
|
+
* 4. Observe hooks (resolved again from the active instances) are then dispatched concurrently, fire-and-forget, whether or not the call was
|
|
120
|
+
* denied (never awaited on the call path). At most {@link OBSERVE_INFLIGHT_MAX} run per plugin on
|
|
121
|
+
* this replica; overflow is dropped. Their verdicts are ignored; errors, timeouts and drops are
|
|
122
|
+
* logged at most once per plugin and mount per minute (Redis counter with a TTL, shared by
|
|
123
|
+
* replicas).
|
|
124
|
+
*
|
|
125
|
+
* The in-flight observe counter is per replica on purpose (it bounds this process's work); it is not
|
|
126
|
+
* cross-request state anybody reads back. Callers fire hooks only for chat conversations, never in
|
|
127
|
+
* eval replays, and skip building the payload when {@link has} is false.
|
|
128
|
+
*/
|
|
129
|
+
export class HookRegistry {
|
|
130
|
+
d;
|
|
131
|
+
/** In-flight observe calls per plugin on this replica (see {@link OBSERVE_INFLIGHT_MAX}). */
|
|
132
|
+
observing = new Map();
|
|
133
|
+
constructor(d) {
|
|
134
|
+
this.d = d;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether calls on the mount point need {@link run}: an active instance has a hook on it, or a plugin
|
|
138
|
+
* expected to run policy hooks on it is not loaded here (M-1). In-process only (regardless of
|
|
139
|
+
* organization), so callers skip building payloads and log lookups when it is false.
|
|
140
|
+
*/
|
|
141
|
+
has(mount) {
|
|
142
|
+
return (this.d.instances().some((i) => i.hooks.some((h) => h.mount === mount)) ||
|
|
143
|
+
this.unloadedPolicy(mount).length > 0);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Plugins expected to run policy hooks on the mount (deployment state) that have no active instance
|
|
147
|
+
* on this replica, sorted by name. A loaded plugin that registered no policy hook is not listed.
|
|
148
|
+
*/
|
|
149
|
+
unloadedPolicy(mount) {
|
|
150
|
+
if (!POLICY.has(mount) || !this.d.expectedPolicy)
|
|
151
|
+
return [];
|
|
152
|
+
return this.d
|
|
153
|
+
.expectedPolicy(mount)
|
|
154
|
+
.filter((p) => this.d.current(p) === undefined)
|
|
155
|
+
.sort();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* `usage.report` handlers by plugin name (delivered by the usage queue, not by {@link run}); each
|
|
159
|
+
* call runs through the instance's `inflight.run`.
|
|
160
|
+
*/
|
|
161
|
+
handlers(mount) {
|
|
162
|
+
return this.entries(mount).map(({ src, hook }) => ({
|
|
163
|
+
plugin: src.name,
|
|
164
|
+
handler: (payload, signal) => src.inflight.run(signal, async (s) => {
|
|
165
|
+
await hook.handler(payload, s);
|
|
166
|
+
}),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
/** Runs the hooks of a mount point (see the class comment). */
|
|
170
|
+
async run(mount, payload, signal) {
|
|
171
|
+
if (signal.aborted)
|
|
172
|
+
throw signal.reason;
|
|
173
|
+
const all = this.entries(mount);
|
|
174
|
+
const unloaded = this.unloadedPolicy(mount);
|
|
175
|
+
if (all.length === 0 && unloaded.length === 0)
|
|
176
|
+
return { allowed: true };
|
|
177
|
+
let enabled;
|
|
178
|
+
let frozen;
|
|
179
|
+
let missing;
|
|
180
|
+
try {
|
|
181
|
+
const abort = abortOf(signal);
|
|
182
|
+
try {
|
|
183
|
+
enabled = await Promise.race([this.d.enabledPlugins(payload.orgId), abort.aborted]);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
abort.cleanup();
|
|
187
|
+
}
|
|
188
|
+
missing = unloaded.filter((p) => enabled.has(p));
|
|
189
|
+
// M-1: an enabled policy plugin is not loaded on this replica: fail closed (R10 stance);
|
|
190
|
+
// observe hooks, if any, still see the denied call (below)
|
|
191
|
+
if (missing.length > 0 &&
|
|
192
|
+
!all.some((e) => e.hook.mode === "observe" && enabled.has(e.src.name))) {
|
|
193
|
+
return this.notLoadedDenial(mount, missing);
|
|
194
|
+
}
|
|
195
|
+
if (!all.some((e) => enabled.has(e.src.name)))
|
|
196
|
+
return { allowed: true };
|
|
197
|
+
// Cost: one clone + deep-freeze per call. The model.request payload carries no media bytes
|
|
198
|
+
// (hookRequestOf, I-3), so it stays small. Freezing in place would freeze the logged object.
|
|
199
|
+
// The clone is skipped when has() is false or no enabled plugin has a hook on this mount.
|
|
200
|
+
frozen = deepFreeze(structuredClone(payload));
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (signal.aborted)
|
|
204
|
+
throw signal.reason;
|
|
205
|
+
const raw = errMsg(err);
|
|
206
|
+
const firstPolicy = all.find((e) => e.hook.mode === "policy")?.src.name ?? unloaded[0];
|
|
207
|
+
if (firstPolicy !== undefined) {
|
|
208
|
+
// R10: no plugin ran; the named plugin is only the first policy plugin on this mount, so
|
|
209
|
+
// the detail says so instead of blaming it
|
|
210
|
+
const detail = clipDetail(`Hooks could not run (organization lookup or payload copy failed) before any plugin was called; ${firstPolicy} is named only as the first policy plugin on ${mount}: ${raw}`);
|
|
211
|
+
this.d.log.warn("plugin hooks could not run; denying the call", {
|
|
212
|
+
mount,
|
|
213
|
+
firstPolicyPlugin: firstPolicy,
|
|
214
|
+
error: raw,
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
allowed: false,
|
|
218
|
+
plugin: firstPolicy,
|
|
219
|
+
reason: failedReason(firstPolicy),
|
|
220
|
+
cause: "hook_error",
|
|
221
|
+
detail,
|
|
222
|
+
preflight: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
for (const name of new Set(all.map((e) => e.src.name))) {
|
|
226
|
+
void this.warnOnce(name, mount, "prepare", "plugin observe hooks skipped", raw);
|
|
227
|
+
}
|
|
228
|
+
return { allowed: true };
|
|
229
|
+
}
|
|
230
|
+
const active = all.filter((e) => enabled.has(e.src.name));
|
|
231
|
+
const timeoutMs = this.d.config().hooks.timeoutMs;
|
|
232
|
+
let outcome = missing.length > 0 ? this.notLoadedDenial(mount, missing) : { allowed: true };
|
|
233
|
+
// policy hooks grouped per plugin (entries are sorted, so groups are contiguous)
|
|
234
|
+
const groups = [];
|
|
235
|
+
for (const e of active) {
|
|
236
|
+
if (e.hook.mode !== "policy")
|
|
237
|
+
continue;
|
|
238
|
+
const last = groups[groups.length - 1];
|
|
239
|
+
if (last && last.src === e.src)
|
|
240
|
+
last.hooks.push(e.hook);
|
|
241
|
+
else
|
|
242
|
+
groups.push({ src: e.src, hooks: [e.hook] });
|
|
243
|
+
}
|
|
244
|
+
for (const g of outcome.allowed ? groups : []) {
|
|
245
|
+
const denied = await this.runPolicies(mount, g.src, g.hooks, frozen, signal, timeoutMs);
|
|
246
|
+
if (denied) {
|
|
247
|
+
outcome = denied;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// observe hooks are resolved again now: if the plugin was upgraded while the policy hooks ran,
|
|
252
|
+
// the current generation observes (the retiring one no longer accepts calls)
|
|
253
|
+
for (const e of this.entries(mount)) {
|
|
254
|
+
if (e.hook.mode === "observe" && enabled.has(e.src.name)) {
|
|
255
|
+
this.dispatchObserve(mount, e.src, e.hook, frozen, signal);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return outcome;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The M-1 denial: `missing` (sorted) are enabled policy plugins that are not loaded on this replica.
|
|
262
|
+
* The model-visible reason is generic; the detail and a throttled warning name the plugin for the
|
|
263
|
+
* admin.
|
|
264
|
+
*/
|
|
265
|
+
notLoadedDenial(mount, missing) {
|
|
266
|
+
const plugin = missing[0];
|
|
267
|
+
const detail = `extension ${missing.join(", ")} declares a policy hook on ${mount} (permissions.policyHooks) and is enabled, but is not loaded on this replica; the call was denied without running any hook`;
|
|
268
|
+
for (const p of missing) {
|
|
269
|
+
void this.warnOnce(p, mount, "not-loaded", "policy extension not loaded on this replica; denying calls (check the extension's status in the admin console)", "not loaded");
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
allowed: false,
|
|
273
|
+
plugin,
|
|
274
|
+
reason: unavailableReason(plugin),
|
|
275
|
+
cause: "hook_error",
|
|
276
|
+
detail,
|
|
277
|
+
notLoaded: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** Active entries of a mount point, sorted by plugin name (stable: registration order within). */
|
|
281
|
+
entries(mount) {
|
|
282
|
+
return this.d
|
|
283
|
+
.instances()
|
|
284
|
+
.flatMap((src) => src.hooks.filter((h) => h.mount === mount).map((hook) => ({ src, hook })))
|
|
285
|
+
.sort((a, b) => (a.src.name < b.src.name ? -1 : a.src.name > b.src.name ? 1 : 0));
|
|
286
|
+
}
|
|
287
|
+
/** One plugin's policy hooks in registration order; reruns once on a newer generation if revoked. */
|
|
288
|
+
async runPolicies(mount, src, hooks, payload, signal, timeoutMs, rerun = false) {
|
|
289
|
+
const plugin = src.name;
|
|
290
|
+
for (const hook of hooks) {
|
|
291
|
+
if (signal.aborted)
|
|
292
|
+
throw signal.reason;
|
|
293
|
+
let r;
|
|
294
|
+
try {
|
|
295
|
+
r = await src.inflight.run(signal, (s) => this.call(hook, payload, s, timeoutMs));
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
if (signal.aborted)
|
|
299
|
+
throw signal.reason;
|
|
300
|
+
if (err instanceof ExtensionUnloadedError) {
|
|
301
|
+
const cur = this.d.current(plugin);
|
|
302
|
+
if (!rerun && cur && cur !== src) {
|
|
303
|
+
const again = cur.hooks.filter((h) => h.mount === mount && h.mode === "policy");
|
|
304
|
+
return this.runPolicies(mount, cur, again, payload, signal, timeoutMs, true);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
r = { kind: "error", message: errMsg(err) };
|
|
308
|
+
}
|
|
309
|
+
if (signal.aborted)
|
|
310
|
+
throw signal.reason;
|
|
311
|
+
if (r.kind === "error") {
|
|
312
|
+
this.d.log.warn("plugin policy hook failed; denying the call", {
|
|
313
|
+
plugin,
|
|
314
|
+
mount,
|
|
315
|
+
error: r.message,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
allowed: false,
|
|
319
|
+
plugin,
|
|
320
|
+
reason: failedReason(plugin),
|
|
321
|
+
cause: "hook_error",
|
|
322
|
+
detail: clipDetail(r.message),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (r.kind === "timeout") {
|
|
326
|
+
const detail = `did not finish within ${timeoutMs} ms`;
|
|
327
|
+
this.d.log.warn("plugin policy hook timed out; denying the call", {
|
|
328
|
+
plugin,
|
|
329
|
+
mount,
|
|
330
|
+
timeoutMs,
|
|
331
|
+
});
|
|
332
|
+
return { allowed: false, plugin, reason: timeoutReason(plugin), cause: "timeout", detail };
|
|
333
|
+
}
|
|
334
|
+
if (r.deny !== undefined) {
|
|
335
|
+
return { allowed: false, plugin, reason: sanitizeDenyReason(r.deny), cause: "deny" };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
/** Fire-and-forget observe call through the instance's in-flight tracker, capped per plugin. */
|
|
341
|
+
dispatchObserve(mount, src, hook, payload, signal) {
|
|
342
|
+
const plugin = src.name;
|
|
343
|
+
const n = this.observing.get(plugin) ?? 0;
|
|
344
|
+
if (n >= OBSERVE_INFLIGHT_MAX) {
|
|
345
|
+
void this.warnOnce(plugin, mount, "overflow", "plugin observe hook dropped: too many calls in flight", `limit ${OBSERVE_INFLIGHT_MAX}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const timeoutMs = this.d.config().hooks.timeoutMs;
|
|
349
|
+
this.observing.set(plugin, n + 1);
|
|
350
|
+
void (async () => {
|
|
351
|
+
try {
|
|
352
|
+
const r = await src.inflight.run(signal, (s) => this.call(hook, payload, s, timeoutMs));
|
|
353
|
+
if (r.kind === "error" || r.kind === "timeout") {
|
|
354
|
+
await this.warnOnce(plugin, mount, "failed", "plugin observe hook failed; the call continued", r.kind === "timeout" ? `timed out after ${timeoutMs} ms` : r.message);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
// the caller aborted or the instance was revoked: nothing to report to anyone
|
|
359
|
+
this.d.log.info("plugin observe hook cut off", { plugin, mount, reason: errMsg(err) });
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
const left = (this.observing.get(plugin) ?? 1) - 1;
|
|
363
|
+
if (left <= 0)
|
|
364
|
+
this.observing.delete(plugin);
|
|
365
|
+
else
|
|
366
|
+
this.observing.set(plugin, left);
|
|
367
|
+
}
|
|
368
|
+
})();
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Calls one handler with its own signal (aborted on timeout or when `s` aborts). Rejects with
|
|
372
|
+
* `s.reason` when `s` aborts first, so callers can tell an abort from a hook failure.
|
|
373
|
+
*/
|
|
374
|
+
async call(hook, payload, s, timeoutMs) {
|
|
375
|
+
const ac = new AbortController();
|
|
376
|
+
const onAbort = () => ac.abort(s.reason);
|
|
377
|
+
if (s.aborted)
|
|
378
|
+
onAbort();
|
|
379
|
+
else
|
|
380
|
+
s.addEventListener("abort", onAbort, { once: true });
|
|
381
|
+
let timer;
|
|
382
|
+
const abort = abortOf(s);
|
|
383
|
+
try {
|
|
384
|
+
const timedOut = new Promise((resolve) => {
|
|
385
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
386
|
+
});
|
|
387
|
+
const handled = Promise.resolve()
|
|
388
|
+
.then(() => hook.handler(payload, ac.signal))
|
|
389
|
+
.then((v) => ({ v }), (err) => ({ err }));
|
|
390
|
+
const result = await Promise.race([handled, timedOut, abort.aborted]);
|
|
391
|
+
if (result === "timeout") {
|
|
392
|
+
ac.abort(new Error(`hook timed out after ${timeoutMs} ms`));
|
|
393
|
+
return { kind: "timeout" };
|
|
394
|
+
}
|
|
395
|
+
if ("err" in result)
|
|
396
|
+
return { kind: "error", message: errMsg(result.err) };
|
|
397
|
+
const v = result.v;
|
|
398
|
+
// only `{ deny: string }` is recognized; anything else counts as "no objection"
|
|
399
|
+
if (typeof v === "object" &&
|
|
400
|
+
v !== null &&
|
|
401
|
+
typeof v.deny === "string") {
|
|
402
|
+
return { kind: "ok", deny: v.deny };
|
|
403
|
+
}
|
|
404
|
+
return { kind: "ok" };
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
abort.cleanup();
|
|
409
|
+
s.removeEventListener("abort", onAbort);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** Logs a warning at most once per plugin, mount and kind per minute (counter in Redis, TTL). */
|
|
413
|
+
async warnOnce(plugin, mount, kind, msg, error) {
|
|
414
|
+
const key = `capk:hook-warn:${plugin}:${mount}:${kind}`;
|
|
415
|
+
let first = true;
|
|
416
|
+
try {
|
|
417
|
+
first = (await this.d.redis.set(key, "1", "PX", WARN_WINDOW_MS, "NX")) === "OK";
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Redis unavailable: log anyway rather than hiding the failure
|
|
421
|
+
}
|
|
422
|
+
if (first)
|
|
423
|
+
this.d.log.warn(msg, { plugin, mount, error });
|
|
424
|
+
}
|
|
425
|
+
}
|