Chat agent decisions (AGT)#
Decisions about what the chat agent may do, what it is shown, and how a turn is driven. Cost and context shaping live in chat context; sandbox delegation in sandbox.
Primary code: reporting/services/chat_graph.py,
reporting/services/chat_orchestrator.py, reporting/services/mcp_runtime.py,
reporting/services/headless_chat.py, reporting/services/agent_run.py.
AGT-001 — Chat tools fail closed#
Applies to: mcp_runtime.list_tools_for_user(chat_safe_only=True)
Chat sees only built-ins whose required_permissions are all in
_CHAT_SAFE_PERMISSIONS (read/inspection only), plus mutating built-ins that
carry an action-confirmation resolver.
Why: a newly added write or delete tool is hidden from chat by default rather than exposed by default. The failure mode of the alternative is silent.
The exceptions are documented at the tool registration, and there are only two:
reports__create— creates a new private report and modifies nothing existing. It still carries a conditional resolver for the one case where it publishes (filing into a space).sandbox__delegate— isolation is the control; see SBX-009.
reports__clone is gated unconditionally, because whether it publishes
depends on the source’s placement, and reading that is the resolver’s job rather
than the handler’s.
Don’t: add a third exception without recording it here and at the registration.
AGT-004 — Thread ids are namespaced server-side#
Applies to: chat_graph.namespaced_thread_id
user:{user_id}:thread:{client_thread_id}.
Why: clients supply the thread id, so without server-side namespacing one user can reach another’s thread. Everything downstream — checkpoints, the sandbox resume id, session memory — inherits this isolation, which is what makes those safe to persist.
AGT-005 — Report configs are validated at save time#
Applies to: CreateVersionRequest.validate_report_config
Configs are validated against the Report schema when saved, and markdown
panels must carry content in markdown.
Why: without it an agent stores a config that renders empty and gets no signal. An actionable error at save time is the difference between a retry and a silently broken report.
AGT-006 — Headless runs execute as the schedule’s creator, with permission-based bypass#
Applies to: reporting/authnz/headless.py, reporting/services/agent_run.py
Identity: resolve_stored_user() rebuilds the creator’s permissions from the
last role claim seen on an authenticated request (User.role, synced by
get_or_create_user). Archived users hard-stop.
Confirmation bypass is gated by chat:bypass_permissions (Editor+).
call_tool_for_chat(bypass_confirmations=True) re-checks the permission and
AUDIT-logs every bypassed execution. Without it, headless runs keep the normal
confirmation flow, so mutating tools fail closed for the run. Creator RBAC and
chat_safe_only always apply.
The same permission gates the chat UI’s “Bypass confirmations” toggle
(ChatStreamRequest.bypass_confirmations, 403 without it, default off).
Headless turns get a system-prompt addendum (_HEADLESS_PROMPT_ADDENDUM)
telling the model nobody can answer: don’t ask for confirmation, follow the
skills, summarize blocks instead of retrying.
Don’t: weaken any of this without explicit sign-off. A headless run is an unattended agent holding a real user’s permissions.
AGT-007 — Scheduled chats run on Temporal, and their sessions are not chat sessions#
Applies to: reporting/services/chat_schedules.py,
reporting/temporal_workflows/scheduled_chat.py
Execution is Temporal-only; the polling worker is gone. Run sessions are created
with origin="scheduled" + scheduled_chat_id, excluded from
list_chat_sessions, listed via list_scheduled_chat_sessions, and
POST /api/v1/chat/stream rejects them (403).
Why the rejection: a scheduled run’s transcript is a record of what an unattended agent did. Allowing a user to continue that thread interactively would blur which turns were unattended.
Schedules are per-user (created_by owner; list/get/update/delete are
owner-scoped, 404 otherwise). chat:schedule:read_all (Admin) unlocks viewing
every user’s schedules; mutations stay owner-only.
Overlap is ScheduleOverlapPolicy.SKIP with no mutex, unlike
ConfiguredWorkflow, and runs use maximum_attempts=1. Disabling pauses the
Schedule rather than deleting it. Monthly specs over-fire on days 28–31, and
load_scheduled_chat drops the non-matching firings via schedule_due.
POST /chat/schedules/<id>/run is keyed on run_requested_at so a recovering
reconcile pass is idempotent.