Repository navigation
fix(guard): survive unlookable paths and judge each action under a deadline (#460) - #466
Conversation
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 39 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configuration
⛔ Files ignored due to path filters (1)
📒 Files selected for processing (10)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| from typing import TYPE_CHECKING, Any, TextIO | ||
|
|
||
| from omind import compliance, filelock, paths, policy | ||
| from omind import compliance, deadline, filelock, paths, policy |
EM review: changes requested (Flatline Roundtable: HAL9000, Cortana, TheDixieFlatline)The OSError fix is solid: guarded lookups, a 4,096-char cap on the walked cwd, and a linear EM decision: once the deadline fires, judging fails CLOSEDSeveral gates in Today all of them fail open past the budget, and Rule: when the deadline fires anywhere in judging,
Must fix
Should fix
Cut
Dismissed
Version: at push time, set it one patch above origin/main. #465 (#455) is merging as 10.2.26, so this becomes 10.2.27. Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/ |
…okup (#460) EM review on #466: once the deadline fires anywhere in judging, decide() and check_action() deny with the explicit "command too large/complex to judge safely" reason and a deadline-exceeded event. Several gates after the hard rules are hard denies too (global config, capability questions, repo work, operator note-rule denies), and padding a command past the budget skipped all of them. - Every fact lookup (gh repo view, git ls-remote, git config ...) runs under its own timeout from deadline.lookup_timeout: at most 3 s, and never into the 3 s reserve left for judging. A slow lookup makes that fact unknown; it never spends the budget. - Handlers that failed open on any exception now re-raise the deadline: _note_rules_verdict, _git_dash_c_path, _local_code_texts, _rules_command_view, _bash_write_repo, the repo lookup in _repo_root_for_action, and CommandSite.write_repos. - _rules_command_view memoises resolution and the parent walk per command; _dir_repo walks parents lazily. `cd x; ` x 2000 through check_action with note rules: 8 s (deadline) -> about 1 s. - The steps after the verdict (budget re-arm, excerpt, retrieval suggestion) run outside the deadline; the hard-rule verdict is cached per check, so the fail-open re-check reuses it. - Removed the unused deadline.active(), deadline.expired() and Deadline.expired(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zj1PCfsSSJgzLxi32jvde
92406d8 to
9d8b7b0
Compare
Response to the EM review (9d8b7b0)Rebased onto EM decision: past the deadline, judging fails CLOSED. Done
Must fix
Every shape is under the 8 s budget. The Should fix
Cut
Other notes
Gates
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/ |
| candidates.append(base / dash_c) | ||
| except deadline.DeadlineExceededError: | ||
| raise # never partial information: the guard denies (#460) | ||
| except Exception: |
| rule_id=verdict.rule_id, | ||
| ) | ||
| if not verdict.allow and verdict.rule_id == "omi-gate" and omi_dir is not None: | ||
| from omind import retrieve |
…adline (#460) decide() raised OSError (ENAMETOOLONG) on `cd x; ` repeated ~2000 times, and ValueError on a Write path holding a NUL: the walked cwd or target went to stat/resolve uncaught, breaking invariant 2. Such paths now degrade to an unknown repo (_is_worktree_root, _repo_root_for_action, _is_global_config_path). A `cd` chain stops at 4,096 chars (unknowable past PATH_MAX) and joins as text, so it is walked in linear time. Judging one action now runs under a cooperative wall-clock deadline (omind.deadline, 8 s inside the 15 s OMI guard hook timeout), checked at the walk, stage, rule and note-rule loop points. Past it the hard rules fail CLOSED ("command too large/complex to judge safely — split it or shorten it", rule omi-guard-deadline, event deadline-exceeded); the soft gates fail open. The hard rules always get a 3 s floor, so a slow note-rule lookup never turns a plain command into a deny. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zj1PCfsSSJgzLxi32jvde
…okup (#460) EM review on #466: once the deadline fires anywhere in judging, decide() and check_action() deny with the explicit "command too large/complex to judge safely" reason and a deadline-exceeded event. Several gates after the hard rules are hard denies too (global config, capability questions, repo work, operator note-rule denies), and padding a command past the budget skipped all of them. - Every fact lookup (gh repo view, git ls-remote, git config ...) runs under its own timeout from deadline.lookup_timeout: at most 3 s, and never into the 3 s reserve left for judging. A slow lookup makes that fact unknown; it never spends the budget. - Handlers that failed open on any exception now re-raise the deadline: _note_rules_verdict, _git_dash_c_path, _local_code_texts, _rules_command_view, _bash_write_repo, the repo lookup in _repo_root_for_action, and CommandSite.write_repos. - _rules_command_view memoises resolution and the parent walk per command; _dir_repo walks parents lazily. `cd x; ` x 2000 through check_action with note rules: 8 s (deadline) -> about 1 s. - The steps after the verdict (budget re-arm, excerpt, retrieval suggestion) run outside the deadline; the hard-rule verdict is cached per check, so the fail-open re-check reuses it. - Removed the unused deadline.active(), deadline.expired() and Deadline.expired(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zj1PCfsSSJgzLxi32jvde
…endent of runner speed (#460) CI run 37085768401 (ubuntu, coverage on) overran the bash -c x 100,000 shape. The stage split (_program_stages_cached, one Python step per segment) and _stage_ends ran before the walk's first deadline.check(): about 0.4 s untraced, several seconds under coverage on a slow runner. Both loops now check the deadline. With a 0.3 s budget and 0.2 s floor under coverage, bash -c x 100,000 returns in 0.33 s (was 0.99 s); the longest stretch between checks is about 0.13 s untraced. Tests: - test_the_issue_460_shapes_always_return_inside_the_hook_margin: the issue's shapes at full size on a deliberately slowed judge (busy-wait in _basename and _chdir) must return within budget + floor + margin. It fails (20 s) without the new check points. - The "well inside the budget" test runs at the real budget, at sizes that hold on a CI runner under coverage; the cd-chain tests are scaled the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zj1PCfsSSJgzLxi32jvde
9d8b7b0 to
c645613
Compare
CI fix for run 37085768401 (c645613)Rebased onto What CI caught. Some judging ran with no I found it by recording the gap between consecutive Under coverage, with a 0.3 s budget and a 0.2 s floor:
All three end with the deadline deny. At the real 8 s budget under coverage, the The The bash-c test failure itself. That test raised the budget to 600 s so it could measure the shape, which is why it ran past 15 s. The runner was just slow under coverage; the real defect is the unchecked stretch above. Timing tests no longer depend on runner speed
Gates
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/ |
…overage) The 100k find-actions verdict takes ~0.4 s untraced; CI's 3.10 coverage run is ~7x slower and crossed the 3 s traced bound. A quadratic regression still costs 10x+ more, so traced_bound(2.0) keeps the test meaningful. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Zj1PCfsSSJgzLxi32jvde
|
EM merge (#460). Reviewed by the Flatline Roundtable (HAL9000, Cortana, TheDixieFlatline), then two fix rounds and an EM CI-headroom fix. Part 1: Part 2: the hook deadline. EM decision: once the 8 s judging budget is spent, judging fails closed. The action is denied with the explicit "command too large/complex to judge safely" reason, and a
Verified: every probe suite is unchanged (#413, #434, #446, #447, #453, #456, #457, #461, #465, #467), and the #445 timings hold. EM fix: the #455 find-actions timing bound went from All 23 CI jobs are green. Releasing as 10.2.28. Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/ |
Summary
Fixes #460 in two parts. Version 10.2.26, one patch above main (10.2.25 after #463).
1. Unlookable paths no longer raise (invariant 2)
On main,
decide()raisedOSError: File name too longoncd x;× 2000,cd a/a/…,git -C a/a/… commitand a long Write path. It raisedValueErroron a Write path holding a NUL. Each of these paths now degrades to "unknown repo", which is fail-open for the soft gates:_is_worktree_rootreturns False onOSError/ValueError. That covers_enclosing_repoand_dir_repo(the guard: Bash writes into a repo other than the resolved one skip the consult gate #448 write-target walk)._repo_root_for_actiontreats an unstat-able Write path as a file and walks its parents._is_global_config_pathalso suppressesValueErrorfromresolve().rules.pyrepo lookups needed no change. They only see roots that_is_worktree_rootalready found on disk, and their subprocess calls already catchOSError._chdirjoins paths as text, not with/. On Python 3.13 a/-built Path keeps every segment it was joined from, so acdchain re-joined all of them at each step. That made it quadratic:cd x;× 20k took about 1 s just to walk, and × 100k ran past the deadline. Past 4,096 characters (PATH_MAX) the directory is unknowable (None), which is the walk's existing "unknowable cd" value.cd x;× 100k now takes about 1.1 s throughcheck_action.2. The overall judging deadline
New module
omind/deadline.pyholds a small cooperative deadline in aContextVar.check()raisesDeadlineExceededErroronce the budget is spent and does nothing outside a judging scope.provision.OMI_GUARD_TIMEOUTis 15 s, which is the shortest hook timeout omind writes for the OMI guard. The other 7 s cover interpreter start (it can take seconds under antivirus), adapter rendering and the gate's retrieval suggestion. A test pinsbudget + floor <= OMI_GUARD_TIMEOUT - 3.check_action()opens one scope for the whole action, so note rules count against it.decide()opens one too when called directly. A nested scope keeps the outer deadline._shell_sitesevent loop,_local_shell_subjectsstages, the_hard_rule_subjectsloops, each hard rule and each subject search,_enclosing_repo/_dir_repoparent walks, write-target loops, and inrules.evaluateeach rule, each_rule_matchesand each_judge._hard_policy_verdictcatches the deadline and denies withomi-guard (hard): command too large/complex to judge safely — split it or shorten it, the guard: _shell_walk / _program_stages scale superlinearly (x=1; repeats quadratic; bash -c body n^1.5) #445 budget reason, underrule_idomi-guard-deadline. It logs adeadline-exceededcompliance event with outcomedeny._hard_rule_subjectsre-raises the deadline instead of swallowing it, so a partial subject list is neverlru_cached.decide()now runs the hard rules before resolving the repo; the repo lookup is pure and nothing before the hard rules used it. Anything after the hard rules that hits the deadline allows the action and logsdeadline-exceededwith outcomefail-open. This covers the repo, freshness, capability, global-config and consult gates. Note rules already fail open on any exception, so a deadline inside them does the same.deadline.at_least). Without it, a slow note-rule network lookup (gh repo view/ls-remote) that spent the budget would turn every plain command into a deadline deny.Why cooperative checks and not
signal.SIGALRMor a thread:SIGALRMis POSIX-only, fires only on the main thread (the web app judges off it), and interrupts at an arbitrary bytecode, possibly inside a lock or a log write. A watchdog thread cannot stop the judging thread in CPython; it could only race to print a verdict while judging continues. Checks at the loop points work on Windows and on any thread, and the guard always stops at a known point. The cost is oneContextVar.getandtime.monotonic()per iteration. A single long C-level regex call is not interruptible, but #445 already bounds those.One existing test changed:
test_check_fail_open_still_honours_hard_policy_rulescrashed_repo_root_for_actionto reach the fail-open path. That function now runs after the hard rules, so the crash can no longer pre-empt them. The test now crashes_note_rules_verdict, which still runs first, and its intent and assertions are unchanged.Test evidence
New tests in
tests/test_guard.py. All 12 fail on main (verified by checking out main's sources): sixOSError/ValueErrorraises, a hard-time-limit overrun, and missing deadline machinery.test_paths_the_filesystem_cannot_look_up_do_not_raise(6 shapes)test_a_long_cd_chain_is_walked_in_linear_time(cd x;× 50k inside a 1 s bound)test_the_deadline_is_a_no_op_outside_a_scope,test_the_judging_budget_fits_inside_the_hook_timeouttest_hard_rules_fail_closed_past_the_deadline: a slow subject search denies with the reason and logs the event. The partial result is not cached.test_soft_gates_fail_open_past_the_deadline: anomi-gatedeny becomes an allow loggedfail-open, andsudo idis still denied.test_a_slow_earlier_step_does_not_deny_a_plain_command: the hard-rule floor.Gates, all passing locally:
ruff check .: passesmypy src(strict): no issues, 59 filespytest: 3554 passed, 12 skipped#445 timings are unchanged.
perf446.pymeasures walk 0.01–0.02 s and hard ≤0.01 s.probe459.pymeasures ≤0.02 s for every shape, and its budget verdict is unchanged.Issue shapes through
check_actionat n = 100,000:bash -c 'echo hi';echo x > f;cd x;+ commitAll are well inside the deadline, and none is denied.
Closes #460
Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/
🤖 Generated with Claude Code
https://claude.ai/code/session_011Zj1PCfsSSJgzLxi32jvde