Skip to content

feat(plugin): add UserLevel plugin dispatch adapter and route (PR 4/5)#32

Open
shaun0927 wants to merge 3 commits into
Q00:release/bootstrapfrom
shaun0927:feat/plugin-user-level-dispatch
Open

feat(plugin): add UserLevel plugin dispatch adapter and route (PR 4/5)#32
shaun0927 wants to merge 3 commits into
Q00:release/bootstrapfrom
shaun0927:feat/plugin-user-level-dispatch

Conversation

@shaun0927
Copy link
Copy Markdown

Summary

PR 4 of the five-PR plan under SSOT #25 (see PR #3 / docs/userlevel-plugin-dispatch.md).

Wires the UserLevel plugin layer into the runtime dispatch contract so ooo <plugin> <command> input can be routed through the existing guarded external command runner — without bypassing trust, risk class, or shell-injection protection.

Stacking

Requires PR #30 (PR 2) and PR #31 (PR 3) to be merged first. This branch is stacked on top of feat/plugin-user-level-resolver; until the base PRs merge, this diff includes their commits.

What ships

Ourocode.Runtime.UserLevelPluginInvocation

Implements the runtime Adapter behaviour. Reads :capabilities from the dispatch context, runs the Resolver, evaluates trust state and risk class, and either invokes via :external_command_runner or returns a structured :blocked result. Argv is always a list — Dispatcher.guarded_external_command_runner stays the shell-injection authority.

Blocked reasons surfaced:

  • :trust_missing
  • :ambiguous_match
  • :unknown_plugin_or_command
  • :destructive_action_requires_approval
  • :not_user_level_plugin_input
  • {:external_command_failed, reason}

Destructive risk_class commands require explicit context.destructive_action_approved? = true. Default fails closed.

Ourocode.Plugin.UserLevel.PreflightView

JSON-safe projection of a PreflightResult shaped like the existing Ourocode.Command.CapabilityPreflight.Projection. Lets any UI render UserLevel plugin preflight using the same shape as the slash command preflight (trust, side_effects, candidates, match_explanation).

Ourocode.Plugin.UserLevel.Entry

Router refinement helper. Takes a parsed TaskRequest and a capability snapshot; when the input targets a known UserLevel plugin, swaps the routing_decision to :user_level_plugin and attaches plugin_id. Keeps Ourocode.Runtime.Router itself transport- and registry-agnostic.

Ourocode.Runtime.Dispatcher.RouteResolution

  • Adds :user_level_plugin to @supported_routes
  • Adds adapter_keys/1 clauses (plugin-id-scoped + generic fallback)
  • Reuses existing validate_adapter_route guard to reject decisions that incorrectly carry adapter_route

Ourocode.TaskRequest

routing_decision type extended with :user_level_plugin kind/execution_route and optional :plugin_id field.

What is intentionally NOT in this PR

  • Registry supervision wiring in ApplicationServices. Until the live TUI integration ships, callers pass capability snapshots directly via context. PR 5 wires the registry into the runtime supervision tree alongside the artifact watcher.
  • /plugins refresh slash command. Ships with PR 5 once the registry is supervised.
  • Continuation/auto-run policy and artifact detection. PR 5.

Closes

Testing

  • 4 ExUnit modules (async: true) covering:
    • Adapter happy path (Superpowers fixture, mock runner)
    • Trust blocked / unknown command blocked / destructive blocked + explicit approval
    • Shell-injection argv passthrough (argv stays a list of binaries)
    • Runner contract errors (:external_command_runner_not_configured, :invalid_external_command_runner)
    • PreflightView projection across :unique_match, :ambiguous, :not_applicable, missing trust
    • Entry.refine/2 rewrites only when applicable
    • Dispatcher RouteResolution validation + adapter_keys/1 for :user_level_plugin
  • Local Elixir toolchain unavailable in authoring environment; CI is the source of truth. Reviewers please confirm mix test (and especially the new files under test/ourocode/plugin/user_level/ and test/ourocode/runtime/) is green before approving.

🤖 Generated with Claude Code

shaun0927 and others added 3 commits May 25, 2026 18:13
Introduces the Ourocode.Plugin.UserLevel namespace that lets the runtime
discover installed Ouroboros UserLevel plugins and treat their commands
as first-class registry entries without reimplementing trust or storage
semantics.

What is added:

- Ourocode.Plugin.UserLevel.Capability + .Capability.Command
  Normalized identity and command surface (plugin_id, source, version,
  install scope, trust scopes, manifest digest, declared commands,
  expected artifacts, continuation hints). Identity stability via
  (plugin_id, version, manifest_digest) tuple so re-discovery without
  manifest changes returns the same struct.

- Ourocode.Plugin.UserLevel.Discovery
  Behaviour with a Discovery.run/2 helper that normalizes raw
  descriptors into Capability structs and surfaces per-descriptor
  validation errors separately so one bad command never loses a whole
  plugin.

- Ourocode.Plugin.UserLevel.Discovery.OuroborosCLI
  First discovery adapter; invokes `ouroboros plugin list --json` via a
  pluggable command runner. Tests inject a stub runner to avoid spawning
  real processes. Failure modes (exit != 0, runner unavailable,
  malformed JSON, unexpected shape) all surface as structured errors.

- Ourocode.Plugin.UserLevel.Registry
  Small Agent that caches the latest discovery snapshot with a 60 s
  TTL, explicit refresh, and identity-preserving merge. Discovery
  failure degrades the snapshot but preserves last good capabilities,
  so missing/broken ouroboros CLI never blocks boot.

- Ourocode.Plugin.UserLevel.RegistryEntry
  Projects Capability into the existing Command.Registry plugin-source
  entry shape (mirrors PluginSurfaceEntry's metadata so the existing
  CapabilityPreflight.Trust and Projection modules apply unchanged).

What is NOT changed in this PR:

- No supervision wiring — the registry is standalone and ships dead
  code until PR 4 wires it into application_services.ex alongside the
  dispatch adapter that needs it. This keeps PR 2 boot-safe.
- No new slash command — `/plugins refresh` ships with PR 4.
- No router/dispatcher changes — those land in PR 3.

Tests: 5 ExUnit files (1255 LOC total with lib code) cover capability
shape, identity, command lookup, discovery normalization,
OuroborosCLI parsing of the superpowers fixture, registry TTL +
degraded handling, identity-stable merge, and registry projection
into the existing plugin-source entry shape.

Closes Q00#5
Closes Q00#8
Closes Q00#9
Closes Q00#18
Closes Q00#27
Closes Q00#29

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a pure resolver from `ooo <plugin> <command> [args ...]`-shaped
input to a structured PreflightResult. The result is read-only: it
describes what dispatch would do without executing, mutating trust, or
touching the registry.

What is added:

- Ourocode.Plugin.UserLevel.PreflightResult
  Struct with kind (:unique_match | :ambiguous | :unknown |
  :not_applicable), task_input, matched plugin/command, parsed argv
  tail, trust state, remediation string, risk class, expected
  artifacts, continuation policy, candidates (ambiguous case), and a
  match_explanation (matched_by + confidence).

- Ourocode.Plugin.UserLevel.Resolver
  resolve/2 turns task_input + capabilities into a PreflightResult.
  applies_to?/2 is the cheap predicate routing layers call to decide
  whether to swap their routing decision before invoking dispatch.

Resolution rules (intentionally narrow):

  * Direct ooo/ouroboros prefix only. Free-form natural language is
    deferred until the exact path is stable.
  * Plugin id and command name/alias matching is exact (case
    insensitive at lookup time but capability fields are preserved).
  * Argument tokens are passed through verbatim — no shell parsing,
    no case folding. Shell injection input becomes argv tokens that
    Dispatcher.guarded_external_command_runner will still guard.
  * Duplicate plugin ids surface as :ambiguous with candidates, never
    silent guess.
  * Trust state: :allowed when the capability declares trust_scope,
    :missing otherwise (with a remediation suggestion that points the
    user at `ouroboros plugin trust ...`).

What is intentionally NOT in this PR:

- Router or Dispatcher route additions. Those land in PR 4 together
  with the UserLevelPluginInvocation adapter, so dispatch can be tested
  end-to-end in one place.
- TUI panel rendering. PR 4 ships PreflightPanel.
- Decision journal. PR 5.

Tests: 2 ExUnit modules (async: true) covering unique_match (canonical
+ alias + mixed-case + arg case preservation + shell injection input),
trust missing, unknown plugin/command, missing tokens, ambiguous
duplicate ids, not_applicable inputs, and applies_to? predicate.

Closes Q00#16
Closes Q00#23

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the UserLevel plugin layer into the runtime dispatch contract so
`ooo <plugin> <command>` input can be routed through the existing
guarded external command runner without bypassing trust, risk class,
or shell-injection protection.

What is added:

- Ourocode.Runtime.UserLevelPluginInvocation
  Implements the runtime Adapter behaviour. Reads :capabilities from
  the dispatch context, runs the Resolver, evaluates trust state and
  risk class, and either invokes via :external_command_runner or
  returns a structured :blocked result. Argv is always a list — the
  Dispatcher's guarded_external_command_runner stays the
  shell-injection authority.

  Blocked reasons surfaced:
    :trust_missing, :ambiguous_match, :unknown_plugin_or_command,
    :destructive_action_requires_approval, :not_user_level_plugin_input,
    {:external_command_failed, reason}

  Destructive risk_class commands require explicit
  context.destructive_action_approved? = true. Default fails closed.

- Ourocode.Plugin.UserLevel.PreflightView
  JSON-safe projection of a PreflightResult shaped like the existing
  Ourocode.Command.CapabilityPreflight.Projection. Lets any UI render
  UserLevel plugin preflight using the same shape as the slash command
  preflight (trust, side_effects, candidates, match_explanation).

- Ourocode.Plugin.UserLevel.Entry
  Router refinement helper. Takes a parsed TaskRequest and a capability
  snapshot; when the input targets a known UserLevel plugin, swaps the
  routing_decision to :user_level_plugin and attaches plugin_id. Keeps
  Ourocode.Runtime.Router itself transport- and registry-agnostic.

- Ourocode.Runtime.Dispatcher.RouteResolution
  Adds :user_level_plugin to @supported_routes, adds adapter_keys/1
  clauses (plugin-id-scoped + generic fallback), reuses existing
  validate_adapter_route guard to reject decisions that incorrectly
  carry adapter_route.

- Ourocode.TaskRequest
  routing_decision type extended with :user_level_plugin kind /
  execution_route and optional :plugin_id field.

What is intentionally NOT in this PR:

- Registry supervision wiring in ApplicationServices. Until the live
  TUI integration ships, callers pass capability snapshots directly via
  context. PR 5 wires the registry into the runtime supervision tree
  alongside the artifact watcher.
- /plugins refresh slash command. Ships with PR 5 once the registry is
  supervised.
- Continuation/auto-run policy and artifact detection. PR 5.

Tests: 4 ExUnit modules (async: true) — adapter happy path, trust
blocked, unknown command blocked, destructive blocked + approved,
shell-injection argv passthrough, runner contract errors, view
projection across kinds, entry refinement (rewrites only when
applicable), and dispatcher route validation + adapter_keys.

Closes Q00#15
Closes Q00#17 (minimal — trust-blocked structured error path)
Closes Q00#20
Closes Q00#21

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shaun0927 shaun0927 force-pushed the feat/plugin-user-level-dispatch branch from 8e66c2b to 2530661 Compare May 25, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant