jcp-gateway: meter each call on the upstream server's own process tree #198
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/gateway-upstream-energy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
jcp-gateway metered each upstream tool call with the machine-wide meter. On macOS 27 every machine-wide CPU rail is frozen to an unprivileged reader, so every call was sealed
Estimator.The work of a call runs in the upstream MCP server, a process the gateway spawned. The kernel counts that process's own CPU energy (
proc_pid_rusageri_energy_nj), and any same-uid process can read it, live. This change reads that count across the server's process tree and seals the result asHwShunt. It is gradedWholeonly when nothing was seen missing and the operator has declared the server self-contained.What
sandbox-meter:
TreeMeter(src/probe/tree.rs, macOS). It meters the CPU energy of another live process tree: the root and every descendant (proc_listchildpids). A reading also carries the gaps it saw:reaped-descendantgoneexitedpredates-spanunsettled-at-start/unsettled-at-stoptree-changingunreadable/unlistedgpu/gpu-unknownsrc/probe/gpu_clients.rs.neural-engineri_lifetime_max_neural_footprint).Settling. The count is folded only at a context switch. Read from another process, CPU time also moves in ~10 ms steps, so "CPU time unchanged" proves nothing. An end is read only when every member has no running thread (
pti_numrunning) and two consecutive walks agree on membership, parents, reaped-descendant times and exit times.jcp-gateway
Upstream::pid()names the process. The tree meter is built lazily for it and follows a pid change.HwShunt,Whole: a tree reading with no gap, and the operator has calleddeclare_upstream_self_contained()/ passed--upstream-self-contained;HwShunt,Partial: any other tree reading;_metagainsenergy_domainand, whenPartial,energy_partial_because. The sealed receipt format is unchanged, and the plaintext provenance and coverage still equal the sealed ones.Why
Wholeneeds a declaration. No reader can see work a process hands outside its tree. One measured case: an on-device model client served by a system daemon showed no Neural Engine footprint while the Neural Engine drew about 1 W. Its own CPU count advanced only tens of millijoules. A system service over XPC, a database server or a remote API are the same. Only the operator can say the server does none of that.Fixes to the binary
notifications/initializedhung it. Notifications are now sent with no wait.Partialwithreply-not-the-response.tools/call,resources/readorprompts/getsent without anidis now refused before it reaches the server, with nothing sent back. The attempt is sealed on a frame whose action,notification:<tool>, no grant carries.Spec.
jcp/SPEC.md§5.1 rung 3 now covers metering another process tree: its domain, the settle rule (MUST), every gap that makes a figurePartial, and thatWholeMUST NOT be sealed without the operator's declaration.jcp/README.mdandsandbox/README.mdare updated to match.Evidence
powermetricsand Core ML's compute plan.proc_pid_rusageread sharing no code with the meter, to the nanojoule (sandbox-meter) and to the µJ rounded up (gateway). The servers range from a bare echo to tens of millijoules, and the tests ran at load average 150–190.sandbox-exec; a server sending notifications and requests mid-call.Whole. It reproduced by round trip 2 of 40 before the fix, and readPartialin 240 of 240 round trips after it.cargo test -p sandbox-meterandcargo test -p jcp-gatewayare green at load average ~187. Linux cross-checks are clean, and clippy reports nothing new.jcp-gateway (upstream process tree)step. The sandbox-meter step now also grepstree meter:. Each step fails unless its platform-gated tests printed their line.Not covered
ri_lifetime_max_neural_footprintnonzero on a real process: building a Core ML model needs tooling the runner does not have. That gap is covered by unit tests and by today's probe./usr/bin/ccat test time. They passed in a user session but have not yet run under the macOS runner's launchd context; this PR's CI is their first run there.🤖 Generated with Claude Code