> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instructorphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 3 execution policy

## Introduction

The `ExecutionPolicy` class is the central configuration object for every sandbox execution. It controls how long a command may run, how much memory it may consume, which files it can access, which environment variables are available, whether network access is permitted, and how much output is retained.

The policy is **immutable**: every `with*()` method returns a new `ExecutionPolicy` instance, leaving the original unchanged. This makes policies safe to share across services, store in configuration, and compose through method chaining without any risk of side effects.

```php theme={null}
use Cognesy\Sandbox\Config\ExecutionPolicy;

$base = ExecutionPolicy::in('/tmp')->withTimeout(10);

// $base is unchanged -- $extended is a new instance
$extended = $base->withMemory('256M')->withNetwork(true);
// @doctest id="7aad"
```

## Creating a Policy

### From a Directory

The most common way to create a policy is with the `in()` static factory, which sets the base working directory:

```php theme={null}
$policy = ExecutionPolicy::in('/var/sandbox');
// @doctest id="0079"
```

The base directory must exist and be writable. For container drivers (Docker, Podman, Firejail, Bubblewrap), a unique temporary subdirectory is created inside this path for each execution and automatically cleaned up afterward.

### Default Policy

When you do not need a specific directory, use `default()`, which sets the base directory to `/tmp`:

```php theme={null}
$policy = ExecutionPolicy::default();
// @doctest id="8a0e"
```

### Default Values

A freshly created policy uses the following defaults:

| Setting              | Default                                | Description                         |
| -------------------- | -------------------------------------- | ----------------------------------- |
| `baseDir`            | `/tmp` (or the value passed to `in()`) | Working directory / temp parent     |
| `timeoutSeconds`     | `5`                                    | Maximum wall-clock duration         |
| `idleTimeoutSeconds` | `null` (disabled)                      | Maximum time without output         |
| `memoryLimit`        | `128M`                                 | Memory cap (container drivers)      |
| `readablePaths`      | `[]`                                   | Extra paths mounted as read-only    |
| `writablePaths`      | `[]`                                   | Extra paths mounted as read-write   |
| `env`                | `[]`                                   | Explicit environment variables      |
| `inheritEnv`         | `false`                                | Whether to inherit host environment |
| `networkEnabled`     | `false`                                | Whether network access is allowed   |
| `stdoutLimitBytes`   | `1048576` (1 MB)                       | Maximum retained stdout             |
| `stderrLimitBytes`   | `1048576` (1 MB)                       | Maximum retained stderr             |

## Timeout Configuration

### Wall-Clock Timeout

The wall-clock timeout defines the maximum number of seconds a command may run before it is forcefully terminated. The minimum allowed value is 1 second.

```php theme={null}
$policy = $policy->withTimeout(30); // 30 seconds
// @doctest id="7932"
```

When a timeout occurs, the resulting `ExecResult` will have `timedOut()` returning `true` and an exit code of `124` (matching the GNU `timeout` convention).

### Idle Timeout

The idle timeout terminates a command if it produces no output for the specified number of seconds. This is useful for detecting stuck processes that are technically still running but have stopped making progress.

```php theme={null}
$policy = $policy->withIdleTimeout(10); // Kill after 10 seconds of silence
// @doctest id="add4"
```

To disable the idle timeout (the default), pass `null`:

```php theme={null}
$policy = $policy->withIdleTimeout(null);
// @doctest id="443c"
```

The idle timeout is tracked independently of the wall-clock timeout. A command is terminated as soon as either limit is reached, whichever comes first. The `TimeoutTracker` internally records the reason (`TimeoutReason::WALL` or `TimeoutReason::IDLE`) for diagnostic purposes.

## Memory Limit

The memory limit controls the maximum amount of RAM a sandboxed process may use. It is enforced by container drivers (Docker, Podman) via `--memory` flags. The host driver does not enforce memory limits at the OS level.

```php theme={null}
$policy = $policy->withMemory('256M');
// @doctest id="ba13"
```

Accepted formats include numeric values with `K`, `M`, or `G` suffixes (e.g., `512K`, `256M`, `1G`). The value is normalized internally to megabytes and clamped to a maximum of 1 GB. An `InvalidArgumentException` is thrown for invalid formats or if `-1` (unbounded) is passed.

```php theme={null}
// All of these are valid
$policy->withMemory('512M');  // 512 megabytes
$policy->withMemory('1G');    // Clamped to 1024M
$policy->withMemory('65536K'); // Normalized to 64M
// @doctest id="7ee3"
```

## File-System Access

Container and sandbox drivers restrict file-system access by default. Only the working directory is writable. To grant access to additional paths, use the `withReadablePaths()` and `withWritablePaths()` methods.

### Readable Paths

Mount host paths as read-only inside the sandbox:

```php theme={null}
$policy = $policy->withReadablePaths('/data/config', '/etc/app');
// @doctest id="62aa"
```

In container drivers, these are mounted at sequential container paths: `/mnt/ro0`, `/mnt/ro1`, and so on. Your command should reference these container paths, not the host paths.

### Writable Paths

Mount host paths as read-write inside the sandbox:

```php theme={null}
$policy = $policy->withWritablePaths('/data/output', '/var/cache');
// @doctest id="80da"
```

In container drivers, these are mounted at `/mnt/rw0`, `/mnt/rw1`, etc.

### Important Notes

* Both methods **replace** the current list of paths. Pass all paths in a single call:

```php theme={null}
// Correct -- both paths are included
$policy = $policy->withReadablePaths('/data/a', '/data/b');

// Incorrect -- only '/data/b' survives
$policy = $policy->withReadablePaths('/data/a');
$policy = $policy->withReadablePaths('/data/b');
// @doctest id="c667"
```

* Paths containing symlinks, `..` components, or colons are silently skipped for safety. Use `realpath()` to resolve symlinks before passing paths to the policy.

## Environment Variables

By default, the sandboxed process receives no environment variables from the host. You can pass specific variables and optionally inherit the host environment.

### Explicit Variables

Pass an associative array of key-value pairs:

```php theme={null}
$policy = $policy->withEnv([
    'APP_ENV' => 'testing',
    'LOG_LEVEL' => 'debug',
]);
// @doctest id="d72c"
```

### Inheriting the Host Environment

To start with the host's environment and then overlay your own variables, pass `inherit: true`:

```php theme={null}
$policy = $policy->withEnv(
    ['APP_ENV' => 'staging'],
    inherit: true,
);
// @doctest id="f776"
```

You can also toggle inheritance independently:

```php theme={null}
$policy = $policy->inheritEnvironment(true);
// @doctest id="aca0"
```

### Blocked Variables

Certain security-sensitive environment variables are **always** stripped, regardless of inheritance settings. This is a hard-coded safety measure that cannot be overridden. The blocked patterns include:

* **Dynamic linker:** `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `DYLD_LIBRARY_PATH`, `DYLD_FRAMEWORK_PATH`
* **PHP configuration:** `PHP_INI_SCAN_DIR`, `PHPRC`
* **Cloud credentials:** `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `GOOGLE_APPLICATION_CREDENTIALS`, `GCP_*`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`
* **Language tooling:** `GEM_HOME`, `GEM_PATH`, `RUBY*`, `NODE_OPTIONS`, `NPM_*`, `PYTHON*`, `PIP_*`

## Network Access

Network access is disabled by default. Enable it when your command needs to reach external services:

```php theme={null}
$policy = $policy->withNetwork(true);
// @doctest id="8f4a"
```

For container drivers, this controls the `--network` flag (`none` vs. default bridge). For the host driver, this setting serves as a policy declaration only -- no OS-level network restriction is enforced. For Firejail, the `--net=none` flag is used. For Bubblewrap, the `--unshare-net` flag isolates the network namespace.

## Output Caps

Output caps limit how much stdout and stderr data is retained in memory. When a stream exceeds its cap, only the most recent bytes (up to the cap size) are kept, and the corresponding `truncatedStdout()` or `truncatedStderr()` flag is set on the result.

```php theme={null}
$policy = $policy->withOutputCaps(
    stdoutBytes: 5 * 1024 * 1024,  // 5 MB for stdout
    stderrBytes: 1 * 1024 * 1024,  // 1 MB for stderr
);
// @doctest id="2b14"
```

The minimum cap is 1024 bytes. Values below this threshold are automatically clamped upward. The default cap is 1 MB (1,048,576 bytes) for each stream.

Output caps protect your application from memory exhaustion when running commands that produce large amounts of output. The streaming callback (if provided) still receives all chunks in real time, even when the retained buffer is truncated.

## The `with()` Method

For advanced use cases, you can set multiple policy properties in a single call using the general-purpose `with()` method. Any parameter you omit retains its current value:

```php theme={null}
$policy = $policy->with(
    timeoutSeconds: 60,
    memoryLimit: '512M',
    networkEnabled: true,
    inheritEnv: true,
);
// @doctest id="4049"
```

This is the same mechanism that all `with*()` convenience methods use internally.

## Accessing Policy Values

Every policy setting has a corresponding accessor method:

```php theme={null}
$policy->baseDir();           // string
$policy->timeoutSeconds();    // int
$policy->idleTimeoutSeconds(); // ?int
$policy->memoryLimit();       // string (e.g., "128M")
$policy->readablePaths();     // list<string>
$policy->writablePaths();     // list<string>
$policy->env();               // array<string, string>
$policy->inheritEnv();        // bool
$policy->networkEnabled();    // bool
$policy->stdoutLimitBytes();  // int
$policy->stderrLimitBytes();  // int
// @doctest id="8459"
```

These accessors are useful when building custom drivers or when your application logic needs to inspect the policy (for example, to display timeout values in a UI).
