docs/adr/0009-local-executor-no-sandbox.md

# ADR-0009: Local executor without sandboxing

## Status

Accepted

## Context

Conjure must execute commands and file operations from skill instructions. The simplest implementation uses Elixir's `System.cmd/3` to run commands directly on the host:

```elixir
System.cmd("bash", ["-c", command], opts)
```

This provides no isolation between skill execution and the host system. A malicious or buggy skill could:

- Read/write any file accessible to the BEAM process
- Execute arbitrary commands
- Access network resources
- Consume unlimited resources
- Access environment variables and secrets

For production deployments, this is unacceptable. However, sandboxing adds complexity:

- Docker requires daemon installation and permissions
- Firecracker/gVisor require specialized setup
- seccomp/AppArmor require Linux-specific configuration
- All isolation mechanisms add latency

During development and testing, developers often:

- Run trusted skills they wrote themselves
- Need fast iteration without container overhead
- Work on systems without Docker (Windows without WSL, restricted environments)
- Debug skill behavior with direct filesystem access

## Decision

We will provide `Conjure.Executor.Local` as an unsandboxed executor for development use only.

The module will:

1. Execute commands directly via `System.cmd/3`
2. Perform file operations via Elixir's `File` module
3. Include prominent documentation warnings
4. Log warnings when used
5. Be explicitly named to indicate its nature

```elixir
defmodule Conjure.Executor.Local do
  @moduledoc """
  Local execution backend using System.cmd.

  ## Security Warning

  This executor provides NO SANDBOXING. Commands and file operations
  execute with the same permissions as the BEAM process.

  **DO NOT USE IN PRODUCTION** unless you fully trust all loaded skills
  and accept the security implications.

  For production deployments, use `Conjure.Executor.Docker` or implement
  a custom executor with appropriate isolation.
  """

  require Logger

  @behaviour Conjure.Executor

  @impl true
  def init(context) do
    Logger.warning(
      "[Conjure] Using Local executor - NO SANDBOXING. " <>
      "Do not use in production with untrusted skills."
    )
    {:ok, context}
  end

  @impl true
  def bash(command, context) do
    System.cmd("bash", ["-c", command],
      cd: context.working_directory,
      stderr_to_stdout: true
    )
    |> handle_result()
  end

  # ... other implementations
end
```

The Local executor will be the default only if no other executor is configured, ensuring users make an explicit choice for production.

## Consequences

### Positive

- Zero setup required for development
- Fast execution (no container overhead)
- Works on any platform with Elixir
- Simple debugging (direct filesystem access)
- No external dependencies
- Enables rapid skill development iteration

### Negative

- Security risk if misused in production
- No resource limits (memory, CPU, time)
- No filesystem isolation
- No network isolation
- Users may forget to switch executors for production

### Neutral

- Warnings logged on every initialization
- Documentation emphasizes security implications
- Production configuration should explicitly set Docker executor

## Mitigations

While we accept the security tradeoffs for development, we implement some basic protections:

### 1. Path Validation

```elixir
def view(path, context, _opts) do
  with {:ok, safe_path} <- validate_path(path, context.allowed_paths) do
    File.read(safe_path)
  end
end

defp validate_path(path, allowed_paths) do
  normalized = Path.expand(path)

  if Enum.any?(allowed_paths, &String.starts_with?(normalized, Path.expand(&1))) do
    {:ok, normalized}
  else
    {:error, :path_not_allowed}
  end
end
```

### 2. Timeout Enforcement

```elixir
def bash(command, context) do
  task = Task.async(fn ->
    System.cmd("bash", ["-c", command], opts)
  end)

  case Task.yield(task, context.timeout) || Task.shutdown(task) do
    {:ok, result} -> handle_result(result)
    nil -> {:error, :timeout}
  end
end
```

### 3. Configuration Warnings

```elixir
# In production config
config :conjure, executor: Conjure.Executor.Docker

# If Local is used in prod, log error on startup
def check_executor_safety do
  if Mix.env() == :prod and executor() == Conjure.Executor.Local do
    Logger.error(
      "[Conjure] Local executor used in production! " <>
      "This is a security risk. Set `config :conjure, executor: Conjure.Executor.Docker`"
    )
  end
end
```

## Alternatives Considered

### No local executor

Require Docker or other sandbox for all execution. Rejected because:

- Blocks development on systems without Docker
- Adds friction for getting started
- Hurts adoption

### Local with seccomp/Landlock

Use Linux security modules for some isolation. Rejected because:

- Linux-only
- Complex configuration
- Partial protection gives false confidence

### Local with user namespaces

Run as unprivileged user. Rejected because:

- Linux-only
- Still allows network access
- Complex setup

### Warn once only

Only log warning on first use. Rejected because:

- Easy to miss in logs
- Subsequent sessions won't see warning
- Per-init warning is minimal overhead

## References

- [System.cmd/3 documentation](https://hexdocs.pm/elixir/System.html#cmd/3)
- [OWASP Command Injection](https://owasp.org/www-community/attacks/Command_Injection)
- [Docker security best practices](https://docs.docker.com/engine/security/)