* feat(cli): pad session arm/disarm/status + consent config resolution (PLAN-2613 S2, TASK-2617)
The S2 CLI contract S3's plugin skills and S4's web composer build against.
S1 gated push delivery on a server-side armed bit declared at stream
connect; nothing decided WHETHER to arm or sent the declaration. S2 adds
both, defaulting off everywhere.
- ResolveAutoArm (internal/cli/arm_consent.go): pure consent resolver.
.pad.toml [push] auto_arm is the only per-repo enabler (D4); a per-user
config auto_arm=false vetoes it (deny-wins); default off. Config
surfaces: PadToml.Push.AutoArm + config.Config.Push.AutoArm (*bool,
unset != false), both nil-safe.
- Wire contract: StreamSessionIdentity.Armed sends ?armed=true on the
event stream — S1's server gate finally has a sender. The monitor
announces armed = live local arm OR resolved auto_arm, so a repo
opt-in works end to end with a safe default-off skew.
- Verbs pad session arm/disarm/status: arm/disarm manage a per-session
local arm-state file; status reports the resolved local/auto decision
plus the server's own armed/connected counts (new Client.ListSessions),
degrading gracefully when padd is unreachable.
- Arm-state file (session_arm_state.go): keyed per session by
CLAUDE_CODE_MESSAGING_SOCKET (cwd fallback for headless, secondary to
auto_arm). Mandatory liveness — a dead-owner file (socket vanished /
pid gone) reads as disarmed and is reaped, so a crashed session can
never arm a future monitor. Local client state only; the server's
armed bit stays the sole delivery authority.
Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
* fix(cli): address Codex R1 on push-consent (fail-closed config, owner-identity liveness)
- HIGH-1: user config.toml read now fails CLOSED. config.LoadPushConfigAutoArm
reads the [push] auto_arm value strictly — absent → no opinion, but
present-but-unparseable → error — and ResolveAutoArmFromDisk refuses to
auto-arm when it can't confirm the user's veto (was: swallowed by the
lenient config.Load and treated as no-opinion).
- HIGH-2: arm-state liveness now checks owner IDENTITY, not just presence.
Socket-keyed files record the socket's mtime and require an exact match,
so a reused socket path can't revive a stale file. Headless files record
a Linux /proc start-time token (portable fallback documented) to reject a
reused pid.
- MED-1: arm-state writes are atomic (temp + rename) and reaping is
non-destructive (re-checks staleness before removing) — a concurrent
re-arm is never clobbered.
- MED-2: pad session status applies the .pad.toml URL override, so it
queries the same server the monitor connects to.
- LOW: malformed arm-state files are now reaped (safe now that writes are
atomic — a corrupt file can't be a torn in-progress write).
Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
* fix(cli): address Codex R2 on push-consent (atomic config write, stronger owner identity)
- HIGH-1: Config.Save() is now atomic (temp + rename), so a monitor
reconnecting while `pad configure` rewrites config.toml can't read a
truncated/partial file, miss a [push] auto_arm=false veto, and arm.
- finding 2: socket owner identity now uses inode+device (unix) as the
primary signal, with mtime as the non-unix fallback — a rebound socket
or a lingering stale node at the same path gets a new inode and is
rejected, closing the mtime-collision / reused-node gaps.
- finding 3: headless liveness fails closed when a proc-start token was
recorded but can't be re-verified (was: fell back to bare pid-liveness,
which a reused pid passes); zombies (state 'Z') now report not-alive.
- finding 5: `pad session status` applies an explicit --url override too,
not just the .pad.toml one.
- finding 4 (connect-time TOCTOU): documented as an accepted, bounded
residual — a disarm racing an in-flight connect is corrected on the next
reconnect; fully closing it needs S3's server-side disarm-on-open signal.
Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V