Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,63 @@ means the guarantees changed or an upgrade needs manual steps to stay safe.

---

## 1.14.4 — 2026-08-18

The lab container gets a working directory. No image changed — this release is
one key in three compose files, and the lint that keeps it there.

### Fixed

**The lab had no working directory at all.** No compose file set `working_dir`,
no lab `Dockerfile` set `WORKDIR`, and neither base image sets one either, so
`Config.WorkingDir` was empty and the container started in `/` — both for the
container's own command and for a `docker compose exec` given no `--workdir`.

That is invisible for as long as the command is `sleep infinity` and whoever
arrives `cd`s on the way in. It stops being invisible the moment an agent *is*
the command: run Claude Code that way and it takes `/` for its project — asks to
trust `/`, and files its per-project state under that key rather than the
workspace's. Reported against a `sal`-managed lab pinned to 1.14.2 whose
`compose.override.yaml` runs the agent as the lab's command; its `.claude.json`
had exactly one project key, `"/"`.

`working_dir: /workspace` does both jobs, measured against compose v5.1.4 rather
than assumed: it sets the container's `WorkingDir`, so the command starts there,
and it becomes the default for `docker compose exec`, so a shell lands there
too. One key covers both ways in.

Three files take it — `stack/compose.yaml`, `template/deployment/compose.yaml`
and `examples/dev-container/.devcontainer/compose.yaml`. The dev-container
example needs it despite `devcontainer.json` already setting `workspaceFolder`:
that covers the terminals VS Code opens, and neither the container's command nor
a hand-run `exec`. `examples/claude-code` is deliberately left alone — its lab
image carries a `WORKDIR` of its own and its project mount is commented out for
whoever copies it to fill in.

The new suite in `tests/integration/00-config-lint.test.sh` is conditional on
the mount for that reason: a lab that mounts a workspace must name it as its
working directory, and a lab that mounts none is skipped. That excludes
`examples/claude-code` by rule rather than by list, so the next deployment shape
added is judged on what it mounts.

### Upgrading

**Add `working_dir: /workspace` to the `lab` service of your own
`compose.yaml`.** Repinning does not deliver this one: the template is a file
you copied, not something fetched at build time, so the fix reaches an existing
deployment only by hand. It matters if anything in your stack runs an agent as
the lab's command — an `sal`-managed lab with a `compose.override.yaml`
`command:`, or any `docker compose run` — and is cosmetic otherwise.

If an agent has already been running in `/`, it has per-project state filed
under that key. For Claude Code that is a `"/"` entry in `~/.claude.json`
holding the trust decision and history; moving to `/workspace` starts a fresh
one, and the old entry can be dropped.

**Nothing to rebuild.** No image, bank entry or provider file changed.

---

## 1.14.3 — 2026-08-17

The stacks tier stops leaking images. Nothing in any image, template or bank
Expand Down
5 changes: 5 additions & 0 deletions examples/dev-container/.devcontainer/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ services:
# whole stack config as read-only while leaving the project writable.
- ../.devcontainer:/workspace/.devcontainer:ro
- proxy-certs:/proxy-certs:ro
# As in the template: without this there is no working directory at all and
# the container's command begins in `/`. devcontainer.json's
# `workspaceFolder` covers the terminals VS Code opens; this covers the
# command itself, and any `docker compose exec` run by hand.
working_dir: /workspace
environment:
HTTP_PROXY: http://proxy:8080
HTTPS_PROXY: http://proxy:8080
Expand Down
7 changes: 7 additions & 0 deletions stack/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ services:
# cred-gateway/gateway.d — this file is the reference skeleton, and the
# content is the deployment's.
- ./lab/setup.d:/etc/agent-setup.d:ro
# Where the container's own command starts, and where `docker compose exec`
# lands when it is given no `--workdir` of its own. Without it there is no
# working directory at all — nothing here sets one and the lab image sets
# none — so both begin in `/`. An agent run as the lab's command then takes
# `/` for its project: Claude Code asks to trust `/`, and files its
# per-project state under that key instead of the workspace's.
working_dir: /workspace
environment:
HTTP_PROXY: http://proxy:8080
HTTPS_PROXY: http://proxy:8080
Expand Down
17 changes: 12 additions & 5 deletions template/deployment/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ name: secure-agent-lab

services:
broker:
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.3:stack/broker
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.4:stack/broker
volumes:
# The credential directory. Read-only, and the only place secrets live.
- ${AGENT_CREDS_DIR:-${HOME}/.config/agent-creds}:/secrets:ro
Expand Down Expand Up @@ -74,7 +74,7 @@ services:
restart: unless-stopped

proxy:
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.3:stack/proxy
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.4:stack/proxy
depends_on:
broker:
condition: service_healthy
Expand Down Expand Up @@ -116,7 +116,7 @@ services:
restart: unless-stopped

cred-gateway:
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.3:stack/cred-gateway
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.4:stack/cred-gateway
depends_on:
broker:
condition: service_healthy
Expand Down Expand Up @@ -151,7 +151,7 @@ services:
# the dashboard, not the audit trail.
observer:
profiles: ["observer"]
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.3:stack/observer
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.4:stack/observer
volumes:
- audit-logs:/var/log/audit:ro
ports:
Expand Down Expand Up @@ -185,7 +185,7 @@ services:
# different non-root uids sharing one directory — can both write to it. Runs
# as root for exactly that reason; needs no network of its own either.
log-rotator:
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.3:stack/log-rotator
build: https://github.com/lpezet/secure-agent-lab.git#v1.14.4:stack/log-rotator
volumes:
- audit-logs:/var/log/audit
restart: unless-stopped
Expand Down Expand Up @@ -214,6 +214,13 @@ services:
# The project the agent works on. Point this wherever you like — it is
# the one mount whose contents the agent can write.
- ${WORKSPACE_DIR:-./workspace}:/workspace
# Where the container's own command starts, and where `docker compose exec`
# lands when it is given no `--workdir` of its own. Without it there is no
# working directory at all — nothing here sets one and the lab image sets
# none — so both begin in `/`. An agent run as the lab's command then takes
# `/` for its project: Claude Code asks to trust `/`, and files its
# per-project state under that key instead of the workspace's.
working_dir: /workspace
# lab.env, NOT .env. Each bank entry declares what the lab side needs under
# `lab_env` in its manifest — GH_TOKEN=proxy-injected and the like — and
# those belong in a file rather than restated here. It is a separate file
Expand Down
34 changes: 34 additions & 0 deletions tests/integration/00-config-lint.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,40 @@ for c in stack/compose.yaml template/deployment/compose.yaml examples/*/compose.
check_contains "$c — sets the gRPC CA variable" "$(cat "$c")" "GRPC_DEFAULT_SSL_ROOTS_FILE_PATH"
done

suite "a lab that mounts the workspace starts in it"
# Without working_dir there is no working directory at all — no compose file
# here sets one and the lab image sets none either — so the container's command
# begins in `/`, and so does a `docker compose exec` given no `--workdir`. An
# agent run as that command then takes `/` for its project: Claude Code asks to
# trust `/` and files its per-project state under that key rather than the
# workspace's, which stays invisible until a container recreate loses it.
#
# Conditional on the mount, because examples/claude-code deliberately has none:
# its lab image carries a WORKDIR of its own and leaves the project mount to
# whoever copies it.
if ! python3 -c 'import yaml' 2>/dev/null; then
skip "PyYAML unavailable — cannot read the lab service" ""
else
for c in "${COMPOSES[@]}"; do
[ -f "$c" ] || continue
read -r mounted wd <<<"$(python3 - "$c" <<'PYEOF'
import re, sys, yaml
lab = ((yaml.safe_load(open(sys.argv[1])) or {}).get("services") or {}).get("lab") or {}
vols = [v for v in (lab.get("volumes") or []) if isinstance(v, str)]
print("yes" if any(re.search(r":/workspace(?::|$)", v) for v in vols) else "no",
lab.get("working_dir") or "-")
PYEOF
)"
if [ "$mounted" != yes ]; then
skip "$c — lab mounts no workspace" ""
elif [ "$wd" = /workspace ]; then
ok "$c — lab starts in /workspace"
else
ko "$c — lab does not start in the workspace it mounts" "working_dir: $wd"
fi
done
fi

suite "observer and log-rotator stay off secure/lab"
# Deliberately no `networks:` key for either — see CLAUDE.md. They still land
# on Compose's implicit `default` network, but every other service declares
Expand Down