2026-03-13 16:22:34 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* PluginCapabilityValidator — enforces the capability model at both
|
|
|
|
|
|
* install-time and runtime.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Every plugin declares the capabilities it requires in its manifest
|
|
|
|
|
|
* (`manifest.capabilities`). This service checks those declarations
|
|
|
|
|
|
* against a mapping of operations → required capabilities so that:
|
|
|
|
|
|
*
|
|
|
|
|
|
* 1. **Install-time validation** — `validateManifestCapabilities()`
|
|
|
|
|
|
* ensures that declared features (tools, jobs, webhooks, UI slots)
|
|
|
|
|
|
* have matching capability entries, giving operators clear feedback
|
|
|
|
|
|
* before a plugin is activated.
|
|
|
|
|
|
*
|
|
|
|
|
|
* 2. **Runtime gating** — `checkOperation()` / `assertOperation()` are
|
|
|
|
|
|
* called on every worker→host bridge call to enforce least-privilege
|
|
|
|
|
|
* access. If a plugin attempts an operation it did not declare, the
|
|
|
|
|
|
* call is rejected with a 403 error.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @see PLUGIN_SPEC.md §15 — Capability Model
|
|
|
|
|
|
* @see host-client-factory.ts — SDK-side capability gating
|
|
|
|
|
|
*/
|
|
|
|
|
|
import type {
|
|
|
|
|
|
PluginCapability,
|
|
|
|
|
|
PaperclipPluginManifestV1,
|
|
|
|
|
|
PluginUiSlotType,
|
|
|
|
|
|
PluginLauncherPlacementZone,
|
|
|
|
|
|
} from "@paperclipai/shared";
|
|
|
|
|
|
import { forbidden } from "../errors.js";
|
|
|
|
|
|
import { logger } from "../middleware/logger.js";
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Capability requirement mappings
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Maps high-level operations to the capabilities they require.
|
|
|
|
|
|
*
|
|
|
|
|
|
* When the bridge receives a call from a plugin worker, the host looks up
|
|
|
|
|
|
* the operation in this map and checks the plugin's declared capabilities.
|
|
|
|
|
|
* If any required capability is missing, the call is rejected.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @see PLUGIN_SPEC.md §15 — Capability Model
|
|
|
|
|
|
*/
|
|
|
|
|
|
const OPERATION_CAPABILITIES: Record<string, readonly PluginCapability[]> = {
|
|
|
|
|
|
// Data read operations
|
|
|
|
|
|
"companies.list": ["companies.read"],
|
|
|
|
|
|
"companies.get": ["companies.read"],
|
|
|
|
|
|
"projects.list": ["projects.read"],
|
|
|
|
|
|
"projects.get": ["projects.read"],
|
|
|
|
|
|
"project.workspaces.list": ["project.workspaces.read"],
|
|
|
|
|
|
"project.workspaces.get": ["project.workspaces.read"],
|
|
|
|
|
|
"issues.list": ["issues.read"],
|
|
|
|
|
|
"issues.get": ["issues.read"],
|
[codex] Add plugin orchestration host APIs (#4114)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The plugin system is the extension path for optional capabilities
that should not require core product changes for every integration.
> - Plugins need scoped host APIs for issue orchestration, documents,
wakeups, summaries, activity attribution, and isolated database state.
> - Without those host APIs, richer plugins either cannot coordinate
Paperclip work safely or need privileged core-side special cases.
> - This pull request adds the plugin orchestration host surface, scoped
route dispatch, a database namespace layer, and a smoke plugin that
exercises the contract.
> - The benefit is a broader plugin API that remains company-scoped,
auditable, and covered by tests.
## What Changed
- Added plugin orchestration host APIs for issue creation, document
access, wakeups, summaries, plugin-origin activity, and scoped API route
dispatch.
- Added plugin database namespace tables, schema exports, migration
checks, and idempotent replay coverage under migration
`0059_plugin_database_namespaces`.
- Added shared plugin route/API types and validators used by server and
SDK boundaries.
- Expanded plugin SDK types, protocol helpers, worker RPC host behavior,
and testing utilities for orchestration flows.
- Added the `plugin-orchestration-smoke-example` package to exercise
scoped routes, restricted database namespaces, issue orchestration,
documents, wakeups, summaries, and UI status surfaces.
- Kept the new orchestration smoke fixture out of the root pnpm
workspace importer so this PR preserves the repository policy of not
committing `pnpm-lock.yaml`.
- Updated plugin docs and database docs for the new orchestration and
database namespace surfaces.
- Rebased the branch onto `public-gh/master`, resolved conflicts, and
removed `pnpm-lock.yaml` from the final PR diff.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run packages/db/src/client.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-database.test.ts
server/src/__tests__/plugin-orchestration-apis.test.ts
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/plugin-scoped-api-routes.test.ts
server/src/__tests__/plugin-sdk-orchestration-contract.test.ts`
- From `packages/plugins/examples/plugin-orchestration-smoke-example`:
`pnpm exec vitest run --config ./vitest.config.ts`
- `pnpm --dir
packages/plugins/examples/plugin-orchestration-smoke-example run
typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- PR CI on latest head `293fc67c`: `policy`, `verify`, `e2e`, and
`security/snyk` all passed.
## Risks
- Medium risk: this expands plugin host authority, so route auth,
company scoping, and plugin-origin activity attribution need careful
review.
- Medium risk: database namespace migration behavior must remain
idempotent for environments that may have seen earlier branch versions.
- Medium risk: the orchestration smoke fixture is intentionally excluded
from the root workspace importer to avoid a `pnpm-lock.yaml` PR diff;
direct fixture verification remains listed above.
- Low operational risk from the PR setup itself: the branch is rebased
onto current `master`, the migration is ordered after upstream
`0057`/`0058`, and `pnpm-lock.yaml` is not in the final diff.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
Roadmap checked: this work aligns with the completed Plugin system
milestone and extends the plugin surface rather than duplicating an
unrelated planned core feature.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in a tool-enabled CLI
environment. Exact hosted model build and context-window size are not
exposed by the runtime; reasoning/tool use were enabled for repository
inspection, editing, testing, git operations, and PR creation.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (N/A: no core UI screen change; example plugin UI contract
is covered by tests)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 08:52:51 -05:00
|
|
|
|
"issues.relations.get": ["issue.relations.read"],
|
2026-03-13 16:22:34 -05:00
|
|
|
|
"issue.comments.list": ["issue.comments.read"],
|
|
|
|
|
|
"issue.comments.get": ["issue.comments.read"],
|
|
|
|
|
|
"agents.list": ["agents.read"],
|
|
|
|
|
|
"agents.get": ["agents.read"],
|
|
|
|
|
|
"goals.list": ["goals.read"],
|
|
|
|
|
|
"goals.get": ["goals.read"],
|
|
|
|
|
|
"activity.list": ["activity.read"],
|
|
|
|
|
|
"activity.get": ["activity.read"],
|
|
|
|
|
|
"costs.list": ["costs.read"],
|
|
|
|
|
|
"costs.get": ["costs.read"],
|
[codex] Add plugin orchestration host APIs (#4114)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The plugin system is the extension path for optional capabilities
that should not require core product changes for every integration.
> - Plugins need scoped host APIs for issue orchestration, documents,
wakeups, summaries, activity attribution, and isolated database state.
> - Without those host APIs, richer plugins either cannot coordinate
Paperclip work safely or need privileged core-side special cases.
> - This pull request adds the plugin orchestration host surface, scoped
route dispatch, a database namespace layer, and a smoke plugin that
exercises the contract.
> - The benefit is a broader plugin API that remains company-scoped,
auditable, and covered by tests.
## What Changed
- Added plugin orchestration host APIs for issue creation, document
access, wakeups, summaries, plugin-origin activity, and scoped API route
dispatch.
- Added plugin database namespace tables, schema exports, migration
checks, and idempotent replay coverage under migration
`0059_plugin_database_namespaces`.
- Added shared plugin route/API types and validators used by server and
SDK boundaries.
- Expanded plugin SDK types, protocol helpers, worker RPC host behavior,
and testing utilities for orchestration flows.
- Added the `plugin-orchestration-smoke-example` package to exercise
scoped routes, restricted database namespaces, issue orchestration,
documents, wakeups, summaries, and UI status surfaces.
- Kept the new orchestration smoke fixture out of the root pnpm
workspace importer so this PR preserves the repository policy of not
committing `pnpm-lock.yaml`.
- Updated plugin docs and database docs for the new orchestration and
database namespace surfaces.
- Rebased the branch onto `public-gh/master`, resolved conflicts, and
removed `pnpm-lock.yaml` from the final PR diff.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run packages/db/src/client.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-database.test.ts
server/src/__tests__/plugin-orchestration-apis.test.ts
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/plugin-scoped-api-routes.test.ts
server/src/__tests__/plugin-sdk-orchestration-contract.test.ts`
- From `packages/plugins/examples/plugin-orchestration-smoke-example`:
`pnpm exec vitest run --config ./vitest.config.ts`
- `pnpm --dir
packages/plugins/examples/plugin-orchestration-smoke-example run
typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- PR CI on latest head `293fc67c`: `policy`, `verify`, `e2e`, and
`security/snyk` all passed.
## Risks
- Medium risk: this expands plugin host authority, so route auth,
company scoping, and plugin-origin activity attribution need careful
review.
- Medium risk: database namespace migration behavior must remain
idempotent for environments that may have seen earlier branch versions.
- Medium risk: the orchestration smoke fixture is intentionally excluded
from the root workspace importer to avoid a `pnpm-lock.yaml` PR diff;
direct fixture verification remains listed above.
- Low operational risk from the PR setup itself: the branch is rebased
onto current `master`, the migration is ordered after upstream
`0057`/`0058`, and `pnpm-lock.yaml` is not in the final diff.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
Roadmap checked: this work aligns with the completed Plugin system
milestone and extends the plugin surface rather than duplicating an
unrelated planned core feature.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in a tool-enabled CLI
environment. Exact hosted model build and context-window size are not
exposed by the runtime; reasoning/tool use were enabled for repository
inspection, editing, testing, git operations, and PR creation.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (N/A: no core UI screen change; example plugin UI contract
is covered by tests)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 08:52:51 -05:00
|
|
|
|
"issues.summaries.getOrchestration": ["issues.orchestration.read"],
|
|
|
|
|
|
"db.namespace": ["database.namespace.read"],
|
|
|
|
|
|
"db.query": ["database.namespace.read"],
|
2026-03-13 16:22:34 -05:00
|
|
|
|
|
|
|
|
|
|
// Data write operations
|
|
|
|
|
|
"issues.create": ["issues.create"],
|
|
|
|
|
|
"issues.update": ["issues.update"],
|
[codex] Add plugin orchestration host APIs (#4114)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The plugin system is the extension path for optional capabilities
that should not require core product changes for every integration.
> - Plugins need scoped host APIs for issue orchestration, documents,
wakeups, summaries, activity attribution, and isolated database state.
> - Without those host APIs, richer plugins either cannot coordinate
Paperclip work safely or need privileged core-side special cases.
> - This pull request adds the plugin orchestration host surface, scoped
route dispatch, a database namespace layer, and a smoke plugin that
exercises the contract.
> - The benefit is a broader plugin API that remains company-scoped,
auditable, and covered by tests.
## What Changed
- Added plugin orchestration host APIs for issue creation, document
access, wakeups, summaries, plugin-origin activity, and scoped API route
dispatch.
- Added plugin database namespace tables, schema exports, migration
checks, and idempotent replay coverage under migration
`0059_plugin_database_namespaces`.
- Added shared plugin route/API types and validators used by server and
SDK boundaries.
- Expanded plugin SDK types, protocol helpers, worker RPC host behavior,
and testing utilities for orchestration flows.
- Added the `plugin-orchestration-smoke-example` package to exercise
scoped routes, restricted database namespaces, issue orchestration,
documents, wakeups, summaries, and UI status surfaces.
- Kept the new orchestration smoke fixture out of the root pnpm
workspace importer so this PR preserves the repository policy of not
committing `pnpm-lock.yaml`.
- Updated plugin docs and database docs for the new orchestration and
database namespace surfaces.
- Rebased the branch onto `public-gh/master`, resolved conflicts, and
removed `pnpm-lock.yaml` from the final PR diff.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run packages/db/src/client.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-database.test.ts
server/src/__tests__/plugin-orchestration-apis.test.ts
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/plugin-scoped-api-routes.test.ts
server/src/__tests__/plugin-sdk-orchestration-contract.test.ts`
- From `packages/plugins/examples/plugin-orchestration-smoke-example`:
`pnpm exec vitest run --config ./vitest.config.ts`
- `pnpm --dir
packages/plugins/examples/plugin-orchestration-smoke-example run
typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- PR CI on latest head `293fc67c`: `policy`, `verify`, `e2e`, and
`security/snyk` all passed.
## Risks
- Medium risk: this expands plugin host authority, so route auth,
company scoping, and plugin-origin activity attribution need careful
review.
- Medium risk: database namespace migration behavior must remain
idempotent for environments that may have seen earlier branch versions.
- Medium risk: the orchestration smoke fixture is intentionally excluded
from the root workspace importer to avoid a `pnpm-lock.yaml` PR diff;
direct fixture verification remains listed above.
- Low operational risk from the PR setup itself: the branch is rebased
onto current `master`, the migration is ordered after upstream
`0057`/`0058`, and `pnpm-lock.yaml` is not in the final diff.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
Roadmap checked: this work aligns with the completed Plugin system
milestone and extends the plugin surface rather than duplicating an
unrelated planned core feature.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in a tool-enabled CLI
environment. Exact hosted model build and context-window size are not
exposed by the runtime; reasoning/tool use were enabled for repository
inspection, editing, testing, git operations, and PR creation.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (N/A: no core UI screen change; example plugin UI contract
is covered by tests)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 08:52:51 -05:00
|
|
|
|
"issues.relations.setBlockedBy": ["issue.relations.write"],
|
|
|
|
|
|
"issues.relations.addBlockers": ["issue.relations.write"],
|
|
|
|
|
|
"issues.relations.removeBlockers": ["issue.relations.write"],
|
|
|
|
|
|
"issues.assertCheckoutOwner": ["issues.checkout"],
|
|
|
|
|
|
"issues.getSubtree": ["issue.subtree.read"],
|
|
|
|
|
|
"issues.requestWakeup": ["issues.wakeup"],
|
|
|
|
|
|
"issues.requestWakeups": ["issues.wakeup"],
|
2026-03-13 16:22:34 -05:00
|
|
|
|
"issue.comments.create": ["issue.comments.create"],
|
|
|
|
|
|
"activity.log": ["activity.log.write"],
|
|
|
|
|
|
"metrics.write": ["metrics.write"],
|
2026-03-31 13:18:50 -05:00
|
|
|
|
"telemetry.track": ["telemetry.track"],
|
[codex] Add plugin orchestration host APIs (#4114)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The plugin system is the extension path for optional capabilities
that should not require core product changes for every integration.
> - Plugins need scoped host APIs for issue orchestration, documents,
wakeups, summaries, activity attribution, and isolated database state.
> - Without those host APIs, richer plugins either cannot coordinate
Paperclip work safely or need privileged core-side special cases.
> - This pull request adds the plugin orchestration host surface, scoped
route dispatch, a database namespace layer, and a smoke plugin that
exercises the contract.
> - The benefit is a broader plugin API that remains company-scoped,
auditable, and covered by tests.
## What Changed
- Added plugin orchestration host APIs for issue creation, document
access, wakeups, summaries, plugin-origin activity, and scoped API route
dispatch.
- Added plugin database namespace tables, schema exports, migration
checks, and idempotent replay coverage under migration
`0059_plugin_database_namespaces`.
- Added shared plugin route/API types and validators used by server and
SDK boundaries.
- Expanded plugin SDK types, protocol helpers, worker RPC host behavior,
and testing utilities for orchestration flows.
- Added the `plugin-orchestration-smoke-example` package to exercise
scoped routes, restricted database namespaces, issue orchestration,
documents, wakeups, summaries, and UI status surfaces.
- Kept the new orchestration smoke fixture out of the root pnpm
workspace importer so this PR preserves the repository policy of not
committing `pnpm-lock.yaml`.
- Updated plugin docs and database docs for the new orchestration and
database namespace surfaces.
- Rebased the branch onto `public-gh/master`, resolved conflicts, and
removed `pnpm-lock.yaml` from the final PR diff.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run packages/db/src/client.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-database.test.ts
server/src/__tests__/plugin-orchestration-apis.test.ts
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/plugin-scoped-api-routes.test.ts
server/src/__tests__/plugin-sdk-orchestration-contract.test.ts`
- From `packages/plugins/examples/plugin-orchestration-smoke-example`:
`pnpm exec vitest run --config ./vitest.config.ts`
- `pnpm --dir
packages/plugins/examples/plugin-orchestration-smoke-example run
typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- PR CI on latest head `293fc67c`: `policy`, `verify`, `e2e`, and
`security/snyk` all passed.
## Risks
- Medium risk: this expands plugin host authority, so route auth,
company scoping, and plugin-origin activity attribution need careful
review.
- Medium risk: database namespace migration behavior must remain
idempotent for environments that may have seen earlier branch versions.
- Medium risk: the orchestration smoke fixture is intentionally excluded
from the root workspace importer to avoid a `pnpm-lock.yaml` PR diff;
direct fixture verification remains listed above.
- Low operational risk from the PR setup itself: the branch is rebased
onto current `master`, the migration is ordered after upstream
`0057`/`0058`, and `pnpm-lock.yaml` is not in the final diff.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
Roadmap checked: this work aligns with the completed Plugin system
milestone and extends the plugin surface rather than duplicating an
unrelated planned core feature.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in a tool-enabled CLI
environment. Exact hosted model build and context-window size are not
exposed by the runtime; reasoning/tool use were enabled for repository
inspection, editing, testing, git operations, and PR creation.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (N/A: no core UI screen change; example plugin UI contract
is covered by tests)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 08:52:51 -05:00
|
|
|
|
"db.migrate": ["database.namespace.migrate"],
|
|
|
|
|
|
"db.execute": ["database.namespace.write"],
|
2026-03-13 16:22:34 -05:00
|
|
|
|
|
|
|
|
|
|
// Plugin state operations
|
|
|
|
|
|
"plugin.state.get": ["plugin.state.read"],
|
|
|
|
|
|
"plugin.state.list": ["plugin.state.read"],
|
|
|
|
|
|
"plugin.state.set": ["plugin.state.write"],
|
|
|
|
|
|
"plugin.state.delete": ["plugin.state.write"],
|
|
|
|
|
|
|
|
|
|
|
|
// Runtime / Integration operations
|
|
|
|
|
|
"events.subscribe": ["events.subscribe"],
|
|
|
|
|
|
"events.emit": ["events.emit"],
|
|
|
|
|
|
"jobs.schedule": ["jobs.schedule"],
|
|
|
|
|
|
"jobs.cancel": ["jobs.schedule"],
|
|
|
|
|
|
"webhooks.receive": ["webhooks.receive"],
|
|
|
|
|
|
"http.request": ["http.outbound"],
|
|
|
|
|
|
"secrets.resolve": ["secrets.read-ref"],
|
|
|
|
|
|
|
|
|
|
|
|
// Agent tools
|
|
|
|
|
|
"agent.tools.register": ["agent.tools.register"],
|
|
|
|
|
|
"agent.tools.execute": ["agent.tools.register"],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Maps UI slot types to the capability required to register them.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @see PLUGIN_SPEC.md §19 — UI Extension Model
|
|
|
|
|
|
*/
|
|
|
|
|
|
const UI_SLOT_CAPABILITIES: Record<PluginUiSlotType, PluginCapability> = {
|
|
|
|
|
|
sidebar: "ui.sidebar.register",
|
|
|
|
|
|
sidebarPanel: "ui.sidebar.register",
|
|
|
|
|
|
projectSidebarItem: "ui.sidebar.register",
|
|
|
|
|
|
page: "ui.page.register",
|
|
|
|
|
|
detailTab: "ui.detailTab.register",
|
|
|
|
|
|
taskDetailView: "ui.detailTab.register",
|
|
|
|
|
|
dashboardWidget: "ui.dashboardWidget.register",
|
2026-03-14 15:05:04 -07:00
|
|
|
|
globalToolbarButton: "ui.action.register",
|
2026-03-13 16:22:34 -05:00
|
|
|
|
toolbarButton: "ui.action.register",
|
|
|
|
|
|
contextMenuItem: "ui.action.register",
|
|
|
|
|
|
commentAnnotation: "ui.commentAnnotation.register",
|
|
|
|
|
|
commentContextMenuItem: "ui.action.register",
|
|
|
|
|
|
settingsPage: "instance.settings.register",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Launcher placement zones align with host UI surfaces and therefore inherit
|
|
|
|
|
|
* the same capability requirements as the equivalent slot type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const LAUNCHER_PLACEMENT_CAPABILITIES: Record<
|
|
|
|
|
|
PluginLauncherPlacementZone,
|
|
|
|
|
|
PluginCapability
|
|
|
|
|
|
> = {
|
|
|
|
|
|
page: "ui.page.register",
|
|
|
|
|
|
detailTab: "ui.detailTab.register",
|
|
|
|
|
|
taskDetailView: "ui.detailTab.register",
|
|
|
|
|
|
dashboardWidget: "ui.dashboardWidget.register",
|
|
|
|
|
|
sidebar: "ui.sidebar.register",
|
|
|
|
|
|
sidebarPanel: "ui.sidebar.register",
|
|
|
|
|
|
projectSidebarItem: "ui.sidebar.register",
|
2026-03-14 15:05:04 -07:00
|
|
|
|
globalToolbarButton: "ui.action.register",
|
2026-03-13 16:22:34 -05:00
|
|
|
|
toolbarButton: "ui.action.register",
|
|
|
|
|
|
contextMenuItem: "ui.action.register",
|
|
|
|
|
|
commentAnnotation: "ui.commentAnnotation.register",
|
|
|
|
|
|
commentContextMenuItem: "ui.action.register",
|
|
|
|
|
|
settingsPage: "instance.settings.register",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Maps feature declarations in the manifest to their required capabilities.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const FEATURE_CAPABILITIES: Record<string, PluginCapability> = {
|
|
|
|
|
|
tools: "agent.tools.register",
|
|
|
|
|
|
jobs: "jobs.schedule",
|
|
|
|
|
|
webhooks: "webhooks.receive",
|
[codex] Add plugin orchestration host APIs (#4114)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - The plugin system is the extension path for optional capabilities
that should not require core product changes for every integration.
> - Plugins need scoped host APIs for issue orchestration, documents,
wakeups, summaries, activity attribution, and isolated database state.
> - Without those host APIs, richer plugins either cannot coordinate
Paperclip work safely or need privileged core-side special cases.
> - This pull request adds the plugin orchestration host surface, scoped
route dispatch, a database namespace layer, and a smoke plugin that
exercises the contract.
> - The benefit is a broader plugin API that remains company-scoped,
auditable, and covered by tests.
## What Changed
- Added plugin orchestration host APIs for issue creation, document
access, wakeups, summaries, plugin-origin activity, and scoped API route
dispatch.
- Added plugin database namespace tables, schema exports, migration
checks, and idempotent replay coverage under migration
`0059_plugin_database_namespaces`.
- Added shared plugin route/API types and validators used by server and
SDK boundaries.
- Expanded plugin SDK types, protocol helpers, worker RPC host behavior,
and testing utilities for orchestration flows.
- Added the `plugin-orchestration-smoke-example` package to exercise
scoped routes, restricted database namespaces, issue orchestration,
documents, wakeups, summaries, and UI status surfaces.
- Kept the new orchestration smoke fixture out of the root pnpm
workspace importer so this PR preserves the repository policy of not
committing `pnpm-lock.yaml`.
- Updated plugin docs and database docs for the new orchestration and
database namespace surfaces.
- Rebased the branch onto `public-gh/master`, resolved conflicts, and
removed `pnpm-lock.yaml` from the final PR diff.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm --filter @paperclipai/db typecheck`
- `pnpm exec vitest run packages/db/src/client.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-database.test.ts
server/src/__tests__/plugin-orchestration-apis.test.ts
server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/plugin-scoped-api-routes.test.ts
server/src/__tests__/plugin-sdk-orchestration-contract.test.ts`
- From `packages/plugins/examples/plugin-orchestration-smoke-example`:
`pnpm exec vitest run --config ./vitest.config.ts`
- `pnpm --dir
packages/plugins/examples/plugin-orchestration-smoke-example run
typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- PR CI on latest head `293fc67c`: `policy`, `verify`, `e2e`, and
`security/snyk` all passed.
## Risks
- Medium risk: this expands plugin host authority, so route auth,
company scoping, and plugin-origin activity attribution need careful
review.
- Medium risk: database namespace migration behavior must remain
idempotent for environments that may have seen earlier branch versions.
- Medium risk: the orchestration smoke fixture is intentionally excluded
from the root workspace importer to avoid a `pnpm-lock.yaml` PR diff;
direct fixture verification remains listed above.
- Low operational risk from the PR setup itself: the branch is rebased
onto current `master`, the migration is ordered after upstream
`0057`/`0058`, and `pnpm-lock.yaml` is not in the final diff.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
Roadmap checked: this work aligns with the completed Plugin system
milestone and extends the plugin surface rather than duplicating an
unrelated planned core feature.
## Model Used
- OpenAI Codex, GPT-5-based coding agent in a tool-enabled CLI
environment. Exact hosted model build and context-window size are not
exposed by the runtime; reasoning/tool use were enabled for repository
inspection, editing, testing, git operations, and PR creation.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots (N/A: no core UI screen change; example plugin UI contract
is covered by tests)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 08:52:51 -05:00
|
|
|
|
database: "database.namespace.migrate",
|
2026-03-13 16:22:34 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Result types
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Result of a capability check. When `allowed` is false, `missing` contains
|
|
|
|
|
|
* the capabilities that the plugin does not declare but the operation requires.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface CapabilityCheckResult {
|
|
|
|
|
|
allowed: boolean;
|
|
|
|
|
|
missing: PluginCapability[];
|
|
|
|
|
|
operation?: string;
|
|
|
|
|
|
pluginId?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// PluginCapabilityValidator interface
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export interface PluginCapabilityValidator {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin has a specific capability.
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasCapability(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capability: PluginCapability,
|
|
|
|
|
|
): boolean;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin has all of the specified capabilities.
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasAllCapabilities(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capabilities: PluginCapability[],
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin has at least one of the specified capabilities.
|
|
|
|
|
|
*/
|
|
|
|
|
|
hasAnyCapability(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capabilities: PluginCapability[],
|
|
|
|
|
|
): boolean;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin is allowed to perform the named operation.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Operations are mapped to required capabilities via OPERATION_CAPABILITIES.
|
|
|
|
|
|
* Unknown operations are rejected by default.
|
|
|
|
|
|
*/
|
|
|
|
|
|
checkOperation(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
operation: string,
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Assert that a plugin is allowed to perform an operation.
|
|
|
|
|
|
* Throws a 403 HttpError if the capability check fails.
|
|
|
|
|
|
*/
|
|
|
|
|
|
assertOperation(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
operation: string,
|
|
|
|
|
|
): void;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Assert that a plugin has a specific capability.
|
|
|
|
|
|
* Throws a 403 HttpError if the capability is missing.
|
|
|
|
|
|
*/
|
|
|
|
|
|
assertCapability(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
capability: PluginCapability,
|
|
|
|
|
|
): void;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check whether a plugin can register the given UI slot type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
checkUiSlot(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
slotType: PluginUiSlotType,
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Validate that a manifest's declared capabilities are consistent with its
|
|
|
|
|
|
* declared features (tools, jobs, webhooks, UI slots).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Returns all missing capabilities rather than failing on the first one.
|
|
|
|
|
|
* This is useful for install-time validation to give comprehensive feedback.
|
|
|
|
|
|
*/
|
|
|
|
|
|
validateManifestCapabilities(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
): CapabilityCheckResult;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get the capabilities required for a named operation.
|
|
|
|
|
|
* Returns an empty array if the operation is unknown.
|
|
|
|
|
|
*/
|
|
|
|
|
|
getRequiredCapabilities(operation: string): readonly PluginCapability[];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get the capability required for a UI slot type.
|
|
|
|
|
|
*/
|
|
|
|
|
|
getUiSlotCapability(slotType: PluginUiSlotType): PluginCapability;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Factory
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create a PluginCapabilityValidator.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This service enforces capability gates for plugin operations. The host
|
|
|
|
|
|
* uses it to verify that a plugin's declared capabilities permit the
|
|
|
|
|
|
* operation it is attempting, both at install time (manifest validation)
|
|
|
|
|
|
* and at runtime (bridge call gating).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Usage:
|
|
|
|
|
|
* ```ts
|
|
|
|
|
|
* const validator = pluginCapabilityValidator();
|
|
|
|
|
|
*
|
|
|
|
|
|
* // Runtime: gate a bridge call
|
|
|
|
|
|
* validator.assertOperation(plugin.manifestJson, "issues.create");
|
|
|
|
|
|
*
|
|
|
|
|
|
* // Install time: validate manifest consistency
|
|
|
|
|
|
* const result = validator.validateManifestCapabilities(manifest);
|
|
|
|
|
|
* if (!result.allowed) {
|
|
|
|
|
|
* throw badRequest("Missing capabilities", result.missing);
|
|
|
|
|
|
* }
|
|
|
|
|
|
* ```
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function pluginCapabilityValidator(): PluginCapabilityValidator {
|
|
|
|
|
|
const log = logger.child({ service: "plugin-capability-validator" });
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// Internal helpers
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function capabilitySet(manifest: PaperclipPluginManifestV1): Set<PluginCapability> {
|
|
|
|
|
|
return new Set(manifest.capabilities);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildForbiddenMessage(
|
|
|
|
|
|
manifest: PaperclipPluginManifestV1,
|
|
|
|
|
|
operation: string,
|
|
|
|
|
|
missing: PluginCapability[],
|
|
|
|
|
|
): string {
|
|
|
|
|
|
return (
|
|
|
|
|
|
`Plugin '${manifest.id}' is not allowed to perform '${operation}'. ` +
|
|
|
|
|
|
`Missing required capabilities: ${missing.join(", ")}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// Public API
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
hasCapability(manifest, capability) {
|
|
|
|
|
|
return manifest.capabilities.includes(capability);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
hasAllCapabilities(manifest, capabilities) {
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
const missing = capabilities.filter((cap) => !declared.has(cap));
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: missing.length === 0,
|
|
|
|
|
|
missing,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
hasAnyCapability(manifest, capabilities) {
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
return capabilities.some((cap) => declared.has(cap));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
checkOperation(manifest, operation) {
|
|
|
|
|
|
const required = OPERATION_CAPABILITIES[operation];
|
|
|
|
|
|
|
|
|
|
|
|
if (!required) {
|
|
|
|
|
|
log.warn(
|
|
|
|
|
|
{ pluginId: manifest.id, operation },
|
|
|
|
|
|
"capability check for unknown operation – rejecting by default",
|
|
|
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: false,
|
|
|
|
|
|
missing: [],
|
|
|
|
|
|
operation,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
const missing = required.filter((cap) => !declared.has(cap));
|
|
|
|
|
|
|
|
|
|
|
|
if (missing.length > 0) {
|
|
|
|
|
|
log.debug(
|
|
|
|
|
|
{ pluginId: manifest.id, operation, missing },
|
|
|
|
|
|
"capability check failed",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: missing.length === 0,
|
|
|
|
|
|
missing,
|
|
|
|
|
|
operation,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
assertOperation(manifest, operation) {
|
|
|
|
|
|
const result = this.checkOperation(manifest, operation);
|
|
|
|
|
|
if (!result.allowed) {
|
|
|
|
|
|
const msg = result.missing.length > 0
|
|
|
|
|
|
? buildForbiddenMessage(manifest, operation, result.missing)
|
|
|
|
|
|
: `Plugin '${manifest.id}' attempted unknown operation '${operation}'`;
|
|
|
|
|
|
throw forbidden(msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
assertCapability(manifest, capability) {
|
|
|
|
|
|
if (!this.hasCapability(manifest, capability)) {
|
|
|
|
|
|
throw forbidden(
|
|
|
|
|
|
`Plugin '${manifest.id}' lacks required capability '${capability}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
checkUiSlot(manifest, slotType) {
|
|
|
|
|
|
const required = UI_SLOT_CAPABILITIES[slotType];
|
|
|
|
|
|
if (!required) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: false,
|
|
|
|
|
|
missing: [],
|
|
|
|
|
|
operation: `ui.${slotType}.register`,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const has = manifest.capabilities.includes(required);
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: has,
|
|
|
|
|
|
missing: has ? [] : [required],
|
|
|
|
|
|
operation: `ui.${slotType}.register`,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
validateManifestCapabilities(manifest) {
|
|
|
|
|
|
const declared = capabilitySet(manifest);
|
|
|
|
|
|
const allMissing: PluginCapability[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// Check feature declarations → required capabilities
|
|
|
|
|
|
for (const [feature, requiredCap] of Object.entries(FEATURE_CAPABILITIES)) {
|
|
|
|
|
|
const featureValue = manifest[feature as keyof PaperclipPluginManifestV1];
|
|
|
|
|
|
if (Array.isArray(featureValue) && featureValue.length > 0) {
|
|
|
|
|
|
if (!declared.has(requiredCap)) {
|
|
|
|
|
|
allMissing.push(requiredCap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check UI slots → required capabilities
|
|
|
|
|
|
const uiSlots = manifest.ui?.slots ?? [];
|
|
|
|
|
|
if (uiSlots.length > 0) {
|
|
|
|
|
|
for (const slot of uiSlots) {
|
|
|
|
|
|
const requiredCap = UI_SLOT_CAPABILITIES[slot.type];
|
|
|
|
|
|
if (requiredCap && !declared.has(requiredCap)) {
|
|
|
|
|
|
if (!allMissing.includes(requiredCap)) {
|
|
|
|
|
|
allMissing.push(requiredCap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Check launcher declarations → required capabilities
|
|
|
|
|
|
const launchers = [
|
|
|
|
|
|
...(manifest.launchers ?? []),
|
|
|
|
|
|
...(manifest.ui?.launchers ?? []),
|
|
|
|
|
|
];
|
|
|
|
|
|
if (launchers.length > 0) {
|
|
|
|
|
|
for (const launcher of launchers) {
|
|
|
|
|
|
const requiredCap = LAUNCHER_PLACEMENT_CAPABILITIES[launcher.placementZone];
|
|
|
|
|
|
if (requiredCap && !declared.has(requiredCap) && !allMissing.includes(requiredCap)) {
|
|
|
|
|
|
allMissing.push(requiredCap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
allowed: allMissing.length === 0,
|
|
|
|
|
|
missing: allMissing,
|
|
|
|
|
|
pluginId: manifest.id,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getRequiredCapabilities(operation) {
|
|
|
|
|
|
return OPERATION_CAPABILITIES[operation] ?? [];
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getUiSlotCapability(slotType) {
|
|
|
|
|
|
return UI_SLOT_CAPABILITIES[slotType];
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|