- Status: done
- Date: 2026-05-30
- Specs touched: docs/specs/BRAIN_HOST_PROTOCOL.md (well-known endpoint, in the companion slice host-agent-well-known-identity.md)
The enforcement slice: writeOverride/writeEnv now act on the permission fields parsed in slice 1 (install-permissions-folders-schema.md), and the install endpoint authoritatively validates the user's per-folder elections. This turns the advisory install-plan (install-plan-endpoint.md) into enforced container identity + bind mounts.
POST /api/v1/appsrequest body gainsconfig.folders[]— per-folder{folder, source?, subfolder?}elections (BRAIN_UI_PROTOCOL.md Pattern Bconfig).resolveElections(man, scope, elections)(new,install_plan.go) is the gate: it returns one fully-resolvedlifecycle.FolderMountper declared folder (an omitted election takes the menu default), or a 422. Rejects: an election for a folder the app never declared, a duplicate election, a source not allowed for the scope (household forces shared; personal offers personal/shared), and a subfolder on a non-pick-subfolder folder or one that is absolute / escapes via...folderSourceMenu(scope)extracted as the single source of truth shared bybuildInstallPlan(advisory menus) andresolveElections(write-path validation) so the two never drift.- The handler loads the manifest, validates, and on rejection emits an
app.installsuccess=falseaudit record (elevation-class mutation rule) before returning the 422 — synchronously, before the job starts.
Install/installtakemounts []FolderMount;InstallCustompassesnil(Door-2 compose owns its ownuser:).- When the manifest declares folders,
installresolves anisolation:WellKnownIdentity(slice 4a) for themalmo-sharedGID always, plus themalmo-appUID/GID for a household instance;ResolveHome(slice 2) for a personal instance's owner UID/GID + home.ErrUnknownUser(owner deleted between plan and commit) rolls the install back as a terminal error, not a retry. Folderless apps skip this entirely and keep today's network/cap_drop-only override (verified: no host identity calls). writeOverridestamps, per service:user: <uid>:<gid>; onevolumesentry per folder binding the elected host source (<home>/<Folder>/personal,/srv/malmo/shared/<Folder>/shared, narrowed by subfolder) at/malmo/<folder>with:ro/:rwfrom the manifest mode;group_add: ["<malmo-shared gid>"]when any source is shared; anddevicespassthrough forpermissions.devices.writeEnvinjectsMALMO_FOLDER_<NAME>=/malmo/<folder>per folder (stable regardless of source).
- New
catalog/files-demo/(whoami image on port 8080, declaringfolders: documents read). whoami binds a privileged port and would break under the forced non-rootuser:, so it stays the folderless smoke test;files-demois the folders vehicle. (Deviates from the slice-2 handoff note that said "extend whoami" — extending whoami was unworkable for that reason.)
Realizes the enforcement half of APP_ISOLATION.md # User content (the toggle table's folders rows: personal source → home bind; shared source → shared bind + group_add) and APP_MANIFEST.md # folders (fixed /malmo/<folder> mount + injected MALMO_FOLDER_<NAME>). The household/personal user: split matches "personal instance runs as the owner; household runs as a shared service identity."
Dev loop (real fake host-agent over the socket + real docker), per feedback_verify_before_commit:
- Household, default elections —
user: 2000:2000,group_add: ["2001"],/srv/malmo/shared/Documents:/malmo/documents:ro,MALMO_FOLDER_DOCUMENTS=/malmo/documents; container came up healthy as the non-root UID undercap_drop: ALL. - Personal, elect personal source —
user: 3181:3181(FNV-hashed owner UID in [3000,3999]),/home/alex/Documents:/malmo/documents:ro, nogroup_add. - Illegal elections — household electing
personal→ 422 ("source "personal" is not allowed for a household install"); undeclared folder → 422. Both auditable.
Unit tests: internal/lifecycle/lifecycle_folders_test.go (household-shared-write, personal-source-read-with-subfolder, deleted-owner rollback, folderless skip) and internal/api/elections_test.go (defaults, personal-may-elect-shared, subfolder override, seven rejection cases) + instances_test.go reject-and-audit HTTP test. All green under -race.
- GPU deferred.
permissions.gpuis still parsed-not-enforced. The spec's "refuse at capacity check if absent" (APP_ISOLATION.md# GPU) needs a host GPU-capability query that doesn't exist yet; emitting a GPU reservation blind would either silently under-grant or failcompose upinstead of giving the specced capacity error. Folded into a follow-up with the host capability endpoint. No catalog app usesgputoday. - Device existence not validated.
devicesare passed through (/dev/x:/dev/x) but the spec's "brain validates each exists before start" needs the same host hardware-introspection endpoint as GPU. Deferred together. An absent device currently fails atcompose uprather than at a capacity check. - Double manifest load. The install handler loads the manifest to validate, and
lifecycle.Installloads it again. Acceptable (file read + parse); a single-load refactor would changeInstall's signature to take a parsed manifest.
- Slice 5 — consent + config UI in
web-ui/src/views/StoreView.vue: render the install-plan permission lines + per-folder source/subfolder pickers, submit the elections asconfig.folders[]. - GPU + device capacity — a host capability endpoint (
/v1/identity/well-known's neighbor) sogpu/deviceshonor the "refuse if absent" contract instead of failing atcompose up.