Open question: where do connector credentials belong?

add-polyfill-connector-systemProject noteOpen questions
Created openspec/changes/add-polyfill-connector-system/design-notes/credential-storage-open-question.mdView on GitHub →

Status: sprint-needed Owner: project owner Created: 2026-04-19 Updated: 2026-04-24 Related: openspec/changes/add-polyfill-connector-system/design-notes/credential-bootstrap-automation-open-question.md, pdpp-trust-model-framing.md

Status: open Raised: 2026-04-19 Context: today the GitHub PAT bootstrap (bin/bootstrap-github-pat.js) drives a real browser login, generates a PAT, and stores it by appending to .env.local with 0600 perms. Every API-based connector reads its credentials from the same file at startup. This works for single-user dev on a trusted laptop. Nothing about it is appropriate for the spec's threat model.

Per-connector inventory of current credential land:

ConnectorSecretWhere it lives today
ynabYNAB_PAT.env.local
gmailGMAIL_USER, GMAIL_APP_PASSWORD.env.local
chatgptCHATGPT_EMAIL, CHATGPT_PASSWORD.env.local
usaaUSAA_MEMBER_ID, USAA_PASSWORD.env.local
amazonAMAZON_EMAIL, AMAZON_PASSWORD.env.local
githubGITHUB_USERNAME, GITHUB_PASSWORD, GITHUB_PERSONAL_ACCESS_TOKEN.env.local
oura/spotify/strava/notion/reddit/pocket/slackvarious *_TOKEN, *_SECRET.env.local
browser-session connectors (shopify, heb, loom, …)Playwright storage state at ~/.pdpp/browser-profile/ (cookies on disk)filesystem, 0700

Two classes of secret here, in tension:

  1. "Durable" credentials — API tokens, PATs, OAuth refresh tokens, app passwords. Written once, read many times.
  2. "Session" credentials — cookies, storage state. Generated by a browser login, consumed by a headless browser, re-used until they expire.

Plus a third, which we keep pretending isn't a secret:

  1. Account passwords — needed by auto-login helpers (USAA, Amazon, ChatGPT, GitHub), so they can reconstitute class-2 when it expires. These are the highest-blast-radius thing in the file and live next to everything else.

What the spec says today

Nothing. spec-core.md and the Collection Profile spec are silent on how a personal server obtains or stores credentials for polyfill connectors. The runtime's INTERACTION protocol covers asking for creds at run time; it doesn't specify where the answer is stored for the next run.

That silence is probably correct — "credential vault" is an implementer concern, not a wire-protocol concern — but the spec should name the requirement so implementers don't all invent their own dotfile.

What I'd expect a principled implementation to have

  1. A vault interface owned by the runtime (or a sibling service), addressed by (owner_id, connector_id, credential_name). Write via INTERACTION response; read from the connector's START env. Implementations:
    • file+age: encrypted file store (age or GPG), key held by the owner.
    • keyring: OS keyring (libsecret, Keychain, Windows Credential Manager).
    • cloud: Vercel Secrets / AWS Secrets Manager / HashiCorp Vault for hosted deployments.
    • memory: in-process only (single run), useful for ephemeral agents.
  2. The connector never sees the full vault — only the secrets it declared in its manifest's (future) credentials_schema.
  3. Grant-scoped access — a connector spawned under grant X can read only credentials last provided under grant X's bootstrap, not the full bag.
  4. Separation of class-2 from class-1 — browser session state is a derived artifact; losing it should prompt re-login, not force a password re-entry. Today they're colocated; losing .env.local loses both.
  5. Provenance: a credential carries how it was obtained (user-entered, auto-bootstrapped, grant-delegated) so the runtime can reason about trust level.
  6. Rotation hooks: when a PAT approaches expiry, the runtime can re-run the bootstrap tool automatically, again via INTERACTION for the 2FA challenge.

What today's reference already does right

  • Per-run owner tokens are minted from AS (not stored in a file) — the OAuth device grant flow is canonical.
  • The interactive binding + INTERACTION kind=credentials is the right way to collect creds at runtime.
  • Browser storage state lives in its own directory with 0700 perms, separate from the env dotfile.

What today's reference gets wrong

  • Env dotfile is plaintext. Compromise of the file = compromise of every connected account's password, bank login, personal access tokens, 2FA fallback paths.
  • The bootstrap tool (bin/bootstrap-github-pat.js) writes to the same plaintext file it read from — any implementer following our example will do the same.
  • No owner-scoping: .env.local is globally addressable. A multi-tenant PDPP server would need per-owner keyspaces, which env vars can't give.
  • No rotation signal. The system has no way to know a token is about to expire.
  • No separation of class-1 from class-3. The USAA password (class 3) and the YNAB PAT (class 1) are written to the same file with the same perms.

Candidate direction (to review, not decided)

Add a Credential Vault capability to the polyfill runtime spec:

  • Manifest declares credentials_schema (already proposed in connector-configuration-open-question.md) with per-credential metadata: class: 'password' | 'long_lived_token' | 'session', rotation_hint, obtained_via: 'user' | 'bootstrap_tool'.
  • Runtime API: getCredential(owner_id, connector_id, name) + setCredential(...). No direct file access from connectors.
  • Default backend in the reference: file+age with key bootstrapped on first run, encryption at rest. Password class stored separately from token/session class, with stricter access policy (only auto-login helpers can read password class; everything else reads session or token class).
  • Grant enforcement: the connector process inherits env from a vault-filtered view, never from the host process's env.
  • Bootstrap tools (today's bootstrap-github-pat.js) integrate with the vault instead of appending to dotfiles.

Cross-cutting questions

  1. Who owns the vault interface — Collection Profile runtime, or a sibling capability? Probably sibling — it's referenced by the runtime and by bootstrap tools, not exclusive to Collection Profile's wire protocol.
  2. Does the disclosure artifact specify credentials in scope? "This grant accesses your Amazon cookies and GitHub PAT" is a truer disclosure than "this grant accesses Amazon and GitHub."
  3. What happens to class-3 (passwords) in hosted PDPP? Hosting provider storing the user's USAA password is a liability; session state is only slightly better. Maybe hosted deployments forbid class-3 credentials and rely on user re-login via an inbox interaction.
  4. Is the Playwright persistent profile a credential vault? It already stores auth cookies for many sites. Should it be addressable through the same interface as class-1?

Action items (paused, awaiting direction)

  • Inventory every .env.local key across the 30 connectors, classify by class 1/2/3.
  • Draft credentials_schema manifest field (joint with connector-configuration-open-question.md).
  • Pick a default vault backend: file+age is likely the simplest + best-documented path.
  • Refactor bootstrap-github-pat.js to use the vault once implemented; today it's a canonical example of the wrong thing and should be visibly marked as "temporary pattern pending vault."
  • Decide: does this land in Collection Profile or as its own capability spec?

Related

  • connector-configuration-open-question.md — credential schema is one shape of option schema.
  • rs-storage-topology-open-question.md — per-connector vault partitions parallel per-connector RS partitions; decide together.
  • unattended-operation.md — unattended re-auth depends on class-3 being available; if we forbid class-3 in some deployments, unattended operation stops working for password-based sources.