aslice Setup — Declarative Whole-Machine Setup with aslice-machine.toml

State, identity, privilege, and recovery contracts: STATE-AND-RECOVERY. Protected-volume patching: SYSTEM-VOLUMES. These specifications do not establish completed implementation or platform validation.

1. The scenario

A Mac comes back from system recovery with nothing on it. Getting from blank to ready to work takes an afternoon of reconstructing the old setup: install the package manager, recall the packages, restore the Dock and Finder preferences, change the shell, add the repositories, enable the services, and find the runtime versions the projects expect. The old machine and its unsaved shell history held those choices; the rest depended on memory.

aslice-machine.toml is where that knowledge gets written down — one declarative file:

# on a blank Mac, after the aslice installer (MANUAL §2):
aslice machine apply https://example.org/my/aslice-machine.toml      # or a local path, or ./aslice-machine.toml

That one command installs the packages, selects the runtime streams, enables the services, writes the Dock/Finder/etc. preferences, enrolls and sets the login shell, and configures aslice itself. Every step is planned, shown, and consented before anything changes. And because the file is plain data, it doubles as the shareable artifact: a common language for "this is how my machine is set up" that can be diffed, reviewed, and version-controlled.

There is an inverse, for the machine that already exists:

aslice machine export > aslice-machine.toml
aslice machine export --defaults com.apple.dock,com.apple.finder > aslice-machine.toml

2. The file

aslice-machine.toml uses TOML, like package.toml, aslice.toml, sources.toml, and the lock file. It is data, never code: no hooks, script blocks, or evaluated expressions. The setup file can execute nothing (§7); you can inspect its declarations before applying them.

2.1 Schema overview

# aslice-machine.toml — declarative system setup for aslice. Apply: aslice machine apply
schema = 1                     # required; unknown schema versions are a hard error

packages = [                   # the wishlist: constraints, not exact state
  "extended:ffmpeg@7",                  #   version constraint (DESIGN §12.1 syntax)
  "postgresql +ssl",           #   variants
  "audiolab:convolver",        #   repository-namespaced (REPOSITORIES.md)
]

[aslice]                       # optional: aslice's own configuration (MANUAL §13)
flavor = "v3"                  # any key from the configuration reference is allowed

[[repos]]                      # optional: repositories beyond the pre-pinned official one
name = "audiolab"
url  = "https://repo.example.org"


[runtimes.default]             # profile-wide runtime stream selections (DESIGN §12.9)
php    = "8.4"
python = "3.12"

[services]                     # launchd services to enable (DESIGN §12.8)
start = ["postgresql", "redis"]

[grafts]                       # pre-approved installer-script grafts (§2.8)
allow = ["audiolab:convolver"] #   pnpm-style allow-list; anything not listed asks

[shell]
default = "zsh"                # login shell; package names resolve through the profile (§2.5)

[defaults.user."com.apple.dock"]      # current-user preferences — no privileges
autohide = true
tilesize = 48

[defaults.user."com.apple.finder"]
ShowPathbar   = true
FXPreferredViewStyle = "Nlsv"

[defaults.system."com.apple.loginwindow"]   # /Library/Preferences — consent-gated (§3.4)
# …

Unknown top-level keys are a hard error. The reasoning: a file written for a newer aslice must fail on an older one — fail outright, never half-apply. The schema key versions the document as a whole, and incompatible future changes bump it.

2.2 packages

Each entry is a name with optional decoration — an @version constraint, +variant/-variant flags, a required repo: namespace for every non-core package (including extended) — the same syntax aslice install accepts on the command line. Note what the file asks for, though. This is the constraint layer, not the state layer: it says "ensure ffmpeg 7", and the solver picks the exact build against the current index snapshot. If what you want is a machine reproduced bit-for-bit, that is the lock file's job (aslice lock export; PACKAGE-FORMAT §7). The difference is package.json vs. package-lock.json, and both flow through the convergence verbs of §3.1.

Resolution follows the ordinary rules: newest eligible version, flavor auto-detected, then binary cost preference. Newly required compilation is disclosed and needs interactive consent or --allow-source-builds for unattended execution. One consequence of the wishlist being reproducible: a package that exists only as a local build is a plan error unless its repository is reachable. "Rely on something only my old disk had" is refused by construction.

2.3 runtimes

[runtimes.default] maps runtime names to streams, applied as aslice default <runtime> <stream> (DESIGN §12.9): recorded in the state DB, consumed by the shim layer. Two kinds of selection deliberately stay out of this file. Project pins (aslice.toml in a project tree) belong to projects — commit aslice.toml there. Session selections (aslice use) are by definition not machine setup.

2.4 services

start lists packages whose declared launchd services should be enabled, exactly as aslice service start <pkg> would enable them (DESIGN §12.8). Enabling happens after the packages install, and each generated plist binds the runtime alias selected at enable time — so a [runtimes.default] change in a later apply never silently moves a running service out from under you. Root-domain services (domain = "system") remain trust-gated per REPOSITORIES §3: a file cannot enable what the repository's trust level forbids.

2.5 shell

default sets the user's login shell. The value is either an aslice package name (zsh, fish, bash) or an absolute path. Package names resolve to the profile path (/opt/aslice/profiles/default/bin/zsh), never a raw store path — deliberately, so that a generation rollback which removes the shell also moves the login shell back (§3.5).

Shell enrollment and account selection are separate operations on 10.11–12:

  1. chsh(1) refuses any shell not listed in /etc/shells — a system file aslice does not otherwise touch. Enrolling an aslice shell means appending one line to /etc/shells. The write is performed by the aslice-system helper (DESIGN §10.4) under the §3.4 consent gate, recorded in the state DB, and undone if aslice added the line and the shell is later unset or uninstalled. The line is appended, never reordered; Apple's entries are never modified.
  2. The OS default is /bin/bash through 10.14 and /bin/zsh for newly created accounts from Catalina onward (Apple shell defaults). Existing or migrated accounts may differ. Compare the requested shell with the account's actual login-shell record, not an OS-version assumption; an already matching selection needs no change. chsh itself runs unprivileged: it edits the user's own directory record and may prompt for the login password. That prompt is macOS's, not aslice's.

If the requested shell package is not installed yet, the plan installs it first; if it is not in any configured repository, the plan fails before anything changes.

2.6 defaults

macOS preference keys, written with defaults(1) semantics:

Schema 1 supports string, integer, float, boolean, and arrays of those. Deferred to a later schema version: dict values, data (raw plist blobs), date, and by-host (-currentHost) domains. Look at the deferred list and a pattern emerges — these are the types that share poorly. Opaque blobs and machine-bound settings have no place in a file meant to travel between machines (§6).

One thing to know about preferences: applications read them at launch, not continuously. So after applying, aslice tells you what to restart — for a small set of well-known system domains it names the application (com.apple.dock → Dock, com.apple.finder → Finder, com.apple.controlcenter/com.apple.systemuiserver → SystemUIServer, etc.), and for anything else it prints a generic "log out or restart the affected apps" note. It never kills processes on its own.

Writing a default for an application that is not installed yet is fine, and common: the key sits in the domain's plist until the application picks it up at first launch. Which means the ordering between [defaults] entries and packages is not the user's problem.

2.7 [aslice] and [[repos]]

[aslice] carries ordinary configuration keys (MANUAL §13) — flavor, mirrors, gc.*, and friends — applied exactly as aslice config set would apply them. The section exists so that a machine prepared for an older Mac (flavor = "v1") or an offline mirror fleet can say so in the same file as everything else.

[[repos]] declares additional repositories. Applying one is aslice repo add <url>: TOFU key pinning, with its interactive confirmation (REPOSITORIES §7). One rule is absolute: a file cannot elevate trust. verified status comes only from the normal grant flow, never from a document. And a declared repo whose URL is already configured under another name — or whose name is configured with another URL — is a conflict error, not a silent override.

2.8 [grafts]

[grafts]
allow = ["protools-hd", "audiolab:convolver"]

Some packages carry grafts: vendor scripts confined to isolated staging (STATE-AND-RECOVERY §4). [grafts].allow selects packages whose existing local approval may be reused. A name alone grants no authority.

Exporting this list carries package selection, not transferable authorization. It cannot reproduce another machine's trust or approvals.

3. aslice machine apply

3.1 One operation, three documents

The operation predates the feature. aslice apply already replayed an exported lock file (aslice apply aslice.lock, PACKAGE-FORMAT §7) and executed a saved plan (aslice plan install extended:ffmpeg > plan.json && aslice apply plan.json, DESIGN §12.1). Declarative setup is the same operation at a third fidelity — make reality match this document — and the spelling is split by document kind: plans and locks keep top-level aslice apply; the machine file gets aslice machine apply, so the command names what it converges:

Document Fidelity What apply does
plan.json exact, pre-resolved execute the saved plan
aslice.lock exact state replay the locked profile (PACKAGE-FORMAT §7)
aslice-machine.toml constraints + preferences plan (resolve wishlist, diff preferences), show, confirm, execute

The file kind is detected, never guessed: JSON is a plan, lock_version = N is a lock, schema = N with setup tables is a machine file, and anything else is an error that names what was actually found. With no argument, aslice machine apply reads ./aslice-machine.toml if it exists; a machine file passed to top-level aslice apply is refused with a pointer here — the split is by document kind, with no overlap. The argument may also be an https:// URL. In that case the file is fetched through the ordinary TLS stack (DESIGN §12.10), hash-printed, and planned before any consent is asked — you always review the resolved plan rather than trusting the URL blindly.

3.2 The plan and the order of operations

Machine apply is one transaction for the complete managed-state change. First validate the file and resolve its packages against authenticated metadata. A newly declared repository must complete its separate trust-establishment flow before it can supply that metadata; missing trust stops planning and identifies the prerequisite. Establishing trust is explicit and does not install packages or approve grafts.

Before mutation, compute the complete package, configuration, runtime, service, preference, and shell changes; verify recovery prerequisites; show the plan; and collect every required consent. A refusal stops the apply before managed state changes. --dry-run performs no writes. Machine-wide serialized operation plans remain deferred until their schema exists; package-plan JSON cannot stand in for them (STATE-AND-RECOVERY §8).

After confirmation, take prefix mutation ownership before preparation, acquire the protected-root lock when required in the defined order, authenticate and stage all inputs, recheck the planned base state, and persist the journal and before-images. Apply configuration and the resolved package set, runtime selections, services, user/system preferences, and shell changes under that journal. Privileged effects require the helper and their own capability checks wherever they occur, including package system effects and root services. Commit after reconciliation, then run bounded service health checks while retaining mutation ownership. Checks default to 60 seconds per service with positive finite overrides; failure reports committed installation and returns nonzero.

Any pre-commit failure rolls back the whole apply through STATE-AND-RECOVERY §5. Recovery or a required reboot may leave the transaction pending; it must not report a partial apply as success.

3.3 Idempotence, convergence, and --prune

Apply is convergent. Run the same file twice and the second run is a no-op ("0 changes"); run it against a partially-configured machine and it does only the missing work. The default posture is additive: aslice machine apply never removes a package or a preference merely because the file doesn't mention it. Your machine may legitimately have more than your shared file describes, and the file makes no claim on the difference.

aslice machine apply --prune opts into the other direction of convergence: anything recorded as file-managed (§3.5) but no longer declared gets retracted — packages uninstalled (only if they are still leaves that nothing else needs), preference keys restored to their recorded pre-apply values, services disabled. Two hard limits keep this safe:

Two parts of a setup file write to OS territory. N5 (DESIGN §2.2) forbids that in general; here it is allowed through the same declared, flagged, reversible model as [system-patch] (DESIGN §12.11):

Run interactively, each gated step prompts with what will be written and why. Run non-interactively — scripts, --json, pipes, the recovery-terminal scenario — gated steps are refused (exit 2) unless you pass --accept-system-changes: the same flag and the same contract as system packages and system patches (DESIGN §12.7 and DESIGN §12.11). --dry-run shows the complete plan, gated steps included, and changes nothing.

Graft-bearing packages require matching local approval or a fresh informed decision (§2.8). Names in the machine file do not waive approval. --accept-grafts does not grant system capabilities or replace --accept-system-changes. Collect required consents before mutation; refusal aborts the whole apply.

Planning reads untrusted data and verifies its inputs; it does not execute package code. A shared file supplies desired state, not repository trust or consent.

3.5 Recorded inverses: preferences ride generations

Machine apply uses the durable transaction and conflict rules in STATE-AND-RECOVERY §5. Record exact pre-change values before preferences, shell, and /etc/shells changes; journal external operations and reconcile them after interruption. Rollback restores managed state only when expected values still match, and does not undo application-data migrations. Protected-volume changes require the separate Recovery/reboot workflow in SYSTEM-VOLUMES.

The same record powers --prune (§3.3) and aslice history. Every setup-applied change is attributable: which file, which apply, which generation.

3.6 Failure handling

If any step fails before commit, roll back the entire apply, restoring the recorded before-state in reverse journal order. Completed steps do not stand as a successful partial setup. After commit, a health failure retains the committed installation and reports incomplete verification; an eligible rollback is a separate transaction. Concurrent external edits, missing backups, or unavailable protected state produce needs-attention; retain the journal and recovery tools and report the exact conflict rather than overwriting it. A power loss resumes recovery before any new mutation. Repository trust established separately before planning is retained and is not reset by package rollback (STATE-AND-RECOVERY §5 and STATE-AND-RECOVERY §7).

4. aslice machine export

4.1 What it captures

aslice machine export writes an aslice-machine.toml describing the current machine to stdout — redirect it wherever you like. What gets captured:

Export sorts its output and uses stable formatting, so two exports diff cleanly in version control.

4.2 --defaults: on demand, never automatic

Why are preferences not exported by default? Because packages, selections, services, and shell are knowable — aslice manages them, so it can enumerate them — and preferences are not. There is no baseline to diff a user's ~/Library/Preferences against: tens of thousands of keys per machine, written by every app ever launched, most of them meaningless to reproduce. An export that "captured your current setup" by dumping domains would be mostly noise. Worse, application preference domains can contain account tokens, server addresses, recent-file lists, and internal identifiers — auto-capturing them would leak exactly what a shared file must not contain.

So export captures preferences only for domains the user names explicitly:

aslice machine export --defaults com.apple.dock,com.apple.finder          # user domains
aslice machine export --defaults com.apple.dock --system-defaults com.apple.loginwindow

Named domains are read with defaults read and emitted as [defaults.user."…"] / [defaults.system."…"] tables. The output carries a generated header comment naming the capture date and a standing warning: review before sharing — application domains can contain account- or machine-specific values. Values whose types are deferred (dict, data, date — §2.6) are emitted as comments with a note, never silently dropped.

4.3 What it cannot capture

The following parts of a setup are outside export's scope:

5. aslice machine import --from-brewfile

An existing Brewfile can supply package selections for migration. Import is mechanical translation — no cleverness — complementing aslice adopt --from-homebrew (which reads the Cellar):

aslice machine import --from-brewfile Brewfile > aslice-machine.toml

Treat the import as a starting point for hand-tuning, not a fidelity guarantee. The output says so itself, in its header comment.

6. Sharing setups

The file is the artifact: small, diffable, reviewable, data-only. A team can keep aslice-machine.toml in a repo next to its onboarding docs; a forum answer can be one file instead of forty screenshots of System Settings. And convergence makes sharing safe in both directions: applying a stranger's file never removes your extras without --prune, and gated steps ask you personally, every time.

Two rules keep shared files healthy:

  1. No secrets, ever. Nothing in the schema legitimately holds a credential, and export's defaults capture warns at generation time (§4.2). Treat any token-shaped value in a shared file as a leak — because it is one.
  2. Machine-specific values stay out. Hostnames, hardware serials, per-display layouts: the test is whether a value names this machine rather than how I like machines. If it names this machine, it does not belong in the file. The deferred by-host domains (§2.6) exist as a category partly to keep that boundary visible.

7. Security and trust

8. What this is not

9. Command reference

aslice machine apply [aslice-machine.toml | https://…]
                                  # converge the machine to the file (default: ./aslice-machine.toml)
  --dry-run                       # print the full plan, change nothing
  --prune                         # also retract file-managed items no longer declared (§3.3)
  --accept-system-changes         # consent to [defaults.system] and /etc/shells enrollment, non-interactive
  --accept-grafts                 # consent to grafts not in [grafts].allow, non-interactive (§3.4)
  --json                          # machine-readable plan and report
aslice machine export             # write this machine's aslice-machine.toml to stdout
  --defaults <domain,…>           # also capture these user preference domains (§4.2)
  --system-defaults <domain,…>    # also capture these system preference domains
aslice machine import --from-brewfile <Brewfile>
                                  # translate a Brewfile into an aslice-machine.toml on stdout (§5)

Plans and lock files keep the top-level verb: aslice apply plan.json executes a saved plan (DESIGN §12.1) and aslice apply aslice.lock replays a locked profile (PACKAGE-FORMAT §7).

Exit status: 0 applied (or nothing to do); 1 error (schema, resolution, execution); 2 refused at a consent or trust gate. The plan is always printed before execution, and security events are logged unsuppressibly per DESIGN §12.5.

History

Document revision history | Version | Date | Changes | |---|---|---| | v0.20 | September 2026 | Classify FFmpeg as extended and align affected package references and examples. | | v0.19 | September 2026 | Align wishlist resolution with newest-eligible selection and explicit source-build consent. | | v0.18 | September 2026 | Remove retired comparison references and competitive framing; retain aslice requirements and link their owning specifications. Align affected contract summaries where applicable. | | v0.17 | September 2026 | Hold mutation ownership through preparation and post-commit service checks; distinguish whole-batch pre-commit rollback from committed health failure. | | v0.16 | September 2026 | Documentation audit repairs: contract summaries aligned; owner-approved namespace, rollback, GC, naming, prefix, and graft decisions applied where relevant; semantic anchors and explicit citations added. Runtime implementation and platform acceptance remain pending. | | v0.15 | September 2026 | Consolidate revision notes into a collapsible history table; no specification changes. | | v0.14 | September 2026 | resolve the default-shell contradiction: distinguish pre-Catalina defaults, Catalina-and-later new accounts, and actual account state; keep shell enrollment separate from account selection. Apple source captured locally. | | v0.13 | September 2026 | prose rewrite of the setup scenario, TOML format, and export explanation; no content changes. | | v0.11 | Not recorded | prose-fix pass — an intensifier removed (§1) and the Brewfile comparison stated precisely (§7); gems kept deliberately ('what the list waives is the question, not the evidence', 'the price of admission', 'there is nothing else to buy'); companion refreshed to DESIGN v1.20; no schema or semantic changes | | v0.10 | Not recorded | grafts — §2.8 introduces the `[grafts]` allow-list (pnpm-style pre-approval of declared installer scripts; signed manifests only), §2.1's schema overview gains the table, §3.4 gains the graft consent gate, §4.1's export captures recorded approvals, §7's attack-surface accounting gains the fourth bound, §3.2's ordering note gains the elevated-graft case, and §9's reference gains `--accept-grafts`; companion refreshed to DESIGN v1.19; model in DESIGN §12.15. | | v0.9 | Not recorded | the file is renamed `aslice-machine.toml` — a reserved, self-describing name that other tools can recognize — and the command surface splits by document kind: `aslice machine apply` / `export` / `import --from-brewfile` for the machine file, while top-level `aslice apply` keeps plans and lock files (owner decision, September 2026); no schema changes. | | v0.8 | Not recorded | companion refreshed to DESIGN v1.17; no schema or semantic changes. | | v0.7 | Not recorded | companion refreshed to DESIGN v1.16; no schema or semantic changes. | | v0.6 | Not recorded | NOMENCLATURE.md vocabulary reference added to the header; companion refreshed to DESIGN v1.15; no schema or semantic changes. | | v0.5 | Not recorded | review pass — the trust-stickiness reference retargeted to REPOSITORIES.md §4; companion refreshed to DESIGN v1.14; no schema or semantic changes. | | v0.4 | Not recorded | prose rewrite throughout — chapters reworded in the project's technical-writing voice; no schema or semantic changes. | | v0.3 | Not recorded | second editorial pass — sentence-level revision for readability; no schema or semantic changes. | | v0.2 | Not recorded | editorial pass — prose revised for directness; no schema or semantic changes. | | Not recorded | September 2026 | corpus review corrections: artifact identity, protected execution, durable recovery, trust persistence, replay, platform limits, and examples aligned with STATE-AND-RECOVERY and SYSTEM-VOLUMES. These are specification changes; runtime acceptance remains pending. |

Source document