Skip to content

emrg: the sandbox grants paths only, and every extra root has to be named by hand #2492

Closed

Description

@argszero

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:

  1. Only directories and files can be granted. A Windows credential stored in the registry has no path to grant.
  2. Every extra root is named by hand, per session. A tool that keeps state outside the workspace needs the same /sandbox add again 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

  1. 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.
  2. A default manifest takes effect without a per-session command, so a host that never types /sandbox add still has the state its tools need writable.
  3. The default manifest is visible and withdrawable: /sandbox list shows 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-84 records why), and a default that is neither shown nor removable would be that constant back under a new name.
  4. A default entry never widens a tier that grants nothing — the property writable_roots has today by returning [] for read-only.
  5. 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-2025 leg: the default registry key is writable under workspace-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-only the 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

  1. argszero commented on Oct 22, 2026

    @argszero
    OwnerAuthor

    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)

    • The writable surface becomes a list of typed grants: 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. Today providers/darwin.py::_root_grant asks os.path.isdir at profile-build time, so a path named as a file silently widens from literal to subpath the day it becomes a directory.
    • One producer: sandbox/roots.py returns grants, and the Seatbelt profile, the in-process fence, the bubblewrap mounts and the Windows ACL path all read that one list. The writable_roots path projection stays as a derived view for the readers that only need paths.
    • A default manifest keyed by platform, applied without /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 list shows every entry with its provenance (default or session), and /sandbox remove <path> withdraws a default entry in that same one command.
    • A default never widens a tier that grants nothing: read-only still derives no grant.

    Out of scope (Stage 2, a later PR)

    • Registry enforcement in emrg/sandbox/win32/, and therefore the manifest's HKCU\Software\LarkCli\keychain entry 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.
    • A macOS pytest leg. The macOS case is not claimed in this staging: Test runs ubuntu-latest and windows-2025 only, and build-release.yml's macos-15 leg 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-only the manifest grants nothing; a registry grant handed to the darwin, linux and win32 dialects is refused by name rather than ignored.

  2. argszero commented on Oct 22, 2026

    @argszero
    OwnerAuthor

    Handled by #2493

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions