Skip to content

Refuse partial coverage, and keep the log to itself - #3

Merged
burnshall-ui merged 1 commit into
mainfrom
fix/refuse-partial-coverage
Aug 15, 2026
Merged

burnshall-ui merged 1 commit into
mainfrom
fix/refuse-partial-coverage

Conversation

@burnshall-ui

Copy link
Copy Markdown
Owner

Follow-up to #2, closing the remaining items from the review.

Two limits that were enforced by quietly covering less

Exceeding MAX_WATCH_DIRS logged a line and then carried on watching the first
512 directories. Nothing afterwards distinguished that from full coverage —
the lines a partial watcher emits look exactly like complete ones. Recursion
depth had no limit at all, and each addWatchRecursive frame carries ~12 KB of
path and dirent buffers; MAX_WATCH_DIRS alone would permit a 512-deep chain,
roughly 6 MB of stack against an 8 MB default that already holds a 2 MB
Context.

Both are now fatal, on the same reasoning as the exits added in #2: a
supervisor can act on a dead process, but nobody acts on a healthy-looking one
that is missing half the tree.

That made three scattered "should we still be running" checks into a set worth
naming, so they are now one bailIfBlind:

Condition Why it is fatal
No watchable root Blocks forever on an empty inotify instance, looking healthy
Last watch gone Same state, reached at runtime
Log writes failing Watching perfectly and recording nothing
More directories than MAX_WATCH_DIRS Coverage would be partial, and partial lines look complete
Tree deeper than MAX_DEPTH Same, and unbounded recursion would overflow the stack

Permissions

The log is created 0600 rather than 0644, and directories it creates 0700.
A record of every file an agent touched is a map of the system for whoever can
read it.

It is also opened O_NOFOLLOW. ocwatch usually runs as root and the log path is
an operator-supplied argument that may point somewhere world-writable, so a
symlink in that position now fails loudly at startup instead of being appended
through. This is a behaviour change for anyone deliberately symlinking their
log — the failure is loud and immediate.

Unit

%h instead of a hardcoded /root, so the same unit works for any user. The
argument defaults compiled into the binary are still /root-based; the README
now says so rather than leaving it to be discovered.

Tests

27 integration checks (5 new) and 8 unit tests, stable across repeated runs. The
new scenarios build a 600-directory tree and a 70-deep one and assert a non-zero
exit with the gap named in the log, plus a check that the log is created 0600.

🤖 Generated with Claude Code

Follow-up to #2, closing the remaining review items.

Two limits were enforced by quietly covering less of the tree. Exceeding
MAX_WATCH_DIRS logged a line and carried on watching the first 512
directories; nothing afterwards distinguished that from full coverage,
because the lines a partial watcher emits look exactly like complete
ones. It is now fatal, on the same reasoning as the other exits: a
supervisor can act on a dead process, but nobody acts on a healthy
looking one that is missing half the tree.

Recursion depth had no limit at all. Each addWatchRecursive frame
carries ~12 KB of path and dirent buffers, and MAX_WATCH_DIRS alone
would permit a 512-deep chain — about 6 MB of stack against an 8 MB
default that already holds a 2 MB Context. MAX_DEPTH bounds it, and
exceeding it is fatal for the same reason as the watch limit rather than
being a second, quieter kind of failure.

Both conditions share one field carrying the reason, and the three
scattered "should we still be running" checks are now one bailIfBlind
naming the whole set: log broken, nothing to watch, coverage incomplete.
That set is the actual subject of this branch, so it deserved a name.

The log is created 0600 rather than 0644, and directories it creates
0700 — a record of every file an agent touched is a map of the system
for whoever can read it. It is opened O_NOFOLLOW: ocwatch usually runs
as root and the log path is an operator-supplied argument that may point
somewhere world-writable. A deliberate symlink there now fails loudly at
startup rather than being appended through.

The unit uses %h instead of hardcoded /root, so it works for any user.
The compiled argument defaults are still /root-based, which the README
now says out loud rather than leaving to be discovered.

27 integration checks, 8 unit tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@burnshall-ui
burnshall-ui merged commit a8b9518 into main Aug 15, 2026
4 checks passed
@burnshall-ui
burnshall-ui deleted the fix/refuse-partial-coverage branch August 15, 2026 18:03
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