ADR 0029 — Paired local browser capture prototype
- Status: Amended
- Date: 2026-10-23
- Scope: WIP implementation on
wip/chrome-extension; not a released integration - Amended by: ADR 0030 — Gmail availability and unpacked-only distribution; the original rationale below is preserved
- Related: ADR 0024 and ADR 0005, which this record extends
- Restructured: 2026-10-25
Context
Section titled “Context”A browser sees authenticated content that Imprint’s URL fetcher cannot retrieve from a URL alone. The dashboard API intentionally rejects extension origins, and its general commands include filesystem/configuration/destructive actions. Allowing every extension origin would widen that boundary. An experimental capture client should reuse local storage, source replacement and queue locking without granting the captured website access to local commands or credentials.
Decision
Section titled “Decision”Implement a development-only unpacked Manifest V3 extension with click-to-preview
and explicit save. Use activeTab/scripting for page extraction, trusted local
extension storage for credentials, and an extension worker that accepts messages
only from its own popup/settings. It contacts only literal 127.0.0.1 HTTP
addresses, refuses redirects, and requests no cookies or persistent site access.
Add a separate /api/browser router under the existing local API. Keep peer/Host
validation and ordinary dashboard origin rules. On browser routes require the
paired extension ID plus a random Bearer token; validate an exact extension
Origin when supplied. Preflight approval is origin-specific and does not grant
authenticated access. Pair through a local Python command, bind one existing
workspace, store only token hashes in a separate SQLite database, and support
rotation/revocation. The local API’s existing trust of local native clients is
unchanged; these credentials do not claim isolation from hostile local software.
Queue bounded inline text/metadata as an internal browser-capture command.
The existing dispatcher launches the same interpreter with a queue job ID;
content/tokens are not process arguments. The worker takes the shared queue lock,
rechecks its pairing and uses store_inline_content. Pin workspace at submission.
Status returns only a pairing’s own capture job state, not raw job bodies or logs.
Reuse existing vector fields and doc_metadata; add no vector collection schema
migration. Queue history retains raw capture content under existing local data
directory access rules. Browser records have source_type=browser and do not
participate in unauthenticated URL refresh.
Keep valid short inline captures/notes as a single chunk below the file chunker’s 100-character minimum, honoring inline ingestion’s existing 10-character floor.
Use URL-based page identities, separate content-addressed selection identities,
and Gmail account/message identities. Preserve SPA URL fragments. Gmail capture
only saves expanded message bodies with stable DOM message IDs, never a whole
partially rendered thread as a replacement source. Require the account email
explicitly in settings to avoid relying on mutable /u/0 account slots. This
is user-supplied provenance and must match the tab. PDF/editor export adapters
and mailbox OAuth synchronization remain future work.
Rejected alternatives
Section titled “Rejected alternatives”Sending URLs alone fails for signed-in applications. Granting extension origins general dashboard access exposes unrelated commands. Native messaging can avoid HTTP permissions but adds an OS-specific host registration/install lifecycle; defer it until the prototype establishes useful capture flows. Gmail API OAuth is more suitable for comprehensive mailbox synchronization but requires a different permission/verification lifecycle. Automatic all-site capture requires broader permissions and data retention controls and is outside this first slice.
Consequences
Section titled “Consequences”Users must run this branch’s backend, load unpacked source and pair manually. One token grants one workspace; changing workspaces rotates the pairing. Existing memories are preserved on revocation. Revocation prevents future/queued ingestion but does not stop an already executing job. A multi-document job can partly index before failure. Unchanged source chunks retain existing capture metadata. Gmail selectors, browser permissions and account identity need live validation before release. No claims of Google Docs, PDFs, real Gmail, Store approval or native Windows/macOS Chrome validation follow from synthetic Chromium tests.
Invalidation conditions
Section titled “Invalidation conditions”- If Chrome stops allowing extension requests to loopback HTTP, the transport must change; native messaging, deferred here, is the recorded alternative.
- If capture must run without a click or cover a whole mailbox, this click-to-save design does not apply; automatic capture and Gmail OAuth were left out of scope.
- If the local API gains authentication for every local client, the separate browser credentials could merge into it.
Validation
Section titled “Validation”Python regressions cover exact origins, peer/Host checks, credential rotation, workspace/job isolation, bounded inputs, source identities, revocation before execution, shared lock release and real dispatcher/subprocess failure reporting. Node checks cover loopback URLs, trusted senders and transport settings. The disposable Chromium smoke covers actual extension loading/pairing/storage and save/status plus synthetic article, selection and expanded Gmail extraction. Executed results and remaining limits belong in platform validation.