Repository navigation
levain 0.6.6
The cockpit chat panel, with a consent box that asks you to approve the tool call itself and binds the approval to it. BREAKING for chat API clients: approve needs the current decision id.
Added
- A chat panel in the cockpit for
levain serve --chat. Enter the token the server printed (held in the page only, never stored), pick an entity, open a session, send a turn, and see the reply and tool activity. - What a held action asks you to approve is the tool call itself. For each held action, the consent box (and the REPL's held-action line) shows the tool name and the call's arguments as the OpenHands runtime stored them: the mapping it builds the action from, not a rendering of the built object. The two keys the model may add to any call and the runtime drops before building (its own
security_riskrating and itssummary) are not shown, because they are the model's account of itself;summarystays shown when the tool takes it as an input (an MCP tool whose schema declares it). A one-line short form parsed from the same text is shown beside it, never instead of it. Every member of the held batch is listed, inert ones (finish,think) included, because approving runs them all. Printable ASCII is shown as itself and every other character (tab, CR, every non-ASCII letter, emoji and look-alike such as a Cyrillicа) as\u{XXXX}, with a backslash shown as\\, so two different calls never look the same. A call that cannot be read is marked "not shown in full" and can only be rejected (409undecidablefrom the server; the REPL rejects too). - An approval binds to the bytes it was shown. When a turn halts, the server records a SHA-256 over the ordered event id, tool name and full stored arguments of every held action. At approve it reads that again from the live conversation and refuses with 409
stale_decisionif anything differs or cannot be read; nothing runs, the decision id is spent, and the hold can then only be rejected (GET /chat/session.jsonstill returns itspendingset, without adecision_id). The digest itself is never sent to a client. - After an unclear outcome the panel never offers a decision on its own. A turn or decision request that does not come back as a 202 with a job, lost contact with a running job, or a job the server no longer holds all read as "the outcome of the last turn/decision is unknown; the previous request may already have run", with compose blocked and one "Re-read the session" button. Only a real click on it reads the session, and the warning stays attached to whatever that read shows: a fresh consent box if the session is still held, or an idle session ready for the next message. An error response is treated the same way, the server's own
409 stale_decisionincluded, because nothing the page receives proves who wrote it (a proxy can answer with a JSON error after forwarding the request). A token refusal after the page sent a change (open, turn, decision, close) or while it follows one also lets go of the session and says the outcome is unknown, never just "not accepted".GET /chat/session.json?id=<session_id>returns the currentdecision_idand thependingset for a gated session. - Known limits: an action read back from a persisted conversation event file is trusted to match its stored call, so anyone who can edit the operator's conversation files is outside what the consent box defends; the box defends against the model's arguments, not against installed tool code (a tool runs its own code with whatever arguments it is given). Non-ASCII text is shown escaped and backslashes are doubled (the price of an unambiguous display). A reloaded page cannot resume a gated session, because the session id lives only in the page (nothing runs, and the hold stays for the API or the REPL). Any token holder with a session id may reject it. The open and close requests still read an error as a definite refusal (an open that did run leaves a session the page does not show; it ends with the server), and a decision response that arrives after its session was closed in the page is not discarded (it can write an error line into the next session on that page; nothing is approved by it).
Changed
- BREAKING (chat API):
POST /chat/approverequires the current decision id inexpect. There is no approve-by-session-id-alone path, for the panel or any API caller: a missingexpectis refused with 400decision_id_required(and does not spend the hold), and a stale or wrong one is 409stale_decision. Read the id from the halting turn's result (decision_id) or fromGET /chat/session.json?id=<session_id>while the session is gated.POST /chat/rejectstill accepts a missingexpect(rejecting runs nothing); one that names an id must match. Clients that approved by session id alone (the 0.5.4 contract) must be updated.