Skip to content

Secrets: keychain-backed session states (stage 1), then origin-bound credentials the agent never sees (stage 2) #8

Description

@slabbdev

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)

  • navette state save dailydev stores the live session; navette state load dailydev restores it into a fresh session
  • store param on both routes; cookies omitted from the response when storing
  • list/delete work; _index stays consistent
  • No-keystore environments fail loudly (error text names the missing service)
  • CI stays green ×3 OS (no keychain round-trip asserted in CI; wrapper JSON + index logic covered as pure functions)

Activity

  1. slabbdev commented on Oct 9, 2026

    @slabbdev
    OwnerAuthor

    Both stages shipped on main — recap for closure:

    • Stage 1 — 4950645: navette state save|load|list|delete, store param on /sessions/state + /sessions/load (the cookie payload never returns to the caller), keyring-backed (Keychain · Credential Manager · Secret Service), explicit error when no OS keychain exists — no silent plaintext fallback. src/keystore.rs.
    • Stage 2 — 9a32cbf: navette creds set (hidden stdin, double-entry, bound to the session page's exact origin), GET /creds (sites/origins/usernames only — passwords have no read-back API), POST /login {site} resolves and injects the credential inside the daemon — never in a request, response, or log; 403 unless the session sits on the credential's origin. Two-step flows (email → continue → password) orchestrated server-side; 2FA/captcha stay human via session_show. MCP +1 (19 tools). Verified live: the-internet fill+submit → /secure; leboncoin auth mechanics end-to-end with a throwaway credential.

    The pitch line is now true end-to-end: the agent browses as you without ever seeing your password — nothing in the Playwright stack does this cleanly (storageState is plaintext by design). Closing this as delivered unless something above misses the bar.

  2. slabbdev commented on Oct 9, 2026

    @slabbdev
    OwnerAuthor

    Delivered on main: stage 1 (4950645 — keychain-backed session states, store param, no plaintext fallback) and stage 2 (9a32cbf — creds set/GET /creds/POST /login, origin-bound, no read-back API, 19 MCP tools), verified live on the-internet and leboncoin. Closing as delivered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions