Level 0 (off, default): no stderr output beyond boot line.
Level 1 (blocks): each block/warn emitted as JSON with reason and
request context (host, method, path, response_status for inbound).
Level 2 (full): level-1 events + egress_request and egress_response
JSON lines for every forwarded connection.
Block logging at level 1+ replaces the previous plain-text stderr write.
DLP warn logging is also gated on level 1+. All block call sites now pass
_req_ctx(flow) so the blocked request is visible in the log entry.
Boot message shows log level label (off/blocks/full).
Adds PRD 0053 documenting wire format, manifest format, and all log event
shapes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a top-level `log: true` option to the egress config that logs the
full request (method, path, headers, body) and response (status, headers,
body) for every forwarded connection as JSON lines on stderr.
Wire format: `log: true` at the root of routes.yaml, parsed into the new
`Config` dataclass alongside `routes`. The sidecar addon switches from
`self.routes` to `self.config` and writes `_log_request` / `_log_response`
JSON lines when `self.config.log` is set.
Manifest: `egress.log: true` in bottle YAML flows through `EgressConfig.Log`
→ `Egress.prepare()` → `egress_render_routes(..., log=)` → routes.yaml.
`EgressPlan` also carries the flag for introspection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_supervise.py: drop TOOL_PIPELOCK_BLOCK import; update TOOLS
assertion to match the 3-item tuple (egress, capability, list-egress)
- test_supervise_server.py: remove pipelock from tools-list assertion,
fix test_rejected_response_sets_isError to use capability-block
- contrib/claude and contrib/codex: remove tls_passthrough=True from
EgressRoute constructors (field removed with pipelock)
- test_egress.py: drop tls_passthrough parameter from _provider_route,
remove tls_passthrough-only tests, fix EgressRoute constructions
- test_agent_provider.py: drop route.tls_passthrough assertions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Strip pipelock from all unit and integration test fixtures:
proxy_plan fields removed from DockerBottlePlan/SmolmachinesBottlePlan
constructors; pipelock-specific test classes deleted or renamed
- Update test_sidecar_init: remove test_pipelock_loses_egress_tokens,
rename "pipelock" daemon fixtures to "git-gate" throughout
- Remove test_pipelock_binary_present_and_versioned from integration test
- Remove test_pipelock_answers_on_bundle_ip from smolmachines launch test
- Update _SANDBOX_BLOCK_MARKERS: remove "pipelock" marker (egress blocks)
- Dockerfile.sidecars: remove pipelock build stage and COPY; update layout
comments and port table
- egress_entrypoint.sh: update comments now that egress is sole proxy
- Clean up pipelock references in comments/docstrings across backend,
network, manifest, supervise, git_gate, yaml_subset, agent_provider,
sidecar_bundle, sidecar_init, egress_addon_core modules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace _merge_provider_route's five-case nested conditional with a flat
provisioned-wins merge: provider routes claim their hosts outright, manifest
routes for unclaimed hosts append unchanged. Token slot assignment moves to a
single _assign_token_slots pass over the merged list.
Add _route_to_yaml_fields as the single authoritative EgressRoute→YAML mapping,
eliminating the risk of EgressRoute and egress_addon_core.Route silently
drifting apart when new fields are added.
egress_manifest_routes is now a pure lifter with no slot assignment.
_merge_provider_route and _find_or_alloc_token_env are removed.
Tests updated: conflict-die case removed, upgrade-bare replaced with
provider-wins semantics, slot-assignment tests moved to TestSlotAssignment.
Add `provisioned_env: dict[str, str]` to `AgentProvisionPlan`. When
`forward_host_credentials=True`, `agent_provision_plan` reads the host
Codex access token at prepare time and stores it under
`CODEX_HOST_CREDENTIAL_TOKEN_REF`. Both backends merge `provisioned_env`
over `os.environ` before calling `egress_resolve_token_values`, so the
token slot resolves like any other manifest-declared token ref.
Removes `egress_resolve_token_values_with_provider` and the sentinel
`continue` skip from `egress_resolve_token_values`. The function is now
fully generic — it neither knows nor cares about provider identity.
Extract egress_resolve_token_values_with_provider into bot_bottle/egress.py.
Both docker and smolmachines launch paths now call the shared function
instead of duplicating the forward_host_credentials / CODEX_HOST_CREDENTIAL_TOKEN_REF
resolution block.
Also fixes the host_env: object annotation on smolmachines._resolve_token_env
to the correct dict[str, str].
Closes#118.
Remove provider-specific branching from egress.py and pipelock.py.
Previously, `egress_routes_for_bottle` and `pipelock_effective_tls_passthrough`
both contained `template == "codex"` checks — the same pattern the rest
of the PR moved out of the backends.
Root cause: `EgressRoute` had no `tls_passthrough` field, so pipelock
couldn't learn from the synthesised Codex routes that they needed
passthrough. Fix:
- Add `EgressRoute.tls_passthrough: bool`. `egress_manifest_routes` lifts
the existing `pipelock.tls_passthrough` manifest flag here; provider
routes set it directly.
- Add `AgentProvisionPlan.egress_routes`. `agent_provision_plan` populates
it for Codex + `forward_host_credentials`, including `tls_passthrough=True`.
- Replace Codex-specific `egress_routes_for_bottle` logic with a generic
`_merge_provider_route` helper. Backends call `egress_routes_for_bottle(bottle,
plan.egress_routes)`; no provider type checks inside egress or pipelock.
- Rewrite `pipelock_effective_tls_passthrough` to read `route.tls_passthrough`
from the merged route set instead of re-implementing the provider check.
- Both backends now call `agent_provision_plan` before `Egress.prepare` and
`PipelockProxy.prepare`, threading `plan.egress_routes` to both. `has_provider_auth`
is derived from `egress_manifest_routes` (manifest routes only — provider
routes carry no auth roles, so the result is identical).
Assisted-by: Claude Code
`egress_render_routes` now emits hand-rolled YAML in the same style
as `pipelock_render_yaml`. The egress addon parses it via
`yaml_subset.parse_yaml_subset` — the same parser the manifest
loader + pipelock_apply use.
Why bother: routes.yaml is bind-mounted into the egress sidecar
AND surfaced to operators through `routes edit` (PRD 0019). JSON-
in-yml renders ugly in $EDITOR and signals "this is data" rather
than "this is config you can read at a glance". Real YAML reads
cleanly.
Mechanics:
- `yaml_subset.py` drops its `claude_bottle.log` dependency.
Errors now raise `YamlSubsetError` (a `ValueError`); the
manifest loader + pipelock_apply catch it at the boundary
and forward to `die` / `PipelockApplyError` so callers see
the same behavior they did before.
- `Dockerfile.egress` adds one COPY line for `yaml_subset.py`
so it sits flat in `/app/` next to the addon. The addon
uses an absolute-import-with-fallback shim so the same file
works inside the container AND from the host's unit tests.
- `egress_apply._merge_single_route` round-trips current
routes.yaml through `parse_yaml_subset` + a new
`_render_routes_payload` helper instead of `json.loads` +
`json.dumps`.
End-to-end: rebuilt the egress image, ran `./cli.py start` to a
full bring-up, confirmed the addon's boot log shows `egress:
loaded 9 route(s)` — i.e., the YAML parses inside the container.
453 unit + 3 integration tests pass.
The manifest key is `egress:` now; finish the rename so the rest of
the codebase matches. Files (Dockerfile.egress, claude_bottle/egress.py
etc.), classes (Egress, EgressConfig, EgressRoute, EgressPlan,
DockerEgress), constants (EGRESS_HOSTNAME, EGRESS_ROUTES, ...),
container name prefix (claude-bottle-egress-*), docker network alias
(egress), the introspection host (_egress.local), the MCP tool IDs
(egress-block, list-egress-routes), and the preflight label all drop
the `-proxy` suffix.