Compare commits

..

7 Commits

Author SHA1 Message Date
didericis-claude 0d4d930b29 docs: bump PRD number from 0052 to 0053
lint / lint (push) Successful in 1m30s
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 43s
Renames docs/prds/0052-user-provider-plugins.md to 0053-user-provider-plugins.md
and updates the heading inside the file. 0052 is now reserved for the egress
DLP addon.
2026-06-06 16:06:09 -04:00
didericis-claude 3866461bf4 fix: correct broken imports and fileno() guard after rebase
codex_auth.py was moved into contrib/codex/ but still used `.log`/
`.util` relative imports that resolved to the parent bot_bottle
package before the move — update to `...log` / `...util`.

_read_winsize() called sys.stdin.fileno() outside the OSError guard;
pytest's redirected stdin raises UnsupportedOperation (an OSError
subclass) there, breaking test_returns_first_tty_size. Move fileno()
inside the try block so any non-TTY stream is skipped cleanly.
2026-06-06 16:06:09 -04:00
didericis-claude 664c67a272 docs: PRD 0052 — user-defined agent provider plugins
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 16:06:09 -04:00
didericis-claude 26a1a51cbb refactor: move codex_auth into contrib/codex
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 16:06:09 -04:00
didericis-claude e82bbb587f refactor(egress): centralize block logging in _block helper
test / unit (pull_request) Successful in 30s
test / integration (pull_request) Successful in 49s
lint / lint (push) Successful in 1m26s
test / unit (push) Successful in 31s
test / integration (push) Successful in 49s
Update Quality Badges / update-badges (push) Successful in 1m13s
2026-06-06 17:00:42 +00:00
didericis-claude c89a0d334a feat(egress): log block reason to stderr on blocked requests
lint / lint (push) Successful in 1m24s
test / unit (pull_request) Successful in 31s
test / integration (pull_request) Successful in 41s
2026-06-06 16:56:26 +00:00
didericis ac9b6d593f fix(tests): fix integration test failures from deprecated git key, missing wget, and wrong prompt path
test / integration (pull_request) Successful in 41s
test / unit (pull_request) Successful in 31s
test / unit (push) Successful in 30s
Update Quality Badges / update-badges (push) Successful in 1m3s
lint / lint (push) Successful in 1m23s
test / integration (push) Successful in 42s
- test_sandbox_escape: migrate manifest fixture from deprecated `git`
  key to `git-gate` (PRD 0047) — `remotes` → `repos`, field names
  `Name`/`Upstream`/`IdentityFile` → `url`/`identity`
- test_smolmachines_launch probes: replace `wget` (not in node:22-slim)
  with `curl -s --show-error --max-time 3` (installed in Dockerfile.claude)
- test_smolmachines_launch prompt test: correct path /root/ → /home/node/
  to match guest_home in smolmachines/prepare.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:29:36 -04:00
3 changed files with 26 additions and 34 deletions
+16 -23
View File
@@ -82,6 +82,14 @@ class EgressAddon:
{"Content-Type": "text/plain; charset=utf-8"}, {"Content-Type": "text/plain; charset=utf-8"},
) )
def _block(self, flow: http.HTTPFlow, reason: str) -> None:
sys.stderr.write(f"{reason}\n")
flow.response = http.Response.make(
403,
reason.encode("utf-8"),
{"Content-Type": "text/plain; charset=utf-8"},
)
def request(self, flow: http.HTTPFlow) -> None: def request(self, flow: http.HTTPFlow) -> None:
request_path, _, query = flow.request.path.partition("?") request_path, _, query = flow.request.path.partition("?")
@@ -100,25 +108,18 @@ class EgressAddon:
scan_text = auth_header + "\n" + body scan_text = auth_header + "\n" + body
dlp_result = scan_outbound(route, scan_text, os.environ) dlp_result = scan_outbound(route, scan_text, os.environ)
if dlp_result is not None and dlp_result.severity == "block": if dlp_result is not None and dlp_result.severity == "block":
flow.response = http.Response.make( self._block(flow, f"egress DLP: {dlp_result.reason}")
403,
f"egress DLP: {dlp_result.reason}".encode("utf-8"),
{"Content-Type": "text/plain; charset=utf-8"},
)
return return
# Strip inbound Authorization — agent cannot smuggle tokens. # Strip inbound Authorization — agent cannot smuggle tokens.
flow.request.headers.pop("authorization", None) flow.request.headers.pop("authorization", None)
if is_git_push_request(request_path, query): if is_git_push_request(request_path, query):
flow.response = http.Response.make( self._block(
403, flow,
( "egress: git push over HTTPS is not supported; "
b"egress: git push over HTTPS is not supported; " "use the bottle.git SSH path (gitleaks-scanned by "
b"use the bottle.git SSH path (gitleaks-scanned by " "git-gate's pre-receive hook).",
b"git-gate's pre-receive hook)."
),
{"Content-Type": "text/plain; charset=utf-8"},
) )
return return
@@ -135,11 +136,7 @@ class EgressAddon:
) )
if decision.action == "block": if decision.action == "block":
flow.response = http.Response.make( self._block(flow, decision.reason)
403,
decision.reason.encode("utf-8"),
{"Content-Type": "text/plain; charset=utf-8"},
)
return return
if decision.inject_authorization is not None: if decision.inject_authorization is not None:
@@ -159,11 +156,7 @@ class EgressAddon:
if result is None: if result is None:
return return
if result.severity == "block": if result.severity == "block":
flow.response = http.Response.make( self._block(flow, f"egress DLP: {result.reason}")
403,
f"egress DLP: {result.reason}".encode("utf-8"),
{"Content-Type": "text/plain; charset=utf-8"},
)
elif result.severity == "warn": elif result.severity == "warn":
sys.stderr.write(f"egress DLP warn: {result.reason}\n") sys.stderr.write(f"egress DLP warn: {result.reason}\n")
+4 -5
View File
@@ -120,11 +120,10 @@ class TestSandboxEscape(unittest.TestCase):
# is intentionally unreachable — the pre-receive # is intentionally unreachable — the pre-receive
# gitleaks hook must reject BEFORE git-gate # gitleaks hook must reject BEFORE git-gate
# attempts the upstream push. # attempts the upstream push.
"git": {"remotes": { "git-gate": {"repos": {
"unreachable.invalid": { "throwaway": {
"Name": "throwaway", "url": "ssh://git@unreachable.invalid:22/throwaway.git",
"Upstream": "ssh://git@unreachable.invalid:22/throwaway.git", "identity": str(cls._key_path),
"IdentityFile": str(cls._key_path),
}, },
}}, }},
}, },
@@ -110,10 +110,10 @@ class TestSmolmachinesLaunch(unittest.TestCase):
# (high-numbered) so we're confirming TSI refusal, not # (high-numbered) so we're confirming TSI refusal, not
# just "no service listening." # just "no service listening."
r = self.bottle.exec( r = self.bottle.exec(
"wget -T 3 -t 1 -O - http://127.0.0.1:9 2>&1 || true" "curl -s --show-error --max-time 3 http://127.0.0.1:9 2>&1 || true"
) )
# `wget` to a denied destination produces a connect error. # `curl` to a denied destination produces a connect error.
# The exact phrasing varies (busybox vs gnu); we assert # The exact phrasing varies by curl version; we assert
# the response is NOT the body of any real service. # the response is NOT the body of any real service.
self.assertNotIn("hello-from-vm", r.stdout) self.assertNotIn("hello-from-vm", r.stdout)
self.assertTrue( self.assertTrue(
@@ -126,10 +126,10 @@ class TestSmolmachinesLaunch(unittest.TestCase):
def test_prompt_file_lands_in_guest(self): def test_prompt_file_lands_in_guest(self):
# provision_prompt copies the host-side prompt.txt into the # provision_prompt copies the host-side prompt.txt into the
# guest at /root/.bot-bottle-prompt.txt. The content # guest at /home/node/.bot-bottle-prompt.txt. The content
# must match what the manifest declared so claude-code's # must match what the manifest declared so claude-code's
# --append-system-prompt-file reads the right text. # --append-system-prompt-file reads the right text.
r = self.bottle.exec("cat /root/.bot-bottle-prompt.txt") r = self.bottle.exec("cat /home/node/.bot-bottle-prompt.txt")
self.assertEqual(0, r.returncode, msg=r.stderr) self.assertEqual(0, r.returncode, msg=r.stderr)
self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n")) self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n"))
@@ -143,7 +143,7 @@ class TestSmolmachinesLaunch(unittest.TestCase):
# connect fails, which is the property chunk 3 will # connect fails, which is the property chunk 3 will
# preserve once egress is actually running. # preserve once egress is actually running.
r = self.bottle.exec( r = self.bottle.exec(
f"wget -T 3 -t 1 -O - http://{self.plan.bundle_ip}:9099 " f"curl -s --show-error --max-time 3 http://{self.plan.bundle_ip}:9099 "
"2>&1 || true" "2>&1 || true"
) )
self.assertTrue( self.assertTrue(