Confine coders and Tester commands on Linux - #3
Open
Jerhaad wants to merge 8 commits into
Open
Conversation
added 8 commits
August 3, 2026 00:20
Seatbelt and Landlock cannot share a rendered profile: Seatbelt takes ordered rules where a deny beats a broader allow, while Landlock unions every matching rule so a nested one can only widen an ancestor's grant. The Linux renderer therefore enumerates carve-outs into sibling grants rather than layering denials over a broad allow, and keeps the enumerated directory listable so tools can still scan it. Landlock survives execve but has to be applied before it, so the launcher re-executes the Fluent binary instead of shelling out to a sandbox helper. An unenforced ruleset fails the launch rather than running a coder unconfined.
`#[serial]` orders these against each other but not against the rest of the suite, so a leaked HOME pointing at a deleted tempdir changed what every later test in the process rendered.
Both launch sites hardcoded `sandbox-exec`, so Linux had no confinement to fall back to. They now ask the platform for a launcher and its arguments, which is where Seatbelt and Landlock diverge: Landlock has no launcher binary, so Fluent re-executes itself to apply the ruleset before the exec it confines. `FLUENT_SANDBOX_BACKEND` selects a backend explicitly. Tests that stand a mock launcher on PATH need it, because a kernel facility has no binary to replace; it cannot turn confinement off, only pick which backend must succeed. Assertions about a rendered profile now name the root they care about rather than the syntax that grants it, since the two backends spell the same grant differently and Landlock expresses a denial as the absence of a grant.
A suite that never looked at what changed is the expensive kind of green: a chart template, a compose file, or a migration whose index its own query cannot use passes on the strength of unrelated checks. A command can now declare the paths it covers and runs only when the candidate touches them, which is what lets a project attach a gate to a file class its main suite cannot judge. Skipped commands are recorded rather than dropped, so a Reviewer can tell a gate that passed from one that never ran. An unknown change set runs everything: the safe direction here is more gates, not fewer.
Treating the Linux backend as a transliteration of the Seatbelt one fails open: Landlock has no deny rule, so the nested denial that works on macOS silently leaves the broader grant standing.
Introduce a single read_secret() function with platform-specific implementations behind cfg(target_os) gates. macOS keeps the existing security find-generic-password calls. Linux queries secret-tool (libsecret CLI) and falls back to environment variables when no keyring daemon is running. Add a CredentialConfig table mapping service names to environment variable fallbacks, eliminating per-call cfg branches in inject_oauth_token, inject_brave_search_key, and the Anthropic API key lookup. The aws configure export-credentials path remains unchanged as it is already cross-platform. Add unit tests for the environment variable fallback and absent secret-tool scenarios, neither of which requires a running keyring.
An internet password is a different Keychain class from a generic one, so the Anthropic key stopped resolving on macOS when both went through find-generic-password. A refresh no longer falls back to the environment: the variable it would read is the stale token it was called to replace, so falling back reported success while changing nothing.
The platform temp directory sits inside a hierarchy the policy grants — /tmp on Linux, /var/folders on macOS — so a fixture there was enumerated as part of a system rule and the assertions read their own effect. The environment fallback is a Linux-only path, so its test no longer runs where the Keychain answers instead.
Owner
|
@Jerhaad thank you for sending this. I'll take some time to understand it over the next day or so and get back to you with thoughts and comments. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fluent's confinement is Seatbelt-only: both coder launch sites and the Tester hardcode
sandbox-exec, so on Linux there is nothing to fall back to. This adds a Landlock backend so a Linux host runs coders confined rather than unconfined.Why Landlock
It needs no external binary and no user namespace, and its path-hierarchy rules line up with SBPL's
subpathgrants. Both launch sites now ask the platform for a launcher and its arguments; that is where the two diverge, because Landlock has no launcher binary. Fluent re-executes itself (fluent sandbox-run, hidden) to apply the ruleset before theexecveit confines — restrictions surviveexecve, and applying them in apre_exechook would have to allocate betweenforkandexec.The constraint that shapes the renderer
Landlock unions every matching rule, so a nested rule can only widen an ancestor's grant. There is no deny. The nested denial that works under Seatbelt's first-match-wins ordering would silently leave the broader grant standing, which fails open.
Every carve-out is therefore enumerated into sibling grants at render time. Three consequences, each found by running it rather than reading it:
/tmp) otherwise hands back every secret the home rules withhold.~/.sshout of$HOMEalso stops anything listing$HOME./dev/stdin,/dev/stdout,/dev/stderr, and/dev/fdare symlinks into/proc/self/fd; the kernel refuses them as rule targets withEBADFD, and a rule buys nothing since Landlock mediates opening a path, not writing through an open descriptor..fluent/expertise/learnings/landlock-rules-union-never-subtract.mdrecords this for future agents.Fails closed, loudly
Landlock is commonly compiled in but left out of the boot
lsm=list. There the syscall returnsEOPNOTSUPPwhile a best-effort ruleset reports success and enforces nothing, so the availability probe demands ABI 1 outright and an unenforced ruleset fails the launch rather than running a coder unconfined.tests/linux_sandbox.rsdrives the real launcher against the kernel and names which outcome each assertion expects — a forbidden action that failed because the launcher never started proves nothing about confinement. Verified on a kernel that does enforce (Landlock ABI 4): reads and writes inside the root work, a withheld~/.sshkey is unreadable, a write outside every writable root is denied, and a handoff-only policy closes the shared temp trees.Known gaps
common.sb's "inbound localhost only", so this restricts no network rather than fake it. Outbound matches Seatbelt, which already allows all.keep-awakestays macOS-only. It inhibits laptop idle sleep; its message now says so instead of just "macOS-only".Two other changes
Credentials.
security find-generic-passwordreturns nothing on Linux, so every injected secret was silently absent. Lookups go through one helper with a per-platform implementation — Keychain on macOS,secret-toolthen a named environment variable on Linux, since a headless host often runs no keyring daemon. The environment fallback is refused during refresh: the variable it would read is the token the refresh exists to replace.Change-scoped Tester commands. A command may declare the paths it exercises and then runs only for a candidate that touches them:
This lets a project attach a gate to a file class its main suite cannot judge; without it such a change lands green on the strength of checks that never looked at it. Existing configs are unaffected — a command with no
when_changedalways runs, and an undeterminable diff runs everything. Skipped commands are recorded intester-results.jsonso a Reviewer can tell a gate that passed from one that never ran.Testing
FLUENT_SANDBOX_BACKENDselects a backend explicitly. The integration tests stand a mocksandbox-execonPATHto observe how Fluent drives a sandboxed launch, and a kernel facility has no binary to replace, so they pin Seatbelt; the Landlock backend is covered separately against a real kernel. It cannot disable confinement, only choose which backend must succeed.Assertions about a rendered profile now name the root they care about rather than the syntax granting it, since Landlock expresses a denial as the absence of a grant.
One unrelated fix was needed to get there: the Fargate tests set
HOMEto a tempdir and never restored it, so a leakedHOMEpointing at a deleted directory changed what every later test in the process rendered.Run on both platforms, each compared against this branch point on the same host.
macOS 26.5.1, arm64: library suite fully green (1227 passed, 0 failed). Integration suite 376 passed with 2 failures —
post_land_retry_ignores_a_malformed_retained_candidateandpre_land_no_expertise_retry_runs_only_learner_and_preserves_mode— whichmainfails identically on the same machine, so they predate this branch.Linux: 1228 passed with
login_status_preflight_accepts_authenticated_worker_homefailing, and 376 passed withconcurrent_learner_retry_and_land_never_mutate_after_mergeandupdate_replaces_binary_and_rematerializes_skillsfailing. All three fail onmainhere too. Notablylogin_status_preflightpasses on macOS, so that one looks Linux-specific rather than flaky.The Keychain item-class split is verified rather than assumed: in a throwaway keychain on macOS, an internet-class item is returned by
find-internet-passwordand not byfind-generic-password. That is the whole reason the class has to be part of the config.Licensing
The repository publishes no LICENSE (#2), so there is no grant under which to offer this work. Everything here is mine to license and I am offering it under MIT; say the word if you would rather have it under whatever license you adopt.