Tool Server Pressure Inventory
Tool Server Pressure Inventory validates public pressure-inventory fixtures without reading or mutating live host process state.
The write-up
Long sessions leave helper processes that slow the host, but a reaper that kills stale helpers is dangerous when an abandoned process looks like one a live session still uses. Long agent sessions leave helper processes behind: MCP servers, dev servers, keepalives. They pile up and the host slows down. The obvious fix is a reaper that walks the process table and kills stale helpers. A reaper is also dangerous, because the hard case is telling a genuinely abandoned process apart from one a live session is still using. Kill the wrong one and you break work in flight.
It is the read-only classifier over synthetic input: it classifies each helper by kind and owner chain, marks the few safe to close, and requests launch for over-budget owners. tool_server_pressure_inventory is the read-only classifier from that problem, ported to run over synthetic input. It takes a fixture that supplies a ps-shaped process table, a helper-kind policy, and an owner-status taxonomy. It classifies each helper by kind and by owner chain, marks the few that are safe to close, and for owners that are over their helper budget it emits a launch request rather than a kill. Every row carries a command_hash, not a command line.
It reads no live process table, imports no process-control module, and sends no signal, writing metadata-only records that show a reaper's safety reasoning without the harmful part. The component reads no live process table, imports no process-control module, and sends no signal. It writes metadata-only result records so a reader can see the safety reasoning of a reaper without the part that could harm a host.
Purpose
The owner chain is the real decision
A loose reaper closes from a shallow signal like age or name, but the real decision is the owner chain: a launchd-parented helper is detached while a session-traced one is not. A loose reaper decides "close this" from a shallow signal: the process is old, or its name matches a helper pattern. That is enough to close a live session's MCP server by mistake. The interesting decision is the owner chain. A helper whose parent is launchd (ppid == 1) has been detached from any session and is a candidate. A helper that still traces back through a live agent session is not.
The narrow safe-close predicate
A row becomes a close candidate only when it is detached, its kind is allowlisted, and its idle age passes the 300-second threshold; everything else routes to owner-check or keep. So the safe-close predicate is deliberately narrow. A row becomes a close candidate only when it is detached, its kind is on an allowlist, and its idle age is past a threshold (DEFAULT_MIN_AGE_SECONDS, 300 seconds). Everything else routes to requires_owner_check or keep.
Active owners get a launch request, not a kill
When an owner is over budget it emits a launch request asking the session to launch its own lease, and an audit enforces that an active-owner descendant is never a close candidate. The second half of the design is that active owners never get a kill. When an owner is over its helper budget the component emits a launch request: a row that asks the owning session to launch or reuse its own lease. The inventory is not a kill list. The invariant, enforced by an audit pass over the component's own output, is that an active-owner descendant can never be a close candidate.
How it works
One pass, two entry points
The pipeline runs in one pass; run validates a fixture while run_pressure_bundle validates an exported source-body bundle, both delegating to _build_result. The pipeline runs in one pass over the fixture. run loads the positive and negative inputs and writes the result records. run_pressure_bundle is the second entrypoint: it validates an exported source-body bundle instead of a fixture. Both call _build_result, which orchestrates loading, classification, redaction, and the negative-case sweep.
The classifier's decision law
build_tool_server_pressure_inventory parses rows, matches each command to a helper kind, walks the parent chain for owner status, and routes to keep, safe-close, or owner-check. The classifier is build_tool_server_pressure_inventory. It calls _parse_process_rows to turn the synthetic ps_text into rows of pid, ppid, age, and command, then for each row calls _process_kind to match the command against the policy's match_substrings. Rows whose command matches no helper kind are dropped. For each kept row _owner_status_for_process walks the parent chain up to eight hops, stopping at a keep runtime, a detached parent, an active owner hint, or a cycle. _inventory_owner_and_decision then applies the decision law: keep kinds route to keep, detached and allowlisted and old-enough rows route to candidate_safe_close, and everything else routes to requires_owner_check. Over-budget active owners are summarized by _active_owner_pressure_groups, and _owner_release_request_for_group builds a helper_owner_release_request_v1 whose permitted_action is ask_owner_to_release and whose result is requested.
Two guards over the classifier
_redaction_findings rejects command previews, signal keys, and absolute paths, and _audit_inventory_claim re-reads the output to fail any close candidate that breaks the safety invariant. Two guards sit on top of the classifier. _redaction_findings walks every emitted dict and rejects a command preview key, a truthy process-signal key, or an absolute host path. _audit_inventory_claim re-reads the component's own inventory and fails it if any close candidate is an active owner, is not a ppid == 1 allowlisted detached orphan, or is younger than the age threshold, and it fails any launch request that reads as a kill. _positive_findings runs these over the live fixture and _negative_findings runs them over the eight crafted failure inputs.
The bundle lane's manifest check
_source_module_manifest_result verifies each exported module's relation, recomputes the target SHA-256, confirms required anchors, and reruns the redaction guard, keeping result records metadata-only. _source_module_manifest_result covers the bundle lane. It reads source_module_manifest.json, checks each declared module's source_to_target_relation against the accepted set, recomputes the target body's SHA-256 against the declared target_sha256, confirms the required anchors are present in the body, and reruns the redaction guard on the body text. Result records carry refs, hashes, counts, and verdicts only; body_in_receipt stays false. _write_receipts then emits the result, board, validation, and sign-off records.
| Function | Role |
|---|---|
run | Validate a fixture and write the four result records |
run_pressure_bundle | Validate an exported source-body bundle |
_build_result | Load inputs, classify, run guards, assemble the result |
build_tool_server_pressure_inventory | Classify each helper by kind, owner chain, and decision |
_parse_process_rows | Parse synthetic ps_text into pid, ppid, age, command |
_owner_status_for_process | Walk the parent chain up to eight hops for owner status |
_inventory_owner_and_decision | Apply the keep / safe-close / owner-check decision law |
_active_owner_pressure_groups | Group over-budget active owners into launch requests |
_redaction_findings | Reject command previews, process signals, absolute paths |
_audit_inventory_claim | Re-check the component's own output against the invariant |
_source_module_manifest_result | Verify the exported source body by digest and anchors |
_write_receipts | Write the metadata-only result, board, and validation records |
Diagram source & refs
flowchart TD Fixture["Synthetic process table pressure policy, owner classes"] Parse["_parse_process_rows ps rows to pid, ppid, age, cmd"] Kind["_process_kind match command to helper kind"] Owner["_owner_status_for_process walk parent chain up to 8 hops"] Decision["_inventory_owner_and_decision keep / safe-close / owner-check"] Groups["_active_owner_pressure_groups over-budget owners to launch request"] Audit["_audit_inventory_claim re-check own output"] Redact["_redaction_findings reject previews, paths, signals"] Result records["_write_receipts metadata-only result records"] Fixture --> Parse Parse --> Kind Kind --> Owner Owner --> Decision Decision --> Groups Decision --> Audit Groups --> Audit Audit --> Redact Redact --> Result recordsNegative cases
EXPECTED_NEGATIVE_CASES lists eight inputs the fixture must reject, each keyed to the error code it must raise. EXPECTED_NEGATIVE_CASES lists eight inputs the fixture must reject, each keyed to the error code it must raise.
Eight rejection codes for kill-unsafe inputs
The eight inputs each raise a specific code, covering active-owner or cycle kill candidates, unknown-owner kills, premature closes, sent signals, command or path leaks, and launch overclaims. active_owner_kill_candidate and owner_chain_cycle_safe_close both raise TSPI_ACTIVE_OWNER_KILL_CANDIDATE: an active-owner descendant, including one reached through a parent-chain cycle, is marked as a close candidate. unknown_owner_kill raises TSPI_UNKNOWN_OWNER_KILL_FORBIDDEN when a close candidate is not a ppid == 1 allowlisted detached orphan. premature_safe_close raises TSPI_PREMATURE_SAFE_CLOSE when a detached orphan younger than the age threshold is marked closeable. process_signal_sent raises TSPI_PROCESS_SIGNAL_FORBIDDEN. command_preview_leak raises TSPI_COMMAND_PREVIEW_FORBIDDEN when a row carries a live command line instead of a hash. absolute_path_leak raises TSPI_ABSOLUTE_PATH_FORBIDDEN. owner_release_overclaim raises TSPI_OWNER_RELEASE_OVERCLAIM when a launch request reads as a kill, terminate, or force-close.
A run passes only when every expected case is observed, the positive fixture raises nothing, and the secret scan is clean. A run passes only when every expected case is observed, the positive fixture raises nothing, and the secret scan is clean.
Prior Art Grounding
Process-inventory and owner-reference prior art
It draws on psutil process iteration, Kubernetes owner-reference garbage collection, and MCP tool servers, but stays weaker: it classifies synthetic rows and reads no live host state. The classifier draws on established process-inventory and ownership patterns. psutil.process_iter() is a common way to iterate process metadata without ad hoc ps parsing. Kubernetes garbage collection uses owner references to separate objects that may be collected from objects a live controller still owns, which is the same detached-versus-owned distinction applied here. The Model Context Protocol gives the local "server exposes callable tools" shape that names these helpers. This component keeps the result weaker than any of them: it classifies synthetic rows for pressure and close eligibility, and it reads no live host state.
- psutil process iteration: https://psutil.readthedocs.io/en/latest/#psutil.process_iter
- Kubernetes owner-reference garbage collection: https://kubernetes.io/docs/concepts/architecture/garbage-collection/
- Model Context Protocol tool servers: https://modelcontextprotocol.io/docs/concepts/tools
Validation Result record Path
From microcosm-substrate, with result records written under /tmp:
What a pass certifies
A pass means rows stayed digest-only, the eight negative cases raised their codes, and the bundle body matched its declared digest and anchors. A pass means synthetic classification and source-manifest shape held: rows stayed digest-only, the eight negative cases raised their codes, and the bundle body matched its declared digest and anchors.
Scope boundary
Scope limit
The strongest supported claim
Over synthetic fixtures it classifies kind, owner status, and close eligibility correctly, keeps active owners out of the close set, requests launch for over-budget owners, and rejects the eight failures. The strongest claim this component supports is that over synthetic process-table fixtures it classifies helper kind, owner status, and close eligibility correctly, keeps active-owner descendants out of the close set, turns over-budget owners into launch requests, and rejects the eight named boundary failures. The bundle lane adds that an exported source body matched its declared digest and anchors while staying metadata-only.
The whole proof boundary and ceiling
That is the whole boundary: the component reads no live table, sends no signal, mutates no host, and AUTHORITY_CEILING holds every action flag at false as projection and validation only. That is the whole proof boundary. The component does not read a live process table, send a process signal, mutate host state, or authorize cleanup. It does not use external model services, export private account or session state, or claim whole-system correctness. AUTHORITY_CEILING holds process_signal_authority, live_process_table_read_authorized, host_mutation_authorized, release_authorized, and whole_system_correctness_claim at false. The scope limit is projection and validation only.
Context & evidence
In short Tool Server Pressure Inventory validates the public tool-server pressure membrane: synthetic process rows, active-owner descendants, over-budget owner launch requests, redaction findings, source-module digests, metadata-only result records, negative cases, and scope limits. It treats pressure inventory rows as fixture evidence, not live process control, process signalling, host mutation, provider authority, launch-scope decision, public sharing, or whole-system correctness.
Scope limit Declared public helper-process pressure inventory fixture and source-module digest evidence only; no live process reads, process signalling, host mutation, launch-scope decision, external model access, non-public data equivalence, publishing-scope decision, or whole-system correctness.
Covers Tool Server Pressure Inventory
Source
Source Source module: src/microcosm_core/organs/tool_server_pressure_inventory.py · Design note · Source registry