Projects/Multi-Agent Software Team/Part 2
Shared Repo and Test Oracle
Implement the repo dict, path allowlists, full-file writes, and a fake test runner that execs fizzbuzz in an isolated namespace.
The shared repo is a versioned dict. Every write produces a new snapshot you can put in the trace (or a diff of paths). This part builds apply_patch, run_tests, and the read-only test spec. No git. Git is a later adapter.
Patch API
apply_patch(repo, role, path, content) -> {ok, error, repo}
path in ALLOW[role]contentis str, len ≤ 4000pathmatches^[a-zA-Z0-9_./-]+$and does not contain..- Copy repo (
dict(repo)is shallow; values are strings so OK) and set the path - Return error dicts, do not raise, so the supervisor can tell the agent "patch rejected"
Test oracle
Read src/fizzbuzz.py, exec into ns = {}, get ns["fizzbuzz"], run cases. Catch SyntaxError, KeyError, TypeError. Result:
{"passed": int, "failed": int, "errors": [str], "ok": bool}
ok is failed == 0 and errors empty and passed == len(cases).
Do not exec the tests file. Agents might try to rewrite it. The oracle is code you wrote in the supervisor module.
Isolated exec
Use a fresh dict. Do not pass __builtins__ unrestricted if you can avoid it — in CPython you can set {"__builtins__": {"range": range, "str": str, ...}}. In Pyodide this still is not a security boundary against a determined model, but it documents intent. Part 5: never exec model code with real builtins on a machine with secrets.
Run to execute this in your browser. Nothing is sent to a server.
Step-by-step environment rules
- One repo object passed into every role. Roles return patches, they do not close over a global if you can help it (a global is OK in Try it; prefer arguments).
- Tests are code in the supervisor, spec text in the repo for the planner to read.
- Forbidden writes return an observation the agent can see:
forbidden_path. A fake coder that tries to cheat should fail the eval. - Deterministic fizzbuzz. No random. Same repo always same test result.
Why full-file writes
Unified diffs are painful to parse when the model miscounts lines. Full-file replacement needs a size cap. For a one-function repo it is the right trade. If you add more files later, the coder patches one path per turn (supervisor rule) to avoid nuking PLAN.md.
Copying, identity, and traces
dict(repo) is a shallow copy. That is correct because values are strings. If you later store nested dicts (a parsed AST cache), a shallow copy will leak mutations across "snapshots." Prefer copying the values you mutate. After each successful patch, append {path, n_bytes, sha256[:12]} to the trace — not the full file. Full files belong in the repo; traces belong in logs.
When a patch is rejected, do not keep a half-applied repo. apply_all should apply sequentially and stop on first error, returning the last good snapshot. Partial applies are how PLAN.md updates while src stays stale and you debug ghosts.
Exercise
Add a second allowed file src/util.py that the coder does not need. Write a test that a planner patch to src/fizzbuzz.py is forbidden. Print both errors.
Check your understanding