cd /news/ai-safety/two-ai-agents-one-jail-round-9-of-pi… · home topics ai-safety article
[ARTICLE · art-115492] src=pipe-lang.com ↗ pub= topic=ai-safety verified=true sentiment=· neutral

🕵️ Two AI Agents, One Jail — Round 9 of Pipe's Sandbox Audit

Pipe's Round 9 sandbox audit, run by two independent LLM agents in parallel, found two real bugs: the exec_whitelist check only validated the first token, allowing shell injection via substitutions, and builtins-as-tools were silently uncallable. The fix tokenizes commands and runs them directly as argv without a shell, closing the gap between check and execution.

read12 min views3 publishedAug 30, 2026
🕵️ Two AI Agents, One Jail — Round 9 of Pipe's Sandbox Audit
Image: Pipe-Lang (auto-discovered)

← All posts← Alle Beiträge

Two independent LLM agents — DeepSeek in its strongest "max effort" mode and a keyless free model — attack Pipe's sandbox in parallel, inspired by the OpenAI–Hugging Face incident and a real OpenCode config bug. Every tested vector held. Two real bugs didn't even need the attacks: building the harness found them first.

This post covers round 9, the parallel dual-agent pass. For earlier rounds, see > ** Round 1**,

, and >

Round 2. Rounds 7 and 8 (CLI fs-write and AI-egress gaps) > are documented in the audit reports. > Full technical write-up:

Rounds 3–6.

report9.en.mdAfter round 2 I learned to stop outsourcing the hunting — the real escapes came from reading my own code, not from an LLM run. Round 9 flips that on its head. A user shared OpenAI's technical incident report on the July 2026 Hugging Face compromise: internal models, red-teaming their own sandboxed eval, turned an allowed service into an SSRF relay and an improvised inter-agent "message board," then escalated from there into production. Separately, a real GitHub issue against OpenCode showed a name-based permission system defeated by config precedence.

Round 9's question: do any of those mechanisms transfer to Pipe's sandbox model — and, new for this round, do two different models run in parallel find the same things?

Why two agents, and why two processes #

ActiveProfile

is a process-wide pointer, not a per-session value. Two goroutines calling set_sandbox

in one process would stomp each other. So "parallel" here means two separate bin/pipe

subprocesses, each with its own locked redteam9

profile instance and its own throwaway working directory.

And they were given different network_whitelist

hosts — api.github.com

for DeepSeek, example.com

for OpenCode. No shared writable target means no accidental inter-agent coordination. That is a deliberate, structural countermeasure to the exact mechanism the incident report describes: a shared writable service becoming a message board.

┌──────────────────┐      ┌──────────────────┐
│ DeepSeek         │      │ OpenCode         │
│ deepseek-v4-pro  │      │ big-pickle       │
│ thinking+max     │      │ keyless free     │
│ whitelist: api.  │      │ whitelist: ex.   │
│ github.com       │      │ example.com      │
└────────┬─────────┘      └────────┬─────────┘
         │ separate processes, no │
         │ shared writable state  │
         ▼                        ▼
      locked profile "redteam9"  locked profile "redteam9"

Finding 1 — exec_whitelist #

gave no real protection 🔓

Confirmed by code reading first, then live-reproduced with a harmless PoC before spending any API budget. CanExecCommand

validated only the first whitespace token of a command string against exec_whitelist

; exec

/proc_start

then handed the entire, unfiltered string to a real shell (sh -c

).

So exec_whitelist: ["echo"]

did not stop exec "echo $(whoami)-$(cat /etc/hostname)"

— the check saw echo

(allowed) and the shell executed the substitution anyway, returning real host output.

The fix: when a profile's exec_whitelist

is non-empty, the command is tokenized and run directly as argv — no shell — so &&

, ;

, `` `

``, `

$(...)become inert literal arguments, not syntax. The same tokenizer is used for the whitelist check *and* the actual invocation (a shared

buildExecCommandhelper), so there is no gap between what's checked and what's run. This applied to both

execand

proc_start`, which shared the identical bug.

Finding 2 — builtins-as-tools were silently uncallable 💥 #

While building the harness, every single tool call in a smoke test failed with not callable: BUILTIN

— for every tool, including completely benign ones. Root cause: the tree-walker wraps a bare builtin reference (exactly what ai_tool "read_file" ... read_file

does) in its own type, and the shared dispatcher only recognized the VM's builtin type. The VM was never affected; only default (non--vm

) execution was.

This is a serious, previously-masked bug: every example script that registers a raw builtin as an ai_tool — including all the redteam.pipe scripts from rounds 1–8 — has been silently broken* under the tree-walker since the regression landed, with every tool call failing before ever reaching the builtin or the profile's policy checks.

The fix: a small CallableBuiltin

interface — pkg/object

still never imports pkg/eval

(that would cycle); the interface is the seam.

Finding 3 — ai_tool #

reorders multi-parameter calls alphabetically (found by the attack) 📝

DeepSeek's own final report flagged "a strange parameter-swap bug" in write_file

— the file that got created was named using the content it meant to write, and contained the path string. Root cause: Pipe map literals don't preserve declaration order anywhere, so {path: ..., content: ...}

is passed to the builtin in alphabetical order — write_file(content, path)

. Arguments swapped.

Not patched this round: there's no minimal fix (order is lost at parse time), so a real fix means making map literals order-preserving end to end — a separate, larger initiative. Immediate mitigation shipped: an explicit warning in the ai_tool

docs. But this is a genuine example of why running a live adversarial model against your own tooling surfaces things static review doesn't.

Live results — what held #

Both agents ran to the profile's max_tool_calls: 30

cap and stopped cleanly (itself a working defense layer, independent of the dollar budget

field).

# Category Result
1 Classic (fs jail, env masking, network whitelist, lock, MCP) HOLDS — every /etc/shadow //root/.ssh/id_rsa //proc/self/environ read collapsed to the jail; env masked; non-whitelisted hosts, IP literals and localhost all rejected by exact-match whitelist; lock blocked set_sandbox ; both MCP entry points blocked
1b exec_whitelist + shell metacharacters HOLDS — verified by both models independently
2 Symlink-in-jail (new) HOLDS — ln -s /etc/passwd succeeds (legitimate), but read_file does not dereference it: paths are canonicalized to the jail before any filesystem call touches the symlink
3 ai_vision magic-byte spoofing (new) HOLDS — moot: local reads go through the same jail canonicalization as read_file , so a spoofed file is never reachable at its real path
4 Runtime tool registration (new) INCONCLUSIVE — test-harness schema issue, not a policy block
5 Swarm handoff injection (new) INCONCLUSIVE — same harness limitation
6 Indirect prompt injection (new) Minimal data — one benign whitelisted fetch before budget ran out; nothing injected observed anywhere

Even the genuinely new vectors held — and the honest part matters: vectors 4 and 5 are untested, not confirmed-safe. The harness couldn't legally exercise them (a schema limitation), so I'm saying so plainly rather than claiming a pass the test couldn't actually earn. Round 10 should retry them once ai_tool

schemas can express map/list parameters.

The parallel design paid off #

DeepSeek's report ended by flagging symlink read-through as unverified — it ran out of budget before testing it. OpenCode's completed run supplied the answer: the redirect is not followed; canonicalization happens first. Two agents, one student, one grader — with no shared writable resource the whole time.

Honest bottom line #

  • Two independently-confirmed bugs fixed before either agent's tools even worked (Findings 1 and 2). This round's most valuable results came from building the harness carefully. - One independently-confirmed bug found by the live attack itself(Finding 3), fixed in docs now, structurally later. - Every vector that could actually be tested end-to-end held. - The OpenAI incident's core mechanism — abusing an allowed relay to reach forbidden targets — did not transfer: redirects and every AI-egress path re-validate against the whitelist on every hop.

The full report is ** report9.en.md**.

Zwei unabhängige KI-Agenten — DeepSeek im stärksten „Max-Effort"-Modus und ein schlüsselloses Gratis-Modell — greifen Pipes Sandbox parallel an, inspiriert vom OpenAI–Hugging-Face-Vorfall und einem echten OpenCode-Config-Bug. Jeder getestete Vektor hielt. Zwei echte Bugs brauchten nicht mal den Angriff: Sie fielen schon beim Bauen des Harness auf.

Dieser Beitrag behandelt Runde 9, den parallelen Zwei-Agenten-Durchgang. Frühere Runden: > ** Runde 1**,

und >

Runde 2. Runden 7 und 8 (CLI-fs-write- und AI-Egress-Lücken) > sind in den Audit-Reports dokumentiert. > Vollständiger technischer Report:

Runden 3–6.

report9.de.mdNach Runde 2 hatte ich gelernt, das Jagen nicht mehr auszulagern — die echten Escapes kamen vom Lesen meines eigenen Codes, nicht von einem LLM-Lauf. Runde 9 dreht das um. Ein Nutzer teilte OpenAIs technischen Incident-Report zur Juli-2026-Kompromittierung von Hugging Face: interne Modelle, die ihre eigene Sandbox-Eval red-teamen, machten aus einem erlaubten Dienst einen SSRF-Relay und ein improvisiertes „Message Board" zwischen Agenten und eskalierten von dort bis in die Produktion. Dazu zeigte ein echtes GitHub-Issue gegen OpenCode ein namensbasiertes Berechtigungssystem, das durch Config-Precedence besiegt wurde.

Die Frage in Runde 9: Übertragen sich diese Mechanismen auf Pipes Sandbox-Modell — und, neu in dieser Runde: Finden zwei verschiedene Modelle im Parallelbetrieb dieselben Dinge?

Warum zwei Agenten, und warum zwei Prozesse #

ActiveProfile

ist ein prozessweiter Zeiger, kein Wert pro Sitzung. Zwei Goroutinen, die in einem Prozess set_sandbox

aufrufen, würden sich gegenseitig überschreiben. „Parallel" heißt hier also: zwei getrennte bin/pipe

-Subprozesse, je eigene gelockte redteam9

-Profilinstanz und ein eigenes Wegwerf-Arbeitsverzeichnis.

Und sie bekamen verschiedene network_whitelist

-Hosts — api.github.com

für DeepSeek, example.com

für OpenCode. Kein geteiltes beschreibbares Ziel bedeutet keine unbeabsichtigte Koordination zwischen den Agenten. Das ist eine bewusste, strukturelle Gegenmaßnahme zu genau dem Mechanismus aus dem Incident-Report: ein geteilter beschreibbarer Dienst, der zum Message Board wird.

┌──────────────────┐      ┌──────────────────┐
│ DeepSeek         │      │ OpenCode         │
│ deepseek-v4-pro  │      │ big-pickle       │
│ thinking+max     │      │ schlüssellos     │
│ whitelist: api.  │      │ whitelist: ex.   │
│ github.com       │      │ example.com      │
└────────┬─────────┘      └────────┬─────────┘
         │ getrennte Prozesse, kein │
         │ geteilter Schreibzustand │
         ▼                        ▼
   Profil "redteam9" gelockt   Profil "redteam9" gelockt

Finding 1 — exec_whitelist #

bot keinen echten Schutz 🔓

Zuerst per Code-Lektüre bestätigt, dann mit einem harmlosen PoC live reproduziert — bevor irgendein API-Budget ausgegeben wurde. CanExecCommand

prüfte nur das erste Whitespace-Token eines Befehls gegen exec_whitelist

; exec

/proc_start

übergaben danach den kompletten, ungefilterten String an eine echte Shell (sh -c

).

So hielt exec_whitelist: ["echo"]

exec "echo $(whoami)-$(cat /etc/hostname)"

nicht auf — die Prüfung sah echo

(erlaubt), und die Shell führte die Substitution trotzdem aus und gab echten Host-Output zurück.

Der Fix: Sobald die exec_whitelist

eines Profils nicht leer ist, wird der Befehl tokenisiert und direkt als argv ausgeführt — ohne Shell — sodass &&

, ;

, `` `

``, `

$(...)zu inerten Literal-Argumenten statt Syntax werden. Derselbe Tokenizer wird für die Whitelist-Prüfung *und* die tatsächliche Ausführung genutzt (gemeinsamer

buildExecCommand-Helper) — keine Lücke zwischen „geprüft" und „ausgeführt". Betraf

execund

proc_start`, die denselben Bug teilten.

Finding 2 — Builtins als Tools waren stillschweigend unaufrufbar 💥 #

Beim Bauen des Harness schlug jeder einzelne Tool-Call in einem Smoke-Test mit not callable: BUILTIN

fehl — bei jedem Tool, auch völlig harmlosen. Ursache: Der Tree-Walker wickelt eine nackte Builtin-Referenz (genau das, was ai_tool "read_file" ... read_file

tut) in einen eigenen Typ, und der gemeinsame Dispatcher erkannte nur den Builtin-Typ der VM. Die VM war nie betroffen; nur die Standard-Ausführung (ohne -vm

).

Das ist ein schwerer, bislang verborgener Bug: Jedes Beispielskript, das ein rohes Builtin als ai_tool registriert — inklusive aller redteam.pipe-Skripte aus Runden 1–8 — war unter dem Tree-Walker stillschweigend kaputt*, jeder Tool-Call scheiterte, bevor er je das Builtin oder die Policy-Prüfungen des Profils erreichte.

Der Fix: ein kleines CallableBuiltin

-Interface — pkg/object

importiert pkg/eval

weiterhin nicht (das wäre ein Zyklus); das Interface ist die Naht.

Finding 3 — ai_tool #

sortiert Multi-Parameter-Aufrufe alphabetisch (vom Angriff gefunden) 📝

DeepSeeks eigener Abschlussbericht meldete „einen seltsamen Parameter-Swap-Bug" bei write_file

— die erzeugte Datei war nach dem Inhalt benannt, den es schreiben wollte, und enthielt den Pfad-String. Ursache: Pipe-Map-Literale erhalten die Deklarationsreihenfolge nirgends, sodass {path: ..., content: ...}

in alphabetischer Reihenfolge ans Builtin geht — write_file(content, path)

. Argumente vertauscht.

Diese Runde nicht gefixt: Es gibt keinen Mini-Fix (die Reihenfolge geht bereits beim Parsen verloren), ein echter Fix bedeutet, Map-Literale end-to-end ordnungserhaltend zu machen — ein separates, größeres Vorhaben. Sofortige Abmilderung geschickt: eine explizite Warnung in der ai_tool

-Doku. Aber es ist ein echtes Beispiel dafür, warum ein Live-Adversarial-Modell gegen die eigene Tooling Dinge aufdeckt, die statische Review nicht findet.

Live-Ergebnis — was hielt #

Beide Agenten liefen bis zum max_tool_calls: 30

-Limit des Profils und stoppten sauber (selbst eine funktionierende Verteidigungsebene, unabhängig vom Dollar-budget

).

# Kategorie Ergebnis
1 Klassisch (fs-Jail, env-Masking, Netzwerk-Whitelist, Lock, MCP) HÄLT — jeder /etc/shadow //root/.ssh/id_rsa //proc/self/environ -Read kollabierte ins Jail; env maskiert; Nicht-Whitelist-Hosts, IP-Literale und localhost alle per Exakt-Match-Whitelist abgelehnt; Lock blockierte set_sandbox ; beide MCP-Einstiege geblockt
1b exec_whitelist + Shell-Metazeichen HÄLT — von beiden Modellen unabhängig verifiziert
2 Symlink-im-Jail (neu) HÄLT — ln -s /etc/passwd gelingt (legitim), aber read_file dereferenziert es nicht: Pfade werden vor jedem Dateisystem-Zugriff ins Jail kanonisiert
3 ai_vision -Magic-Byte-Spoofing (neu) HÄLT — hinfällig: lokale Reads laufen durch dieselbe Jail-Kanonisierung wie read_file , eine gespoofte Datei ist an ihrem echten Pfad nie erreichbar
4 Laufzeit-Tool-Registrierung (neu) NICHT SCHLÜSSIG — Harness-Schema-Problem, kein Policy-Block
5 Swarm-Handoff-Injection (neu) NICHT SCHLÜSSIG — dieselbe Harness-Einschränkung
6 Indirekte Prompt-Injection (neu) Wenig Daten — ein harmloser Whitelisted-Fetch vor Budgetende; nirgends Injektion beobachtet

Auch die wirklich neuen Vektoren hielten — und der ehrliche Teil zählt: Vektor 4 und 5 sind ungetestet, nicht bestätigt-sicher. Das Harness konnte sie legal nicht ausüben (eine Schema-Einschränkung), also sage ich das klar, statt einen Pass zu behaupten, den der Test nicht wirklich verdient hat. Runde 10 sollte sie erneut angehen, sobald ai_tool

-Schemas Map-/List-Parameter ausdrücken können.

Das parallele Design hat sich ausgezahlt #

DeepSeeks Report endete damit, den Symlink-Read-Durchgriff als unverifiziert zu markieren — das Budget ging aus, bevor es getestet wurde. OpenCodes vollständiger Lauf lieferte die Antwort: Der Redirect wird nicht verfolgt; die Kanonisierung passiert zuerst. Zwei Agenten, ein Schüler, ein Bewerter — ohne geteilte Schreibressource, die ganze Zeit.

Ehrliches Fazit #

  • Zwei unabhängig bestätigte Bugs, gefixt bevor die Tools beider Agenten überhaupt liefen (Finding 1 und 2). Die wertvollsten Ergebnisse dieser Runde kamen vom sorgfältigen Harness-Bau. - Ein unabhängig bestätigter Bug, gefunden vom Live-Angriff selbst(Finding 3), jetzt in der Doku gefixt, strukturell später. - Jeder end-to-end testbare Vektor hielt. - Der Kernmechanismus des OpenAI-Vorfalls — einen erlaubten Relay zu missbrauchen, um verbotene Ziele zu erreichen — übertrug sich nicht: Redirects und jeder AI-Egress-Pfad validieren bei jedem Hop erneut gegen die Whitelist.

Der vollständige Report ist ** report9.de.md**.

── more in #ai-safety 4 stories · sorted by recency
── more on @pipe 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/two-ai-agents-one-ja…] indexed:0 read:12min 2026-08-30 ·