Skip to content

Commit 805dbaf

Browse files
authored
Merge pull request #171 from LockInTime/docs/credential-broker-decision
docs: record unsigned credential broker decision
2 parents caeb26c + 94b0547 commit 805dbaf

1 file changed

Lines changed: 131 additions & 1 deletion

File tree

‎docs/roadmap/architecture-decisions.md‎

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)