Files
pad/plugin/monitors/monitors.json
T
xarmian 052c971785 feat(plugin): consent-gated push monitor + tri-state arm/disarm + envelope (PLAN-2613 S3, TASK-2618) (#1150)
* feat(plugin): consent-gated push monitor + tri-state arm/disarm + envelope (PLAN-2613 S3, TASK-2618)

The plugin layer of the push-consent gate. S2 built the CLI arm/disarm/status
verbs and the arm-state file; S3 makes the monitor existence itself the gate
(D1) and adds the tri-state, the envelope, and the connect ritual.

- Tri-state arm-state file: a session can be explicitly ARMED, explicitly
  DISARMED, or absent. `pad session disarm` now writes a session-scoped OFF
  marker (not a file removal), so a within-session disconnect wins even in an
  auto_arm=true repo — the disconnect verb must not be a lie there. The marker
  dies with the session (same liveness), so across sessions auto_arm remains
  the standing contract. ResolveAnnouncedArmed folds the tri-state over
  auto_arm; the monitor announces its result.

- Gated monitors (monitors.json): the single always-on monitor is replaced by
  two — an `always` auto-arm monitor and an `on-skill-invoke:connect` manual
  monitor — both running scripts/pad-monitor.sh. The wrapper gates on a new
  hidden `pad session should-arm`, dedupes concurrent monitors with a
  liveness-aware per-session lockfile, and carries the reconnect loop so an
  in-session disarm stops the stream on its next reconnect. No consent → the
  monitor exits → nothing listening.

- D5 envelope: a push notification carries the verbatim direction-with-authority
  framing (confirm in-session before anything destructive/irreversible); item-
  change kinds stay a light informational label.

- /pad:connect + /pad:disconnect skills; /pad:status gains a one-line connection
  header from `pad session status`. /pad:connect runs the workspace's
  on-session-start playbooks on the first connect only (D8), tracked by a
  Booted flag carried forward across arm/disarm. plugin 0.2.1 → 0.3.0.

Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V

* fix(plugin): address Codex R1 on S3 (disarm stops active stream, fail-closed local state)

- HIGH-1: a within-session disarm now stops an ACTIVE stream, not just the
  next reconnect. The monitor re-checks consent every 2s while streaming and
  cancels the connection when it flips to not-armed, then exits (D1's whole-
  stream-behind-consent gate at the top of the loop), so the plugin wrapper
  keeps it dead. Fixes /pad:disconnect being a lie for an idle SSE that might
  never naturally reconnect.
- HIGH-2: a corrupt/unreadable local arm-state file now fails CLOSED
  (LocalArmError -> not armed) instead of falling through to auto_arm, so a
  corrupted disarm marker can't silently re-arm an auto_arm repo. It is not
  reaped (reaping would re-arm on the next read); it is session-keyed and a
  re-arm overwrites it.
- Shell wrapper: an empty (mid-startup) lock pid is treated as live so two
  monitors can't both steal the lock; INT/TERM now exit (a trap otherwise
  resumes the loop and reconnects without a lock).
- Docs: plugin/skills/pad describes the new push-envelope line format;
  connect/status skills distinguish "consent set (armed)" from the server's
  observed connection counts rather than claiming "Connected".

Bounded/safe-direction residuals documented in code: the reap TOCTOU and the
Booted carry-forward race (both fail-closed / benign), and lock pid-reuse
(dedupe only, fails toward not-streaming).

Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V

* fix(plugin): address Codex R2 on S3 (disarm-watcher timing, semantic corruption fail-closed)

- HIGH-1: the disarm-watcher now starts BEFORE the connection is opened, so a
  disarm during connection/header negotiation cancels the request too (the
  request is built on streamCtx). streamWatchEvents also re-checks consent
  before delivering each notification and stops the stream if it was
  withdrawn, so no push is printed after a disarm even within the poll window.
- HIGH-2: a syntactically-valid but semantically-garbage arm-state file (e.g.
  {} or {"pid":1}) now fails CLOSED via a well-formedness check (StartedAt +
  PID must be present, as our writer always stamps them) before liveness or
  reaping — so it can't be judged owner-dead, reaped, and re-armed through
  auto_arm, nor mistaken for a live headless arm naming init.
- LOW: the cleanup trap uses condition 0 (portable) rather than the EXIT name.
  The disconnect skill note reflects the ~2s active-stream drop.

Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V

* fix(plugin): /pad:disconnect always disarms, never gated on a linked workspace (Codex R3)

Consent is session-scoped (keyed by the messaging socket, not the workspace),
so a session that connected in one repo must be able to disconnect from
anywhere — including a directory with no .pad.toml. The old precondition let a
session move to an unlinked directory, "disconnect", and keep receiving pushes.
Verified: `pad session disarm` from an unlinked cwd disarms the socket-keyed
session state; should-arm then reports not-armed back in the original repo.

Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V

* fix(cli): enforce the Armed != Disarmed writer invariant in arm-state validation (Codex R4)

armStateWellFormed checked only StartedAt + PID, so a well-stamped file that
violated the writer invariant — both armed and disarmed false (or both true) —
passed validation and, since SessionArmState only branches on Disarmed,
resolved to LocalArmOn and armed. The writer always sets exactly one of the
two; require it, so a neither/both file fails closed (LocalArmError).

Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
2026-08-18 00:24:19 -04:00

15 lines
952 B
JSON

[
{
"name": "pad-events-auto",
"when": "always",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/pad-monitor.sh",
"description": "Pad item events for repos that opted in via .pad.toml push.auto_arm. Exits immediately (no stream, nothing listening) unless this session has consented — the security gate (PLAN-2613). Use /pad:connect to enable pushes for a session that didn't auto-arm."
},
{
"name": "pad-events-connect",
"when": "on-skill-invoke:connect",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/pad-monitor.sh",
"description": "Pad item events after you run /pad:connect. Starts the push/watch stream for this session once you've consented; /pad:disconnect stops it. Explicit subscriptions (pad watch <ref>) and pushes addressed to you (pad push, or the web item view's push composer) arrive as notifications. Silent when the pad CLI isn't installed, no .pad.toml is present, or padd is unreachable."
}
]