v0.4.3
Agentao 0.4.3
A permission-hardening + LLM-resilience release on top of 0.4.2.
No breaking changes; no public API or wire-format change.
pip install -U agentao upgrades in place from any 0.4.x release.
The headline features:
- A hardline shell-safety floor that denies disk-wipe-class
operations before any rule is consulted — includingfull-access.
Embedded hosts that take policy responsibility themselves can opt
out per construction. - An explicit LLM HTTP retry policy that replaces the OpenAI
SDK's built-in retry. HonorsRetry-After, bounds total sleep,
is cancellable, and is streaming-aware — it never replays text
that has already reached the user. - A tightened config trust boundary: project-scope
permissions.jsonis no longer loaded; project-scopemcp.jsonis
add-only; MCPdestructiveHintadds friction even when the same
server claimsreadOnlyHint. - A
SearchTextToolskip-list + ripgrep fallback that kills
the "stuck onnode_modules" failure mode and rescues Windows
boxes withrg.exebut nogit.
Plus a mask_secret redact helper, Windows UTF-8 console
enforcement at import, a workspace-write sensitive-write preset,
and a contextvars-propagation fix for parallel tool batches.
Why this release
Three converging threads landed in the 2026-05-03 → 2026-05-04
window:
- Permission-hardening plan rev 3 — closed all five PRs
(docs/design/permission-hardening-plan.md). The hardline floor
(PR 2), Windows UTF-8 (PR 3),mask_secret(PR 4), and the
shell-rc sensitive-write preset (PR 5) are user-visible additions
from that plan. PR 1 (correctness fixes —isinstanceJSON guard,
MCP error classification, contextvars propagation) is also
included. - LLM resilience review — replaced the SDK's built-in retry
with an Agentao-controlled policy after observing
Retry-After-strands-the-caller and "upstream"-substring-bypass
failure modes in the wild. - Config trust boundary tightening (#25) — three small but
load-bearing escalation paths (projectpermissions.json, project
mcp.jsonoverrides, untrusted MCP annotation consumption) closed
in one PR.
Permission hardening
Hardline floor (opt-out)
agentao/permissions_hardline.py now runs as a pre-check inside
PermissionEngine.decide_detail(). The matched patterns:
rm -rfagainst/, system roots (/etc /usr /var /boot /bin /sbin /lib /home /root), or~/$HOMEmkfs[.*]dd … of=/dev/(sd|nvme|hd|mmcblk|vd|xvd)…and> /dev/(sd|nvme|…)- Fork bomb
:(){ :|:& };: kill -1,kill -9 -1shutdown / reboot / halt / poweroffat command positioninit [06],telinit [06]systemctl (poweroff|reboot|halt|kexec)
Each pattern handles sudo/env wrappers (with flags and value
args), quoted paths ("$HOME", '/etc/passwd'), split rm flags
(-r -f, --recursive --force), path-qualified rm (/bin/rm),
;/&&/|| + newline separators, command substitution, and
$HOME word boundaries.
The floor is default ON. Embedded hosts that sandbox Agentao
in a container or run their own deny pipeline can opt out:
PermissionEngine(
project_root=...,
user_root=...,
enable_hardline=False,
)full-access then means literal full access. The dual contract is
locked by tests/test_permissions.py::test_full_access_default_blocks_hardline_commands
and ::test_full_access_with_hardline_off_honors_literal_contract.
Decision reasons carry source prefixes
PermissionDecisionDetail.reason (and the host-facing
PermissionDecisionEvent.reason) now uses a stable taxonomy:
hardline:<description>— opt-out floor refusedmode-preset:<rule_id>— preset rule matcheduser-rule:<rule_id>— user JSON rule matched
Hosts can render audit displays without parsing free-form text.
User-initiated denials are not in this taxonomy; they still
surface on ToolLifecycleEvent(cancelled).
Workspace-write sensitive-write preset
A new mode-scoped preset rule flags shell writes to ~/.bashrc,
~/.zshrc, ~/.profile, ~/.bash_profile, ~/.zprofile,
~/.netrc, ~/.pgpass, ~/.npmrc, ~/.pypirc via redirection
(>, >>, FD-prefixed redirects), tee, cp/mv, or sed -i.
The rule emits ASK, not DENY — installers (Homebrew, pyenv,
rustup, nvm) and devops scripts legitimately edit these files, so
the operator gets a prompt rather than a wall. full-access does
not carry the rule (preserving its literal-full-access contract).
Known coverage gap, deferred to a bashlex follow-up: variable
indirection (dst=~/.bashrc; echo X > "$dst"),
process-substitution wrappers, and literal expanded paths that
don't use ~ or $HOME.
LLM HTTP retry policy
The OpenAI SDK's built-in retry is now disabled (max_retries=0)
and replaced with a policy LLMClient controls end-to-end:
| Aspect | Behavior |
|---|---|
| Retryable | 429 + 5xx + connection errors |
| Non-retryable | 4xx other than 429 |
| Backoff | Retry-After if present (seconds or HTTP-date), capped at MAX_BACKOFF_SECONDS; otherwise jittered exponential |
| Wall-clock budget | MAX_TOTAL_RETRY_SECONDS so a long Retry-After cannot strand the caller |
| Cancellation | Sleeps run through _interruptible_sleep; a cancelled token aborts the loop immediately |
| Streaming | chat_stream retries only before any chunk has reached on_text_chunk |
The streaming-aware boundary is the key invariant: once any text
has been delivered to the host, mid-stream errors propagate
unchanged. Retrying would replay text the user already saw.
Raised exceptions carry .streamed so hosts (and the
LLM_CALL_COMPLETED error event in runtime/llm_call.py) can
choose between regenerate-from-scratch and resume-style retry
without counting LLM_TEXT events themselves.
The historical "upstream" substring matching 502 bodies and
silently falling back to non-streaming (bypassing the retry
policy) is fixed.
MCP error classification
agentao/mcp/client.py now exposes:
from agentao.mcp.client import classify_mcp_error, McpErrorKind
# McpErrorKind is one of: AUTH, SESSION_EXPIRED, TRANSPORT_DROPPED, OTHERThe call_tool retry path is now bucketed:
| Bucket | Behavior |
|---|---|
AUTH (401/403/unauthorized/forbidden) |
Surface immediately; no retry. Retrying with the same creds just produces another 401/403 plus a reconnect storm. |
SESSION_EXPIRED |
Reconnect, retry once. |
TRANSPORT_DROPPED (anyio resource errors, httpx remote-disconnect, stringified pipe/connection close) |
Reconnect, retry once. |
OTHER |
Surface without reconnecting. |
Order matters: AUTH wins over session/transport so a server
stuffing multiple signals into one message
("401 Unauthorized: session expired") doesn't trigger a retry
storm. connection refused is intentionally not in
TRANSPORT_DROPPED — the server isn't listening at all and a
reconnect would fail identically.
The live transport is now disconnected before reconnecting so the
old subprocess / SSE stream doesn't leak for the manager's
lifetime.
Config trust boundary (#25)
Three small but load-bearing escalation paths closed:
-
PermissionEngineno longer loads project-scope
.agentao/permissions.json. A checked-in
{"tool": "*", "action": "allow"}would have defeated the user
policy because the engine returns on the first matching rule.
Only<user_root>/permissions.jsonis honored; a stray project
file is logged once and ignored. Permissions are a user/host
concern, not a cwd concern — same model OS perms / IDE
workspace-trust use. -
Project
.agentao/mcp.jsonis add-only. It may declare new
server names but cannot override a user-scope entry with the same
key. Collisions warn and skip. Prevents a checked-inmcp.json
from silently redirecting a known name (e.g."github") to a
different transport. -
McpToolconsultsreadOnlyHint/destructiveHintonly on
trusted servers, and only to add friction. Per the MCP spec we
never make tool-use decisions from annotations on untrusted
servers. When both are set on the same trusted server,
destructiveHintwins (security-positive bias — a Codex review
during #25's/simplifypass caught the gap). New
McpTool.mcp_annotationsproperty for host introspection.
Other improvements
-
SearchTextToolskip-list + ripgrep fallback — default-skips
.git,node_modules,.venv/venv,__pycache__,.tox,
dist,build,target,.next/.nuxt, and the usual language
caches. Fallback chain becomesgit grep→rg→ Python so
non-git trees and Windows boxes withrg.exebut nogitare
rescued. Cross-platform viashutil.which; noIS_WINDOWS
branching. Caller opts back in by naming a skip-dir indirectory
orfile_pattern. -
/replay delete <id>and/replay delete all— mirrors
/sessions delete. Removes specific replay files by id-prefix or
wipes all of them (single-key confirmation). The active
recorder's file is skipped/refused so an in-flight write isn't
orphaned. -
agentao.redact.mask_secret— canonical helper for hiding
credentials in logs, audit events, and host UIs. Default shape
sk-A...3xZkfor long values,********(same length) for
short values,(not set)for missing. No internal callers
migrated in this release; replaces the next ad-hoc truncation
site that needs it. -
Windows UTF-8 console enforcement at import —
agentao/__init__.py
now calls_ensure_utf8()on package import. On Windows it sets
PYTHONIOENCODING=utf-8(only if unset), switches the console
code page to CP_UTF8 (kernel32.SetConsoleOutputCP/SetConsoleCP),
and reconfiguressys.stdin/stdout/stderrwith encoding=utf-8
and a lenient error handler. POSIX is a single-instruction no-op.
Embedded hosts that import Agentao from a Windows console no
longer hitUnicodeEncodeErroron CJK file paths, curly quotes,
or model-output emoji. -
ToolExecutorparallel batches now propagate contextvars —
ThreadPoolExecutorworkers don't inherit the parent thread's
context, socontextvars.ContextVarstate (host turn IDs, request
metadata, structured-logging scopes) was lost when tools ran in
parallel.contextvars.copy_context()is now captured on the
parent thread per submission and dispatched through
Context.run()— callingcopy_context()inside the worker would
copy the worker's empty context. Each plan gets a fresh context.
Documentation
- Developer guide §7.6 — WeChat bot blueprint (mirrored to
examples/wechat-bot/): long-polling daemon, contact-scoped
permission preset, contrast table separating personal-account
from Official Account / Enterprise WeChat webhook track. - Host contract clarifications:
EventStream.add_observer
fan-out documented; newarchitecture/embedding-vs-acp.{md,zh.md}
decision tree; internalTransport/AgentEventchannel
documented as a peer surface; newPUBLIC_EVENT_PROMOTION_PLAN.md
stagingMCPLifecycleEventandLLMCallEventpromotion. examples/protocol-injection/— runnable end-to-end sample
replacing every host→Agentao injection slot with a small adapter.examples/personas/— new home for prompt-configuration
samples; seeded withdaily-driver(evidence-first, citation-
format table) andkawaii-buddy(情绪价值小助手 with mascot
board).examples/skills/— host-agnostic skill gallery (#26)
withzootopia-ppt,pro-ppt,ocrand bilingual README.
Co-located skills indata-workbench,ticket-automation,
batch-schedulernow carry callouts pointing back to the gallery.docs/design/permission-hardening-plan.{md,zh.md}rev 3 —
marked PRs 1–5 all landed; §10 reframed as post-ship follow-ups
with only thebashlexsupersedence of PR 5's regex tier
remaining open.docs/design/pi-mono-borrow-review.{md,zh.md}— decision
record from surveying ~590 commits inpi-monobetween v0.66 and
v0.73, with keep / cut / reframe verdicts for each candidate.
What did not change
- No public API or wire-format change.
agentao.host
Pydantic models, thehost.events.v1.json/host.acp.v1.json
schemas, and theAgentao(...)constructor signature are
byte-for-byte unchanged from 0.4.2. - No required code change to upgrade.
pip install -U agentao
is the only step. - No CLI command rename.
/replay deleteis additive; existing
/replay show,/sessions delete, etc. are unchanged. - The
agentao.harnessdeprecated alias is still alive. Its
removal stays scheduled for 0.5.0.
Tests
2466 passed, 4 skipped, 9 deselected under
AGENTAO_TEST_LIVE_MODELS=0 AGENTAO_TEST_LIVE_LLM=0 (CI's offline
mode). The strict typing gate (mypy --strict --package agentao.host) and the schema drift gate
(scripts/write_host_schema.py --check,
scripts/write_replay_schema.py --check) both green.
Upgrade
pip install -U agentaoIf you embed Agentao with a custom permission engine and want the
hardline floor disabled (because you sandbox in a container or run
your own deny pipeline), pass enable_hardline=False:
from agentao.permissions import PermissionEngine
engine = PermissionEngine(
project_root=...,
user_root=...,
enable_hardline=False,
)If you previously kept a project-scope .agentao/permissions.json,
move its rules into your user-scope file
(<user_root>/permissions.json) — the project file is now ignored
with a one-time warning.
Out of scope (deferred)
bashlex-based supersedence of PR 5's regex tier. Variable
indirection, process-substitution wrappers, and literal expanded
paths in the workspace-write sensitive-write preset will be
closed by a parser-based pass (mirroring the hardline scanner's
approach). Not blocking —workspace-writealready ASKs on
everything not on the read-only allowlist.HOMEresolution undersudo. The hardline patterns match
~ / $HOME / ${HOME}syntactically. Reopen if a real bug surfaces.agentao.harnessalias removal — still scheduled for 0.5.0.docs/releases/v0.4.0.mdandv0.4.1.md— backfilling these
remains deferred (carried over from v0.4.2.md).