AGENTS.md@apps/lemon_web · git:20261014.c236208 · 2026-10-14 · sha256 efd05643ecbc13f6

AGENTS.md@apps/lemon_web git:20261014.c236208A

Immutable. This exact content never changes and is served at /api/v1/blob/efd05643ecbc13f6.

# LemonWeb contributor guide

The assistant's Phoenix LiveView app is an operator client of the control plane:
LiveViews call `LemonControlPlane.Methods.Registry` in-process through
`LemonWeb.Rpc` and project Bus events with `LemonControlPlane.EventBridge.project/1`,
so they see the same schemas, scopes and bounded payloads as the TUI. Where a page
uses a lifecycle service directly it is the same shared service, never a Web-only
router, store or settings implementation. See [README](README.md) and
[user guide](../../docs/user-guide/web.md).

## Chat lifecycle

- `/` creates an isolated tab session; `/sessions/:session_key` resumes a validated
  key. Derive agent identity from that key, not an independent client claim.
- Subscribe only on connected mount. Scope session events and approvals to the
  session key. Preserve streaming/final dedupe so completion does not append the
  same answer twice. Transcript streams are bounded.
- Setup readiness fails closed before uploads are consumed, messages appended or
  router calls made. Setup mutations belong to shared CLI/provider services.
- Private chat resume may use `SessionLifecycle.history(..., redact: false)` only
  in-process. Management inspection, download and JSON-RPC history stay redacted.
- Reconcile active run on mount and before active controls. Only exact `:none`
  means idle; errors/unexpected responses are unavailable. A later lookup failure
  preserves known running/stopping state and the draft rather than falsely idle.
- Follow-up/steer/redirect use existing queue modes. Bounded guidance is text-only.
  Successful guidance keeps the original active run as stop target, not the newly
  returned submission ID. Refusal preserves drafts and exposes fixed safe text.
- Uploads retain configured count/size limits; do not submit while uploading.
  Display filenames, not managed filesystem paths. Keep readiness checks before
  consuming upload entries.

## Authentication

- Every route except `/login` and `/healthz` requires operator access, checked by
  `LemonWeb.Plugs.RequireOperator` per request and `LemonWeb.LiveAuth` on every
  LiveView mount (including the connected mount). There is no optional pipeline.
- The only credential is the control-plane operator token
  (`Authorize.configured_operator_token/0`). Login posts it from a form and
  delegates the comparison to `Authorize.from_params/2`; non-operator session
  tokens are refused. The session stores only a marker derived from the configured
  token, so rotation logs every browser out. Never accept tokens in query strings
  or headers for browser routes; `/login#token=` is read client-side only.
- No configured token fails closed (503) unless tokenless loopback compatibility
  is explicitly enabled and the actual socket peer (`conn.remote_ip` / LiveView
  `peer_data`) is loopback. Never consult forwarded headers. Failed logins are
  throttled per peer.
- Production cookie signing/encryption needs the configured secret key base.
  Avoid putting credentials, private paths or raw service failures into assigns,
  logs or HTML.

## Management boundaries

- Sessions use `SessionLifecycle` for bounded/redacted list/history/export and
  lifecycle changes. Prune previews default archived-only/unpinned, show exact
  candidate keys, keep confirmation server-side and invalidate on mutations.
- Provider settings use `LemonAgent.ModelRuntime.ProviderConfiguration`. Preview
  precedes every write; apply checks a server-held revision, destructive actions
  require exact confirmation. Credentials use password fields and filtered logs;
  never retain secret values in socket drafts. Re-entry hashes bind preview.
- Profiles use `ProfileStore` and sanitize before assignment. IDs, names, model/
  node/status/availability and canonical keys are allowed; paths/system prompts
  are not. Clone/rename/delete recheck opaque revision; delete also confirms exact
  ID and inherits trash-first rollback. Refused/stale writes keep safe drafts.
- Memory uses `LemonMemory.Lifecycle`: only bounded Safety-redacted summaries,
  allowlisted provenance, counts and digests. No raw URLs/workspace keys/paths,
  provider details, secret names or store errors. Delete previews bind exact
  document ID and every persisted-field revision; transactional stale refusal
  mutates neither document nor FTS row.
- Blueprints use `LemonAutomation.Blueprint.Catalog`; Web never reads manifests,
  copies skills, resolves paths or writes cron stores. State admits bounded IDs,
  counts, parsed schedule/action fields, audit enums and digest, not free-form
  names, prompts, skill bodies, commands, env values or paths. Activation requires
  retyping the fresh exact digest; drift clears preview, replay stays unchanged.
- All service projections are allowlists, not raw maps with a few keys removed.
  Redaction is additional defense, not a substitute for narrow projections.

## Verification

From the umbrella root: `mix test apps/lemon_web/test`.
Use connected LiveView tests for bus events, `file_input`/`render_upload` for
uploads and explicit session routes for stable keys. Verify initial and live
unavailable states, stale preview refusal, private-chat versus management
redaction, query-token redirect and required auth without credentials.
For UI changes, exercise actual browser interaction and responsive rendering;
static disconnected HTML alone cannot prove subscriptions or upload behavior.