Skip to content

Commit c33933f

Browse files
committed
damage-control: packs/groups state, heal+migration, CLI, MCP, doctor, docs (#1141)
Built by dotdev.dev
1 parent 3149d4e commit c33933f

23 files changed

Lines changed: 755 additions & 18 deletions

‎.claude/skills/agentwire-cli/SKILL.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,9 @@ agentwire fetch <url> --limit 4000 # cap chars (default 8000, 0 = no limit)
286286
agentwire safety check "cmd" # test if command would be blocked
287287
agentwire safety status # show pattern counts and recent blocks
288288
agentwire safety logs # query audit logs
289+
agentwire safety packs list|status [--json] # vendor rule packs (aws, databases, ...): off by default
290+
agentwire safety packs enable|disable <name> # HUMAN-ONLY (tty + protected); agents suggest, never run
291+
agentwire safety groups list|status|enable|disable <g> # default groups; kernel can't be disabled
289292
agentwire safety install # install damage control hooks
290293
agentwire hooks install # install permission hook (Claude Code only)
291294
agentwire hooks uninstall # remove permission hook (Claude Code only)

‎.claude/skills/agentwire-mcp-tools/SKILL.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,6 @@ up|down`) — the MCP surface is read-only introspection.
182182

183183
**Chrome tab bookkeeping (MCP):** `chrome_tab_track(tab_id, url, session)` / `chrome_tab_untrack(tab_id, session)` — agentwire can't call `tabs_close_mcp` itself (that server runs inside the calling agent's own client), so this is pure tracking: a worktree session records a verification tab right after `tabs_create_mcp`, then closes it + untracks before finishing. `worktree_remove`/`worktree_prune --gc-merged` check this during teardown and surface any tab a session never closed, so the tearing-down agent can close it. `chrome_tab_list(session)` shows what's tracked.
184184

185+
**Damage-control packs (read-only, #1141):** `safety_packs_list()` / `safety_groups_list()` show which vendor rule packs (aws, databases, containers, ...) and default groups are on. When the user's work touches a vendor whose pack is off, SUGGEST `agentwire safety packs enable <name>` — enabling/disabling is human-only (tty gate + protected command); never run it. See wiki internals/damage-control.md.
186+
185187
**Briefing Mode** (asymmetric-verbosity orchestration): an `anchor` (terse, human-facing) fans out `correspondent` worktrees (verbose researchers) via `worktree_create(roles="correspondent")`; they file reports into `research_dir()` (`~/.agentwire/research/<session>/`) and drop a passive `msg_send(kind="ingest", ref="<path>")` pointer; the anchor `msg_pull()`s on the human's cue and briefs with `say(text=..., display=...)` (voice + toast, different content). See wiki briefing-mode.md.

‎CLAUDE.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ LLM-maintained knowledge base at `~/.agentwire/wiki/` using the Karpathy LLM Wik
166166

167167
Deterministic (zero-LLM) recovery from the Claude Code usage-limit dialog: a launchd watchdog (`agentwire limits tick`, 60s) plus ensure's completion poll detect the dialog, park the session (option 1: stop and wait), parse the reset time, email the owner via Resend, and nudge the session to continue after reset. Park state under `~/.agentwire/usage-limit/` guards every surface — ensure exits 7 (`usage_limit`), the scheduler skips dispatch, the idle hook never reaps a parked session. CLI under `agentwire limits ...`. Full reference: [`docs/wiki/usage-limit-recovery.md`](docs/wiki/usage-limit-recovery.md).
168168

169+
## Damage-Control Packs (#1141)
170+
171+
Every bundled `rules/*.yaml` / `tooldefs/*.yaml` is owned by one entry in `agentwire/safety/groups.py`: a default **group** (on; a human may opt out — `agentwire safety groups disable <g>`; the kernel `control-plane` + `core` never can) or an opt-in vendor **pack** (aws, gcp, firebase, cloud-hosting, databases, containers, infrastructure, publish — off until `agentwire safety packs enable <name>`). State is `packs:` / `disabled_groups:` in the protected `~/.agentwire/damagecontrol.yml`; mutations are human-only (tty gate + `PROTECTED_COMMAND_PATTERNS`), reads (`list|status`, MCP `safety_packs_list`/`safety_groups_list`) are open. **The hooks never read the state — they load what is installed, so "off" means "not installed"** (heal removes only a *pristine* file of a disabled entry, never an edited one). A pre-packs install is migrated by recording every pack with any installed file as enabled, so upgrading changes nothing. **Agents SUGGEST enabling a pack when the user's work touches that vendor; they never run it.** Detail: [`docs/wiki/internals/damage-control.md`](docs/wiki/internals/damage-control.md#rule-groups-and-packs-1141).
172+
169173
## Expired-Login Detection
170174

171175
An expired Claude login refuses every turn with a **synthetic** assistant message (`Login expired · Please run /login`, `error: authentication_failed`, zero tokens, 16ms) and then goes idle. The pane is alive and the agent process is running, so every liveness check passes forever — which is how #867 cost `memory-manager` 7217s and `ai-morning-briefing` 14400s on the same 2026-08-04 outage, reported as `incomplete — Timeout waiting for task completion`. **The detector reads the TRANSCRIPT, never the pane** (`agentwire/auth_expired.py`): Claude renders this inline, not as a select-menu, so the phrase also appears in any pane merely *quoting* the incident, and a false positive here halts the whole board. Keyed on the structured `error` field, and on the **LAST** assistant turn only — so recovery needs no reset mechanism. **Field-proven across two outages that render DIFFERENTLY** (`Login expired` 08-04 / `Not logged in` 07-07, same `error`), which is the argument for the structured field; the residual risk is a future RESTRUCTURING of that field, not a rewording. All 16 real `rate_limit` rows are structurally identical to an auth refusal and reach the same predicate — only the error-value check keeps a transient blip from gating the machine. Transcripts are located by `conversation_ids` (#871) or, when the record predates that, by files in the project's history dir written since **the attempt began** — never "the newest `.jsonl`". That anchor is load-bearing: the refusal lands ~15ms after the prompt submits, seconds before the completion wait is entered, so anchoring at the wait misses it entirely. Detection is **machine-wide** (`~/.agentwire/auth-expired/state.json`) — one outage, one owner email (throttled 1h, shared Resend wiring), `detected_at` carried forward, an `OUTAGE_TTL` so a stale flag can never wedge the scheduler, and a `clear_state()` on the completion success path so a successful turn genuinely reopens the gate rather than the docs merely claiming it does. Surfaces: ensure exits **8** (`auth_expired`) and never retries, scheduler dispatch skips without consuming `last_run`, worktree finalize opens no PR, `agentwire doctor` reports it. No credential pre-flight: the secret is Keychain-gated on macOS, and `expiresAt` is the *access* token's, which lapses routinely and refreshes silently. Full reference: [`docs/wiki/auth-expired.md`](docs/wiki/auth-expired.md).

‎agentwire/doctor_cli.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,27 @@ def _render_damage_control_section() -> int:
547547
if not (missing or outdated or unknown):
548548
print(f" [ok] Damage-control {label} installed and match bundled")
549549

550+
# Rule groups and packs (#1141): informational only, never scored — a
551+
# disabled pack is a choice, not drift.
552+
try:
553+
from agentwire.safety_packs_cli import overview
554+
555+
rows = overview()
556+
on = [r["name"] for r in rows if r["kind"] == "pack" and r["enabled"]]
557+
off = [r["name"] for r in rows if r["kind"] == "pack" and not r["enabled"]]
558+
out = [r["name"] for r in rows if r["kind"] == "group" and not r["enabled"]]
559+
print(f" [..] Damage-control packs enabled: {', '.join(on) or '(none)'}")
560+
print(f" [..] Packs available: {', '.join(off) or '(none)'} "
561+
f"(agentwire safety packs enable <name>)")
562+
for r in rows:
563+
if r["suggest_enable"]:
564+
print(f" [..] {', '.join(r['binaries_found'])} found here — "
565+
f"pack '{r['name']}' is off")
566+
if out:
567+
print(f" [..] Opted-out default groups: {', '.join(out)}")
568+
except Exception as e:
569+
print(f" [..] Could not read pack state: {e}")
570+
550571
# Files the bundle used to ship and no longer does. Informational: heal
551572
# never deletes them, and an edited one is the user's own (#1137).
552573
for section, files in safety_commands.retired_installed().items():

‎agentwire/hooks/damage-control/bash-tool-damage-control.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def log_escape(*args, **kwargs): pass
3434

3535

3636
# === BEGIN AGENTWIRE HOOK STAMP (generated — do not edit) ===
37-
AGENTWIRE_HOOK_STAMP = {"core_sha256": "33f11ebb4e4472aedb37164dcb4c5d1010c5472bc516028d9aaa2c12070bdebc", "generated_at": "2026-10-19T00:29:08Z"}
37+
AGENTWIRE_HOOK_STAMP = {"core_sha256": "05b94cdd15dd83517e6b4fa5e7235c98eb4bd484ec0b5d7b8c1cc90de4cdb810", "generated_at": "2026-10-19T11:15:19Z"}
3838
# === END AGENTWIRE HOOK STAMP ===
3939
# === BEGIN GENERATED FROM agentwire/safety/_core.py ===
4040
"""
@@ -5263,6 +5263,11 @@ def _refused_for_quoted_substitution(
52635263
"agentwire tasks promote writes the protected .agentwire.tasks.yml — "
52645264
"host-only, run it from your own terminal (not through an agent).",
52655265
),
5266+
(
5267+
r'\bagentwire\s+safety\s+(?:packs|groups)\s+(?:enable|disable)\b',
5268+
"agentwire safety packs/groups enable|disable changes damage-control "
5269+
"policy — host-only, run it from your own terminal (not through an agent).",
5270+
),
52665271
]
52675272

52685273

‎agentwire/hooks/damage-control/edit-tool-damage-control.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def log_disabled(*args, **kwargs): pass
3030

3131

3232
# === BEGIN AGENTWIRE HOOK STAMP (generated — do not edit) ===
33-
AGENTWIRE_HOOK_STAMP = {"core_sha256": "33f11ebb4e4472aedb37164dcb4c5d1010c5472bc516028d9aaa2c12070bdebc", "generated_at": "2026-10-19T00:29:08Z"}
33+
AGENTWIRE_HOOK_STAMP = {"core_sha256": "05b94cdd15dd83517e6b4fa5e7235c98eb4bd484ec0b5d7b8c1cc90de4cdb810", "generated_at": "2026-10-19T11:15:19Z"}
3434
# === END AGENTWIRE HOOK STAMP ===
3535
# === BEGIN GENERATED FROM agentwire/safety/_core.py ===
3636
"""
@@ -5259,6 +5259,11 @@ def _refused_for_quoted_substitution(
52595259
"agentwire tasks promote writes the protected .agentwire.tasks.yml — "
52605260
"host-only, run it from your own terminal (not through an agent).",
52615261
),
5262+
(
5263+
r'\bagentwire\s+safety\s+(?:packs|groups)\s+(?:enable|disable)\b',
5264+
"agentwire safety packs/groups enable|disable changes damage-control "
5265+
"policy — host-only, run it from your own terminal (not through an agent).",
5266+
),
52625267
]
52635268

52645269

‎agentwire/hooks/damage-control/mcp-tool-damage-control.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def log_escape(*args, **kwargs): pass
4949

5050

5151
# === BEGIN AGENTWIRE HOOK STAMP (generated — do not edit) ===
52-
AGENTWIRE_HOOK_STAMP = {"core_sha256": "33f11ebb4e4472aedb37164dcb4c5d1010c5472bc516028d9aaa2c12070bdebc", "generated_at": "2026-10-19T00:29:08Z"}
52+
AGENTWIRE_HOOK_STAMP = {"core_sha256": "05b94cdd15dd83517e6b4fa5e7235c98eb4bd484ec0b5d7b8c1cc90de4cdb810", "generated_at": "2026-10-19T11:15:19Z"}
5353
# === END AGENTWIRE HOOK STAMP ===
5454
# === BEGIN GENERATED FROM agentwire/safety/_core.py ===
5555
"""
@@ -5278,6 +5278,11 @@ def _refused_for_quoted_substitution(
52785278
"agentwire tasks promote writes the protected .agentwire.tasks.yml — "
52795279
"host-only, run it from your own terminal (not through an agent).",
52805280
),
5281+
(
5282+
r'\bagentwire\s+safety\s+(?:packs|groups)\s+(?:enable|disable)\b',
5283+
"agentwire safety packs/groups enable|disable changes damage-control "
5284+
"policy — host-only, run it from your own terminal (not through an agent).",
5285+
),
52815286
]
52825287

52835288

‎agentwire/hooks/damage-control/read-tool-damage-control.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def log_disabled(*args, **kwargs): pass
3636

3737

3838
# === BEGIN AGENTWIRE HOOK STAMP (generated — do not edit) ===
39-
AGENTWIRE_HOOK_STAMP = {"core_sha256": "33f11ebb4e4472aedb37164dcb4c5d1010c5472bc516028d9aaa2c12070bdebc", "generated_at": "2026-10-19T00:29:08Z"}
39+
AGENTWIRE_HOOK_STAMP = {"core_sha256": "05b94cdd15dd83517e6b4fa5e7235c98eb4bd484ec0b5d7b8c1cc90de4cdb810", "generated_at": "2026-10-19T11:15:19Z"}
4040
# === END AGENTWIRE HOOK STAMP ===
4141
# === BEGIN GENERATED FROM agentwire/safety/_core.py ===
4242
"""
@@ -5265,6 +5265,11 @@ def _refused_for_quoted_substitution(
52655265
"agentwire tasks promote writes the protected .agentwire.tasks.yml — "
52665266
"host-only, run it from your own terminal (not through an agent).",
52675267
),
5268+
(
5269+
r'\bagentwire\s+safety\s+(?:packs|groups)\s+(?:enable|disable)\b',
5270+
"agentwire safety packs/groups enable|disable changes damage-control "
5271+
"policy — host-only, run it from your own terminal (not through an agent).",
5272+
),
52685273
]
52695274

52705275

‎agentwire/hooks/damage-control/write-tool-damage-control.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def log_disabled(*args, **kwargs): pass
2929

3030

3131
# === BEGIN AGENTWIRE HOOK STAMP (generated — do not edit) ===
32-
AGENTWIRE_HOOK_STAMP = {"core_sha256": "33f11ebb4e4472aedb37164dcb4c5d1010c5472bc516028d9aaa2c12070bdebc", "generated_at": "2026-10-19T00:29:08Z"}
32+
AGENTWIRE_HOOK_STAMP = {"core_sha256": "05b94cdd15dd83517e6b4fa5e7235c98eb4bd484ec0b5d7b8c1cc90de4cdb810", "generated_at": "2026-10-19T11:15:19Z"}
3333
# === END AGENTWIRE HOOK STAMP ===
3434
# === BEGIN GENERATED FROM agentwire/safety/_core.py ===
3535
"""
@@ -5258,6 +5258,11 @@ def _refused_for_quoted_substitution(
52585258
"agentwire tasks promote writes the protected .agentwire.tasks.yml — "
52595259
"host-only, run it from your own terminal (not through an agent).",
52605260
),
5261+
(
5262+
r'\bagentwire\s+safety\s+(?:packs|groups)\s+(?:enable|disable)\b',
5263+
"agentwire safety packs/groups enable|disable changes damage-control "
5264+
"policy — host-only, run it from your own terminal (not through an agent).",
5265+
),
52615266
]
52625267

52635268

‎agentwire/mcp_status.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,34 @@ def network_status() -> str:
9090
if output:
9191
return output
9292
return f"Failed to check network: {data.get('error', 'Unknown error')}"
93+
94+
95+
def _format_entries(label: str, data: dict) -> str:
96+
if not data.get("success"):
97+
return f"Failed to list {label}: {data.get('error', 'Unknown error')}"
98+
lines = []
99+
for e in data.get("entries", []):
100+
mark = "on" if e["enabled"] else "off"
101+
suffix = " (vendor CLI found on this machine)" if e.get("suggest_enable") else ""
102+
lines.append(f"[{mark}] {e['name']} — {e['description']}{suffix}")
103+
return "\n".join(lines) or f"No {label}."
104+
105+
106+
@mcp.tool()
107+
def safety_packs_list() -> str:
108+
"""List damage-control vendor packs (aws, databases, ...) and whether each is on.
109+
110+
Read-only. Enabling or disabling a pack is human-only: SUGGEST
111+
`agentwire safety packs enable <name>` to the user when their work touches
112+
that vendor — never run it yourself.
113+
"""
114+
return _format_entries("packs", run_agentwire_cmd(["safety", "packs", "list"]))
115+
116+
117+
@mcp.tool()
118+
def safety_groups_list() -> str:
119+
"""List damage-control default groups and whether each is on (read-only).
120+
121+
Kernel groups are always on. Opting a group out is human-only.
122+
"""
123+
return _format_entries("groups", run_agentwire_cmd(["safety", "groups", "list"]))

0 commit comments

Comments
 (0)