diff --git a/.env.example b/.env.example --- a/.env.example +++ b/.env.example @@ -11,6 +11,9 @@ # OPTIONAL: comma-separated chat IDs with GUEST (read-only) access: only /status, # /health and host-local commands marked "guest": true — no LLM, nothing else. # TELEGRAM_GUEST_USERS= +# OPTIONAL: seconds a /confirm-protected command stays armed (default 180, min 30). +# OGMA_CONFIRM_TTL=180 + # --- optional: the LLM (assistant) layer --- # claude = free-text chat is handled by Claude Code (the default; needs the claude CLI). # off = command-only mode: the bot runs its predefined commands only, no LLM at all. diff --git a/CHANGELOG.md b/CHANGELOG.md --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ ### Added (Phase 2 — hardened command runner) - **/confirm for protected commands.** `"confirm": true` on a local command (and core `/restart`) arms the command instead of running it; it fires only on - /confirm within 60s, /cancel disarms. Typos can no longer restart the gateway. + /confirm within the confirm window (default 180s, `OGMA_CONFIRM_TTL`; 60s + proved too tight for phone-paced replies), /cancel disarms. Typos can no + longer restart the gateway. - **Per-argument validation.** `"validate": [regex, ...]` + `"min_args"` on a local command are checked by the gateway before anything executes; a broken pattern disables the command (fails closed). diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ [Self-management](#self-management-ogmactl) - your own host-local commands from `config/commands.local.json` — with optional per-argument regex validation (`"validate"`), a guest flag, and `"confirm": true` for anything destructive - `/confirm` / `/cancel` — protected commands (core: `/restart`) arm instead of firing and run - only after `/confirm` within 60s + only after `/confirm` within the confirm window (default 180s, `OGMA_CONFIRM_TTL`) - `/help` — usage - every executed command is recorded in the append-only audit log (`state/audit.log`, JSON lines: who, what, exit code, duration) — view it with `/logs audit` @@ -180,6 +180,7 @@ | `TELEGRAM_BOT_TOKEN` | BotFather token (**required**) | — | | `TELEGRAM_ALLOWED_USERS` | comma-separated allowed chat IDs (**required**; full/admin access) | — | | `TELEGRAM_GUEST_USERS` | chat IDs with read-only access: `/status`, `/health`, local commands marked `"guest": true` — no LLM | — | | `OGMA_LLM` | `claude` = assistant layer on; `off` = command-only mode (no LLM) | `claude` if the CLI exists | +| `OGMA_CONFIRM_TTL` | seconds a /confirm-protected command stays armed (min 30) | `180` | | `CLAUDE_BIN` | path to the `claude` CLI | `~/.local/bin/claude` | | `CLAUDE_TIMEOUT` | per-message timeout (seconds) | `300` | | `OGMA_MAX_CONCURRENT` | max concurrent Claude runs across chats (raise only on a roomy host) | `1` | diff --git a/config/commands.local.example.json b/config/commands.local.example.json --- a/config/commands.local.example.json +++ b/config/commands.local.example.json @@ -15,7 +15,8 @@ " args - optional usage hint shown in /help, e.g. '' or ' '.", " guest - true to let TELEGRAM_GUEST_USERS chats run it (default false = admins only).", " Only mark read-only commands as guest.", " confirm - true to arm the command instead of running it: the user must send /confirm", - " within 60s (or /cancel). Use for anything destructive or disruptive.", + " within the confirm window (default 180s, OGMA_CONFIRM_TTL) or /cancel.", + " Use for anything destructive or disruptive.", " validate - optional list of regexes, one per positional argument, checked by the gateway", " BEFORE anything is executed. The list length is the maximum number of args;", " a broken regex disables the whole command (fails closed).", diff --git a/gateway.py b/gateway.py --- a/gateway.py +++ b/gateway.py @@ -82,7 +82,9 @@ _inflight_lock = threading.Lock() DENY_COOLDOWN = 600 # seconds between replies to a non-allowed sender _denied: dict[str, float] = {} # sender -> when we last answered its denial -CONFIRM_TTL = 60 # seconds a /confirm-protected command stays armed +# How long a /confirm-protected command stays armed. Human-paced: the prompt has +# to reach a phone and the reply travel back — 60s proved too tight in practice. +CONFIRM_TTL = max(30, int(cfg("CONFIRM_TTL", "180") or "180")) _pending_confirm: dict[str, tuple[float, str, list[str]]] = {} # chat -> (expiry, cmd, argv) _confirm_lock = threading.Lock()