Commit Graph

33 Commits

Author SHA1 Message Date
didericis 66fc29c72e docs(prd-0012): name the three stuck categories and add pipelock path
Introduces cred-proxy block, pipelock block, and capability gap as the
three named categories of stuck. Adds pipelock-edit support (restart-
based for v1) parallel to the existing cred-proxy routes-edit path,
plus a pipelock audit log. Broadens Goals to cover all three paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 04:19:50 -04:00
didericis a6222aaa57 docs(prd-0012): adopt text-only notify protocol + SIGHUP routes reload
Rewrites Scope, Proposed Design, Data model, and Open questions to
match the model where /supervise/notify is text-in/text-out, routes
edits + SIGHUP reload are supervisor-side tooling, and manifest
rebuilds are the heavy path. Adds the per-bottle routes-edit audit log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 04:19:50 -04:00
didericis a74dd2b97f docs: research on git-gate commit approval; link from PRD 0012
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 22s
2026-05-24 23:39:17 -04:00
didericis 83756fa8c9 docs(prd-0012): open question for gitlock/pipelock exception flow
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 22s
2026-05-24 23:12:55 -04:00
didericis b4c9e149b0 docs: add PRD 0012 — stuck-agent recovery flow
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 22s
2026-05-24 23:10:30 -04:00
didericis afa8ca67a4 docs(prd-0011): drop the migration command requirement
test / unit (pull_request) Successful in 13s
test / integration (pull_request) Successful in 23s
claude-bottle has a single primary user today; an automated
JSON → MD migration tool is overkill. Hand-rewriting one file
is the migration cost. The resolver still dies with a pointer
at the README's manifest section if a stale claude-bottle.json
is found alongside no .claude-bottle/ directory, so the breaking
change isn't silent.

Drops: SC #6 (migration tool), the "Migration command" In Scope
sub-bullet, the migrate_manifest.py / cli wiring entries from
Existing code touched, the tests/integration/test_migrate_manifest.py
entry from Tests, the destructive-vs-additive open question.
Renumbers the remaining success criteria 6, 7 (formerly 7, 8).
Backward-compat section rewritten around hand-rewrite.
2026-05-24 21:46:22 -04:00
didericis 894bdea288 docs: add PRD 0011 — per-file Markdown manifest
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 22s
Specs the implementation chosen in the PR #16 closing comment:
per-file MD-with-YAML-frontmatter layout for both bottles and
agents, with a hand-rolled YAML subset parser (no PyYAML).

Layout:
- $HOME/.claude-bottle/bottles/<name>.md   (home-only)
- $HOME/.claude-bottle/agents/<name>.md    (home agents)
- $CWD/.claude-bottle/agents/<name>.md     (repo-supplied agents)

The trust boundary that PRD-0011-v1 (closed PR #15) tried to
enforce in the resolver now falls out of filesystem layout —
$CWD/.claude-bottle/ has no bottles/ subdir, the loader doesn't
look there. Filesystem layout IS the enforcement.

Eight success criteria, including: stdlib-only (no new runtime
dep), idempotent migration command, agent files shaped close to
Claude Code's existing subagent spec so the same file can drop
into ~/.claude/agents/.

PRD-only; no implementation in this commit. PRD slot 0011 is
intentionally reused — the v1 file was never merged to main.
2026-05-24 21:39:58 -04:00
didericis 32b62cbacc feat(cred_proxy)!: cred-proxy is the only Anthropic auth path
test / unit (pull_request) Successful in 13s
test / integration (pull_request) Successful in 23s
Removes the legacy `CLAUDE_BOTTLE_OAUTH_TOKEN` -> `CLAUDE_CODE_OAUTH_TOKEN`
forward in prepare.py. Bottles that need claude-code to authenticate
must declare a cred_proxy route with role: "anthropic-base-url" — there
is no fallback that hands the token to the agent directly.

Drops the now-dead BottleSpec.forward_oauth_token field, the CLI
setter that read CLAUDE_BOTTLE_OAUTH_TOKEN from the host env at
prepare time, and the forward_oauth_token=False arg in the six
pipelock integration tests.

PRD 0010 and README updated; the dev ~/claude-bottle.json gains an
anthropic-base-url route so the implementer/researcher agents keep
working.

BREAKING: bottles previously relying on the implicit OAuth forward
will now produce an agent environ without any Anthropic credential.
Verified with --dry-run: a bottle with no anthropic-base-url route
yields env_names: [] (no token at all); a bottle that declares the
route yields ANTHROPIC_BASE_URL plus a non-secret placeholder for
CLAUDE_CODE_OAUTH_TOKEN.
2026-05-24 12:56:09 -04:00
didericis fcbbc4484d refactor(cred_proxy): flat routes, role-driven provisioning (PRD 0010)
test / unit (pull_request) Successful in 14s
test / integration (pull_request) Successful in 22s
Replace bottle.tokens (with Kind enum and hardcoded per-kind
route/auth tables) with bottle.cred_proxy.routes — each route
declares its own path, upstream, auth_scheme, token_ref, and
optional role[]. The manifest is now the source of truth for the
proxy's runtime route table; adding an upstream is a manifest edit,
not a code change.

Agent-side rewrites move from per-kind dispatch to per-role tags
on routes:
  anthropic-base-url -> set ANTHROPIC_BASE_URL=<proxy><path>
  npm-registry       -> write ~/.npmrc registry=
  git-insteadof      -> write ~/.gitconfig [url] insteadOf, keyed
                        off route.upstream (suppressed when
                        bottle.git brokers the same host)
  tea-login          -> add a ~/.config/tea/config.yml login

Roles are a list (string accepted as sugar). A gitea route
typically carries ["git-insteadof", "tea-login"]. Singleton roles
(anthropic-base-url, npm-registry) appear on at most one route.

token_env slots are assigned per distinct TokenRef in declaration
order — two routes sharing a token_ref (e.g. github API + git
endpoints) share a slot.

Drops: TOKEN_KINDS, _KIND_ROUTES, _KIND_AUTH_SCHEME, _TOKEN_DEFAULT_HOST,
cred_proxy_route_path_for_gitea, the kind field on CredProxyUpstream,
and the kind-based hardcoding in pipelock_token_hosts (now derives
from route.UpstreamHost).

Legacy bottle.tokens manifests now die with a hint pointing at
bottle.cred_proxy.routes + this PRD. Tests rewritten end-to-end.
Docs + example.json + the dev ~/claude-bottle.json updated to match.
2026-05-13 21:49:55 -04:00
didericis 27b2d78b11 fix(cred_proxy): close git-push bypass + route through pipelock (PRD 0010)
test / unit (pull_request) Successful in 15s
test / integration (pull_request) Successful in 29s
Three coupled fixes that close a documented bypass of git-gate's
gitleaks pre-receive hook:

1. cred-proxy refuses git smart-HTTP push at runtime. Any path
   ending in /git-receive-pack or /info/refs?service=git-receive-pack
   returns 403 with a pointer at the bottle.git SSH path. Fetch
   (upload-pack) is still allowed — the bypass we're closing is
   push, where gitleaks is the load-bearing scanner. Hard guarantee.

2. The provisioner suppresses the cred-proxy `~/.gitconfig` insteadOf
   rewrite for any host already declared in bottle.git. git-gate is
   the canonical git path there; we don't write a competing rule
   that would let `git clone https://<host>/...` succeed in ways
   that confuse on push. Defense in depth — (1) is the hard guarantee.

3. cred-proxy routes its outbound HTTPS through pipelock. The
   sidecar's environ now sets HTTPS_PROXY=<pipelock-url>, and the
   image's entrypoint runs `update-ca-certificates` over the
   per-bottle pipelock CA (docker cp'd into
   /usr/local/share/ca-certificates/pipelock.crt before start) so
   the proxy's HTTPS client trusts pipelock's bumped certs.

   Consequence: pipelock's allowlist + body scanner now sit in the
   cred-proxy egress path the same way they sit in front of direct
   agent traffic. The cred-proxy upstream hosts (api.github.com,
   github.com, gitea hosts, registry.npmjs.org) come OFF
   pipelock's passthrough_domains. Only api.anthropic.com remains
   on passthrough (LLM body content legitimately trips DLP).

PRD 0010 updated to reflect all three. Tests adjusted: the
"cred-proxy hosts go on passthrough" assertion in
test_pipelock_allowlist flips to "they don't", a new
TestIsGitPushRequest exercises the smart-HTTP refusal predicate,
and the gitconfig renderer tests cover the per-host suppression
matrix.
2026-05-13 21:09:33 -04:00
didericis c8ab90d01d fix(manifest): allow token + git on the same host (PRD 0010)
test / unit (pull_request) Successful in 13s
test / integration (pull_request) Successful in 22s
git-gate holds an SSH IdentityFile for push/fetch; cred-proxy holds
a PAT for HTTPS REST API calls. The two brokers are orthogonal —
the common dev setup names both on the same host (e.g. gitea.dideric.is
SSH for push, gitea.dideric.is PAT for `tea pr create`).

The original PRD 0010 wording called this a "configuration smell"
and rejected it at parse time. That was wrong; this drops the
overlap rejection from the validator and updates the PRD prose to
match. Tests flip from "rejection" to "coexistence" assertions.
2026-05-13 16:38:36 -04:00
didericis 9fa9717135 docs: switch cred-proxy to sidecar shape
test / unit (pull_request) Successful in 15s
test / integration (pull_request) Successful in 27s
Make the cred-proxy a per-bottle sidecar container on the bottle's
internal docker network instead of a root-owned process inside the
agent container. The boundary becomes container namespace
separation, matching pipelock and git-gate. Update summary,
problem, goals, in-scope, architecture diagram, components,
existing code touched, external deps, and open questions; add a
"Considered alternatives" section recording the rejected
in-container shape.
2026-05-13 15:35:38 -04:00
didericis 3747927b9e docs: align cred-proxy architecture diagram
Trim one trailing space from the four arrow/HTTPS rows and add
one dash to the bottle-container bottom edge so all box-bound
lines are 68 columns.
2026-05-13 15:35:37 -04:00
didericis 1411719973 docs: add PRD 0010 for credential proxy
Per-bottle reverse proxy that holds API tokens (Anthropic OAuth,
GitHub PAT, Gitea PAT, npm) in a root-owned process; agent gets
only URLs in its environ. AWS / SigV4 explicitly out of scope.
2026-05-13 15:35:37 -04:00
didericis 30d92bef48 docs: drop ssh from README/example, supersede PRD 0007 (PRD 0009)
test / unit (pull_request) Successful in 13s
test / integration (pull_request) Successful in 21s
- README architecture diagram drops the socat/ssh image box and
  the agent's ~/.ssh/config; the prose-bullets section drops the
  ssh image; the manifest example swaps `ssh:` for `git:` so
  someone copy-pasting it picks up the new shape.
- claude-bottle.example.json: `default` bottle's `"ssh": []` is
  gone (now just an empty bottle); the gitea-dev example already
  uses `git:` since the ExtraHosts work.
- PRD 0007 carries a "Superseded by PRD 0009" header at the top
  with a one-paragraph block explaining why; the file stays so
  the rationale of the prior design is still in-tree.
- git_gate.py: drop the now-stale shadow-route mention from a
  docstring (the validator went away in the manifest layer).
2026-05-12 23:57:50 -04:00
didericis efcafae810 docs(prds): add PRD 0009 to remove ssh-gate and bottle.ssh
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 34s
ssh-gate was built for non-git SSH (PRD 0007), but every
upstream currently declared in any bottle is a git remote, and
those now flow through git-gate (PRD 0008) with credential
isolation, gitleaks scanning, and `insteadOf` URL rewrites.
ssh-gate is left doing L4 forwarding with no gating value over
git-gate's path; carrying it means a redundant sidecar lifecycle,
a shadow-route validator between bottle.ssh and bottle.git, and
a third place to keep an SSH identity in sync.

Goal is straightforward deletion: bottle.ssh becomes a parse
error pointing at bottle.git, the SshEntry / SSHGate / socat
provisioner / pipelock allowlist branch all go away, and PRD
0007 carries a "Superseded by PRD 0009" header so the rationale
of the prior design stays in the tree.
2026-05-12 23:34:11 -04:00
didericis 9b7bcc0149 docs(git-gate): document ExtraHosts on bottle.git entries
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 19s
- example manifest swaps the gitea-dev bottle from ssh: to git:
  and shows ExtraHosts pinning gitea.dideric.is to its Tailscale IP
- README's git-gate paragraph names the field and the case it
  solves (upstream resolvable on the host but not from the gate
  container's default DNS)
- PRD 0008's manifest-field bullet mentions the field for parity
2026-05-12 23:18:46 -04:00
didericis ae7e22065f docs(prds): expand PRD 0008 to bidirectional mirror scope
test / unit (pull_request) Successful in 11s
test / integration (pull_request) Successful in 18s
The gate now fronts every git operation, not just push. Fetch
(clone, pull, ls-remote) is mirrored via git daemon's
--access-hook running 'git fetch origin --prune' against the
real upstream before each upload-pack; fail-closed if upstream
is unreachable so the agent never serves stale data.

Push path is unchanged in concept (gitleaks gate → forward) but
the hook now pushes to 'origin' rather than 'upstream', matching
the remote name the entrypoint configures.
2026-05-12 21:26:19 -04:00
didericis c91395425c docs(prds): add PRD 0008 git gate
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 13s
Per-bottle sidecar that fronts the agent's git remotes, runs
gitleaks via a pre-receive hook, and only forwards to the real
upstream on a clean scan. Upstream push credentials live in the
gate, not the agent — so a misbehaving agent cannot push a
secret-bearing commit past it.
2026-05-12 18:24:33 -04:00
didericis a3d77cd015 fix(ssh-gate): listen on the upstream port so URL-supplied ports work
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 12s
Bug: git fetch failed with "connect to host
claude-bottle-ssh-gate-implementer port 30009: Connection refused".
OpenSSH treats a URL-supplied port (the user's remote was
ssh://git@gitea.dideric.is:30009/...) as overriding the
~/.ssh/config Port directive, so even though the config wrote
Port 30000 the agent dialed :30009 — where nothing was listening
because the gate had been assigned BASE_LISTEN_PORT + index.

Fix: the gate's listen port now equals the upstream port. Same
script, same socat, just port = entry.Port. Two entries on the
same upstream port are rejected at prepare time (the gate is one
container with a flat port space).

Re-smoked: probe nc github.com via the gate at :22, banner came
back as expected.

PRD 0007 updated to record the design refinement.
2026-05-12 16:19:07 -04:00
didericis b2927b1483 docs(prd): note gate image must be self-sufficient at boot on 0007
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 13s
The gate's agent-facing leg sits on the `--internal` network, so
the forwarder image cannot rely on apk/apt at startup. Surfaced
by the DNS spike — a placeholder using `apk add socat` died
silently and gave a false-negative DNS-on-internal result.
2026-05-12 15:50:34 -04:00
didericis cb0f0f133d docs(prd): resolve gate-DNS open question on 0007
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 14s
Spike: container on a `--internal` user-defined network resolves
another container's name via the embedded resolver at 127.0.0.11
and reaches it over TCP, while egress to the public internet
remains blocked. The PRD's design assumption holds — no design
change needed.
2026-05-12 15:48:55 -04:00
didericis 02a0fe679d docs(prd): 0007 SSH egress gate
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 15s
PRD 0006 enabled pipelock's native TLS interception, which broke
git fetch over SSH from inside the agent: pipelock's SNI gate
rejects the SSH banner that follows CONNECT. Document the
architectural fix — a dedicated per-agent TCP-forwarder sidecar
built from bottle.ssh entries — so pipelock can stay maximally
strict on the HTTPS path with no SSH carve-outs.
2026-05-12 15:41:26 -04:00
didericis f44e884d8a docs(prd): fold 0006 walkthrough resolutions into the design
test / unit (pull_request) Successful in 15s
test / integration (pull_request) Successful in 14s
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>
2026-05-12 14:22:59 -04:00
didericis 6716f091c1 docs(prd): add 0006, enable pipelock's native TLS interception
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 13s
Supersedes the abandoned PR #8 (`mitmproxy-tls-interception`),
which built a mitmproxy + addon chain on the (falsified) premise
that pipelock could not MITM. Empirical proof from the impl-time
spike: with `tls_interception: { enabled: true, ca_cert, ca_key }`
in pipelock's config, pipelock answered a credential POST over
HTTPS with `STATUS=403 / body: blocked: request body contains
secret: GitHub Token` and emitted both `scanner:"tls_intercept"`
and `scanner:"body_dlp"` events. Standalone, no second proxy.

Net change vs PR #8: one sidecar instead of two, no vendored
addon, no addon-verdict pattern matching, no HTTPS-trust /
DNS / lookup workarounds. Same end-state behavior — pipelock's
DLP fires on plaintext for HTTPS hosts in the allowlist.

Also cleaning up the now-stale TLS-research notes:

- `docs/research/tls-mitm-for-pipelock.md` is removed. Its
  entire premise (mitmproxy in front of pipelock) is moot now
  that pipelock does the work natively. The mechanics of CONNECT
  bumping and the CA-lifecycle considerations it documented are
  the same as what pipelock implements; the PRD restates the
  parts that matter for the integration.
- `docs/research/pipelock-assessment.md` had two stale claims
  corrected: the "Pipelock does not perform TLS inspection (no
  CA trust injection)" line in §Scope gaps and the
  "no TLS termination" cell in the comparison table. Both now
  point at the `tls_interception` config and `pipelock tls`
  CLI instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 14:15:44 -04:00
didericis 45203e2cd6 docs(prd): add 0004 split out provisioners
test / unit (pull_request) Successful in 12s
test / integration (pull_request) Successful in 13s
2026-05-11 19:36:39 -04:00
didericis f0b67a3e94 docs(prd): update PRD 0003 to reflect the shipped design
test / run tests/run_tests.py (pull_request) Successful in 14s
Renames the file and rewrites the body around what actually shipped:
class-based BottleBackend ABC (not a free create_docker_bottle
function), the two-phase prepare/launch split, the backend/docker/
subpackage layout, env.py reshaped into a backend-neutral ResolvedEnv,
and PipelockProxy split between top-level and backend/docker/.
2026-05-11 14:47:17 -04:00
didericis 70a22fa210 refactor: rename platform abstraction to backend
test / run tests/run_tests.py (pull_request) Successful in 21s
Across the package:
  - claude_bottle/platform/         -> claude_bottle/backend/
  - platform/docker/platform.py     -> backend/docker/backend.py
  - class BottlePlatform            -> BottleBackend
  - class DockerBottlePlatform      -> DockerBottleBackend
  - get_bottle_platform()           -> get_bottle_backend()
  - env var CLAUDE_BOTTLE_PLATFORM  -> CLAUDE_BOTTLE_BACKEND
  - dict _PLATFORMS                 -> _BACKENDS

"Backend" is shorter and more established as the term for a
pluggable strategy-pattern implementation. "Platform" was vague
(could mean OS, hardware, cloud) and mildly redundant — Docker is
itself a platform.

The previous PRD section claiming "the Backend protocol was
rejected" referred to a low-level run/exec/cp/network_connect
protocol; the name was never the reason. The PRD is updated to
describe that rejected design by shape rather than by name.

The bottle/agent concepts and the manifest schema are unchanged.
2026-05-10 23:59:38 -04:00
didericis d5c056f36e docs(prd): add 0003 bottle factory abstraction
test / run tests/run_tests.py (pull_request) Successful in 17s
2026-05-10 21:56:10 -04:00
didericis cc5e772519 docs: replace stale .sh paths with claude_bottle/*.py equivalents
test / run tests/run_tests.py (push) Successful in 13s
Cleans up references to the pre-refactor bash layout (cli.sh,
lib/*.sh, scripts/*.sh) across README, Dockerfile, the pipelock PRD,
and research notes. Refreshes line numbers in the oauth-token note
against the current cli/start.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 00:27:25 -04:00
didericis 4694db1201 PRD 0002: Test pipeline on Gitea Actions (#3)
test / run tests/run_tests.py (push) Successful in 20s
2026-05-09 02:48:03 -04:00
didericis ba7616a4ae PRD 0001: Per-agent egress proxy via pipelock (#1) 2026-05-08 01:56:43 -04:00
didericis c45f384fb8 Initial commit 2026-05-07 22:45:36 -04:00