Hoppa till innehåll

Sandlåda och säkerhet

En agent läser filer, kör skalkommandon och anropar externa API:er å dina vägnar. Felet du bryr dig om är sällan illvilja — det är en agent som missläser en sökväg och skriver om din arbetsutcheckning, eller en som pushar en gren du inte hade hunnit granska klart. Så frågan produkten måste svara på är inte “går agenten att lita på” utan “hur stort är skadeomfånget när den har fel”.

Control Center svarar på det i lager, och lagren är medvetet oberoende. Saknas ett håller de andra fortfarande. Det spelar roll, eftersom det yttersta lagret — OS-sandlådan — är det enda villkorliga: det beror på att värden erbjuder en backend, och på Windows gör ingen värd det.

Fem saker begränsar en agentkörning på varje värd, med eller utan sandlåda. Ingen av dem är en kärngräns; alla är verkliga. Där värden erbjuder en backend sitter OS-sandlådan ovanpå dem, och “Vad sandlådan tillför och var den slutar” nedan beskriver vad det ändrar.

Sin egen copy-on-write-worktree. Varje samtal skapar sin egen utcheckning av varje länkat repo, klonad copy-on-write genom den medföljande rift-native-delen i stället för kopierad byte för byte, så isoleringen är billig nog att vara standard. Det registrerade repositorium du lade till i arbetsytan är inte katalogen agenten arbetar i — två samtal som rör samma repo kan inte se varandras ändringar och ingen av dem redigerar din utcheckning.

Granulariteten är värd att vara precis om: worktreen är per samtal, inte per agent. Två agenter i samma samtal får distinkta overlay-arbetskataloger som delar samtalets repos/ — vilket är poängen, eftersom de samarbetar om en och samma ändring — medan deras privata agentkataloger hålls åtskilda. Copy-on-write är den enda backenden där rift finns: ett filsystem som inte kan reflinka, eller en datakatalog på en annan volym än repot, misslyckas med tilldelningen i stället för att falla tillbaka. Windows är undantaget — där finns ingen copy-on-write-backend, så git worktree är backenden, inte en tyst försämring.

Sökvägsbegränsning i den inbyggda harnessens filverktyg. Harnessens verktyg read, write, edit, apply_patch, search, find och search_files löser varje sökväg mot körningens arbetskatalog plus delade rötter — samtalets repos/-katalog, där worktreena ligger, och ytans bilagekatalog när den finns — och vägrar allt som hamnar utanför. Absoluta sökvägar accepteras bara om de redan låg innanför. En symlänk inuti worktreen som pekar ut ur den löses också upp och vägras. Det är därför en agent inte kan write till ~/.ssh ens utan en sandlåda i bilden.

Lägets verktygsyta. Ett samtals läge avgör vilka verktyg som materialiseras för körningen över huvud taget. I de skrivskyddade lägena (plan, review, orchestrate) finns de muterande verktygen helt enkelt inte i registret, och skalkommandopolicyen för dessa lägen nekar varje muterande kommando direkt — git commit, git push, npm install, docker och resten — i stället för att fråga om dem. Ett verktyg som inte finns kan inte anropas, vilket är en starkare garanti än ett verktyg som finns och är grindat.

De enhetliga åtgärdsräckena. Varje muterande verktyg deklarerar de effektklasser det kan producera, och policy-motorn löser var och en till tillåt, fråga eller neka innan anropet körs. En prompt utan ansluten godkännare löses till nekat — en fråga som ingen kan svara på är aldrig ett ja. Se Räcken för lösningsmodellen.

Mäklade inloggningsuppgifter och en sanerad miljö. Inloggningsuppgifter präglas per start och återkallas när körningen avslutas, och en körnings GitHub-token kan läsa men inte pusha — se “Pushar går genom Control Center” nedan. ACP, harnessens bash och Claude Code under den inbyggda inlindningen får en miljö rensad från dynamiska länkarens och tolkens injektionsvektorer (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND och deras släktingar) innan processen startar. Claude Code via NoSandboxAdapter ärver värdmiljön.

Harnessens filverktyg körs i serverprocessen, inte i en barnprocess, så ingen sandlådeprofil skulle kunna begränsa dem ens om en sådan kopplades på — bara bash går genom den sandlådade kommandoköraren. Deras inneslutning är sökvägskontrollen som beskrivs ovan, och den kontrollen är därför bärande i stället för en andra försvarslinje.

Verktyget bash är den andra platsen att vara precis på. Dess kommandon utvärderas mot lägets kommandopolicy och startas sedan med en sanerad miljö och worktreen som arbetskatalog. Där en backend finns lindas den starten in, så sandlådan är det som hindrar ett skalkommando från att skriva utanför worktreen; där ingen finns — Windows, eller Linux utan bwrap och socat — är kommandopolicyen det enda mellan kommandot och resten av disken. I läget chat nekar policyn hårt en fast lista (privilegieeskalering, förstörelse av system och disk, container- och namnrymdsflykt) och frågar vid fjärrtillståndsmutationer (git push, gh pr create, paketpubliceringar och -installationer). Allt som står på ingen av listorna körs. Pipelines, kedjor och underskal tolkas och varje delkommando utvärderas separat, där det mest restriktiva beslutet vinner.

Claude Code, när det används som adapter, är fallet där sandlådan väger tyngst. Control Center startar det med --dangerously-skip-permissions, eftersom en icke-interaktiv claude -p annars skulle blockera för evigt på sin egen godkännandeprompt. Dess läs-, skriv- och redigeringsanrop sker inuti dess egen process och Control Center ser dem aldrig. Dess Bash-anrop skickas, innan de körs, till en hook på servern som klassificerar kommandoraden och frågar den matchande regeln, och de mcp__*-verktyg som Control Center serverar passerar grinden som vanligt. Där en backend finns sker de osedda anropen fortfarande inuti sandlådeinlindningen, så OS:et är gränsen runt dem. Där ingen finns är ingenting det. Varje adapter anger detta i sin verkställighetsmatris — se adapternas ärlighetsmatris.

Det finns inga behörighetsflaggor per agent. Om en agent får skapa en commit, pusha, öppna en pull request, skriva till ett ärendesystem eller nå nätverket avgörs av räckena — tillåt, fråga först eller neka — och samma regler gäller oavsett vilken adapter som kör agenten.

En push är den åtgärd som måste hålla även mot en agent som försöker kringgå en regel, så den framtvingas genom vad agenten har, inte genom vad den kör. Med en GitHub-app konfigurerad får en körning i en GitHub-worktree en repo-avgränsad installationstoken som kan läsa innehåll och arbeta med pull requests men inte pusha. Körningens git-konfiguration skriver om varje push-URL till GitHub (https://github.com/, git@github.com:, ssh://git@github.com/) till agentkörningsgatewayen, en loopback-slutpunkt på cc_server. Gatewayen läser vilka refs pushen uppdaterar, frågar regeln Pusha till en remote — så en regel som är begränsad till grenar, till exempel “neka push till main”, gäller — och skickar först därefter pushen vidare till GitHub med en skrivtoken som den präglar för ändamålet och aldrig lämnar till agenten. En nekad push syns i gits egen utdata:

! [remote rejected] main -> main (Control Center policy: …)

Gatewayen skickar bara vidare pushar till worktreens eget repo eller till ett GitHub-repo som är länkat till arbetsytan. Fetch går direkt till GitHub med lästoken.

Andra skalkommandon möter en mjukare grind. Harnessens bash-verktyg och, via en hook, Claude Codes Bash-verktyg klassificerar varje kommandorad och frågar den regel den hör till: git commit, merge, rebase, cherry-pick, revert och am är Skapa en commit; gh pr create är Öppna en pull request; gh pr merge, review, comment och close är Publicera en granskning eller sammanslagning; gh issue create, edit, comment och close är Skriv till ett externt ärendesystem; paketinstallationer är Installera ett paket; curl och wget är Använd nätverket. Det känner igen det vanliga sättet att skriva en åtgärd, och en agent som är fast besluten att dölja en (eval, en skriptfil, ett direkt API-anrop) kan göra det. Pushar är inte beroende av det.

Ingen API-nyckel för ärenden läggs i en sandlåda. Agenter skriver ärenden via Control Centers MCP-verktyg, som frågar Skriv till ett externt ärendesystem.

Regeln Använd nätverket läses en gång, när körningen startar. En sandlådas nätverk är antingen på eller av, så neka stänger av det medan tillåt och fråga först lämnar det på; harnessens verktyg web_fetch och web_search frågar fortfarande vid varje anrop. Inloggningsuppgifterna återkallas när körningen rivs ner, tillsammans med varje skrivtoken som gatewayen har präglat, så ett handtag överlever inte körningen.

Enumen SandboxBackend har tre värden — native (Seatbelt på macOS, bubblewrap på Linux och WSL2), none och microvm. Inlindningen av agentkommandon använder bara de två första: microvm är backenden med avskild VM för interaktiva terminaler och stationer, inte en tyst uppgradering av en bash-inlindning. Att förstå den inbyggda formen förklarar vad du vinner där en sådan kopplas på och vad den aldrig skulle ge dig.

cc_server sonderar värden en gång vid start och där en backend svarar matar den båda de skarvar som dispatchvägen har, eftersom transporterna inte delar någon. Den första (SandboxPort) lindar in Claude Code-CLI:n. Den andra (SandboxManager) lindar in ACP-adaptrar och den inbyggda harnessens bash-verktyg. Att bara mata den ena skulle lämna de andra startande utan inlindning, vilket är vad som brukade hända. På en värd utan backend, eller under --sandbox off, startar de alla utan inlindning och servern säger vilket fall du är i på sin startrad. ACP och harnessens bash sanerar fortfarande miljön; Claude Code via NoSandboxAdapter ärver värdmiljön.

En yta väljer medvetet att stå utanför: den serverdrivna terminalen tar ett backend-argument per session och cc_server konstruerar den med värdskalet som standard, så en terminal du öppnar från appen körs utan sandlåda om inte en klient uttryckligen anger native. Det är ett riktigt skal på servervärden, inte en agentkörning.

På Linux bygger bubblewrap en monteringsnamnrymd, så agenten ser verkligen bara det som bands in i den. På macOS tillämpar sandbox-exec en Seatbelt-profil som är tillåtande som standard — en helt neka-som-standard-profil gör ett interaktivt skal obrukbart, eftersom antalet dyld-, XPC- och Mach-anrop att räkna upp är opraktiskt. macOS-profilen utgår därför från (allow default) och skär ut nekanden: läsningar av hemliga sökvägar, en skrivpolicy återställd till neka med uttryckliga tillåtanden ovanpå, unlink-blockering på varje nekad sökväg och dess förfäder så att en namnändring inte kan kringgå ett skrivnekande, exec-nekanden för alltid farliga binärer och för allt som kopierats in i en skrivbar katalog samt nätverk begränsat till loopback-proxyportarna plus DNS.

Den praktiska konsekvensen: på Linux är agentens vy av filsystemet snävare; på macOS kan agenten fortfarande läsa det mesta av värden, och dina registrerade utcheckningar skyddas av uttryckliga skriv- och namnändringsnekanden i stället för av osynlighet. Båda är isolering på namnrymdsnivå, inte en säkerhetsgräns i kärnan — backendarna säger det själva i sina egna förmågsanteckningar.

Bind-monteringsuppsättningen är där gränsen mellan agenter dras. Overlay-dispatch (Claude Code och ACP) monterar upp till fyra sökvägar, lösta per körning: agentens overlay-arbetskatalog läs-och-skriv, agentens konfigurationskatalog skrivskyddad, samtalets delade repos/-katalog läs-och-skriv när den finns och ytans bilagekatalog skrivskyddad när den finns. En systeragents katalog monteras aldrig. Harnessens bash-inlindning är snävare: den bind-monterar bara körningens arbetskatalog. Det är samma form som harnessens sökvägskontroll i processen framtvingar i mjukvara, så de två stämmer överens — men bara sökvägskontrollen täcker harnessens egna filverktyg, som aldrig når en monteringsnamnrymd över huvud taget.

Dina registrerade utcheckningar ansluter till skrivförbudsuppsättningen ovanpå det, i varje läge och på alla tre inlindade vägar (Claude Code, ACP, harnessens bash), och varje regel skrivs ut i både sin bokstavliga och sin symlänksupplösta stavning. Den andra stavningen är inte en finess: kärnan matchar en nekanderegel mot den upplösta sökvägen, och macOS gör upplösning till det vanliga snarare än det exotiska fallet — /tmp är egentligen /private/tmp, /var/folders/… är egentligen /private/var/folders/… — så en utcheckning som nåddes via endera skulle ha varit skrivbar medan profilen såg ut att förbjuda det.

Nätverket, under en sandlåda, är en domän-allowlist per session som framtvingas av loopback-proxyer i processen. Utgående trafik nekas som standard: med nätverket av startar proxyna fortfarande men deras allowlist är tom, så varje utgående anslutning nekas. Med det på bygger uppslagaren allowlisten från en kurerad baslinje — LLM-API-leverantörer, git-hostning, paketregister, modellkatalogen — plus GitHubs domäner, och matchar varje anslutnings målvärd mot den. Jokertecken fungerar (*.example.com matchar underdomäner, inte toppdomänen). En separat alltid-nekad-lista vinner över allowlisten och täcker molnmetadatanslutningar och telemetrimottagare. Det som inte är konfigurerbart är listan själv: specifikationen bär ett egressAllowlist-fält för extra domäner, men inget i dispatchvägen fyller det, så i praktiken får varje körning baslinjen plus GitHub och inget mer.

Nekanden, på macOS, är observerbara: en monitor avlyssnar systemloggen efter Sandbox-nekanden, filtrerar bort känt brus, deduplicerar retry-stormar och lyfter fram var och en som en SandboxViolation med namn på vad som försöktes och vad som blockerades. På Linux tolkar inlindningsvägen stderr efter EPERM, Operation not permitted och Permission denied och sänder ut samma händelsetyp — det finns ingen systemloggavlyssning. De anländer på körningens egen händelseström, så ett nekat systemanrop syns i transkriptet i stället för bara i en logg. Telemetrin finns bara medan en inbyggd sandlåda faktiskt lindar in processen — på en värd utan backend finns inget att observera. Den är inte heller kopplad till räckena: en överträdelse kommer från ett nekat systemanrop, aldrig från en regeluppslagning.

Sandlådan har ingen godkännandeväg. Den startar, kör, sänder ut händelser och rivs ner; det finns ingenstans i den för en människa att svara på en fråga. När du ser en bekräftelse i en yta — godkänn den här pushen, godkänn den här borttagningen — restes den av räcken-motorn (eller harnessens kommandopolicy) innan åtgärden ens nådde körtiden.

Den här ordningen är avsiktlig. Ett godkännande som skedde efter systemanropet skulle vara en revisionspost, inte en grind. Det är också därför fail-closed-regeln bor i räckena: utan ansluten godkännare fattas beslutet innan något körs, och det är ett nekande.

Felsök en agent är den enda platsen som anger vad som kan kontrolleras för hand.