ExAthena.Tools.Bash (ExAthena v0.20.0)

Copy Markdown View Source

Executes a shell command via /bin/sh -c with a configurable timeout.

Arguments:

  • command (required) — the shell command.
  • timeout_ms (optional, default 120_000, max 600_000).

Returns captured stdout+stderr plus the exit code. Timeouts kill the spawned process and surface {:error, :timeout} to the loop.

Runs with cd: ctx.cwd, stderr_to_stdout: true. No input redirection.

When the run is confined (ctx.allowed_roots is set), the command is wrapped in an OS sandbox (ExAthena.Sandbox) that blocks writes outside the roots. If no sandbox helper (sandbox-exec/bwrap) is available the tool fails closed: it refuses to run and returns {:error, {:sandbox_unavailable, helper}} naming the missing helper — running unconfined would silently break the confinement contract, and a command-string scan is never a substitute (trivially bypassed). Hosts that accept degradation opt in with confine: :best_effort on the run (ctx.confine_mode == :best_effort), which runs the command unconfined with a logged warning. Either way an [:ex_athena, :sandbox, :unavailable] telemetry event is emitted.

Summary

Functions

Classify a bash invocation's args as read-only or not. Used by the permissions layer to allow read-only bash calls (cat, ls, grep, git log, gh issue view, …) during :plan phase while still denying mutations (rm, mkdir, redirects, git add/commit, mix ecto.migrate, …).

Why args failed to classify read-only, or nil when it is read-only.

Functions

read_only_command?(args)

@spec read_only_command?(map()) :: boolean()

Classify a bash invocation's args as read-only or not. Used by the permissions layer to allow read-only bash calls (cat, ls, grep, git log, gh issue view, …) during :plan phase while still denying mutations (rm, mkdir, redirects, git add/commit, mix ecto.migrate, …).

Allowlist-based: a command is read-only only when every shell segment (split on ;, &&, ||, |, &, newline) starts with a command known to be read-only. Unknown commands — including interpreter one-liners like python -c, perl -e, ruby -e, node -e, and editors like ex/ed — are treated as MUTATING, because arbitrary code execution can never be proven read-only by a string scan. Command substitution ($(…), backticks) and file redirects (except 2>&1-style fd duplication and /dev/null discards) also disqualify a command.

Missing/empty command → false (treat as not read-only so the model gets a clear phase-gated denial rather than a silent allow).

This classifier gates the :plan phase only; it is advisory next to the OS sandbox (ExAthena.Sandbox) which enforces write confinement for confined runs regardless of phase.

read_only_violation(arg1)

@spec read_only_violation(map()) ::
  nil | %{reason: String.t(), segment: String.t() | nil}

Why args failed to classify read-only, or nil when it is read-only.

Returns %{reason: String.t(), segment: String.t() | nil}. segment names the offending command ("cd", "git commit") when a single shell segment is at fault, and is nil when the whole invocation is disqualified by a construct — command substitution, a write pattern, a file redirect.

read_only_command?/1 answers whether; a model that chained four segments also needs which. Denying with only "not recognized as read-only" sent a live subagent into a retry loop, re-sending the same shape three times because nothing told it which part lost.