Swarm v2 development and test environment
Work package SV2-FND-05. A test run, local or in CI, must not write the operator’s live agent homes, brain vault, Redis keys or deployment. This page is the contract every later Swarm v2 package builds on.
Canonical Python and checks
- The interpreter is the shared workspace venv,
~/dev/tcc-ecosystem/.venv. Call its binaries directly. Neveruv run --activeoruv sync --activeagainst it: it is shared with the other editable repositories. - Install or update:
uv pip install --python ~/dev/tcc-ecosystem/.venv/bin/python -e ".[all]". - Lint and format fail independently and CI runs both:
~/dev/tcc-ecosystem/.venv/bin/ruff check .and~/dev/tcc-ecosystem/.venv/bin/ruff format --check .. - Locally, run only the test files a change touches, with at most two workers:
~/dev/tcc-ecosystem/.venv/bin/python -m pytest -n 2 <files>. CI runs the full suite, sharded, and is the judge of it; many agents share the workstation.
What every test gets
The autouse fixture _isolate_real_user_paths in tests/conftest.py, backed by tests/swarm_v2_isolation.py and tests/installer_isolation.py, gives every test:
| Surface | Isolation |
|---|---|
| Agent homes | HOME and Path.home() point at a temporary home; CLAUDE_CONFIG_DIR, CODEX_HOME, COPILOT_HOME, AGENTIHOOKS_HOME and HERDR_CONFIG_PATH are unset, XDG_CONFIG_HOME points into the temporary home |
| Brain and vault | AGENTIBRAIN_HOME points into the temporary home; VAULT_ROOT is unset |
| Kubernetes | KUBECONFIG points at a path inside the test directory |
| Swarm session | every inherited AGENTIHOOKS_* variable is removed, so a run from an agent’s shell matches CI |
| Redis | connections to port 6379 are refused and the swarm Redis URL is a missing socket; keys built by the hooks package (hooks._redis, the memory store, the event relay) carry the run prefix agentihooks-test-<run id>. The swarm, inbox and gate stores under scripts/ keep their production agentihooks:* names and are tested against a per-test fakeredis |
| herdr | herdr_host.binary() and herdr_setup.binary() report no herdr, as in CI |
| Live writes | any write into a live root, from any code, aborts the test |
| Live programs | spawning a real kubectl, helm, argocd or herdr, one that resolves outside the test directory, aborts the test. The guard checks the program of a direct spawn; every argument after a leading env, timeout, nohup, nice, sudo, exec, command, xargs, stdbuf or setsid; and every word of a shell -c command, including commands run through os.system, after quotes and backslash escapes are removed and with $, = and :- treated as separators, so ${VAR:-kubectl} and ku""bectl are caught. A live program named as a shell argument (echo kubectl) is therefore refused too where one is installed. A program started by os.spawn*, or whose name the shell builds at run time from a variable’s value, is not seen: os.spawn* raises no audit event, and a variable’s value is not known before the shell runs |
The live roots are read once, from the environment the run started with: the home names .claude, .claude.json, .codex, .copilot, .agentihooks, .agents, .agentibrain, .kube, .config/herdr, .bashrc, .local/bin and .config/systemd/user, plus every absolute path named by CLAUDE_CONFIG_DIR, CLAUDE_CODE_HOME_DIR, AGENTIHOOKS_CLAUDE_HOME, CODEX_HOME, COPILOT_HOME, AGENTIHOOKS_HOME, AGENTIBRAIN_HOME, VAULT_ROOT, KUBECONFIG, HERDR_CONFIG_PATH and XDG_CONFIG_HOME/herdr.
The operator’s ledger folder and the shared ledger port are kept out by tests/ledger_guard.py.
Fakes for Kubernetes and herdr
No default kubectl fake is installed: with no real binary on PATH (CI) the call fails as missing, and with one (a workstation) the call aborts. A test that needs either program writes an executable fake into its own tmp_path and puts that directory first on PATH. The guard lets a program run only when it resolves inside the test directory; a fake that is a symlink to a real binary resolves outside it and is refused. A test of herdr logic can instead patch herdr_host.binary.
Fixture identities
tests.swarm_v2_isolation.build(root, run_id=None) returns an Identities record for packages that need a full temporary installation:
claude_home,codex_home,brain_home,vault(inside the brain home),archiveandkubeconfig, all underroot, with a fixture-only kube context and namespaceagentihooks-test-<run id>whose server is unreachable;redis_url, a socket insiderooton database 15, andredis_prefix,agentihooks-test-<run id>; the socket path, database index and prefix are the fixture’s Redis boundary, and no ACL user is created;traps: a symlink to the real~/.claude, a..escape out ofrootand an environment value naming the real~/.codex, for rejection tests;environ(), the variables that steer the live resolvers into the fixture.
Checks, each of which aborts the test and counts test_live_path_rejections_total by dimension in swarm_v2_isolation.REJECTIONS:
| Call | Refuses |
|---|---|
confine(root, label, path) | a path that resolves outside root (escape) |
confine_environ(identities, environ) | an environment whose steering values leave the fixture, or a foreign key prefix |
owned(identities, key) | a Redis key outside the run prefix, so production names such as agentihooks:swarm:* are never touched (redis) |
sweep(redis, identities) | a sweep by a run that does not own the prefix’s owner marker (redis) |
remove(identities) | a root whose .fixture-run marker names another run, or a live root (cleanup) |
Writes into live roots count under write, live programs under program. The autouse fixture records each test’s nonzero counts as the junit property test_live_path_rejections_total, which evidence/SV2-FND-05/generate_case_results.py sums per case. CI’s test job disables the junit report, so the case results come from the package’s own run with --junitxml, the command recorded in the evidence manifest.
Recovery
Every run and every build call takes a new run id, so lock and key names never repeat across runs. An interrupted run leaves its own prefixed locks and its own temporary root; the next run neither sees nor consumes them, and a sweep or removal deletes only resources whose owner marker matches the run that asks.
Rollback
The package touches tests, fixtures and documentation only. Reverting its pull request restores the previous suite guards; no stored state outside the repository depends on it.