Expand description
credential service — the paired CEF host’s browser-pane Basic-Auth
credential broker calls this, host→srv, to resolve an identity for a
pane and to look up / save / delete / fill a stored site credential.
Fill is the only method here that ever returns a plaintext
password, and it’s called exactly once per approved use, at the
moment the host is about to hand the credential to CEF’s
AuthCallback::cont(). Lookup deliberately returns only a masked
username — the credential-approval window (agentmux-cef,
open_subwindow-based, see credential_broker there) uses Lookup
to decide what to display, and must never receive the real password
even transiently.
Storage itself lives in [agentmux_srv::identity::browser_credential_store]
(OS keychain via the existing secret_store.rs wrapper) — this module
is just the /agentmux/service RPC surface over it, following the same
shape as super::host_ipc (the other host→srv service module).
§Auth: host-only, NOT the shared X-AuthKey
Every method on this service requires args[1] to be this srv
instance’s AGENTMUX_HOST_REG_SECRET, the same host-only credential
host_ipc.Register proves (see that module’s doc comment and
Config::host_reg_secret).
An earlier cut of this module gated on the instance-wide X-AuthKey
alone, reasoning that — unlike host_ipc.Register’s
“session-hijack-shaped” port+token — every method here only touches data
scoped to a caller-supplied identity_id. That reasoning was wrong,
and it defeated this feature’s entire stated security goal (reagent P0
on PR #2824). X-AuthKey is deliberately re-injected into every spawned
agent’s own environment as AGENTMUX_AUTH_KEY
(server/agent_handlers/input.rs), and AGENTMUX_BLOCKID is injected
alongside it. So any agent could, with a plain curl from its own Shell
tool:
credential.ResolveIdentitywith its own knownblock_id→identity_idcredential.Fillwith thatidentity_idplus the origin/realm it already knows (it is, after all, the thing browsing that site) → the plaintext password
…with no human approval window anywhere in the path. Fill uniquely
returns a raw password, which is exactly the class of secret that needed
host_ipc.Register’s treatment, not a weaker one.
The gate is applied to the whole service rather than to Fill alone:
ResolveIdentity leaks identity_ids, Lookup confirms which sites have
saved credentials, and Save/Delete let an agent plant or destroy
them. None of those are things an agent should reach either, and a
uniform gate cannot be defeated by finding the one method someone forgot
to cover.
Note this proves caller is the host, not a human approved this exact use. That is the boundary the threat model needs — the host is the process that runs the approval window, so a capability token it mints for itself would add no defence against the attacker in question (an agent), only an internal consistency check within the host.
Structs§
Functions§
- handle_
credential_ 🔒service - handle_
delete 🔒 - handle_
fill 🔒 - The one method that returns a real, unmasked password. Called exactly once per approved credential use — see this module’s own doc comment.
- handle_
lookup 🔒 - handle_
resolve_ 🔒identity block_id→identity_id, via the same instance-lookup the identity resolver already uses for OAuth env-var injection at spawn time (identity::resolver::inject::…) — reusingStore::instance_get_active_for_blockhere keeps “which identity owns this pane” resolved in exactly one place in the codebase.- handle_
save 🔒 - host_
caller_ 🔒allowed Some(error)when the caller failed to prove it’s the paired host — checked for EVERY method before dispatch, so a new method added later is gated by construction rather than by remembering to gate it.- mask_
username 🔒 us***@example.com/ab***style masking for the approval window’s display — enough for a human to recognize which saved login is being offered, not enough to be useful if somehow logged or screenshotted wrong. Keeps at most the first 2 characters before any masking.- reject_
unless_ 🔒host_ caller