BeamAgent.CommandValidator behaviour (beam_agent_ex v0.1.0)

Copy Markdown View Source

Behaviour for command execution validation (Layer 2).

Implement this behaviour to define custom security policies for command execution. The validator is called after static policy evaluation (Layer 1) and before the security guard (Layer 3) applies rate limits and temporal pattern detection.

The default implementation (beam_agent_command_validator_default) defers to the policy result. Replace it to implement deep inspection, intent-based reasoning, or integration with external security systems (e.g., Citadel).

Callbacks

  • validate/2required. Called for every command before execution. Must return quickly (the command blocks until validation completes).
  • on_execution/3optional. Called after command execution with the result. Fire-and-forget: the guard does not use the return value and catches any crash. Use this for auditing, learning, or adaptive security.

Validators that need internal state should manage it themselves (ETS, persistent_term, or a dedicated process). The guard does not hold or manage validator state.

Validation Context

The validator receives comprehensive context — not just the command string. See validation_context/0 for the full set of fields, which includes the parsed command structure, raw command, command form, session state, agent identity, working directory, environment variables, command history, static policy result, and extensible metadata.

For post-execution notification, on_execution/3 receives an execution_context/0 — the same fields minus policy_result (a pre-execution concern) and command_struct (passed as the first argument).

Configuration

# In config/config.exs:
config :beam_agent, command_validator: :beam_agent_command_validator_default

Custom Validator Example

defmodule MyValidator do
  @behaviour BeamAgent.CommandValidator

  @impl true
  def validate(%{program: "npm"}, %{agent: :claude, policy_result: :ask}), do: :allow
  def validate(_command, %{policy_result: :allow}), do: :allow
  def validate(_command, %{policy_result: {:deny, reason}}), do: {:deny, reason}

  @impl true
  def on_execution(_command, _ctx, {:ok, %{exit_code: 0}}), do: :ok
  def on_execution(_command, _ctx, _result), do: :ok
end

Summary

Types

Context passed to on_execution/3 after command execution.

Static policy evaluation result.

Context passed to validate/2 before command execution.

Callbacks

Called after command execution with the result.

Validate a command before execution.

Types

execution_context()

@type execution_context() :: %{
  raw_command: binary() | String.t() | [binary()],
  command_form: :list | :string,
  session_state: atom() | nil,
  agent: atom() | nil,
  opts: map(),
  cwd: binary() | nil,
  env: [{String.t(), String.t()}] | nil,
  history: [map()],
  timestamp: integer(),
  metadata: map()
}

Context passed to on_execution/3 after command execution.

Contains the raw command, command form, session state, agent identity, execution options, working directory, environment variables, command history, timestamp, and extensible metadata. Does not include policy_result or command_struct (those are pre-execution concerns).

policy_result()

@type policy_result() :: :allow | :ask | {:deny, binary()}

Static policy evaluation result.

  • :allow — command passes static policy
  • :ask — command requires interactive confirmation
  • {:deny, reason} — command is blocked with a binary reason

validation_context()

@type validation_context() :: %{
  command_struct: map(),
  raw_command: binary() | String.t() | [binary()],
  command_form: :list | :string,
  session_state: atom() | nil,
  agent: atom() | nil,
  opts: map(),
  cwd: binary() | nil,
  env: [{String.t(), String.t()}] | nil,
  history: [map()],
  timestamp: integer(),
  policy_result: policy_result(),
  metadata: map()
}

Context passed to validate/2 before command execution.

Extends execution_context/0 with :command_struct (the parsed command structure from Layer 0) and :policy_result (the Layer 1 static policy evaluation result).

Callbacks

on_execution(command, context, exec_result)

(optional)
@callback on_execution(
  command :: map(),
  context :: execution_context(),
  exec_result :: {:ok, map()} | {:error, any()}
) :: :ok

Called after command execution with the result.

Optional, fire-and-forget notification. The guard does not use the return value and wraps the call in a try/catch — a crash here is logged but never breaks command recording.

Use this for auditing, adaptive security, or feeding execution data into an external system. If you need internal state, manage it yourself (ETS, persistent_term, a dedicated process). The guard does not hold or thread validator state.

validate(command, context)

@callback validate(command :: map(), context :: validation_context()) ::
  :allow
  | {:deny, reason :: binary()}
  | {:deny, reason :: binary(), details :: map()}

Validate a command before execution.

Called for every command after static policy evaluation. Must return one of:

  • :allow — command proceeds to Layer 3
  • {:deny, reason} — command is blocked with the given binary reason
  • {:deny, reason, details} — command is blocked; details map is included in telemetry metadata for observability