docs(prd): fold 0006 walkthrough resolutions into the design
After the open-question walkthrough, all four collapsed: - Q1 (mount semantics): resolved to `docker cp` between `docker create` and `docker start`, mirroring the existing pipelock YAML handling. No bind mount, no UID/permission concern. Folded into §Proposed Design > CA lifecycle as "Sidecar install". - Q2 (cert validity / TTL): pre-decided in the question text. Per-bottle ephemerality is enforced by regenerating per launch, not by short validity windows. Pipelock's defaults are fine. Folded into §Proposed Design as a one-line "Per-bottle ephemerality" note. - Q3 (`passthrough_domains` shape): not v1 scope; the shape is pre-recorded so the follow-up is mechanical. Moved into §Out of scope. - Q4 (stage-dir cleanup ordering): reading start.py confirmed the ExitStack-then-outer-finally order is correct. Folded into §Proposed Design as a "Teardown" note. The §Open questions section is dropped. None of the four was a real design question — they were verifications and pre-decided items left in for defensiveness. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -136,9 +136,13 @@ The feature is **done** when all of the following ship:
|
||||
leaving `ca.pem` and `ca-key.pem` under `stage_dir`. The host
|
||||
file owner is whatever the upstream image's user is; the
|
||||
sidecar mount is read-only so this is fine.
|
||||
- `DockerPipelockProxy.start` mounts the stage dir into the
|
||||
sidecar at `/h:ro` and references the CA paths in the rendered
|
||||
YAML.
|
||||
- `DockerPipelockProxy.start` `docker cp`s the CA cert + key
|
||||
into the sidecar at `/etc/pipelock/ca.pem` and
|
||||
`/etc/pipelock/ca-key.pem` between `docker create` and
|
||||
`docker start`, mirroring the existing pattern for the YAML
|
||||
config. If pipelock's image runs as non-root, a `docker exec
|
||||
-u 0 chown pipelock:pipelock /etc/pipelock/ca*.pem` lands
|
||||
between the `cp` and the `start`.
|
||||
- **`claude_bottle/backend/__init__.py`**: new abstract method
|
||||
`provision_ca(plan, target)` on `BottleBackend`, default no-op.
|
||||
`BottleBackend.provision` orchestrates `ca → prompt → skills →
|
||||
@@ -183,7 +187,10 @@ The feature is **done** when all of the following ship:
|
||||
- A manifest field to disable / customize interception per bottle.
|
||||
Doable but premature.
|
||||
- Wiring `passthrough_domains`. The default `[]` is correct for
|
||||
v1; add the manifest field when a pinning host shows up.
|
||||
v1; add the manifest field when a pinning host shows up. The
|
||||
shape is pre-recorded so the follow-up is mechanical:
|
||||
`bottle.egress.tls_passthrough_domains: [host, ...]`,
|
||||
mirroring the existing `egress.allowlist`.
|
||||
- `cross_request_detection`, `entropy_budget`,
|
||||
`fragment_reassembly`, `reverse_proxy`, `scan_api` — features
|
||||
pipelock exposes but we don't need for the body-DLP gap.
|
||||
@@ -204,21 +211,29 @@ generated at prepare time.
|
||||
### CA lifecycle
|
||||
|
||||
- **Generation.** Host-side, at prepare time, via a one-shot
|
||||
`docker run --rm -v <stage>:/h pipelock tls init`. Output is
|
||||
`<stage>/ca.pem` + `<stage>/ca-key.pem`, both mode 600.
|
||||
- **Sidecar mount.** `DockerPipelockProxy.start` adds
|
||||
`-v <stage>:/h:ro` to the sidecar's `docker run`. The rendered
|
||||
YAML references `/h/ca.pem` and `/h/ca-key.pem`. The private
|
||||
key is read-only from pipelock's perspective; the host stage
|
||||
dir is owned by the launching user.
|
||||
`docker run --rm -v <stage>:/h -e PIPELOCK_HOME=/h pipelock tls
|
||||
init`. Output: `<stage>/ca.pem` + `<stage>/ca-key.pem`, mode 600.
|
||||
- **Sidecar install.** `DockerPipelockProxy.start` `docker cp`s
|
||||
the CA cert + key into the sidecar at `/etc/pipelock/ca.pem`
|
||||
and `/etc/pipelock/ca-key.pem` between `docker create` and
|
||||
`docker start`. Same pattern the proxy already uses for the
|
||||
YAML config — no bind-mount, no UID/permission concern from
|
||||
the one-shot generation step. The rendered YAML references
|
||||
the in-container paths.
|
||||
- **Bottle install.** `provision_ca` (Docker impl) does
|
||||
`docker cp <stage>/ca.pem agent:/usr/local/share/ca-certificates/claude-bottle-mitm.crt`,
|
||||
then `update-ca-certificates`. The CA env trio is set at
|
||||
`docker run -e` time (Docker propagates run-time env into
|
||||
`docker exec`, verified in PR #8's spike).
|
||||
- **Teardown.** The sidecar container is destroyed, the stage
|
||||
dir is removed by `start.py`'s existing `finally` block, and
|
||||
the CA dies with both.
|
||||
`docker exec`).
|
||||
- **Per-bottle ephemerality.** Enforced by *regenerating per
|
||||
launch*, not by validity windows. Pipelock's defaults
|
||||
(`cert_ttl: 24h` for leaves, `--validity 87600h` for the CA)
|
||||
are fine — the CA lives only as long as the sidecar, which is
|
||||
the bottle's lifetime.
|
||||
- **Teardown.** Sidecar removed via `ExitStack` callback, then
|
||||
the launch context manager's outer `finally` `shutil.rmtree`s
|
||||
`stage_dir`. CA dies with both, in that order, so the sidecar
|
||||
is never reading a deleted mount on shutdown.
|
||||
- **Fingerprint.** Computed via stdlib in `provision_ca` and
|
||||
logged once to stderr (`claude-bottle: mitm ca fingerprint:
|
||||
sha256:<hex>…`). The private key never appears in any log.
|
||||
@@ -259,33 +274,6 @@ backend module.
|
||||
image's own `tls init` command in a one-shot container.
|
||||
Fingerprint uses Python stdlib `ssl` + `hashlib`.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Mount semantics for the stage dir.** The sidecar runs with a
|
||||
`-v <host-stage>:/h:ro` bind mount. The CA files were written by
|
||||
the one-shot `pipelock tls init` container with whatever UID
|
||||
pipelock's image uses; the sidecar reads them as that same UID.
|
||||
Should work, but confirm on first impl by inspecting the file
|
||||
modes/owners and that the sidecar actually loads them. Fallback:
|
||||
`docker cp` the cert/key into the running sidecar after `docker
|
||||
create` (mirror PR #8's mitmproxy lifecycle).
|
||||
- **Cert validity / TTL.** Defaults are `cert_ttl: 24h` for
|
||||
per-host leaves; the CA validity from `pipelock tls init` is
|
||||
10 years by default (`--validity 87600h`). The CA outlives the
|
||||
bottle either way; per-bottle ephemerality is enforced by
|
||||
*generating a fresh one each launch*, not by setting a short
|
||||
CA validity. Document; no tuning in v1.
|
||||
- **`passthrough_domains` shape.** Once we expose this through
|
||||
the manifest in a follow-up, the natural place is
|
||||
`bottle.egress.tls_passthrough_domains: [host, ...]`, mirroring
|
||||
the existing `egress.allowlist` shape.
|
||||
- **Stage-dir cleanup ordering.** The stage dir holds the CA
|
||||
private key briefly. `start.py`'s existing `finally` block
|
||||
`shutil.rmtree`s it. Confirm the rmtree fires after the sidecar
|
||||
is stopped, so the sidecar isn't reading a deleted mount when
|
||||
it shuts down. The current order is correct (teardown unwinds
|
||||
via ExitStack before the outer `finally` runs); verify.
|
||||
|
||||
## References
|
||||
|
||||
- `docs/research/pipelock-assessment.md` (now corrected) —
|
||||
|
||||
Reference in New Issue
Block a user