Skip to content

perf(tools): use load_config_readonly on the approval guard path (#76194 salvage) - #76879

Merged
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/76194-approval-readonly-config
Aug 2, 2026
Merged

perf(tools): use load_config_readonly on the approval guard path (#76194 salvage)#76879
kshitijk4poor merged 2 commits into
NousResearch:mainfrom
kshitijk4poor:salvage/76194-approval-readonly-config

Conversation

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Salvage of #76194 by @spfcraze — cherry-picked to preserve authorship, plus one review follow-up commit.

Context — what this changes for users

Every terminal command an agent runs passes through the approval guard, and the guard path loaded config 2-3x per command via load_config() — which pays a defensive deepcopy on every call (~356µs of the ~376µs warm-cache cost). Swapping the 6 read-only guard-path sites to load_config_readonly() (the API built for exactly this; precedent #74211/#74322) is an 18.9x per-call improvement on a path that runs constantly during agentic work.

Swapped sites: load_permanent_allowlist, _get_approval_config, _get_cron_approval_mode, 2 sites in check_all_command_guards, and tirith_security._load_security_config.

Review & verification (full pipeline run — mutation-audit focused)

The regression risk for this PR class is a swapped site (or downstream caller) mutating the now-live cache dict. Independently audited:

  • All 6 sites traced: scalar .get() chains or fresh-dict/set copies only. _load_security_config returns a freshly-built literal dict (fully isolated). load_permanent_allowlist copies into a set().
  • _get_approval_config is the one site that returns the live sub-dict — ALL 4 production callers audited (deny-rule matcher, breaker threshold, mode, timeout, smart-policy readers): every one is a scalar read; no .pop/.setdefault/[]=/update/del anywhere.
  • save_permanent_allowlist correctly still uses mutable load_config().
  • Sibling audit: no remaining hot per-command load_config sites — the PR got them all. (Next-tier per-tool-call candidates noted for a separate follow-up: hook_output_spill, write_approval, tool_backend_helpers.)
  • Tests: new file's 3 pins green; 13 broader failures verified PRE-EXISTING on upstream/main (environment-sensitive approval tests). Mutation check: reverting production makes all 3 pins fail (legacy loader called 2x/5x, identity assert fails); restored → green. Ruff clean.

Follow-up commit (review finding folded)

  • _get_approval_config docstring now carries the live-cache do-not-mutate contract, so future callers don't trip the one latent footgun the swap creates.

Closes #76194.

spfcraze and others added 2 commits August 2, 2026 19:44
The terminal-command guard path loaded config 2-3x per invocation via
load_config(), which pays a defensive deepcopy of the entire config on
every call (~356us of the ~376us warm-cache cost measured on a real
config.yaml). All six swapped call sites were audited read-only — every
caller takes scalar reads or iterates the returned structures; none
mutate (the save path at save_permanent_allowlist keeps load_config) —
so they now use load_config_readonly(), the API built for exactly this
(precedent: NousResearch#74211, NousResearch#74322; the one unsafe-site lesson from NousResearch#56085's
salvage is covered by the mutation audit and a cache-integrity test).

Measured (real config.yaml, warm cache): load_config 376.0us ->
load_config_readonly 19.9us (18.9x); full guard pass
check_all_command_guards('ls -la','local') 930.7us -> 241.8us (3.85x).

Tests: new test_approval_config_readonly.py drives the real functions
against a temp HERMES_HOME — readonly call counts per function, a
no-deepcopy pin for the full guard pass, and cache-identity/integrity
checks. Existing test mocks retargeted from load_config to
load_config_readonly (same injection intent). Note: 6
test_approval_mode_parity failures are pre-existing ordering flakes —
identical with the change stashed on clean main.
Review follow-up on the NousResearch#76194 salvage: the readonly swap makes this
function leak the live config-cache 'approvals' sub-dict to callers.
All current callers are read-only (audited); the docstring now carries
the do-not-mutate contract for future ones.
@kshitijk4poor
kshitijk4poor enabled auto-merge (rebase) August 2, 2026 15:43
@kshitijk4poor
kshitijk4poor merged commit 024f3e0 into NousResearch:main Aug 2, 2026
35 checks passed
@kshitijk4poor
kshitijk4poor deleted the salvage/76194-approval-readonly-config branch August 5, 2026 07:10
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.

2 participants