Gateway Subprocess Guard

Bounded, non-interactive shell-outs so a hung command can never stall the gateway event loop

The gateway bounds every git and shell command it runs, so a child stuck on a credential prompt or a lock can never freeze your WebSocket clients, bots, or heartbeat.

Diagram

Subprocess Guard

finishes in time

hangs past timeout

📋 Subprocess call

🛡️ run_guarded
timeout + diagnosis

✅ CompletedProcess

⚠️ returncode=124
+ diagnosis in stderr

Quick Start

  1. Default posture — nothing to configure

    The guard is already applied inside the gateway. Every git command the kanban dispatcher runs, plus the macOS and Windows startup probes, are bounded automatically. An agent that queues kanban work needs no code change — the guard wraps the dispatcher’s git calls for you.

    python
    from praisonai_bot.kanban.sqlite_store import SQLiteKanbanStore
    
    store = SQLiteKanbanStore()
    
    # The dispatcher's git calls (worktree add, merge, rev-parse) are guarded
    # automatically — no timeout wiring on your side.
    task = store.create_task({
        "title": "Refactor auth module",
        "repo_path": "/repos/service-a",
    })
  2. Operator tuning — raise the ceilings

    Export the two environment variables to widen the timeouts for a legitimately slow repo. Both are read fresh on each call.

    bash
    # Every git command from the kanban dispatcher (default 300s)
    export PRAISONAI_KANBAN_GIT_TIMEOUT=600
    
    # Every other guarded call without an explicit timeout (default 120s)
    export PRAISONAI_SUBPROCESS_TIMEOUT=180

How It Works

On timeout the guard kills the whole process group, reaps whatever output exists, and returns a CompletedProcess with returncode=124 — never an exception.

Diagram
Child processrun_guardedCallerChild processrun_guardedCallerhangs on prompt / lockrun command (bounded)spawn in own process groupgroup kill (killpg / taskkill /T)partial outputreap (bounded 5s) + diagnoseCompletedProcess(returncode=124, stderr=diagnosis)

Two layers of defence work together:

LayerWhat it does
Preventstdin is /dev/null; GIT_TERMINAL_PROMPT=0, GCM_INTERACTIVE=never, DEBIAN_FRONTEND=noninteractive are layered under your env so tools fail fast instead of prompting.
ExplainOn timeout the child’s partial output is matched against known stall signatures and turned into an actionable message in stderr.

Configuration Options

Two operator-tunable environment variables control the ceilings.

Env varApplies toDefaultNotes
PRAISONAI_KANBAN_GIT_TIMEOUTEvery git command from the kanban dispatcher (_run_git)300 sDeliberately generous — worktree add / merge on a large repo is legitimately slow. Non-positive, non-finite (inf, nan), or non-numeric values are ignored and the default is used.
PRAISONAI_SUBPROCESS_TIMEOUTEvery other guarded call whose caller did not pass an explicit timeout=120 sSame validation rules as above.

Diagnostic Messages

The guard decodes why a child stalled and writes it to stderr, so you can act without reading source.

Signature the child emittedWhat stderr will sayFix
Username for / password for / passphrase / authentication failedDETECTED: credential prompt.Configure a credential helper, or set GIT_TERMINAL_PROMPT=0.
index.lock / unable to create / another git processDETECTED: git lock contention.Retry once the other git exits, or remove a stale .git/index.lock.
[y/N] / (yes/no) / are you sure / continue? / overwrite? / proceed?DETECTED: confirmation prompt.Re-run with --yes / -y / --force / --no-confirm.
host key / fingerprint / known_hostsDETECTED: SSH host-key prompt.Pre-populate known_hosts, or pass -o StrictHostKeyChecking=accept-new.
(unmatched)No interactive prompt was detected. The command may be genuinely slow, blocked on network I/O, or deadlocked. Raise timeout= if the work is expected to take longer.Investigate manually.

Adopted Call Sites

The guard already wraps every gateway path that shells out.

SiteWhy it mattersTimeout used
gateway/kanban_dispatcher.py::_run_gitEvery dispatcher git call — bounded by the guard and run off the gateway event loop in a worker thread. A hung git still returns returncode=124 + diagnosis; a healthy-but-slow one no longer stalls the gatewayPRAISONAI_KANBAN_GIT_TIMEOUT (300 s default)
kanban/sqlite_store.py::_is_git_repogit rev-parse on the create_task path15 s explicit
gateway/preflight.py::_scan_launch_agentlaunchctl list on macOS startup15 s explicit
gateway/pairing.py::_restrict_windows_aclicacls while hardening the pairing secret on Windows15 s explicit

User-Visible Behaviour Change

The create_task git probe now downgrades a slow path instead of hanging — but that means it no longer auto-enables worktree isolation for it.

When a task is linked to a git repo, kanban auto-upgrades it to workspace_kind="worktree". The one exception: if the git rev-parse --is-inside-work-tree probe times out (15 s), the task is created in the shared workspace rather than an isolated worktree, and the gateway log records:

text
Timed out probing <path> for a git worktree; treating it as not a
repository, so this task will run in the shared workspace rather than
an isolated worktree.

Grep for Timed out probing to catch NFS- or lock-contended paths that quietly lost isolation.


Best Practices

Leave the defaults alone unless you see a real timeout

The 300 s git ceiling already tolerates worktree add and merge on large repos. Only raise PRAISONAI_KANBAN_GIT_TIMEOUT after you observe a legitimately-slow git op hitting the limit.

Never set a timeout to 0, negative, inf, or non-numeric

The guard ignores unusable overrides and falls back to the built-in default. Setting PRAISONAI_KANBAN_GIT_TIMEOUT=0 does not disable the guard — it silently uses 300 s.

Grep logs for lost worktree isolation

Watch gateway logs for Timed out probing to catch NFS or lock-contended paths whose create_task git probe timed out and fell back to the shared workspace.


Related