This guide explains the internal layer boundaries in cli_subprocess_core.
It is intended for people reviewing the core architecture, extending the core, or trying to understand where a change should land.
The Core Layers
cli_subprocess_core has a small set of important layers:
- model policy
- provider profile adaptation
- command/session normalization
- execution-surface routing and lower runtime execution
- normalized event and payload emission
These layers exist so the runtime can stay consistent while still supporting multiple external CLI families.
Schema Ownership Inside The Layers
The shared schema boundary sits beside the existing runtime layers, not above them as a second architecture.
CliSubprocessCore.Schemaowns the sharedZoivalidation contract for core-owned dynamic boundaries.CliSubprocessCore.Event,CliSubprocessCore.Payload.*,CliSubprocessCore.ModelRegistry.Model,CliSubprocessCore.ModelRegistry.Selection, andCliSubprocessCore.ModelInputuse that schema layer at map ingress and then project back into the existing public structs.- Forward-compatible shared wire surfaces use
Zoi.map(..., unrecognized_keys: :preserve)plus projection so future fields survive inextrawhere the boundary needs them. - Closed boundaries may still use direct struct validation, but evolving wire
surfaces should not depend on
Zoi.struct/3. - Provider-native app-server, control-protocol, and orchestration schemas stay in the downstream repos that own those boundaries.
Layer 1: Model Policy
Owned by:
CliSubprocessCore.ModelRegistry- the internal model catalog loader and catalog data under
priv/models
This layer answers:
- which models exist
- which model should be used
- whether the request is valid
- which reasoning values are allowed
Layer 2: Provider Profile Adaptation
Owned by:
CliSubprocessCore.ProviderProfile- built-in provider profile modules
This layer answers:
- how to translate normalized intent into provider-specific CLI behavior
- how to interpret provider-specific output inside the shared runtime
Layer 3: Command and Session Normalization
Owned by modules such as:
CliSubprocessCore.CommandCliSubprocessCore.Command.OptionsCliSubprocessCore.SessionCliSubprocessCore.Session.Options
This layer gives the core a provider-agnostic API for one-shot commands and longer-lived sessions.
Layer 4: Lower Runtime Execution
Owned by the lower owner for the selected lane:
ExecutionPlane.Processfor the covered local one-shot command laneExecutionPlane.Process.Transportfor the shared session-bearing lane and non-local command/session placement
This layer starts the external process, manages stdin/stdout/stderr, and captures process exit information through the shared substrate.
It should remain blind to provider policy.
That includes provider-native approval and sandbox posture. The transport layer owns how the process is attached or started. It does not own whether a particular provider should run with a permissive mode such as "danger full access."
If a remote CLI launched over :ssh_exec later fails inside its own sandbox
backend, that is already above the transport-placement boundary. A common real
example is a remote Linux host where the provider CLI tries to use bwrap and
the host's AppArmor or userns policy blocks the loopback/userns setup. The
core transport succeeded; the remote runtime or host policy did not.
Layer 5: Event and Payload Emission
Owned by:
CliSubprocessCore.EventCliSubprocessCore.Payload.*CliSubprocessCore.Runtime
This layer turns provider/runtime activity into the shared event model that the rest of the stack consumes.
The Practical Boundary Rule
When deciding where a change belongs, use this rule:
- if the change affects model choice, put it in the registry/catalog
- if the change affects provider CLI syntax, put it in the provider profile
- if the change affects the covered local one-shot subprocess lane, put it in the execution-plane-backed lower runtime
- if the change affects the shared session-bearing or non-local placement transport seam, put it in the execution-plane-backed transport layer
- if the change affects normalized output shape, put it in payload/runtime
That rule prevents policy leakage across layers.
Example Integration Shape
External repos should consume the core in this order:
- prepare normalized options
- call the core’s model registry
- pass the resolved selection into provider-facing command building
- let the core route the request to the execution-plane-backed command lane or the execution-plane-backed transport lane as appropriate
The core is therefore both:
- a policy owner
- and a command/session boundary above lower runtime owners
But those are still separate internal responsibilities.
What Reviewers Should Watch For
Architecture drift usually shows up as one of these mistakes:
- a provider profile starts choosing fallback models
- transport code learns provider policy
- consumer-facing behavior bypasses normalized payloads
- multiple layers define overlapping defaults
If one layer can be removed without changing the others, the boundaries are probably healthy. If a small change requires editing policy, profile, and transport logic together, the responsibilities are probably leaking.