stacklok / stacklok/mecatl

Security: confine model-facing Shell with a macOS Seatbelt boundary

Open
#1,608 0 comments 0 reactions 0 assignees View on GitHub

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/security or 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

  1. Add an adapter-local command-invocation wrapper in internal/adapter/osfs, applied after temporary-lease allocation and before spawning the shell.
  2. On Darwin, the wrapper invokes only the fixed path /usr/bin/sandbox-exec with:
    • 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.
  3. Keep the existing direct runner as the explicit unsafe/off behavior only.
  4. Wire the mode once in internal/app so 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.
  5. Keep no-FS sessions shell-less.
  6. Construct a trusted per-command sandbox environment after lease allocation:
    • private HOME, TMPDIR, GOTMPDIR, GOCACHE, and XDG_CACHE_HOME;
    • GIT_TERMINAL_PROMPT=0;
    • offline first-phase Go settings: GOPROXY=off, GOSUMDB=off, GOTOOLCHAIN=local;
    • retain envscrub and gitenv as defense in depth.

Required security properties

The Seatbelt policy must deny:

  • Keychain services and data: com.apple.SecurityServer, securityd/secd, Keychain agents, and Keychain database paths;
  • trustd in 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:

  1. allocate a private lease for every sandboxed invocation, including streaming/background calls; or
  2. 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.

  1. Keychain: disposable keychain sentinel is readable unsandboxed but inaccessible under /usr/bin/security and a compiled Security.framework client.
  2. Process tree: nested shells, compiled programs, background children, and grandchildren retain the boundary.
  3. Mach and IPC: SecurityServer/securityd/trustd lookups, Unix sockets, SSH/GPG/Docker-style sockets, and credential helper paths are denied.
  4. Host escape: AppleScript, direct Apple Events, open, and Launch Services cannot control or launch host applications.
  5. Network: loopback and external TCP access are denied.
  6. Filesystem: absolute paths, .., symlinks, /private aliases, hard links, and writable-ancestor rename attempts cannot escape admitted roots.
  7. Git: no Keychain helper, repository-configured helper, AskPass, interactive prompt, remote operation, or hook execution occurs.
  8. 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.
  9. 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:

  1. Default: required-by-default on supported macOS, with operator-only warned unsafe-off; or initial opt-in?
  2. Filesystem posture: narrowly admitted system/toolchain roots; or broad reads with sensitive-path denies?
  3. Network: offline/no-socket Phase 1; or include an authenticated proxy now?
  4. Local Git mutation: which local mutations, if any, are supported under the policy?
  5. Failure behavior: fail Build when required Seatbelt is unavailable; or start without Shell?
  6. Configuration: operator-global command_sandbox.mode only; is a CLI override needed?
  7. 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 owning user-docs/ Shell/security/settings pages;
  • re-audit docs/adr/0027-cloud-native.md if 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.