diff --git a/build-zulip-plugin.prose.md b/build-zulip-plugin.prose.md index 9333f13..45027ef 100644 --- a/build-zulip-plugin.prose.md +++ b/build-zulip-plugin.prose.md @@ -14,6 +14,30 @@ description: > - generation_count: number — How many generations have been produced - last_generation: timestamp — When the last generation was created - known_issues: array — Issues discovered in the current version + + **Postconditions** (the plugin is GOOD when ALL pass): + + *Connectivity* + - connected == true — Establishes and maintains Zulip event queue + - reconnect_on_failure == true — Reconnects after queue expiry + - recovers_from_network_loss == true — Handles temporary network drops + + *Message Flow* + - dm_response_time_ms < 10000 — DMs get a response within 10 seconds + - stream_mention_detection == true — @mentions in streams are detected and routed + - all_bots_detection == true — @all-bots mentions are detected + - echo_loop_prevention == true — Never replies to its own messages + + *Code Quality* + - syntax_check == "pass" — All Python files are valid + - has_register_function == true — Exposes `register(ctx)` entry point + - plugin_yaml_valid == true — plugin.yaml parses correctly + - follows_base_adapter_pattern == true — Extends BasePlatformAdapter + + *Reliability* + - graceful_disconnect == true — shutdown doesn't crash + - handles_malformed_messages == true — Bad JSON doesn't kill the poll loop + - typing_indicators_work == true — Sends typing notifications ## Parameters @@ -26,45 +50,19 @@ description: > The improvement cycle is **event-driven**, not just cron-based: -- **On check failure**: If any Success Criteria fails, wake immediately to fix +- **On check failure**: If any postcondition fails, wake immediately to fix - **On user request**: `prose run build-zulip-plugin` — explicit upgrade - **On new pattern**: If another plugin or agent reports a better pattern, wake to assimilate - **Retrospective**: Every 24 hours if no other trigger fired — scan for subtle degradation - **On first deploy**: Always run Generation 1 when no plugin exists yet -## Success Criteria (What Makes a Plugin "Good") - -The plugin is considered GOOD when ALL of these pass: - -### Connectivity -- plugin.connected == true — Establishes and maintains Zulip event queue -- plugin.reconnect_on_failure == true — Reconnects after queue expiry -- plugin.recovers_from_network_loss == true — Handles temporary network drops - -### Message Flow -- dm_response_time_ms < 10000 — DMs get a response within 10 seconds -- stream_mention_detection == true — @mentions in streams are detected and routed -- all_bots_detection == true — @all-bots mentions are detected -- echo_loop_prevention == true — Never replies to its own messages - -### Code Quality -- syntax_check == "pass" — All Python files are valid -- has_register_function == true — Exposes `register(ctx)` entry point -- plugin_yaml_valid == true — plugin.yaml parses correctly -- follows_base_adapter_pattern == true — Extends BasePlatformAdapter - -### Reliability -- graceful_disconnect == true — shutdown doesn't crash -- handles_malformed_messages == true — Bad JSON doesn't kill the poll loop -- typing_indicators_work == true — Sends typing notifications - ## Remembers Each generation stores: - What worked well (success patterns) - What broke (failure modes) - What the user complained about (pain points) -- Which Success Criteria passed and failed +- Which postconditions passed and failed ## Generation Rules diff --git a/infrastructure-control.prose.md b/infrastructure-control.prose.md index e8319b3..e802c81 100644 --- a/infrastructure-control.prose.md +++ b/infrastructure-control.prose.md @@ -60,19 +60,23 @@ description: > | Proxmox Root | Password via API ticket | `root@pam:kakashi19` | ✅ | | docker-vm (.7) | SSH root | SSH key | ✅ | | CT 116 (syslog-api) | SSH root | SSH key | ✅ | -| Tanko CT (.122) | SSH jerome | SSH key | ✅ | +| Tanko CT (.122) | SSH jerome | id_ed25519 | ✅ | +| Mumuni CT (.123) | SSH root | id_ed25519 | ✅ | +| Koonimo CT (.114) | SSH jerome | ❌ no key access | | Netbird (.17) | SSH root | SSH key | ✅ | | Gitea | API token | abiba-bot token | ✅ | -| Zulip | Bot API key | per-bot tokens | ✅ | +| Zulip | Bot API key | abiba-bot@chat.sysloggh.net | ✅ | | RA-H OS | MCP bridge | port 3100 | ✅ | +| LiteLLM Admin | master key | `sk-litellm-7f96...` | ✅ | +| Grafana | admin password | `syslog-grafana-2026` | ✅ | ### Reachability Matrix -| From / To | PVE API | docker-vm (.7) | CT 116 | Tanko (.122) | Netbird (.17) | -|-----------|---------|----------------|--------|-------------|---------------| -| **Abiba** (CT 100) | ✅ :443 | ✅ SSH | ✅ SSH | ✅ SSH | ❌ no Netbird | -| **Tanko** (CT 122) | ❌ | ❌ | ❌ | ✅ | ❌ | -| **docker-vm** (.7) | ❌ | ✅ | ❌ | ❌ | ❌ | +| From / To | PVE API | docker-vm (.7) | CT 116 | Tanko (.122) | Mumuni (.123) | Koonimo (.114) | +|-----------|---------|----------------|--------|-------------|---------------|----------------| +| **Abiba** (.24) | ✅ :443 | ✅ SSH | ✅ SSH | ✅ SSH jerome | ✅ SSH root | ❌ SSH | +| **Tanko** (.122) | ❌ | ❌ | ❌ via NetBird | ✅ | ❌ | ❌ | +| **Mumuni** (.123) | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | **Conclusion:** Only Abiba has cross-infrastructure access. All monitoring contracts run from Abiba. @@ -138,14 +142,34 @@ description: > ### Ecosystem B: CT 116 syslog-api (192.168.68.116) -| Container | Image | Port | -|-----------|-------|------| -| harness-litellm | berriai/litellm:1.90.0-rc.1 | :4001 | -| harness-nginx | nginx:alpine | :80 | -| harness-router | inference-harness-router | :9000 | -| harness-postgres | postgres:16-alpine | :5432 | -| harness-redis | redis:7-alpine | :6379 | -| harness-dashboard | inference-harness-dashboard | :3000 | +8 containers in inference-harness stack: + +| Container | Image | Port | Role | +|-----------|-------|------|------| +| harness-litellm | berriai/litellm:1.90.0-rc.1 | :4000→:4001 | API proxy, key mgmt, fallbacks | +| harness-router | inference-harness-router | :9000 (127.0.0.1) | GPU routing, slot booking, CB | +| harness-nginx | nginx:alpine | :80 | Entrypoint, /v1→LiteLLM, /dashboard/, /grafana/ | +| harness-postgres | postgres:16-alpine | :5432 | LiteLLM DB (keys, spend, config) | +| harness-redis | redis:7-alpine | :6379 | Router slots, circuit breakers | +| harness-dashboard | inference-harness-dashboard | :3000 | SyslogAI Harness UI | +| harness-grafana | grafana/grafana | :3000→:3001 | GPU dashboards via Prometheus | +| harness-prometheus | prom/prometheus | :9090 | Metrics scraper, 5 targets | + +**Nginx routing**: +- `/v1/*` → harness-litellm:4000 (API) +- `/admin/*` → harness-litellm:4000 +- `/dashboard/` → harness-dashboard:3000 +- `/litellm/*` → harness-litellm:4000 +- `/grafana/` → harness-grafana:3000 +- `/health/*` → harness-litellm:4000/health/liveliness +- `/gpu/` → 192.168.68.24:9100 (fleet monitor) + +**Prometheus targets**: +- 192.168.68.8:9400 (RTX 3090 — qwen) +- 192.168.68.110:9400 (RTX 5070 — gemma) +- 192.168.68.15:9400 (Strix Halo — ornith) +- 192.168.68.24:9401 (Router metrics exporter) +- harness-litellm:4000 (LiteLLM health) ### Ecosystem C: Netbird (72.61.0.17) @@ -157,6 +181,18 @@ description: > | netbird-crowdsec | crowdsecurity/crowdsec:v1.7.7 | WAF | | netbird-traefik | traefik | Reverse proxy | +> **Dependency note:** NetBird is an **access layer only** — it exists so a +> human on a PC or away from the LAN can reach services by URL. It is **not** +> a dependency of any service backend. Per Section 7, every config and agent +> must reach backends by LAN IP; the `*.sysloggh.net` URLs are reserved for +> human browsers. The 2026-07-02 outage (NetBird VPS hung) took out every +> CNAME'd URL while every LAN-IP backend stayed up — proof the doctrine is +> necessary. +> +> **Recovery:** a hung NetBird VPS (TCP accepts, SSH banner exchange times +> out, TLS hangs) cannot be self-remediated over the network. It requires a +> hard reboot from the hosting provider's console. See Section 5.3. + ### Checks (every 60s) ``` @@ -226,25 +262,103 @@ For docker-vm specifically: ## Section 5: Network Services — Monitoring -| Service | Domain | Status | -|---------|--------|--------| -| Proxmox API | minipve.sysloggh.net:443 | ✅ | -| Authentik | auth.sysloggh.net:443 | ✅ | -| Gitea | git.sysloggh.net:443 | ✅ | -| Zulip | chat.sysloggh.net:443 | ✅ | -| LiteLLM | litellm.sysloggh.net:443 | ✅ | -| Pulse | pulse.sysloggh.net:443 | ✅ | -| SearXNG | searxng.sysloggh.net:8888 | ✅ | -| Firecrawl | firecrawl.sysloggh.net:3002 | ✅ | +### 5.1 Service Inventory -### Checks (every 2 min) +The `Resolves To` column is the load-bearing one. Services that CNAME to +`netbird.sysloggh.net` (72.61.0.17) are routed through the NetBird reverse +proxy and **fail whenever NetBird blips**, even though their LAN backend is +fine. Services that resolve directly to a LAN IP are NetBird-independent. + +| Service | Domain | LAN Backend | Resolves To | NetBird-dep | Status | +|---------|--------|-------------|-------------|-------------|--------| +| Proxmox API | minipve.sysloggh.net:8006 | 192.168.68.12 | LAN IP | No | ✅ | +| LiteLLM | litellm.sysloggh.net | 192.168.68.116 | LAN IP | No | ✅ | +| Authentik | auth.sysloggh.net:443 | 192.168.68.11 | CNAME → netbird | **Yes** | ⚠️ | +| Gitea | git.sysloggh.net:443 | 192.168.68.110 | CNAME → netbird | **Yes** | ⚠️ | +| Zulip | chat.sysloggh.net:443 | 192.168.68.117 | CNAME → netbird | **Yes** | ⚠️ | +| Pulse | pulse.sysloggh.net:443 | 192.168.68.7 | CNAME → netbird | **Yes** | ⚠️ | +| DNS UI | dns.sysloggh.net:443 | 192.168.68.102 | CNAME → netbird | **Yes** | ⚠️ | +| SearXNG | searxng.sysloggh.net:8888 | 192.168.68.7:8888 | LAN IP | No | ✅ | +| Firecrawl | firecrawl.sysloggh.net:3002 | 192.168.68.7:3002 | LAN IP | No | ✅ | + +**Verified 2026-07-02:** NetBird VPS rebooted after a hang; all CNAME'd +services recovered. LAN-IP-direct paths stayed up throughout the outage. + +### 5.2 Checks (every 2 min) ``` ## Maintains -- dns-resolution: { services: [{name, resolves}] } +- dns-resolution: { services: [{name, resolves_to, is_lan_ip}] } - ssl-expiry: { services: [{name, days_remaining}] } - endpoint-reachability: { services: [{name, http_code}] } +- netbird-dependency: { services: [{name, depends_on_netbird: bool}] } +``` + +### 5.3 Network Verification & Routing (every 5 min) + +This block is what catches a NetBird blip before it becomes an outage. It +proves two independent paths for every service: the **URL path** (what a +browser uses, may cross NetBird) and the **LAN-IP path** (what configs and +agents must use, never crosses NetBird). + +``` +## Maintains + +- dual-path-reachability: { + services: [{ + name: string, + url_path: { http_code, tls_ok }, # the *.sysloggh.net path + lan_path: { ip, port, http_code }, # the IP:port path + lan_independent: bool # lan_path works when url_path fails + }] + } +- netbird-ingress-health: { + vps_up: bool, # TCP 22/80/443 accept on 72.61.0.17 + ssh_banner_ok: bool, # banner exchange completes (catches hung box) + containers_up: int, # all 5 netbird-* containers running + proxy_routes_ok: int # traefik routers resolving (chat/git/auth return 2xx/3xx) + } +- routing-regression: { + new_netbird_cnames: [domain], # domains that newly CNAME to netbird.sysloggh.net + # → alert: a service just became NetBird-dependent + config_url_violations: [file] # configs/agents found referencing a *.sysloggh.net + # URL where a LAN IP is required (see Section 7) + } + +## Checks + +- For each service in 5.1: + - curl https:/// → record url_path (code, tls_ok) + - curl http://:/ → record lan_path + - If url_path fails AND lan_path succeeds → netbird-degraded, NOT service-down + - If BOTH fail → service-down (real outage) + +- NetBird ingress (72.61.0.17): + - TCP probe 22, 80, 443 → all accept + - SSH banner exchange completes within 10s (catches the hung-box symptom + where TCP accepts but sshd never sends its banner) + - ssh root@72.61.0.17 'docker ps' → 5/5 netbird-* containers Up + - curl https://chat.sysloggh.net/ → 2xx/3xx (proxy route alive) + +- DNS dependency drift: + - For each *.sysloggh.net, dig +short → flag any new CNAME → netbird.sysloggh.net + - A service moving FROM LAN-IP TO netbird CNAME is a regression (alert) + - A service moving FROM netbird CNAME TO LAN-IP is an improvement (log) + +## Remediations + +- NetBird VPS hung (TCP up, SSH banner timeout, TLS hang): + → This is a host-level hang, not a service fault. Cannot self-remediate via SSH. + → Alert crit immediately: "NetBird VPS hung — needs hard reboot from provider console" + → Do NOT declare backend services down; their LAN-IP paths are still up. +- Single proxy route missing (e.g. git returns 000 but chat ok): + → ssh root@72.61.0.17 'docker restart netbird-traefik' + → re-check after 30s +- New NetBird CNAME detected: + → Alert: " became NetBird-dependent — violates IP-first doctrine (Section 7)" +- Config URL violation detected: + → Alert with file + line; do not auto-edit configs ``` ## Section 6: Alert Routing & Escalation @@ -281,6 +395,63 @@ ZFS pool degraded → Cannot auto-fix → Escalate immediately ``` +## Section 7: Configuration Doctrine — IP-First, URLs for Browsers + +This doctrine is the structural fix for the NetBird-dependency outage. It is +enforced by the `routing-regression.config_url_violations` check in Section +5.3. + +### Rule + +1. **Service configs use LAN IP addresses, always.** Any file that wires a + service, agent, monitor, or integration to another internal service must + reference the backend by its `192.168.68.x` LAN IP and port — never by a + `*.sysloggh.net` URL. NetBird may blip at any time; LAN IPs do not. +2. **`*.sysloggh.net` URLs are reserved for human browser access.** They are + a convenience layer (split-horizon DNS on the LAN, NetBird reverse proxy + off the LAN) for a person at a keyboard. They must never be a load-bearing + dependency in code or config. +3. **Agents and monitors reach backends by LAN IP.** Abiba, Tanko, and Mumuni + all run on the LAN; there is no reason for them to hairpin through NetBird. + The daily infra report and all `ssh`/`curl` checks already follow this. +4. **DNS records should prefer LAN IPs over NetBird CNAMEs.** A service that + can resolve directly to its LAN IP (like `litellm` → `192.168.68.116` and + `minipve` → `192.168.68.12`) is NetBird-independent. Moving a record from + a NetBird CNAME to a LAN IP is an improvement; the reverse is a regression. +5. **NetBird is for remote/PC access only.** When you are out or on a PC, + NetBird carries you in. When you are on the LAN, NetBird is not in the + path. Configs must reflect this — they always assume LAN. + +### Canonical IP map (configs MUST use these) + +| Service | Use in configs | URL (browsers only) | +|---------|----------------|--------------------| +| Proxmox API | `https://192.168.68.12:8006` | `https://minipve.sysloggh.net` | +| LiteLLM API | `http://192.168.68.116:4000` | `https://litellm.sysloggh.net` | +| LiteLLM (nginx) | `http://192.168.68.116` | — | +| Grafana | `http://192.168.68.116:3001` | — | +| Authentik | `https://192.168.68.11` | `https://auth.sysloggh.net` | +| Gitea | `http://192.168.68.110:3000` | `https://git.sysloggh.net` | +| Zulip API | `http://192.168.68.117` | `https://chat.sysloggh.net` | +| SearXNG | `http://192.168.68.7:8888` | — | +| Firecrawl | `http://192.168.68.7:3002` | — | +| RA-H OS bridge | `http://192.168.68.65:3100` | — | + +### Violation examples (to be flagged by Section 5.3) + +- ❌ An agent config setting `LITELLM_BASE_URL=https://litellm.sysloggh.net` +- ✅ Same config setting `LITELLM_BASE_URL=http://192.168.68.116:4000` +- ❌ A monitor curling `https://chat.sysloggh.net/api/v1/server_settings` +- ✅ A monitor curling `http://192.168.68.117/api/v1/server_settings` (and + optionally the URL as a separate browser-path check) + +### Goal state + +Every `*.sysloggh.net` record resolves to a LAN IP (split-horizon DNS on LAN, +NetBird reverse proxy off LAN). NetBird then becomes purely the off-LAN +ingress — if it blips, only remote browser users notice, and no agent, +monitor, or integration breaks. + ## Appendix A: Quick Health Commands ```bash @@ -293,8 +464,30 @@ curl -sfk "$PVE/api2/json/cluster/resources" -H "$AUTH" ssh root@192.168.68.7 "docker ps --format '{{.Names}} {{.Status}}'" ssh root@192.168.68.116 "docker ps --format '{{.Names}} {{.Status}}'" +# GPU fleet quick check +curl -s http://192.168.68.116/health/unified | jq .status +curl -s http://192.168.68.24:9100/gpu-data | jq .summary + +# Grafana status +curl -s http://admin:syslog-grafana-2026@192.168.68.116:3001/api/health + +# Prometheus targets +curl -s http://192.168.68.116:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}' + +# LiteLLM key check +curl -s -H "Authorization: Bearer sk-litellm-7f96080dd99b15c36bd4b333b58a6796" \ + http://192.168.68.116/litellm/key/list | jq '.keys[] | {alias: .key_alias, models: .models}' + # Storage check ssh root@192.168.68.7 "df -h /media/storage /media/mediastore" + +# Router roster reload (if needed) +curl -s -X POST http://192.168.68.116:9000/admin/roster/reload \ + -H "Authorization: Bearer sk-admin-ee09fffd04978b61a1569ac670c68814" + +# Restart stuck GPU (saturation watchdog alternative) +ssh root@192.168.68.8 "systemctl restart llama-server" +ssh root@192.168.68.110 "systemctl restart llama-server" ``` ## Appendix B: CT Inventory @@ -302,9 +495,9 @@ ssh root@192.168.68.7 "df -h /media/storage /media/mediastore" | CT | Name | Node | IP | Role | Agent | |----|------|------|----|------|-------| | 100 | abiba | amdpve | .24 | Pi agent (this host) | ✅ pi | -| 101 | llm-gpu | acerpve | — | GPU VM | ❌ | +| 101 | llm-gpu | acerpve | .8 | GPU RTX 3090 | ❌ | | 102 | adguard | acerpve | — | DNS | ❌ | -| 103 | ocu-llm | ocupve | — | GPU VLM | ❌ | +| 103 | ocu-llm | ocupve | .110 | GPU RTX 5070 | ❌ | | 104 | authentik | minipve | .11 | OIDC | ❌ | | 105 | kagentz | amdpve | — | Agent Zero | ✅ | | 106 | ra-h-os | storepve | .65 | KG bridge | ✅ MCP | @@ -314,10 +507,10 @@ ssh root@192.168.68.7 "df -h /media/storage /media/mediastore" | 110 | gitea | minipve | — | Git | ❌ | | 111 | tdunna | amdpve | — | ? | ❌ | | 112 | tanko | amdpve | .122 | Hermes agent | ✅ | -| 113 | baggy | amdpve | — | ? | ❌ | -| 114 | mumuni | minipve | — | Hermes agent | ✅ | +| 113 | baggy/koonimo | amdpve | .114 | Hermes agent | ✅ | +| 114 | mumuni | minipve | .123 | Hermes agent | ✅ | | 115 | scottdenya | amdpve | — | ? | ❌ | -| 116 | syslog-api | minipve | .116 | LiteLLM stack | ❌ | +| 116 | syslog-api | minipve | .116 | LiteLLM + Grafana | ❌ | | 117 | zulip | storepve | — | Chat | ❌ | | 118 | jitsi | minipve | — | Video | ❌ | @@ -329,5 +522,5 @@ ssh root@192.168.68.7 "df -h /media/storage /media/mediastore" | docker-vm (.7) | SearXNG | `/opt/search-stack/searxng/docker-compose.yml` | | docker-vm (.7) | Home stack | `/opt/home_stack/docker-compose.yml` | | docker-vm (.7) | Audiobookshelf | `/opt/audiobookshelf/docker-compose.yml` | -| CT 116 (.116) | LiteLLM | `/root/docker-compose-litellm.yml` | +| CT 116 (.116) | Inference Harness | `/opt/inference-harness/docker-compose.yml` | | Netbird (.17) | Netbird | Docker run (not compose) | diff --git a/pi-approval-architecture.prose.md b/pi-approval-architecture.prose.md new file mode 100644 index 0000000..2097ea2 --- /dev/null +++ b/pi-approval-architecture.prose.md @@ -0,0 +1,107 @@ +--- +kind: pattern +name: pi-approval-architecture +description: > + Documents pi's approval model vs Hermes, what commands are available, + and the architectural constraints that prevent mid-turn tool approval in pi. + Updated with /approve and /deny stub commands for cross-agent UX consistency. +agent: abiba +version: 1.0.0 +--- + +# Pi Approval Architecture + +## TL;DR + +| Feature | Hermes | Pi (Abiba) | +|---------|--------|------------| +| `/approve` command | ✅ Full — unblocks agent | ✅ Stub — acknowledges, no blocking | +| `/deny` command | ✅ Full — blocks agent | ✅ Stub — acknowledges, no blocking | +| Dangerous command detection | ✅ 40+ patterns + Tirith | ❌ None (LLM judgment only) | +| Mid-turn tool approval | ✅ Agent thread blocks on Event | ❌ No hook in agent loop | +| Smart approval (aux LLM) | ✅ `approvals.mode: smart` | ❌ N/A | +| Yolo mode | ✅ `/yolo`, `approvals.mode: off` | ❌ N/A | +| Permanent allowlist | ✅ `command_allowlist` + `shell-hooks` | ❌ N/A | +| Session-scoped approval | ✅ `approve_session()` | ❌ N/A | +| Approval timeout | ✅ 60s default, configurable | ❌ N/A | + +## Why Pi Can't Do Mid-Turn Approval + +Pi's agent loop is **418 lines** of TypeScript. It's deliberately minimal: + +``` +LLM decides → calls tool → tool executes immediately → output goes to LLM → repeats +``` + +There is no middleware between "LLM decides to call bash" and "bash executes." +The extension system (`injectIntoSession`) feeds messages IN but doesn't intercept +tool calls mid-turn. + +Hermes has a full approval pipeline: +``` +LLM decides → check_all_command_guards() → if dangerous → notify_cb → block thread +→ user responds /approve → resolve_gateway_approval() → Event.set() → unblock +``` + +This pipeline is deeply integrated into Hermes' gateway, tool executor, and +session management. Replicating it in pi would require rewriting the core agent +loop — violating pi's design constraint of minimal core. + +## What Pi HAS: `/approve` and `/deny` Stubs + +Added to prevent `/approve` and `/deny` from being forwarded to the LLM as +garbled input. When a user types these commands to Abiba: + +- **`/approve`** → "Pi doesn't have pending approvals. Commands execute immediately." +- **`/approve session`** → Same response +- **`/deny`** → Same response + "For Hermes agents (Tanko, Mumuni), these work with their built-in approval system." + +This keeps the UX consistent across agents — users can type `/approve` anywhere +without getting confused by LLM responses. + +## Pi's Full Command Set (Hermes Parity) + +| Command | Handler | Description | Hermes Equivalent | +|---------|---------|-------------|-------------------| +| `/status` | System | PM2 processes, containers, session info | `/status` | +| `/retry` | Session | Re-injects last user message | `/retry` | +| `/continue` | Session | Continues truncated response | `/continue` | +| `/new [topic]` | Session | Clears conversation context | `/new` | +| `/stop` | Control | Interrupts current LLM processing | `/stop` | +| `/model [name]` | Info | Show current model, list available | `/model` | +| `/compact` | Session | Requests context compaction | `/compact` | +| `/yolo` | Stub | Pi has no approval system — accepted for UX | `/yolo` | +| `/verbose` | Stub | Verbosity controlled by prompt, not toggle | `/verbose` | +| `/approve` | Stub | Pi executes immediately — no approvals | `/approve` | +| `/deny` | Stub | Pi executes immediately — no approvals | `/deny` | +| `/help [cmd]` | Info | Shows command help | `/help` | + +## What Pi COULD Add (Without Core Changes) + +These would work within pi's current architecture: + +1. **Pre-message dangerous command scan**: Before forwarding an agent response to + Zulip, scan the text for dangerous bash commands and add a warning banner. + Doesn't prevent execution but alerts user. + +2. **User-facing approval prompt injection**: When user asks pi to do something + dangerous (detected via regex on user input), pi could respond with a + confirmation request before proceeding. This works because it's at the + message level, not mid-turn. + +3. **Response rate-limiting**: If pi generates many bash commands rapidly, + throttle and ask for confirmation. Works within the extension's send path. + +## Files + +| File | Purpose | +|------|---------| +| `/root/.pi/agent/extensions/zulip/index.js` | Pi Zulip extension with command system | +| `/root/prose-contracts/zulip-approval-fix.prose.md` | Hermes Zulip approval fix | +| `/root/prose-contracts/pi-approval-architecture.prose.md` | This document | + +## Related + +- Hermes `tools/approval.py` — Full approval pipeline (dangerous patterns, Tirith, smart mode) +- Hermes `gateway/slash_commands.py:_handle_approve_command` — Slash command handler +- Hermes Zulip adapter fix — HTML stripping for command detection diff --git a/zulip-approval-fix.prose.md b/zulip-approval-fix.prose.md new file mode 100644 index 0000000..70c2330 --- /dev/null +++ b/zulip-approval-fix.prose.md @@ -0,0 +1,74 @@ +--- +kind: responsibility +name: zulip-approval-fix +description: > + Fixes broken /approve and /deny slash commands for Hermes agents in Zulip. + Zulip delivers messages as HTML (

/approve

), but the gateway's slash + command parser expects plain text. HTML tags prevent command matching. +agent: abiba +version: 2.0.0 +status: deployed +--- + +## Root Cause + +The Hermes gateway ALREADY has native `/approve` and `/deny` handling via +`slash_commands.py:_handle_approve_command()`. No custom approval interception +is needed in the platform adapter. + +The bug: Zulip's API returns message content as **rendered HTML** +(`

/approve session

`), but the gateway's slash command prefix matcher +checks `text.startswith("/approve")`. The `

` tag prevents matching. + +## Fix (Applied) + +One line added to `~/.hermes/plugins/platforms/zulip/adapter.py`: + +```python +content = msg.get("content", "") +content = _strip_html(content) # Strip Zulip HTML for /commands +``` + +Plus a small `_strip_html()` helper that strips HTML tags and decodes entities. + +## How the Full Flow Works + +1. Agent tries dangerous command → `check_all_command_guards()` +2. Gateway's `_approval_notify_sync` sends approval prompt via `send()` +3. User sees: "⚠️ Dangerous command requires approval — Reply /approve or /deny" +4. User types `/approve session` in Zulip +5. Zulip API returns `

/approve session

` +6. Adapter strips HTML → `/approve session` +7. Gateway's slash command handler matches `/approve` +8. `_handle_approve_command` calls `resolve_gateway_approval(session_key, "session")` +9. Agent thread unblocks, command executes + +## Gateway Slash Commands Supported + +| Command | Effect | +|---------|--------| +| `/approve` | Approve once | +| `/approve session` | Approve for this session | +| `/approve always` | Permanently approve this pattern | +| `/approve all` | Approve ALL pending commands | +| `/approve all session` | Approve all + remember session | +| `/deny` | Deny oldest pending | +| `/deny all` | Deny all pending | + +## Files Changed + +| File | Host | Change | +|------|------|--------| +| `~/.hermes/plugins/platforms/zulip/adapter.py` | .122, .123 | +_strip_html() helper + HTML stripping in message handler | + +## Deployed + +- ✅ Mumuni (.123) — patched + gateway restarted +- ✅ Tanko (.122) — patched (jerome user), gateway restart pending + +## Verification + +1. Send DM: "run rm /tmp/test" to Mumuni/Tanko in Zulip +2. Agent should respond with approval prompt: "⚠️ Dangerous command requires approval" +3. Reply `/approve` — agent should execute and respond +4. Reply `/deny` — agent should block and explain why