# Raw Sessions And Transport
`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.RawSession` for a stable raw-session handle above the
extracted transport layer
- `ExecutionPlane.Process.Transport` for direct transport lifecycle control
- `ExecutionPlane.Process.Transport.Options` for validated startup options
- `ExecutionPlane.Process.Transport.RunOptions` for validated one-shot
execution options
- `ExecutionPlane.Process.Transport.RunResult` for captured direct transport
execution results
- `TransportError` for direct transport failures
- `TransportInfo` for transport metadata snapshots
- `ProcessExit` for normalized process exits
## Start A Raw Session
Use `CliSubprocessCore.RawSession` when you want the core-owned raw-session
handle and normalized result collection:
```elixir
{: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.
```elixir
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:
- `:command` or a normalized `ExecutionPlane.Command`
- `:args`, default `[]`
- `:cwd`, default `nil`
- `:env`, default `%{}`
- `:clear_env?`, default `false`
- `:user`, default `nil`
- `:stdout_mode`, `:line` or `:raw`, default `:line`
- `:stdin_mode`, `:line` or `:raw`, default `:line`
- `:pty?`, default `false`
- `:interrupt_mode`, `:signal` or `{:stdin, payload}`
- `:subscriber`, `pid()` or `{pid(), :legacy | reference()}`
- `:startup_mode`, `:eager` or `:lazy`
- `:task_supervisor`, default `ExecutionPlane.TaskSupervisor`
- `:event_tag`, default `:execution_plane_process`
- `:headless_timeout_ms`, default `30_000`
- `:max_buffer_size`, default `1_048_576`
- `:max_stderr_buffer_size`, default `262_144`
- `:max_buffered_events`, default `128`
- `:stderr_callback`, default `nil`
- `:close_stdin_on_start?`, default `false`
- `:replay_stderr_on_subscribe?`, default `false`
- `:buffer_events_until_subscribe?`, default `false`
## Event Model
Direct transport subscribers receive the transport-owned mailbox contract.
Legacy subscribers receive:
- `{:transport_message, line}`
- `{:transport_data, chunk}`
- `{:transport_error, error}` where `TransportError.match?(error)` is true
- `{:transport_stderr, chunk}`
- `{:transport_exit, exit}` where `ProcessExit.match?(exit)` is true
Tagged subscribers receive:
- `{event_tag, ref, {:message, line}}`
- `{event_tag, ref, {:data, chunk}}`
- `{event_tag, ref, {:error, error}}` where `TransportError.match?(error)` is true
- `{event_tag, ref, {:stderr, chunk}}`
- `{event_tag, ref, {:exit, exit}}` where `ProcessExit.match?(exit)` is true
Use `ExecutionPlane.Process.Transport.extract_event/2` instead of
hard-coding the outer event atom:
```elixir
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
end
```
`CliSubprocessCore.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
```elixir
: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?`, and `interrupt_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_kind`
- `target_id`
- `lease_ref`
- `surface_ref`
- `boundary_class`
- `observability`
- `adapter_metadata`
## One-Shot Command Execution
For direct exact-byte execution below provider parsing, use
`ExecutionPlane.Process.Transport.run/2`:
```elixir
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.