Commit Graph

6 Commits

Author SHA1 Message Date
didericis 3755e66abe feat(pipelock): enable tls_interception with per-bottle ephemeral CA
First step of PRD 0006. Pipelock now does the CONNECT bumping that
PR #8's mitmproxy chain was supposed to provide — natively, in the
same single sidecar PRD 0001 wired up.

- claude_bottle/pipelock.py: pipelock_build_config grows optional
  ca_cert_path / ca_key_path kwargs. When both are passed the
  rendered YAML carries a `tls_interception: { enabled: true,
  ca_cert, ca_key }` block. PipelockProxy gains class-level
  CA_CERT_IN_CONTAINER / CA_KEY_IN_CONTAINER constants that
  subclasses set to wherever they place the CA inside the
  sidecar. PipelockProxyPlan gains ca_cert_host_path /
  ca_key_host_path fields (default empty Path() — sentinel for
  "not yet populated", filled by launch via dataclasses.replace).

- claude_bottle/backend/docker/pipelock.py: new
  pipelock_tls_init(stage_dir) helper runs `pipelock tls init`
  in a one-shot container against a host-mounted scratch dir.
  DockerPipelockProxy sets its class constants to
  /etc/pipelock-ca.pem and /etc/pipelock-ca-key.pem; .start
  docker-cp's the cert + key into those paths between
  `docker create` and `docker start`. Pipelock runs as root in
  its distroless image, so no chown is needed (verified).

- claude_bottle/backend/docker/launch.py: calls pipelock_tls_init
  between network creation and proxy.start. Prepare stays
  side-effect-free on docker; the one-shot ca-init container
  only runs on a real launch, not on `start --dry-run`.

- tests/unit/test_pipelock_yaml.py: new assertions that
  pipelock_build_config emits the tls_interception block only
  when both paths are supplied (and rejects a half-set pair),
  plus a test that the docker proxy's prepare plumbs the
  in-container paths through to the rendered YAML.

The end-to-end "bumping actually fires" assertion lands in
chunk 4 (HTTPS integration tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 14:45:36 -04:00
didericis 427ef96e3f feat(pipelock): enforce DLP body-scan hits by default
test / unit (push) Successful in 19s
test / integration (push) Failing after 21s
Adds bottle.egress.dlp_action ("block" | "warn", default block) and
wires it into pipelock as request_body_scanning.action. Pipelock's
own default is "warn", which previously meant claude-bottle detected
credential patterns in outbound bodies but forwarded the request
anyway.

The matching integration test posts a manifest env var shaped like
a GitHub PAT to api.anthropic.com via plain HTTP forward proxy so
pipelock can see the body. Pipelock answers 403 from its body-scan
layer instead of forwarding to the upstream.

Behavior change: bottles without an explicit egress.dlp_action now
block on body-scan hits. Set egress.dlp_action: "warn" to restore
the prior detect-only behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 11:39:25 -04:00
didericis 64a31a382b chore(types): add pyright strict config and fix resulting errors
test / unit (push) Successful in 11s
test / integration (push) Successful in 12s
Adds pyrightconfig.json (strict, Python 3.11) covering cli.py,
claude_bottle/, and tests/. Fixes the 49 strict-mode errors:

- Type DockerBottle.teardown as Callable[[], None].
- ResolvedEnv default_factory uses parameterized list[str] / dict[str, str].
- Erase BottleBackend generics at the registry boundary
  (BottleBackend[Any, Any]) since selection is runtime-driven and
  callers use the unparameterized interface.
- DockerBottleBackend.launch returns Generator[DockerBottle, None, None];
  @contextmanager now flags Iterator returns as deprecated.
- Sidestep cli.list submodule shadowing builtins.list in main()'s argv
  annotation via an aliased re-import in cli/__init__.py.
- Cast cfg[...] results in test_pipelock_yaml at the dict[str, object]
  boundary.
- Annotate write_fixture's fn parameter and _manifest_with_runtime's
  return type.
2026-05-12 10:03:48 -04:00
didericis f943e14891 refactor(pipelock): take stage_dir, derive yaml_path internally
test / unit (pull_request) Successful in 11s
test / integration (pull_request) Failing after 12s
PipelockProxy.prepare now accepts (bottle, slug, stage_dir) and derives
the yaml_path itself, so callers don't need to know the filename.
DockerBottleBackend.prepare_proxy becomes a one-line wrapper whose only
caller already has bottle and slug in scope, so it's inlined and
deleted.
2026-05-11 16:50:22 -04:00
didericis 30b4f12288 refactor(pipelock): expose structured config; assert on dict in tests
Split pipelock config building from YAML rendering: pipelock_build_config
returns a dict, pipelock_render_yaml serializes it, and _build_pipelock_yaml
chains the two onto disk. Unchanged behavior — pipelock loads the same YAML.

The yaml test now asserts on the structured config dict, which is
robust to cosmetic YAML changes (key order, quoting). The two checks
that only make sense on the rendered output — file mode 0600 and
no-secret-leakage — stay against the on-disk content.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 16:23:12 -04:00
didericis 4462863d56 test: reorganize suite into unit/integration/canaries directories
Replace the hand-maintained INTEGRATION_NAMES classifier (and the
bespoke run_tests.py around it) with a directory-driven split:

  tests/unit/         unit tests, always run
  tests/integration/  Docker-dependent, skip cleanly without Docker
  tests/canaries/     upstream-regression checks, opt-in via
                      CLAUDE_BOTTLE_RUN_CANARIES=1

The pinned-pipelock-image check moves to the canary suite — it tests
upstream packaging, not our code, so it shouldn't gate every dev push.
A scheduled canaries.yml workflow runs it weekly.

The manifest-runtime tests collapse the four assertRaises cases for
distinct 'runtime' values into one subTest loop and drop the
error-message-wording assertions; the contract is "any value is
rejected", not "the error literally contains 'auto-detect'".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 16:23:02 -04:00