Skip to content

fix: apply reviewed maintenance repairs for #3394 - #3686

Closed
kovtcharov wants to merge 6 commits into
amd:feat/bypass-permissions-shell-gatesfrom
kovtcharov:codex/maintenance-pr-3394
Closed

kovtcharov wants to merge 6 commits into
amd:feat/bypass-permissions-shell-gatesfrom
kovtcharov:codex/maintenance-pr-3394

Conversation

@kovtcharov

Copy link
Copy Markdown
Contributor

Follow-up fixes for #3394. Merged current origin/main into the isolated maintenance worktree without committing. Resolved the changelog conflict by retaining both bypass and project-map entries; git diff --name-only --diff-filter=U is empty. The staged mainline changes are the merge, not unrelated edits.

This PR targets the original feature branch because the current account cannot push to amd/gaia. It carries the prepared maintenance changes for that PR.

Test plan

  • Previously completed local validation: Validation: 265 Python shell/stdio/server tests passed (3 SWIG deprecation warnings), including real compound-command execution and audit-record assertions. go test ./internal/ui/chat/... ./internal/cli/... and go build ./... both pass. Changed-file Black/isort/Flake8 checks and git diff --check were run. New regression asserts a granted gh redirect under bypass reaches shell=True with its intact original command.
  • Fresh CI on the combined feature branch.
  • Remaining live-model, hardware or platform checks described in the original PR, where applicable.
  • Human review before merging the original PR.

This commit also resolves main conflicts. Preserve both parents with a merge commit or fast-forward the original branch to this head; squashing/rebasing this follow-up discards the main ancestry needed to resolve the original PR conflict.

Ovtcharov and others added 5 commits September 8, 2026 11:14
Bypass permissions turned off the confirmation prompt and nothing else, so a
user who had granted blanket consent still could not get the agent to run a
build or a test suite. Compound commands were refused before they parsed, and
no interpreter, test runner or package manager was in the allowlist — "verify
your work before claiming it is done" was not something the agent could do,
however well it was prompted.

Under bypass, the shell's own gates now come off with the prompt: operators
(&&, ||, ;, >) parse and run, the read-only binary policy is replaced by a
developer set, and the rate limit is lifted. Off by default and byte-identical
to before when off — ALLOWED_COMMANDS is untouched and the read-only tier keeps
claiming exactly what it claims, so amd#2768's hardening of it stays meaningful.

The developer set adds node, npm, make, cmake, go, cargo, sed, awk, curl,
sleep, timeout, export, cp and mv. It also names python, python3, pytest and
gh, which are consolidation rather than new reach: those already had paths via
execute_python_file and shell:execute skill grants, and listing them here gives
bypass one answer to "may this binary run" instead of three. rm stays out — not
a boundary, since anything in the set can delete a file, but a tripwire against
an accidental recursive delete.

Stdio transport only. The HTTP surface is a bound socket, and an unguarded
shell reachable over it is remote code execution rather than a relaxed
permission model, so the handler pins bypass off and the request model forbids
unknown fields. bypass_permissions is also a separate handler attribute from
auto_approve_gated_tools: an unattended harness that merely pre-approves
prompts must not inherit an unguarded shell.

Skipping the prompt does not skip the record. Every command run under bypass is
written to file_audit.log with its full arguments and its per-segment
breakdown, so `cd build && make` is two auditable invocations rather than one
opaque string. The per-segment walk is what produces that record, so it stays
intact in both modes.

Closes amd#3373
Closes amd#3374
The bypass-control guard read `.path` off every entry in the app's route
list, which raises on the `_IncludedRouter` wrapper that `include_router`
leaves on the CI FastAPI version. Skipping entries without a `.path` would
have walked straight past the mounted API and left the assertion passing on
an empty set, so the walk now descends into sub-routers and asserts it
reached a known route before checking for bypass paths.
…oint

The guard asserted /v1/gaia/query was reachable before checking for bypass
routes, and that endpoint is not mounted in the CI environment — so the
guard failed there while passing locally. Keying it on the /v1/gaia/ prefix
still proves the walk descended into the included router, without depending
on which endpoints a given environment mounts.
… says

Under bypass permissions, `ls\nrm -rf /tmp/x` ran both commands while only
`ls` was ever checked. `rm` is deliberately outside the developer command
set, so the line break walked a refused binary straight past the allowlist,
and the audit log recorded one invocation where two had run.

shlex was asked to treat `;&|<>` as punctuation but left the newline in its
whitespace set, so it vanished during tokenisation and `_split_pipeline`
never saw a break. The newline joins the punctuation set and comes out of
`whitespace`, so an unquoted one now survives as its own token.

shlex emits a run of adjacent punctuation as a single token, so a newline
also arrives fused to whatever preceded it — `;\n`, `&&\n`, `\n|\n`. Segment
splitting keys on that rather than on set membership alone.

A newline inside quotes is still ordinary data: `echo "a\nb"` stays one
operand, and is covered by a test so the fix cannot regress into refusing it.
@github-actions github-actions Bot added documentation Documentation changes devops DevOps/infrastructure changes chat Chat SDK changes mcp MCP integration changes llm LLM backend changes cli CLI changes tests Test changes performance Performance-critical changes agents agent::email Email agent changes tui Go terminal UI (gaia-tui) labels Sep 11, 2026
@kovtcharov
kovtcharov marked this pull request as ready for review September 17, 2026 20:04
@github-actions

Copy link
Copy Markdown
Contributor

Request changes

This is a large, mostly-good bundle: a task-start project map for the flagship agent, a --trace recorder for the TUI, a real fix for Claude tool-call history, a canonical port map, and the removal of a decorator's silently-ignored keyword arguments. Four things should be sorted before merge; none of them is in the new feature code.

The runner-health monitor will now page the team about healthy machines. Tightening the staleness limit to three days works for the scheduled Monday run, but the monitor can also be started by hand — and any manual run from Thursday onward sees a perfectly normal weekly heartbeat as four-plus days old, reports every runner offline, and fires the Teams alert. Pick a limit that sits above a healthy age but below one genuinely missed weekly beat.

The TUI-driving skill lost two sections that have nothing to do with this change. Rewriting the dev-mode instructions also deleted the note about how cards and conversation context interact, and the whole "how to report what you saw" section. Both look like collateral from editing the tail of the file — please put them back.

Two user-visible defaults change with nothing telling existing users. The Agent UI's MCP server and the Telegram health endpoint both move off their old port, so anyone with a saved MCP client config or a health check pointed at the old one gets a silent connection failure. Separately, the tool decorator now rejects keyword arguments it used to ignore, which means an agent written outside this repo that passes them stops importing. The reference docs were updated correctly, but a changelog line for each would save the support round trip.

Some genuinely useful model-facing guidance was deleted along with the dead keyword arguments. The removed text included instructions like "after indexing a document you must query it, never answer from memory" — that guidance was never reaching the model, so removing it changes nothing today, but it's now gone from the tree entirely rather than moved to the docstring, which is where it would take effect.

Real-world evidence

N/A — no evidence bundle was produced for this PR, and the CI lane here has no inference backend. The verdict rests on static review plus the test suite the PR ships, which is substantial: new unit tests for the project map, the trace writer, the port map, the publish-workflow gate, Claude tool history, and the email fixes. The user-facing surfaces this PR touches — the Agent UI MCP port move, the TUI --trace flag end to end, the flagship's project map in a real prompt, and gaia telegram start --background staying alive — are not exercised by any evidence I can see and are worth a pass on the strix-halo lane before release.

🔍 Technical details

🟡 Important

1. Manual runs of the runner monitor now report healthy runners as stale (.github/workflows/monitor_selfhosted_runners.yml:84)

runner_heartbeat.yml fires Sunday 01:00 UTC; the monitor is scheduled Monday 01:00 UTC, so the scheduled path sees age ≈ 86400 and passes. But the monitor also has workflow_dispatch:, and a manual run any time from ~Thursday onward sees age > 259200 for a runner that beat normally on Sunday. Every runner lands in missing, Fail if any runner missing exits 1, and the if: failure() && steps.check.outputs.missing != '' guard on the Teams step is satisfied — so the "runners are offline" card goes out naming healthy boxes. The old 8-day limit couldn't do this.

A threshold derived from the cadence rather than from the Monday check keeps both properties (catches one missed beat, survives a mid-week manual run):

            # One missed weekly beat is >= 8 days old at the Monday check; a
            # healthy beat is <= 7 days old whenever this runs, including a
            # manual mid-week dispatch. 7d + 12h grace separates the two.
            if [ "$age" -gt 648000 ]; then
              echo "  STALE — no heartbeat in over 7.5 days"

2. .claude/skills/driving-the-tui/SKILL.md drops two unrelated sections

The hunk at @@ -96,20 +96,14 @@ replaces the mode paragraph and removes ## The card/context trap (the tool_result.render / SSEClient.appendTurn / displayedCard invariant) and ## Reporting what you saw (which pointed at CLAUDE.md → How You Communicate). Neither relates to the mode-agreement change this PR is making, and the file also loses its trailing newline. Restore both sections beneath the new mode guidance.

3. Breaking changes with no changelog entry

  • AGENT_UI_MCP_PORT moves the module-run default and src/gaia/ui/routers/mcp.py's StartAgentServerRequest.port from 8765 → 8766, and TELEGRAM_HEALTH_PORT moves the adapter's health server and gaia telegram status from 8765 → 8768. Both are correct fixes (gaia mcp serve already used 8766, so the module constant was the outlier), but a saved claude_code MCP entry or an external health probe on 8765 now fails with no hint. docs/guides/mcp/agent-ui.mdx and docs/guides/telegram-adapter.mdx were updated; nothing states the change was a change.
  • src/gaia/agents/base/tools.py:46 — @tool(...) now raises TypeError for name= / description= / parameters=. Every in-tree call site is cleaned (verified: no matches for @tool([^)]*(name=|description=|parameters=)) in src/, hub/, tests/, or docs/), so nothing in this repo breaks. But gaia.agents.base.tools.tool is public SDK surface, and a third-party or user-scaffolded agent that passes those now fails at import rather than at runtime. Fail-loudly is the right call per CLAUDE.md; it just needs to be announced. docs/spec/tool-decorator.mdx and docs/sdk/core/tools.mdx document the new behaviour well.

4. Deleted prompt guidance that would have worked in a docstring

src/gaia/agents/tools/rag_tools.py dropped index_document's "IMPORTANT: After successfully indexing a document, you MUST call query_specific_file… Never answer from memory/knowledge after indexing", and src/gaia/agents/tools/file_tools.py dropped search_file's "REQUIRED STRATEGY / NEVER give up after just 1-2 failed searches". Both sat in the inert description=, so the model never saw them — but the model does see the docstring, and index_document's docstring is one line ("Index a document with path validation and detailed statistics"). Moving the useful sentences into the docstrings is a behaviour change, so it wants an eval run; leaving them deleted loses authored intent with no record. At minimum, keep the index_document instruction:

        def index_document(file_path: str) -> Dict[str, Any]:
            """Add a document to the RAG index so its contents can be queried.

            After a successful index, query the document (query_specific_file or
            query_documents) before answering — never answer from memory.
            """

🟢 Minor

  • src/gaia/agents/base/project_map.py:2928 — [cwd, *list(cwd.parents)[: _MAX_ASCEND - 1]] walks cwd plus 3 parents, but docs/guides/project-map.mdx says "the nearest repository up to four levels above it". One of the two is off by one.
  • .github/workflows/runner_heartbeat.yml:116 — the watchdog's 10-minute deadline treats "queued" as "no runner has the label", but a healthy self-hosted box already running a long job (an eval lane, say) also leaves the heartbeat queued past 10 minutes. Querying /actions/runners for status would distinguish offline from busy.
  • tui/internal/daemon/client.go:630 — callerMode hardcodes the email → GAIA_EMAIL_AGENT_MODE / gaia → GAIA_GAIA_AGENT_MODE mapping in Go, a second copy of knowledge the Python side already owns. Adding a built-in agent now means editing this switch, and forgetting silently yields user mode.
  • src/gaia/llm/providers/claude.py:4669 — _tool_name_map is instance state reset at the top of _to_anthropic_tools, so two overlapping chat() calls with different tool sets on one provider instance make _restore_tool_name raise. The error text calls this out, which is good, but threading the map through the call (returned alongside the converted tools) would remove the race instead of diagnosing it.
  • tui/internal/event/trace.go:8927 — an empty or whitespace-only frame fails json.Compact, passes utf8.Valid, and lands as rec.Unparsed = "", which omitempty erases — producing a record with only at and seq and none of the three keys the docs tell readers to check.

Strengths

  • The @tool cleanup is the safe version of a risky change. The removed keywords were already inert (documented as known issue Update Driver Check #3 in docs/spec/agent-ui-known-issues.md, now correctly deleted), so the JSON tool schema the model sees is byte-identical and no eval is needed — and every in-repo call site was actually migrated, so the new TypeError cannot fire on an in-tree import. tests/unit/test_tool_decorator.py pins the new rejection.
  • tests/unit/test_publish_workflow_contract.py is the right shape for the bug it guards. The defect was a step that stayed green whether the tests passed, failed, or never ran; asserting the absence of || and 2>/dev/null in the step's run catches a regression that no behavioural test could. Adding [api] alongside [dev] correctly explains why the old fallback existed.
  • The port map is centralized and pinned. src/gaia/mcp/ports.py plus tests/mcp/test_port_map.py asserting the parser defaults match the constants (not just the constants' values) means the bridge, Agent UI MCP, TUI MCP and Telegram health defaults cannot drift apart again — including the standalone gaia-mcp console script, which is why build_parser was extracted.
  • project_map.py ships with its own guardrails. A per-section token budget enforced on every render, a cheap filesystem fingerprint for cache invalidation, __init_subclass__ raising on a wrong MRO (the failure that would otherwise be symptomless), is_agent_own_source correctly exempting an installed wheel under a user's venv, and 721 lines of tests covering the predicate, the 32K budget, cache invalidation and the once-per-session index trigger.
  • TraceWriter gets the boring details right: nil-receiver-safe on every method so call sites tap unconditionally, per-event flush so a crashed run keeps what it had, sticky first-error returned from Close, 0600 under 0700, base64 for non-UTF-8 frames, and it removes the 0-byte file it created when a launch dies early.
  • The Anthropic tool-history work is thorough — sanitizing <skill>/<tool> names outbound, detecting sanitization collisions, and the paired post_tool_messages fix in src/gaia/agents/base/agent.py so dedup guidance lands after the complete result group rather than inside it.

No prompt-injection attempts found in the diff or changed docs.

@kovtcharov-amd
kovtcharov-amd force-pushed the feat/bypass-permissions-shell-gates branch from 79eb81d to 0bd22d1 Compare September 21, 2026 17:44
@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

This PR no longer contributes anything — its base branch already contains all of it. Recommend closing it rather than resolving the conflict.

GitHub shows it as conflicting, but that conflict is only the branch trying to revert work the base has since moved past. Merging it in either direction produces nothing new; merging it the way GitHub would would undo roughly 400 lines the base branch now has. Note this targets feat/bypass-permissions-shell-gates, not main, so the conflict is with that branch's newer state, not with main. Target PR #3394 is still open, so if a repair is still wanted it needs to be re-derived against the branch as it stands today.

🔍 Technical details

Probe (non-destructive, no branches touched):

git merge-tree --write-tree -X theirs fork/codex/maintenance-pr-3394 origin/feat/bypass-permissions-shell-gates
git diff --stat origin/feat/bypass-permissions-shell-gates <tree>

Empty residual — the base is a superset. Running it with the branch winning instead yields 7 files changed, 46 insertions(+), 412 deletions(-) across shell_tools.py, test_shell_guardrails.py, security-model.mdx, shell-tools-mixin.mdx, the gaia npm SPEC.md/CHANGELOG.md, and tui/internal/cli/root.go, which is the revert described above.

Nothing was pushed to this branch.

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

This branch looks like an older snapshot of #3394 rather than a separate change, so I've left its merge conflicts alone — resolving them would mean redoing work #3394 has already done, on a worse base.

Worth a maintainer deciding between the two before either gets more effort. If #3394 is the one being carried forward, this can close; if there's a repair here that #3394 lacks, cherry-picking it across is cheaper than reconciling two copies.

🔍 Technical details

git cherry refs/3394 refs/3686 marks three of the four non-merge commits as already applied on #3394 (patch-identical): the route-walk guard commits and fix(shell): a newline separates commands under bypass. The fourth, feat(shell): --bypass-permissions lifts the shell guardrails too, is #3394's fc1437a2e under a different SHA.

Neither branch contains the other (git merge-base --is-ancestor fails both directions); they fork from the same cfc3d2959. #3394 is strictly ahead: it carries two later fixes this branch doesn't (test(shell): fix the executor fixture for the current @tool signature, fix(shell): refuse a redirect on the argv-only skill-granted path) and has merged main up to recent main, whereas this branch's last merge is far older.

Conflicts here are in src/gaia/agents/tools/shell_tools.py, tests/unit/test_shell_guardrails.py, tui/internal/cli/root.go, and the gaia hub CHANGELOG.md/SPEC.md — all files #3394 also rewrites, so a resolution on this branch would be invalidated the moment #3394 lands.

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

This PR no longer adds anything — everything in it is already on the branch it targets, and that branch is now ahead of it in three places.

#3394's branch (feat/bypass-permissions-shell-gates) absorbed main on its own, so the merge this PR performed has been redone there from a newer starting point. Where the two versions differ, #3394's is the better one every time. My recommendation is to close this and let #3394 land; nothing needs to be salvaged first.

🔍 Technical details

git log --cherry-mark --left-right origin/feat/bypass-permissions-shell-gates...0cd113b42 marks three of this PR's four commits = (patch-identical to commits already on the base): 79eb81def, a90d1ca52, 1fa004012.

The remaining two:

  • 5bebb58d9 feat(shell): --bypass-permissions lifts the shell guardrails too is a divergent copy of the base's fc1437a2e — same 15 files, 953 vs 959 added lines. Diffing the added lines per file, every delta is in the base's favour:
    • hub/agents/gaia/npm/SPEC.md — the base adds a 7-line paragraph this copy lacks, documenting that the TUI refuses --use-claude for a daemon-transport agent.
    • tui/internal/cli/root.go — the base's help text scopes the flag correctly ("subprocess agents only: …"); this copy's does not.
    • tests/unit/test_shell_guardrails.py — this copy leaves an unused import pytest the base dropped.
  • 0cd113b42 is a merge commit (parents 79eb81def + cfc3d2959) whose combined diff is 7 files. cfc3d2959 is the old base tip, so those resolutions are against a main that has since moved; 8d43fcd93 on the base redoes them against current main.

The base also carries two repairs this PR does not have at all: 59000a3e9 test(shell): fix the executor fixture for the current @tool signature and 0bd22d1b3 fix(shell): refuse a redirect on the argv-only skill-granted path.

Merging the base in here produces 34 conflict hunks across 11 files, all of which resolve to the base's side. I aborted rather than push a resolution that yields an empty PR.

…ance-pr-3394

The base branch took main's merge, which replaced the shell executor this
branch was repairing: the whole-line pipeline (_split_pipeline, _tokenize,
_check_rate_limit) became a per-step walk (_parse_line -> _Step ->
_run_step/_run_pipeline), and chaining with &&, ||, ; and | now runs by
default instead of being bypass-only. Every conflict was that same
stale-vs-current split, so each one resolves to the base side.

The three maintenance repairs this branch carries are already on the base
under different SHAs, and the base carries two the branch does not:
_is_lone_granted_segment / _redirects (refusing a redirect on the argv-only
skill-granted path) and the executor-fixture repair. _is_granted_binary is
superseded by _is_granted_segment, which applies policy_argv before matching.
The merged tree is therefore identical to the base tip.

Conflicts resolved to the base side, all eleven:
  src/gaia/agents/tools/shell_tools.py
  tests/unit/test_shell_guardrails.py
  docs/plans/security-model.mdx
  docs/spec/shell-tools-mixin.mdx
  hub/agents/gaia/npm/{CHANGELOG.md,SKILL.md,SPEC.md}
  hub/agents/gaia/python/gaia_agent/stdio.py
  hub/agents/gaia/python/tests/test_stdio.py
  tui/internal/{cli/root.go,ui/chat/bypass.go}
@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

Closing this — it has no changes left to merge. Everything it proposed was folded into #3394 directly, so its diff against that branch is now empty and merging it would only create an empty merge commit.

Nothing is lost by closing: the repairs are already in #3394. Reopen if you find something that did not carry over.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent::email Email agent changes agents chat Chat SDK changes cli CLI changes devops DevOps/infrastructure changes documentation Documentation changes llm LLM backend changes mcp MCP integration changes performance Performance-critical changes tests Test changes tui Go terminal UI (gaia-tui)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants