Skip to content

fix(runtime): keep ownership of a recorder whose interpreter link moved - #350

Open
mikehasa wants to merge 2 commits into
mainfrom
fix/runtime-stop-exe-identity
Open

mikehasa wants to merge 2 commits into
mainfrom
fix/runtime-stop-exe-identity

Conversation

@mikehasa

Copy link
Copy Markdown
Owner

agentacct stop, start, status and repair disowned a still-running recorder after its interpreter symlink moved under it. stop failed with "does not match agentacct ownership proof; no process was signalled", and start refused to replace the processes. The upgrade order uv tool install "agentacct==X.Y.Z" --force && agentacct stop && agentacct start got stuck whenever the reinstall picked a different interpreter.

Cause

The runtime manager's ownership proof took the executable to be the live argv[0] resolved at check time. For a uv/pipx console script on a non-framework interpreter, argv[0] is the venv's bin/python symlink. uv tool install --force or a pipx reinstall recreates that link, and uv python upgrade moves the minor-version link beneath it (cpython-3.12-… → cpython-3.12.N-…). Either way the path resolved at check time stopped equalling the one recorded at spawn, even though every other identity field (birth time, pgid, cwd, argv, nonce) and the running image were unchanged.

Fix

  • ManagedProcess gains image: the kernel-reported program file (psutil exe()), recorded at spawn. The handshake reads it from a fresh Process on each poll, because psutil caches exe() per object and would keep the transient pre-exec image such as /usr/bin/env.
  • _executable_matches accepts when either {recorded launch path, recorded image} matches either {live launch path, live image}. The previous check is one of these routes, so nothing that matched before stops matching. Records written by earlier releases have no image and are matched through the live image, so a runtime started by an older release can be stopped after upgrading to this one.
  • _running_image treats a symlink result as unknown. A running program is a regular file, so a symlink is either psutil guessing from argv[0] (on macOS, after the running file was deleted) or a stale path. Following it would name the link's new target.
  • Birth time, process group, cwd, full argv and the per-start nonce are unchanged. state.json keeps its schema version, and older releases ignore the new key.

Scope:

  • Covered while the interpreter the recorder started with is still installed, and on Linux even after it is deleted, because psutil strips (deleted).
  • On macOS a deleted interpreter can no longer be identified, so the runtime still refuses rather than guess.

Tests

In tests/test_runtime_manager.py:

  • A real runtime is launched through a venv-style interpreter link and the link is then retargeted. status, start and stop still own the process.
  • The same scenario with a record written without image.
  • A record naming a different program is still refused.
  • A match-table unit test.
  • _running_image fallbacks: empty, symlink, AccessDenied, NoSuchProcess and OSError results all return empty.
  • The handshake fake now caches exe() like psutil, which pins the fresh-Process read.

The link-based tests skip on macOS framework builds. Their launcher rewrites argv[0] to Python.app, so there is no venv link in the live argv to retarget.

Verification

  • pytest -q: 4909 passed.
  • The retarget scenario was reproduced on the pre-fix activation.py, where stop was refused with the exact production error. With the fix, status reads running and stop stops.
  • On macOS, the deleted-interpreter case still refuses and signals nothing.

The runtime manager's ownership proof took a process's executable to be its
live argv[0] resolved at check time. For a uv/pipx console script, argv[0] is
the venv's bin/python symlink. Reinstalling the tool recreates that link, and
`uv python upgrade` moves the minor-version link beneath it. When either
points at a different interpreter, the resolved path no longer equals the
one recorded at spawn. stop, start, status and repair then disowned the
still-running watcher and dashboard ("does not match agentacct ownership
proof; no process was signalled"), so the documented deploy order
`uv tool install ... && agentacct stop && agentacct start` got stuck.

- Record the kernel-reported program image (psutil exe()) as a new
  ManagedProcess.image field, read from a fresh Process on each handshake
  poll because psutil caches exe() per object and would otherwise keep the
  transient pre-exec image.
- Accept the executable when either the recorded launch path or the
  recorded image matches either the live launch path or the live image.
  The previous check is one of these routes, so nothing that matched before
  stops matching. A pre-image record is matched through the live image,
  so a runtime started by an earlier release can be stopped after upgrading.
- Treat a symlink returned by exe() as unknown. The kernel never reports
  one, so it is psutil guessing from argv[0] (macOS, running file deleted),
  and following it would name the link's new target.
- Birth time, process group, cwd, full argv and the per-start nonce are
  unchanged, and state.json keeps its schema version; older releases
  ignore the new key.

Tests launch a real runtime through a venv-style interpreter link, retarget
the link, and assert status/start/stop still own it, including for a record
written without an image. They also cover refusing a record that names a
different program, the match table, and the fresh-Process handshake.

This branch has not been deployed

No deployments
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