mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 15:05:40 +00:00
e747a1610c
## Summary TASK-2767 (IDEA-2750 part 2, with part 3 riding along — the keying fix and the reaping are one mechanism). The local session registry (`~/.pad/sessions`) was keyed on the pid of the `pad session register` subprocess, which is dead before anyone reads the file. One session left a new file per call and its own pid appeared in none of them; the only live identifier was the harness pid a reader could parse out of the socket path's basename. In practice nothing wrote it (zero callers in `plugin/`, `skills/`, or hooks) and nothing read it. Now: - **One record per session, keyed on the harness session pid** — `$PAD_SESSION_PID` (harness-agnostic override), else `$CLAUDE_PID` (verified present in both the tool shell and a live plugin monitor's `/proc/<pid>/environ`), else the calling process. A set-but-invalid value is an error, not a silent fall-through. - **The record carries the agent name** the session's writes are attributed to (`ResolveAgentName`: `.pad.toml agent_name` → `$PAD_AGENT` → detected runtime; `--agent` overrides, `--agent ""` is anonymous), the harness session id, and the messaging socket's identity (inode/device/mtime — the same binding the arm-state file uses). - **One owner-identity type, one verdict.** `internal/cli/session_owner.go`: `SessionOwner` + tri-state `OwnerLiveness` (`alive` / `dead` / `unknown`). `armStateOwnerAlive` is now `OwnerLiveness(...) == alive` with its file contract preserved (socket identity else mtime; headless pid + start token; fail closed). The registry pruner takes the opposite posture on `unknown`: on Windows `pidAlive` reports dead for every pid, and a reaper built on that would delete every live session's record. - **Verbs:** `pad session register [--agent]` (writes/refreshes; prunes dead records), `pad session list [--agent] [--cwd] [--all] [--format json]` (liveness per row, newest first; dead hidden unless `--all`), `pad session prune [--older-than DUR]` (dead always; unknown only under an explicit bound; alive never). Nothing on MCP — host-local filesystem state. - **Who registers:** `plugin/scripts/pad-monitor.sh` runs `pad session register` on start, BEFORE the consent gate — presence is a fact, consent is a grant, and the record is local/0600/never on the wire. - **Legacy v1 files** list as `legacy` rows: owner = socket-basename pid (else registrar pid), liveness by pid only (v1 recorded no socket identity, and the socket-without-identity rule would have judged every legacy record dead while its session ran). A legacy row can say a session exists, never who it is. Lead rulings on the four open decisions, all as built: `agent`/`--agent` vocabulary; no server-presence merge in `list`; register from the monitor script before the gate; wire follow-on (agent name on the stream) filed separately as IDEA-2750 part 2b. One ordering change from the plan's section A: pid precedence is `PAD_SESSION_PID` > `CLAUDE_PID` > self (explicit override beats detection, mirroring `PAD_AGENT` over runtime detection); the plan listed `CLAUDE_PID` first. ## Behaviour changes for existing users of `~/.pad/sessions` / `pad session register` - Registry files are keyed on the **harness session pid** (`PAD_SESSION_PID` → `CLAUDE_PID` → self), not the `pad` command's pid; repeated registrations overwrite one record instead of accumulating. - `pad session register` records the agent name, harness session id and socket identity; stores the **real path** of the cwd; prints a different text line and a different JSON shape (the full `SessionRecord`); and **rejects** an invalid `PAD_SESSION_PID` / `CLAUDE_PID` instead of silently keying on itself. - Existing v1 files are read as `legacy` rows (owner = socket-basename pid, no agent name) and dead ones are pruned by the next register. - The plugin monitor now registers (and prunes) on every start, before the consent gate. - `armStateOwnerAlive` now delegates to the shared `OwnerLiveness`; the consent gate's observable behaviour is unchanged on every platform and key type (codex round 4 traced every caller; matrix M29 pins the socket-keyed mapping). https://claude.ai/code/session_016zc6oxBvpax6Z3iQMsAJno
50 lines
1.4 KiB
Go
50 lines
1.4 KiB
Go
//go:build linux
|
|
|
|
package cli
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
// pidIsSelfOrAncestor reports whether pid is this process or one of its
|
|
// ancestors, walking /proc/<pid>/stat's ppid field (via procStatFields,
|
|
// the parser shared with the start-token reader). It is
|
|
// how a registration's CLAUDE_PID / PAD_SESSION_PID claim is checked on
|
|
// Linux (TASK-2767, codex round 3): a `pad session register` run by a
|
|
// harness — as a hook, a monitor, or a tool shell — is a DESCENDANT of
|
|
// that harness's session process, so a claimed session pid that is not
|
|
// in the ancestry is a claim about some other process. It does not make
|
|
// the claim proof of much else (init is everyone's ancestor), but it
|
|
// refutes the case the registry exists to prevent: naming a sibling
|
|
// session's pid as one's own.
|
|
func pidIsSelfOrAncestor(pid int) (bool, error) {
|
|
cur := os.Getpid()
|
|
for depth := 0; depth < 128 && cur > 0; depth++ {
|
|
if cur == pid {
|
|
return true, nil
|
|
}
|
|
parent, err := procParentPID(cur)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if parent == cur || parent <= 0 {
|
|
return false, nil
|
|
}
|
|
cur = parent
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func procParentPID(pid int) (int, error) {
|
|
fields, err := procStatFields(pid)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
const ppidIndexAfterComm = 1 // field 4: state(3), ppid(4)
|
|
if len(fields) <= ppidIndexAfterComm {
|
|
return 0, errProcStatMalformed
|
|
}
|
|
return strconv.Atoi(fields[ppidIndexAfterComm])
|
|
}
|