cli_subprocess_core no longer owns the raw subprocess substrate.
ExecutionPlane.Process.Transport owns the local session-bearing process lane,
and the same transport seam also owns non-local placement beneath the shared
execution_surface contract.
CliSubprocessCore.RawSession is the core-owned handle above that substrate
when you want exact-byte stdin/stdout defaults without provider parsing.
Public Modules
CliSubprocessCore.RawSessionfor a stable raw-session handle above the extracted transport layerExecutionPlane.Process.Transportfor direct transport lifecycle controlExecutionPlane.Process.Transport.Optionsfor validated startup optionsExecutionPlane.Process.Transport.RunOptionsfor validated one-shot execution optionsExecutionPlane.Process.Transport.RunResultfor captured direct transport execution resultsTransportErrorfor direct transport failuresTransportInfofor transport metadata snapshotsProcessExitfor normalized process exits
Start A Raw Session
Use CliSubprocessCore.RawSession when you want the core-owned raw-session
handle and normalized result collection:
{:ok, session} =
CliSubprocessCore.RawSession.start("sh", ["-c", "cat"],
stdin?: true,
stdout_mode: :raw,
stdin_mode: :raw
)
:ok = CliSubprocessCore.RawSession.send_input(session, "alpha")
:ok = CliSubprocessCore.RawSession.close_input(session)
{:ok, result} = CliSubprocessCore.RawSession.collect(session, 5_000)
IO.inspect({result.stdout, result.exit.code})RawSession.start/2,3 and start_link/2,3 still accept the shared generic
placement contract through one :execution_surface value.
Shared Execution Surface
Placement stays generic above the substrate:
:surface_kind:transport_options:target_id:lease_ref:surface_ref:boundary_class:observability
Landed built-in surface kinds are:
:local_subprocess:ssh_exec:guest_bridge
Use CliSubprocessCore.ExecutionSurface.capabilities/1,
path_semantics/1, remote_surface?/1, and nonlocal_path_surface?/1 when a
higher layer needs to reason about placement without reaching around the seam.
Direct Transport Access
Use ExecutionPlane.Process.Transport directly when you need transport-level
lifecycle control or exact non-provider one-shot execution.
alias ExecutionPlane.Command
alias ExecutionPlane.Process.Transport
command =
Command.new("sh", ["-c", "cat"],
env: %{"TERM" => "xterm-256color"}
)
ref = make_ref()
{:ok, transport} =
Transport.start(
command: command,
subscriber: {self(), ref},
startup_mode: :eager
)Supported startup options are normalized by the shared lower transport options contract:
:commandor a normalizedExecutionPlane.Command:args, default[]:cwd, defaultnil:env, default%{}:clear_env?, defaultfalse:user, defaultnil:stdout_mode,:lineor:raw, default:line:stdin_mode,:lineor:raw, default:line:pty?, defaultfalse:interrupt_mode,:signalor{:stdin, payload}:subscriber,pid()or{pid(), :legacy | reference()}:startup_mode,:eageror:lazy:task_supervisor, defaultExecutionPlane.TaskSupervisor:event_tag, default:execution_plane_process:headless_timeout_ms, default30_000:max_buffer_size, default1_048_576:max_stderr_buffer_size, default262_144:max_buffered_events, default128:stderr_callback, defaultnil:close_stdin_on_start?, defaultfalse:replay_stderr_on_subscribe?, defaultfalse:buffer_events_until_subscribe?, defaultfalse
Event Model
Direct transport subscribers receive the transport-owned mailbox contract. Legacy subscribers receive:
{:transport_message, line}{:transport_data, chunk}{:transport_error, error}whereTransportError.match?(error)is true{:transport_stderr, chunk}{:transport_exit, exit}whereProcessExit.match?(exit)is true
Tagged subscribers receive:
{event_tag, ref, {:message, line}}{event_tag, ref, {:data, chunk}}{event_tag, ref, {:error, error}}whereTransportError.match?(error)is true{event_tag, ref, {:stderr, chunk}}{event_tag, ref, {:exit, exit}}whereProcessExit.match?(exit)is true
Use ExecutionPlane.Process.Transport.extract_event/2 instead of
hard-coding the outer event atom:
receive do
message ->
case ExecutionPlane.Process.Transport.extract_event(message, ref) do
{:ok, {:message, line}} -> IO.puts(line)
{:ok, {:exit, exit}} -> IO.inspect(exit.code)
:error -> :ignore
end
endCliSubprocessCore.RawSession keeps the same underlying transport event
payloads while carrying them through a core-owned session handle.
IO Operations
ExecutionPlane.Process.Transport.send/2 normalizes payloads through the
active stdin mode:
- line mode appends a trailing newline when needed
- raw mode preserves exact bytes
:ok = ExecutionPlane.Process.Transport.send(transport, %{kind: "ping"})
:ok = ExecutionPlane.Process.Transport.end_input(transport)end_input/1 sends EOF through the active stdin contract. interrupt/1
follows the transport-owned interrupt contract and surfaces the resulting exit
as a ProcessExit facade value.
Metadata
CliSubprocessCore.RawSession.info/1 includes a transport entry containing
transport metadata recognized by TransportInfo.match?/1
from the shared Execution Plane
transport snapshot.
That transport snapshot carries:
- generic execution-surface metadata
- retained stderr tail
- delivery metadata
- the active
stdout_mode,stdin_mode,pty?, andinterrupt_mode
It intentionally omits lower runtime handles such as BEAM pids, OS pids, ports, process-owned invocation data, and transport modules. Those details stay inside the lower runtime boundary.
The generic placement metadata remains:
surface_kindtarget_idlease_refsurface_refboundary_classobservabilityadapter_metadata
One-Shot Command Execution
For direct exact-byte execution below provider parsing, use
ExecutionPlane.Process.Transport.run/2:
alias ExecutionPlane.Command
alias ExecutionPlane.Process.Transport
command =
Command.new("sh", ["-c", "printf \"alpha\" && printf \"beta\" >&2"])
{:ok, result} =
Transport.run(command,
stderr: :stdout,
timeout: 5_000
)Supported run options are:
:stdin:timeout:stderr:close_stdin
The return value is %ExecutionPlane.Process.Transport.RunResult{} with
captured stdout, stderr, output, and normalized exit data.
Buffering And Shutdown
Buffering, stderr retention, startup modes, interrupt delivery, and forced
shutdown are transport-owned behaviors now. CliSubprocessCore.RawSession,
Channel, and Session forward those semantics upward without re-owning the
substrate internals.
See guides/shutdown-and-timeouts.md for the surfaced lifecycle contract and
guides/execution-surface-compatibility.md for the placement seam surfaced by
the core.
Chunk-First Overflow Controls
When a provider profile opts into line-based stdout framing, the raw transport now exposes the full oversize-line control set:
:max_buffer_size:oversize_line_chunk_bytes:max_recoverable_line_bytes:oversize_line_mode:buffer_overflow_mode
The intended default is :chunk_then_fail plus :fatal: try to reconstruct the complete line
within a bounded window, then raise a structured overflow error once the recoverable ceiling is
exceeded. Provider profiles should pass those values through transparently rather than silently
reverting to optimistic drop-and-continue behavior.