GuideIn-Conversation Widgets

In-Conversation Widgets

Agents sometimes need something from you mid-task — an API key, a confirmation, a choice between options. Without widgets, the only way to get that input is for the agent to ask in chat and for you to paste a value back. That breaks two things at once: your flow (the agent now needs to wait, you need to context-switch) and the security guarantee (the value lands in the model’s context window, in logs, in transcripts that survive).

Widgets fix that. When the agent calls a widget tool, a small form renders inline in the transcript. You fill it out. The values flow directly browser → daemon — they never reach the agent. A sanitized “[Agor] User submitted X” prompt is auto-queued into the agent’s next turn so it can pick up where it left off.

Widgets are a primitive. v1 ships one widget type — env_vars — as the canonical example. Future widget types (confirmations, OAuth connect, MCP-server picker, file picker) use the same plumbing.


The env_vars widget

This is the widget the agent calls when it needs an environment variable it doesn’t have. Typical use: an MCP integration needs HUBSPOT_API_KEY and the agent doesn’t want to fail with “please go to Settings”.

What the agent does

The agent calls the MCP tool agor_widgets_request_env_vars:

{
  "names": ["HUBSPOT_API_KEY"],
  "reason": "Needed to call the Hubspot Private Apps API.",
  "instructions": "Get a key at https://app.hubspot.com/private-apps. Make sure the **CRM > contacts > read** scope is enabled.",
  "default_scope": "global"
}

The tool returns within milliseconds. The agent ends its turn voluntarily. No HTTP timeout, no executor pause, no daemon-side await.

What you see

A small form renders inline in the transcript:

  • The variable name(s) the agent is asking for
  • A short reason
  • An optional instructions block (rendered as sanitized markdown — links and emphasis only, no scripts)
  • A password input per variable
  • A scope selector (Session / Global)
  • Dismiss and Save & continue buttons
  • A disclaimer: “Type values here, never paste them into chat.”

When you hit Save & continue, the value goes directly from the form to the daemon via POST /widgets/:widget_id/submit. It does not traverse the model. The daemon writes it through the same encrypted-at-rest path as Settings → Environment Variables (AES-256-GCM via the daemon’s master secret).

What the agent sees next

A new user-role message arrives in the agent’s next turn:

[Agor] User submitted HUBSPOT_API_KEY (scope: global). You can now retry the operation that needed it.

The agent retries. Only the name appears in that prompt — the value is read from the encrypted store at executor spawn time, as if you had set it via the Settings panel.

The already_present short-circuit

If you already have the requested variables set in the chosen scope, the widget skips the form entirely. The transcript shows a ”✓ already configured” badge and the agent auto-resumes with:

[Agor] HUBSPOT_API_KEY was already configured. You can proceed.

One fewer click in the common “agent didn’t know I had this” case.

Dismissal

If you hit Dismiss, the widget marks itself dismissed and queues an explicit prompt into the agent so it doesn’t loop:

[Agor] User dismissed the request for HUBSPOT_API_KEY. Do not re-request immediately — ask whether to proceed without, or move on to other work.

Security guarantees

The widget primitive has one hard requirement: submitted values never enter the LLM’s context. Triple-checked:

PathCarries values?
Agent → MCP tool callNo — input schema only accepts names + reason + instructions
Widget message rowNo — the row stores params (names, reason, …) and lifecycle state
MCP tool return valueNo — returns { widget_id, status } only
Browser → daemon submitYes — direct HTTP, authenticated, never traverses the agent
Daemon → users serviceYes — encrypted at the boundary, never logged
Auto-resume prompt to agentNo — built from sanitized result_meta (just names + scope)
widget:resolved WebSocket eventNo — same sanitized payload
Transcript reloadNo — the row never held values

The disclaimer text on the form (“Type values here, never paste them into chat”) is there for one specific reason: if the agent goes off-script and asks you to type a value into chat instead of into the widget, don’t. The widget is the only path that preserves the no-LLM-context guarantee.

For the full security analysis, see docs/internal/in-conversation-widgets-design-2026-05-19.md.


Transcript persistence

The widget message persists in the transcript with its final state:

  • submitted → ✅ + names + scope + timestamp
  • dismissed → ⊘ + names
  • already_present → ✓ + names + “already configured”

The names of submitted variables are visible to anyone who can read the transcript. The values are not, and never were. This is the same surface as Settings → Environment Variables, which also shows names without values.

If a particular request is sensitive enough that even the name shouldn’t appear in the transcript, dismiss the widget and add the variable via Settings.


Multi-user behavior

When worktree_rbac: true and someone other than the session creator submits the widget (under others_can: prompt), the env var lands in the session creator’s identity — that’s whose env the agent will read at retry. The submitting user is recorded in result_meta.submitted_by for audit.

This is consistent with dangerously_allow_session_sharing: false (the default-safe posture). For the full multi-user story see Multiplayer Unix Isolation.


Why widgets, not chat?

Three reasons:

  1. Secrets stay out of the model. The values flow directly to the daemon’s encrypted store. The agent never sees them.
  2. No flow-breaking context switch. You don’t have to leave the conversation to go set up Settings → Env Vars.
  3. Async-friendly. The widget is durable. Submit it now, submit it in an hour, submit it tomorrow — the session picks up wherever it was, on whatever device. Slack and gateway sessions work the same way.

This is the v1 instance of a broader primitive. Future widgets — confirmations, OAuth-connect, MCP-server picker — use the same machinery and the same security guarantees.

BSL 1.1 © 2026 Maxime Beauchemin