fix(remote): stop persisting the daemon bearer token, and authenticate forced-reconnect release correctly - #1648
fix(remote): stop persisting the daemon bearer token, and authenticate forced-reconnect release correctly#1648thymikee wants to merge 4 commits into
Conversation
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed exact head |
ADR 0007 requires generated connection profiles to strip daemon and Metro bearer tokens; only the Metro half was honored. `connect` was writing the daemon bearer token into the 0600 connection-state file, and every later command read it back out. Stop writing `authToken` into `RemoteConnectionState['daemon']` and resolve it at each reader from the existing flag -> environment (AGENT_DEVICE_DAEMON_AUTH_TOKEN) -> remote-config-profile chain instead, matching src/cli/auth-session.ts's precedence. Behavior change: a user who ran `connect --daemon-auth-token <value>` and relied on later commands picking the token back up from the state file will now get an auth failure. They must export AGENT_DEVICE_DAEMON_AUTH_TOKEN, set daemonAuthToken in their remote config, or pass --daemon-auth-token on each command. website/docs/docs/remote-proxy.md is updated to show the supported env-var workflow.
…vious endpoint's own credential connect --force released the previous connection's lease using the new connection's ambient daemonAuthToken instead of the previous endpoint's own credential, and swallowed the resulting auth failure — silently orphaning the old lease when replacing a connection with a differently-authenticated one. Resolve the release token from the previous connection's own remote-config profile first, fall back to the ambient token only when the two connections share the same daemon endpoint, and otherwise skip the release and surface an actionable notice (tenant, run id, lease id, endpoint) through the existing connect notice channel instead of hiding the failure.
…e's own token resolvePreviousOwnDaemonAuthToken read the previous connection's profile through resolveRemoteConfigProfile, which folds AGENT_DEVICE_DAEMON_AUTH_TOKEN (and other env defaults) into the result. When the previous config file declared no token and the new connection's credential came from that same global env var, it was misclassified as belonging to the previous endpoint and sent there on forced-reconnect release — recreating the credential leak the prior fix was meant to close, just via env instead of --daemon-auth-token. Read the previous profile with the new readRemoteConfigFile (a provenance- preserving, file-only load with no ambient env/CLI merging), so only a token the previous config file itself declares can satisfy rule 1. Rules 2 and 3 are unchanged.
f0586db to
d79bf78
Compare
|
Fixed at Root cause confirmed:
Red run for the new regression (env-merging read restored, tokenless profile A, differing endpoints, B supplied through That is the leak itself rather than a proxy for it: a release request was issued, carrying the env-sourced new token against the old endpoint. After the fix no request is issued and the unreleasable-lease notice surfaces instead. You were also right about why the existing coverage missed it — the old unreleasable-old-token test supplied B through flags with an empty env, so it never took the production environment path. The new fourth test is the only one that does; the other three were re-checked and still pin what they claim (test 1 uses a file-declared token and never depended on the merge; tests 2 and 3 involve no env var).
|
|
Re-reviewed exact head P1 — the previous config file is trusted after it may have changed identity. Validate the old file’s saved hash/endpoint provenance before trusting its token (or use an equivalent endpoint-bound source), and add a same-path A→B regression proving no release request sends B to A. The current test uses distinct immutable old/new paths, so it cannot catch this. Fallow and FreeRange both failed before checkout on GitHub action-download 500/503 outages; all other substantive checks are green. |
…point Rule 1 reads the previous connection's own config file to recover a credential that provably belongs to the previous endpoint. It re-read `previous.remoteConfigPath` and trusted whatever token that file holds *now* — but a config path is routinely reused, so "connect to A from ./remote.json, re-point ./remote.json at B, connect --force" classified B's token as A's own and sent it to A during lease release. Same cross-endpoint leak the env-merge fix closed, arriving through the file instead of the environment. The file must now still vouch for the previous endpoint, by either of two independent facts: its bytes still hash to the `remoteConfigHash` recorded at connect time (so it is literally the declaration that stood up the previous connection), or — if it changed — it still declares the same daemon base URL. The second is what keeps an ordinary credential rotation releasing its lease instead of orphaning one; endpoint equality, not the fact of an edit, is what separates rotation from re-pointing. Endpoint comparison runs both sides through `buildRemoteConnectionDaemonState`, the same normalizer that produced the stored `daemon.baseUrl`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Rva4YGtSCAKJqH5PbpcCU
|
Re-reviewed exact head 5968975. P1 remains: an unchanged remote-config file is trusted by hash even when explicit CLI flags overrode its endpoint and token for the previous connection. Supported repro: profile P declares endpoint B and token B; initial connect uses CLI endpoint A and token A, so state records A; later connect --force re-reads unchanged P, hash-matches it, classifies B as A own credential, and sends token B to endpoint A during lease release. resolveConnectProviderProfile explicitly merges profile flags first and CLI flags second, so hash equality proves file identity, not that its token was the effective credential for the stored endpoint. Require normalized declared endpoint equality even on the hash-match branch, fail closed when the profile declares no endpoint, and add this unchanged-profile CLI-override regression asserting no release request sends B to A. No device evidence applies; this is remote auth and state logic. No readiness label. |
Summary
Two commits: an ADR-0007 conformance fix, and the P1 regression it exposed (caught in review of #1639 — thanks).
1. Stop persisting the daemon bearer token (
7ce71723f) — BREAKINGADR 0007 states that generated connection profiles "must strip daemon and Metro bearer tokens". The Metro half was honoured; the daemon half was not —
connectwrote the token into the connection-state file and every later command read it back. The same file'ssanitizeDaemonBaseUrlalready strips credentials out of the base URL, so the intent was there; the token just sat beside it as a field.After this, a token passed once to
connectis not reused by later commands:Every alternative path ADR 0007 names already existed (
AGENT_DEVICE_DAEMON_AUTH_TOKEN, thedaemonAuthTokenconfig key,--daemon-auth-token), so this is conformance rather than new machinery.website/docs/docs/remote-proxy.mdis updated.2. Authenticate a forced reconnect against the endpoint it is actually releasing (
f0586db76)Commit 1 removed the persisted token and switched both release call sites to the caller's
flags.daemonAuthToken. One of those two was wrong.disconnectreleases the current connection, so the ambient token belongs there. ButcleanupForcedPreviousConnectionreleases the previous one —previous.leaseId,previous.daemon.baseUrl,previous.tenantall come offprevious— while the token came off the new connection's flags. So replacing profile A with a differently-authenticated B sent B's token to A's endpoint, andreleasePreviousLease's barecatch {}swallowed the auth failure: the reconnect reported success and A's lease plus provider-side resources were orphaned, silently. The persisted token had been masking the distinction, which is why it only surfaced once commit 1 removed it.Now resolved for the endpoint being released: the previous connection's own profile token first; the ambient token only when previous and next base URLs match; otherwise the new credential is not sent to the old endpoint and an actionable notice naming tenant, run id, lease id and previous base URL is surfaced instead.
releasePreviousLeaseno longer swallows failures. Reconnect still succeeds — this is a notice on the success path, reusing the existingRuntimePreparationNotice/LeasePreparationNoticechannel rather than a new one.Validation
The P1 regression pin proven red —
resolvePreviousLeaseAuthforced back to the ambient token, then:Three new tests cover the rule's three cases: old-profile token used over the new one; no recoverable old token with a differing endpoint (no release attempted, notice surfaced); same endpoint falling back to the ambient token so ordinary same-profile
--forcekeeps working.Two pre-existing
--forcetests were strengthened, not relaxed: they previously only asserted that a release happened, and now assert which token was sent (assert.equal(releaseRequest?.daemonAuthToken, 'test-old-…')plus an explicitnotEqualagainst the new one) againstlease-oldathttps://old.example. The only removed fixture line was the old config's tokenless form. All token literals are obviously fake.isRemoteConnectionState/isRemoteConnectionDaemonStatewere checked for strictness (remote-connection-state.ts:294,:333): they tolerate unknown keys, so a stale on-disk state file carryingauthTokenstill parses and the field is simply never read. No migration needed.Scope
Not device-facing, so no simulator/emulator evidence applies. Split out of #1639 per review. Local gates are currently unreliable on this machine due to a competing test run — pushed on GitHub CI's authority with maintainer agreement.