Reliability & Trust Model
August 12, 2026 · View on GitHub
This document describes the reliability primitives that ship with Grinta and is deliberately honest about what is and is not guaranteed. Read it before deploying Grinta against a production codebase.
Trust model (no automatic sandbox by default)
Grinta is a single-user local CLI agent. In the default standard profile,
and in the policy-only hardened_local profile, commands run as the OS user that
launched Grinta and inherit that user's filesystem and network permissions.
There is no Docker or VM isolation by default. This keeps common local workflows
(asdf, pyenv, npm scripts, native debuggers) available, but it means policy
checks are not a host boundary.
The optional sandboxed_local profile adds OS-native, process-scoped isolation
for supported non-interactive subprocess commands. It requires the platform
backend described in SUPPORT_MATRIX.md. Interactive PTY
sessions remain unsandboxed, and the profile is not complete host isolation.
What protects you instead:
- Workspace scoping. File reads/writes outside the workspace root are
rejected by
path_validation.py; symlinks and Windows junctions cannot be used to escape the workspace. - Command risk classification. Dangerous patterns (
rm -rf /,git push --force,sudo, recursive deletes outside the workspace, etc.) are flagged and gated. - Secret masking. Provider keys and credentials are masked in logs and observations before they reach the model or the persisted event stream.
- Risk-aware blast radius warnings. Edits across many files trigger an inline warning that the model surfaces in its observation.
- Optional process isolation.
sandboxed_localcombines the hardened policy withbwrap, AppContainer, orsandbox-execfor supported non-interactive commands.
If you need a complete disposable-host boundary, run Grinta inside a container
or VM you manage. sandboxed_local reduces process capability; it does not make
the whole CLI a sandbox.
Circuit breaker & recovery
CircuitBreaker adapts to repeated identical failures from a tool: after a
threshold it stops re-issuing the same call and surfaces a recovery prompt to
the model. Recovery rounds are preserved across intervening housekeeping
actions so the agent does not lose context on an unrelated tool result.
- Null-action recovery. When the model returns no actionable tool call the controller injects a recovery prompt rather than looping.
- Pending-action timeouts. Two tiers: debugger tools default to 120 s, terminal tools (bash, PowerShell, interactive shell) default to 600 s. These map to
TOOL_BRIDGE_TIMEOUT_DEBUGGERandTOOL_BRIDGE_TIMEOUT_TERMINAL_RUNinbackend/core/constants.py, which respect any explicitaction.timeout.
Debugger latency contract
The DAP debugger (backend/execution/dap/) is one of the slowest tools
because it spawns a real debug-adapter subprocess (for Python, typically
debugpy.adapter when debugpy is installed). The runtime provides two
reliability primitives for that path:
- Off-loaded sync work.
debugger()inbackend/execution/io_mixins/_aes_io_run_mixin.pyrunsDAPDebugManager.handleviaasyncio.to_thread, so the event loop is never blocked during cold start. - Granular progress logging. Every DAP step (
spawning adapter,initialize,launch,initialized event,configurationDone,ready in N s) emits an INFO log line so "frozen" becomes "visibly working".
Python debugging uses the same auto-detection model as other DAP adapters and
LSP servers: install debugpy in the active environment when you need the
debugger tool for Python (pip install debugpy).
Failure path: if the adapter cannot start, the returned ErrorObservation
includes the adapter's stderr tail so the model can react meaningfully
instead of seeing a bare DAPError.
Crash & shutdown contract
- EventStream. Closed by orchestrator and CLI teardown paths on
/quit,Ctrl-C, and normal session shutdown. Uncaught exceptions are logged bybackend/core/logging/logger.py. - Worker pool. The
ThreadPoolExecutorused bycall_async_from_syncis shut down at interpreter exit (atexit) withcancel_futures=Trueso the process exits promptly even with stuck non-daemon threads. - Asyncio loop teardown.
_LOOP_FINALIZE_WAIT_SEC(default 3 s) caps the time spent inloop.shutdown_asyncgens()andloop.shutdown_default_executor()per sync-bridge call, and is now skipped entirely when the loop never scheduled either, so simple sync tools do not pay a 5 s tail. - DAP cleanup. Adapter subprocesses are torn down on
start()failure, on dispatch failure, and onDAPDebugManager.close_all()(called at REPL exit).
What to do when something goes wrong
- Hung tool call. Open
logs/workspaces/<ws>/sessions/<session_id>/session.jsonland look for the most recent_handle_action STARTand the matchingEND. If you see the newDAP: …lines, the debugger is working through its handshake. If you see no progress for > 30 s, copy the tail and file an issue. - Wedged debug session. Run
/healthin the REPL — it reports whetherdebugpyis importable (install withpip install debugpywhen needed) and checksgit/rgavailability. - Provider failure. The agent retries with exponential back-off; the UI
may show compact messages while some transient classes are kept out of the
model transcript (
notify_ui_only). Use/costto see cumulative spend before retrying.
Late runtime errors after user stop
Memory/runtime status callbacks can still fire after the user stops the
agent or after a finished run. Those diagnostics are recorded on the
controller (set_last_error, logs), but Grinta does not transition
STOPPED → ERROR or FINISHED → ERROR: that would conflate a deliberate
terminal with a broken session and could break WAL/reconnect semantics.
The canonical terminal-state rules live in VALID_TRANSITIONS in
backend/orchestration/services/state_transition_service.py; invalid late
transitions are rejected and logged.
Workspace checkpoints and restore recovery
Before eligible edits, mutating commands, destructive commands, and selected
phase boundaries, rollback middleware asks RollbackManager for a checkpoint.
The manager uses the standalone
ShadowGit package through Grinta's
compatibility adapter in backend/execution/rollback/shadow_repo.py.
The snapshot store is independent of the workspace's .git and preserves
file bytes, symlinks, and POSIX executable modes. Restore moves extra files to
quarantine rather than deleting them irreversibly. A restore journal records
progress so an interrupted restore can be detected and recovered on the next
open. Retention pruning bounds checkpoint history; Grinta owns the retention
policy while ShadowGit owns object-store pruning and integrity mechanics.
Two kinds of rate limiting
- LLM provider limits (TPM/RPM/429). Grinta’s inference client and
RecoveryService/ retry queue handle back-off; optional HUD toasts may appear. Grinta does not require Redis or any other in-repo store for that path. - Application / API rate limits. If the project you are editing throttles its own HTTP surface (in-memory, gateway, database-backed, etc.), that is separate infrastructure: it does not fix provider 429s on the agent’s model calls.
Edit verification (grounding gate)
After an edit is followed by failing feedback (tests, linters, etc.),
ActionExecutionService can require a grounding read or terminal check
before further writes or finish. Default is strict for safety; relaxing it
(e.g. more edits before the gate, or narrower path rules) is a deliberate
product trade-off—see backend/orchestration/services/action_execution_service.py
and step_guard_service.py.
Pending action lifecycle
PendingActionService tracks every in-flight runnable action by stream id,
not a single global slot. Parallel read batches can therefore clear one action
without colliding with siblings still awaiting observations.
clear_for_action(action)— remove one outstanding row (observation arrival, parallel batch completion).clear_primary()— remove only the latest row (step-liveness timeout, single-action recovery).clear_all()— hard reset (agent stop, user message preemption).has_outstanding()— serial batch drain and step barriers consult this before dequeuing the next action from the same LLM response.
Structured logs include pending_action_id, clear_reason, and
outstanding_count on every PENDING_ACTION_CLEARED line.
Prompt window immutability
select_prompt_events deep-copies events before truncation. Windowing never
mutates state.history in place. Action/observation pairs are dropped as
causal units so the model never sees an orphaned tool call without its result.
Background drain barrier
After parallel batches and before compaction, the controller calls
drain_step_barrier, which:
- Drains
_background_tasksspawned viarun_or_schedule. - Waits until
PendingActionService.has_outstanding()is false.
A suspend-aware deadline (default 2 s) prevents indefinite hangs. On timeout
the step continues with a DRAIN_STEP_BARRIER_TIMEOUT warning in logs; the
session is not killed.
Persistence degraded mode
EventStream.persistence_health is one of ok, degraded, or failed:
- First
persist_eventfailure →degraded. - Three consecutive failures →
failed. - Next successful write →
ok.
Delivery to subscribers continues in-memory when disk writes fail. The next
agent step emits a one-line StatusObservation so the model knows durability
may be incomplete. /health and collect_orchestration_health surface
persistence_health and add persistence_degraded / persistence_failed
warnings.
Quarantined WAL segments are moved to lost_events/ rather than deleted when
flush permanently fails.
What to do when something goes wrong (log triage)
Search logs/workspaces/<ws>/sessions/<session_id>/session.jsonl for:
outstanding_count— pending rows not cleared; may indicate a parallel batch race or late observation.persistence_health=degradedorPERSISTENCE_DEGRADED— disk or SQLite writes failing; verify free space and AV locks.DRAIN_STEP_BARRIER_TIMEOUT— background work or pending actions did not finish before the step barrier; compaction may run on slightly stale history (soft degradation, not session death).lost_events/under the workspace events directory — quarantined WAL payloads that could not be flushed.
What this document does not promise
- No guarantee against a malicious model intentionally destroying files in the workspace. ShadowGit-backed checkpoints cover eligible guarded actions, but they are not a substitute for normal version control, backups, or an isolated disposable environment.
- No guarantee against a malicious MCP server. Only enable MCP servers you trust.
- No guarantee that long-running terminals will behave identically across PowerShell, bash, and zsh. Behaviour is normalised through the PTY layer but extreme cases (TUIs that probe terminal capabilities aggressively) may need explicit mode hints.