Metadata-Version: 2.4
Name: sandboxa
Version: 0.2.0
Summary: Run commands in a restricted user systemd sandbox
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: pydantic>=2
Requires-Dist: ruamel-yaml
Description-Content-Type: text/markdown

# sandboxa

`sandboxa` runs a command inside a restricted environment scoped to a
target project directory.

It is designed to reduce accidental host exposure while still allowing practical
project work.

## What it does

- Launches via `systemd-run --user --pty`.
- Applies isolation properties:
  - `ProtectHome=tmpfs`
  - `TemporaryFileSystem=$HOME:rw,mode=700,...`
  - `PrivateTmp=true`
  - `PrivateDevices=true`
- Sets the command working directory to the target project path.
- Binds the target project path into the sandbox.
- Optionally binds extra paths (`--extra-data-path` / YAML `extra_data_paths`).
- Optionally binds OpenCode configuration paths when `allow_settings`
  includes `opencode`.
- Applies `InaccessiblePaths` for YAML `excluded_paths` entries.
- Refuses to run if a marker path exists in project (default: `.profile`).

## Security model

- Uses `systemd-run --user` isolation properties to reduce what the launched
  process can see from the host (`ProtectHome=tmpfs`, a writable tmpfs mounted
  at `$HOME`, `PrivateTmp=true`, `PrivateDevices=true`).
- Grants filesystem access by explicit bind mounts only:
  - target project path
  - optional OpenCode config paths when `allow_settings` includes `opencode`
  - optional `extra_data_paths`
- Supports explicit deny-listing inside project scope via `excluded_paths`
  (`InaccessiblePaths`) for paths that currently exist.
- Adds a safety guard against accidentally sandboxing a complete home-like
  directory by refusing to run when the marker path exists
  (default: `.profile`).
- Applies strict config validation (`pydantic`, unknown YAML keys are rejected)
  to prevent silent misconfiguration.

## Non-goals

- Not a full container/VM boundary; this is a practical isolation wrapper,
  not a hardened multi-tenant sandbox.
- Not a replacement for system-level mandatory access controls or seccomp
  policy authoring.
- Not a network isolation tool; networking behavior is not managed here.
- Not a privilege-escalation mechanism; it runs as the invoking user via
  `systemd-run --user`.
- Not a generic policy engine; supported controls are intentionally narrow and
  centered on path exposure for local development workflows.

## Requirements

- Linux with user `systemd` session support (`systemd-run --user`).
- Python 3.
- Python packages:
  - `ruamel.yaml`
  - `pydantic`

Install dependencies:

```shell
pip install pydantic ruamel.yaml
```

Install sandboxa:

```shell
pipx install sandboxa
```

## Usage

```shell
./sandboxa \
  [RUN_COMMAND] \
  [--project-path PATH] \
  [--extra-data-path PATH ...] \
  [--excluded-path PATH ...] \
  [--allow-settings NAME ...] \
  [--refuse-if-exists NAME] \
  [--config-file FILE] \
  [--no-reconnect] \
  [--dry-run] \
  [-- COMMAND_ARGS...]
```

Common examples:

```shell
# Run default command (opencode) in current directory
./sandboxa

# Run bash in current directory sandbox
./sandboxa bash

# Target explicit project directory
./sandboxa --project-path /path/to/project

# Pass args to run command
./sandboxa bash -- -lc "echo smoke-ok"

# Add extra bind paths
./sandboxa --extra-data-path /data/shared --extra-data-path /mnt/cache

# Override excluded paths from CLI
./sandboxa --excluded-path build/ --excluded-path secrets/

# Do not check for existing sessions to be resumed.
./sandboxa opencode --no-reconnect

# Preview effective config and command without executing
./sandboxa bash --dry-run -- -lc "echo smoke-ok"
```

## Configuration file

By default the script reads user config from `~/.config/sandboxa.yaml` and
project config from `.sandboxa.yaml` in the target project directory.
Use `--config-file` to override the project config path; absolute paths are
supported.

Supported keys:

- `allow_settings: <list[string]>` (see *Settings Templates* below)
- `extra_data_paths: <list[path]>`
- `excluded_paths: <list[string]>`
- `refuse_if_exists: <string>`

Example:

```yaml
allow_settings:
  - opencode

extra_data_paths: []

excluded_paths:
  - build/
  - secrets/

refuse_if_exists: .profile
```

Precedence:

- CLI arguments override YAML values.
- Project YAML values override user YAML values for most keys.
- For `allow_settings` and `excluded_paths`, user and project YAML values are merged.
- YAML values override built-in defaults.

## Settings Templates (`allow_settings`)

Even in a constrained environment there is sometimes the need to use data from your real home
directory.
This could be the configuration of your favorite editor or the cache of your userspace package
manager.

Some popular use-cases are implemented in *sandboxa* in order to ease the burden of finding the
required files to be accessible in the sandbox.

The following contexts are supported.

| Keyword for `allow_settings` | Purpose | Limitations |
|--|--|--|
| `opencode` | [OpenCode](https://opencode.ai/) | - |
| `podman` | [Podman](https://www.vim.org/) | not working for root-less containers (see [issue](https://github.com/containers/podman/discussions/21739)) |
| `ssh` | OpenSSH agent and key material | exposes `~/.ssh` and optionally `SSH_AUTH_SOCK` if set |
| `vim` | [VIM](https://www.vim.org/) | without history (use `extra-data_path: [~/.vim/]` if you really need it) |

## License

This project is licensed under GNU GPL v3 or later.
See `LICENSE` for the full text.
