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 fromdefaultBranch. - A lane with
workspace.worktreegets its own branch and worktree,afe/<ticket>/<scope>, cut from the integration branch.scopeis the lane'sworktreevalue, 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.readandws.searchare argv of standard POSIX tools (find -P,cat,grep) run through the sandbox exec, andws.writegoes throughput. The engine still checks every path (relative, no.., no symlink on it) before it runs, so an arbitraryProject.sandbox.imageneeds only those tools.ws.editreads the file out, replaces the text once and writes it back.ws.exec,ws.test, ascriptnode and anacpagent run in the same sandbox. - Git runs inside too, hardened. Clone, worktrees, commit and merge go through
SandboxGitwith no hooks, nocore.fsmonitorand no system or global config. The clone arrives as agit bundlethe 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 incrementalgit bundleofafe/*andrefs/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
.gitfolder of its ticket, so the engine's owngitruns 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).