Open question: where do connector credentials belong?
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:
| Connector | Secret | Where it lives today |
|---|---|---|
| ynab | YNAB_PAT | .env.local |
| gmail | GMAIL_USER, GMAIL_APP_PASSWORD | .env.local |
| chatgpt | CHATGPT_EMAIL, CHATGPT_PASSWORD | .env.local |
| usaa | USAA_MEMBER_ID, USAA_PASSWORD | .env.local |
| amazon | AMAZON_EMAIL, AMAZON_PASSWORD | .env.local |
| github | GITHUB_USERNAME, GITHUB_PASSWORD, GITHUB_PERSONAL_ACCESS_TOKEN | .env.local |
| oura/spotify/strava/notion/reddit/pocket/slack | various *_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:
- "Durable" credentials — API tokens, PATs, OAuth refresh tokens, app passwords. Written once, read many times.
- "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:
- 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
- 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.
- The connector never sees the full vault — only the secrets it declared in its manifest's (future)
credentials_schema. - Grant-scoped access — a connector spawned under grant X can read only credentials last provided under grant X's bootstrap, not the full bag.
- 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.localloses both. - Provenance: a credential carries how it was obtained (user-entered, auto-bootstrapped, grant-delegated) so the runtime can reason about trust level.
- 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
interactivebinding + 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.localis 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 inconnector-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+agewith 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
- 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.
- 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."
- 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.
- 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.localkey across the 30 connectors, classify by class 1/2/3. - Draft
credentials_schemamanifest field (joint withconnector-configuration-open-question.md). - Pick a default vault backend:
file+ageis likely the simplest + best-documented path. - Refactor
bootstrap-github-pat.jsto 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.