AGENTS.md@apps/lemon_web ยท diff
git:20260927.6027bc7 to git:20261014.c236208
22 added, 11 removed. Audit A to A.
# LemonWeb contributor guide
- The assistant's Phoenix LiveView app provides chat/resume and authenticated
- management. It calls shared runtime/lifecycle services, not a Web-only router,
- store or settings implementation. See [README](README.md) and
+ 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
- - Optional chat auth and required management auth are different pipelines. No
- configured token may allow local chat, but required management returns 503.
- Never move management routes into the optional pipeline.
- - Valid bearer/query auth establishes a hashed session marker. Query-token login
- redirects server-side to the same path with only the token removed before
- rendering; JavaScript cleanup is too late for history/referrer leakage.
- - Production cookie signing needs the configured secret key base. Avoid putting
- credentials, private paths or raw service failures into assigns, logs or HTML.
+ - 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.