Run commands and observe processes
Use environment.operations.shell for a bounded foreground execution, or environment.operations.processes for a command whose observation continues across calls. These are typed Provider APIs, not the model-facing shell_exec / shell_wait tool signatures.
Command access must be configured explicitly where required. The file-only Direct Local quickstart does not enable execution. Direct Local runs as your Host account; a mapped directory is not a shell sandbox.
Execute a configured executable
This complete example runs the current Python interpreter against an application-owned temporary directory. It needs no Agent, network, container, or model account:
import asyncio
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
from a13n_harness.providers.environment.builtins import select_builtin_environment_providers
from a13n_harness.providers.environment.commands import ArgvCommand, CommandRequest
from a13n_harness.providers.environment.retention import EnvironmentOutputPolicy
async def main() -> None:
(direct_local,) = select_builtin_environment_providers(("direct_local",))
with TemporaryDirectory(prefix="a13n-command-") as temporary:
executable = str(Path(sys.executable).resolve())
environment = await direct_local.create(
{
"root": {"path": str(Path(temporary).resolve())},
"allowed_executables": [executable],
},
environment_id="command-example",
)
async with environment:
await environment.ensure_ready(frozenset({"shell"}))
shell = environment.operations.shell
if shell is None:
raise RuntimeError("Shell operations are unavailable")
result = await shell.exec(
CommandRequest(
command=ArgvCommand(
executable=executable,
arguments=("-c", "print('hello from command')"),
),
cwd="/",
output_policy=EnvironmentOutputPolicy(
max_inline_bytes=4096,
max_output_bytes=4096,
overflow="fail",
),
)
)
assert result.status.exit_code == 0
assert result.output.stdout.inline == b"hello from command\n"
print(result.output.stdout.inline.decode(), end="")
asyncio.run(main())
ArgvCommand sends one executable and argument tuple without implicit shell parsing. Its eligibility comes from Provider configuration. For shell syntax, use ShellCommand(profile_id=..., script=..., login=...) and a configured shell profile; an arbitrary executable path is not a profile ID.
Command request fields
| Field | Default / meaning |
|---|---|
command |
Required ArgvCommand or ShellCommand |
cwd |
Optional logical working path in this Environment |
environment |
CommandEnvironment(set={}, unset=()); changes are checked against Provider policy |
network |
configured; deny requests supported denial, not an assumed capability |
limits |
CommandLimits() with optional wall time, stdin bytes, process count, memory, and CPU time |
initial_stdin |
Optional bytes supplied at launch |
keep_stdin_open |
False; select true when later stdin writes are intended |
output_policy |
Required finite inline/total bounds and overflow policy |
Environment variable keys must be unique across set/unset and cannot contain NUL or =. Values cannot contain NUL. At most 1,024 environment entries are accepted. Command strings cannot be empty or contain NUL; command shape is bounded to 1,025 values and 1 MiB encoded bytes. Provider-specific validation can narrow it further.
Requested limits must be positive and finite where applicable. Unsupported limits fail rather than silently becoming enforced. E2B, for example, does not support a per-command wall-time deadline even though its request timeout and sandbox TTL exist.
Start, inspect, wait, and release
After ensure_ready({"processes"}), obtain the optional processes facet. Its methods are:
| Method | Behavior |
|---|---|
start(request) |
Return ProcessStartResult with a bound process handle and receipt |
list(limit=...) |
Bounded discovery where supported; does not attach output |
rebind(identity, output_policy=...) |
Explicitly attach an eligible retained native process where supported |
inspect(handle) |
Read current status and available metadata |
wait(handle, condition=..., timeout_seconds=...) |
Wait for initial_terminal or tree_cleaned; a wait deadline is not a command deadline |
read_output(handle, ..., policy=...) |
Bounded stdout/stderr observation using explicit cursors or offsets |
write_stdin(handle, data, close_after_write=False) |
Write bytes and report accepted count/input state |
close_stdin(handle) |
Close input without reading output |
signal(handle, "interrupt" or "terminate") |
Supported cooperative control |
kill(handle) |
Separate forceful control operation |
release(handle) |
Release observation; not an implicit kill |
Use the exact handle returned by the Provider. A ProcessIdentity identifies provider, Environment, generation, and native process, but is not a portable access grant. Do not construct opaque handle payloads or copy Harness Run-local process references into Provider calls. rebind support does not mean all Providers can discover/recover arbitrary old processes.
Status includes phase, optional termination reason/exit code/signal/timestamps, and cleanup outcome. An initial exit can precede process-tree cleanup. Missing or unknown status is not evidence that a command never ran. Always distinguish command completion, output completion, and cleanup completion.
Read retained output
EnvironmentOutputPolicy requires positive max_inline_bytes, max_output_bytes, and an overflow choice of fail, truncate, or retain; inline cannot exceed total. This is not ToolOutputPolicy, whose overflow vocabulary includes spill instead.
read_output() accepts independent stdout/stderr cursors or start offsets, optional wait_seconds=0, and a policy. Apply returned chunks at their start_offset and use the returned next cursor for that stream. Output reads are observations, not mutations that consume bytes. Inspect available_start / available_end, capture kind, coverage, origin, dropped/produced counts, and completion flags rather than treating every preview as complete.
The separate outputs facet reads and releases returned retained output references. A process handle, output reference, and output cursor are different bound values. Scope/generation checks still apply. SDK-text observations may have unknown producer totals or incomplete history; repeatedly waiting does not restore discarded bytes.
When full output matters beyond an observation budget, write an application log file and manage its retention explicitly. Never automatically restart a command after a disconnect, missing handle, or output eviction.
Ports and lifecycle
The optional ports facet supports inspect(PortTarget(...)) and wait(target, desired="listening" or "not_listening", timeout_seconds=...). PortTarget carries optional alias, address selection (loopback by default or any), and port 1–65,535. This is observation, not a public ingress/tunnel service or permission to expose a port.
Provider configuration and actual descriptors determine support. Keep lifecycle recovery separate from command replay, and close a fresh adapter after each independent scope. Close releases local resources; it does not imply retained target destruction.