AGENTS.md@apps/lemon_control_plane · git:20260927.6027bc7 · 2026-09-27 · sha256 96ccfe52ccc7bee8

AGENTS.md@apps/lemon_control_plane git:20260927.6027bc7A

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

# LemonControlPlane contributor guide

The control plane is Lemon's authenticated HTTP/WebSocket API and optional A2A
network edge. It delegates domain operations to existing services; it is not
another session store, scheduler or agent runtime. See [README](README.md).

## API ownership

- `Methods.Registry` and protocol schemas define the retained RPC surface.
  Add methods only for a real client; migrate callers when removing a method.
  Do not maintain a second prose method catalog or advertise unavailable APIs.
- A method declares authorization scope, validates bounded parameters and
  delegates to the owning service. Keep read, write, admin, approval and pairing
  capabilities distinct. Node connections are not operators.
- Handlers run inline unless slow external/human-approval work explicitly uses
  async dispatch. Async handlers use `ctx.conn_pid`, not `self()`, as the
  authenticated connection identity; approval events must continue flowing.
- Runtime clients consume the advertised `features.methods`/events. Heartbeats
  use ordinary cron jobs and the automation heartbeat policy, not another RPC
  family or session-liveness store.

## Authentication and execution nodes

- Operator tokens are required by default and compared in constant time.
  `LEMON_CONTROL_PLANE_ALLOW_UNAUTHENTICATED_LOOPBACK` is explicit, default-off
  compatibility for actual direct loopback peers only. Use the socket peer
  supplied by the HTTP router; never trust address/role fields in request params.
- After `connect`, the server returns `hello-ok`; subsequent requests use the
  established auth context. Stored identity determines token role/scopes.
  Unknown identity types fail closed, never acquire operator privileges.
- Pairing requires operator authorization. A one-time challenge exchanges for a
  bounded node session. Keep the exchange credential out of status projections.
- Worker session and recovery credentials bind durable node ID and exact
  controller URL. Re-pairing an expired session recovers the same identity and
  revokes old sessions/live sockets. Legacy repair is explicitly authorized.
- Off-loopback controllers require `wss://` unless plaintext is explicitly
  permitted over a verified encrypted overlay. Prefer token env vars to flags;
  credentials never enter URLs, status, logs or errors.
- Named execution accepts only JSON-safe execution data. Provider credentials,
  callbacks and source process state stay local; cwd/credentials resolve at the
  destination. Cancellation/steer/redirect bind invocation, run and authenticated
  session. Report acceptance only after destination acknowledgement; stale or
  disconnected sessions and terminal races fail closed.
- General event fanout excludes nodes; nodes receive only targeted node traffic.
  Non-node clients receive the general event stream; there is no per-connection
  subscription API or durable replay cursor.

## Mutations and management

- `:outcome_unknown` is duplicate-risk, not a retryable rejection. Profile chat
  retains the caller-generated run ID and returns a bounded uncertain receipt
  with `retrySafe: false`. Never inspect raw failure terms into wire responses.
- Sessions use `LemonCore.SessionLifecycle`: bounded/redacted reads, exact-key
  delete confirmation, and prune previews bound to the exact candidate set and
  millisecond cutoff. Do not manipulate raw run/index tables in handlers.
- Profiles derive paths from validated IDs in `ProfileStore`; chat uses the
  canonical profile session and router path.
- Provider, memory and blueprint mutations keep their owning service's revision
  and exact-confirmation contracts. Catalog clients submit bounded bundle IDs,
  never arbitrary paths. Projections exclude prompts, skills, URLs, raw paths,
  credentials and arbitrary errors.
- Long-running work remains in the runtime. One-shot CLI clients must not create
  a second router/scheduler or mutate lifecycle stores locally as a shortcut.

## A2A

- A2A peers are independent principals, not operator sockets or execution nodes.
  Core owns protocol/client/persistence helpers; this app owns authentication,
  cards, JSON-RPC/SSE framing, rate limits and supervised task attachment.
- Scope all task reads/mutations to authenticated peer identity. An inbound
  `messageId` durably replays the original task for the same peer, and conflicts
  across peers; never submit a second run for a replay.
- Terminal transitions are first-writer-wins. Accepted cancellation cannot be
  overwritten by delayed finalization or append a post-cancel context turn.
  Runner leases guard terminal writes against reclaimed workers.
- Unknown submissions reconcile the fixed run ID for a bounded interval and
  retain nonterminal reconciliation if unresolved. Later reads may reconcile
  from durable run state. An ambiguous abort is not proof of cancellation.

## Verification

From the umbrella root: `mix test apps/lemon_control_plane/test`.
Use isolated method fixtures plus real WebSocket scenarios for authentication,
role separation, approvals and targeted events. Establish the suite's supervised
Registry/ETS baseline after other apps stop shared runtime applications; restore
only the application set a test owns. Exercise stale confirmations, replay and
ambiguous mutation outcomes, not only registry membership.