fix: an edited vault secret reaches a live brokered conversation (#1736) - #1737
Conversation
A brokered conversation kept injecting a vault secret's old value after the vault was edited (#1736). Two causes, both in the live process: - The environment + vault secrets were split into `state.brokered` once, at init; the refresh before a turn re-read only connection tokens. - A change minted a new session, and `Sessions.create` inserts a new row with a new token. The token is in the env of every process the sandbox already runs, the idle ACP peer that carries the next turn included, so the re-mint reached only the next spawn. That resume path never ran a broker refresh at all. `Broker.refresh/4` (`Sessions.update_rules/4`) rewrites the rules of the conversation's live sessions in place, tokens kept. `Egress.refresh_before_turn/1` re-reads the environment and vault before every turn, rewrites the rules when anything moved, and mints a fresh session only when the current one is expiring or the rewrite found nothing to rewrite. It runs from `run_turn/6`, so both the fresh-spawn and the idle-peer paths get it. Reattach was already fine: it runs from init, which reads the vault. The refresh family leaves the server for `Egress` (the server only shrinks); the pin drops to 2775. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GP4S6417fiRQARugfSUM7u
Review Loop · Review in progressReviewing the entire current PR. Approval is pending. Revision: |
|
Jake asked me to collect feedback from agents actively using Review Loop while I burn down its remaining work. As you work on this PR, please reply here occasionally with concise feedback: useful or incorrect findings, duplicate reports, CI broken by fixes, retry/stopping friction, and unclear or overly long messages. Include the run/finding ID when practical. No need to pause your work or request another review just to gather feedback. I see this run stopped on a failed reviewer and repeated the expired-token finding. I’m investigating those service failures separately; your assessment of the findings and workflow will help prioritize the fixes. |
… hands back a connection token Two findings from the Review Loop on #1737: - When the refresh has to mint a replacement session (expiring, or no live row to rewrite), the idle ACP peer kept the old token in its env and the next turn resumed it, so brokered requests 407'd despite a successful re-mint. `Egress.refresh_before_turn/1` now says whether the token was replaced, and `run_turn/6` drops the connection on `true` so the turn is a fresh spawn carrying the new env. - A vault override of a connection's key, added and then deleted mid-conversation, dropped the connection token for one turn: the removal underlay held only inference credentials. It now holds the connection tokens too. The server-level regression needed the tenant's real key: the factory encrypts a connection's token under it, and `stub_happy_sprite/1`'s zero key made the connection silently vanish at provision. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GP4S6417fiRQARugfSUM7u
|
Review Loop run
Workflow notes: the run stopped on a failed reviewer ("Required independent turn did not complete successfully"), so it is unclear whether other reviewers would have added anything. The evidence sections were precise enough to act on without opening the linked run (file, function, line, the exact test that under-asserted). The duplicate cost a re-read but not a wrong fix. One reviewer noted it could not execute the broker tests for lack of Hex deps; it still reasoned correctly from the static read. |
|
Jake asked me to check in with active contributors as we harden Review Loop. Your missing-dependency feedback led to trusted reviewer setup (#1741 supplies the timeout). A live setup now reaches If your broker work already covers HTTPS-proxy clients using Erlang httpc, please point me to it. Does this match failures you’ve seen in other conversations? |
|
Jake asked me to keep collecting contributor feedback as Review Loop rolls out. Reviewer dependency setup is now merged in #1742, and Action recovery is published (Fountain pin upgrade: #1743). When this PR next runs through review, please flag remaining missing dependencies, repeated findings, or long silent turns. We are investigating a tool-progress stall separately; the setup fix is not a claim that runtime reliability is solved. |
The changelog carried both Fixed entries (#1736 and #1732). main grew conversation_server.ex by one line, so a comment in run_turn/6 loses one to keep the file on its pin. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GP4S6417fiRQARugfSUM7u
Closes #1736.
The report
A client app writes a fresh one-hour GitHub App installation token into a project vault under
GITHUB_TOKENbefore every prompt. Git works at conversation start; an hour in,gh issue liston the same conversation returns401 Bad credentialsand stays broken across further prompts, although the vault holds a valid token.Two causes, both in the live process
ConversationServersplit the environment + vault secrets intostate.brokeredonce, at init. The refresh before a turn (broker_refresh/1) re-read only the tenant's connection tokens. An edited vault or environment secret was never read again while the process was alive.Egress.reprepare/5minted a new session:Sessions.create/1inserts a new row with a new token, and the old row stays valid until its TTL with the old rules. The token is inHTTPS_PROXYin the env of every process the sandbox already runs, so a re-mint reaches only the next spawned process. Claude's idle ACP peer carries the next turn without a spawn (resume_acp_connection/5), and that path never ran a broker refresh at all.Reattach was already correct:
do_reattachruns only fromdispatch_provision, right after init reads the vault, so a wake after a park or a deploy picks up an edit.The fix
Fountain.Broker.refresh/4→Native.refresh/4→Sessions.update_rules/4rewritesrules_ciphertext(and thecredential_keysmeta) on every live session of the conversation, tokens kept. The nextlookup/1on any of the conversation's tokens decrypts the new rules.Egress.refresh_before_turn/1re-reads the environment and vault before every turn, the way the connection refresh does (connections first, then the tenant's own secrets, the order init merged them). A changed value replaces the broker's; a deleted key leaves, and an inference credential it was masking takes the name back. When anything moved the live rules are rewritten in place; a fresh session is minted only when the current one is expiring, or when the rewrite found no live session.run_turn/6, so the fresh-spawn path and the idle-peer path both get it.conversation_server.exintoEgress(the server only shrinks, Tracker: ConversationServer by subtraction #1369): the server is 19 lines shorter thanmainand the pin drops to 2775.Egress's moduledoc says why this one function takes the state.docs/concepts/secrets.mdanddocs/concepts/vault.mdstate the brokered exception to "the merge happens once". Changelog entry under Unreleased.Tests
broker_native_test.exs:refresh/4rewrites every live session of the conversation and no other's; leaves an expired row alone and reports zero; resolves the tenant from the conversation.egress_test.exs:refresh_tenant_secrets/5(unchanged, edited, added, deleted, deleted-and-masking-inference) andrefresh_rules/4.conversation_server_broker_test.exs: turn two goes through a real idle ACP peer. A vault value edited between turns reachesBroker.refreshwith the same peer and the same token and noprepare; a deleted vault key hands the name back to the environment; an unchanged secret writes nothing; a rewrite with no live session falls back toprepare; a vault edited while parked reaches the broker on the wake. The three edit-between-turns tests fail on the old server code and pass on the new.Fixture note: the file's shared
@sessionhasexpires_at: nil, whichexpiring?/1treats as expiring, so the pre-existing tests re-minted on every turn and could not have caught a stale idle peer. The new describe uses a session with a real end.Re-reading the vault on every turn
Two row fetches and two decrypts per turn, ahead of a sandbox spawn or an ACP prompt; not measurable. A deleted environment or vault row now drops its secrets from the broker mid-conversation, which reads as correct.
Review Loop round 1 (fb5e906)
Egress.refresh_before_turn/1now returns whether the token was replaced andrun_turn/6drops the connection ontrue, so the turn is a fresh spawn carrying the new env. Two server tests assert the old peer is dead and the spawn env carries the new token (no-live-session and expiring cases). This also closes the "busy peer outlives the 6h TTL" item that was left open.mix precommitis green (credo, dialyzer 0 errors, sobelow, prod release assembled, 4411 core + 144 + 33 extension tests, 0 failures). Prose gates on the two pages: docs-style clean, vale 0 errors/0 warnings, destink clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01GP4S6417fiRQARugfSUM7u