Skip to content

Chat

Chat is the browser client, served by the q15-web container. It authenticates exactly one owner with WebAuthn, keeps the passkeys and sessions in its own store rather than in the agent’s memory, and keeps no transcript of its own: what you read in the browser is the agent’s transcript, read back to you.

Chat is the app you look at: streaming replies that arrive as they are written, the tool activity behind them, paged history, and attachments in both directions. The route list is in the reference.

The app is a web app, so install it from the browser’s menu and it behaves like one you installed yourself: full screen, no browser chrome, and a passkey tap instead of a login.

On Android it also registers as a share target. A scan from a document scanner, or a photo from the gallery, can be shared straight into q15 and arrives in the composer’s attachment tray, already sealed and ready to send. Chrome is the browser that supports it today, and the app has to be installed, not merely open in a tab.

A turn in the browser client: the question, a tool call with its arguments rendered as fields, and the answer.

Enrollment is a host operation, including for the first device. The commands use podman compose; on Docker they are the same with docker compose.

Terminal window
podman compose exec q15-web /usr/local/bin/q15-web auth enroll 'Laptop'
podman compose exec q15-web /usr/local/bin/q15-web auth list
podman compose exec q15-web /usr/local/bin/q15-web auth revoke DEVICE_ID

The enroll command prints an enrollment blob and waits. You open the locked page in a browser, paste the blob into its enrollment panel, create the device credential, and paste the one-line response back into the waiting host command within five minutes. Only the host command submits the registration, over a private Unix socket, so an origin that can run a browser ceremony still cannot enroll itself. Enrollment can be run over SSH from the device you are enrolling.

Two constraints matter:

  • Every credential requires user verification — a PIN or a biometric check.
  • Synced and backup-eligible credentials are refused, because revoking a key that several devices share cannot revoke one physical device. A device-bound platform authenticator or a security key with a PIN is what works.

There is no recovery password. Host access is the recovery path: enroll a spare authenticator in advance, and if one is lost, enroll a replacement and revoke the lost device.

A session cookie alone grants nothing. Each HTTP request and each socket upgrade must carry a fresh proof signed by a session key that lives in the browser and is not exportable, so a copied cookie is not enough to impersonate a device. Reloads reuse the session key without another passkey gesture.

Sessions expire server-side after 30 days, with no sliding renewal; signing out deletes the session server-side. Revoking a device deletes its credential and all of its sessions at once, and other devices are unaffected. The hard limits are 32 devices, 128 sessions and 64 pending login challenges. A login ceremony expires after two minutes and an enrollment ceremony after five.

Everything that carries content is sealed between your browser and the agent: chat bodies, reasoning, tool arguments and results, live deltas and replayed history. A passive carrier gets ciphertext and frame sizes, and q15-web itself holds no key and parses nothing. The algorithms, the key derivation and the one cap that applies to attachments are in the reference.

The web tier keeps exactly one private directory, q15_web_state mounted at /var/lib/q15-web: the 0600 credential store and a private admin socket. It holds no transcript. Keep it across updates and rebuilds, because deleting it means enrolling every device again, and read the restore warning in Backups before reusing an old copy. The bridge socket it dials lives on a second volume, mounted in the agent and the web tier only. Both are in the volume table.

These are the shipped tier’s own stated limits, and they are worth reading before you point a domain at it:

  • A TLS terminator sees cookies and routing metadata. Sealing hides content from a passive carrier. It does not hide who is talking, or when, or how much.
  • It is not end-to-end encryption from the edge. An origin or relay that serves modified JavaScript, or that replaces the key exchange, is outside the guarantee. Authenticating the code your browser runs would need a separate mechanism.
  • Host administrators and the web process itself are trusted. Access to the admin socket or a writable state directory can enroll an identity.
  • A compromised browser, operating system or authenticator is out of scope.
  • It bounds work and state, not availability. The authentication budget limits how much work a client can cause; it is not a promise that the service stays up under a distributed attack.

See Security for how this fits into the rest of the architecture.