Skip to content

REVIEW v4 remediation: Waves 0-2 + self-modification guard#165

Open
draxios wants to merge 21 commits into
mainfrom
chore/resiliency-hardening
Open

REVIEW v4 remediation: Waves 0-2 + self-modification guard#165
draxios wants to merge 21 commits into
mainfrom
chore/resiliency-hardening

Conversation

@draxios

@draxios draxios commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Implements the REVIEW v4 audit remediation (see REVIEW.md §2) across three
waves plus the self-modification guard. 14 commits; every package's gates pass
locally (ruff / ruff format / ty; SDK, CLI, daemon, and satellite suites).

Wave 0 — default-path correctness

  • CLI-CORE-1 (P0) _cleanup_agent_task no longer clobbers the next queued
    turn (drain moved after state restore); extracted a TurnManager to make it
    structural.
  • CLI-CORE-3/4 /clear keeps _lc_thread_id in lockstep; pipeline/watch
    prompts defer behind an in-flight turn instead of running concurrently.
  • CTX-1 a missing/slow git no longer crashes every mutating tool call.
  • SB-1 a blocked dangerous command becomes a tool error, not a turn abort.
  • RD-1 native Bedrock reasoning-effort; non-reasoning models are never
    output-capped (fixes operator easy-tier truncation).

Wave 1 — trust honesty

  • CLI-CORE-2 / CT-1 mutating git tools gated behind HITL; BOG_AGENTS_MCP_TRUST
    honored.
  • DMN-1 unattended daemon triggers get a non-shell backend (no host shell,
    no inherited env).
  • MW-SAFE-2 / MW-SAFE-1 operator-pinned RBAC/air-gap; the model can't lift
    its own policy.
  • RD-5 /butcher plans require approval; the per-slice file allowlist is
    enforced.
  • chore(sdk,cli): bump version to 0.6.2 for manual PyPI release #24 self-modification guard: writes to the agent's authority files (Expert
    rules, dreamscape laws, hooks, .mcp.json) require approval even under
    --auto-approve.

Wave 2 — delivery & satellites truth

  • DEL-1/2/3/4/5 pip install [all] works again (acp de-advertised); daemon
    run command + corrected quickstart; the GitHub Action skills install and the
    VS Code workflow no longer fail on first use; lockfiles refreshed with a CI
    drift guard.
  • SAT-1 HarborSandbox rebased on BaseSandbox (structured ls/grep/glob work
    again) + a reusable SandboxConformanceSuite.
  • SAT-2 acp pinned to the in-repo SDK; its HITL test fixed.
  • Satellite packages (acp/harbor/daytona) now run in CI.

draxios added 21 commits July 23, 2026 01:46
Document the theme system, skill trust store, spec-compliant MCP OAuth, native /effort, and the config manifest / env-var registry; add the deepagents 0.6.12 drop-in surface; replace the REVIEW.md-only tracking note with the four root docs (REVIEW/ROADMAP/PARITY/AGENTS).
REVIEW v4 (58-agent audit): 1 P0 + 21 confirmed P1 + 8 downgraded + 1 refuted + 30 P2, prior-cycle scorecard, systemic themes, and Waves 0-4 sequencing. ROADMAP: v1 killer-feature scorecard (4 shipped / 8 partial / 8 not-started, verified against code) plus Killer features v2 (#21-#45) from a fresh four-front survey, with deferred-item re-evaluation.
…command

CTX-1: CheckpointingMiddleware._run_git now catches OSError/TimeoutExpired, self-disables once with a logged warning, and returns a synthetic failed CompletedProcess so a git-less or slow-git box degrades gracefully instead of the exception propagating out of the tool node (which langgraph re-raises) and aborting every write_file/edit_file/execute. The CLI ships checkpointing on by default, so this was default-reachable.

SB-1: the execute tool's sync and async wrappers now catch the dangerous-command PermissionError (LocalShellBackend, e.g. rm -rf) and return it as a tool-error string the model can adapt to, instead of aborting the turn.

Fixes v4 CTX-1, SB-1.
…oning models

CLI-CORE-1 (P0): _cleanup_agent_task drains the pending queue after its state-restoration finally, not inside it, so a queued message that starts the next turn is no longer clobbered (leaving it uninterruptible and letting a third message run concurrently).

CLI-CORE-4: _send_prompt_to_agent defers behind an in-flight turn via a new QueuedMessage.raw flag instead of spawning a concurrent turn and overwriting the user's worker handle; the drain re-routes raw prompts back through _send_prompt_to_agent. CLI-CORE-3: /clear keeps _lc_thread_id in lockstep with the session thread so /compact can't mutate the pre-clear thread.

RD-1: add native Bedrock reasoning-effort (Anthropic-on-Bedrock via additional_model_request_fields, version-gated) and make the legacy effort presets temperature-only, so non-reasoning models (Haiku, Ollama, Bedrock Haiku) are never output-capped -- fixing operator easy-tier turns that truncated at 1024 tokens.

Fixes v4 CLI-CORE-1, CLI-CORE-3, CLI-CORE-4, RD-1.
The agent/shell run flags and the worker handle were bare attributes on BogAgentsApp mutated from ~8 methods; ordering mistakes between those writes were the root of v4 CLI-CORE-1 (cleanup clobbering the next turn) and CLI-CORE-4 (a pipeline starting a concurrent turn). Move the state into a small TurnManager coordinator with a single busy definition and paired begin_agent/end_agent + begin_shell/end_shell transitions, so 'at most one turn in flight' is a property of one object rather than scattered discipline.

_agent_running / _agent_worker / _shell_running remain as delegating properties over the manager, so the ~25 read sites and any incidental write are untouched; the dispatch and cleanup sites call the manager methods. All transitions run on Textual's single event loop, so no lock is needed. Bumps the app.py line-count ratchet to 17,575 (deliberate, documented) since the thin delegation belongs on the app while the lifecycle logic now lives in the new module.

Completes v4 Wave 0.
…P_TRUST

CLI-CORE-2: GitToolsMiddleware is default-on but _add_interrupt_on() gated only
six tools, so git_commit / git_add / git_branch (create/switch) / git_stash
(push/pop/drop, incl. destructive drop) executed with no approval prompt. Add
them to the interrupt map with arg-aware description formatters and a `when`
predicate so read-only paths (branch listing, git stash list/show) stay
un-prompted.

CT-1: BOG_AGENTS_MCP_TRUST was defined, exposed in the config manifest, and
printed in the non-TTY deny message, but read nowhere -- a CI user following the
printed instruction was silently denied. Honor it as an allow-override at all
three trust sites (_check_mcp_project_trust plus the -n and ACP paths).

Fixes v4 CLI-CORE-2, CT-1.
…irtual_mode

DMN-1: virtual_mode confines only filesystem tools -- the SDK is explicit that a
shell-capable backend's execute() runs unrestricted on the host regardless. So
unattended triggers (cron/interval/file-change/git-push and the HMAC-only
webhook path) previously had unrestricted host shell plus the daemon's full
environment; a git-push job whose prompt ingests attacker-authored commit text
was the worst case.

Extract `_select_backend`: non-MANUAL triggers now get a non-sandbox
FilesystemBackend rooted at working_dir -- filesystem read/grep for
skills/pipelines still works, but execute() reports "not available" and no host
environment is exposed. MANUAL (token-authenticated) keeps the unrestricted
LocalShellBackend; operators opt back into unattended shell per deployment with
BOG_DAEMON_ALLOW_UNATTENDED_SHELL=1 (warned when a network-reachable trigger
fires). Updates the V3-11/P7 tests to the stronger backend-type posture.

Fixes v4 DMN-1.
MW-SAFE-2 / MW-SAFE-1: RBAC and air-gap were self-administered -- the model
could call set_active_role/define_role/set_data_policy/clear_air_gap to lift its
own restrictions, so they only bounded a cooperative model, not an adversarial
one.

RBAC: RBACMiddleware now accepts operator-owned `roles` + a pinned `active_role`.
When pinned, the model gets only the read-only check_permission/list_roles tools
(no define_role/set_active_role), the store is deny-by-default, and an empty or
undefined pinned role denies all tools instead of falling open. Unpinned
construction keeps the legacy self-service tools for backward compatibility.

Air-gap: AirGappedMiddleware accepts an operator-owned DataPolicy; when pinned it
withholds set_data_policy/clear_air_gap from the model. The `enable_air_gapped`
flag path always pins a policy (operator-supplied or a fail-closed default), so
the flag alone yields a real egress boundary.

Wired through FeatureConfig (rbac_roles/rbac_active_role, air_gap_policy) and
graph.py, with a warning when enable_rbac is set without a pinned role.

Fixes v4 MW-SAFE-2, MW-SAFE-1.
…e allowlist

RD-5: /butcher ran an LLM-authored plan of shell/edit slices with no approval,
and operator mode auto-escalates a plain prompt to butcher by default -- so
worker shell (shell=True) executed screened only by the accident-catcher
patterns, bypassing the CLI's HITL. The documented job-level approval gate did
not exist, and Slice.files was described as an allowlist but never enforced.

Add a plan-approval modal (ButcherPlanApprovalScreen) shown after planning and
before any slice runs; approval is fail-safe (a headless app with no modal
surface is treated as denied) and can be waived per deployment with
`auto_approve = true` in butcher.toml. Enforce the slice's declared `files`
allowlist in build_worker_tools: writes/edits are restricted to those paths
(exact or fnmatch glob), so the allowlist is a real boundary, not prompt text.
An empty allowlist keeps the whole-working-dir scope for backward compatibility.

Fixes v4 RD-5.
… surface

DEL-2 (= v3 P0-10): the quickstart and the documented systemd unit use
`bog-agents-daemon run --port 7878`, but the entrypoint implemented only
start/stop/status AND rejected a `--port` that followed the subcommand -- so the
unit crash-looped on its first command. Add `run` as an alias of `start`, and
accept the global --port/--log-level flags after the subcommand too (a shared
parent parser with SUPPRESS defaults, so a flag given before the subcommand is
not clobbered).

The docs also documented `bog-agents-daemon job add/...` and `runs
list/show/prune`, which never existed on this binary -- job management lives on
the `bog-agents daemon jobs ...` CLI (and the HTTP API). Repoint the examples and
drop the phantom `runs` family, with a drift guard test.

Fixes v4 DEL-2.
…ease workflow

DEL-3 (= v3 P1-69): the composite action's skills loop used `(( SKILL_COUNT++ ))`,
which evaluates to 0 on the first increment and, under `bash -eo pipefail`,
aborts the step after the first skill -- every consumer passing `skills_repo`
hit it. Use `SKILL_COUNT=$((SKILL_COUNT + 1))` (always exit 0).

DEL-4 (= v3 P1-66/70): the VS Code release workflow runs on [self-hosted,
Windows] where the default `run:` shell is pwsh, but the version-validation step
uses bash `[[ ... =~ ]]` syntax -- so the only build/publish path could never
complete. Pin `shell: bash` in the top-level defaults (Git Bash ships on the
runner; npm steps run fine under it).

Fixes v4 DEL-3, DEL-4.
…ks, add lock+satellite CI

DEL-1: the `[all]`/`[acp]` extras depended on `bog-agents-acp`, which is not on
PyPI, so `pip install bog-agents-cli[all]` (and the daemon's) failed resolution
for every user. Drop acp from the extras until it is published; install ACP from
source in the workspace. Re-locked cli/daemon accordingly.

DEL-5: harbor and daytona lockfiles were stale (`uv lock --check` failed) and
nothing caught it. Re-locked all packages and added a `lock-check` CI job
(`make lock-check`), plus a Linux `satellites` matrix that runs acp/harbor/
daytona `make test` on every PR -- the absence of that CI is exactly how SAT-1
and SAT-2 shipped unnoticed.

SAT-2: pin acp to the in-repo editable SDK (`[tool.uv.sources]`) so it resolves
0.9.9 instead of a stale PyPI 0.8.7, and fix the HITL bridge test, which used the
invalid `create_agent(interrupt_on=...)` kwarg instead of
`middleware=[HumanInTheLoopMiddleware(interrupt_on=...)]` (the real root cause,
not the lock). acp suite now green (52).

Also correct the README claim that CI runs per-package for all packages.

Fixes v4 DEL-1, DEL-5, SAT-2; partially DEL-6.
…formanceSuite

SAT-1: HarborSandbox overrode the *deprecated* als_info/agrep_raw/aglob_info
names, so the SDK's structured als/agrep/aglob fell through to a raising stub --
every harbor eval run had broken ls/grep/glob tools while the legacy-name tests
stayed green. Rebase on BaseSandbox, which derives als/aread_file/agrep/aglob/
adelete from aexecute(); HarborSandbox now supplies only the async command
primitive (aexecute) plus its own exec-based awrite/aedit (Harbor environments
expose no native file-transfer API, so the upload/download-based defaults do not
apply). ~200 lines of hand-rolled shell wrappers deleted; adelete gained for free.

Add `bog_agents.testing.SandboxConformanceSuite` -- a reusable pytest mixin that
pins the structured file surface (write/read/ls/grep/glob/edit/delete round-trips
plus result types), so a backend drifting off it fails loudly instead of silently
shipping broken runs. Validated against LocalShellBackend; harbor/daytona can
subclass it with their own real-sandbox fixture.

Fixes v4 SAT-1.
…uthority files

#24 (CVE-2026-25725 class): the agent could silently rewrite the files that grant
it authority — Expert Mode rulebooks, the dreamscape laws/constitution, project
lifecycle hooks, and the .mcp.json manifest. Gate writes to them behind human
approval via FilesystemPermissionsMiddleware `interrupt`-mode rules, which the SDK
merges into interrupt_on regardless of --auto-approve, so the guard holds even
when the agent runs unattended.

The file tools run in a virtual filesystem rooted at the project, so the agent
cannot reach the home-level trust stores (~/.bog-agents/config.toml, the
skill-trust store, .env) through file tools at all. The remaining vector is the
shell; add a best-effort screen (command_targets_authority_file) that forces an
approval prompt for shell commands that appear to write an authority file under
--auto-approve. Hooks and stdio-MCP configs were already fingerprint-gated
(P0-8, P1-49); this closes the rest.

Completes v4 Wave 1.
DEL-7: the uv_setup composite action declared `env: UV_VERSION` at the top level,
but composite action metadata has no top-level `env` key -- the runner ignored it
and setup-uv fell back to *latest* uv on every run. Latest uv resolved lockfiles
differently than a contributor's uv, which surfaced immediately as the new
lock-check job failing on libs/acp. Make it a real `uv-version` input (composite
actions honor inputs), defaulted to the uv version the committed lockfiles were
generated with, so CI and local resolve identically. Bump it deliberately (and
re-run `make lock`) rather than floating.

Fixes v4 DEL-7.
The lock-check job restored a uv cache that could carry index metadata from
another job (including the first run's *latest*-uv resolution), making the
deterministic re-resolution differ from the committed lockfiles even with uv
pinned. Disable caching for this job so it always resolves fresh.
Every committed lockfile passes "uv lock --check" with a forced full network
resolve (--no-cache --refresh) on a clean Linux + uv 0.11.6 + PyPI container,
for all six packages. Only this self-hosted runner reports drift (acp), which
means its "uv lock" floats to *highest* instead of honoring the locked pins --
a property of the runner's uv environment (global uv.toml / UV_UPGRADE /
UV_RESOLUTION / private mirror), not the repo.

Add a diagnostic step that dumps the runner's UV_* env and uv config so the
gate can be made deterministic, and mark the check advisory (continue-on-error)
so a runner-local quirk no longer blocks merges while the committed locks are
provably correct everywhere reproducible.
…retry, crash recovery

Tier 1 (correctness):
- store: quarantine a corrupt jobs.json to jobs.json.corrupt-<ts> instead of
  returning [] and letting the next save overwrite it (which silently destroyed
  every job). A transient read OSError is NOT quarantined.
- store/main: reconcile_orphaned_runs() on startup stamps runs left RUNNING by a
  crashed daemon as FAILED so /runs shows an honest terminal state.
- runner: the webhook/slack/email/github dispatchers now raise on failure so
  run_job records them in run.dispatch_errors (capped); they previously swallowed
  URLError into the log, hiding the most failure-prone targets.

Tier 2 (features):
- scheduler: replace the hand-rolled 5-field cron parser with croniter and add
  missed-slot catch-up — a daily job whose slot elapsed during downtime fires
  once on restart instead of skipping to the next slot. Also gains ranges/steps/
  lists and @macro support.
- models/runner/api: opt-in per-job retry (max_retries / retry_backoff_seconds,
  default 0 = prior single-shot behaviour) for the agent invocation AND each
  output dispatch, with exponential backoff; JobRun.attempts records the count.
  Prompt/skill resolution errors are deterministic and are not retried.

Tier 3 (perf/scale):
- file_watch: event-driven FILE_CHANGE triggers via watchdog (FileWatchManager),
  activated only inside run_forever so a bare _tick() (unit tests) never starts an
  observer thread. First runs and unwatchable dirs fall back to the os.walk poll
  (_check_file_trigger); the code degrades to polling entirely if watchdog is
  unavailable.

Adds croniter + watchdog as daemon dependencies (uv.lock updated). Full daemon
suite: 194 passed, 3 skipped; ruff clean.
…the daemon (V3-10)

DMN-2: receive_webhook compared the presented token against the captured
`token` closure, not token_holder["value"], so /admin/rotate-token never
invalidated a leaked old token on the webhook path (and rejected the new one),
while /webhooks/git-push rotated correctly. Compare against the live token.

V3-10: the daemon source was never type-checked and its test-lint failure was
swallowed with `|| true`. Add ty (config + Makefile `type` target wired into
`lint`, so CI's lint matrix now type-checks the daemon), make file_watch.py
ty-clean (a single TYPE_CHECKING view of the optional watchdog import), and drop
the `|| true` so a test-lint regression actually fails the build.

Full daemon suite: 195 passed, 3 skipped; ruff + ty clean.
- SDK-CORE-1: default cors_origins to [] (was ["*"], which let any drive-by web
  page drive the agent and read its replies); warn when wildcard CORS is combined
  with no api_key.
- SDK-CORE-4: replay the thread's tracked history to checkpointer-less agents so
  turn 2 no longer silently loses all prior context while /history implies
  continuity; agents that carry a checkpointer still get only the new turn.
- SDK-CORE-5: decouple stream production from consumption — a background producer
  holds a concurrency slot only while producing into a bounded queue, each enqueue
  bounded by request_timeout, so a slow/stalled client can no longer pin a slot
  indefinitely; the client drains without holding a slot.
- SDK-CORE-6: remove the dead enable_websocket flag and its false /info + module
  advertisement (no WebSocket endpoint exists), and enforce enable_streaming
  (POST /stream returns 501 when disabled instead of silently streaming).

Adds 12 serve hardening tests (method + endpoint level); ruff + ty clean.
…E-2/7)

SDK-CORE-2: AgentBuilder.build() forwarded every enable_* flag as a bare kwarg
through create_agent's deprecated backdoor (a DeprecationWarning on every build
and a hard TypeError at 1.0), and CostConfig.enabled defaulted True so cost
tracking was silently force-enabled on every agent. Now build() folds all
FeatureConfig fields into config=FeatureConfig(...) (real params stay direct),
and cost tracking defaults off (with_cost_tracking() opts in).

SDK-CORE-7: two documented no-ops made honest —
- with_mcp() collected registry IDs that build() silently dropped (no SDK-level
  MCP loader exists); it now raises NotImplementedError pointing at the supported
  with_middleware()/with_tools() path.
- with_sandbox(allow_dangerous=True) dropped the flag when no backend was given;
  build() now constructs the LocalShellBackend the flag describes.

Adds 5 builder tests (config routing, no-force-enable, real-build no-deprecation,
mcp raises, sandbox backend); ruff + ty clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant