Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .adr/0011-one-openai-compatible-transcription-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 0011 — Voice transcription uses one OpenAI-compatible endpoint

Status: **Accepted** (2026-08-09)

## Context

Microphone audio must remain behind Collie's authenticated, same-origin bridge: a phone must not
receive a provider credential or gain another listener. The browser records a completed clip, the
bridge submits it to the configured provider, and the resulting text remains an editable draft before
the existing guarded send path.

Alternatives considered:

- **LiteLLM** adds a Python runtime or separate proxy process, listener, supervision, and logging
boundary for a single provider call.
- **Vercel AI SDK** has experimental transcription plus provider/registry surface without removing
Collie's capture, validation, privacy, or draft-review responsibilities.
- **Bespoke fetch** would make Collie own multipart details, cancellation, retries, and
OpenAI-compatible typing.

## Decision

Use the official `openai` JavaScript SDK with exactly one configured OpenAI-compatible endpoint.
Do not add a registry, fallback, streaming, conversion, playback, local service, or additional ingress.

## Consequences

- Operators select a model identifier whose endpoint implements Collie's narrow completed-file,
final-text transcription subset; model names and transcription quality are not portable.
- The configured provider receives audio and owns its own retention and logging policy. Collie's
current configuration, request bounds, privacy, and audit behavior are canonical in the
[README](../README.md#voice-input-optional) and [Architecture](../ARCHITECTURE.md#6-security-model).
- A compatible private service may be the single upstream, but remains independently deployed and
secured; Collie does not manage it.

## Revisit

Revisit this decision only when a concrete provider cannot meet the narrow final-text contract, or
when product requirements genuinely need provider-specific behavior. That is the threshold for a new
decision, not a reason to pre-build a registry.
52 changes: 52 additions & 0 deletions .adr/0012-synchronous-one-shot-voice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 0012 — Voice remains a synchronous one-shot BFF

Status: **Accepted** (2026-08-16)

## Context

Voice transcription needs a final text result before it can enter Collie's existing editable draft and
explicit Send path. The current requirement is a completed clip, not partial text, playback, or a
background task. A five-minute, 8 MiB clip still needs to tolerate bounded slow but continuously
progressing mobile upload, which makes a small fixed browser timeout dishonest.

Several larger designs look attractive once a request takes longer than a normal mutation:

- A **status side-channel** needs operation identity, lifecycle state, polling, retention, and a policy
for what a browser may infer after it loses the original response.
- An **async in-memory job** separates request receipt from result delivery but still loses work on a
bridge restart; adding status/recovery semantics turns it into a tracker rather than a simpler call.
- **Durable or resumable upload** requires audio storage, cleanup, ownership, replay and retry rules,
and a new privacy boundary for the most sensitive payload in this feature.
- A **realtime transport** adds connection lifecycle, framing, codec/backpressure and partial-result
contracts even though the product wants one completed file and final text.

None solves a current user requirement, and all would expand the bridge's state, failure, and security
surface beyond the existing same-origin, write-gated BFF and one configured provider call.

## Decision

**Keep voice transcription synchronous and one-shot.** A pane-local browser operation records a
completed clip, makes one bounded request through the existing same-origin, write-gated bridge, and
receives final text for the ordinary editable draft. Do not add a status endpoint, async job, durable or
resumable upload, or realtime transport for this flow.

## Consequences

- There is no audio, operation, retry queue, or recovery record across an interrupted upload, page
cancellation, or bridge restart. The operator records again after failure; a successful transcript
retains only the existing editable browser draft semantics.
- The size-aware total deadline supports a bounded slow-but-progressing uplink. It does not promise
completion through a long interruption or below the accepted uplink floor. MediaRecorder receives a
codec-aware bitrate **hint**, not a guaranteed bitrate or a new quality/acceptance contract.
- Health domains stay split: root snapshot freshness, pane freshness, and fresh-only Herdr state are
independent loader facts; voice phases and the pane write lock are local operation state. A pending
voice request never becomes a global connection or reconnecting signal.
- Network topology and security posture do not change: no listener, front door, browser provider
credential, or realtime channel is added. The same-origin write gate, server-held provider
credential, and metadata-only privacy boundary remain in place.

## Revisit

Revisit only when product requirements actually need partial results, status after a lost response,
background completion, or recovery across restart/interruption. Any such requirement should choose its
persistence and security model explicitly rather than silently growing this one-shot path.
2 changes: 2 additions & 0 deletions .adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,5 @@ A superseded ADR is never deleted or edited into agreement with the present. Mar
| [0008](./0008-collie-does-not-run-a-terminal-emulator.md) | Collie does not run a terminal emulator | Accepted |
| [0009](./0009-a-generic-menu-is-driven-by-the-keys-it-names.md) | A generic menu is driven by the keys it names, never by digits | Accepted |
| [0010](./0010-long-sends-are-verified-via-the-paste-placeholder.md) | Long sends are verified via the paste placeholder, not by chunking them | Accepted |
| [0011](./0011-one-openai-compatible-transcription-endpoint.md) | Voice transcription uses one OpenAI-compatible endpoint | Accepted |
| [0012](./0012-synchronous-one-shot-voice.md) | Voice remains a synchronous one-shot BFF | Accepted |
30 changes: 28 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Collie configuration. Copy to your plugin config dir as `.env`:
# Collie configuration. For a Herdr-installed candidate, copy this to its protected plugin config:
# cp .env.example "$(herdr plugin config-dir herdr.collie)/.env"
# Do not put provider credentials in the checkout's ignored `.env`.
# All values are optional; the defaults suit a single-user, tailnet-only setup.

# --- Networking ---
Expand All @@ -10,7 +11,8 @@ COLLIE_HOST=127.0.0.1
# How the bridge is published on the tailnet (read by collie-ctl.sh when it runs `tailscale serve`,
# not by the bridge itself): "https" (default — tailnet :443, Tailscale-managed cert) or "http"
# (plain HTTP on :$COLLIE_PORT — for Headscale / `.internal` domains without HTTPS certs; then set
# COLLIE_PUBLIC_HOSTS below, and note PWA install + Web Push need a secure context).
# COLLIE_PUBLIC_HOSTS below. PWA install, Web Push, and remote browser microphone capture need HTTPS;
# localhost/loopback may be treated as trustworthy by the browser).
# COLLIE_SERVE_MODE=https
# Skip tailscale serve entirely (set to 1 when using a reverse proxy like Caddy/Nginx).
# The bridge stays on 127.0.0.1 only — your proxy handles TLS, auth, and public access.
Expand Down Expand Up @@ -95,6 +97,30 @@ COLLIE_SUBMIT_KEYS=Enter
# COLLIE_SERVE_MODE=http (no TLS = rebinding is otherwise same-origin). Unset = legacy behavior.
# COLLIE_PUBLIC_HOSTS=herd.your-tailnet.ts.net

# --- Voice transcription (optional; disabled until you uncomment a setting) ---
# This root dotfile is a template: after copying it to the protected plugin config, uncomment and fill
# a setting below. Any nonblank setting opts in; an omitted model resolves to Collie's default,
# gpt-4o-transcribe. The browser sees only the resulting capability boolean; model, endpoint and key
# stay in this protected external .env and are read at bridge startup. Official OpenAI requires a key.
# COLLIE_TRANSCRIPTION_API_KEY=your_openai_api_key
# COLLIE_TRANSCRIPTION_MODEL=gpt-4o-transcribe
# Optional OpenAI-compatible base URL, including its API prefix. Custom/local endpoints may omit a
# key; set a model override when they do not support Collie's default. `http:` remains accepted for a
# trusted loopback/private service or independently encrypted transport, but remote plain HTTP exposes
# audio and any key to that network — prefer HTTPS. This remains outbound only, not a second front door.
# COLLIE_TRANSCRIPTION_BASE_URL=https://transcription.example.invalid/v1
# To disable after enabling, clear or comment ALL THREE COLLIE_TRANSCRIPTION_* settings, then run:
# herdr plugin action invoke restart --plugin herdr.collie
# Removing only the key leaves a configured keyless custom endpoint enabled.
# A recording is capped at 5 minutes / 8 MiB (WebM or MP4). Browser Fetch sends it once to the
# same-origin bridge and refuses redirects; the bridge forwards it once to the provider, with a 60s
# provider timeout and no retry. duration_ms is browser-reported lifecycle metadata, not parsed media
# duration. Only provider success and error bodies are capped at 256 KiB decoded; bridge-to-provider
# redirects are refused separately. Collie does not intentionally persist or log audio or provider
# bodies; no audio file is written under stateDir. A successful transcript becomes the ordinary
# editable browser draft (see README for its retention). Provider and browser/Bun/OS/proxy buffering
# remain outside Collie's guarantee.

# --- Web Push (optional) ---
# Generate with: bunx web-push generate-vapid-keys (after `bun add web-push`)
# COLLIE_VAPID_PUBLIC=
Expand Down
Loading
Loading