DrumsBench

Experiments / EXP-001

macOS sessions-per-Studio harness: design, build on fakes, test suites pass on the Studio

Build the Experiment 1 harness (envd, drumsctl, verify server, sampler, probes, report) and show that its two test suites pass on the target Mac Studio build.

verifiedBacked by measurement files we can point to, on named hardware, with the run recorded.
Date
2026-10-20 to 2026-10-22
Hypothesis
One macOS user session per environment, with no virtual machines, can serve as a computer-use environment; this record builds the tool that will test that claim and does not test it.
Arms
macos-sessions
Hardware
Mac Studio M4 Max, 16 cores, 48 GB (studio-a)
OS
macOS 15.7.5 (24G624)
Workload
pytest and swift test against fakes and in-process servers; swift build -c release
Code
exp1-sessions-per-studio at f0d01ce
Evidence
studio-a: drums-pytest.log (operator account home, not on the branch)
studio-a: drums-swift-test.log (operator account home, not on the branch)
studio-a: drums-build.log (operator account home, not on the branch)
data/results/exp-001-studio-test-suites.json
Limitation
Every test runs against fakes or in process. This shows the code runs on this build; it says nothing about sessions, isolation, density or cost.

In one paragraph

Over 2026-10-20 and 2026-10-21 we built the harness for Experiment 1, "how many independent computer-use environments does one Mac Studio run with no VMs, at what cost per valid rollout, with zero observed cross-contamination next to a positive control that fired". An environment is one macOS user account logged in to its own GUI session in the background; a small Swift agent, envd, runs inside each session and drumsctl drives every envd from the console session. Everything was built on a laptop against fakes. On 2026-10-22 the repository at head f0d01ce was copied to the target machine, studio-a, and both test suites passed there: 813 Python tests passed with 1 skipped in 479 s, and 105 of 105 Swift tests passed in 1 min 19 s (both measured). That is software evidence that the code runs on macOS 15.7.5 with Command Line Tools only. It is not a benchmark. No environment has been logged in, no isolation test has run against a real session, and no density, throughput, isolation or cost number exists for the Mac.

Setup

What an environment is on macOS

One local macOS user account (envNN, home /Users/envNN) logged in to its own GUI session in the background through fast user switching (MultipleSessionEnabled). No virtual machines. Inside that session one Swift agent, envd, runs as the LaunchAgent run.drums.envd and serves an HTTP API on 127.0.0.1:7700+N. The apps under test (LibreOffice Writer, Google Chrome) run as that user with their profiles inside ~/work.

Why a session and not a process: per-pid event routing inside one session (CGEventPostToPid) shares the pasteboard, focus and dialogs with everything else in that session, so it is not an environment. The design uses it only as a positive control.

Why not VMs: Apple's macOS license and the Virtualization framework cap macOS guests at two per host (vendor claim; see Limitations). The results table is to list "macOS VMs: cap 2 per host, not measured" as a row.

Appleone Mac Studio, one macOSconsole sessiondrumsctlverify server :7600metrics samplerenv00envd:7700TextEditcanaryenv01envd:7701LibreOfficeGoogle ChromeWriterChromeenv02envd:7702LibreOfficeGoogle ChromeWriterChromeenvNNenvd:7700+NLibreOfficeGoogle ChromeWriterChromebackground GUI sessions, fast user switchingread onlyHTTP, 127.0.0.1:7700+N, generation on every mutating callshared by every session: kernel, WindowServer, HID, filesystem, networknot shared: pasteboard, cfprefsd, tccd grants, home folder, event streamnot a security boundary: one kernel, one WindowServer, one root.an app in one session is one user account away from the next, not one machine.
One Mac, one kernel, one WindowServer, N background login sessions. Each session holds its own envd on its own port, its own pasteboard server, its own preferences daemon and its own home folder. drumsctl drives them all from the console session.

Shared and not shared

Shared by every sessionNot shared (per session)
one kernelpasteboard server
one WindowServer (the shared single point of failure)cfprefsd (preferences)
one root filesystem, one set of system daemonstccd state (Screen Recording and Accessibility grants)
one HID stackhome folder
one network stack (every envd on 127.0.0.1, own port)input event stream (the session event tap)

The isolation model document says it plainly: "This is not a security boundary. An app in one session is one user account away from another session, not one machine away."

Components, at head f0d01ce

The envd API

Every route runs a local guard first: any Origin header gets 403 forbidden_origin, and Host must be 127.0.0.1:<port> or localhost:<port>, else 403 forbidden_host. A web page loaded in Chrome on the same machine therefore cannot drive another environment over DNS rebinding.

routeneeds genwhat it does
GET /healthnoenv, uid, gen, on_console, screen size and scale, version, ax_trusted, screen_recording, reset_kills, held keys
POST /gensets itreleases held keys, sets the generation
GET /screenshotnoPNG with headers X-Drums-Gen, X-Drums-Captured-At, X-Drums-Stale (digest equals the last capture of that region), X-Drums-Black (99.5% of about 10,000 sampled pixels under 8/8/8; design value)
POST /actionyesclick, dblclick, type (1 to 2000 chars, one Unicode event per char, 1 ms apart), key, keydown, keyup, scroll
GET /lognolast 5000 entries in memory: actions, screenshots, pasteboard polls every 2 s, forced key releases
GET, POST, DELETE /pbGET no; POST and DELETE yesgeneral pasteboard text, SHA-256 hash or empty, change count
POST /launchyesspawn an absolute path with args and env; Apple apps go through /usr/bin/open -a
POST /killyesone pid or all; envd, its ancestors, loginwindow, Dock, Finder, WindowServer, sshd, tmux and other session keepers are protected
GET /windowsnolayer-0 app windows of this user's session, front to back
GET /axtextnothe focused UI element's text through Accessibility
POST /resetgen_next above currentsee reset semantics

One-shot modes (macOS only, for the probes; no server, no lock): --oneshot-screenshot, --oneshot-type, --oneshot-axtext, --oneshot-hold. The input modes check Accessibility first and exit 2 with ax_not_trusted when the grant is missing, because macOS would otherwise drop the events in silence.

Reset semantics and the generation counter

prepared state(checkpoint)env01env02env03reset: back to the checkpointreset = kill apps, clear pasteboard,swap ~/work for a fresh clone, then g+1env02 generationg=1g=2g=3action, g=1appliedaction, g=1 (late)409 stale_gen, not appliedaction, g=3applied
A reset swaps the work tree for a ready clone of the golden tree, checks it against a manifest, and raises the generation. Late calls that carry the old generation are refused with 409 and do nothing.

The canary, env00

env00 never receives an action. Before the first window of a sweep or soak, drumsctl opens an empty TextEdit document there. Every 2 s a watcher reads the focused text through /axtext, TextEdit's layer-0 window list and the pasteboard hash, and writes a line to canary.jsonl. Any change inside a window's span counts as one cross-delivery, and because the watcher's flag is sticky every rollout that ends after that moment in that window becomes env_fail with reason isolation. An unreachable canary at sweep start is an error unless --no-canary is given, in which case the table says Canary: off. The console session can be watched the same way (author claim: that recipe has not been run on a machine yet).

The verify server

A local sqlite HTTP service on port 7600 that never trusts the agent under test. The tasks post to it: /form (a coloured page with a Token field and Submit), /vis (a page that counts frames only while visible and posts every 5 s), /vis/count, /lease, /audit. At the end of every sweep window and of the soak drumsctl asks /audit for rows whose token names another environment and writes them as audit cross rows. Recorded deviation: drumsctl takes no /lease in this version, so the generation half of the audit never fires; the token half is what counts.

One rollout

sequenceDiagram
participant C as drumsctl (console)
participant E as envd (env session)
participant A as Writer or Chrome
participant V as verify server
C->>E: POST /gen
C->>E: POST /launch (gen)
E->>A: spawn as the env user
loop each step
  C->>E: POST /action (gen)
  E->>A: CGEvent on the session tap
  C->>E: GET /screenshot, poll until changed
end
A->>V: form post with the token (browser tasks)
C->>V: ask for the verdict
C->>E: POST /reset (gen_next)
E->>A: kill apps, clear pasteboard, swap work tree
E-->>C: gen, ready_ms, manifest_ok
One scripted rollout on one environment. drumsctl in the console session drives envd in the env session over HTTP; envd posts events on the session tap; the verify server, not the agent, decides the verdict; the reset raises the generation.

Trajectories

Each rollout writes results/<run>/trajectories/<rollout_id>.jsonl: one header line (env, gen, rollout id, task, token, n, os, poll 200 ms and step timeout 5000 ms, both design values), one step line per step reached (the action as sent; the observation as hash, optional png, stale, black and capture-failed flags; status; latency; step_ms, the time from the action send to the first changed region poll; shot counts), and one footer (outcome valid, task_fail or env_fail; reason; verdict; reward 1.0, 0.0 or null; step p95; reset ready time). The rollout id is D<env>-G<gen>-R<rollout>; the token is the rollout id plus 8 random hex characters, and every typed string holds the token so a verifier can tell whose text it reads. Flat companions sit next to the trajectories: rollouts.jsonl, events.jsonl, isolation.jsonl, canary.jsonl, pb.jsonl, windows.jsonl, host.jsonl, power.jsonl, sweep.json, soak.json and meta.json. Screenshots are not saved by default; only hashes are kept.

A rollout is valid when the pre-check passed (work tree equals the golden manifest, pasteboard empty, no leftover app windows), every step applied under the right generation with no timeout, the verifier returned a verdict, every isolation check active during the rollout stayed clean, and the verdict was ok. task_fail means the verifier said not done; because the policy is a scripted oracle, a task failure counts against the valid fraction. env_fail means the environment was unusable (reward null, never 0), with reasons such as precheck:work_dirty, stale_gen, timeout, black_screenshot, verifier_unavailable or isolation.

The four tasks

idappwhat the oracle doesverify
writer_save (W)Writeropen the note, click the body, type a sentence plus the token, save, select all, copy, quitthe note holds the token exactly once and no other; the pasteboard equals the typed sentence
browser_form (B)Chrome, own profile dirload the form page, click the field, type the token, click Submitexactly one form row with this token for this env
clipboard_cross (C)Writer and Chrometype the token in Writer, select all, copy, click the Chrome field, paste, Submitserver row and pasteboard
visibility (V)Chromeload the visibility page, 20 screenshots 1 s apartat least 18 server ticks and 18 distinct screenshot hashes (design values)

All coordinates ship as null, and drumsctl refuses a task with a null coordinate. They are measured once per OS on the target machine; that step has not happened.

The eleven isolation tests

Rows go to isolation.jsonl as clean, cross or lost. A lost row is never silent: the gate isolation_unverified fails any window with one. Trial counts below are design values.

  1. positive_control (the gate). Runs the pasteboard round and the keyboard-storm round on two environments that share one session, or on a fake with a leak wired in, and reports whether the detector fired. If it does not fire, no zero may be published. On the Studio only its pasteboard half can fire, because drumsctl cannot start a second Writer in one session; probe P is the control of record for the typing half (see EXP-003).
  2. pasteboard. All N environments set a tagged string at the same moment, then read back; 50 trials per window, 10 in the soak, plus after every W and C rollout. Catches a shared or leaking pasteboard.
  3. keyboard_storm. All N environments type 10 distinct tokens (about 220 characters) within 5 s into the Writer the sweep opened; each document is saved and read from the host; 10 trials. Catches keystrokes landing in another session.
  4. modifier_leak. Environment A holds Shift, then Cmd, 5 s each, while B types 12 lowercase letters; B's document must hold exactly the lowercase text (capitals mean Shift leaked, missing text means Cmd leaked); 3 trials, rotating. Catches system-wide modifier state.
  5. dialog_steal. B opens Save As for 5 s while A types a token; the other environments' screenshot p95 before and during is compared; B's Save As field is read through Accessibility against a baseline, and only new text with a piece of the token counts; a bystander more than 20% and more than 50 ms slower is a cross row; 3 trials.
  6. canaries. env00 as above, and optionally the console; rows go to canary.jsonl.
  7. observation_privacy. For 120 s only odd environments act; even environments' screenshots below the clock, hashed every 2 s, must never change, and no screenshot may hold 8 or more sampled pixels of another environment's form-page colour; 1 trial per window.
  8. files_and_prefs. One environment writes a token file into its own ~/work and a preference through its own envd; every other environment's work dir is walked, defaults read runs in every other environment, and every log in the run folder is searched for the token; 3 trials, rotating.
  9. reset_during_write. Set the pasteboard, type, save, reset 200 ms later; 50 ms after the reset returns, check the tree against the manifest and the pasteboard; 20 trials; on demand.
  10. stale_generation. Per trial, 4 old-generation clicks at the same moment as a reset; after it returns one more old-generation click must get 409; the log must show no old-generation action applied after the reset; a new-generation click must work; 100 trials in the soak, 20 in Gate 2; applied_stale must be 0.
  11. crash_blast_radius. Kill A's apps, then A's envd through A's own envd (the operator may not signal another user's process); A's health must fail or come back at a lower generation within 5 s; other environments' screenshots keep being timed; wait up to 60 s for launchd to restart envd; reset A; the pasteboard must be empty; 1 trial; on demand.

Tests 2, 3, 4, 5 and 7 run after each sweep window; the soak runs 10 and then 2; tests 1, 8, 9 and 11 run on demand; test 6 runs the whole time. Every detector has a unit test with a deliberate leak that makes it fire. That proves each detector reads its inputs. It does not prove those inputs would carry a leak on the Studio.

What ran

On the laptop, 2026-10-20 evening to 2026-10-21 22:45: the whole harness, built task by task against fakes (a fake envd, an in-process verify server, made-up sampler data), with a whole-branch review and fix rounds at the end. Head f0d01ce, 2026-10-21 22:45 -0400. Nothing pushed; main untouched.

On studio-a, 2026-10-22, over SSH: the repository copied by git bundle, a uv virtual environment on Python 3.12.14, then make test (both suites) and make build-envd.

813passed
Python tests on studio-a (measured)
1 skipped (the envd-linux real X round trip needs a display); 479 s wall; pytest -W error; Python 3.12.14. Software evidence, not a benchmark.
105passed
Swift tests on studio-a (measured)
105 of 105; 1 min 19 s wall; Swift 6.1.2 with Command Line Tools only. Software evidence, not a benchmark.
36.6s
envd release build on studio-a (measured)
A build time. Not a performance figure for the harness.

Harness test suites and envd release build on studio-a verified2026-10-22 · studio-a

MetricValueUnitnNote
python tests passed813testspytest -W error, Python 3.12.14; parametrized tests expand, so this is above the 723 test functions in the source
python tests skipped1teststhe envd-linux real X round trip; needs a real X display
python suite wall time479stest runtime, not a performance figure for the harness
swift tests passed105 / 105testsSwift 6.1.2
swift suite wall time79sprinted as 1m19s; test runtime, not a performance figure
envd release build wall time36.6sswift build -c release; a build time, not a performance figure

Host: Mac Studio M4 Max, 16 cores, 48 GB, macOS 15.7.5 (24G624). An institution-managed Mac with MDM and endpoint protection. Xcode Command Line Tools only (Swift 6.1.2, no full Xcode). Python 3.12.14 in a uv virtual environment.. Workload: pytest with -W error across drumsctl, envd-linux, metrics, report, scripts and verify-server; swift test for envd (EnvdCore); swift build -c release for envd. Method: Run once over SSH on 2026-10-22 at repository head f0d01ce after copying the repository by git bundle. Every Python test runs against drumsctl/tests/fake_envd.py or in process; the Swift tests are unit tests. Counts and wall times were read from the run logs kept in the operator account's home on the Studio.. Source: studio-a: drums-pytest.log (operator account home, not on the branch); studio-a: drums-swift-test.log (operator account home, not on the branch); studio-a: drums-build.log (operator account home, not on the branch); branch exp1-sessions-per-studio, head f0d01ce.

  • Software evidence, not a benchmark result. The rows show that the code runs on this build; they say nothing about sessions, isolation, density or cost.
  • Every Python test runs against fakes or in process; no test touched a real environment session, a real envd LaunchAgent or a real screen capture.
  • The run logs live in the operator account's home on the Studio and are not on the branch; the same counts were last measured on the development laptop on the final tree.
  • The envd binary sizes (release build and ad-hoc-signed install) differ and are not published as figures.

The same counts had last been measured on the laptop on the final tree. The static count is lower (723 Python test functions at head; measured by grep) because parametrized tests expand into several cases.

Results

Failures and surprises

Limitations

What we will not claim

Copied word for word from report/report/not_claimed.md:

  • Not a security boundary. One kernel, one WindowServer, one root. We measure accidental cross-talk over the stated env-hours.
  • Not model quality. The policy is scripted; inference cost is excluded.
  • Not automatic session creation. Each account took one human login and the recorded TCC clicks.
  • Not any app. Writer and the Chromium family only. No Electron, Finder, drag-and-drop, hover, IME, games.
  • Not full state reset. Keychain, TCC, Launch Services and prefs outside the manifest are not reset.
  • Not long-run stability. Longest window is 60 min; monthly Screen Recording re-prompts and replayd decay are untested.
  • Not headless unless the no-monitor rerun was done. Not one macOS version: numbers are for the build in meta.json on one M4 Max 48 GB.
  • Not a license opinion. Run on a Mac we own; hosting for others is not supported by this result.
  • Not the best Linux. One documented VM config on the same Mac, asymmetry stated.
  • Not default Chrome behavior if throttling flags were required; the table says which flags each OS needed.
  • Zero means zero observed by the listed checks, next to a positive control that fired.

A further sentence, accepted on 2026-10-22 and to be added to that file: the positive control fires the typing and pasteboard detectors with interference forced inside one session. It shows those two detectors can fire. It does not show that a leak between sessions, or through another channel, would be caught.

Reproduce

Prerequisites at the Studio: Xcode Command Line Tools with Swift 6, Homebrew, brew install uv. Every host-changing script refuses to run without DRUMS_HOST_OK=1. Set RUN once (export RUN=results/<date>-<host>-mac) and keep it for the whole run. Steps 0 and the two suites are what this record covers; the rest is the full table from the README, listed so the reader can see where this record stops.

  1. git clone <repo>; uv sync; make test; make build-envd (both suites; envd release build). This record ends here.
  2. DRUMS_HOST_OK=1 make setup-host (Homebrew, casks, pmset, MultipleSessionEnabled, /usr/local/drums, the signing identity; make signing-identity redoes that step alone).
  3. DRUMS_HOST_OK=1 make create-users (env00 to env32, hidden standard users).
  4. DRUMS_HOST_OK=1 make install-launchagent.
  5. make golden (the note must be empty).
  6. DRUMS_HOST_OK=1 make seed-env-homes.
  7. make gate0 (FileVault, user count, CGSession, console session state, env00 home group).
  8. make meta, then make baseline-sample (60 s of host samples before any env login).
  9. make verify-server in a second terminal with the same RUN.
  10. DRUMS_HOST_OK=1 make probes (A, B, C, P in order; stops at the first FAIL).
  11. By hand: log in the remaining accounts, grant Accessibility, take the first capture per session, count clicks and minutes per login.
  12. make health, make gate1; after the coordinates are measured, make gate2 COORDS=....
  13. By hand: measure the six task coordinates in env01's session; pass as COORDS= from here on.
  14. DRUMS_HOST_OK=1 make park-console (by hand on builds without CGSession).
  15. make health and stat -f %Su /dev/console.
  16. make sweep COORDS=....
  17. make soak N=<N_max> COORDS=....
  18. make positive-control COORDS=..., then make isolation-extra N=<N_max> COORDS=....
  19. make report EUR_USD=<rate> STUDIO_USD=<invoice> (make power-sample as root during the sweep and soak for watts).
  20. make baseline-linux (prints the Linux steps; run inside the Ubuntu guest with OS=linux PY=python3).
  21. make results RUNS="results/<mac run> results/<linux run>" LINUX_HOST=....

Sources