Repository navigation
emrg: the sandbox grants paths only, and every extra root has to be named by hand #2492
Description
What happens today
The sandbox's writable surface is a list of paths and nothing else. emrg/sandbox/roots.py:48 returns list[str], and four enforcement dialects read that one list: the Seatbelt profile (emrg/sandbox/providers/darwin.py:112, each root as subpath for a directory or literal for a file), the in-process fence (emrg/sandbox/fence.py), the bubblewrap mounts, and the Windows DACL grants (emrg/sandbox/win32/acl.py:342, grant_write(api, path, sid)).
Under workspace-write that list is the workspace root plus the two temp spellings plus whatever the host named with /sandbox add during that session (SandboxPolicy.extra_roots, emrg/sandbox/policy.py:76). Two consequences:
- Only directories and files can be granted. A Windows credential stored in the registry has no path to grant.
- Every extra root is named by hand, per session. A tool that keeps state outside the workspace needs the same
/sandbox addagain in every session that runs it.
Why it matters
lark-cli's user identity expires roughly every two hours and renews by taking a lock and writing a token. On macOS and Linux those locations are directories, so /sandbox add reaches them. On Windows the token is at HKCU\Software\LarkCli\keychain, which is not a directory: a confined task can read a message but cannot renew, so the identity dies and the task quietly degrades to whatever the bot identity can do.
What a fix must satisfy
- The writable surface accepts two kinds of object — a directory and a registry key — with the kind explicit rather than inferred from a spelling, because a registry key is not a path and no path operation applies to it.
- A default manifest takes effect without a per-session command, so a host that never types
/sandbox addstill has the state its tools need writable. - The default manifest is visible and withdrawable:
/sandbox listshows every default entry with its provenance, and one command withdraws it. This is the line the code already draws — a deployment constant nobody chose and nothing could withdraw was deleted by host decision D5 (emrg/sandbox/roots.py:69-84records why), and a default that is neither shown nor removable would be that constant back under a new name. - A default entry never widens a tier that grants nothing — the property
writable_rootshas today by returning[]forread-only. - The registry kind is consumed only where it can be enforced (the win32 provider). The other dialects must neither silently ignore a registry grant nor report it as granted.
Acceptance
- Every claim is a repository test; nothing rests on hand-checking a machine.
- Directory case, on both legs: a write inside a default root succeeds; a write outside the granted set is still refused.
- Registry case, on the
windows-2025leg: the default registry key is writable underworkspace-write, and a key outside the manifest is still refused. - Withdrawal: after removing a default root, the same write is refused.
- Tier independence: at
read-onlythe manifest grants nothing.
A constraint to decide, not to assume. The Test workflow has exactly two pytest legs — test (ubuntu-latest) and test-windows (windows-2025). macOS appears only in build-release.yml (macos-15), which builds artifacts and runs no suite. So a three-platform acceptance needs a macOS pytest leg added (a real cost: one more runner on every PR), or the macOS case is not claimed. Whoever takes this decides it explicitly and states which in the PR.
Suggested staging: the model change (typed grants plus the manifest plumbing) and the registry enforcement in emrg/sandbox/win32/ are independently reviewable — the first can land with the directory half proven on both legs and the registry half proven on the Windows leg alone.
Activity
argszero commentedon Oct 22, 2026
Taking Stage 1 only — the typed-grant model plus the default-manifest plumbing — in one PR, in the directory half proven on both pytest legs. Announcing before writing code so a second instance does not implement the same half twice; the registry enforcement in
emrg/sandbox/win32/stays open for a later PR.In scope (Stage 1)
directory,file,registry. A path grant's kind is recorded once, where the host names it, instead of being re-inferred from the disk on every call. Todayproviders/darwin.py::_root_grantasksos.path.isdirat profile-build time, so a path named as a file silently widens fromliteraltosubpaththe day it becomes a directory.sandbox/roots.pyreturns grants, and the Seatbelt profile, the in-process fence, the bubblewrap mounts and the Windows ACL path all read that one list. Thewritable_rootspath projection stays as a derived view for the readers that only need paths./sandbox add, total over absent paths (an entry not on this host is absent, never fatal):~/Library/Application Support/lark-cli/and~/.lark-cli/on macOS;~/.local/share/lark-cli/and~/.lark-cli/on Linux;%USERPROFILE%\.lark-cli\and%APPDATA%\Microsoft\Protect\on Windows./sandbox listshows every entry with its provenance (defaultorsession), and/sandbox remove <path>withdraws a default entry in that same one command.read-onlystill derives no grant.Out of scope (Stage 2, a later PR)
emrg/sandbox/win32/, and therefore the manifest'sHKCU\Software\LarkCli\keychainentry together with its parent key. Until code that can enforce a registry grant exists, such a grant is refused by name rather than passed to any dialect as if it were a path — on Windows too, so nothing reports a grant it does not enforce.Testrunsubuntu-latestandwindows-2025only, andbuild-release.yml'smacos-15leg builds artifacts and runs no suite, so a third pytest leg would pay a runner on every PR.Acceptance in this PR — assertions in both directions, on directories the test creates: a write inside the default manifest succeeds; a write outside the granted set is still refused; after withdrawal the same write is refused; at
read-onlythe manifest grants nothing; a registry grant handed to the darwin, linux and win32 dialects is refused by name rather than ignored.argszero commentedon Oct 22, 2026
Handled by #2493