Merge pull request 'Say that the egress allowlist bounds the container, not the harness' (#163) from harness-tool-egress into development

Reviewed-on: #163
Reviewed-by: branderson <brad@branderson.io>
This commit was merged in pull request #163.
This commit is contained in:
2026-08-21 14:22:49 -07:00
3 changed files with 84 additions and 0 deletions
+51
View File
@@ -922,6 +922,57 @@ The sandbox-context.sh `SessionStart` hook tells the agent to ask for this
by name when a task is blocked on a single missing domain, rather than
stopping the whole session over it.
### The firewall bounds the container, not the harness
Everything above is about what the *container* can connect to. A harness may
also offer the session a tool whose network request does not come from the
container at all, and the allowlist has no view of those.
Claude Code's `WebSearch` is the instance to reason from. Observed in one
session, in this container:
```
$ curl https://aider.chat/docs/config/options.html
curl: (6) Could not resolve host: aider.chat
```
and in the same session, `WebSearch` returned that page's content. The
retrieval did not use the container's network, because the container has no
route to that host. The only channel into a session that is not the
container's own network is the model API connection - which dejima already
routes through the host-side credential proxy - so the search ran on the API
side and its result arrived in the model's response.
That cuts both ways, and neither is closed here:
- **Inward.** Text from any site on the internet can enter the session without
the DNS allowlist seeing the name. The allowlist is described elsewhere in
this document as bounding where a prompt injection could fetch its next
instruction from; that is true of anything the container fetches, and not
true of this.
- **Outward.** A search query is session-controlled text sent to a third
party. It is low bandwidth next to an HTTP body, but it is a channel out
that no `.dejima/allowed-domains` line was ever reviewed for.
What this is not is unmonitored. The traffic rides the model API connection,
which in the default configuration is exactly the connection dejima terminates
in-container and forwards through its own host-side proxy - so it crosses a
process the operator runs. dejima does not inspect what crosses it, and this
document does not claim it does.
The lever an operator has is the harness's own, not dejima's: Claude Code
takes a `permissions.deny` entry for a tool name, and dejima already writes a
settings overlay it could go in. It is not written there by default, because
the tool is useful and the exposure is a search query rather than a file.
**This is not a Claude Code quirk, it is a harness question.** Any harness may
ship a tool whose fetch happens somewhere other than the container - Cline's
tool set includes `fetch_web_content`, and which side executes it is a thing
its integration has to establish rather than assume. A harness's own network
reach belongs in its worked example in
[development.md](development.md#adding-a-new-harness) alongside the rest of
what it can and cannot do.
## Network request logging
If `[logging] loki_url` is set (see Prerequisites in the README), every DNS query and every new
+11
View File
@@ -129,6 +129,17 @@ What they change is what the row below it costs.
## What this repository does not claim
- **That the egress allowlist bounds what reaches the model.** It bounds what
the container can connect to. A harness tool executed outside the container
- Claude Code's `WebSearch` is the one confirmed by observation - puts text
from any site into the session without the allowlist seeing the name, and
sends a session-controlled query out to a third party. It rides the model
API connection, so it crosses dejima's own credential proxy rather than
going somewhere unseen, but nothing inspects it. Which of a harness's tools
fetch from the container and which do not is a per-harness question, and
belongs in that harness's assessment.
[security.md](security.md#the-firewall-bounds-the-container-not-the-harness)
has the evidence.
- **That escape is impossible.** It is a row in the table with controls of its
own, not an outcome that has been designed away.
- **That the hooks stop a hostile agent.** They stop an unlucky one.
+22
View File
@@ -2641,6 +2641,28 @@ EOF
done
}
# The egress allowlist is the control this repository leans on hardest in
# prose - it is named as what bounds where a prompt injection could fetch its
# next instruction from. That is true of what the container fetches and not of
# a harness tool executed elsewhere, so both documents have to keep saying so.
# A reader who takes the allowlist as the boundary and is not corrected has
# been misled by us, not by the harness.
@test "both documents say the egress allowlist does not bound a harness's own tools" {
local sec="$REPO_ROOT/docs/security.md" tm="$REPO_ROOT/docs/threat-model.md"
grep -q 'The firewall bounds the container, not the harness' "$sec" \
|| { echo "security.md drops the limit of the allowlist"; return 1; }
# The evidence, not just the assertion: a reader has to be able to see why
# this is a fact about the setup rather than a worry about it.
grep -q 'Could not resolve host: aider.chat' "$sec" \
|| { echo "security.md asserts it without the observation behind it"; return 1; }
grep -q 'egress allowlist bounds what reaches the model' "$tm" \
|| { echo "the threat model does not disclaim it"; return 1; }
# And that it is framed per harness rather than as one harness's quirk,
# since the next harness has to be asked the same question.
grep -q 'per-harness question' "$tm" \
|| { echo "the threat model reads as a Claude Code quirk"; return 1; }
}
@test "docs/threat-model.md exists and is linked from the README and docs/security.md" {
[ -f "$REPO_ROOT/docs/threat-model.md" ]
grep -q 'docs/threat-model\.md' "$REPO_ROOT/README.md"