Why
The auth pattern that works today (daily.dev, demo recipes): a human logs in once in a visible window, /sessions/state exports the cookie jar, the recipe keeps it in a plaintext file (chmod 600 by convention) and re-imports it later. That file is the login — a daily.dev cookie lives ~7 days — and it sits in cleartext on disk. Plaintext-at-rest is the weak link of an otherwise honest chain.
Stage 1 — session states in the OS keychain (small, philosophy-consistent)
- CLI:
navette state save|load|list|delete NAME (+ optional --session, --url for the daemon).
- HTTP: optional
store param on POST /sessions/state and POST /sessions/load. With store, the cookie payload never returns to the caller — the route answers {ok, stored: NAME} and the secret stays inside the navette process + the OS keychain.
- Storage: the
keyring crate (macOS Keychain · Windows Credential Manager · Linux Secret Service). Service navette; secret is a wrapper JSON {origin, saved_at, cookies}; a _index entry keeps the enumeration the keyring APIs don't give us.
- No plaintext fallback: on a box with no OS keychain (headless Linux without Secret Service) → explicit error, no silent cleartext file.
- Honest ACL note: macOS re-prompts on a new binary (ACL bound to the signing identity) — implicit per-release consent, which is a feature.
Stage 2 — origin-bound credentials (design-first; revisits a stated rule)
Today's non-negotiable is "login is human; passwords belong to the user" (README, marketplace SKILL.md). Stage 2 revisits it the way a browser does, with the secret opaque to the agent:
navette creds set SITE — the human types the password once; it lands in the keychain bound to the exact origin recorded from the live session, never free-text.
- The agent calls
POST /login {"site": SITE} — navette resolves the secret internally (keychain → fill → submit) and returns only {logged_in}. The LLM never sees the secret, not in arguments, not in results, not in logs.
- Origin-bound fill: credentials are never typed anywhere but the registered origin — a hostile page or a prompt-injected agent can neither harvest the secret nor weaponize a login against another site.
- 2FA / captcha stay human: fall back to
session_show (visible window), then resume.
- Stated limit: at fill time the password does reach the page — the same as human typing. Origin-binding is the defense, not obfuscation.
- Requires a design doc before code: consent UX, log redaction, what
creds list may show.
Pitch line once both land: the agent browses as you without ever seeing your password — no Playwright stack does this cleanly (their storageState is plaintext by design).
Acceptance (stage 1)
Why
The auth pattern that works today (daily.dev, demo recipes): a human logs in once in a visible window,
/sessions/stateexports the cookie jar, the recipe keeps it in a plaintext file (chmod 600 by convention) and re-imports it later. That file is the login — a daily.dev cookie lives ~7 days — and it sits in cleartext on disk. Plaintext-at-rest is the weak link of an otherwise honest chain.Stage 1 — session states in the OS keychain (small, philosophy-consistent)
navette state save|load|list|delete NAME(+ optional--session,--urlfor the daemon).storeparam onPOST /sessions/stateandPOST /sessions/load. Withstore, the cookie payload never returns to the caller — the route answers{ok, stored: NAME}and the secret stays inside the navette process + the OS keychain.keyringcrate (macOS Keychain · Windows Credential Manager · Linux Secret Service). Servicenavette; secret is a wrapper JSON{origin, saved_at, cookies}; a_indexentry keeps the enumeration the keyring APIs don't give us.Stage 2 — origin-bound credentials (design-first; revisits a stated rule)
Today's non-negotiable is "login is human; passwords belong to the user" (README, marketplace SKILL.md). Stage 2 revisits it the way a browser does, with the secret opaque to the agent:
navette creds set SITE— the human types the password once; it lands in the keychain bound to the exact origin recorded from the live session, never free-text.POST /login {"site": SITE}— navette resolves the secret internally (keychain → fill → submit) and returns only{logged_in}. The LLM never sees the secret, not in arguments, not in results, not in logs.session_show(visible window), then resume.creds listmay show.Pitch line once both land: the agent browses as you without ever seeing your password — no Playwright stack does this cleanly (their
storageStateis plaintext by design).Acceptance (stage 1)
navette state save dailydevstores the live session;navette state load dailydevrestores it into a fresh sessionstoreparam on both routes; cookies omitted from the response when storinglist/deletework;_indexstays consistent