e579c3d4fd
First step of PRD 0005. Three new files for the mitmproxy-in-front-of-pipelock topology — wiring into the bottle launch comes in the next commit. - claude_bottle/mitmproxy/__init__.py: abstract MitmproxyProxy base + MitmproxyProxyPlan. Mirrors the PipelockProxy shape (prepare / start / stop) and adds extract_ca_cert for the CA cert hand-off into the agent. - claude_bottle/mitmproxy/addon.py: the vendored Python addon mitmproxy loads inside the sidecar. Forwards each decrypted request to pipelock as a plain HTTP forward-proxy call, inspects the response, and short-circuits the flow with 403 on a pipelock block (status=403 + body starts with `blocked: `, pinned empirically against pipelock 2.3.0 in the impl spike). Self-contained — no claude_bottle imports — so it loads in a sidecar that doesn't have claude_bottle on its path. - claude_bottle/backend/docker/mitmproxy.py: DockerMitmproxyProxy with create / cp / network connect / start lifecycle. Pinned to mitmproxy/mitmproxy@sha256:00b77b5d… (multi-arch manifest for v12.2.3). - tests/unit/test_mitmproxy_verdict.py: pins the verdict fingerprint so a pipelock-side body shape change breaks loudly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
"""Unit: the addon's verdict function pinning pipelock-block vs.
|
|
relayed-upstream 4xx.
|
|
|
|
The fingerprint shape is the contract the addon depends on; this
|
|
test should break loudly if pipelock changes its 403-body prefix
|
|
under a version bump."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from claude_bottle.mitmproxy.addon import is_pipelock_block
|
|
|
|
|
|
class TestIsPipelockBlock(unittest.TestCase):
|
|
def test_block_dlp_body(self):
|
|
# Pipelock v2.3.0 DLP block, captured in the impl spike.
|
|
self.assertTrue(is_pipelock_block(
|
|
403,
|
|
b"blocked: request body contains secret: GitHub Token",
|
|
))
|
|
|
|
def test_block_allowlist_body(self):
|
|
# Pipelock v2.3.0 allowlist block, captured in the impl spike.
|
|
self.assertTrue(is_pipelock_block(
|
|
403,
|
|
b"blocked: domain not in allowlist: example.com",
|
|
))
|
|
|
|
def test_block_header_dlp_body(self):
|
|
# Header DLP path; same body prefix per the spike.
|
|
self.assertTrue(is_pipelock_block(
|
|
403,
|
|
b"blocked: request header Authorization contains secret",
|
|
))
|
|
|
|
def test_403_without_blocked_prefix_is_not_a_block(self):
|
|
# A real-upstream 403 relayed by pipelock — body is whatever
|
|
# the upstream sent, almost certainly not starting with
|
|
# `blocked: `. Must be treated as allow so the addon hands
|
|
# the flow back to mitmproxy.
|
|
self.assertFalse(is_pipelock_block(
|
|
403,
|
|
b'{"error":"forbidden","detail":"insufficient permissions"}',
|
|
))
|
|
|
|
def test_non_403_with_blocked_prefix_is_not_a_block(self):
|
|
# Defensive: if some intermediate ever returns 502/504 with
|
|
# a body that happens to begin `blocked: `, we should still
|
|
# not short-circuit. Block status is always 403 by contract.
|
|
self.assertFalse(is_pipelock_block(502, b"blocked: ..."))
|
|
|
|
def test_200_is_not_a_block(self):
|
|
# Allow path, normal forwarded response.
|
|
self.assertFalse(is_pipelock_block(200, b'{"ok":true}'))
|
|
|
|
def test_empty_body_is_not_a_block(self):
|
|
self.assertFalse(is_pipelock_block(403, b""))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|