Security: confine model-facing Shell with a macOS Seatbelt boundary
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 152
- Forks
- 16
- Avg merge
- 14h 48m
- Merged PRs (30d)
- 536
Description
Problem
Mecatl’s model-facing Shell runs under the operator’s macOS login identity. Existing environment scrubbing prevents provider and auth variables from reaching the command, but it is not an operating-system authority boundary.
An approved Shell command—and any process it starts through a build script, test, repository input, or downloaded content—can still potentially use ambient same-user authority to access:
- the login Keychain through
/usr/bin/securityor Security.framework; - Unix sockets such as SSH/GPG agents, Docker/Colima/OrbStack sockets, and credential brokers;
- loopback or external network services;
- Apple Events and Launch Services;
- filesystem paths outside the attached workspace;
- Git credential helpers and interactive credential prompts.
The shell-spawn chokepoint is internal/adapter/osfs/osfs.go (CommandRunner.run), after managed temporary-lease allocation and before exec.CommandContext. Main and child runner construction converges at internal/app/build.go (newCommandRunnerForRoot).
Goal
On supported macOS versions, run every model-facing Shell process tree under a deny-default Seatbelt (/usr/bin/sandbox-exec) policy that grants only the authority required for its attached environment:
- read/write access to the exact environment workspace;
- read/write access to an exact, private, per-command temporary lease;
- narrowly enumerated read-only system and installed-toolchain roots;
- only the minimal process, signal, device, sysctl, and Mach-service access proven necessary by qualification tests.
The model command must remain a single verbatim argv element. It must never be interpolated into SBPL or another shell string.
Non-goals
- Sandboxing Mecatl’s own Keychain calls, ToolHive credential refresh, or other host-side application behavior.
- Sandboxing fork-time Git or operator-configured hooks in this first slice.
- Permitting Shell network access, loopback, Unix sockets, or a credential proxy.
- Changing
tool.CommandRunner,tool.Environment, Shell schemas, events, or exported engine APIs. - Treating deprecated Seatbelt as a permanent cross-platform sandbox solution.
Proposed design
- Add an adapter-local command-invocation wrapper in
internal/adapter/osfs, applied after temporary-lease allocation and before spawning the shell. - On Darwin, the wrapper invokes only the fixed path
/usr/bin/sandbox-execwith:- a reviewed, embedded deny-default SBPL profile;
- canonicalized trusted
-DWORKSPACE=…and-DSCRATCH=…parameters; - the configured shell,
-c, and the unmodified model command as final argv.
- Keep the existing direct runner as the explicit unsafe/off behavior only.
- Wire the mode once in
internal/appso it covers main-session Shell, direct-write Subagents, read-only Subagents and Team members, force-copy Team members and Parallel branches, and capture/streaming/foreground/background command paths. - Keep no-FS sessions shell-less.
- Construct a trusted per-command sandbox environment after lease allocation:
- private
HOME,TMPDIR,GOTMPDIR,GOCACHE, andXDG_CACHE_HOME; GIT_TERMINAL_PROMPT=0;- offline first-phase Go settings:
GOPROXY=off,GOSUMDB=off,GOTOOLCHAIN=local; - retain
envscrubandgitenvas defense in depth.
- private
Required security properties
The Seatbelt policy must deny:
- Keychain services and data:
com.apple.SecurityServer,securityd/secd, Keychain agents, and Keychain database paths; trustdin the offline initial profile;- all direct TCP/UDP networking, including loopback;
- Unix-domain sockets;
- Apple Events and Launch Services;
- reads outside admitted roots and writes outside the workspace/private lease;
- direct Keychain utilities and container runtimes as defense in depth;
- inherited credential-bearing or connected file descriptors.
auto and yolo may affect permission approval, but must never weaken the syscall-level sandbox.
A Seatbelt launch, profile-generation, canonicalization, or root-validation failure must not fall back to unsandboxed execution.
Compatibility contract
Phase 1 intentionally does not support:
git fetch,git push, or any remote Git credential flow;- uncached Go/module/toolchain downloads;
- package managers or remote build services;
- tests requiring host loopback listeners;
- host SSH/GPG/Docker/credential sockets.
A future network feature, if needed, must use a separately designed and authenticated host-side proxy. It is an outlives-a-call resource and requires inventory in docs/adr/0027-cloud-native.md.
Important implementation constraint: temporary scope
The current runner does not yet provide an exact private scratch path on every execution route:
- managed calls allocate a private lease;
- system temporary scope currently points at a configured shared directory;
- plain streaming currently does not allocate a managed lease.
Sandboxed mode must either:
- allocate a private lease for every sandboxed invocation, including streaming/background calls; or
- reject/translate system temporary scope under Seatbelt.
It must not claim “exact private scratch” while granting a shared system temporary root.
Qualification scenarios
Security tests on supported Darwin must use non-vacuous controls: prove each operation succeeds unsandboxed against a disposable fixture, then prove the identical sandboxed operation is denied.
- Keychain: disposable keychain sentinel is readable unsandboxed but inaccessible under
/usr/bin/securityand a compiled Security.framework client. - Process tree: nested shells, compiled programs, background children, and grandchildren retain the boundary.
- Mach and IPC: SecurityServer/securityd/trustd lookups, Unix sockets, SSH/GPG/Docker-style sockets, and credential helper paths are denied.
- Host escape: AppleScript, direct Apple Events,
open, and Launch Services cannot control or launch host applications. - Network: loopback and external TCP access are denied.
- Filesystem: absolute paths,
.., symlinks,/privatealiases, hard links, and writable-ancestor rename attempts cannot escape admitted roots. - Git: no Keychain helper, repository-configured helper, AskPass, interactive prompt, remote operation, or hook execution occurs.
- Functionality: shell pipelines/redirection, cancellation/process-group cleanup, local Git inspection, offline Go build/test/race/cgo, managed-temp isolation, capture, streaming, and all delegation paths work within admitted roots.
- Failure closed: a broken/missing Seatbelt executable or invalid profile never runs the original command unsandboxed.
Darwin integration tests must fail—not skip—when required Seatbelt support cannot compile or launch. Non-Darwin platforms may skip Darwin integration tests while retaining portable policy-construction coverage.
Decisions required
This is Architectural work. Resolve these before drafting the acceptance plan:
- Default: required-by-default on supported macOS, with operator-only warned
unsafe-off; or initial opt-in? - Filesystem posture: narrowly admitted system/toolchain roots; or broad reads with sensitive-path denies?
- Network: offline/no-socket Phase 1; or include an authenticated proxy now?
- Local Git mutation: which local mutations, if any, are supported under the policy?
- Failure behavior: fail Build when required Seatbelt is unavailable; or start without Shell?
- Configuration: operator-global
command_sandbox.modeonly; is a CLI override needed? - Support range: oldest qualified macOS release and explicit deprecation/replacement posture for
sandbox-exec.
Delivery requirements
After decisions are recorded:
- draft a Split acceptance plan and a new ADR;
- add strict operator-tier configuration parsing; project configuration must not weaken the sandbox;
- update
docs/architecture.md,docs/design/IMPLEMENTATION-NOTES.md,docs/design/PRODUCTION-READINESS.md, and the owninguser-docs/Shell/security/settings pages; - re-audit
docs/adr/0027-cloud-native.mdif the implementation adds an outlives-a-call proxy, cache, monitor, or similar resource; - run the normal documentation and full verification gates.
Prior art
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading internal/adapter/osfs/osfs.go around CommandRunner.run and internal/app/build.go around newCommandRunnerForRoot. Resolve the listed architecture decisions, then draft the Split acceptance plan and new ADR before implementing. Done means the Darwin qualification scenarios, failure-closed behavior, delegation paths, temporary-scope contract, and required documentation gates are covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, macos
- Domain
- operating-systems, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 20/100