id: SMH-SPEC-PLUG0001 type: spec title: "Plugin System" research: [SMH-RESEARCH-TBKDJHVX, SMH-RESEARCH-WASM0001, SMH-RESEARCH-AHDXRKXM, SMH-RESEARCH-EVY30PHD]
Plugin System
Intent
One typed plugin system where unprivileged wasm components extend Smith. Built-ins and user plugins share the same contracts, effects, and sandbox; parity is law, not aspiration.
Engine mechanics (determinism, fuel, memory ceilings, artifact validation) live in SMH-SPEC-WASM0001.
Product integration points live in SMH-SPEC-SPEC0001.
Runtime and parity
Plugins run in a restricted runtime without direct operating-system, filesystem, environment, network, or clock access. Plugin execution is resource-bounded. Host effects are available only through typed Smith SDK component interfaces granted by manifest and trust decisions. Built-in plugins have no privileged effect path.
Built-in coding tools are embedded component artifacts loaded through the normal sandbox; they call the same typed host primitives available to user plugins.
Contributions
Plugins may contribute:
- tools,
- commands,
- providers and models,
- credential sources,
- prompts and context transforms,
- event handlers,
- themes and keybindings,
- layout and render descriptions,
- VCS workflows.
Provider protocol
A provider plugin is a pure codec. The guest builds request templates carrying secret references; the host substitutes plaintext at the effect boundary, executes transport, and records the redacted effect. Raw response chunks flow back to the guest, which decodes bytes into normalized events as pure computation. Replay re-executes decoding over recorded bytes. Custom transports and routing policy arrive later as additive granted interfaces or host-executed plans, never by redesigning this pipeline.
Tool visibility and authority
Tool publication, activation, advertisement, and invocation authority are distinct.
| Dimension | Meaning |
|---|---|
| Registered | Validated contribution exists in the pinned generation |
| Active | Host policy enables invocation in that generation |
| Advertised | Definition appears in a particular provider request |
| Authorized | Caller may invoke it and requested effects remain within granted scopes |
Model-emitted calls require an active tool advertised by the originating request's pinned loadout. SDK-mediated composition may invoke active unadvertised tools only with explicit caller authorization. No call path can invoke disabled tools or bypass argument validation, capability grants, secret handling, recording, or shared budgets. The host validates caller invocation authority and delegated effect scopes before execution. Nested effects require both the target's grants and caller-delegated authority; scopes may only narrow along the call chain. A target plugin's grants alone do not authorize its caller.
Discovery does not execute discovered tools or grant capabilities. Discovered definitions enter request loadouts only through validated publication at safe boundaries. Publication and discovery cannot change a request's pinned definitions or handlers mid-round.
Generations
Contribution identities are namespaced by their plugin and coexist. Short names resolve through the alias layer; ambiguity fails deterministically, naming every candidate and the override path; no implicit winner is selected.
Plugin events are ordered and typed. Contributions are declared at instantiation through a registration entry point; the host validates the resulting complete set before publication. Manifest statics stay minimal and curated: requested capabilities plus hints concerning safety, performance, and robustness. Registration performs no operational effects and mutates no active runtime. A candidate generation can declare and validate contributions but cannot invoke operational effects or mutate the active runtime before publication. A failed load leaves no registrations or subscriptions active. Reload publishes one complete generation or retains the previous generation.
A generation owns every plugin instance, registration, subscription, binding, and contribution. Instances are interchangeable dispatch targets: pooled freely for parallel calls, immutable after instantiation, with local state as scratch carrying no semantic guarantee. Per-instance configuration binds at instantiation and never mutates after. All durable state lives in the host; guests address it through per-call resource handles. Instance affinity, where genuinely needed, is expressed through resources that pin calls to a serving instance. Session, branch, queue, secrets, cancellation, and active-turn state live outside the generation and survive reload. Guest-local implementation state resets on reload. Removed contributions disappear at publication. Calls already running complete against their pinned generation; later calls resolve against the published generation.
Each provider request pins one generation through its response and complete tool-call/result batch. The pinned generation supplies both advertised definitions and executable handlers.
Durable resources
Plugin state, work, and effect operations use the shared session authority and committed-change contracts in SMH-SPEC-SPEC0001.
Resources expose only their authorized scope; possession of a state or work identifier alone grants no access or execution authority.
The host owns durable identities, commit ordering, checkpoints, effect evidence, and terminal receipts.
Guests request validated operations; they cannot mutate arbitrary core work records or assert host-effect completion.
Durable plugin state declares ownership, version, value bounds, retention, and branch/fork semantics. State is independent of guest instance lifetime; per-call handles expire with their owning invocation and are reacquired rather than persisted as live handles. Missing definitions do not erase stored payloads or permit incompatible interpretation. Related state and work-checkpoint changes publish together under the shared committed-change contract. Guest failure does not undo already committed state or completed external effects.
Recoverable work records the execution provenance and authorization needed by its continuation. Recovery cannot substitute a same-named implementation for a recorded generation or silently widen grants. Unavailable prerequisites follow the shared blocked-continuation contract; this does not promise indefinite artifact retention. Shared work mechanisms do not require a plugin-facing generic workflow-definition API. Ordinary child work remains invocation-owned; a background ownership contract cannot be inferred from returning before a child finishes.
Budgets and failure
Smith SDK calls may catch ordinary typed guest errors, but not cancellation, budget exhaustion, sandbox violations, or host-effect recording failures. Smith SDK child work is structured and host-orchestrated: it shares the parent invocation budget and generation pin, serializes per plugin instance, and finishes or cancels before the parent handler returns. Fan-out cannot multiply resource limits. The budget covers deadline, guest fuel, component memory and table ceilings, host allocations, logs, conversions, and output. Budget knobs resolve by precedence: explicit user or trust-time configuration, then manifest hints, then product defaults. Manifest hints may only tighten; raising any ceiling is a trust decision made at trust or install time.
Event boundary
Plugin handlers run synchronously in deterministic registration order under one bounded invocation budget shared with structured child tasks. Event domains — turn, provider stream, tool, session, and plugin lifecycle — are delivered under these laws; concrete event records are world surface and evolve under the world's lifecycle rules. They return typed actions rather than mutable runtime ownership. Actions may request cancellation, queued input, reload, context edits, continuation, or runtime-only model, provider, tool, prompt, and configuration changes. Cancellation applies immediately. Other actions are validated as one batch and apply atomically between provider requests, after the current stream and complete tool-result batch. Conflicting mutations are rejected; no implicit winner is selected.
A failed or over-budget handler emits one typed diagnostic, is disabled for the current generation, and does not abort the turn unless its event contract requires a decision. Failure of a required decision terminates the run without publishing its partial actions; continuation cannot override that failure or erase recorded effect evidence. Terminal agent events remain singular.
sequenceDiagram
participant P as Provider round
participant A as Agent boundary
participant H as Ordered handlers
participant G as Runtime generation
P-->>A: Stream and complete tool batch
A->>H: Typed lifecycle events
H-->>A: Typed action batch
A->>A: Validate and reject conflicts
A->>G: Publish coherent runtime changes
G-->>P: Next request snapshot
Continuation and settlement
Turn-complete and pre-settlement boundaries are actionable. A validated continuation ensures one next provider request; an already-eligible tool follow-up or queued-input request satisfies it. Duplicate continuation requests at one boundary coalesce, without consuming input twice or changing its delivery policy. Explicit stop, cancellation, or terminal run failure overrides continuation. Ending a run leaves queued input intact unless a separate accepted queue action changes it.
All work belonging to the run finishes or cancels before settled notification. Settled notification cannot reopen the completed run; returning continuation for that run is invalid and diagnosed. Accepted queued input remains for a later run; new-run requests received during notification wait until all settled handlers finish. No handler observes a reentrant run start during that notification dispatch. Settlement and terminal notifications remain singular for each run.
A run settles only after continuation decisions and child work resolve.
stateDiagram-v2
[*] --> Running
Running --> Boundary: Stream and tool batch complete
Boundary --> Running: Valid next request
Boundary --> Settling: Stop or no next request
Running --> Settling: Cancellation or terminal failure
Settling --> Settled: Child work finished
Settled --> [*]: Notifications delivered
Discovery and modules
Version 1 discovers plugin packages by path convention. Each package carries a manifest declaring its plugin artifact, world version, and requested capabilities. Package, artifact, and manifest identities are deterministic and validated before instantiation. Artifacts failing validation never execute. Manifest and artifact reads are bounded before materialization, and canonical paths cannot escape their declared root.
A module contract is a typed, major-versioned component interface. A package that exports a contract ships the contract WIT alongside its artifact; the artifact exports both the contract interface and its implementation constructors in one component. Remote updates deliver implementations only; interfaces change only with host world releases. Undeclared exports remain private. A module can have zero, one, or multiple implementations. One implementation binds automatically. Zero or ambiguous implementations fail only when consumed, with sorted candidates and the consumer reported. Explicit user binding overrides automatic binding and must satisfy the requested contract.
Module consumers use logical major-versioned handles. A new invocation resolves each handle against the published generation; nested accesses during an active invocation resolve within its pinned generation. Reload can rebind only to an implementation satisfying the same major contract; guest-local state resets.
Precedence is built-in, global user, project, then explicit override. Later registration replaces earlier registration only where the earlier contract explicitly permits replacement. Installed capabilities are validated as a complete set before atomic replacement or removal.
Project trust
Project-local executable configuration, plugins, modules, and managed packages are not enumerated or loaded before trust is resolved. A project trust identity is its lossless, platform-tagged canonical native path. Symlink aliases share one decision; distinct native paths cannot collide through text conversion. Interactive mode can ask and persist the decision. Non-interactive modes require an explicit or previously persisted decision to load project-local executable behavior. An explicit plugin path grants one-run trust only to that exact canonical source. Capability grants resolve at trust time for project plugins and at install time for user plugins. Ignoring untrusted project behavior produces a visible diagnostic.
Trust does not remove the plugin sandbox or tool boundaries.
System plugins
System plugins ship inside the smith release artifact, hash-pinned at build time; their trust is the release signature, transitively.
They get no runtime privilege: same sandbox, loader, fuel, and grant model as user plugins.
The smith: package prefix is reserved; external artifacts exporting smith: contracts fail validation with a concrete fix.
Product default grants — for example net-scope for the OAuth plugin — are configuration shipped by the product, not runtime privilege.
Trust tiers: embedded (signed with the release), installed user (hash recorded at install, grants at install), project (trust-gated before enumeration, grants at trust time).
World surface
The smith:plugin world is the entire plugin API.
No non-WIT construct is required to build or consume a plugin; raw wit-bindgen guests are first-class before and after any SDK exists.
Design laws:
- Two layers: a core world every plugin sees, plus granted capability interfaces — HTTP via net-scope, credential exchange via secret-proxy, logging, and UI intents via the UI grant — imported only with matching grants. Grants gate not only ambient reach but also resource and presentation decisions: the plugin manager can load, pool, or skip UI-granted plugins per frontend.
- Flat interface set inside one world:
http,secrets,log,stream,contrib; resources carry methods, no prefix hierarchies. Grouping pressure means an interface is too wide. - The WIT package containing the plugin world uses full SemVer; its version is independent of Smith's release version. Plugin-published contracts carry their own major-version identities.
- Plugin evolution and deprecation follow the compatibility policy below.
- Unstable items are identified explicitly; the 1.0.0 world and subsequent world-major introductions contain zero unstable items. Stable plugin contracts never lose compatibility through an unstable designation.
- One naming grammar, one error grammar, one documentation shape across all interfaces.
- Names follow verb-noun functions, noun types, no abbreviations; symmetric verb pairs;
openfor acquired handles,createfor pure values; faults are nouns. - Parameter order is data first, options record last.
- One world-wide glossary distinguishes near-synonyms: secret, credential, chunk, frame, provider, effect.
- One-purpose interfaces; an interface needs two real consumers before entering the world.
- Explicit parameters: buffers, bounds, cancellation, and provenance are arguments, never ambient state.
- Precise failure sets: typed result variants per operation family; no single generic error case.
- Resources are the polymorphism unit: module contracts are resource types, implementations export constructors, the host mediates binding.
- Pull, never callback: the host pulls chunks and re-invokes renewal; function values never cross the boundary.
- Plugin-to-plugin calls route through host effects; guests never link each other directly.
- Values cross by copy: calls are batched; chatty interfaces fail review.
- Shared values include an unknown catch-all for forward tolerance; interface diffs validate version changes against the applicable compatibility policy.
- No SDK requirement: the world alone stays complete and comfortable; migration assistance does not require a Smith-authored guest SDK.
Guest languages: Rust first, TypeScript via jco as maintained compatibility target, per SMH-SPEC-WASM0001.
Compatibility and migration
During prototype operation, Rust APIs and plugin contracts may break freely; the world remains 0.0.0. Released pre-1.0 worlds advance through minor versions beginning at 0.1.0, without a stability promise or a 0.99 ceiling. A deprecated item remains functional throughout exactly its introducing minor line and is removed from the next minor world: deprecated in 0.2.0 means removed in 0.3.0, never 0.2.x. Historical pre-1.0 artifacts have no permanent support guarantee.
Stable plugin compatibility begins at world 1.0.0. Existing compiled plugins remain usable across Smith releases, including Smith major upgrades, without rebuilding. New world majors may coexist with older worlds; introducing world 2 never by itself retires world 1. Evolution within a stable world major preserves existing contracts and behavior. Stable deprecations are reserved for serious security or correctness emergencies, not routine cleanup. Emergency retirement is an explicitly announced exception to permanent compatibility; continued availability and retirement timing are decided per case, not by a fixed window.
Deprecation metadata names the affected item, introducing version, replacement, migration instructions, and before/after examples. Pre-1.0 metadata also names the next minor removal version. Stable emergency metadata names the reason and affected contracts; a removal version is recorded when a retirement decision exists, not invented to satisfy a fixed timetable. Documentation builds incorporate this metadata into Smith's retrievable plugin reference and migration guidance. Plugin checking distinguishes deprecated-but-supported items from removed or retired contracts and exposes corrective guidance.
Smith ships doctor deprecator as a skill or command that users can invoke to prompt the Smith agent to refactor plugins away from deprecated APIs. It uses authoritative migration guidance from Smith docs and preserves normal project trust and effect boundaries. It does not silently modify plugins or promise that every migration is automatic.
Availability and migration obligations differ before and after stabilization.
flowchart TB
item["Deprecated plugin item"] --> policy{"World contract"}
policy -->|"Pre-1.0"| minor["Available in introducing minor line"]
minor --> next["Removed from next minor world"]
policy -->|"Stable emergency"| supported["Supported pending case-specific decision"]
supported --> decision{"Retirement announced?"}
decision -->|"No"| available["Remains available"]
decision -->|"Yes"| retirement["Explicit emergency exception"]
item --> metadata["Replacement, instructions, examples"]
metadata --> docs["Built Smith documentation"]
docs --> doctor["Doctor deprecator"]
doctor --> agent["User-invoked agent refactoring"]
Acceptance criteria
- Project plugins cannot execute before trust resolution.
- Built-in and user plugins use the same sandboxed effect APIs.
- Event handlers are bounded, ordered, and publish conflict-free runtime actions only at safe points.
- Reload atomically replaces one complete generation while active provider rounds remain pinned.
- Callable module handles rebind only to conforming implementations of the same major contract.
- Structured child work cannot suppress cancellation, detach implicitly, or exceed its shared budget.
- Scoped state and work operations reject unauthorized identifiers and expired invocation handles before mutation.
- Related state/checkpoint changes recover atomically; guest failure preserves prior committed state and effect evidence.
- Recovery retains recorded generation and authorization requirements; missing prerequisites block without silent substitution.
- A failed or over-budget handler disables only itself for the generation.
- Active unadvertised tools require explicit composition authority; disabled or ungranted calls fail before effects.
- Nested effects require target grants and caller-delegated authority; call chains cannot widen scopes.
- Discovery and publication preserve the definitions and handlers pinned by an active provider round.
- Duplicate continuation requests produce no duplicate provider request; stop and cancellation defeat continuation without discarding queued input.
- Required-decision failure terminates the run without partial actions or lost effect evidence.
- Settled continuation is rejected; runs requested during notification start only after child work and all notification handlers finish.
- Pre-1.0 deprecations remain functional for their introducing minor line and disappear from the next minor world, with matching migration metadata.
- Stable compiled plugins retain behavior across Smith and world-major introductions, except explicitly announced emergency retirements.
- Built docs expose deprecation instructions and examples; doctor deprecator makes that guidance available for user-invoked agent refactoring.