What's New in v0.4.5
🇧🇷 Versão em português · 📋 Full CHANGELOG · 📦 Download
This release removes unrestricted bash from the places where no human
confirms commands, and it finishes what v0.4.4 left half-done. In the
standard profile, garraia mcp-server and the gateway only give the model a
shell inside a docker/podman sandbox. Saying "yes" to a confirmation
request approves it again on every channel with a person on the other end.
garraia whatsapp link asks who may talk to GarraIA. And the boot now
refuses two configurations that used to start wide open: an exposed bind
without a credential, and half-configured TLS.
bash only in a sandbox where no human is in the loop (#1272)
Before, in standard, garraia mcp-server and the gateway runtime registered
a bash that ran on the host. The risky-command tier only catches what
looks dangerous, so a cat /etc/shadow or an echo x > /anywhere requested
by the model went through. The decision is now a pure function of the profile
and agent.sandbox, and it never comes from detecting a container:
agent.sandbox for bash | standard (default) | isolated-pod (explicit) |
|---|---|---|
Required and usable: docker/podman with mode: all (without bash in elevated) or allowlist with bash, binary present | registered; every command runs in the container, with no host fallback | registered, in the sandbox |
Not required: mode: off (the default), bash in elevated, allowlist without bash | does not exist | registered on the pod host, with the denylist and risky tier on |
Required but unusable: backend: ssh, no backend, binary missing | does not exist | does not exist, and the warning gives the real reason |
- The same rule covers the gateway's
run_tests. That tool runs thescripts.testofpackage.json,build.rsandconftest.py, andfile_writecan write all three. - No sandbox, no shell. The boot logs one
warn!saying why and how to turn it back on,/api/diagnosticsgains thetools.bashcheck, and thegarra_agentsystem prompt tells the model there is no shell. - The container is now a real boundary. It gets
--cap-drop ALL,--pids-limit 512and the operator's--user <uid>:<gid>(--userns=keep-idon podman). Only the session's canonical working directory is mounted:/,$HOME, an ancestor of it, or a session without aworking_diris a refusal. On timeout the container is removed. git_diffandcode_reviewdo not run programs planted in the repository. Git runs with no fsmonitor, hooks, textconv, filters or submodules, andfile_writerefuses any path with a.gitcomponent.garraia chatis unchanged: a human confirms in the terminal there.
The decision is recorded in the 2026-09-21 Amendment of ADR 0024.
"Yes" approves again, on every channel with a human (#1343)
When a tool asked for confirmation (a risky bash command with
agent.tool_confirmation_enabled, or an R3/R4 device_execute), the turn
paused, and the "yes" in the next message approved nothing. The channels store
history as text, so the paused request never came back and the tool asked
again forever. The request now stays in memory. The next message, if it is
entirely an approval word (sim, yes, ok, confirma, confirmar,
proceed, approve), runs the request once, within 5 minutes, and
only if it comes from the same sender, in the same session and on the same
channel:
| Where | Who can approve |
|---|---|
Web Console (/ws) and desktop (/ws/parrot) | the same connection; after a reconnect it asks again |
/v1/chat/completions (with and without "stream": true) | the owner, with the same Authorization and the same X-Session-Id on both requests |
Mobile app (POST /chat) | the JWT sub |
| Telegram, Discord, Slack, WhatsApp, Matrix, IRC, Signal, LINE, Teams, Google Chat, iMessage, personal WhatsApp | the user's platform id |
garraia chat | the terminal itself |
- In a group, another member's "yes" does not approve, and it ends the request.
- Any other message in between, "no" included, ends the request (#1340). A second "yes" pauses again, and restarting the gateway cancels every pending request.
- These paths still have no resume, on purpose:
- A2A and OpenClaw;
POST /api/sessions/{id}/messages;- the agent's reply in the workspace chat, and scheduled tasks;
garraia askand thegarra_agentofgarraia mcp-server.
- The request reaches you without the internal marker (#1373). Users no
longer see
[CONFIRM_REQUIRED:…]in the message or in thegarraia chattool line. The sentence still names the command and says "Responda sim para executar". - Phone numbers are masked in the log. The WhatsApp and Signal session ids
embed the sender's number, and the runtime spans logged it in full. The
RedactingWriterof stderr andgarraia.lognow replaces every run of 10 or more digits not glued to a letter with…plus the last 4.
Full refusal matrix:
docs/security/threat-model.md §5.16.
Personal WhatsApp: who may talk, and a bridge that updates itself
linkasks who may talk to GarraIA (#1345). After the QR, the command asks for the number with its country code and only says "ready" once someone is authorized. An empty answer leaves the gate closed and prints the warning plus the command that fixes it later.garraia whatsapp allow <number> [--owner] [--yes]authorizes without a terminal. The number needs+and the country code, and<id>@lidis also accepted. It exits 65 on an invalid number, and 64 for--owneroutsideisolated-podor in a pipe without--yes. Owner is only offered inisolated-pod, defaulting to no.- Authorizing and revoking take effect without a restart. The gateway
re-reads
allow,ownersandenabledfromconfig.ymlon every message. To revoke, delete the number from the list in the file.enabled: falserefuses everyone on the next message. A Brazilian mobile number with and without the ninth digit matches as the same number. - Honest status.
garraia whatsapp statusshowsAuthorized: N · Owners: M(counts, never numbers)./api/diagnosticsand the boot log warn when nobody is authorized. - The bridge updates itself at boot (#1373). After a
garraia update, the gateway rewrites the bridge embedded in the binary before launching it. Before, it kept launching the previous version'sbridge.mjsuntil someone linked again.npm cionly runs whennode_modulesis missing or nothing proves the installed tree matches the embedded lock. - The log shows the right ending of the connected number (#1373). Before, the JID's device suffix was counted.
- A dead-bridge error carries the tail of Node's stderr again (#1368).
MCP filesystem without a corrupted npx cache (#1346)
- Provisioning on a new install pins
@modelcontextprotocol/server-filesystem@2026.8.31, instead of downloading the newest build on every cold cache. An existingmcp.jsonis never rewritten: themcp.filesystem_pinnedcheck of/api/diagnosticswarns installs without a version and gives the exactargsto paste. - An
ERR_MODULE_NOT_FOUNDinside<npm cache>/_npx/<hash>/removes only that validated entry and retries once. - Once
max_restartsis exhausted, the error is logged once, not every 30 s. /api/mcp/healthnow lists servers that failed at boot, withstatus,causeandlast_error. Themcp.serverscheck gives the next step for each cause.
Garra knows what it has: garra_status (#1347)
With linked WhatsApp connected, Garra answered "I don't have access to WhatsApp". Three things changed:
garra_statusjoins the whitelist modes (search,architect,debug,orchestrator,review,edit). The runtime appends an instruction to consult it before denying access to a channel.- The report sees what
/api/channelssees. The list includeswhatsapp_linkedand the push channels, each with astatus(active/offline), and it comes from the same function. On a fresh install the list is empty, instead of showing eightofflinechannels. The report gainsexecution_profile,mcp_servers(name, state and tool count only) andsession.channel, andsession.idis masked. - Operator data stays with the operator. The report withholds
working_dir,project_id, the provider list, the MCP server names and the exact version, and lists what it withheld inwithheld, in two cases:- a restricted-gate turn, such as the WhatsApp
searchfloor; - a session not proven to be the operator's: mobile app, A2A, messaging channels, an unknown session, or a gateway exposed through the keyless opt-out.
- a restricted-gate turn, such as the WhatsApp
- The streaming turn now applies the mode's prompt and
max_tokens, like the batch path.
A boot that refuses what would fail open (#1261, #1247)
- An exposed bind without a credential does not start.
garraia start,start -dandrestartexit 78 when any bind address is not loopback and there is neithergateway.api_keynorGARRAIA_GATEWAY_API_KEY. The refusal comes before the bind, before the fork in-d(so the message reaches the terminal) and beforerestartstops the running daemon. TLS does not exempt the bind, and a name that does not resolve is refused too. - New env var
GARRAIA_GATEWAY_API_KEY. It wins over the file, an empty value counts as unset, and it is never written toconfig.yml.config checkreports it by presence only. - Deliberate opt-out:
gateway.allow_unauthenticated_network_bind: true, file only, with no env var or flag, and a loud warning on every boot. gateway.host/gateway.portin the file are deprecated. Those keys never fed the bind.garraia initstops writing them and removes them on a re-run, andstatus,stop,doctorandadminuse the same address asstart.garraia restartnow readsHOST/PORT, likestart.config checkruns on every boot.start,restartandstart -drun the same check once: eachErroris logged as an error and eachWarningas a warning. Instart -dthe lines go to stderr before the fork.- Only a closed list refuses the boot: today, half-configured TLS (only
tls_cert_pathor onlytls_key_path), which used to serve plain HTTP in silence. Every otherErroris reported and brings nothing down.
Sandbox, runs and runtime
- The sandbox covers the repository tools (#1225). With
agent.sandbox.mode: all,run_tests,git_diff,code_reviewandrepo_searchrun in the container, with an argv built without a shell.backend: sshis refused for them. - Runs ledger (#1227):
garraia runs listreadssessions.dbwithout talking to the gateway, with--status,--limit(default 50) and--json;GET /api/runsis read-only, returns previews of up to 120 characters, and has stricter access than the rest of/api/*;runs.retention_daysturns on a sweep (at boot and every 24 h) that deletes old terminal runs. The default0never deletes, and arunningrun is never deleted.
- Two dead crates are removed (#1226):
garraia-toolsandgarraia-runtime. The workspace is down to 22 crates, and thegarraia max-powersnapshot announcestool_program. - The loop detector warns before it aborts (#1295). On the first repetition (same tool, same arguments, three times in a row), the model receives a corrective note instead of the turn dying. Any later detection in the same task aborts as before, and the blocked call never runs.
garraia max-power(#1228).--goalno longer aborts with "Cannot start a runtime from within a runtime", and with no usable provider it runs offline, as its help promises. The command writes togarraia.log, and a body-read error from an OpenAI-compatible provider now states the real cause.
Fixes from the clean-install smoke test
The v0.4.4 clean-install smoke test found these:
install.shin containers and CI (#1369). Without a real terminal, the installer takes the non-interactive path with no error lines. A non-emptyCIalso counts as no terminal, as ininstall.ps1.garraia.logis kept (#1371).garraia start -dopens the log in append mode, instead of erasing the previous run and writing over the head of the file.garraia status | headexits quietly (#1371), with no broken-pipe panic, in every command that only reads and prints.GET /admin/api/logsreads only the last 512 KiB and no longer returns 500 on a non-UTF-8 byte.- REST sessions survive a restart (#1372).
GET .../history,POST .../messagesandDELETE /api/sessions/{id}stop returning 404 for a session that is insessions.db.DELETEwrites theapi_logoutmarker, and a closed session does not come back. - A provider's key only goes to its own
llm:entry's endpoint (#1370).garraia ask -p openaisentllm.openai.api_keytohttps://api.openai.comeven when the entry had its ownbase_url, and the audit found the same class of bug on four more CLI paths. The kind's environment variable (OPENAI_API_KEY, …) only goes to that kind's default host.
Infrastructure and process
- Quality Ratchet (#1254):
freeze-baseline.pygains--adopt-current-file-metrics --reason '#NNN'. The flag adopts only the file-size metrics and records where they came from. Audit, coverage and clippy stay on the strict ratchet. The re-baseline itself (#1376) ran onmainright before the tag: the ratchet stops repeating months of drift on every PR, the 3500-line ceiling did not go up, and the 16 files above 2500 lines are recorded as accepted debt in.quality/README.md. - A PR without a fragment in
changelog.d/goes red, with an exemption through theno-changeloglabel. - Coverage and the ratchet edit a single comment per PR, instead of posting a new one on every push.
- Swagger UI is vendored, with no download during the build.
- The release uploads the CLI's aarch64 AppImage again (#1344), and CI checks the upload list.
Upgrading from 0.4.4
garraia update
What an existing install runs into when it starts v0.4.5:
- An exposed bind without a credential no longer starts (exit 78, with a
message that says how to fix it).
install.shinstalls and the systemd unit bind to loopback and are unaffected. If you expose the port, do this before upgrading:- set
gateway.api_keyin the file, or exportGARRAIA_GATEWAY_API_KEY="$(openssl rand -hex 32)".garraia initalso writes the key when the machine is server-like (root or a RunPod pod) or whenHOSTis not loopback; - or bind to loopback only again, with
garraia start --host 127.0.0.1, and removeHOSTfrom the environment; - the Docker image (its
CMDbinds0.0.0.0), thedocker-compose*.ymlfiles and RunPod pods needGARRAIA_GATEWAY_API_KEYin the environment. Without it the container exits with 78 andrestart: unless-stoppedloops, sodocker compose psshowsRestarting..env.exampleships the line empty on purpose, for you to fill in; - Helm: the
gatewayApiKeyblock (generated on install and kept on upgrade, orexistingSecret/value).helm template/ArgoCD need one of the two; - Terraform/ECS: the required variable
gateway_api_key_secret_arn. Without it,terraform planfails before the image is swapped; - behind an authenticating proxy, with the port open on purpose, use
gateway.allow_unauthenticated_network_bind: truein the file; /ping,/healthand/api/healthstay open for health checks, and clients send the key asAuthorization: Bearer <key>.
- set
garraia restartnow readsHOST/PORT. A daemon restarted in a pod withHOST=0.0.0.0now binds the exposed address, instead of silently falling back to loopback, so it also needs the credential.- Config errors that block the boot. With only
gateway.tls_cert_pathor onlygateway.tls_key_pathset,start,restartandstart -dexit 78 and name the missing field. To start anyway, setGARRAIA_ALLOW_INVALID_CONFIG=1(exactly1); the finding is still logged as an error. This escape hatch does not turn off the exposed-bind refusal. Every otherconfig checkErrornow shows up in the log on every boot but does not stop it. Amemory.retentionwithinterval_hoursormax_age_daysout of range no longer crashes the worker with a panic: the sweep does not start, and nothing is deleted. - In
standardwithout a sandbox,garraia mcp-serverand the gateway have nobash(and the gateway has norun_tests). The boot warns once and/api/diagnosticsshowstools.bash. To get the shell back, contain it:Or, only if the process really runs in a disposable pod, declareagent:sandbox:mode: allbackend: docker # or podman; the binary must be installedexecution.profile: isolated-pod.file_writenow refuses paths with.git. agent.sandbox.mode: allnow also containsrun_tests,git_diff,code_reviewandrepo_search. The default image (debian:bookworm-slim) only hasgrep, so pointagent.sandbox.imageat an image withgit/cargo/rg, or list the tool inagent.sandbox.elevated. Withnetwork_disabled: true,cargocannot download crates.- The gateway's first boot rewrites the WhatsApp bridge. The gateway
replaces the files that differ from the embedded ones (between 0.4.4 and
0.4.5 only
bridge.mjschanged). It runsnpm cionly ifnode_modulesis missing or nothing proves the tree matches the lock. A 0.4.4 install whosenpm cifinished is adopted withoutnpm, throughnode_modules/.package-lock.json. Ifnpm ciis needed,npmmust be on the gateway process'sPATH. Without it, or if it fails, the bridge does not start.garraia whatsapp statusthen showsBridge: dependencies missing(in English) and/api/diagnosticsgives the steprode npm ci em <dir> e reinicie o gateway(runnpm ciin the directory, then restart the gateway). Do that from a shell that hasnpm; the gateway adopts the tree at the next boot. The linked session is not touched. - WhatsApp with an empty
allow. The gateway always dropped those messages, but it now warns at boot, instatusand in/api/diagnostics. Rungarraia whatsapp allow +<country><number>. Edits toallow/owners/enablednow apply on the next message, and aconfig.ymlthat does not parse keeps the previous list.garraia whatsapp allowrewrites the file, and comments are lost. - Mobile app sessions count as restricted in
garra_status./auth/registeris open, and having an account on the gateway does not prove you are its operator. In those sessions the report withholds the directory,project_id, providers, MCP server names and the exact version. The same applies to web, API, VS Code and desktop on a gateway exposed through the keyless opt-out. - Resuming an approval on
/v1/chat/completionsneeds a stableX-Session-Id. Without it, every request is a new session and the "yes" approves nothing. Send the sameX-Session-Idand the sameAuthorizationon the request and on the "yes". With no owner claimed in the allowlist, the pause stays terminal. - An
llm:entry with its ownbase_urland noapi_keyno longer receives the kind's environment variable, including throughagent.default_provider. An OpenAI-compatible endpoint gets thenot-neededplaceholder, andanthropic/openrouterrefuse with an error. Put theapi_keyin the entry itself. Thehttps://openrouter.ai/api/v1thatgarraia initwrites keeps working with the key in the environment. An ad-hoc--urlonly usesLLM_API_KEYor the key of an entry with that samebase_url. - Limits documented in this release's fragments:
garraia.lognow grows across starts with no rotation, as it already did with a foregroundgarraia start;runs.retention_daysstays at0(never delete) until you change it, and until then the boot logs how many runs exist;- an existing
mcp.jsonis not rewritten. Followmcp.filesystem_pinnedto pin the version, which covers only the top-level package (its dependencies follow their semver ranges); - a REST session comes back from disk without its
working_dir; - a contact who paired with a
/paircode and was never inallowstays admitted until the restart; gateway.host/gateway.portstay in the schema, andconfig checkflags them as deprecated when they differ from the effective bind;- the Web Console shows the bind as read-only;
- pending confirmation requests are dropped when the gateway restarts;
- the loop warning costs at most one extra LLM round, and no setting turns it off;
- the CI's
swagger-ui-cacheaction stays for one release as a leftover.
Known limits
- A mode's
temperaturestill does not reach the provider. The mode's prompt andmax_tokensreach it on every turn. Chat, channel and API turns still send no temperature, so the provider uses its own default. Sending it would change the request of every turn that has a mode (the built-in modes declare 0.3 to 0.7). That waits until the provider can omit the parameter for models that reject it. Seedocs/src/modes.md. - REST sessions closed before 0.4.5 can be read again. The
api_logoutmarker only exists for aDELETEmade from this version on. A REST session closed on an earlier version has no marker, and after the upgradeGET /api/sessions/{id}/historyserves it again as a live session. The route belongs to the operator (loopback, or theapi_keygate on an exposed bind), and the history is already in the operator's ownsessions.db. To close it for good, send a newDELETE, which now writes the marker. - Sandboxed
bashis still a shell line.run_tests,git_diff,code_reviewandrepo_searchstart the container through argv, with no shell (#1225).bashstill builds itsdocker run/podman runas one line that a shell on the host interprets, with the command in single quotes (sh_quote). Convertingbashto argv is not in this release. Both paths apply the same containment flags (--cap-drop ALL,--pids-limit 512,--user/--userns=keep-id,no-new-privileges), and neither has--read-onlyor a memory limit (threat model, priority 9). Instandardwithout a sandbox,bashis not registered at all, so this limit only applies with a configureddocker/podmansandbox or inisolated-pod. - The gateway still resolves provider keys as config > env without looking
at
base_url. The #1370 rule applies to the CLI (ask,chat,max-power,garra_ask/garra_agent). In the gateway, an entry without a key can still receive the kind's environment variable. Seedocs/configuration.md. - A WhatsApp contact identified only by
@lid, with no number, does not match a number inallow. Its messages are refused in silence, andstatuscounts those refusals. To authorize the contact, use a/paircode orgarraia whatsapp allow <id>@lid. - Refusing another member's "yes" has no end-to-end test in each of the 11
bootstrap/channels. One end-to-end test covers the shared path, and a static guard pins, per file, which identifier becomes the sender. The guard proves the identifier, but not that the platform delivers it authenticated. On IRC the nick is the only identity, and it can be taken over without NickServ.