Hecate's agent_loop execution kind runs an LLM-driven loop: the model picks tools, the runtime dispatches them, results feed back to the model, and the loop continues until the model produces a final answer or hits a safety bound (maximum model calls, cost ceiling, approval gate). This document covers how the loop works, the built-in tools, the safety story, and the control surface.
For the high-level execution flow that wraps it (queue, lease, sandbox, events), see architecture.md. For the API endpoints that drive it, see runtime-api.md.
agent_loop is also the runtime behind Hecate Chat when tools are on. In
that mode Chats creates a visible task with execution_profile=chat_agent and
origin_kind=chat; the first tools-on user message starts the task, and
follow-up tools-on messages continue the latest terminal Task Run instead of
creating a new task per message. Turning tools off switches the same transcript
back to direct model chat. Turning tools on again after a direct model segment
starts a fresh task-backed segment in that transcript. Tasks remains canonical
for approvals, events, artifacts, retry/resume, and patch review; Chats is the
conversational entry point with compact activity, task/trace links, and inline
approval actions. When the selected provider supports streaming, the agent loop
streams assistant text into the conversation artifact during the model call so
Chats can show the answer before the Task Run reaches a terminal state.
Providers without streaming fall back to the normal non-streaming chat call.
When a Hecate Chat session is linked to a project, Hecate keeps the Chat UI
simple and injects project workflow guidance into the effective system prompt
for project-linked Hecate-owned turns. The prompt includes bounded project-root
metadata plus role, skill, active-work, and accepted-memory vocabulary, and
tells the model to treat planning/assignment/handoff/memory requests as
proposal-only Project Assistant intent. Project roots and skill entries are
metadata only; root files and SKILL.md bodies are not injected by the simple
chat path. The guidance does not grant Chat or the agent loop direct authority
to create/start project records, tasks, runs, chats, external-agent sessions,
or promoted memory. External Agent sessions linked to a project keep their
agent-owned prompt and receive no Hecate project workflow prompt layer.
For task-backed project-linked Hecate Chat, the agent tool catalog also includes
draft_project_proposal. That tool can create a
project_assistant_proposal task artifact containing reviewable proposal JSON
for the linked project. It does not apply the proposal or mutate project work;
Chats can open the artifact in Projects, where the normal Project Assistant
review/apply boundary still applies.
Invariant: one chat, many segments, one active task-backed loop. A single
Hecate Chat session can carry an arbitrary history of segments — alternating
tools-on and tools-off — in one continuous transcript. Within that transcript
at most one task-backed loop is active at a time. Each tools-on segment
maps to exactly one backing task; while that Task's latest Run is non-terminal
(queued, running, awaiting_approval), the chat is "busy" and the
composer's submit control reflects three states:
- idle — the regular
Send messagebutton (aria-label="Send message"). - busy with empty composer — a red icon button labelled
Stop active taskthat cancels the backing Task Run. - busy with a typed prompt — a
Queue messagebutton that buffers the text locally and replays it automatically once the backing Task Run finishes.
A yellow banner under the composer surfaces a queueing notice plus an
Open task link to the backing task in the Tasks workspace and a Stop
button (aria-label="Stop active task") that also cancels the backing Task Run. The
backend returns 409 chat.agent_session_busy if a client bypasses
the UI. Once the backing Task Run reaches a terminal state, the next tools-on
prompt either continues the same Task with a new Run or — if tools have
been toggled off and on in the meantime — starts a fresh task-backed
segment. Historical segments persist for transcript context but never
resume execution; the only loop the runtime drives forward is the active
one.
The Tasks workspace in the operator UI is the human entry point — create a task, watch its run state, approve or retry, and inspect streamed output:
Contributing here? Start at
AGENTS.mdfor the codebase map and runtime invariants; conventions, workflow, and verification ladders live underdocs-ai/.
- Loop mechanics
- Report-only QA workflow
- Built-in tools
- External MCP tools
- Workspace modes
- Output and stderr in the UI
- System prompt layers
- Approval gating
- Cost tracking
- Retry and resume
- Configuration knobs
- Common failure modes
A run with execution_kind=agent_loop walks model calls:
- The runtime calls the LLM with the running conversation, available tool schemas, and the operator-composed system prompt. If the gateway route supports streaming, assistant text deltas are captured and persisted as a partial conversation snapshot while the model call is still running.
- The model responds with either tool calls or a final answer.
- If tool calls: each call receives a durable running dispatch intent before
the tool can produce side effects. The runtime finalizes that same Step,
appends the result as a
toolmessage, checkpoints the conversation, and then advances to the next call. After the batch, the loop makes another model call. - If a final answer: the loop ends. Ordinary agent-loop tasks persist the
answer as a
summaryartifact; report-only QA tasks persist a versionedworkflow_reportthat labels the answer as agent-reported.
A tool-free assistant response qualifies as a final answer only when its
content is nonblank after trimming whitespace. An empty response is still
checkpointed with its model Step, route, usage, and cost evidence, but the Run
fails without an assistant.final_answer event or terminal artifact. Hecate
does not retry that model call automatically; retry and model selection remain
explicit operator actions. Same-Run crash recovery enforces the same invariant
instead of turning a saved empty assistant response into a successful Run.
The conversation is persisted as an artifact (agent_conversation, JSON-encoded []Message) after every model call and after every individual tool result. During a streaming model call, Hecate also refreshes that artifact with the partial assistant answer at a throttled cadence; after the provider finishes, the usual full snapshot replaces it. A crash mid-loop, an approval pause, or a deliberate retry-from-model-call therefore all start from a known state. One assistant response may contain at most 16 tool calls, and their IDs must be non-empty, unique, and at most 256 bytes; an invalid bundle fails before approval or dispatch because it cannot be recovered safely by identity or bounded operator review.
Same-run crash recovery compares each unresolved call with its durable dispatch Step. A call whose intent is still running, or whose terminal Step exists but whose exact result missed the conversation checkpoint, is never replayed automatically: Hecate settles an ambiguous running intent as failed and inserts a bounded tool-error result explaining that the side effect may already have happened. Calls in the same batch with no durable intent remain eligible, but must pass current policy and approval checks before dispatch.
Each fresh model call records the same runtime accounting regardless of whether
the provider streamed or returned one response body: resolved route metadata
(provider, provider_kind, model), the assistant thinking step,
assistant.* run events, per-model-call cost records, and the updated conversation
snapshot. Resume-after-approval iterations are different: the model has already
produced the tool calls, so the runtime dispatches those approved calls without
emitting a new model.call.started or model.call.completed.
The implementation keeps those responsibilities split so the loop stays auditable:
executor_agent_loop.goowns the high-level loop: resume detection, final answer, approval gate, tool dispatch, cost-ceiling check.executor_agent_loop_chat.goowns one fresh model call: request construction, streaming capture, route capture, assistant events, thinking step, model-call cost, conversation snapshot.executor_agent_loop_run_state.goowns in-memory run assembly: next step index, steps/artifacts, resolved route, cost totals,ExecutionResult.executor_agent_loop_conversation.go,executor_agent_loop_approval_gate.go, andexecutor_agent_loop_tools.goown conversation hydration, approval-gate decisions, and tool dispatch respectively.
sequenceDiagram
autonumber
participant Runner
participant Agent
participant LLM
participant Tools as Tools (built-in + mcp__server__tool)
participant Store
Runner->>Agent: Execute
Agent->>Store: load saved conversation if resume
Note over Agent,Tools: Work-policy-backed native Tasks reuse frozen posture and tools-off snapshots send an empty catalog and skip MCP startup
loop model-call cycle
Agent->>LLM: Chat with messages and tool schemas
LLM-->>Agent: assistant message
Agent->>Store: persist conversation snapshot
alt assistant emitted tool_calls
opt any tool gated by runtime, frozen preset, browser, or per-server MCP policy
Agent->>Store: persist approval as pending
Agent-->>Runner: pause as awaiting_approval
end
loop each unresolved tool_call
Agent->>Store: persist running dispatch intent
Agent->>Tools: dispatch tool_call
Tools-->>Agent: tool result text
Agent->>Store: finalize same Step and persist updated conversation
end
else assistant emitted final answer
Agent->>Store: persist final-answer artifact
Agent-->>Runner: status completed
end
end
Three things bound the loop:
HECATE_TASK_AGENT_LOOP_MAX_MODEL_CALLS(default8) — hard ceiling on LLM round-trips per run. Runaway-cost safety net.Task.BudgetMicrosUSD— per-task cost ceiling. Checked againstpriorCost + costSpentbefore resumed work and immediately after each model call; failing the run preserves the assistant's last message without finalizing it, requesting approval, or dispatching its tools.- Approval gates — when the model requests a gated tool, the loop pauses with
status=awaiting_approvaland emits an approval record. See below.
workflow_mode="qa" is Hecate's first built-in runbook contract. It is not a
generic workflow engine, a Project/Cairnline record, or an External Agent
command. It is available only on a native execution_kind="agent_loop" Task;
ordinary model and task requirements still apply. A QA task can either pin both
requested_provider and requested_model, or omit both to use Hecate
auto-routing before the Task starts.
Hecate owns the contract version. On creation it records
workflow_mode="qa" and workflow_version="v0" on the Task and snapshots
both fields on each Run, including retry/resume descendants. Clients do not
supply workflow_version; the stored mode and version must be a complete,
canonical pair, and unknown, non-canonical, missing, future, or partial
persisted workflow values fail closed before a workspace or executor is
selected. A QA
create rejects configured mcp_servers, native HTTP/search network access, or
an explicitly requested non-ephemeral workspace mode. The runtime then forces
an ephemeral workspace, sandbox_read_only=true, and sandbox_network=false
before the Task is stored. QA also rejects workspace reuse and binds each Run
to its generated Hecate-managed Task/Run directory; follow-up Runs get a fresh
isolated workspace rather than inheriting a prior path. The workspace
CLAUDE.md / AGENTS.md compatibility layer is explicitly excluded, so
repository guidance remains inspectable evidence rather than system-priority
instruction. For a local Git source, QA provisions that isolated workspace by
safe directory copy instead of git clone checkout and excludes every .git
entry, including linked-worktree metadata. That prevents host global Git
filters from running before the dispatcher and later source index or object
changes from becoming QA evidence.
Those stored fields are not the only boundary. The QA tool catalog and the
dispatcher both fail closed. File and artifact inspection can run:
read_file, grep, glob, artifact_read, and list_dir. The git_status
and git_diff tool names return a clear unavailable result in QA v0 without
invoking Git, so the agent can record that limitation rather than observe a
live source repository. QA blocks shell and terminal commands, all
workspace/Git writes, file_edit / apply_patch proposals,
draft_project_proposal, external MCP tools, http_request, web_search,
browser_inspect, and browser_flow. A model that nevertheless returns one of
those calls receives a policy-denied tool result; Hecate does not create an
approval or contact the blocked executor/service.
QA v0 also blocks both browser tools. Its public Task creation surface has no assignment-launch selector for either browser grant, so it must not imply a conditional capability that operators cannot actually select. A future QA contract may add constrained browser capability only with an explicit Hecate runtime selection and its own review; it would still not enable a general URL checker, typing, or retained browser state.
At Run start, Hecate writes a workflow_manifest JSON artifact containing the
versioned contract and no prompt, workspace path, credentials, or model output.
When—and only when—the agent produces a final response, Hecate writes a
workflow_report JSON artifact. Its agent_reported.summary_markdown is the
agent's narrative; it is not proof a test or browser check ran. Its
hecate_observed object separately records the enforced read-only/native
HTTP-and-search/MCP posture, the QA-v0-unavailable browser- and Git-evidence
postures, and the manifest ID.
QA v0 has no shell test runner, arbitrary test-command input, browser
automation, automatic memory candidate, patch-application path, or automatic
post-run Git-summary capture. It also has no Git evidence in v0: its Git tool
names report that the metadata-free snapshot cannot support inspection. These
limits describe
agent-exposed runtime capabilities; Hecate still performs the ordinary local
filesystem work needed to create its managed workspace.
Hecate Chat tools-on runs with text/code attachments also expose
read_attachment and search_attachment. These read private attachment storage,
not workspace paths, and are available only for the current run's admitted input.
They obey the read_file / all_tools approval families and frozen preset
policy. QA and ordinary Tasks without a scoped reader do not receive them.
read_attachment takes attachment_ref, a UTF-8-aligned byte offset, and
max_bytes (default 8192, maximum 32768). Its result contains text, offset,
next_offset, and eof. search_attachment takes the reference, a literal
query (at most 256 UTF-8 bytes), a byte offset, and max_matches (default 10,
maximum 20). It returns bounded match offsets/excerpts and a continuation
next_offset / eof; it does not interpret regular expressions or paths.
Search excerpts total at most 16 KiB before result serialization.
The API-bound reader checks immutable metadata, claim ownership, digest, session lifecycle, cancellation, and provider generation before disclosure. The current storage backends load one bounded file (at most 5 MiB) to produce a page; this is paged model context, not range-based database I/O. The run retains private tool-result bytes up to one quarter of the advertised context window, capped at 64 KiB (also the unknown-window fallback). Oldest results are replaced with explicit omissions before the next model call; oversized single results ask for a smaller page. This is not a tokenizer-based budget for the whole task. The allowance is refreshed from the durably recorded final model and clamped against admission, so a policy rewrite to a smaller model cannot retain the initial model's larger excerpt budget. A failed refresh blocks disclosure.
Raw read/search results never enter steps, trace message content, or saved conversation artifacts. Checkpoints keep file metadata and explicit omissions; completed reads are not replayed after restart, but a same-input run can request them again after revalidation. A new turn without input attachments has no such reader. Model-generated answers, search queries, and other tool arguments may quote files and retain ordinary task persistence; this is not DLP.
The agent gets its standard workspace tools by default. None require operator
config beyond the approval policies; http_request reads the network policy
from env. A configured web-search provider can add web_search; the separate
native browser capabilities are opt-in, local-only, and much narrower than
general browser automation.
Chat-origin Task Runs for project-linked Hecate Chat receive one additional
proposal-only tool.
| Tool | What it does | Policy |
|---|---|---|
shell_exec |
Run a shell command in the workspace through ProcessRunner | Omitted and fail-closed when sandbox_read_only=true; otherwise gated by shell_exec or all_tools policy (default on), with env sanitisation + output cap + timeout and optional OS wrapper (see sandbox.md) |
terminal_open |
Open a long-lived terminal process in the workspace | Omitted and fail-closed when sandbox_read_only=true; otherwise gated by shell_exec or all_tools policy (default on), using LocalWorkspace.OpenTerminal, static command policy, env sanitisation, workspace root, and optional OS wrapper |
terminal_write |
Write stdin to an open native agent-loop terminal | Gated by shell_exec or all_tools policy (default on); terminal handles are scoped to the current run and preserved across same-run approval requeues |
terminal_read |
Read the retained output tail from an open native agent-loop terminal | Gated by shell_exec or all_tools policy (default on); output returned to the model is bounded |
terminal_wait |
Wait for an open native agent-loop terminal to exit | Gated by shell_exec or all_tools policy (default on); wait calls are bounded by timeout_ms, and a timeout leaves the terminal running |
terminal_kill |
Terminate an open native agent-loop terminal | Gated by shell_exec or all_tools policy (default on) |
git_exec |
Run a Git subcommand in the workspace through the sandbox executor | Omitted and fail-closed when sandbox_read_only=true; otherwise gated by git_exec or all_tools policy (default on) |
file_write |
Write or append a file under the workspace | Omitted and fail-closed when sandbox_read_only=true; otherwise gated by file_write or all_tools policy (default on) |
file_edit |
Replace exact text in an existing workspace file | Gated by file_write or all_tools policy (default on); read-only tasks may propose a patch but cannot apply it |
apply_patch |
Apply or propose structured multi-file patches | Gated by file_write or all_tools policy (default on); emits patch artifacts; read-only tasks may propose but cannot apply |
read_file |
Read a file under the workspace, optionally by line range | Ungated by default; gate with read_file or all_tools policy. Path must resolve through WorkspaceFS within the workspace root |
grep |
Search workspace text files with a bounded regular-expression search | Ungated by default; gate with read_file or all_tools policy. Path must resolve through WorkspaceFS within the workspace root |
glob |
Find workspace paths by glob pattern | Ungated by default; gate with read_file or all_tools policy. Path must resolve through WorkspaceFS within the workspace root |
code_intelligence |
Query code semantics, diagnostics, symbols, or structural patterns | Ungated by default; gate with read_file or all_tools policy. Uses only allowlisted executables found on a trusted global gateway PATH or pinned by an exact operator path, with sanitized env, bounded protocol/output limits, and workspace-confined input/output paths. Semantic LSP calls fail closed when the task's read-only or network-denied policy cannot be enforced by the active OS wrapper |
artifact_read |
Read an inline artifact from the current task by artifact ID | Ungated by default; gate with read_file or all_tools policy. Only artifacts belonging to the current task are visible |
list_dir |
List entries under a workspace path | Ungated unless all_tools is set. Path must resolve through WorkspaceFS within the workspace root |
git_status |
Return structured branch and changed-file status | Gated by git_exec or all_tools policy (default on); immutable GitRunner metadata view with optional locks, lazy fetch, fsmonitor, global/system config/attributes, and recursion disabled; conversion attributes fail closed. QA v0 is an exception: it returns an unavailable evidence result without invoking Git. |
git_diff |
Return a bounded workspace or staged diff | Gated by git_exec or all_tools policy (default on); immutable GitRunner metadata view with external diff/text conversion, optional locks, lazy fetch, fsmonitor, global/system config/attributes, and recursion disabled; conversion attributes fail closed. QA v0 is an exception: it returns an unavailable evidence result without invoking Git. |
http_request |
Make an outbound HTTP request | For preset-backed tasks, advertised only when the preset snapshot has sandbox_network=true; unexpected calls fail closed. Then gated by network_egress or all_tools, with private-IP, scheme, and optional host allowlist checks |
web_search |
Search the web through the configured search provider | For preset-backed tasks, advertised only when configured and the snapshot has sandbox_network=true; unexpected calls fail closed. Then gated by network_egress or all_tools; endpoint and API key stay operator-owned |
browser_inspect |
Load one approved static page and return bounded, text-only evidence | Available only to Work-policy-backed native Tasks whose frozen snapshot explicitly enables browser evidence with exact origins, and only when a local executable is configured. Every call requires an agent_loop_tool_call approval even if global tool approvals are disabled. Page scripts are disabled. It is not controlled by sandbox_network, and is available to explicitly granted tools-on Hecate Chat backing Tasks. External Agent, QA, ungranted Tasks, and remote-runtime Tasks remain excluded. |
browser_flow |
Run one approved accessibility click/wait flow | Independently granted to eligible Work-policy-backed native Tasks. One approval covers the query-free start URL and fully declared 1–6 action flow; scripts and same-origin GET/HEAD may cause effects. Eligible tools-on Hecate Chat backing Tasks use the same frozen grant. No External Agent, QA, ungranted Task, or remote-runtime use. |
draft_project_proposal |
Draft a Project Assistant proposal artifact for the linked project | Available only to Chat-origin Task Runs backing Hecate Chat Turns with origin_kind=chat, execution_profile=chat_agent, and a project_id; gated only by all_tools. Creates a project_assistant_proposal artifact for operator review and does not apply or start anything. |
Tool argument schemas are JSON-Schema-shaped and surfaced to the LLM in the standard tools array on each Chat request. Bad arguments are returned to the model as a tool-result error string rather than failing the run, so the model can correct itself.
Before every model call, the code_intelligence tool description distinguishes
three current-Run facts: whether capability/structural calls require approval,
whether semantic LSP operations are permitted by the task and active host
wrapper, and whether the bounded grep fallback requires approval. The same
semantic-policy helper is used by dispatch, so a read-only or network-denied
block is visible before the model spends a call on an operation that must be
refused. The description deliberately reports provider installation as not
checked. Hecate does not silently start provider processes while preparing a
model request or bypass the read_file / all_tools approval family; the model
uses the audited capabilities operation when provider discovery is useful.
code_intelligence with operation=capabilities is permitted by the native
sandbox gate, subject to normal preset tool enablement and approval policy. It
resolves trusted executables and runs bounded gopls version, tsc --version,
and ast-grep --version probes. Results include a normalized, bounded version
token when the provider emits one Hecate recognizes, the operations Hecate may
request, and the explicit
installed_unverified stage. Capability discovery does not initialize a
language server or execute a structural query; initialization or invocation is
verified only by a real query. The other operations are available when the matching executable is installed
on the gateway's trusted global PATH or bound to an exact operator-owned
path:
A trusted ast-grep executable remains installed_unverified when only its
version probe fails because structural dispatch has no version compatibility
contract; the first real structural query remains the invocation check.
| Language / mode | Preferred provider | Operations |
|---|---|---|
| Go | gopls serve |
definition, references, hover, symbols, diagnostics |
| TypeScript / JS | TypeScript 7+ tsc --lsp --stdio |
definition, references, hover, symbols, diagnostics |
| Structural | ast-grep run ... --json=stream |
read-only structural search, including an optional contextual-pattern node-kind selector; rewrites are deliberately not exposed |
The structural allowlist covers Go, JavaScript/TypeScript, Python, Rust, Java, C/C++/C#, HTML/CSS, JSON/YAML, and Bash.
just doctor reports the local discovery state. Typical operator-managed
installs are go install golang.org/x/tools/gopls@latest, a global TypeScript
7+ installation, and npm install -g @ast-grep/cli. Pin versions in
reproducible runtime images instead of resolving latest during gateway
startup. On Windows, the current alpha requires providers to resolve directly
to native .exe files; npm-generated .cmd shims are rejected rather than
routed through a command shell.
An operator can pin exact executables with
HECATE_CODEINTEL_GOPLS_PATH, HECATE_CODEINTEL_TSC_PATH, and
HECATE_CODEINTEL_AST_GREP_PATH. Each value must be an absolute path to a
regular executable. Exact paths are useful for immutable runtime images and
layouts such as /srv/app plus /srv/tools; they are startup configuration,
not tool arguments, and still cannot point inside the active project boundary.
Hecate never auto-installs a provider. PATH discovery rejects providers inside
the marked project boundary and providers below a non-root, non-home filesystem
ancestor shared with a markerless workspace; that covers sibling trees such as
/repo/bin and /repo/node_modules/.bin. A workspace-local executable is
project code, not a trusted runtime dependency. Each semantic query starts a
fresh bounded language server process and initializes exactly one workspace.
It also receives a fresh temporary home plus private Go build, gopls, and XDG
cache locations, which are removed after the query; sanitized operator-managed
Go module and Volta locations remain available for dependency and shim
resolution.
Document-scoped operations open their requested file. workspace_symbols sends its query at
workspace scope; TypeScript also requires a representative project file to open
so the server loads the correct project, while Go can operate from its language
and workspace root alone. Hecate then performs one read request and shuts the
server down. This costs more startup time than pooling but prevents stale
overlays and leaked daemons; per-run pooling can follow measured dogfooding
data.
Successful and failed provider calls both persist bounded Task steps. Failure
steps contain operation shape, an optional bounded structural selector, query
byte length, duration, and one fixed error
category: not_configured, invalid_workspace, cancelled, timeout,
diagnostics_incomplete, provider_unavailable, provider_protocol,
invalid_request, or provider_error. They omit the source path, query text,
provider stderr, and raw protocol payload. This makes cold latency, availability, and useful-result rate
measurable during Projects dogfooding without turning telemetry into a source
disclosure channel.
Capability discovery and fixed-argv structural search remain available to
read-only tasks. Semantic operations can cause a language server to maintain
caches or inspect build metadata, so Hecate permits them in a read-only task
only under Linux bwrap, where the workspace is bound read-only. When a task
denies network, semantic operations also require a wrapper that enforces that
denial (bwrap or macOS sandbox-exec); wrapper-less hosts fail closed. A
write-enabled, network-enabled task may run them without a wrapper because its
resolved preset already authorizes those effects.
LSP line/column arguments and results are 1-based, with columns expressed as
UTF-8 byte offsets. Hecate converts those offsets to the server's negotiated
position encoding. Native request validation caps encoded UTF-8 values at
4 KiB for a path, 64 bytes for a language identifier, 16 KiB for a symbol
query or structural pattern, and 128 bytes for an optional structural selector.
The selector is accepted only by structural_search and must be one ASCII
tree-sitter node-kind token matching [A-Za-z_][A-Za-z0-9_]*; use it to select
the relevant node from a syntactically valid contextual pattern, such as
selector=call_expression with func _() { fmt.Errorf($A) } in Go. A bare Go
expression at file scope is not a valid context. JSON Schema maxLength remains
an additional model-facing hint rather than the enforcement boundary. Returned non-file:
URIs, symlink escapes, and locations
outside the task workspace are omitted and counted. That output filtering is a
disclosure boundary for tool results, not a claim that an operator-installed
language server is untrusted: on platforms without a kernel wrapper, and on
macOS where the current wrapper confines network but not reads, the executable
can still read whatever its OS account can read.
Structural search invokes ast-grep from a neutral temporary directory and
passes the validated workspace path explicitly. When present, the validated
selector is passed as one fixed --selector argument; models cannot choose
other flags. This prevents repository
sgconfig.yml discovery from loading project-supplied custom-language shared
libraries. Regular grep remains the universal fallback. It now also enforces
workspace-entry, aggregate-byte, per-file, per-line, match, and rendered-output
budgets. Grep regex/include and glob patterns are capped at 4 KiB of encoded
UTF-8, and the complete rendered response—including headers and errors—is at
most 64 KiB. Traversal admits at most 100,000 entries. Glob and grep-include
matching also share one 67,108,864-unit match-work budget across full-path and
basename checks; the reusable matcher retains only linear-in-pattern state
instead of a per-entry pattern-by-path matrix. Exhaustion is reported as
scan_limit=pattern_work_limit. A whole ** glob segment matches zero or more
directories, so **/*.go includes both root files and nested files. Grep reports
bounded
incomplete, skipped_files, and fixed-category skip_reasons metadata when
a candidate cannot be searched (for example, a file exceeds the per-file cap,
changes during its read, is non-text, or cannot be opened). Directory walking
is streamed in bounded batches and checks context cancellation between entries;
only handles confirmed to be regular files are read, with bounded reads. When
the aggregate byte budget ends inside a line, grep may retain matches from
earlier complete lines but never searches the incomplete trailing data. Search
responses and Task steps report source traversal cutoffs as scan_limit and
rendered-result truncation independently as output_truncated, so one limit
cannot hide the other. A no-match search therefore cannot materialize or read
an unbounded monorepo before returning.
For a Work-policy-backed native Task carrying an explicit
agent_preset_tools_enabled=false snapshot, that setting is the master tool
gate. Hecate sends an empty catalog, does not start configured MCP hosts, and
keeps the execution as a normal supervised Task/Run so context, route, cost,
events, and the final answer remain inspectable. If an upstream nevertheless
returns a native or namespaced MCP call, Hecate bypasses approval, records a
denied policy step, emits policy.tool_blocked with
policy=agent_preset_tools, and returns a tool error without dispatch. A
missing snapshot preserves the catalog behavior of non-preset Tasks.
The machine-generated system message states that tools are unavailable but omits the absolute workspace path because a model-only call cannot use it.
For a Task carrying an agent_preset_id launch snapshot, sandbox_network is
the master gate for Hecate-native egress. When false, http_request and
web_search are omitted from the model request. If an upstream still returns
either call, the dispatcher records a denied policy step, emits
policy.tool_blocked, and returns a tool error without contacting the
HTTP/search provider. Legacy and non-preset Tasks without a preset
snapshot retain the pre-existing native HTTP/search behavior; setting their
process-network bit is not silently reinterpreted as a new native-tool policy.
When preset network is enabled, global approval and host/private-IP policies
still apply. When the master tools gate is enabled, external MCP servers are
separately configured trusted tool sources and keep their own per-server
approval policy.
Hecate exposes two deliberately narrow local tools instead of a general browser controller:
browser_inspectcaptures static text evidence with page scripts disabled.browser_flowruns one fully declared accessibility interaction with page scripts enabled.
Both tools are available only when the gateway runs locally, the operator
explicitly enables an installed browser in Settings or configures the
HECATE_TASK_BROWSER_EXECUTABLE override, and the native Task carries a
resolved Work policy with tools enabled. This
includes standalone Tasks and native project assignments using hecate_task
or any policies, plus tools-on Hecate Chat backing Tasks using a frozen
hecate_chat or any policy. Hecate does not download or bundle a browser,
attach to an existing browser, or expose either
tool in External Agent sessions, QA v0, non-preset Tasks, or
remote-runtime mode.
Open Settings → Browser setup, select a supported installation, then choose
Enable browser. Hecate passively checks standard Chrome, Chromium, Edge, and
Brave installation locations on macOS, Linux, and Windows, including supported
per-user locations. It does not scan PATH or the current workspace. Opening
Settings does not run a candidate or enable one automatically. Enable
authorizes the displayed installation path for
future approved browser calls. It is not publisher verification, checksum
pinning, or a malware-free verdict; a browser update can change the executable
at that path. Hecate revalidates the selected canonical path before each use.
Changed installation metadata resets working evidence; a changed symlink target
requires selection again.
Setup takes effect without restarting Hecate. Configured means the browser
can be offered to eligible Tasks and Chats but has not yet completed a call in
this Hecate process. Each execution attempt freezes browser availability for its
catalog, approval gate, and dispatcher; enabling during an already-running
attempt takes effect on the next turn or execution attempt. Disable still
closes new browser admission immediately. The first successful approved call marks it Working;
there is no required Check before use. Readiness is operational evidence, not a
grant of access to any site. The saved choice is scoped to this runtime host
and follows the configured memory, SQLite, or Postgres backend. Memory state
does not survive a restart; Working evidence is always process-lifetime.
Disable browser blocks future browser admissions without cancelling calls
that have already started.
For a managed or custom installation, set HECATE_TASK_BROWSER_EXECUTABLE to
an absolute executable path before starting Hecate. This environment setting
overrides the saved choice and makes in-app edits unavailable. An invalid
override fails closed rather than falling back to discovery or saved settings.
Clear the override and restart Hecate to return control to Settings. An empty
override with no saved choice leaves browser tools disabled. If no supported
installation is listed, install one yourself or use the explicit override;
Hecate never downloads it. If a selected installation disappears or becomes
invalid, follow the readiness guidance in Settings and select an available
installation. For a launch or page failure, inspect the failed Task or Chat;
that failure does not disable the configured installation or erase earlier
successful-use evidence. Setup is local-only, including when the UI is opened in a web
browser rather than the desktop app.
The API separates local installation details from general runtime readiness; see browser settings.
The grants are independent. browser_allowed=true enables only
browser_inspect; browser_interactions_allowed=true enables only
browser_flow. If either grant is enabled, the preset must include one or more
exact browser_allowed_origins. Hecate normalizes and snapshots the two grants
and their shared origin list as agent_preset_browser_allowed,
agent_preset_browser_interactions_allowed, and
agent_preset_browser_allowed_origins. Retries and resumes keep that immutable
snapshot; enabling static evidence never implies interaction, and a later
preset edit cannot broaden an existing Task or Chat. Chat snapshots retain the
same grant names without the agent_preset_ prefix, and each new backing Task
copies those values without re-resolving the policy. Legacy Chat snapshots with
absent grants remain browser-disabled.
Both tools accept an absolute HTTP(S) start URL at an allowed origin. A path is allowed; credentials, query strings, and fragments are rejected before the tool call is persisted or shown for approval. Each call receives only the selected exact origin, even if the preset lists several. Another allowed preset origin is therefore not a cross-origin subresource destination for that call.
Every otherwise-permitted call pauses for an agent_loop_tool_call approval,
even when HECATE_TASK_APPROVAL_POLICIES is empty. The approval shows the full
query-free target. A browser_flow approval additionally shows the complete
ordered action list, warns that scripts and same-origin GET/HEAD requests
may run, and states that clicks may change the approved application. The agent
loop admits at most one browser_flow call in a single model tool-call batch,
so one approval cannot hide several separately state-changing flows.
browser_inspect disables page JavaScript before navigation, bypasses service
workers, and permits only selected-origin GET/HEAD URL-loader traffic. It
does not expose scripts, workers, WebSockets, WebTransport, WebRTC, click,
type, form submission, upload, download, clipboard, or device primitives. Its
bounded browser_evidence text/plain artifact contains a redacted final
URL/origin, title, bounded accessibility tree, bounded console lines, and
network counters. It contains no screenshot, downloaded bytes, raw CDP,
cookies, storage, or request/response body.
browser_flow accepts 1–6 actions. Each action is either wait_for, which
waits for one exact computed accessibility role and accessible name, or
click, which targets one exact role/name pair. Click roles are limited to
button, link, checkbox, radio, tab, menuitem, and switch. Hecate
does not accept CSS selectors, XPath, coordinates, arbitrary JavaScript,
keyboard input, text or secret entry, form values, uploads, downloads,
screenshots, or retained sessions.
The browser re-resolves the approved accessibility target immediately before a
physical click and fails closed if the target is absent, ambiguous, disabled,
obscured, or otherwise unavailable. Page scripts are enabled because many
applications need them to render and respond to a click. Workers, realtime
transports, downloads, popups, file choosers, and navigation or subresources
that leave the exact origin are blocked. Only GET and HEAD URL-loader
requests are admitted, but HTTP does not make those requests side-effect-free,
and an approved click can deliberately mutate the application.
The bounded browser_flow_evidence text/plain artifact contains the
approved origin, redacted final URL, title, ordered action outcomes, initial and
final accessibility snapshots, and network counters. It excludes console and
form values, screenshots, cookies, storage, raw protocol data, and
request/response bodies. If an action or policy check fails after an earlier
click, Hecate retains the bounded partial action evidence and explicitly warns
that the application may already have changed; the tool never reports that
partial flow as completed.
Task details and native Hecate Chat display these artifacts as collapsed,
plain-text evidence. Chat loads a report only when expanded, using that message's
original Task/run and artifact references. Its outcome label comes from the
linked native tool step; artifact ready means saved, not successful. Missing
legacy step provenance stays neutral. Reloading evidence is a read-only artifact
request, not a new execution or approval. Reports remain subject to existing
Task access and retention rules.
Every inspection or flow starts a new owned Chromium process tree with a fresh
temporary profile, disables the network cache, bypasses service workers, blocks
downloads, and removes the profile after process teardown. Profile removal is
retried; if it still fails, the tool fails with path-free stale-profile cleanup
guidance and does not claim that no browser state remains. The profile base is
canonicalized and must pass Hecate's known-local filesystem classification both
before creation and through the created directory's opened handle.
One HECATE_TASK_BROWSER_TIMEOUT deadline covers origin preflight, browser
startup, and the complete capture or flow. A cold browser launch may use the
remaining call budget; there is no separate shorter startup timeout and no
automatic retry. Hecate cancels after CDP observes
more than 4 MiB of aggregate streamed response data, including unknown-length
responses. That is a cancellation threshold rather than a wire-byte guarantee:
browser, socket, and peer buffers can overshoot before cancellation reaches
Chromium.
Chromedp connects through a loopback-only relay that rejects any complete
browser-to-Hecate DevTools message over 4 MiB and ends the call after 32 MiB of
aggregate inbound protocol payload. This bounds page-controlled protocol frames
before Hecate decodes them. browser_flow additionally gives its ephemeral
origin an 8 MiB storage quota and launches Chromium with audio muted. Browser
bookkeeping and in-flight writes can overshoot the storage quota slightly; none
of these limits bounds Chromium's entire child-process address space.
The launcher also disables Chromium Breakpad crash reporting. Host-managed
browser policy remains outside the temporary profile and can override broader
privacy or identity expectations, so identity-sensitive use still requires a
dedicated unmanaged runtime.
Before launch, Hecate resolves configured origins and rejects loopback,
private, link-local, multicast, and otherwise non-public addresses unless
HECATE_TASK_BROWSER_ALLOW_PRIVATE_IPS=true. Hostname origins are pinned to a
preflight-selected address for the call to reduce DNS-rebinding exposure. This
and the exact-origin request gate are application controls, not an OS-level
egress firewall or browser sandbox.
A fresh profile is also not a hard identity boundary. Machine or enterprise Chromium policy can still supply integrated authentication or client certificates outside profile storage. Do not use either tool for identity-sensitive sites unless the configured executable runs inside a dedicated unmanaged browser/container with the required OS and network controls.
When sandbox_read_only=true, the agent loop removes broad subprocess and
interactive surfaces that cannot provide a hard read-only workspace guarantee:
shell_exec, git_exec, file_write, and all terminal tools. A model-returned
call to any of them fails closed before process spawn. Structured inspection
tools remain available. Structured Git reads run as fixed, bounded, passive
commands: they do not refresh the index, invoke repository fsmonitor helpers,
load global/system Git configuration or attributes, recurse into submodules, or
lazily fetch objects, and use the offline/read-only OS wrapper where available.
The actual status/diff process reads an immutable temporary gitdir containing a
safe core-config allowlist plus snapshotted HEAD/ref/info metadata, so a source
config change cannot introduce an executable helper between validation and
use. Nested workspaces use the checkout's true top-level metadata but scope
status paths and diffs to the workspace. Because omitting an effective
clean/process driver could instead produce misleading Git state, bounded
NUL-safe attribute resolution fails closed when a scoped tracked path has an
effective or ambiguous conversion filter. file_edit and apply_patch remain
available for proposal artifacts, while their apply paths enforce read-only
policy.
That ordinary read-only posture still allows proposal artifacts from
file_edit or apply_patch. Report-only QA is stricter: its workflow filter
also rejects those proposal calls, so a QA run cannot create a new change
artifact as a side effect of inspection.
shell_exec and git_exec are bounded by the same allowlist semantics as http_request when the task has sandbox_network=true:
- HTTP/HTTPS URLs that appear as command tokens are extracted and validated.
- Hosts that parse as a private/loopback IP literal (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, link-local, multicast) are rejected unless
HECATE_TASK_SHELL_ALLOW_PRIVATE_IPS=true. - When
HECATE_TASK_SHELL_ALLOWED_HOSTSis set, only those exact hostnames are reachable. Empty = all public hosts allowed.
Enforcement is best-effort static parsing of the command string. Tools that respect the allowlist (curl, wget, git fetch, bun install, pip install, etc.) get covered; clever obfuscation (base64-encoded URLs, nc/telnet raw sockets, custom-binary egress) bypasses it. The kernel-enforced backstop comes from Layer 2 of the sandbox: on Linux with bwrap installed, and on macOS, every shell/git call runs inside a wrapper that drops the network namespace (Linux) or applies a Seatbelt profile (macOS) when the task's sandbox_network=false. See sandbox.md for the auto-detection logic and platform coverage. The default sandbox_network=false (no network at all) plus an active Layer 2 wrapper is the strongest guarantee short of a full container.
Native agent-loop terminal tools (terminal_open, terminal_write,
terminal_read, terminal_wait, terminal_kill) use the same workspace
terminal primitive as ACP terminal callbacks. Static validation happens at
spawn time for the initial command. On Unix, known detachers are also rejected
from stdin when the requested process is actually reading interactive shell or
interpreter code. Stdin used as data by a shell/interpreter script or inline
command is not scanned unless a force-interactive option keeps executable stdin
active; explicit shell stdin such as sh - and bash - is scanned. Shell
writes retain at most 64 KiB of syntactically incomplete input across calls and
fail closed at that bound, while validated complete commands leave no cumulative
history. Interpreter input retains only fixed-size token tails. This static
enforcement is best effort, while Unix process-group and Windows Job Object
ownership provide the lifecycle backstop. These tools are unavailable on
read-only tasks because interactive stdin cannot be statically proven
non-mutating. Terminal handles are scoped to the current run and wait for the
owned process unit plus output to drain. Each handle holds its own shared
workspace writer lease for that full lifetime, independently of the execution
attempt lease. They are kept alive when a same-run approval pause returns
awaiting_approval, without opening a workspace-discard window, and closed when
the run reaches a terminal state or is cancelled. Runtime shutdown fences new
handles and closes retained approval-paused handles too.
In addition to the built-ins, an agent_loop task can declare external MCP servers on the mcp_servers task field. Their tools join the LLM's catalog at run start under mcp__<server>__<tool> aliases — for example a read_file tool on a server aliased fs becomes mcp__fs__read_file. Approval gating is per-server (auto / require_approval / block), distinct from the built-in approval policy axis. Report-only QA is the exception: task creation rejects mcp_servers, and the runtime fails closed before starting an MCP host if a malformed or older stored QA task still carries one.
Use MCP when a search server needs organization-specific credentials, ranking,
or retrieval logic beyond Hecate's configured native provider. The native
web_search tool deliberately returns search results only; fetching a result
URL still goes through http_request and its HTTP policy.
See mcp.md#hecate-as-mcp-client for the full schema, transports (stdio + Streamable HTTP), secret-aware env / headers, lifecycle / caching contract, and dry-run probe.
Every run has a workspace — a directory the sandbox locks tools to. Two modes:
- isolated clone (default;
WorkspaceModeempty /persistent/ephemeral) — the workspace manager copies or git-clonestask.WorkingDirectory(ortask.Repo) to a fresh temp dir under${TMPDIR}/hecate-workspaces/<task_id>/<run_id>. Writes don't touch the source. Safe by default. - managed reuse (internal
WorkspaceReuse) — when Hecate Chat needs a new task segment after a managed Task Run, the workspace manager reuses that runtime-owned root byte-for-byte. The operator source remains isolated, while unstaged and untracked work survives the segment boundary. Public task creation cannot set this flag. in_place— the workspace IStask.WorkingDirectory. Tools write directly to the operator's source. Necessarily destructive; opt-in. Requires an absolute existing directory or the run fails up-front rather than silently flipping back to the clone behavior.
The new-task UI exposes in_place as a "Run directly in this workspace" checkbox under WORKSPACE.
Task Detail renders the run's normalized activity array as the primary inspection timeline. Hecate Chat uses the same renderer when a tools-on chat turn projects backing task activity into the transcript, so operators see the same labels in both places: tool calls, approvals, changed files, final-answer artifacts, and terminal run state.
The shared renderer intentionally separates signal from bookkeeping. High-level activity stays visible in chronological order; raw task artifacts and internal markers are grouped under Details. Task Detail opens that activity section by default because it is a run-inspection view. Chats keeps it quieter so the conversation remains the primary surface. Task Detail may additionally expose a per-row Advanced disclosure with raw activity metadata for debugging: activity id/type/status, step/artifact/approval ids, tool kind, path, timestamp, and summary payload. Failed tool rows additionally preview related stdout/stderr artifacts there, so the common "why did this command fail?" path is answerable without leaving the activity row.
Task runs capture process output as structured stdout/stderr artifacts and
typed tool.shell.* events. The operator UI intentionally treats stdout as the
primary terminal pane and only surfaces stderr when stderr exists.
That default keeps successful runs readable: many healthy CLIs write progress,
warnings, or diagnostics to stderr, and rendering an always-empty stderr panel
creates noise. When both streams exist, they should remain visually separated
(stdout first, stderr as a labeled section or toggle) rather than merged into
one undifferentiated transcript. For lower-level consumers, use the run
artifacts and tool.shell.output_chunk events; the event payload includes
stream: "stdout" | "stderr" so tooling can reconstruct or filter either
stream.
Task Detail still records whether each stream artifact exists. The run output
header shows stdout size when available, stderr available when stderr has
content, and stderr empty when the stderr artifact was created but no bytes
were captured. Failed tool rows in Task Detail's Advanced disclosure preview
stdout/stderr artifacts from the same step and keep empty stderr discoverable as
"No bytes captured for this stream." Hecate Chat keeps failed-tool diagnostics
compact: it links only non-empty stdout/stderr artifacts from the chat activity
row, shows capped inline previews for those streams, and leaves the empty-stream
confirmation to Task Detail.
Every fresh agent_loop run prepends a machine-generated system message that tells the model where its workspace lives:
Your workspace is at:
/var/folders/.../run_xxxUse this path (or paths under it) when calling tools.
shell_execuses ProcessRunner with the workspace as its default working directory;git_execruns Git from the workspace through the sandbox executor;git_diffuses GitRunner from the workspace;read_file/list_dir/grep/glob/code_intelligenceresolve relative paths through WorkspaceFS. Tool calls that target paths outside this directory are rejected — don't reuse paths from the user prompt verbatim if they fall outside the workspace.
Without this, the model would read /Users/foo/myrepo from the user prompt and use that path verbatim — landing outside the cloned sandbox and failing with escapes allowed root. The env message is environmental fact, kept separate from the operator-tunable system prompt so it can't be accidentally elided.
The operator-tunable system prompt is composed from three layers, broadest first:
- Global —
HECATE_TASK_AGENT_SYSTEM_PROMPT(env). Applies to every agent_loop run gateway-wide. - Workspace —
CLAUDE.mdorAGENTS.mdin the task's working directory, capped at 8 KiB. Read once per run unless the task setsworkspace_system_prompt_policy="exclude". - Per-task —
Task.SystemPromptset when the task is created.
Layers are concatenated with blank lines between them. Empty layers are skipped (no wasted message). The composed prompt is the second system message in the conversation; the workspace-environment message above is the first.
A standalone native Task may select a Work policy with agent_preset_id.
Task creation freezes the preset instructions ahead of the optional per-Task
instructions inside the per-task layer. It also freezes the preset execution
profile, route hints, and tool/write/network/approval/browser posture. Explicit
provider or model values in the create request win over the corresponding
preset hints. A preset model hint is ignored when the request selects a
different provider, and an effective provider without a model fails Task
creation. This path does not load Project memory, context sources, or skill
bodies and does not create or mutate Cairnline state.
Project assignment tasks set workspace_system_prompt_policy="exclude" and use
the project/preset context builder instead. That keeps project memory and
workspace-instruction bodies controlled by the resolved Agent Preset:
project_memory_policy=include can include bounded project memory bodies, and
context_source_policy=include_enabled can include bounded portable AGENTS.md
workspace-instruction bodies. Host-specific guidance files and SKILL.md bodies
remain metadata-only for this path. If the selected route is a cloud provider,
included project memory and workspace-instruction bodies are sent to that
provider as normal task prompt content. The task also snapshots the resolved
preset id, tools_enabled, and approval_policy, maps writes_allowed=false to
sandbox_read_only=true, and maps network_allowed to sandbox_network;
when the preset enables static browser evidence or browser interaction, it
separately snapshots both browser grants and their shared exact-origin
allowlist. Retries and resumes reuse those task snapshots rather than resolving
a mutable preset again.
Project-linked Hecate Chat uses a separate bounded project prelude in the chat
system prompt. It may include project identity, root metadata, role hints,
enabled skill metadata, active work metadata, and accepted project memory
bodies. Project context-source file bodies, host-specific guidance file bodies,
and SKILL.md bodies are not loaded into chat prompts in V1. External Agent
chat and External Agent project-assignment starts record project metadata for
inspection, but Hecate does not inject project memory bodies, source bodies, or
skill bodies into adapter prompts; the adapter owns any private prompt packing
inside its native session.
A Hecate Chat created with a named Agent Preset carries a separate, immutable
Chat-safe snapshot. Only presets with surface=hecate_chat or surface=any
are eligible. The snapshot's instructions are added to the Chat's effective
per-task prompt after any project prelude and before the operator's per-chat
instructions. When a permitted tools-on Chat turn creates an agent_loop Task,
Hecate copies the snapshot id and tools setting, uses its non-empty execution
profile, maps writes_allowed=false to sandbox_read_only=true, and maps
network_allowed to sandbox_network, and copies the explicit approval
posture into the backing Task's additive mid-loop approval layer. It also copies
the independent frozen browser grants and exact origins; absent legacy fields
remain ungranted. A
tools-disabled Chat snapshot cannot create a tools-on Task; it keeps the
session on the direct-model path.
This Chat contract intentionally does not resolve a live preset at run time or carry project-memory/context-source policy, skills, MCP servers, or External Agent options into the Task. In particular, approval posture does not imply either native browser grant. Later preset edits and deletion do not change an existing Chat or its backing Task. The preset is Hecate runtime state, not Cairnline coordination intent; it never creates or changes a Project, role, assignment, or handoff.
Two distinct approval flows:
Triggered when the task's execution_kind is shell / git / file (or sandbox_network=true) AND a matching policy is in HECATE_TASK_APPROVAL_POLICIES. An agent_loop task whose Agent Preset snapshot explicitly sets tools_enabled=false skips the network pre-execution gate: it advertises no tools, starts no MCP clients, and has no executable network capability to approve. The run is otherwise created in awaiting_approval status before it ever leaves the queue. The UI shows an amber banner with the command being approved. Approve → run becomes queued → executes. Cancel before approval → run becomes cancelled and the pending approval is marked cancelled so stale approval buttons cannot resurrect the run.
When the LLM calls a gated tool inside an agent_loop run, the loop pauses. Policy → tool mapping:
shell_exec→ pauses onshell_exec,terminal_open,terminal_write,terminal_read,terminal_wait, andterminal_killtool callsgit_exec→ pauses ongit_exec,git_status, andgit_difftool calls outside QA v0; QA v0 reports Git evidence unavailable without an approvalfile_write→ pauses onfile_write,file_edit, andapply_patchtool callsread_file→ pauses onread_file,grep,glob,code_intelligence,artifact_read,read_attachment, andsearch_attachmenttool callsnetwork_egress→ pauses onhttp_requestand configuredweb_searchtool callsall_tools→ pauses on every otherwise-permitted tool call (shell_exec,terminal_open,terminal_write,terminal_read,terminal_wait,terminal_kill,git_exec,git_status,git_diff,file_write,file_edit,apply_patch,read_file,grep,glob,code_intelligence,artifact_read,read_attachment,search_attachment,list_dir,http_request,web_search,draft_project_proposal); QA v0 Git evidence remains unavailable without an approval
browser_inspect and browser_flow are intentionally not in that policy
mapping: every otherwise-permitted browser call pauses for
agent_loop_tool_call approval regardless of
HECATE_TASK_APPROVAL_POLICIES. Loading even a static page can disclose
operator-visible content or trigger an application-specific GET effect;
browser_flow additionally runs scripts and approved clicks that can change
the application.
Work-policy-backed native Tasks add one immutable Agent Preset approval layer
to those mid-loop decisions. The task exposes the output-only
agent_preset_approval_policy snapshot, and retries and resumes keep the same
value even if the saved preset changes:
inheritleaves runtime-wide, mandatory browser, and per-MCP-server approval policy unchanged.allowalso adds no approval gate. It is an explicit preset posture, not an auto-approval override, so it cannot bypass a stricter runtime, browser, or MCP-server policy.requiresends every otherwise-permitted tool call selected from the exact catalog advertised for that model call through the normalagent_loop_tool_callapproval flow. This includes tools that are ungated by the runtime default and MCP servers configured withapproval_policy=auto.blockdenies only a call that the runtime-wide policy, mandatory browser gate, or matchingapproval_policy=require_approvalMCP server would otherwise send for approval. It does not block an otherwise-ungated tool merely because the preset saysblock. No approval is created for the denied call; Hecate recordspolicy.tool_blockedwithpolicy=agent_preset_approval, returns a tool-error result to the model, and may still dispatch other ungated calls from that response.
Hard denials are evaluated first. Workflow restrictions, a tools-disabled
preset, read-only or network posture, a missing browser grant/runtime, and an
MCP server with approval_policy=block remain non-approvable and keep their own
policy reason. The preset approval snapshot does not affect pre-execution
approval gates. Empty means no frozen Work policy approval layer is
present and adds no policy; an invalid non-empty stored value fails safely as
require.
This snapshot is admitted only for standard native agent loops created from a
resolved Work policy: directly through agent_preset_id, through a project
assignment, or through a tools-on Hecate Chat carrying its explicit frozen
Chat-safe posture. Legacy Chat Tasks without that approval field add no layer.
It does not apply to External Agent, QA, or non-preset Tasks. A preset may retain browser grants while
using block, but every otherwise-available browser call requires approval and
is therefore denied; assignment launch readiness and the standalone New Task
preview warn about that configured but unusable combination.
Approval never grants a capability denied by the resolved runtime policy. In
particular, unexpected calls on a task whose Agent Preset snapshot sets
tools_enabled=false are hard-denied with policy=agent_preset_tools before
the approval gate and cannot pause for operator approval.
The runtime emits an approval record of kind agent_loop_tool_call, persists the conversation snapshot, and returns status=awaiting_approval for the run. The UI banner shows an ordered, sanitized summary of the entire pending tool-call bundle, including calls that are not independently gated: approving the record authorizes dispatch of that whole model response. The linked approval Step stores a SHA-256 digest of the ordered bundle, not its raw arguments; same-run resume requires both bidirectional approval/Step linkage and an exact digest match, so a changed conversation is re-gated. Summaries are capped at 16 calls, 512 UTF-8 bytes per line, and 4 KiB total. An incomplete marker tells the operator when details were omitted, truncated, unrecognized, or withheld. Arbitrary command arguments, file/patch contents, HTTP headers and bodies, web queries, and MCP arguments are never copied into the summary; only narrow safe fields and byte/count shapes are shown. Exact shell/Git argv is shown only for a tiny parsed allowlist of static read-only Git commands.
On approve, the same run is re-queued; on resume, the loop detects unresolved trailing assistant tool_calls (no second LLM call) and compares them with durable dispatch intents. Calls that never started may dispatch after current policy checks. A call that may already have produced a side effect is not replayed when its exact result is missing; it receives the fail-closed recovery result described above. The resume control step says Dispatch approved tools only when a durable approval for that exact run authorizes the remaining bundle. Crash recovery of an entirely ungated pending bundle is instead recorded as Recover pending tools, so ordinary replay is not mislabeled as operator consent. An ordinary cross-Run resume never inherits approval authority: an unresolved source batch is gated again, and its approval, resume-control, and dispatch Step inputs carry the producing source_run_id plus its 1-based source_model_call_index instead of a current-Run model_call_index. The new Run's model-call count remains zero until it actually contacts the provider. On reject, the run and task terminate cancelled with last_error="approval rejected". On cancel, the run terminates cancelled, any awaiting approval step is marked cancelled, and the pending approval is resolved as cancelled.
The approval banner stays in sync with the run state because approvals ride along in every SSE snapshot (TaskRunStreamEventData.Approvals). No separate refetch needed.
Per-model-call LLM cost is captured at three granularities:
model.call.completedevents — one per LLM round-trip on the persisted run-event log. Each event carries the model-call spend, the run-cumulative figure (this run only), and the task-cumulative figure (entire resume chain viaPriorCostMicrosUSD). Subscribe via/hecate/v1/events?event_type=model.call.completed; the wire shape is inevents.md. These rows are the only run events the retention worker prunes — see themodel_call_eventssubsystem intelemetry.mdandHECATE_RETENTION_MODEL_CALL_EVENTS_*in.env.example.tool.file.patchevents — emitted wheneverfile_write,file_edit, orapply_patchchanges or proposes a file change. Each event points at apatchartifact containing the unified diff, giving operator UIs and future ACP/CLI consumers an inspectable edit record without re-deriving state from the workspace.- Patch review API —
GET /hecate/v1/tasks/{id}/runs/{run_id}/patcheslists patch artifacts. Applied patches can be reverted; proposed patches fromfile_editorapply_patchwithpropose=truecan be applied if the target file still matches the captured before-content. git_summaryartifacts — when the run workspace is a git repository and has changes at run completion, Hecate records agit-changes.jsonartifact with porcelain status entries and a diff stat for quick changed-file review.- Model-step
OutputSummary— each thinking step'sOutputSummary.cost_micros_usdcarries the same model-call figure, so the run-replay UI surfaces it next to "Model call N" without a separate event subscription. TaskRun.TotalCostMicrosUSD+PriorCostMicrosUSD— finalized totals on the run record. Cumulative across the resume chain =Prior + Total.
The per-task ceiling (Task.BudgetMicrosUSD) is checked against priorCost + costSpent before resumed work and immediately after each model call. A response that reaches the ceiling is durably visible in the conversation, but Hecate fails the run before treating it as a final answer, approval request, or executable tool bundle. A chain of resumes therefore cannot escape the ceiling by repeatedly resuming a maxed-out run; the operator must raise the task ceiling first.
Same-run mid-approval resumes seed costSpent from the run's pre-pause TotalCostMicrosUSD, so post-resume model calls add to (rather than overwrite) the pre-pause spend.
Three operations land an agent_loop run back on the queue with prior context:
- Retry —
POST /hecate/v1/tasks/{id}/runs/{run_id}/retry. Creates a new run with the same task config but no conversation history. The cheapest "do it again from scratch" button. - Resume —
POST /hecate/v1/tasks/{id}/runs/{run_id}/resume. Creates a new run that hydrates the source run's conversation and continues from where it left off. Used after afailedorcancelledrun. - Continue —
POST /hecate/v1/tasks/{id}/runs/{run_id}/continuewith{ "prompt": "..." }. Creates the next run in the same agent conversation, hydrates the source conversation, then appends the new user prompt. This is the ACP/editor-session path. - Retry from model call N —
POST /hecate/v1/tasks/{id}/runs/{run_id}/retry-from-model-callwith{ "model_call_index": N, "reason": "..." }.model_call_indexis 1-based within the selected source Run and must be in[1, source_run.model_call_count]. Hecate preserves inherited conversation context, truncates immediately before that Run-local response, and re-issues the call in a new Run. To branch from an inherited response, select the older Run that produced it. The new Run's model-call and Step indices restart at 1; cumulative cost picks up fromsource.PriorCost + source.Total.
These actions share the task runtime's atomic start boundary. Concurrent
requests cannot create two active runs. A resume may raise
budget_micros_usd, but the store compares against the latest durable task in
the same transaction that creates the run, so a stale lower request or a
zero-budget retry cannot erase a higher committed ceiling.
They also reuse every Work policy value frozen on the Task. Retry, Resume, Continue, scheduled starts, and retry-from-model-call never look up the current preset again, so editing or deleting a Work policy cannot broaden or otherwise change an existing Task.
The conversation viewer in the run-replay UI shows a ↻ retry from here button on each assistant model call (terminal runs only).
Env vars that affect agent_loop runs:
| Variable | Default | What it does |
|---|---|---|
HECATE_TASK_AGENT_LOOP_MAX_MODEL_CALLS |
8 |
Hard ceiling on LLM round-trips per run |
HECATE_TASK_AGENT_SYSTEM_PROMPT |
"" |
Global (broadest) layer of the three-layer system prompt |
HECATE_TASK_HTTP_TIMEOUT |
30s |
Timeout for the http_request tool |
HECATE_TASK_HTTP_MAX_RESPONSE_BYTES |
262144 (256 KiB) |
Response size cap for http_request |
HECATE_TASK_HTTP_ALLOW_PRIVATE_IPS |
false |
When false, blocks loopback / RFC1918 / link-local destinations |
HECATE_TASK_HTTP_ALLOWED_HOSTS |
"" |
Comma-separated exact-host allowlist; empty = all public hosts |
HECATE_CODEINTEL_GOPLS_PATH |
"" |
Exact absolute gopls executable; empty discovers gopls from the trusted global PATH |
HECATE_CODEINTEL_TSC_PATH |
"" |
Exact absolute TypeScript 7+ native tsc executable; empty discovers tsc from the trusted global PATH |
HECATE_CODEINTEL_AST_GREP_PATH |
"" |
Exact absolute ast-grep executable; empty discovers ast-grep from the trusted global PATH |
HECATE_TASK_BROWSER_EXECUTABLE |
"" |
Absolute executable override, ahead of host-scoped Settings; invalid fails closed. Empty uses the saved choice or disables both tools. Local runtime only; no browser downloads. See setup. |
HECATE_TASK_BROWSER_TIMEOUT |
20s |
Positive wall-clock limit for one static inspection or complete flow, including preflight and browser startup |
HECATE_TASK_BROWSER_ALLOW_PRIVATE_IPS |
false |
Allows private/local destinations after explicit opt-in. Hostname resolution is pinned for the call, but this is not an OS/network isolation boundary. |
HECATE_TASK_WEB_SEARCH_PROVIDER |
"" |
Empty disables native web search. Supported values: brave, tavily, exa |
HECATE_TASK_WEB_SEARCH_API_KEY |
"" |
API key for the configured search provider. Provider aliases: BRAVE_SEARCH_API_KEY, TAVILY_API_KEY, EXA_API_KEY |
HECATE_TASK_WEB_SEARCH_ENDPOINT |
provider default | Optional provider endpoint override, mostly for tests or private proxies |
HECATE_TASK_WEB_SEARCH_TIMEOUT |
15s |
Timeout for the search-provider request |
HECATE_TASK_WEB_SEARCH_MAX_RESULTS |
5 |
Maximum results returned per web_search call, capped by Hecate and the provider |
HECATE_TASK_WEB_SEARCH_SAFE_SEARCH |
moderate |
Provider safe-search default (off, moderate, or strict for Brave) |
HECATE_TASK_WEB_SEARCH_COUNTRY |
"" |
Optional default country code for searches |
HECATE_TASK_WEB_SEARCH_SEARCH_LANG |
"" |
Optional default search language |
HECATE_TASK_SHELL_ALLOW_PRIVATE_IPS |
false |
Same private-IP block, applied to URLs in shell_exec / git_exec commands when the task has sandbox_network=true |
HECATE_TASK_SHELL_ALLOWED_HOSTS |
"" |
Same exact-host allowlist, applied to shell_exec / git_exec command URLs |
For HECATE_TASK_APPROVAL_POLICIES (which gates mid-loop tool calls and the matching pre-execution task gates; valid values: shell_exec, git_exec, file_write, network_egress, read_file, all_tools) see runtime-api.md#approval-policy-configuration. shell_exec gates one-shot shell calls plus native agent-loop terminal tools. git_exec gates arbitrary git commands plus structured git_status and git_diff; QA v0 is an exception because it reports the metadata-free Git limitation without invoking Git or asking for approval. file_write gates full-file writes, exact-match file_edit, and structured apply_patch calls. read_file gates direct reads, structured search tools (grep, glob, code_intelligence), and task artifact reads. Browser capabilities are different: browser_inspect and browser_flow always require explicit approval and cannot be enabled by changing this list. For per-task mcp_servers knobs (max-servers cap, client-cache sizing, ping intervals) see runtime-api.md#runtime-backend-and-queue-configuration and mcp.md#resource-limits.
Hecate Chat can opt a session into RTK command-output compaction from
the chat settings panel. The setting is off by default. Hecate only
discovers RTK with exec.LookPath("rtk"), so the rtk binary must be
visible in the gateway process PATH; /hecate/v1/system/stats then
reports rtk_available=true plus rtk_path, and the UI suggests
enabling compact command output during new-chat onboarding. When enabled
for that chat, shell/git tool subprocesses launch as
rtk sh -lc <command> after policy validation; the same sandbox wrapper,
environment sanitisation, output caps, and timeouts still apply. Task
telemetry records both the pre-wrapper and post-wrapper command strings
so traces can show what RTK changed, with secret-looking env assignments
redacted before export or persistence.
Per-task fields on POST /hecate/v1/tasks that affect agent_loop:
execution_kind: "agent_loop"— picks this runtimeworkflow_mode: "qa"— optional built-in report-only QA contract. Omit for a normal Task; Hecate assigns and snapshotsworkflow_version: "v0". QA forces an ephemeral read-only/native-HTTP-and-search-disabled posture, rejectsmcp_servers, and allows only the structured inspection set described above.agent_preset_id— optional Work policy for a standard native Task. The preset must supporthecate_task(orany), and is unavailable for QA. Hecate freezes the resolved posture at Task creation; a tools-disabled policy cannot be combined withmcp_servers.prompt— the user message; requiredsystem_prompt— narrowest layer of the three-layer composition; optionalworking_directory— absolute path; required whenworkspace_mode=in_placeworkspace_mode—""/"persistent"/"ephemeral"(all clone) or"in_place"(use source directly)requested_provider/requested_model— pin the LLM provider and model. Omit both on anagent_looptask to use Hecate auto-routing: the router selects eligible provider defaults and retains its normal failover set. An explicit provider without a model returns 422model_not_configuredbefore a run is created.budget_micros_usd— per-task cost ceiling in micro-USD;0disablesmcp_servers— array of external MCP server configs whose tools join the catalog undermcp__<name>__<tool>aliases. Schema inmcp.md#hecate-as-mcp-client.
- HTTP 422
model_not_configured—POST /hecate/v1/tasks/{id}/startreceived an incomplete explicit route, such as a provider withoutrequested_model. Either supply both pin fields or omit both to use Hecate auto-routing. No run is created for the malformed explicit route. escapes allowed root— the LLM picked a path outside the workspace. The env system message normally prevents this; if you see it, check thattask.WorkingDirectorymatches what the model is using, or switch toworkspace_mode=in_placeto align them.api key is required for cloud provider X— the operator pinned provider X but no credentials are configured. The router usesScope.ProviderHintfromrun.Provider(mirrored fromtask.RequestedProvider).model "X" does not support tool-calling— the chosen model rejects thetoolsfield. Tiny / chat-only models (e.g.smollm2:135m) hit this. Pick a tool-capable model:gpt-4o-mini,claude-sonnet-4-6, orqwen2.5-coderfor Ollama. Hecate Chat normally avoids this for new prompts by falling back to direct model chat when tool support is unknown or absent. Native Tasks require a tool-capable model when their effective catalog is non-empty; preset-backed model-only tasks send no tools.browser inspection is unavailableornative browser interaction is unavailable— this is expected unless the local gateway has an enabled browser in Settings or an explicitHECATE_TASK_BROWSER_EXECUTABLE, the Task was created from an eligible Work policy (directly, through a native project assignment, or from a new Chat's frozen policy), and that policy independently granted the requested browser capability with exact origins. Neither tool is offered to tools-off or ungranted Chat, External Agent, QA, non-preset Tasks, or remote-runtime Tasks.workflow_mode=qa requires an ephemeral workspace,does not allow native network access, ordoes not allow mcp_servers— QA v0 deliberately refuses a caller-supplied posture that would widen its report-only contract. Omit the conflicting field; Hecate supplies the required ephemeral, read-only, native-HTTP-and-search-disabled posture itself. QA v0 also blocks static browser inspection and browser interaction; either would need a later explicitly selected Hecate runtime contract.browser inspection URL is invalid,browser inspection origin is not allowed, orthe approved browser flow was invalid— use an absolute HTTP(S) URL without credentials, a query string, or a fragment, and add only its exact origin (not a path) to the native Agent Preset. A flow needs 1–6 exact accessibilityclick/wait_foractions. Keep the origin-plus-path short enough to appear in full in the approval. Do not use either capability for a private/local origin unless the operator deliberately enabledHECATE_TASK_BROWSER_ALLOW_PRIVATE_IPS.agent loop hit per-task cost ceiling—Task.BudgetMicrosUSDwas reached across the run + prior chain. Raise the ceiling and resume to continue.agent loop reached the maximum of N model calls without producing a final answer— the model didn't terminate. Either raiseHECATE_TASK_AGENT_LOOP_MAX_MODEL_CALLS, narrow the prompt, or resume to give it more model calls.
