Context
WorkspaceServerPlugin.extensionFactories was dropped in commit 02dc4b81 (PR feat/plugin-agent-layer-rebased-main) because no v1 plugin exercised the only capability it uniquely enabled. Plugins today register tools via WorkspaceServerPlugin.agentTools and ship Pi extensions as files via WorkspaceServerPlugin.extensionPaths.
What was dropped
A plugin could provide an in-process function (api) => void that Pi invoked with its extension API at agent boot. Unlike file-based extensionPaths (which Pi loads with no runtime context), the factory variant let plugins close over host runtime state — bridge instance, DB pool, fastify-scoped logger, etc.
The use case that justifies bringing it back
Pi's extension API exposes hooks beyond registerTool:
api.on('before_agent_start', (event) => { event.systemPrompt = ... }) — mutate system prompt with live runtime state (different from `pi.systemPrompt` which is static, or `WorkspaceServerPlugin.systemPrompt` which is host-aggregated at boot).
api.on('before_tool_call' | 'after_tool_call', ...) — observe/mutate tool calls (e.g. policy gating, redaction, structured audit logging).
api.registerSkill(...) / api.registerResource(...) — dynamic Pi skill/resource registration not expressible via static pi.skills directories.
A concrete scenario: an org plugin needs to gate `execute_sql` against per-user permissions held in the host's DB pool. The hook needs both Pi's tool-call event AND the host's runtime pool reference. File-based extensions can only reach the pool via a module-singleton hack; an in-process factory closes over it cleanly:
```ts
// Hypothetical v2 — NOT in v1 today
extensionFactories: [(api) => {
api.on('before_tool_call', async (tool) => {
if (tool.name === 'execute_sql') {
const ok = await dbPool.checkPermission(bridge.currentUser, tool.args.query)
if (!ok) throw new Error('blocked by policy')
}
})
}]
```
Acceptance for re-adding
Before bringing the field back, the new design should:
- Have at least one real plugin in this repo (or a documented downstream user) exercising a non-tool Pi hook (`api.on(...)`, `api.registerResource(...)`).
- Include tests that exercise the hook path end-to-end — not just "the array is passed through."
- Either clearly distinguish `agentTools` (boring-side) from `extensionFactories` (Pi-side hooks) in DESIGN.md, or unify them under one mechanism so plugin authors don't have to choose between two ways to register a tool.
Until those conditions are met, the field stays dropped. v1 plugins use `agentTools` for tools and `extensionPaths` for file-based Pi extensions.
Related
- DESIGN.md §11 (Deferred post-v1)
- Commit `02dc4b81`
- Host-level `WorkspaceAgentPiOptions.pi.extensionFactories` is unaffected — hosts (e.g. `@hachej/boring-core`) can still inject their own.
Context
WorkspaceServerPlugin.extensionFactorieswas dropped in commit02dc4b81(PRfeat/plugin-agent-layer-rebased-main) because no v1 plugin exercised the only capability it uniquely enabled. Plugins today register tools viaWorkspaceServerPlugin.agentToolsand ship Pi extensions as files viaWorkspaceServerPlugin.extensionPaths.What was dropped
A plugin could provide an in-process function
(api) => voidthat Pi invoked with its extension API at agent boot. Unlike file-basedextensionPaths(which Pi loads with no runtime context), the factory variant let plugins close over host runtime state — bridge instance, DB pool, fastify-scoped logger, etc.The use case that justifies bringing it back
Pi's extension API exposes hooks beyond
registerTool:api.on('before_agent_start', (event) => { event.systemPrompt = ... })— mutate system prompt with live runtime state (different from `pi.systemPrompt` which is static, or `WorkspaceServerPlugin.systemPrompt` which is host-aggregated at boot).api.on('before_tool_call' | 'after_tool_call', ...)— observe/mutate tool calls (e.g. policy gating, redaction, structured audit logging).api.registerSkill(...)/api.registerResource(...)— dynamic Pi skill/resource registration not expressible via staticpi.skillsdirectories.A concrete scenario: an org plugin needs to gate `execute_sql` against per-user permissions held in the host's DB pool. The hook needs both Pi's tool-call event AND the host's runtime pool reference. File-based extensions can only reach the pool via a module-singleton hack; an in-process factory closes over it cleanly:
```ts
// Hypothetical v2 — NOT in v1 today
extensionFactories: [(api) => {
api.on('before_tool_call', async (tool) => {
if (tool.name === 'execute_sql') {
const ok = await dbPool.checkPermission(bridge.currentUser, tool.args.query)
if (!ok) throw new Error('blocked by policy')
}
})
}]
```
Acceptance for re-adding
Before bringing the field back, the new design should:
Until those conditions are met, the field stays dropped. v1 plugins use `agentTools` for tools and `extensionPaths` for file-based Pi extensions.
Related