Plugin System Scope Resolution — Audit & Fixes
Overview
The plugin system supports a 4-level settings hierarchy (work > user > admin > env > default) and scope-aware plugin resolution (work > user > autoEnable). The pipeline system correctly handles this via "bound facades" in step-pipeline-executor.service.ts, but direct service callers outside the pipeline were bypassing scoping by not passing userId/workId.
This document describes the issues found and the fixes applied.
Design Decision: Required userId
userId is required in BaseFacadeOptions (and all derived types like AiFacadeOptions, ScreenshotFacadeOptions). This means:
- All facade method calls (except CLI
testConnection) must includeuserId - The TypeScript compiler will catch any caller that forgets to pass scope
AiFacadeTestOptions(with optionaluserId) exists for CLI-only use (testConnection)- Facade service classes no longer
implementsthe plugin interfaces (IAiFacade,ISearchFacade, etc.) since the concrete services have richer signatures; the plugin interfaces are satisfied by the pipeline-bound facades instead
Issues Fixed
Issue 1 (CRITICAL): AwesomeReadmeParserService — scope not threaded
Problem: The parseReadme() method's facadeOptions parameter was threaded through correctly, but the syncFromAwesomeReadme() caller in WorkImportService was not passing scope.
Fix: work-import.service.ts — Pass { userId: user.id, workId: work.id } to parseReadme() in the sync flow.
Files modified:
packages/agent/src/services/work-import.service.ts
Issue 2 (CRITICAL): WorkDetailService — askJson missing userId
Problem: When generating work details via AI, the user's AI provider preference and API key were ignored because userId was not passed to askJson().
Fix: Added { userId: user.id } as the 4th argument to askJson(). No workId is available at this point (work not yet created), which is correct.
File modified:
packages/agent/src/services/work-detail.service.ts
Issue 3 (MEDIUM): listPluginModels ignores user credentials
Problem: The model dropdown in plugin settings used plugin.listModels() directly, bypassing the settings hierarchy. Users who configured their own API keys would see models from admin/env credentials instead.
Fix: Replaced direct plugin.listModels() call with aiFacade.getAvailableModels({ providerOverride: pluginId, userId }), which resolves credentials through the full settings hierarchy.
Files modified:
apps/api/src/plugins/plugins.service.ts— InjectAiFacadeService, delegate to facadeapps/api/src/plugins/plugins.module.ts— ImportFacadesModule
Issue 4 (MEDIUM): Work plugin endpoints lack ownership checks
Problem: Any authenticated user who knew a workId could enable/disable plugins and change settings for any work — a security vulnerability.
Fix: Added WorkOwnershipService injection to PluginsController and added ownership checks to all work-scoped endpoints:
| Route | Guard |
|---|---|
GET /api/works/:workId/plugins | ensureCanView |
POST .../plugins/:pluginId/enable | ensureCanEdit |
POST .../plugins/:pluginId/disable | ensureCanEdit |
PATCH .../plugins/:pluginId/settings | ensureCanEdit |
POST .../plugins/:pluginId/capability | ensureCanEdit |
Files modified:
apps/api/src/plugins/plugins.controller.ts— Inject service, add checksapps/api/src/plugins/plugins.module.ts— ImportWorkModule
Issue 5 (MEDIUM): configurationMode not enforced in API layer
Problem: admin-only plugins could have their settings modified by regular users through the API. The agent-level PluginSettingsService enforces this, but the API layer bypassed it with direct TypeORM writes.
Fix: Added enforceConfigurationMode() check in:
enablePluginForUser()— when settings are providedupdateUserPluginSettings()— when settings are providedenablePluginForWork()— when settings are providedupdateWorkPluginSettings()— when settings are provided
File modified:
apps/api/src/plugins/plugins.service.ts
Issue 6 (LOW): ContentExtractorFacade type-safety gap
Problem: extractContent(url, options?) accepted FacadeExtractionOptions which doesn't include userId/workId. The compiler wouldn't flag missing scope for future direct callers.
Fix: Added explicit facadeOptions: BaseFacadeOptions as required 3rd parameter. Updated pipeline binding to use new parameter. Removed legacy ExtendedFacadeExtractionOptions type and casting. Removed implements IContentExtractorFacade since the concrete service now has a richer signature.
Files modified:
packages/agent/src/facades/content-extractor.facade.tspackages/agent/src/pipeline/step-pipeline-executor.service.ts
Issue 7 (LOW): SearchFacade type-safety gap
Problem: Same pattern as Issue 6 — scope passed through extended options casting.
Fix: Added explicit facadeOptions: BaseFacadeOptions as required 3rd parameter. Updated pipeline binding. Removed legacy ExtendedSearchFacadeOptions type and casting. Removed implements ISearchFacade.
Files modified:
packages/agent/src/facades/search.facade.tspackages/agent/src/pipeline/step-pipeline-executor.service.ts
Issue 8 (LOW): getDefaultProvider work check ignores user scope
Problem: getDefaultProvider(workId?, userId?) found active plugins by work capability but only checked global registered.state === 'enabled' without verifying user-scoped enable/disable state.
Fix: Added isPluginEnabled(pluginId, workId, userId) check after finding active work plugin.
File modified:
packages/agent/src/facades/base.facade.ts
Verification
pnpm build— 24/24 tasks successful, 0 type errorspnpm testinpackages/agent— 26 suites, 722 tests passing- Plugin tests — all passing (openai: 16, openrouter: 29, screenshotone: 35, vercel: 33, default-pipeline: 277)
- Manual: Import an awesome readme with user-specific AI key — verify user's key is used
- Manual: Attempt work plugin endpoint as non-owner — should get 403
- Manual: Configure user-specific API key — list models — should use user key