@@ -487,6 +487,135 @@ no TCP listener, fail closed, bounded everything.
487487
488488---
489489
490+ ## 24. Credential broker on the unsigned local tier
491+
492+ Numbered 24 because 22 and 23 are claimed by in-review PRs
493+ [ #170 ] ( https://github.com/LockInTime/headless/pull/170 ) (origin allowlist) and
494+ [ #169 ] ( https://github.com/LockInTime/headless/pull/169 ) (artifact upload).
495+
496+ ** Decision:** Headless will grow a host-owned credential broker and durable
497+ normal-profile login state without paying for Apple Developer Program
498+ membership. Implement the local/community macOS tier and the Linux secret
499+ backend now. Keep the stored record format compatible with a later Developer
500+ ID build. Do not ship silent credential use on unsigned builds ([ #166 ] ( https://github.com/LockInTime/headless/issues/166 )
501+ is out of this product).
502+
503+ The broker is the only component allowed to read password values from macOS
504+ Keychain or Linux Secret Service / KWallet. The agent-facing protocol speaks
505+ origin-bound aliases and short-lived challenge IDs, never password values.
506+ Direct ` fill ` remains available for test passwords, with the existing
507+ disclosure that Headless cannot erase a secret from a model-provider
508+ transcript after the user typed it to the agent. Once the vault exists, the
509+ default is aliases only.
510+
511+ Saved-credential use always requires current user presence: Touch ID or the
512+ macOS account password on macOS; an unlocked approved Linux secret-service
513+ prompt on Linux. Peer UID on the control socket is not authorization to
514+ release a secret. If the OS vault is missing, locked, or a plaintext
515+ Chromium fallback, fail closed.
516+
517+ ** Status:** decided 2026-09-10 (owner). Implementation starts at
518+ [ #155 ] ( https://github.com/LockInTime/headless/issues/155 ) , then
519+ [ #156 ] ( https://github.com/LockInTime/headless/issues/156 ) and
520+ [ #157 ] ( https://github.com/LockInTime/headless/issues/157 ) .
521+
522+ ** Rationale:** agents today put passwords through ` fill ` , so secrets show up
523+ in tool transcripts. A vault is useful. It does not require notarization.
524+ Keychain Services and Local Authentication work on ad-hoc local builds.
525+ Developer ID buys Gatekeeper-friendly distribution and a stable signer
526+ across updates ([ #45 ] ( https://github.com/LockInTime/headless/issues/45 ) ), not
527+ the ability to store a password. Skipping the $99 is fine for this feature.
528+ Skipping per-use confirmation on an unsigned binary is not: after that
529+ toggle, Headless cannot prove a human chose the alias, and there is no
530+ Apple-verified identity to hang a weaker policy on.
531+
532+ Durable cookies are the higher-value login gap. WKWebView already persists;
533+ Linux Chromium currently keeps the profile under the runtime directory and
534+ drops it across reboot. Fixing that is most of "stay logged into staging"
535+ without a password manager.
536+
537+ ** Guarantees**
538+
539+ - Password values never appear in snapshots, command output, MCP, logs,
540+ flows, recordings, diagnostics, errors, environment variables, or process
541+ arguments.
542+ - Page JavaScript cannot enumerate aliases or query the vault.
543+ - Records bind to a canonical exact HTTPS origin (localhost http is the
544+ documented development exception) plus an account identity and a
545+ user-chosen alias.
546+ - Normal-profile aliases, cookies, and storage are invisible to private
547+ contexts ([ #35 ] ( https://github.com/LockInTime/headless/issues/35 ) ). Private
548+ contexts may hold only in-memory credentials enrolled in that context, and
549+ those die with the last session using it.
550+ - Cookie import (backlog G9) stays rejected.
551+ - Passkeys stay as decision 20: omitted unless Apple grants the restricted
552+ entitlement. The vault does not unblock WebAuthn.
553+
554+ ** Residual risks we will state, not paper over**
555+
556+ - Same-user malware, root, and a replaced unsigned binary can still reach
557+ Keychain items the way any local process of that user can, subject to
558+ macOS prompts.
559+ - Rebuilds and signing-identity changes may re-prompt or look like a
560+ different app. Document that. Do not invent a self-signed cert as public
561+ trust.
562+ - Prompt injection can still * name* an alias. Per-use user presence is what
563+ makes that fail closed on this tier.
564+ - Login cookies are as stealable as in any persistent browser. Treat them as
565+ session secrets in the threat model, separate from vault passwords.
566+
567+ ** macOS unsigned tier**
568+
569+ - Ad-hoc or source builds. Gatekeeper may require a manual open.
570+ - Broker owns its Keychain items. No Keychain access groups.
571+ - Every saved-credential use requires Touch ID or the macOS password.
572+ - Capabilities report a local/unnotarized credential tier. Do not describe
573+ this as notarized or enterprise-ready.
574+ - A later Developer ID build may add session-level or trusted-origin
575+ approvals only after a new decision. Notarization alone does not widen
576+ permissions.
577+
578+ ** Linux**
579+
580+ - Secret Service or KWallet only. No Apple dependency. No Chromium basic
581+ plaintext store. Locked or missing backends return a specific error.
582+
583+ ** Durable normal profile ([ #155 ] ( https://github.com/LockInTime/headless/issues/155 ) )**
584+
585+ - One per-user profile. Sessions keep sharing it (architecture §11).
586+ - Cookies and site storage survive host and machine restart until the user
587+ logs out, clears, or uses a private context.
588+ - Linux: private XDG data directory, mode ` 0700 ` , atomic setup, single-owner
589+ lock, migration from the current runtime profile only when that move is
590+ safe, explicit corruption recovery. Not the ephemeral runtime dir.
591+ - macOS: keep the persistent WKWebView data store, but document and test the
592+ contract instead of leaving it implicit.
593+ - No unrestricted ` --profile-path ` . No cookie-import command.
594+
595+ ** Out of this decision**
596+
597+ - [ #166 ] ( https://github.com/LockInTime/headless/issues/166 ) unsigned silent
598+ fill.
599+ - [ #158 ] ( https://github.com/LockInTime/headless/issues/158 ) Settings window.
600+ Interactive CLI plus a native save sheet are enough for the first cut.
601+ - [ #153 ] ( https://github.com/LockInTime/headless/issues/153 ) full settings
602+ registry. Needed later if "aliases only vs allow direct fill" becomes a
603+ persisted user-only setting. It does not block #155 .
604+ - Paying for Developer ID in order to start this work.
605+
606+ ** Consequences:** #154 is the decision. Implementation PRs must keep
607+ passwords off the socket, fail closed without a vault, and advertise the
608+ unsigned tier honestly. Protocol additions (` credentials * ` , ` auth login ` ,
609+ ` AUTH_REQUIRED ` ) are compatible additions; do not bump 0.5 unless a breaking
610+ change appears. Tagged macOS releases still fail closed without Developer ID
611+ secrets; that remains #45 and is not a vault prerequisite.
612+
613+ ** Revisit trigger:** a Developer ID identity is actually used for public
614+ macOS distribution, and we want weaker-than-per-use confirmation. Write a
615+ new decision. Do not turn #166 on just because notarization started working.
616+
617+ ---
618+
490619## Decision log
491620
492621| # | Decision | Status | Date |
@@ -505,5 +634,6 @@ no TCP listener, fail closed, bounded everything.
505634| 19 | Keep macOS agent startup behind the current app | Implemented | 2026-08-12 |
506635| 20 | Omit passkeys unless Apple provisions Developer ID release | Implemented | 2026-08-12 |
507636| 21 | Rust port of shared core, protocol layer first | In progress | 2026-08-22 |
637+ | 24 | Credential broker on the unsigned local tier | Decided | 2026-09-10 |
508638
509- New decisions append here with the same format.
639+ New decisions append here with the same format. 22 and 23 are claimed by open PRs # 170 and # 169 .
0 commit comments