Skip to content

Use servers/_process.py for the CLI's process checks - #518

Merged
Deicyde merged 3 commits into
mainfrom
cleanup/cli-process-helpers
Oct 31, 2026
Merged

Deicyde merged 3 commits into
mainfrom
cleanup/cli-process-helpers

Conversation

@Deicyde

@Deicyde Deicyde commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

A follow-up to #482: the CLI half of sharing the process helpers. lean_probe drops its own copies of the clean environment, the process group check and the token scan, and uses the ones in servers/_process.py that #502 made public, so the CLI and the Lean runtime daemon follow one rule: anything uncertain counts as live, or the scan fails.

Change

  • Loaded by path, not imported. lean_probe loads servers/_process.py from beside its own package, Path(__file__).resolve().parent.parent / "servers" / "_process.py", the expression the daemon uses for PACKAGE_ROOT. The module is registered in sys.modules as autoform_cli._process_helpers before it runs, as the importlib recipe does.
    • A plain import servers would find a consumer's own top-level servers first under python -m autoform_cli or python -, which put the working directory first on sys.path. Since __main__ imports lean_probe, every command would then fail, or run the consumer's _process.py.
    • The loader's docstring says how tests reach the CLI's copy: patch os, psutil or lean_probe._process_helpers, never servers._process.
    • servers/_process.py's docstring now says it is loaded this way and so imports only the standard library and psutil.
  • The token scan. _remember_tagged_processes keeps its signature and calls processes_with_token(_PROCESS_TOKEN_ENV, token). It still tries twice, then fails with the existing "repeated process-table errors" message, or returns silently in cleanup.
    • Behavior change: the scan now skips only a process that has exited or whose environment is denied, as the daemon's does. Any other environment error fails that attempt, as a SystemError already did; before, the CLI skipped that one process.
    • Cost: on a machine where one process keeps raising an environment error psutil does not map to "gone" or "denied" (realistic only on Windows), every probe run would fail at the success gate, and cleanup would find no token carriers.
  • The success gate. The group check is process_group_has_live_members. An error from it becomes SkeletonError("<context> could not inspect its process group"). The gate catches RuntimeError, OSError, SystemError and psutil.Error only, so other exceptions still surface as bugs.
    • Before, a killpg(pgid, 0) error other than ESRCH or EPERM counted as a gone group, and an unreadable process table counted as a live one.
  • Removed: _process_group_is_alive, _process_group_has_live_members and _probe_environment; run_probe and the helper build use inherit_clean_env(). _process_is_alive stays: the "still running" rule is not shared, so the daemon's behavior is unchanged.
  • Tests.
    • The five token scan tests patch psutil.pids and psutil.Process instead of psutil.process_iter. Their assertions are unchanged.
    • New test_tagged_process_scan_fails_closed_on_an_unreadable_environment: an EIO from environ() fails a strict scan and is silent in cleanup; a denied process beside a tagged one is skipped and the tagged one is found.
    • New test_bounded_command_reports_a_process_group_it_cannot_inspect (POSIX), for an unreadable process table and for killpg(_, 0) failing with EINVAL.
    • tests/test_lean_boundary.py gains two pins and a docstring sentence. From a temporary root holding its own servers package, whose files exit if run, a subprocess without PYTHONSAFEPATH (so the working directory comes first on sys.path) imports autoform_cli.__main__ and lean_probe; the CLI loads this repository's _process.py and never imports servers. An AST check holds _process.py to absolute imports of the standard library and psutil.
    • The wheel test expects servers/_process.py in the wheel and checks that lean_probe loads it from the extracted tree.
  • CHANGELOG: a "Process checks." entry under Changed.
  • _process.py is in the daemon's code fingerprint, so each install's daemon restarts once after the upgrade.

Net: +191/−71 across six files.

Verification

  • The locked ruff 0.15.17 check passes on autoform_cli servers tests, as CI runs it, and the touched files compile. ruff format --check flags lean_probe.py, _process.py, test_lean_probe.py and test_plugin_runtime.py in places this PR does not touch, as it does on the base; CI does not run the formatter.
  • No file names _process_group_has_live_members, _inherit_clean_env, _processes_with_token, _probe_environment or _process_group_is_alive, and no autoform_cli module imports the top-level servers package.
  • From three consumer roots, holding a regular servers package, a servers.py module and a namespace servers directory, importing autoform_cli.__main__ with the working directory first on sys.path loads this repository's servers/_process.py and leaves servers out of sys.modules. python -m autoform_cli --help exits 0 from the regular-package root, and the consumer's code never runs.
  • A copy of both packages in one directory, imported under -I as the wheel test does, loads _process.py from that directory. CI runs the wheel test itself.
  • Called directly on macOS, on this head:
  • Mutation checks, each against a copy of this commit before it was rebased onto main (the rebase changed only _process.py's docstring, to keep Move the LSP token scan into servers/_process.py and drop the underscore from its shared names #502's first line):
    • the loader replaced by from servers import _process as _process_helpers: the loader pin and the "never imports servers" test fail, and the loader pin still fails with PYTHONSAFEPATH=1 set;
    • the gate's try removed: both cases of the group test fail, with an OSError and a RuntimeError;
    • processes_with_token skipping every psutil.Error and OSError, as the CLI's old scan did: the environment test fails;
    • from servers import lean_client added to _process.py: the import pin and the loader pin fail.
  • CI coverage:
    • test (Python 3.10 and 3.13): the scan, gate and boundary tests, and the wheel test;
    • real-lean, impact and library steps: run_probe with the shared clean environment and gate on real Lean runs;
    • real-lean-skeleton: tests/test_skeleton.py and tests/test_lean_probe.py against real Lake and Lean, including the success gate;
    • windows-hardening runs only the node IDs it lists. Several of its modules import autoform_cli.__main__ or lean_probe, so the loader runs on Windows. Of its six bounded-command tests, at least test_bounded_command_rejects_invalid_utf8 and test_bounded_command_rejects_a_successful_parent_with_a_live_descendant reach the success gate, where the strict token scan reads Windows' process table. The group check is POSIX-only, and the two new tests are not in that list.
    • No job runs on macOS; the direct runs above are the macOS check.

Overlap

lean_probe kept its own copies of three helpers the Lean runtime daemon
has in servers/_process.py: the clean child environment, the process
group liveness check and the scan for processes carrying a token. The
copies had drifted. The CLI now uses the daemon's versions, so both
sides follow one evidence rule: anything uncertain counts as live, or
the scan fails.

lean_probe loads servers/_process.py by path from beside its package
and registers it as autoform_cli._process_helpers, instead of importing
the top-level servers package. Under `python -m autoform_cli`, the
working directory comes first on sys.path, so a consumer's own
`servers` package would otherwise be imported, or would run in place of
ours. The loader's docstring says how tests reach the CLI's copy.
servers/_process.py says it is loaded this way and so imports only the
standard library and psutil.

Behavior changes in the CLI:
- The token scan skips only a process that has exited or whose
  environment access is denied. Another environment error now fails
  the scan attempt, as it does in the daemon; after two attempts the
  success gate fails with the existing "repeated process-table errors"
  message, and cleanup carries on without that pass's finds.
- The success gate turns an error from the process group check into
  "<context> could not inspect its process group". It catches
  RuntimeError, OSError, SystemError and psutil.Error only, so other
  exceptions still surface as bugs. Before, a killpg error other than
  ESRCH or EPERM counted as a gone group and an unreadable process
  table counted as a live one.

`_process_group_is_alive`, `_process_group_has_live_members` and
`_probe_environment` go away; `_process_is_alive` stays, since the
"still running" rule is not shared.

Tests: the five token scan tests patch psutil.pids and psutil.Process
instead of psutil.process_iter, with unchanged assertions. New tests
cover an unreadable environment and a process group the gate cannot
inspect. tests/test_lean_boundary.py pins that the CLI loads our
_process.py from a root holding its own `servers` package without
importing that package, and that _process.py imports only the
standard library and psutil. The wheel test checks that the wheel
ships servers/_process.py and that lean_probe loads it from the
installed tree. CHANGELOG gains a "Process checks." entry.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 31, 2026
@Deicyde
Deicyde marked this pull request as ready for review October 31, 2026 04:15
@Deicyde

Deicyde commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Review at 1263d6f: merge.

Nothing blocks merge. The PR does what Jack decided on 10-29: it loads servers/_process.py by path and adopts the daemon's fail-closed rule.

  • Behaviour matches the body. The token scan now skips only a process that has exited or whose environment is denied. Any other environment error fails that attempt, and two failed attempts give the existing "repeated process-table errors". The success gate turns every inspection error into "could not inspect its process group", where main counted a killpg(_, 0) EINVAL as a gone group and an unreadable table as a live one. Cleanup stays silent.
  • No duplicate is left. Nothing in autoform_cli still pops PYTHONPATH, probes killpg(_, 0) or scans environments. The remaining killpg calls in lean_probe.py and claims.py send real signals.
  • The Windows cost is smaller than the body suggests. psutil retries ERROR_PARTIAL_COPY from environ() for about a second and then raises AccessDenied (retry_error_partial_copy in psutil/_pswindows.py). So the common Windows read failure is still skipped, not fatal.
  • Necessity. The deduplication is small: three helpers, about 40 lines. The loader, its docstring and two boundary pins cost about the same, so the code is roughly line-neutral. The value is that the CLI and the daemon now share one fail-closed rule. That is worth it, but it is the reason to merge, not the line count.

Notes

  1. The loader's docstring is long, and one sentence is wrong. It runs 15 lines for a 7-line function. It says the module is registered in sys.modules "because dataclasses look their module up there", but _process.py defines no dataclass. Registering is still right, since the importlib recipe does it. Something like this keeps the rules:

    """Load servers/_process.py from beside this package, never as ``servers``.
    
    Under ``python -m autoform_cli`` the working directory comes first on
    ``sys.path``, so ``import servers`` could run a consumer's package. Tests
    patch ``os``, ``psutil`` or ``lean_probe._process_helpers``; patching
    ``servers._process`` does not reach this copy.
    """

Checks

  • Locally on Python 3.14 at this head, tests/test_lean_boundary.py and tests/test_lean_probe.py give 88 passed and 3 skipped (real Lean), and the token tests in tests/test_lsp_protocol.py give 12 passed.
  • The PR merges cleanly with main 1455fb6, and CI passes 9 of 9.

Posted by Claude Code (session jackmccarthy-eb).

Review note on #518: the docstring ran 15 lines for a 7-line function and
said the module is registered because dataclasses look it up, but
servers/_process.py defines no dataclass.
@Deicyde

Deicyde commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Applied the docstring note in 22cbee2: the loader's docstring is now the six lines you suggested, without the dataclass sentence. The relative-import rule it dropped is still pinned by tests/test_lean_boundary.py. The body's matching bullet is updated.

Posted by Claude Code (session jackmccarthy-e8).

@Deicyde

Deicyde commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Head check at 22cbee2: merge.

22cbee2, on top of the reviewed 1263d6f, changes only the _load_process_helpers docstring in lean_probe.py. It now holds the six suggested lines, and the dataclass rationale is gone. CI passes 9 of 9, and the PR merges cleanly with main a87797f.

Posted by Claude Code (session jackmccarthy-eb).

@Deicyde
Deicyde merged commit e35b490 into main Oct 31, 2026
9 checks passed
@Deicyde
Deicyde deleted the cleanup/cli-process-helpers branch October 31, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant