From 3b2c35c9c4007f74dc19dd18451acce414f945ab Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 4 Jun 2026 01:44:08 +0900 Subject: [PATCH] task: add plugin pod api design --- .../artifacts/pod-api-design.md | 462 +++++++++++++++++ .../item.md | 2 +- .../thread.md | 470 ++++++++++++++++++ 3 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 work-items/open/20260603-122317-plugin-feature-contribution-registry/artifacts/pod-api-design.md diff --git a/work-items/open/20260603-122317-plugin-feature-contribution-registry/artifacts/pod-api-design.md b/work-items/open/20260603-122317-plugin-feature-contribution-registry/artifacts/pod-api-design.md new file mode 100644 index 00000000..15f6b642 --- /dev/null +++ b/work-items/open/20260603-122317-plugin-feature-contribution-registry/artifacts/pod-api-design.md @@ -0,0 +1,462 @@ +# Public Pod-side API for Feature / Plugin Contributions + +## 1. Summary recommendation + +Introduce a `pod::feature` public API as the single Pod-side registration layer for built-in features and future external plugins. A feature module should declare its identity, requested capabilities, and contributions, then install those contributions only through typed host registrars for existing Pod/Worker surfaces: `ToolRegistry`, the hardened safe `pod::hook` surface, and host-owned notification/event/history append paths. + +The registry should not become a second runtime, a plugin dispatcher tool, or a generic `Pod` mutation escape hatch. Feature state remains inside the feature module; the Pod owns only install metadata, diagnostics, granted host handles, and normal durable session/runtime surfaces. + +Recommended placement: create `crates/pod/src/feature.rs` (or `crates/pod/src/feature/mod.rs` once it grows) and export it as `pod::feature`. Keep `llm-worker::Interceptor` internal; expose only hardened `pod::hook` types and contribution registrars. + +## 2. Current relevant Pod/Worker surfaces + +The design should build on these existing surfaces rather than bypassing them: + +- `crates/pod/src/hook.rs` + - Current public-ish hook layer wraps `llm_worker::Interceptor` with `HookRegistry`, `HookRegistryBuilder`, `Hook`, and per-event hook traits. + - It already provides Pod-specific hook events such as pre-request, post-assistant, pre-tool-call, post-tool-call, and turn-end. + - It is not yet safe enough as a public plugin API because some hook actions can carry raw `llm_worker::Item` values (`PreRequestAction::ContinueWith`, `TurnEndAction::ContinueWithMessages`). The feature API must depend on the post-hardening surface, not these raw item mutation forms. + +- `crates/pod/src/ipc/interceptor.rs` + - `PodInterceptor` is the bridge between Worker callbacks and Pod behavior. + - It runs hooks, drains pending attachments/notifications, records memory/tool usage, and turns model-visible additions into committed `SystemItem` session log entries before appending them to Worker history. + - This is the right place for host-mediated durable append paths; it is not a plugin API itself. + +- `crates/pod/src/controller.rs` + - Controller startup currently registers built-in Pod tools through ad hoc code paths. + - The feature registry should replace those ad hoc registrations incrementally by installing contributions into the same worker/tool/hook surfaces during Pod construction. + +- `crates/pod/src/pod.rs` + - `Pod` owns the durable session log, metadata, runtime event channel, notification helpers, pending system attachments, scope, and Worker lifecycle. + - It exposes internal methods that can append history or send alerts/events. The public feature API should not expose `Pod` or `Worker` directly; it should expose narrow sinks that route through these existing methods. + +- `crates/pod/src/permission.rs` + - Manifest tool permissions are enforced as a `PreToolCallHook`. + - Feature tools must remain subject to the same PreToolCall permission path. Feature capability grants do not replace per-call tool permission. + +- `crates/llm-worker/src/tool.rs` and `crates/llm-worker/src/tool_server.rs` + - `ToolDefinition`, `Tool`, `ToolMeta`, `ToolResult`, `ToolOutput`, and `ToolServerHandle` define the normal tool execution path. + - Tools registered here get normal schema exposure, execution, bounded output handling, and history result recording. + - The public feature API should register `ToolDefinition`s into this registry rather than introducing a separate plugin dispatch layer. + +- `crates/llm-worker/src/interceptor.rs` + - The lower-level interceptor is powerful and Worker-oriented. It should remain internal because it can influence model request construction too directly. + - Public features should use `pod::hook` only after that API has been narrowed to durable, auditable actions. + +- `crates/tools/src/lib.rs` + - Existing built-in tools already use shared tool abstractions and scoped filesystem/runtime handles. + - Those tool constructors can become built-in feature contributions without changing model-visible tool names. + +- `crates/pod/src/workflow/mod.rs` + - Workflow invocation currently resolves user input segments into system items through the Pod's durable attachment path. + - This is a useful pattern for feature-owned model-visible additions: resolve through a host-owned append path and commit what the model sees. It should not become a general plugin context injection mechanism. + +## 3. Proposed public API shape + +### Types/modules + +Add a new module under `pod`: + +```rust +pub mod feature { + pub mod capability; + pub mod diagnostic; + pub mod event; + pub mod hook; + pub mod registry; + pub mod tool; + + pub use capability::{CapabilityGrantSet, CapabilityRequest, HostCapability}; + pub use diagnostic::{FeatureDiagnostic, FeatureInstallReport}; + pub use registry::{FeatureDescriptor, FeatureId, FeatureInstallContext, FeatureModule, FeatureRegistryBuilder, FeatureRuntimeKind}; + pub use tool::ToolContribution; +} +``` + +Core trait and registry shape: + +```rust +pub trait FeatureModule: Send + Sync + 'static { + fn descriptor(&self) -> FeatureDescriptor; + + fn install(&self, ctx: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError>; +} + +pub struct FeatureDescriptor { + pub id: FeatureId, // source-qualified identity, e.g. builtin:task + pub display_name: String, + pub version: Option, + pub runtime: FeatureRuntimeKind, // Builtin, ExternalProcess, McpBridge, WasmPlaceholder, DeclarativePlaceholder + pub requested_capabilities: Vec, + pub declared_tools: Vec, + pub declared_hooks: Vec, + pub declared_event_channels: Vec, +} + +pub enum FeatureRuntimeKind { + Builtin, + ExternalProcess, + McpBridge, + WasmPlaceholder, + DeclarativePlaceholder, +} + +pub struct FeatureInstallContext<'a> { + // No Pod or Worker reference. + pub feature_id: &'a FeatureId, + pub grants: &'a CapabilityGrantSet, + pub tools: ToolRegistrar<'a>, + pub hooks: PublicHookRegistrar<'a>, + pub notify: FeatureNotifySink<'a>, + pub events: FeatureEventSink<'a>, + pub diagnostics: FeatureDiagnosticSink<'a>, + pub services: FeatureServiceProvider<'a>, +} +``` + +Important details: + +- `FeatureDescriptor` is declarative and serializable. It is safe to show in diagnostics, profile previews, and `ListFeatures`-style future tooling. +- `FeatureModule::install` is runtime code that wires stateful tool/hook implementations into host registrars. +- `FeatureInstallContext` must not expose `Pod`, `Worker`, raw `ToolServerHandle`, raw `Interceptor`, raw `NotifyBuffer`, raw `LogWriter`, raw `event_tx`, or direct history mutation. +- `FeatureServiceProvider` returns only host services backed by granted capabilities, for example scoped filesystem access, WorkItem store access, memory access, Pod orchestration handles, web provider handles, or secret references. It should return `Denied`/`Unavailable` diagnostics instead of exposing partial internals. + +### Example registration snippet + +This is illustrative shape, not proposed final exact Rust syntax: + +```rust +use pod::feature::{ + CapabilityRequest, FeatureDescriptor, FeatureId, FeatureInstallContext, + FeatureModule, FeatureRuntimeKind, HostCapability, ToolContribution, +}; + +pub struct WorkItemFeature { + state: std::sync::Arc, +} + +impl FeatureModule for WorkItemFeature { + fn descriptor(&self) -> FeatureDescriptor { + FeatureDescriptor::builder(FeatureId::builtin("work-item")) + .display_name("WorkItem intake and routing") + .runtime(FeatureRuntimeKind::Builtin) + .request(CapabilityRequest::required( + HostCapability::WorkItemStore { read: true, write: true }, + "create and update WorkItem records through host-owned ticket storage", + )) + .request(CapabilityRequest::optional( + HostCapability::EmitUserEvent, + "surface routing diagnostics to the TUI/actionbar", + )) + .tool("WorkItemCreate") + .tool("WorkItemComment") + .hook("work_item_intake_pre_tool_audit", pod::hook::HookPoint::PreToolCall) + .event_channel("work-item") + .build() + } + + fn install(&self, ctx: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { + let store = ctx.services.work_item_store()?; + + ctx.tools.register(ToolContribution::new( + "WorkItemCreate", + work_item_create_tool(store.clone(), self.state.clone()), + ))?; + + ctx.hooks.pre_tool_call( + "work_item_intake_pre_tool_audit", + WorkItemAuditHook::new(self.state.clone()), + )?; + + ctx.events.declare_channel("work-item")?; + Ok(()) + } +} +``` + +The feature keeps `WorkItemFeatureState`. The Pod keeps only registration records, diagnostics, and the normal host services it already owns. + +### Tool contribution + +A tool contribution should be a thin wrapper around `llm_worker::ToolDefinition` plus feature metadata: + +```rust +pub struct ToolContribution { + pub feature_id: FeatureId, + pub name: ToolName, + pub definition: llm_worker::ToolDefinition, + pub required_capabilities: Vec, +} +``` + +Rules: + +- Register into the existing `ToolRegistry` / `ToolServerHandle`; do not add a plugin-dispatcher tool that multiplexes plugin calls outside normal tool history. +- Preserve normal `PreToolCall` permission evaluation, tool-call history, result history, output truncation/bounding, and diagnostic behavior. +- Host-controlled feature enablement decides whether a contributed tool is installed. Manifest/profile tool permission still decides whether a model may call it at runtime. +- Duplicate tool names should be rejected during feature registry preflight with a diagnostic, not discovered later through a panic or undefined ordering. +- Public feature identity should be source-qualified (`builtin:memory`, `project:foo`, `plugin::bar`), while model-visible tool names should remain explicit stable names. Do not auto-prefix model tool names unless the project deliberately chooses a future namespacing policy. +- Tool schemas/descriptions must be part of the normal `ToolDefinition` path so model-visible surfaces remain inspectable and bounded. +- If a required host service is not granted or configured, the tool should not be registered; the install report should explain the skipped contribution. + +### Hook contribution + +Hook contribution must depend on the safe hook surface produced by `hook-public-surface-hardening`. + +Recommended public hook principles: + +- Public hooks register through `PublicHookRegistrar`, which wraps `HookRegistryBuilder` but exposes only hardened hook traits/actions. +- Public hooks receive snapshots/views, not mutable Pod/Worker handles. +- Public hook return values should be decisions such as continue, deny/rewrite a tool decision through a host-defined synthetic result path, emit diagnostics, or request a durable notification/history append through a host sink. They should not return raw `llm_worker::Item` vectors. +- Public hooks must not be able to mutate request context, session history, or Worker state invisibly. +- Permission enforcement hooks remain host/internal and run before feature hooks for `PreToolCall` so a feature cannot approve a denied tool call. +- Hook ordering should be explicit and stable: internal safety hooks first, public feature hooks in registry order or declared priority bands, internal usage/accounting hooks where needed. Priority should be coarse, not arbitrary integer ordering that lets plugins fight for precedence. + +Possible hardened hook action shape: + +```rust +pub enum PublicPreToolCallDecision { + Continue, + DenyWithSyntheticError { message: String }, + EmitDiagnostic { diagnostic: FeatureDiagnostic }, +} + +pub trait PublicPreToolCallHook: Send + Sync { + fn on_pre_tool_call(&self, event: PublicPreToolCallEvent<'_>) -> PublicPreToolCallDecision; +} +``` + +If a hook needs to add model-visible text, it should use `FeatureNotifySink::notify_model(...)` or another host-owned durable append API, not return an `Item`. + +### Notification/event contribution + +Expose two distinct sinks: + +```rust +pub struct FeatureNotifySink<'a> { /* host-owned */ } +pub struct FeatureEventSink<'a> { /* host-owned */ } +``` + +Recommended behavior: + +- `FeatureNotifySink::notify_model(...)` creates a model-visible notification through the existing durable notification/system-item path. The host commits the corresponding `SystemItem` before it is appended to Worker history. +- `FeatureNotifySink::notify_user(...)` or `FeatureEventSink::emit(...)` creates user-visible diagnostics/progress/action events through the existing alert/event path. These are not model-visible unless explicitly routed through `notify_model`. +- Event payloads should be typed, bounded, and feature-identified. Avoid arbitrary JSON blobs as the first public API; allow an opaque bounded metadata field only if diagnostics require it. +- Notifications and events should require explicit capabilities such as `EmitModelNotification` and `EmitUserEvent`. +- Background feature tasks must use these sinks; they must not hold raw log writers or append directly to history. + +Useful initial event shape: + +```rust +pub struct FeatureEvent { + pub feature_id: FeatureId, + pub level: FeatureEventLevel, // Info, Warn, Error + pub channel: String, // e.g. "work-item" + pub summary: String, + pub detail: Option, + pub model_visible: bool, // false unless host routes through notify_model +} +``` + +`model_visible` should be host-controlled in practice: a feature may request model visibility, but the sink decides whether that capability is granted and records the durable append if it is. + +### Capability request/grant/diagnostics + +Capabilities are requested by descriptors and granted by the host. A feature may request a capability, but it must not assume the capability exists. + +Initial capability categories: + +```rust +pub enum HostCapability { + ContributeTool { name: ToolName }, + ContributeHook { point: pod::hook::HookPoint }, + EmitUserEvent, + EmitModelNotification, + ScopedFs { read: bool, write: bool, execute: bool }, + WorkItemStore { read: bool, write: bool }, + MemoryStore { read: bool, write: bool }, + PodManagement { spawn: bool, message: bool, restore: bool }, + Network { purpose: NetworkPurpose }, + SecretRef { id: String }, +} +``` + +Important separation: + +- Capability grants decide whether a feature may install and receive host services. +- Tool permissions decide whether an installed tool call may execute for a specific Pod/run. +- Scope permissions decide which filesystem paths or delegated Pod capabilities a host service may touch. + +Diagnostics should be first-class: + +```rust +pub struct FeatureInstallReport { + pub feature_id: FeatureId, + pub enabled: bool, + pub granted: Vec, + pub denied: Vec, + pub installed_tools: Vec, + pub installed_hooks: Vec, + pub skipped_contributions: Vec, + pub diagnostics: Vec, +} +``` + +Diagnostics must avoid secrets and must be safe for session logs, TUI display, and future `ListFeatures`/profile validation output. + +## 4. State ownership model + +Feature state belongs to the feature module. + +- A feature may own `Arc` and clone it into contributed tools, hooks, and background tasks. +- The Pod registry stores descriptors, install reports, enabled/disabled status, and host-owned handles. It does not store feature business state. +- Durable feature data must live in a feature-owned or host-granted store with an explicit API: WorkItem files through a WorkItem service, memory records through memory APIs, plugin config/state through a future plugin-state service, etc. +- Session history is not feature storage. It is an audit/replay record of model-visible interactions and host-visible events. +- A feature that needs restoration after process restart should reconstruct itself from its own durable store/config plus normal Pod metadata, not from private data hidden in Worker context. +- Background tasks are allowed only if they communicate through granted sinks/services and have a defined shutdown/lifecycle policy owned by the host. + +This model lets built-ins and plugins share the same contribution shape while keeping Pod runtime ownership clear. + +## 5. Safety invariants / forbidden operations + +Public features/plugins must not be able to perform these operations: + +- Mutate prompt context directly. +- Append, remove, reorder, or rewrite Worker history directly. +- Insert model-visible text that is not committed through a durable host path. +- Return raw `llm_worker::Item` values from public hooks. +- Access raw `Worker`, raw `Pod`, raw `ToolServerHandle`, raw `llm_worker::Interceptor`, raw `NotifyBuffer`, raw session log writer, or raw event sender. +- Register tools outside `ToolRegistry` or bypass normal tool-result history recording. +- Bypass `PreToolCall` permission policy. +- Grant themselves capabilities or infer grants from successful construction. +- Mutate manifest/profile/scope state directly. +- Perform filesystem/process/network/secret access outside granted host services. +- Emit unbounded tool outputs, event payloads, diagnostics, or notification bodies. +- Put secrets into diagnostics, session logs, model context, TUI output, or feature install reports. +- Depend on MCP/WASM/package-distribution mechanics in the base Pod API. + +Positive invariant: if the model can see a feature-produced fact, a future replay/resume must have a durable explanation for why that fact was present. + +## 6. Placement and crate-boundary recommendation + +Recommended placement: + +- `crates/pod/src/feature.rs` or `crates/pod/src/feature/mod.rs` + - public feature traits/types + - feature registry builder + - install reports/diagnostics + - capability request/grant model + - typed registrars/sinks + +- `crates/pod/src/hook.rs` + - remains the public hook module after hardening + - should expose safe Pod-level hook traits/actions only + - should not re-export `llm_worker::Interceptor` power + +- `crates/llm-worker` + - remains owner of generic LLM tools/interceptors/history machinery + - should not depend on `pod::feature` + +- `crates/tools` + - remains a source of reusable tool implementations + - built-in feature modules in `pod` can wrap these constructors into `ToolContribution`s + +- Future external plugin crates/processes + - should adapt into `FeatureDescriptor` + `FeatureModule` or a host-side adapter that produces equivalent contributions + - should not be called directly by the Pod except through the registry/registrars + +Install location in Pod startup: + +1. Resolve manifest/profile and host capability policy. +2. Construct `Pod` and internal safety surfaces. +3. Install host/internal hooks such as manifest permission enforcement. +4. Build and install enabled feature modules through `FeatureRegistryBuilder`. +5. Flush/register tools through the existing Worker tool registry. +6. Freeze/install the Pod interceptor and start normal run/attach behavior. + +The exact sequencing can be adjusted to match current construction, but the invariant should hold: public feature hooks cannot precede host safety hooks, and feature tools must exist before the model receives the final tool schema for a run. + +## 7. Migration path from current built-in registrations + +Recommended migration is incremental and behavior-preserving: + +1. Land hook public-surface hardening first. + - Remove/replace public raw `Item`-carrying hook actions. + - Define which hook decisions are safe for external contributors. + +2. Add `pod::feature` with no behavior change. + - Implement descriptors, capability grants, install reports, and registrars. + - Initially register no external plugins. + +3. Wrap current built-in tool registration as built-in feature modules. + - Start with a small built-in feature whose state/services are already cleanly bounded. + - Preserve existing tool names, schemas, and permission behavior. + - Convert duplicate-name failures into registry diagnostics before flushing tools. + +4. Move larger built-in groups behind feature modules. + - Filesystem/process tools from `crates/tools`. + - Memory tools. + - Pod orchestration tools. + - Task/WorkItem tools once their stores and hooks have explicit capabilities. + - Web tools as configured provider-backed features. + +5. Move built-in hook contributions only after safe hook semantics are stable. + - Keep manifest permission enforcement as an internal host hook, not a feature hook. + - Keep accounting/usage hooks internal unless they become genuine feature behavior. + +6. Treat workflow/user-input expansion separately. + - Workflow invocation already uses a durable system-item attachment pattern. + - Do not expose arbitrary workflow-like context injection to plugins until there is a safe typed command/input-contribution API with durable append semantics. + +7. Add profile/manifest enablement after built-ins work through the same registry. + - Built-ins and external plugins should share descriptor/capability/install-report mechanics. + - Host policy may grant built-ins by default, but built-ins should still declare what they use. + +## 8. Impact on WorkItem / MCP / plugin distribution follow-ups + +WorkItem / intake routing: + +- WorkItem routing can become a built-in feature that contributes WorkItem tools, optional routing hooks, and user-visible action events. +- It should request `WorkItemStore` and event/notification capabilities instead of reaching into ticket files ad hoc. +- Model-visible routing hints or intake results must be committed through notification/history append paths. +- This registry gives the WorkItem feature a clean way to install without making WorkItem a special Pod runtime mode. + +MCP: + +- MCP should be an adapter/runtime kind that produces normal `ToolContribution`s and possibly safe event diagnostics. +- MCP tool calls must still pass through `ToolRegistry`, PreToolCall permission, output bounding, and history result recording. +- MCP resources/prompts should not become invisible prompt injection. If exposed later, they should be explicit tools, user-invoked attachments, or durable notification/history appends. +- MCP transport/session details are out of scope for the base API beyond the `FeatureRuntimeKind::McpBridge` placeholder. + +Plugin distribution: + +- Archive validation, cache extraction, signing/trust, WASM execution, external process supervision, and package update policy should remain separate follow-up designs. +- Distribution mechanisms should eventually produce the same descriptor/capability/contribution objects as built-ins. +- Capability grants are the host trust boundary; package installation alone must not grant runtime authority. + +## 9. Open questions / risks + +1. Tool naming policy is the highest-risk API decision. + - Recommendation: feature identities are source-qualified, model-visible tool names stay explicit and stable, and collisions are rejected by the host. + - Risk: external plugins may need namespacing later. Auto-prefixing now would avoid collisions but would also change model-facing ergonomics and diverge from current built-in tool names. + +2. The exact safe hook action set must be settled by `hook-public-surface-hardening`. + - Especially important: whether public pre-tool hooks may synthesize denials/results, and how durable append requests are represented. + +3. Notification/event durability needs precise semantics. + - User-visible events may be live-only, while model-visible notifications must be durable. The public API should make this distinction impossible to miss. + +4. Capability granularity can easily become either too coarse or too noisy. + - Start with coarse host-service capabilities plus normal tool permissions, then split only when real features need finer grants. + +5. Runtime enable/disable is not designed here. + - Initial registry should be install-at-startup. Hot reload or dynamic plugin enablement needs separate lifecycle, cleanup, and schema-refresh design. + +6. Persistent plugin state needs a future host service. + - The base API says state is feature-owned, but external plugins will still need a sanctioned durable state directory/store with migration/versioning rules. + +7. Background tasks need lifecycle policy. + - If external plugins can spawn tasks, the host must define shutdown, cancellation, panic handling, diagnostic routing, and whether task output may become model-visible. + +8. Existing workflow/input expansion is close to the forbidden boundary. + - It is safe only because it commits system items before model visibility. Any future plugin command/input contribution must preserve that durable replay property. diff --git a/work-items/open/20260603-122317-plugin-feature-contribution-registry/item.md b/work-items/open/20260603-122317-plugin-feature-contribution-registry/item.md index b9f74eb5..7d2acad8 100644 --- a/work-items/open/20260603-122317-plugin-feature-contribution-registry/item.md +++ b/work-items/open/20260603-122317-plugin-feature-contribution-registry/item.md @@ -7,7 +7,7 @@ kind: feature priority: P1 labels: [plugin, registry, tools, hooks, orchestration] created_at: 2026-06-03T12:23:17Z -updated_at: 2026-06-03T16:38:46Z +updated_at: 2026-06-03T16:44:05Z assignee: null legacy_ticket: null --- diff --git a/work-items/open/20260603-122317-plugin-feature-contribution-registry/thread.md b/work-items/open/20260603-122317-plugin-feature-contribution-registry/thread.md index 7769dba3..85ebe392 100644 --- a/work-items/open/20260603-122317-plugin-feature-contribution-registry/thread.md +++ b/work-items/open/20260603-122317-plugin-feature-contribution-registry/thread.md @@ -118,4 +118,474 @@ Report: - any blockers that require parent/user decision +--- + + + +## Plan + +# Public Pod-side API for Feature / Plugin Contributions + +## 1. Summary recommendation + +Introduce a `pod::feature` public API as the single Pod-side registration layer for built-in features and future external plugins. A feature module should declare its identity, requested capabilities, and contributions, then install those contributions only through typed host registrars for existing Pod/Worker surfaces: `ToolRegistry`, the hardened safe `pod::hook` surface, and host-owned notification/event/history append paths. + +The registry should not become a second runtime, a plugin dispatcher tool, or a generic `Pod` mutation escape hatch. Feature state remains inside the feature module; the Pod owns only install metadata, diagnostics, granted host handles, and normal durable session/runtime surfaces. + +Recommended placement: create `crates/pod/src/feature.rs` (or `crates/pod/src/feature/mod.rs` once it grows) and export it as `pod::feature`. Keep `llm-worker::Interceptor` internal; expose only hardened `pod::hook` types and contribution registrars. + +## 2. Current relevant Pod/Worker surfaces + +The design should build on these existing surfaces rather than bypassing them: + +- `crates/pod/src/hook.rs` + - Current public-ish hook layer wraps `llm_worker::Interceptor` with `HookRegistry`, `HookRegistryBuilder`, `Hook`, and per-event hook traits. + - It already provides Pod-specific hook events such as pre-request, post-assistant, pre-tool-call, post-tool-call, and turn-end. + - It is not yet safe enough as a public plugin API because some hook actions can carry raw `llm_worker::Item` values (`PreRequestAction::ContinueWith`, `TurnEndAction::ContinueWithMessages`). The feature API must depend on the post-hardening surface, not these raw item mutation forms. + +- `crates/pod/src/ipc/interceptor.rs` + - `PodInterceptor` is the bridge between Worker callbacks and Pod behavior. + - It runs hooks, drains pending attachments/notifications, records memory/tool usage, and turns model-visible additions into committed `SystemItem` session log entries before appending them to Worker history. + - This is the right place for host-mediated durable append paths; it is not a plugin API itself. + +- `crates/pod/src/controller.rs` + - Controller startup currently registers built-in Pod tools through ad hoc code paths. + - The feature registry should replace those ad hoc registrations incrementally by installing contributions into the same worker/tool/hook surfaces during Pod construction. + +- `crates/pod/src/pod.rs` + - `Pod` owns the durable session log, metadata, runtime event channel, notification helpers, pending system attachments, scope, and Worker lifecycle. + - It exposes internal methods that can append history or send alerts/events. The public feature API should not expose `Pod` or `Worker` directly; it should expose narrow sinks that route through these existing methods. + +- `crates/pod/src/permission.rs` + - Manifest tool permissions are enforced as a `PreToolCallHook`. + - Feature tools must remain subject to the same PreToolCall permission path. Feature capability grants do not replace per-call tool permission. + +- `crates/llm-worker/src/tool.rs` and `crates/llm-worker/src/tool_server.rs` + - `ToolDefinition`, `Tool`, `ToolMeta`, `ToolResult`, `ToolOutput`, and `ToolServerHandle` define the normal tool execution path. + - Tools registered here get normal schema exposure, execution, bounded output handling, and history result recording. + - The public feature API should register `ToolDefinition`s into this registry rather than introducing a separate plugin dispatch layer. + +- `crates/llm-worker/src/interceptor.rs` + - The lower-level interceptor is powerful and Worker-oriented. It should remain internal because it can influence model request construction too directly. + - Public features should use `pod::hook` only after that API has been narrowed to durable, auditable actions. + +- `crates/tools/src/lib.rs` + - Existing built-in tools already use shared tool abstractions and scoped filesystem/runtime handles. + - Those tool constructors can become built-in feature contributions without changing model-visible tool names. + +- `crates/pod/src/workflow/mod.rs` + - Workflow invocation currently resolves user input segments into system items through the Pod's durable attachment path. + - This is a useful pattern for feature-owned model-visible additions: resolve through a host-owned append path and commit what the model sees. It should not become a general plugin context injection mechanism. + +## 3. Proposed public API shape + +### Types/modules + +Add a new module under `pod`: + +```rust +pub mod feature { + pub mod capability; + pub mod diagnostic; + pub mod event; + pub mod hook; + pub mod registry; + pub mod tool; + + pub use capability::{CapabilityGrantSet, CapabilityRequest, HostCapability}; + pub use diagnostic::{FeatureDiagnostic, FeatureInstallReport}; + pub use registry::{FeatureDescriptor, FeatureId, FeatureInstallContext, FeatureModule, FeatureRegistryBuilder, FeatureRuntimeKind}; + pub use tool::ToolContribution; +} +``` + +Core trait and registry shape: + +```rust +pub trait FeatureModule: Send + Sync + 'static { + fn descriptor(&self) -> FeatureDescriptor; + + fn install(&self, ctx: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError>; +} + +pub struct FeatureDescriptor { + pub id: FeatureId, // source-qualified identity, e.g. builtin:task + pub display_name: String, + pub version: Option, + pub runtime: FeatureRuntimeKind, // Builtin, ExternalProcess, McpBridge, WasmPlaceholder, DeclarativePlaceholder + pub requested_capabilities: Vec, + pub declared_tools: Vec, + pub declared_hooks: Vec, + pub declared_event_channels: Vec, +} + +pub enum FeatureRuntimeKind { + Builtin, + ExternalProcess, + McpBridge, + WasmPlaceholder, + DeclarativePlaceholder, +} + +pub struct FeatureInstallContext<'a> { + // No Pod or Worker reference. + pub feature_id: &'a FeatureId, + pub grants: &'a CapabilityGrantSet, + pub tools: ToolRegistrar<'a>, + pub hooks: PublicHookRegistrar<'a>, + pub notify: FeatureNotifySink<'a>, + pub events: FeatureEventSink<'a>, + pub diagnostics: FeatureDiagnosticSink<'a>, + pub services: FeatureServiceProvider<'a>, +} +``` + +Important details: + +- `FeatureDescriptor` is declarative and serializable. It is safe to show in diagnostics, profile previews, and `ListFeatures`-style future tooling. +- `FeatureModule::install` is runtime code that wires stateful tool/hook implementations into host registrars. +- `FeatureInstallContext` must not expose `Pod`, `Worker`, raw `ToolServerHandle`, raw `Interceptor`, raw `NotifyBuffer`, raw `LogWriter`, raw `event_tx`, or direct history mutation. +- `FeatureServiceProvider` returns only host services backed by granted capabilities, for example scoped filesystem access, WorkItem store access, memory access, Pod orchestration handles, web provider handles, or secret references. It should return `Denied`/`Unavailable` diagnostics instead of exposing partial internals. + +### Example registration snippet + +This is illustrative shape, not proposed final exact Rust syntax: + +```rust +use pod::feature::{ + CapabilityRequest, FeatureDescriptor, FeatureId, FeatureInstallContext, + FeatureModule, FeatureRuntimeKind, HostCapability, ToolContribution, +}; + +pub struct WorkItemFeature { + state: std::sync::Arc, +} + +impl FeatureModule for WorkItemFeature { + fn descriptor(&self) -> FeatureDescriptor { + FeatureDescriptor::builder(FeatureId::builtin("work-item")) + .display_name("WorkItem intake and routing") + .runtime(FeatureRuntimeKind::Builtin) + .request(CapabilityRequest::required( + HostCapability::WorkItemStore { read: true, write: true }, + "create and update WorkItem records through host-owned ticket storage", + )) + .request(CapabilityRequest::optional( + HostCapability::EmitUserEvent, + "surface routing diagnostics to the TUI/actionbar", + )) + .tool("WorkItemCreate") + .tool("WorkItemComment") + .hook("work_item_intake_pre_tool_audit", pod::hook::HookPoint::PreToolCall) + .event_channel("work-item") + .build() + } + + fn install(&self, ctx: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { + let store = ctx.services.work_item_store()?; + + ctx.tools.register(ToolContribution::new( + "WorkItemCreate", + work_item_create_tool(store.clone(), self.state.clone()), + ))?; + + ctx.hooks.pre_tool_call( + "work_item_intake_pre_tool_audit", + WorkItemAuditHook::new(self.state.clone()), + )?; + + ctx.events.declare_channel("work-item")?; + Ok(()) + } +} +``` + +The feature keeps `WorkItemFeatureState`. The Pod keeps only registration records, diagnostics, and the normal host services it already owns. + +### Tool contribution + +A tool contribution should be a thin wrapper around `llm_worker::ToolDefinition` plus feature metadata: + +```rust +pub struct ToolContribution { + pub feature_id: FeatureId, + pub name: ToolName, + pub definition: llm_worker::ToolDefinition, + pub required_capabilities: Vec, +} +``` + +Rules: + +- Register into the existing `ToolRegistry` / `ToolServerHandle`; do not add a plugin-dispatcher tool that multiplexes plugin calls outside normal tool history. +- Preserve normal `PreToolCall` permission evaluation, tool-call history, result history, output truncation/bounding, and diagnostic behavior. +- Host-controlled feature enablement decides whether a contributed tool is installed. Manifest/profile tool permission still decides whether a model may call it at runtime. +- Duplicate tool names should be rejected during feature registry preflight with a diagnostic, not discovered later through a panic or undefined ordering. +- Public feature identity should be source-qualified (`builtin:memory`, `project:foo`, `plugin::bar`), while model-visible tool names should remain explicit stable names. Do not auto-prefix model tool names unless the project deliberately chooses a future namespacing policy. +- Tool schemas/descriptions must be part of the normal `ToolDefinition` path so model-visible surfaces remain inspectable and bounded. +- If a required host service is not granted or configured, the tool should not be registered; the install report should explain the skipped contribution. + +### Hook contribution + +Hook contribution must depend on the safe hook surface produced by `hook-public-surface-hardening`. + +Recommended public hook principles: + +- Public hooks register through `PublicHookRegistrar`, which wraps `HookRegistryBuilder` but exposes only hardened hook traits/actions. +- Public hooks receive snapshots/views, not mutable Pod/Worker handles. +- Public hook return values should be decisions such as continue, deny/rewrite a tool decision through a host-defined synthetic result path, emit diagnostics, or request a durable notification/history append through a host sink. They should not return raw `llm_worker::Item` vectors. +- Public hooks must not be able to mutate request context, session history, or Worker state invisibly. +- Permission enforcement hooks remain host/internal and run before feature hooks for `PreToolCall` so a feature cannot approve a denied tool call. +- Hook ordering should be explicit and stable: internal safety hooks first, public feature hooks in registry order or declared priority bands, internal usage/accounting hooks where needed. Priority should be coarse, not arbitrary integer ordering that lets plugins fight for precedence. + +Possible hardened hook action shape: + +```rust +pub enum PublicPreToolCallDecision { + Continue, + DenyWithSyntheticError { message: String }, + EmitDiagnostic { diagnostic: FeatureDiagnostic }, +} + +pub trait PublicPreToolCallHook: Send + Sync { + fn on_pre_tool_call(&self, event: PublicPreToolCallEvent<'_>) -> PublicPreToolCallDecision; +} +``` + +If a hook needs to add model-visible text, it should use `FeatureNotifySink::notify_model(...)` or another host-owned durable append API, not return an `Item`. + +### Notification/event contribution + +Expose two distinct sinks: + +```rust +pub struct FeatureNotifySink<'a> { /* host-owned */ } +pub struct FeatureEventSink<'a> { /* host-owned */ } +``` + +Recommended behavior: + +- `FeatureNotifySink::notify_model(...)` creates a model-visible notification through the existing durable notification/system-item path. The host commits the corresponding `SystemItem` before it is appended to Worker history. +- `FeatureNotifySink::notify_user(...)` or `FeatureEventSink::emit(...)` creates user-visible diagnostics/progress/action events through the existing alert/event path. These are not model-visible unless explicitly routed through `notify_model`. +- Event payloads should be typed, bounded, and feature-identified. Avoid arbitrary JSON blobs as the first public API; allow an opaque bounded metadata field only if diagnostics require it. +- Notifications and events should require explicit capabilities such as `EmitModelNotification` and `EmitUserEvent`. +- Background feature tasks must use these sinks; they must not hold raw log writers or append directly to history. + +Useful initial event shape: + +```rust +pub struct FeatureEvent { + pub feature_id: FeatureId, + pub level: FeatureEventLevel, // Info, Warn, Error + pub channel: String, // e.g. "work-item" + pub summary: String, + pub detail: Option, + pub model_visible: bool, // false unless host routes through notify_model +} +``` + +`model_visible` should be host-controlled in practice: a feature may request model visibility, but the sink decides whether that capability is granted and records the durable append if it is. + +### Capability request/grant/diagnostics + +Capabilities are requested by descriptors and granted by the host. A feature may request a capability, but it must not assume the capability exists. + +Initial capability categories: + +```rust +pub enum HostCapability { + ContributeTool { name: ToolName }, + ContributeHook { point: pod::hook::HookPoint }, + EmitUserEvent, + EmitModelNotification, + ScopedFs { read: bool, write: bool, execute: bool }, + WorkItemStore { read: bool, write: bool }, + MemoryStore { read: bool, write: bool }, + PodManagement { spawn: bool, message: bool, restore: bool }, + Network { purpose: NetworkPurpose }, + SecretRef { id: String }, +} +``` + +Important separation: + +- Capability grants decide whether a feature may install and receive host services. +- Tool permissions decide whether an installed tool call may execute for a specific Pod/run. +- Scope permissions decide which filesystem paths or delegated Pod capabilities a host service may touch. + +Diagnostics should be first-class: + +```rust +pub struct FeatureInstallReport { + pub feature_id: FeatureId, + pub enabled: bool, + pub granted: Vec, + pub denied: Vec, + pub installed_tools: Vec, + pub installed_hooks: Vec, + pub skipped_contributions: Vec, + pub diagnostics: Vec, +} +``` + +Diagnostics must avoid secrets and must be safe for session logs, TUI display, and future `ListFeatures`/profile validation output. + +## 4. State ownership model + +Feature state belongs to the feature module. + +- A feature may own `Arc` and clone it into contributed tools, hooks, and background tasks. +- The Pod registry stores descriptors, install reports, enabled/disabled status, and host-owned handles. It does not store feature business state. +- Durable feature data must live in a feature-owned or host-granted store with an explicit API: WorkItem files through a WorkItem service, memory records through memory APIs, plugin config/state through a future plugin-state service, etc. +- Session history is not feature storage. It is an audit/replay record of model-visible interactions and host-visible events. +- A feature that needs restoration after process restart should reconstruct itself from its own durable store/config plus normal Pod metadata, not from private data hidden in Worker context. +- Background tasks are allowed only if they communicate through granted sinks/services and have a defined shutdown/lifecycle policy owned by the host. + +This model lets built-ins and plugins share the same contribution shape while keeping Pod runtime ownership clear. + +## 5. Safety invariants / forbidden operations + +Public features/plugins must not be able to perform these operations: + +- Mutate prompt context directly. +- Append, remove, reorder, or rewrite Worker history directly. +- Insert model-visible text that is not committed through a durable host path. +- Return raw `llm_worker::Item` values from public hooks. +- Access raw `Worker`, raw `Pod`, raw `ToolServerHandle`, raw `llm_worker::Interceptor`, raw `NotifyBuffer`, raw session log writer, or raw event sender. +- Register tools outside `ToolRegistry` or bypass normal tool-result history recording. +- Bypass `PreToolCall` permission policy. +- Grant themselves capabilities or infer grants from successful construction. +- Mutate manifest/profile/scope state directly. +- Perform filesystem/process/network/secret access outside granted host services. +- Emit unbounded tool outputs, event payloads, diagnostics, or notification bodies. +- Put secrets into diagnostics, session logs, model context, TUI output, or feature install reports. +- Depend on MCP/WASM/package-distribution mechanics in the base Pod API. + +Positive invariant: if the model can see a feature-produced fact, a future replay/resume must have a durable explanation for why that fact was present. + +## 6. Placement and crate-boundary recommendation + +Recommended placement: + +- `crates/pod/src/feature.rs` or `crates/pod/src/feature/mod.rs` + - public feature traits/types + - feature registry builder + - install reports/diagnostics + - capability request/grant model + - typed registrars/sinks + +- `crates/pod/src/hook.rs` + - remains the public hook module after hardening + - should expose safe Pod-level hook traits/actions only + - should not re-export `llm_worker::Interceptor` power + +- `crates/llm-worker` + - remains owner of generic LLM tools/interceptors/history machinery + - should not depend on `pod::feature` + +- `crates/tools` + - remains a source of reusable tool implementations + - built-in feature modules in `pod` can wrap these constructors into `ToolContribution`s + +- Future external plugin crates/processes + - should adapt into `FeatureDescriptor` + `FeatureModule` or a host-side adapter that produces equivalent contributions + - should not be called directly by the Pod except through the registry/registrars + +Install location in Pod startup: + +1. Resolve manifest/profile and host capability policy. +2. Construct `Pod` and internal safety surfaces. +3. Install host/internal hooks such as manifest permission enforcement. +4. Build and install enabled feature modules through `FeatureRegistryBuilder`. +5. Flush/register tools through the existing Worker tool registry. +6. Freeze/install the Pod interceptor and start normal run/attach behavior. + +The exact sequencing can be adjusted to match current construction, but the invariant should hold: public feature hooks cannot precede host safety hooks, and feature tools must exist before the model receives the final tool schema for a run. + +## 7. Migration path from current built-in registrations + +Recommended migration is incremental and behavior-preserving: + +1. Land hook public-surface hardening first. + - Remove/replace public raw `Item`-carrying hook actions. + - Define which hook decisions are safe for external contributors. + +2. Add `pod::feature` with no behavior change. + - Implement descriptors, capability grants, install reports, and registrars. + - Initially register no external plugins. + +3. Wrap current built-in tool registration as built-in feature modules. + - Start with a small built-in feature whose state/services are already cleanly bounded. + - Preserve existing tool names, schemas, and permission behavior. + - Convert duplicate-name failures into registry diagnostics before flushing tools. + +4. Move larger built-in groups behind feature modules. + - Filesystem/process tools from `crates/tools`. + - Memory tools. + - Pod orchestration tools. + - Task/WorkItem tools once their stores and hooks have explicit capabilities. + - Web tools as configured provider-backed features. + +5. Move built-in hook contributions only after safe hook semantics are stable. + - Keep manifest permission enforcement as an internal host hook, not a feature hook. + - Keep accounting/usage hooks internal unless they become genuine feature behavior. + +6. Treat workflow/user-input expansion separately. + - Workflow invocation already uses a durable system-item attachment pattern. + - Do not expose arbitrary workflow-like context injection to plugins until there is a safe typed command/input-contribution API with durable append semantics. + +7. Add profile/manifest enablement after built-ins work through the same registry. + - Built-ins and external plugins should share descriptor/capability/install-report mechanics. + - Host policy may grant built-ins by default, but built-ins should still declare what they use. + +## 8. Impact on WorkItem / MCP / plugin distribution follow-ups + +WorkItem / intake routing: + +- WorkItem routing can become a built-in feature that contributes WorkItem tools, optional routing hooks, and user-visible action events. +- It should request `WorkItemStore` and event/notification capabilities instead of reaching into ticket files ad hoc. +- Model-visible routing hints or intake results must be committed through notification/history append paths. +- This registry gives the WorkItem feature a clean way to install without making WorkItem a special Pod runtime mode. + +MCP: + +- MCP should be an adapter/runtime kind that produces normal `ToolContribution`s and possibly safe event diagnostics. +- MCP tool calls must still pass through `ToolRegistry`, PreToolCall permission, output bounding, and history result recording. +- MCP resources/prompts should not become invisible prompt injection. If exposed later, they should be explicit tools, user-invoked attachments, or durable notification/history appends. +- MCP transport/session details are out of scope for the base API beyond the `FeatureRuntimeKind::McpBridge` placeholder. + +Plugin distribution: + +- Archive validation, cache extraction, signing/trust, WASM execution, external process supervision, and package update policy should remain separate follow-up designs. +- Distribution mechanisms should eventually produce the same descriptor/capability/contribution objects as built-ins. +- Capability grants are the host trust boundary; package installation alone must not grant runtime authority. + +## 9. Open questions / risks + +1. Tool naming policy is the highest-risk API decision. + - Recommendation: feature identities are source-qualified, model-visible tool names stay explicit and stable, and collisions are rejected by the host. + - Risk: external plugins may need namespacing later. Auto-prefixing now would avoid collisions but would also change model-facing ergonomics and diverge from current built-in tool names. + +2. The exact safe hook action set must be settled by `hook-public-surface-hardening`. + - Especially important: whether public pre-tool hooks may synthesize denials/results, and how durable append requests are represented. + +3. Notification/event durability needs precise semantics. + - User-visible events may be live-only, while model-visible notifications must be durable. The public API should make this distinction impossible to miss. + +4. Capability granularity can easily become either too coarse or too noisy. + - Start with coarse host-service capabilities plus normal tool permissions, then split only when real features need finer grants. + +5. Runtime enable/disable is not designed here. + - Initial registry should be install-at-startup. Hot reload or dynamic plugin enablement needs separate lifecycle, cleanup, and schema-refresh design. + +6. Persistent plugin state needs a future host service. + - The base API says state is feature-owned, but external plugins will still need a sanctioned durable state directory/store with migration/versioning rules. + +7. Background tasks need lifecycle policy. + - If external plugins can spawn tasks, the host must define shutdown, cancellation, panic handling, diagnostic routing, and whether task output may become model-visible. + +8. Existing workflow/input expansion is close to the forbidden boundary. + - It is safe only because it commits system items before model visibility. Any future plugin command/input contribution must preserve that durable replay property. + + ---