Skip to content

Let Dreamforge agents hop to the primary Claude account - #149

Merged
QuicksilverSlick merged 7 commits into
mainfrom
fix/agent-pool-primary-token
Nov 10, 2026
Merged

QuicksilverSlick merged 7 commits into
mainfrom
fix/agent-pool-primary-token

Conversation

@QuicksilverSlick

@QuicksilverSlick QuicksilverSlick commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

This needs Russell's read before it ships: it touches agent auth (lane:hold).

Decided 10 Nov 2026 (Russell): Reset Worker gets no dpapi: entry until guest turns have a permission check. A dpapi: login puts the primary's long-lived token in the adapter's environment, and on Reset Worker an outside allowlisted key drives a shell with no permission gate, so that key could read it. So the env edits after this PR skip Reset Worker. token=env already exposes the second account's token this way. This PR only stops handing the shell the pool string, which names the file.

What it does

Slice 6 of the Claude account switchover.

  • A third kind of pool entry. Today every pool is token=env;fallback=<folder>, both on the second account, so a limit on it has nowhere to hop. An entry can now be primary=dpapi:<path>: a setup token in a file encrypted for this Windows user. It uses the same PowerShell call as the kit's lib.mjs, with $ErrorActionPreference = 'Stop' added. The token is read at each launch and each 15-minute sign-in check, goes to the child only in its environment, and isn't kept anywhere.
  • A dead file moves on. A file that is missing, won't decrypt or comes back blank fails that entry and the pool goes to the next login, as for any other dead login. If all the pool is left offering is a dead file, the launch is refused, rather than running the agent on whatever its own environment holds. A folder or env login that is merely out is still offered when every login is out, as it is today. Off Windows, a dpapi entry fails plainly.
  • A silent PowerShell is not a lost sign-in. If PowerShell times out or won't start at a launch, that login is set aside for 30 minutes as a timed limit and comes back by itself. In the sign-in check, no answer from PowerShell concludes nothing.
  • The owner is told about the file. A file failure doesn't say "Claude said: not signed in", because Claude was never asked. For a dead file on the login the work is on, the text, the STATE record (class=token_file) and the card say which login's file failed and why, and name set-account.ps1 as the fix. A garbled entry's reason says to correct the agent's settings instead, though its card still names the script. A silent PowerShell gets the text and the record, with no card. A dead file on a spare login gets one text and nothing else. A text queued at a launch now goes out on the next 30-second maintenance beat, instead of waiting for a turn to finish. That beat only runs once the pool is up, so a boot or lazy wake where no agent starts never reaches it: the card is written, but a normal start exits with the text and the record unsent, and a lazy one holds them.
  • A token the server refused stays out. claude auth status never asks the server, so it says yes to a revoked token. A real 401 on a dpapi login now waits for the file to change or a real answer, instead of a check putting the work back on it every 15 minutes. token=env keeps today's behaviour.
  • A garbled entry is dead. DPAPI: in capitals, a label with a dot or a space, or a label over 32 characters fails that entry with a plain reason. It used to become a folder login with the typo as its folder.
  • The blank-token bug. A folder entry sent CLAUDE_CODE_OAUTH_TOKEN="", and the CLI reads that as signed out. It now sends no token, and the variable is removed where the launch env goes on the command. A PC-wide token still can't win over a folder login. Both places (the agent spawn and the sign-in check) go through one function.

The path is config and is treated as untrusted: it reaches PowerShell in an environment variable and never as command text. A failure says which login failed in fixed words. It never carries the token, the path, or anything PowerShell printed. The one name taken from what was written is a garbled entry's: with no usable label, it goes by a slug of the entry's last path part, as an unlabelled folder does.

The pool strings on this PC do not change with this PR. Nothing uses the new entry until the four env edits Russell approves afterwards. The primary's file also has to exist by then, and the kit's set-account.ps1 can't write it yet: it only has -Phone today, and -Account comes with slice 3. Until those edits, the changes that take effect are: folder logins get the token removed rather than blanked, the agent no longer inherits the pool string, and a waiting owner text goes out on the maintenance beat.

What was exercised

  • cargo test -p buzz-acp --lib on Windows: 1286 passed, 42 failed. The branch point 5e1fce0: 1270 passed, 42 failed. The 42 failing test names are identical, and the 16 extra passes are the new tests. origin/main has since moved to 849bd8b; the comparison is against the commit this branch is built on.
  • cargo fmt, and cargo clippy -p buzz-acp --all-targets -- -D warnings, clean.
  • Two attackers reviewed the first commit. Their findings were reproduced first, by a failing test or by a mutant the suite let through before the new test and fails after it. The mutants were an unconditional token removal (before and after the login's env), a loosened refusal to start, the sign-in check probing a dead file anyway, and the pool string left on the child. Two were confirmed by reading instead: the undelivered launch text (the only two callers that send texts), and the primary token reaching a guest's shell (claude and agents were not run).
  • New tests, none of which reaches PowerShell. With a fake decrypter: the entry order, a dead, blank, silent or garbled entry, a pool of only dead files, the check's verdicts, each file read once per launch, a refused token waiting for a new file, and the owner's words on the text, the record and the card. A real spawned child reports set or unset for each kind of login's token, and that no pool string reaches it. A 17th test, the off-Windows refusal, calls the real non-Windows stub and only compiles off Windows, so it is not among the 16 above.
  • The PowerShell text was run by hand against two paths that don't exist, one of them hostile ('; New-Item ... #). Both exited 1 with empty stdout, and the injected command did not run.

What was not exercised

  • No real token, no real DPAPI decrypt, no real agent. The Windows decrypt path (spawn, 15-second limit, reading stdout) has no test, by design.
  • The maintenance beat's one call to send a waiting text is in the main loop, which has no unit test. The function it calls is tested.
  • That a revoked token still reads as loggedIn:true comes from the CLI's docs and this crate's own note on it. claude was not run.
  • The design's proof is still owed after the env edits: Support Agent answers a turn with the primary entry first, and a forced limit hands off.
  • cargo test ran on Windows only. The non-Windows branch was not run locally: it is left to CI's Linux run, where just test-unit runs buzz-acp's tests.

Known limit

The desktop's agent panel has no wording for class=token_file, so when a running harness has no login left for that reason its "Paused" line is blank, unless the newest turn failed. The text, the card and the STATE record still say why, and the presence dot shows away. Fixing it is a desktop change, so it is left out of this crates-only PR.

Size

Over the design's 150 lines, and at 854 changed lines over the ~600 guide. 283 lines of code added and 48 removed, not counting comments, blank lines and tests. The first commit was 126 and 30; the review findings added the rest. Tests are 385 added and 16 removed. It stays one PR because the code and its failure handling only make sense together.

Factory-Task: cutover/agent-pool-primary-token
Built-by: claude-code/claude-opus-5-5
Claude-Account: russelledeming@gmail.com
Undo: put the old pool strings back if the env edits were made, and revert this PR's merge commit.

🤖 Generated with Claude Code

QuicksilverSlick and others added 7 commits November 9, 2026 13:43
…oken

Every agent's pool is token=env;fallback=<folder>, both on the second
account, so a limit on it had nowhere to hop. A pool entry can now be
label=dpapi:<path>: a long-lived token in a file encrypted for this
Windows user, read at each launch and each sign-in check and handed to
the child only in its environment. A file that gives no token fails its
own entry and the pool moves on. A pool with no login left refuses to
start the agent rather than running it on whatever its own environment
holds.

A folder entry used to send CLAUDE_CODE_OAUTH_TOKEN="", which the CLI
reads as signed out. It now sends no token, and the variable is removed
where the launch env is applied: the agent spawn and the sign-in check.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pool string names the primary's token file, and the agent's tool
shell inherited it. It is now removed from the child, beside the Nostr
key.

Two gates had no test. Making the token removal unconditional, or moving
it after the login's own env, stripped every token=env and dpapi login
and passed the whole suite. So did loosening the refusal to start with
no login left. A real child now shows each kind of login's token, and a
pool with no login left starts nothing.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A real 401 on a dpapi login marked it signed out, and the next sign-in
check cleared the mark: claude auth status answers loggedIn:true for any
token without asking the server. The work went back onto the revoked
token every 15 minutes, with a lost-sign-in text and an all-clear each
time, and every agent shares that one file.

A real 401 now remembers the file's modified time. A check's yes clears
the mark only once the file has changed; a real answer still clears it.
A mark set because the file could not be read still clears on a good
read. token=env keeps today's behaviour.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A launch that could not read a token file marked the login signed out
whatever the reason, and the owner was told "Claude said: not signed in"
when Claude was never asked. A PowerShell timeout moved work off a good
account as a lost sign-in. The card's remedy was also wrong for a dead
file: signing in does not replace it.

A silent or unstartable PowerShell now sets the login aside as a timed
limit, tried again in 30 minutes, and tells the owner once. A dead file
still takes the lost-sign-in path, in its own words: which login's file,
what is wrong and that set-account.ps1 is the fix, on the text, the STATE
record (class=token_file) and the card. A login already inside a limit
keeps it.

The sign-in check's handling of a dead file moves to one small function
with a test: a file that gave no token is never judged by the CLI.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dead token file at boot, a slot refill or an idle move queued the
owner's text, and only a finished turn or a sign-in check that changed
something sent it. An idle agent ran on another account and the phone
stayed silent.

The maintenance beat now sends a text that is waiting. It does nothing
when none is, so a STATE record that merely fell due keeps its carrier
and #factory keeps its cadence.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A launch could read the same dead file once per pool entry plus one,
under the factory lock on the main loop, so a hung PowerShell held the
loop for 30 s or more and the comment's 15 s was low. The loop now stops
when the pool offers a login it already failed, and the comments give the
real bound: 15 s per dpapi entry.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A typo in a dpapi entry (DPAPI: in capitals, a label with a dot or a
space, a label over 32 characters) launched the agent with
CLAUDE_CONFIG_DIR set to the whole string and any token removed. Nothing
named the bad entry. It now fails that entry with a plain reason and the
pool moves on.

Claude-Account: russelledeming@gmail.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@QuicksilverSlick QuicksilverSlick added lane:hold Needs the owner's read before any train (paid path, auth, migrations, or asked for). lane:ready Builder is done: green, agent-reviewed, exercised. The steward puts it in the next train. labels Nov 9, 2026
@QuicksilverSlick
QuicksilverSlick merged commit 5b1cd13 into main Nov 10, 2026
65 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:hold Needs the owner's read before any train (paid path, auth, migrations, or asked for). lane:ready Builder is done: green, agent-reviewed, exercised. The steward puts it in the next train.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant