# `SuperPi.Env.Workspace`
[🔗](https://github.com/oleksandr/super-pi-beam/blob/v0.1.0/lib/super_pi/env/workspace.ex#L1)

The workspace of a conversation: where file and command tools act.

It is configuration, stored in the conversation's `pi.agent` document:

    "workspace" => %{"kind" => "local"}                              # default
    "workspace" => %{"kind" => "shared", "root" => "/mnt/shared/ws"}
    "workspace" => %{"kind" => "scratch"}                            # also "base", "lifetime"
    "workspace" => %{"kind" => "none"}

| Kind | Where | Survives node loss | Capabilities |
|---|---|---|---|
| `local` | The disk of the node, as before (cwd and the permission policy decide). | No | `fs`, `exec`, `net` |
| `shared` | A directory on storage that every node mounts. Paths are confined to `root`. Writes take a cross-node lock. | Yes | `fs`, `exec`, `net` |
| `scratch` | A temporary directory on the node that runs the task, confined, removed when the task ends. `"lifetime" => "conversation"` keeps it until the sweep (`scratch_ttl_hours`, default 24). `"base"` or `config :super_pi, scratch_dir` sets the parent directory. | No | `fs`, `exec`, `net` |
| `none` | No filesystem or commands. The agent has the artifact tools, MCP tools and the web. | n/a | `net` |

## Network

`net` is on in every kind. `"network" => false` in the workspace map removes it, so a deployment
can run an agent that reads a repository and cannot make outgoing calls: web, MCP and every other
tool that declares a `net_read` or `net_write` effect is then not offered and is refused if called.
This differs from `Permissions` (`network: "deny"`): the workspace decides whether the model is
shown a network tool, the policy decides whether one call is allowed.

## Capability of a tool

A tool states its effects (`SuperPi.Tools.effects/2`) and the effects give the capability:
`fs_write` needs `fs`, `exec` needs `exec`, `net_read` and `net_write` need `net`, `read` and
`state_write` need nothing. The built-in file and shell tools are matched by name, because `read`
is also the effect of tools that read Super-Pi state. Artifact tools need `artifacts`. A tool that
states no effects needs `fs`, `exec` and `net`, so an unknown tool is not offered where any of
them is missing.

Artifact tools (`SuperPi.Artifacts`) are offered with `shared`, `scratch` and `none`, and with
`local` when the workspace sets `"artifacts" => true`. A tool whose capability the workspace
does not have is not offered and is refused if called. The tools never fall back to the local
disk: a `shared` workspace without a `root` is an error.

Confinement of `shared` and `scratch` follows symlinks, so a link inside the workspace cannot
lead out of it. It applies to file tools. A shell command runs with the root as its working
directory but is not sandboxed by this module; use an OS-level sandbox for untrusted commands.

# `t`

```elixir
@type t() :: %SuperPi.Env.Workspace{
  artifacts?: term(),
  capabilities: term(),
  confined?: term(),
  env: term(),
  kind: term(),
  notice: term(),
  root: term()
}
```

# `build`

Resolves a workspace from its configuration map.

# `capability_for`

First capability a tool needs, or `nil`. Kept for callers that ask about one capability.

# `cleanup_scratch`

Removes the scratch directory of a finished task.

# `contain!`

Absolute path for `path` inside a confined workspace. A relative path is joined to `base` (the
root, or a `cd` inside it). Raises `throw({:workspace_error, message})` for a path outside the root.

# `offered_tools`

```elixir
@spec offered_tools([map()], map()) :: [map()]
```

The tool list to offer a conversation: tools the workspace cannot serve are removed and the
artifact tools are added when the workspace has them. Safe to call on every request.

# `required_capabilities`

```elixir
@spec required_capabilities(String.t(), map() | nil) :: [atom()]
```

Capabilities a tool needs, all of them: a list of `:fs`, `:exec`, `:net` or `:artifacts`.
`agent_cfg` supplies the tool's declared effects (`SuperPi.Tools.effects/2`). See the module doc.

# `resolve`

```elixir
@spec resolve(map()) :: {:ok, t()} | {:error, String.t()}
```

Resolves the workspace for a tool call. `ctx` has `:conversation_id` and, in a turn, `:active_task_id`.

# `supports?`

True when the workspace can serve the tool.

# `sweep`

Removes scratch directories that nothing needs: the directory of a task that is no longer
running, and any directory idle longer than `scratch_ttl_hours` (default 24). Returns the number
of directories removed.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
