Skip to content

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

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

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
@Q00
Copy link
Copy Markdown
Owner

Q00 commented Jun 3, 2026

Superseded by #36, which consolidates this stack and wires the UserLevel plugin path into the live runtime before merging to release/bootstrap.

@Q00 Q00 closed this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants