Commit Graph

544 Commits

Author SHA1 Message Date
didericis-claude f596464f3f docs(prd): add PRD 0031 — split _merge_provider_route into named case helpers
test / unit (pull_request) Successful in 31s
test / integration (pull_request) Successful in 41s
2026-06-02 05:08:59 +00:00
didericis-claude e528d5c5af docs(prd): update PRD 0030 design to reflect provisioned_env approach
test / unit (pull_request) Successful in 33s
test / integration (pull_request) Successful in 45s
test / unit (push) Successful in 31s
test / integration (push) Successful in 43s
Revises the Design section to describe the implemented solution:
provisioned_env on AgentProvisionPlan rather than an intermediate
egress_resolve_token_values_with_provider function. Drops the old
sentinel/lazy-import design narrative.
2026-06-02 04:54:09 +00:00
didericis-claude 0e29bcc829 refactor(egress): use provisioned_env instead of sentinel for Codex token (PRD 0030)
test / unit (pull_request) Successful in 39s
test / integration (pull_request) Successful in 45s
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.
2026-06-02 04:53:23 +00:00
didericis-claude 8c2b59ca94 complete(prd): mark PRD 0030 active
test / unit (push) Successful in 45s
test / integration (push) Successful in 1m0s
test / unit (pull_request) Successful in 39s
test / integration (pull_request) Successful in 58s
2026-06-02 04:22:52 +00:00
didericis-claude 75f0f9d907 refactor(egress): deduplicate token resolution across backends (PRD 0030)
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.
2026-06-02 04:22:43 +00:00
didericis-claude 6682357fbb docs(prd): add PRD 0030 — deduplicate egress token resolution
Extracts the forward_host_credentials / CODEX_HOST_CREDENTIAL_TOKEN_REF
resolution block, currently copy-pasted in both docker and smolmachines
launch files, into a single shared function in bot_bottle/egress.py.

Closes #118. Found via #117 hotspot review.
2026-06-02 04:17:39 +00:00
didericis 2dd8113f7c fix(smolmachines): retry CA install after exec SIGKILL
test / unit (push) Successful in 38s
test / integration (push) Successful in 54s
2026-06-01 23:28:33 -04:00
didericis-codex 36e3443d2e fix(codex): defer workspace trust handling
test / unit (pull_request) Successful in 29s
test / integration (pull_request) Successful in 42s
test / unit (push) Successful in 30s
test / integration (push) Successful in 44s
2026-06-02 03:11:51 +00:00
didericis-codex d6ebd0d2eb fix(egress): skip token slots for unauth provider routes
test / unit (pull_request) Successful in 30s
test / integration (pull_request) Successful in 43s
2026-06-02 03:06:10 +00:00
didericis-codex eb6bace84f complete(prd): mark PRD 0029 active
test / unit (pull_request) Successful in 35s
test / integration (pull_request) Successful in 42s
2026-06-02 03:00:47 +00:00
didericis-claude f8fc29ce87 refactor(manifest): remove empty EGRESS_ROLES and related plumbing
test / unit (pull_request) Successful in 36s
test / integration (pull_request) Successful in 53s
EGRESS_ROLES, EGRESS_SINGLETON_ROLES, and PROVIDER_EGRESS_ROLES were
all empty frozensets after the codex_auth and claude_code_oauth roles
were removed. Delete the constants and all validation code that iterated
over them (the singleton-role loop and provider-role check in
_validate_egress_routes, the EGRESS_ROLES membership test in
EgressRoute.from_dict). EgressRoute.from_dict now rejects any role
string unconditionally; _validate_egress_routes loses its
agent_provider_template parameter entirely.

Assisted-by: Claude Code
2026-06-01 22:24:17 -04:00
didericis-claude 938a0e05d6 refactor(manifest): remove codex_auth egress role
Both provider-owned roles are now gone. Provider auth routes are
provisioner-owned (claude: auth_token, codex: forward_host_credentials);
the role field and validation plumbing stay for future use but EGRESS_ROLES
is empty. Any manifest declaring a role now fails at parse time.

Assisted-by: Claude Code
2026-06-01 22:24:17 -04:00
didericis f768d3a853 fix(agent): move default claude env vars to the right location 2026-06-01 22:24:17 -04:00
didericis-claude f32b7eb299 fix(agent): always emit passthrough egress route for api.anthropic.com
Mirrors the Codex pattern: Claude always gets a tls_passthrough route
for api.anthropic.com so user-set tokens aren't stripped by pipelock,
whether or not auth_token is declared. Auth injection (scheme + token_ref)
and the placeholder env only apply when auth_token is set.

Assisted-by: Claude Code
2026-06-01 22:24:17 -04:00
didericis-claude de9bd7eb83 feat(manifest): add agent_provider.auth_token for Claude OAuth via egress
Operators can now declare:

  agent_provider:
    template: claude
    auth_token: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN

and the provisioner injects a provider-owned api.anthropic.com egress
route (Bearer, tls_passthrough) rather than requiring a manually
declared route with the former claude_code_oauth role.

Changes:
- Add auth_token field to AgentProvider; validate claude-only.
- Remove claude_code_oauth from EGRESS_ROLES / PROVIDER_EGRESS_ROLES.
  Manifests that declare the role now fail at parse time with "unknown
  role" — the provisioner owns the route.
- agent_provision_plan: replace manifest_egress_routes/has_provider_auth
  with auth_token; Claude branch injects the api.anthropic.com route,
  placeholder env, and nonessential-traffic flags when auth_token is set.
- Add hidden_env_names: frozenset[str] to AgentProvisionPlan; Claude
  branch populates it with CLAUDE_CODE_OAUTH_TOKEN.
- Remove auth_role from AgentProviderRuntime and placeholder_env_for().
- print_util.visible_agent_env_names: accept hidden_env_names from the
  plan instead of dispatching on agent_provider_template.
- Both backends: drop manifest_egress_routes call, pass auth_token.
- PRD 0029 rescoped to cover both Codex and Claude provider auth.

Assisted-by: Claude Code
2026-06-01 22:24:17 -04:00
didericis-claude 952dcd7eec refactor(agent): move placeholder env injection into agent_provision_plan
The has_provider_auth check and egress-placeholder injection were
duplicated in both backends. Move them into agent_provision_plan so
the provisioner owns that decision entirely:

- Replace has_provider_auth: bool param with manifest_egress_routes,
  compute has_provider_auth internally from the route roles.
- Inject CLAUDE_CODE_OAUTH_TOKEN=egress-placeholder inside the plan
  when has_provider_auth, alongside the existing nonessential-traffic
  vars. Backends no longer touch the placeholder env.
- Remove placeholder_env from AgentProviderRuntime; expose
  placeholder_env_for() for print_util's hide-from-summary logic.

Assisted-by: Claude Code
2026-06-01 22:24:17 -04:00
didericis-claude 59df0b0f0f fix(codex): emit passthrough egress routes when not forwarding host credentials
When forward_host_credentials is false, Codex bottles should still get
tls_passthrough routes for the OpenAI/ChatGPT hosts so that tokens a
user sets via `codex login` after launch aren't stripped by pipelock's
header DLP. Previously no routes were emitted, which would have blocked
those requests entirely once pipelock enforcement tightens.

Rename the test to reflect the new expected behavior.

Assisted-by: Claude Code
2026-06-01 22:24:17 -04:00
didericis-claude c0219dddd5 fix(egress): break circular import with manifest via TYPE_CHECKING
manifest → agent_provider → egress → manifest created a cycle that
caused ImportError on any module import. With from __future__ import
annotations already present, Bottle is only needed at type-check time
(annotations are lazy strings under PEP 563).

Assisted-by: Claude Code
2026-06-01 22:24:17 -04:00
didericis-claude 884cedc160 refactor: provision egress routes via AgentProvisionPlan
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
2026-06-01 22:24:17 -04:00
didericis-codex 76a7921ae6 refactor(agent): move claude env defaults into plan 2026-06-01 22:24:17 -04:00
didericis-codex c8ab0c67a8 refactor(agent): surface provider env defaults 2026-06-01 22:24:17 -04:00
didericis-codex e808e81b87 refactor(agent): group provider provisioning into plan 2026-06-01 22:24:17 -04:00
didericis-codex 36ce7aed4f refactor(codex): derive trusted paths from guest home 2026-06-01 22:24:17 -04:00
didericis-codex a5d83bdcdc fix(codex): trust launch home directory 2026-06-01 22:24:17 -04:00
didericis-codex 8e6583fcb7 fix(codex): trust bottle workspace on launch 2026-06-01 22:24:17 -04:00
didericis-codex ac1aa197d4 fix(smolmachines): reset codex runtime db before auth check 2026-06-01 22:24:17 -04:00
didericis-codex 68e5097534 fix(codex): make host-credential bottles actually authenticate
Debugging a live codex smolmachines bottle surfaced three independent
failures past the sign-in screen; fix each so forward_host_credentials
works end to end:

- codex_auth: dummy access/id tokens now inherit the *real* host token's
  exp instead of now+1h. Codex (0.135) refreshes when its local token's
  JWT exp lapses; with a placeholder refresh_token that refresh fails and
  drops to the sign-in screen. Aligning exp tracks the real token's life.

- prepare: set CODEX_CA_CERTIFICATE to the agent CA bundle for codex
  bottles. Codex is rustls and ignores the system store / NODE_EXTRA_CA_
  CERTS; it reads CODEX_CA_CERTIFICATE (fallback SSL_CERT_FILE) for custom
  roots across HTTPS + wss, so it must be pointed at the egress MITM CA or
  injection can't work without tls_passthrough.

- pipelock: auto tls_passthrough the Codex API hosts when
  forward_host_credentials is on. Egress injects the bearer before
  pipelock, whose header DLP then flags the JWT ("request header contains
  secret") and the retry storm trips its 429. passthrough host-gates the
  CONNECT but skips decrypt+rescan of egress-owned auth. The auto-added
  routes aren't in bottle.egress.routes, so the hosts are added explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 22:24:17 -04:00
didericis-codex f8a4e6f40b fix(codex): include account claims in dummy auth 2026-06-01 22:24:17 -04:00
didericis-codex a6332b9535 fix(codex): provision dummy user auth state 2026-06-01 22:24:17 -04:00
didericis-codex 62dd7b2aa5 fix(codex): forward host credentials to api route 2026-06-01 22:24:17 -04:00
didericis-codex 711cb9c194 feat(codex): inject host credentials via egress 2026-06-01 22:24:17 -04:00
didericis-codex 0b80ffb16a docs(prd): add Codex host credentials egress plan 2026-06-01 22:24:17 -04:00
didericis 2350cd11e0 build(images): install python3 in claude/codex bottles
test / unit (push) Successful in 37s
test / integration (push) Successful in 56s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 22:23:38 -04:00
didericis-codex 6ea19a8d53 fix(git-gate): use smart http for smolmachines pushes
test / unit (pull_request) Successful in 40s
test / integration (pull_request) Successful in 54s
test / unit (push) Successful in 37s
test / integration (push) Successful in 44s
2026-05-29 23:21:50 -04:00
didericis-codex 630e65e9a4 test(egress): cover blocked git push fail-fast
test / unit (pull_request) Successful in 28s
test / integration (pull_request) Successful in 42s
2026-05-29 22:13:17 -04:00
didericis-codex 7bffaa791c fix(git-gate): shorten daemon client timeout
test / unit (pull_request) Successful in 30s
test / integration (pull_request) Successful in 42s
2026-05-29 22:02:17 -04:00
didericis-codex de2267d1b4 fix(git-gate): bound daemon client sessions
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 44s
2026-05-29 21:57:31 -04:00
didericis-codex dcaee53cec docs(codex): clarify codex auth marker
test / unit (pull_request) Successful in 28s
test / integration (pull_request) Successful in 40s
test / unit (push) Successful in 28s
test / integration (push) Successful in 42s
2026-05-29 02:45:00 -04:00
didericis-codex cea832b21d fix(codex): stop injecting api key placeholder
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 41s
2026-05-29 02:39:37 -04:00
didericis-codex 50baf63669 docs(prd): mark PRD 0028 active
test / unit (pull_request) Successful in 35s
test / integration (pull_request) Successful in 45s
test / unit (push) Successful in 29s
test / integration (push) Successful in 44s
2026-05-29 02:27:42 -04:00
didericis-claude 6c673bece6 fix(git-gate): scope new-branch scan to incoming commits
test / unit (pull_request) Successful in 28s
test / integration (pull_request) Successful in 40s
A new ref made the pre-receive hook scan the full ancestry
(`log_opts="$new"`), so historical test-fixture findings rejected every
new-branch push (#106). Scope it to `$new --not --all` — only commits
new to the gate, which (since the bare repo is populated solely by
upstream mirror-fetch and gitleaks-gated pushes) loses no coverage on
what a push actually brings to the upstream. Also add BatchMode=yes +
ConnectTimeout=10 to both the forward and access-hook ssh so an
unreachable upstream fails fast instead of hanging.

Refs #106

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:59:20 -04:00
didericis-claude 9dc0dfd5ee docs(prd): PRD 0028 — git-gate new-branch push scan scope
test / unit (pull_request) Successful in 29s
test / integration (pull_request) Successful in 42s
git-gate's pre-receive scans the full ancestry of a new branch, so the
repo's historical test-fixture findings block every new-branch push
(issue #106). Scope the new-ref scan to incoming commits
(`$new --not --all`) with no loss of coverage, and harden the forward
ssh against hangs.

Refs #106

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 01:52:07 -04:00
didericis 2ea73e40a8 docs(decisions): ADR 0003 — system prompts stay user-directed
test / integration (pull_request) Successful in 41s
test / integration (push) Successful in 42s
test / unit (pull_request) Successful in 28s
test / unit (push) Successful in 26s
Record that we considered auto-generating an agent's system prompt from
its bottle's egress/git config (so it would know its access up front)
but opted to keep prompts operator-authored: we may want to withhold
that information from the agent directly, and the agent can infer its
access on its own regardless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 00:40:19 -04:00
didericis-claude 7b2474a5d3 refactor(manifest): drop dead _load_json_or_die helper
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 47s
test / unit (push) Successful in 28s
test / integration (push) Successful in 41s
It had no callers — a leftover from the pre-PRD-0011 bot-bottle.json
loader (the manifest is per-file Markdown now). Removing it also drops
the now-unused `json` import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 00:23:56 -04:00
didericis-claude 847baa84be refactor(manifest): raise ManifestError instead of die()
test / unit (pull_request) Successful in 36s
test / integration (pull_request) Successful in 59s
Review feedback on #102: a manifest that can't be read should raise an
exception, not call die() (a SystemExit). That SystemExit was the whole
reason the dashboard had to special-case Die.

manifest.py now raises ManifestError (a plain Exception) for every
validation failure. The CLI dispatcher catches it and prints+exits 1
(same UX as before); the dashboard catches it with a normal
`except ManifestError` and degrades to a status-line warning. Manifest
tests assert on ManifestError + its message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 00:15:15 -04:00
didericis-claude 99ec267c74 fix(dashboard): surface launch/crash failures (#100)
test / unit (pull_request) Successful in 29s
test / integration (pull_request) Successful in 43s
The dashboard runs under curses.wrapper and cmd_dashboard only caught
KeyboardInterrupt, so failures vanished:
- die() prints to stderr, but under curses that lands on the alternate
  screen and is wiped on exit, so config errors gave no reason.
- Die is a SystemExit, so the new-agent flow's `except Exception` never
  caught config errors; they crashed the TUI.
- the startup manifest probe was unguarded.

Now: Die carries its message (+ log.error()); cmd_dashboard re-surfaces
a Die's reason once the terminal is restored and writes any other
crash's traceback to ~/.bot-bottle/logs/dashboard-crash.log; the startup
probe and the new-agent flow degrade a bad config to a status-line
warning instead of crashing.

Closes #100

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:49:21 -04:00
didericis 848515e5d4 docs: surface docs-folder conventions in AGENTS.md
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 47s
test / unit (push) Successful in 28s
test / integration (push) Successful in 42s
Replace the two thin "docs live in …" lines with the conventions the
docs/ READMEs establish: the three document types (PRD / research note
/ decision record) with their numbering and the PRD Status lifecycle,
plus the cross-cutting rule that decision rationale stays self-contained
in the repo rather than in Gitea issue threads. Points at the per-folder
READMEs as the source of truth instead of duplicating them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:26:37 -04:00
didericis ae1531835d docs: drop "forge" jargon for concrete Gitea wording
test / integration (pull_request) Successful in 53s
test / integration (push) Successful in 57s
test / unit (pull_request) Successful in 33s
test / unit (push) Successful in 36s
We use Gitea, not an abstract forge. Reword the docs added in this
branch: "forge thread" -> "Gitea thread", and the research note's
generic "forge" -> "Gitea" / "hosting provider" as context demands,
keeping its portability argument coherent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:05:02 -04:00
didericis 5c5f576df0 docs(research): add README describing research notes
Document what research notes are (opinionated investigations of a
question/design space), their unnumbered kebab-case naming, and their
loose verdict-first shape — explicitly freeform, not a template. Point
the AGENTS.md research line at it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 23:05:02 -04:00
didericis d329e511fd docs: drop docs/INDEX.md, add PRD README with format
Remove the one-line docs/INDEX.md (its directory pointers are covered
by docs/README.md's "when to write which document" table). Add
docs/prds/README.md documenting the PRD naming, Status lifecycle, and
section format. Repoint the AGENTS.md repository-layout list at the
new READMEs and add the decisions/ dir.

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