Skip to main content

Workspaces

How a folder or a git project gives an agent files, and what keeps that safe in a sandbox.

The folder workspace​

kind: Project
metadata: {name: notes}
spec:
workspace:
type: folder
path: ./notes

An agent with workspaceAccess reads the folder through ws.tree, ws.read and ws.search. When it is deep with workspaceAccess: write, it also writes the folder through ws.write, ws.edit and ws.delete.

Every path is confined. .. and absolute paths are refused, and every path component is opened from the root without following symlinks (O_NOFOLLOW). A symlink anywhere on a path is refused, even one pointing inside the root, or one created inside the sandbox while a call runs.

The git workspace​

kind: Project
metadata: {name: app}
spec:
workspace:
type: git
url: https://github.com/acme/app # or `path: /srv/app`
defaultBranch: main
tokenEnv: GITHUB_TOKEN # a private repository
sandbox: {image: afe-sandbox:test, cpus: 2, memory: 2g, timeout: 300}
ci:
script: python3 -m pytest -q
test: python3 -m pytest -q # the command behind `ws.test`; defaults to `script`
  • The project is cloned once into <workDir>/repos/<project>, and fetched before each ticket.
  • A ticket works in an integration worktree on afe/<ticket>/integration, cut from defaultBranch.
  • A lane with workspace.worktree gets its own branch and worktree, afe/<ticket>/<scope>, cut from the integration branch. scope is the lane's worktree value, with the lane's params already substituted.
  • Reopening after a restart finds the existing worktrees instead of recreating them. On the host path, worktrees stay after the ticket ends. A cleanup command is on the backlog.

When the Runtime has a sandbox plugin, a git project's workspace lives inside the ticket's sandbox. That is one sandbox per ticket (a Pod, or a container) on a volume of its own, with the repository at /workspace/repo and the worktrees at /workspace/worktrees/<scope>.

The host path above stays only for a runtime without a sandbox plugin. afe run --local takes the same path, so local and cluster runs share one code path, and the sandbox image must contain git (docker/sandbox does). On Kubernetes, a folder project is refused, because the worker has no shared storage.

  • The file tools run inside. ws.tree, ws.read and ws.search are argv of standard POSIX tools (find -P, cat, grep) run through the sandbox exec, and ws.write goes through put. The engine still checks every path (relative, no .., no symlink on it) before it runs, so an arbitrary Project.sandbox.image needs only those tools. ws.edit reads the file out, replaces the text once and writes it back. ws.exec, ws.test, a script node and an acp agent run in the same sandbox.
  • Git runs inside too, hardened. Clone, worktrees, commit and merge go through SandboxGit with no hooks, no core.fsmonitor and no system or global config. The clone arrives as a git bundle the worker cuts with the token, so no token ever enters the sandbox.
  • The workspace is durable. After every node that ran the sandbox, the engine checkpoints it. A dirty worktree becomes a WIP commit on a separate ref (refs/afe-wip/<ticket>/<scope>), leaving the ticket's branches and index untouched. An incremental git bundle of afe/* and refs/afe-wip/* is saved in the store, keyed by ticket and step. A checkpoint of an unchanged workspace saves nothing, and checkpoints of one ticket run one at a time.
  • A lost sandbox is recreated, and the interrupted node runs again. A sandbox that fails an exec because it is gone, or that never becomes ready, is removed with its volume, and the job goes back to the queue. The next worker restores the repository, the branches and the worktrees from the stored bundles, and re-runs the interrupted node from its start (its turn token is dropped). A completed node is never repeated, but the interrupted node's paid calls may be. Files ignored by git (.venv, node_modules, build output) are not in the bundles and are not restored: a flow must be able to rebuild them.
  • One volume per ticket is a trust change. The agent's shell can reach every worktree and the .git folder of its ticket, so the engine's own git runs hardened as above. The pull request and its approval stay the barrier before anything reaches the default branch.
  • Lifecycle. Opening a node's sandbox is idempotent: the same ticket and scope reuse the same sandbox, and a signature change (image, limits, env names, egress) recreates it. The sandbox is removed when the ticket ends (done, failed, cancelled). A paused ticket keeps it while it waits (scaling a paused ticket's Pod to zero is backlog, V7c).

git.log and git.diff read the branch a node works on. They are read-only.

The work folder​

On the host path, clones and worktrees live under Runtime.spec.workDir (default ~/.afe-work). For a sandboxed git project, the ticket's repository lives inside the sandbox, and the worker keeps only temporary clones and bundles under workDir for the duration of a call.

Every worker uses the same folder: the same machine, or a shared volume. afe worker and afe run --local check at startup that it exists or can be created, and is writable. Otherwise they refuse to start.

kind: Runtime
metadata: {name: prod}
spec:
store: {type: postgres, dsnEnv: AFE_POSTGRES_DSN}
workDir: /srv/afe-work
sandbox: {type: docker}

Written files​

Every file an agent writes is recorded by ticket, round, node, activation, scope and path. Two parallel branches of the same round writing the same path in the same scope is a conflict: the ticket goes to needs_human. The same path in two git worktrees is two different files. A human node with attachFiles: true lists the files written in the ticket so far, in its request.

Troubleshooting​

A ticket goes to needs_human with a write conflict. Two parallel branches (two lanes, or a lane and the main flow) wrote the same path in the same round and scope. Route the flow so the paths do not overlap, or merge the branches before both write, then answer the pending request.

A folder project is refused on Kubernetes The worker has no shared storage on Kubernetes, so it cannot serve a plain folder to every node. Use a git workspace instead, even for a repository with no remote (path: /srv/app works without a URL).

See also​