Repository navigation
Description
Part of #1192.
Scope
After this PR, a slim lease on any iOS runtime from 18.5 up gets a simulator whose slim services are off from its one boot, proven by a readback, and a destroyed simulator leaves no folder in /private/var/tmp. It replaces the in-simulator slim pass (F1) with ADR 0060: write the launchd overrides file while the simulator is shut down, boot once, read back. It re-syncs the label list to simslim v0.13.0 and supersedes #206.
One task: the file write, the readback, the estimate and the label list all change one step (makeReady's slim branch) and its one test block, and splitting them would ship a release with two slim mechanisms.
In short
A slim simulator now has its services turned off before it boots, so it boots once and needs no shell inside the runtime. After the boot Simlock checks which services are off, and that check decides the mode: if it disagrees, the lease gets a full device and the worker log says why. Deleting a simulator also deletes its folder in /private/var/tmp.
flowchart LR
A[create or erase] --> B["write disabled.plist<br/>(changed)"] --> C[boot once<br/>(changed: was boot, pass, reboot)] --> D["read back<br/>(new)"] --> E[slim or full]
F[destroy] --> G[simctl delete] --> H["remove /private/var/tmp folder<br/>(new)"]
New term: launchd overrides file, /private/var/tmp/com.apple.CoreSimulator.SimDevice.<udid>/disabled.plist, an XML property list mapping a launchd label to true (off) or false (on), e.g. com.apple.tipsd → true.
The slim-skipped log line (warn, from onSlimSkipped, F12) carries one of these details, word for word, <n>, <m>, <label> and <error> filled in:
<n> of <m> managed labels did not read back as written at boot; first: <label>(device grantedfull;<m>counts every managed label written,trueandfalse)could not write the launchd overrides file: <error>(device granted the mode the readback shows)could not read back the launchd overrides: <error>(device grantedslim)
One prepare reports at most one slim-skipped fact. When the write failed and the readback then disagrees or fails, the details are joined with ; , write first.
A failed folder removal on destroy logs warn could not remove the launchd overrides folder of <udid>: <error>.
If this goes wrong, a user sees slim leases granted full on runtimes that worked before.
Technical spec
Facts
Commit 019ba5c unless named.
| # | Fact | Source |
|---|---|---|
| F1 | Today's pass runs simctl spawn <udid> /bin/sh -c with launchctl disable system/<label> in chunks of 50, each limited to 60 s, then shuts down and boots again |
src/drivers/ios/index.ts:1053-1066, :162, :166, :899-917 |
| F2 | The iOS 26.4 and 27.0 runtimes on a maintainer Mac have only df and launchctl in RuntimeRoot/bin |
ls <runtime>/Contents/Resources/RuntimeRoot/bin printed df launchctl for both (Xcode 27.0, 2026-11-02) |
| F3 | simctl erase and simctl delete leave /private/var/tmp/com.apple.CoreSimulator.SimDevice.<udid>/disabled.plist in place, in a private device set |
xcrun simctl --set <tmp> create/erase/delete on Xcode 27.0 printed after erase: disabled.plist and after delete: disabled.plist |
| F4 | A disabled.plist written before a new iOS 27.0 simulator's first boot is honoured at that boot, and the file is not rewritten by it |
maintainer Mac, 2026-11-02: three labels written <true/>, one boot + bootstatus -b (25 s), launchctl print-disabled system printed "com.apple.tipsd" => disabled and the two others; the file afterwards held the same three entries |
| F5 | The file is an XML plist, a flat dict of bare label (no system/) to <true/> (disabled) or <false/> (explicit enable); simslim merges, writes a temp file in the same folder and renames it, file mode 0644, folder mode 0700 |
simslim v0.13.0 (d594c230) disabled_store.go:22-30, :69-99, :107-125, :81 |
| F6 | launchctl print-disabled system prints one line per entry, "<label>" => disabled or => enabled; simslim also accepts true as disabled |
e2e/slow-ios-slim.test.ts:44-63; F4's output; simslim simctl.go:423-446 |
| F7 | makeReady boots, then for prepare + slim + 18.5+ runs the pass; recover, full mode, no slim config → no pass; below 18.5 or an unparsable runtime id → skipped with runtime-too-old / unknown-runtime |
src/drivers/ios/index.ts:792-813, :2301-2326, :2288-2294 |
| F8 | reclaim shuts down, erases and rewrites the mark; destroy and destroyLegacy shut down and run simctl delete; nothing in src/ touches /private/var/tmp |
src/drivers/ios/index.ts:1093-1101, :1115-1118, :1148-1153; grep -rn "var/tmp|disabled.plist" src/ → no hits |
| F9 | The driver keeps slimSignature and slimMarkToken in driver data for idempotence, stored in nullable ios_device_data columns |
src/drivers/ios/index.ts:184-185, :979-994, :1009-1013; src/drivers/ios/device-data.ts:8-9, :43 |
| F10 | A slim spec's boot estimate is SLIM_BOOT_ESTIMATE_MS (150 000 ms), a full one COLD_BOOT_ESTIMATE_MS (60 000 ms) |
src/drivers/ios/index.ts:156, :100, :1228-1234 |
| F11 | The Filesystem port has mkdirp(path, {mode}), writeFileAtomic(path, contents, {mode}), readFile, and rm, which is recursive and forced; MemoryFilesystem implements all four |
src/ports/filesystem.ts:27-32, :52-76, :213-214, :451-457 |
| F12 | onSlimmed carries deviceId, address, categories, labelCount, durationMs, signature, unknownLabels; onSlimSkipped carries deviceId, reason ("runtime-too-old" | "unknown-runtime" | "apply-failed"), detail and the daemon logs it as a warning; onSlimmed is bridged to device.slimmed |
src/drivers/ios/index.ts:188-202; src/daemon/main.ts:1386, :1586 |
| F13 | slim-labels.ts copies simslim's profiles.go at 65bda5bd; at v0.13.0 navd is in widgets and assistantd, gamed and sharingd are AlwaysEnabled (never disabled); sharingd is not in Simlock's list |
src/drivers/ios/slim-labels.ts:3-9, :26-28, :34, :214; simslim v0.13.0 profiles.go:43-46, :85-90, :275-280, :317-321 |
| F14 | A device's mode is fixed for its life, and a request takes a warm device only of its own mode | src/core/domain.ts:28-40; src/core/warm-pool/policy.ts:252-258 |
| F15 | The driver has an optional logger, used today only by the device input |
src/drivers/ios/index.ts:223-224 |
| F16 | The slow lane picks the first runtime the catalog pairs with the first paired model, leases a cold slim device on a default-slim worker and asserts every label in ALL_LABELS reads disabled and exactly one device.slimmed with labelCount equal to ALL_LABELS.size; a second test asserts a recovered slim device still reads every label disabled |
e2e/slow-ios-slim.test.ts:80-91, :155-190, :261 |
| F17 | The unit tests drive the driver with ScriptedProcessRunner and MemoryFilesystem |
src/drivers/ios/index.test.ts:22, :2884-2896 |
Modules touched
Searches: slimSignature, slimMarkToken, SLIM_, applySlim, sanitizeSlimLabels, labelsFor, destroy(, estimate(, slim in docs/ and website/.
flowchart LR
core[core ManagedDeviceLifecycle] --> drv["drivers/ios/index.ts (changed)"]
drv --> labels["drivers/ios/slim-labels.ts (changed)"]
drv --> fs[ports/filesystem]
drv --> simctl[xcrun simctl]
daemon[daemon/main.ts bridge] -.callbacks.-> drv
Files: src/drivers/ios/index.ts, src/drivers/ios/slim-labels.ts, src/drivers/ios/index.test.ts, src/drivers/ios/slim-labels.test.ts, e2e/slow-ios-slim.test.ts, src/drivers/ios/AGENTS.md, docs/internal/EVENTS.md, website/src/content/docs/docs/worker/capacity.mdx, website/src/content/docs/docs/reference/configuration.mdx, website/src/content/docs/docs/using-devices/choosing-a-device.mdx. src/drivers/ios/device-data.ts keeps its columns (ADR 0060 §3).
sequenceDiagram
participant C as core
participant D as iOS driver
participant F as /private/var/tmp/...<udid>
participant S as simctl
C->>D: makeReady(prepare, slim)
D->>F: read, merge, write (atomic)
alt write fails
D-->>D: log reason, still boot
end
D->>S: boot, bootstatus -b, listapps
D->>S: spawn launchctl print-disabled system
alt every label as written
D-->>C: mode slim (+ onSlimmed)
else disagrees
D-->>C: mode full (+ onSlimSkipped)
else readback fails or times out
D-->>C: mode slim (+ onSlimSkipped)
end
Contract and event changes
None to the wire. device.slimmed keeps its fields (F12); its trigger in docs/internal/EVENTS.md becomes "after a successful write and a readback showing every managed label as written"; a failed write followed by a matching readback grants slim with a slim-skipped line and no event (decided in delivery, pending the maintainer). A write skipped because the file is already as wanted counts as successful: a matching readback grants slim and fires device.slimmed (decided in delivery, pending the maintainer). A recover boot writes nothing, reads nothing back and returns the mode as today (decided in delivery, pending the maintainer). labelCount is the number of labels written true; unknownLabels is the unknown category names from resolveSlimCategories; durationMs runs from the start of the file read to the end of the readback. signature stays slimSignature(categories). SlimSkippedFact.reason keeps its three values; the new details use apply-failed.
Managed labels: every label in SLIM_CATEGORIES plus the never-disable labels. A managed label in a configured category is written true, every other managed label false (ADR 0060 §1). Configured categories that resolve to none known: skipped and granted full, nothing written, as today (index.ts:870-878).
The file read from disk: a key is a label, the value true or false. An entry Simlock does not manage is kept as it is. A file that does not parse as a plist dict is left unchanged and counts as a failed write: logged, the device boots, and the readback decides its mode.
The readback (launchctl print-disabled system): a line "<label>" => <value>; disabled or true reads disabled, anything else or no line reads enabled. A label written true is as wanted when it reads disabled; one written false when it reads enabled.
Other code on the same state
Searches: disabled.plist, var/tmp, CoreSimulator.SimDevice in src/ (0 hits); launchctl in src/ (only F1's script); slimSignature|slimMarkToken (index.ts, device-data.ts).
launchd_sim(host process) reads the file at boot and rewrites it onlaunchctl disable/enable. Simlock writes only while the device is shut down (after create or reclaim), so they never write at once.reclaimerases; the file survives (F3), so the nextpreparefinds it as wanted and only boots and reads back.- A lease holder running
xcrun simctl spawn <udid> launchctl enable ...changes the file through launchd; the nextprepare(after reclaim) writes it back. device-data.tsstill reads the two columns; the driver stops writing them.
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
| read existing file | absent | treated as empty | none (local file) |
| read existing file | not a plist dict | slim skipped, could not write ..., file left as is, device boots, readback decides |
none |
mkdirp folder 0700 + writeFileAtomic 0644 |
throws | slim skipped, could not write ..., device boots, readback decides |
none |
| boot | as today | BootTimeoutError as today |
ios.slim.bootTimeoutMs, from simctl boot |
simctl spawn <udid> launchctl print-disabled system |
ok, all as written | slim, onSlimmed |
120 s, from the spawn |
| same | ok, any label not as written | full, slim skipped with count and first label |
120 s |
| same | non-zero exit or timeout | slim; slim skipped, could not read back ... |
120 s |
rm folder on destroy / destroyLegacy |
throws | logged warn, destroy resolves |
none |
simctl delete on destroy |
fails | throws as today, folder not removed | 30 s, as today |
A restart between the write and the boot leaves the file written; the next prepare finds it as wanted. Two makeReady calls never run on one device at once (core holds the device).
Rules in play
- ADR 0060 §1-§6: "writes the launchd overrides while the simulator is shut down" — the whole task.
- ADR 0002 §4 (kept): "Runtimes older than iOS 18.5 are skipped" — gating unchanged.
- ADR 0002 §6 crash recovery (kept): "a plain boot with no slim pass" —
recoverwrites nothing. - ADR 0007: the registry is the only record of a device's mode — the driver returns mode; nothing new stored.
- Safety rule 1: "only ... devices ... that exist in its own registry" — the folder removed is the destroyed registry device's own UDID.
- Vendor rule (MIT):
slim-labels.tskeeps simslim's notice, now naming v0.13.0. - Documentation rule 9: the website pages that say "boots twice" change in this PR.
Tests
Seam: IosSimctlDriver (makeReady, destroy, estimate) with ScriptedProcessRunner and MemoryFilesystem, the store root passed as a driver option; model on the slim mode block of src/drivers/ios/index.test.ts. Labels: src/drivers/ios/slim-labels.test.ts. Real boot: e2e/slow-ios-slim.test.ts.
- The core prepares a slim device on iOS 18.5+: the overrides file holds every label of the configured categories
trueand every other managed labelfalsebefore the one boot, and the device comes backslimwith oneonSlimmed— cases: no file yet; file with the runtime's own entries (kept); file already as wanted (not rewritten, still booted and read back, oneonSlimmed); file slimmed under wider categories (dropped labels nowfalse). The scripted runner shows onebootand no/bin/sh. - The core prepares a slim device and the readback disagrees or fails: the device comes back with ADR 0060 §2's mode and one
onSlimSkippedwith the detail above — cases: atruelabel readsenabled(full); atruelabel missing (full); atruelabel readstrue(as wanted:slim,onSlimmed, no skip fact); afalselabel missing (as wanted:slim,onSlimmed, no skip fact); afalselabel readsdisabled(full); readback exits non-zero (slim); readback times out (slim). - The core prepares a slim device and the file cannot be written: the device still boots, the readback decides its mode (ADR 0060 §2), and
onSlimSkippedsays the write failed — cases:writeFileAtomicthrows, readback all as wanted (slim, noonSlimmed);mkdirpthrows, readback disagrees (full, one fact with both details); write throws, readback fails (slim, one fact with both details); existing file not a plist dict (left unchanged, readback decides). - The core prepares a device that must not be slimmed: the overrides file is not touched and no readback runs — cases: full mode;
purpose: "recover"; runtime 18.4; runtime id that does not parse; configured categories none known (full). Extends "a purpose: 'recover' boot never applies slim" (index.test.ts:2784). - The core destroys a simulator: its overrides folder is removed after
simctl delete, and a failed removal is logged without failing the destroy — cases: removal succeeds;rmthrows;simctl deletefails (folder kept, error thrown);destroyLegacyremoves it too (extendsindex.test.ts:1626). doctorasks for a slim spec's boot estimate: it getsCOLD_BOOT_ESTIMATE_MS— cases: slim spec; full spec. Rewrites "estimates the boot cost with the slim reboot+apply budget" (index.test.ts:2884).- The label list matches simslim v0.13.0 — cases:
navdinwidgets;assistantd,gamedandsharingdin no category and in the never-disable list. - On a real Mac, a slim lease on the newest installed iOS runtime and a re-lease of the same released simulator are granted
slimwith every label disabled after one boot — cases: cold lease (extends the test ate2e/slow-ios-slim.test.ts:155;catalogModelAndOspicks the highest version the catalog pairs with the model, not the first); release, then lease the same device again.
Done when
pnpm exec vitest run --project unit src/drivers/iospasses.grep -rn '/bin/sh' src/drivers/ios/index.tsfinds nothing.- Slow lane, real simulator:
e2e/slow-ios-slim.test.tspasses on a Mac whose newest iOS runtime is 27.0. src/drivers/ios/AGENTS.md,docs/internal/EVENTS.mdand the three website pages no longer say a slim device boots twice, that erase removes the overrides, or that narrowingios.slim.categoriesnever turns a service back on.
Out of scope
- Removing folders left by simulators deleted before this change.
- Copying the overrides file to a cloned simulator (Named baselines: a lease starts from a device the operator prepared #327).
- The capacity rule that counts a booting device at full size (
capacity.mdx:150-152); only its stated reason changes. - Dropping the
slim_signatureandslim_mark_tokencolumns.
Depends on
Written by an agent.
Activity
test: the overrides file is merged, kept or left alone, and the devic…
test: the readback decides the mode and says why when it disagrees or…
test: a failed overrides write still boots and the readback decides t…
test: a device that must not be slimmed gets no overrides file and no…
test: destroy removes the launchd overrides folder after simctl delete (
feat(ios): destroy removes the launchd overrides folder (#1196)
test: a slim spec's boot estimate is the cold-boot one (#1196)
feat(ios): a slim boot is quoted at the cold-boot estimate (#1196)
test: the label list matches simslim v0.13.0 (#1196)
feat(ios): re-sync the slim label list to simslim v0.13.0 (#1196)
docs(ios): slim writes the overrides before one boot and reads them b…
test(ios): the readback names the sorted first label, the file modes …
fix(ios): slim lease time note matches one boot; test asserts slimmed…
test(ios): slim re-lease waits for the released device to be ready (#…
docs(adr): 0060 to Accepted (#1196)
docs(adr): keep one 0060 row after 0059 with the accepted status (#1196)
refactor(ios): split the screen-capture wiring and a slim test's arra…
docs(adr): 0060 iOS slim writes launchd overrides while the simulator…
feat(ios): slim writes launchd overrides before one boot, reads them …