* fix(agent-team): serve() starts the inbound Slack listener (D-1) Coordinator.serve() now constructs and starts the SlackListener concurrently with the tick/drain loop on a background daemon thread, but ONLY when the live transport is a SlackTransport AND SLACK_APP_TOKEN is configured. When Slack is not the transport or the app token is absent, serve() behaves exactly as before (tick/recover only) — Slack is never made mandatory. - New injectable build_listener seam + default_slack_listener_factory sharing the coordinator's own transport, ledger db_path, and resume_queue put. - AUTHZ-01 owner-allowlist + open-status CAS untouched: serve() sources AGENT_TEAM_SLACK_OWNER_IDS in SlackListener.serve, which still fails closed. - SlackListener.close() added for clean Socket Mode teardown on shutdown; serve() stops the listener + joins the thread in a finally. - Tests: start-when-Slack+app-token, no-start otherwise, clean shutdown, idempotent start, serve start/stop around the loop, listener close(). * fix(agent-team): systemd unit loads ~/orchestrator/.env + uses venv python (D-2/D-7) D-2: add EnvironmentFile=-/home/adam/orchestrator/.env (optional '-') so the P2 GPT-4.1 review loop's cross_reviewer sub-process can read the non-Claude provider key once a task reaches REVIEW. Mirrors the sea-haven-secrev unit. D-7: point ExecStart at the agent-team venv interpreter (/home/adam/orchestrator/agent-team/.venv/bin/python) instead of /usr/bin/env python3, which resolved the system interpreter without the installed deps under systemd's PATH. All hardening (NoNewPrivileges / ProtectSystem=full / ProtectHome=read-only / ReadWritePaths) is retained unchanged (locked decision). * docs(agent-team): land provisioning + operator runbooks under docs/provisioning - PROVISIONING-RUNBOOK.md: merged final state (6 checkers, dep-bump fixer, P5 intake-checker loop), SLACK_CHANNEL_ID, the gated P3-live flip steps (GitHub App + agent-apply env + gated_build_verify_wiring), and D-1/D-2/D-7 marked FIXED so the demo can use the live Slack answer path. - P1-DEMO-SCRIPT.md: live Slack answer path now available (D-1 fixed); both the Slack and operator-CLI answer paths documented for all four exit criteria. - DEPLOY-AUDIT.md: D-1/D-2/D-7 RESOLVED (this PR); D-4/D-5 dep pinning and the operator-CLI divergence kept as provisioning notes. - OPERATOR-RUNBOOK.md (new): incident handling for pipeline stalls, parked tasks, failed HITL resumes, budget exhaustion, transport outages, and COMPLACENCY/COVERAGE alarms — each grounded in real run-team.py verbs, plus the re-alarm-backoff -> Jira-after-N-nights escalation ladder (design §5/§6.6). * fix(agent-team): supervise the Slack listener thread — recurring ALARM + respawn sh-security-review (logic) MEDIUM: a crashed listener thread was logged once, then the daemon ran on 'deaf' — posting clarifier questions but receiving no answers, every gate silently parking, process never exiting so systemd Restart=on-failure never fired. serve() now calls _supervise_slack_listener() each pass: when the listener is enabled but its thread is dead, it emits a recurring ERROR ALARM and respawns via the idempotent starter (self-heal). No-op when alive or disabled. +3 tests. (authz detector: wiring clean — AUTHZ-01 fail-closed allowlist + open-status CAS intact, dead listener fails SAFE.)
9.5 KiB
DEPLOY-AUDIT — agent-team/DEPLOY-R720.md + systemd unit vs. actual code
Cross-check of the drafted deploy doc (agent-team/DEPLOY-R720.md) and the
systemd unit (agent-team/systemd/agent-team-coordinator.service) against the
actual current code in agent-team/agent_team/** and agent-team/run-team.py.
Each finding: location → claimed → actual → fix. Severity: 🔴 blocker / 🟠 should-fix / 🟡 nit. Items confirmed clean are stated explicitly.
Status (deploy-readiness PR): the three deploy-correctness bugs that blocked the live provisioning session — D-1, D-2, D-7 — are RESOLVED in this PR (
feature/agent-team-deploy-readiness). The remaining items (D-4 / D-5 dependency pinning, the operator-CLI divergence) are kept below as provisioning notes — they do not block the coordinator deploy.
✅ D-1 — serve now starts the Slack inbound listener — RESOLVED (this PR)
- Location:
agent_team/coordinator.py(Coordinator.serve+_maybe_start_slack_listener/_slack_listener_enabled/_stop_slack_listener/default_slack_listener_factory);agent_team/transport/slack_listener.py(SlackListener.serve+ newclose). - Was:
Coordinator.serve()did onlybind_subscription_invoker(),setup(),recover(), then an infinite tick/sleep loop — it never constructed or startedSlackListener, so a deployed daemon posted clarifier questions and expired them on deadline but could not hear Slack answers. - Now:
serve()starts theSlackListeneron a background daemon thread, concurrently with the tick/drain loop, when the live transport is aSlackTransportANDSLACK_APP_TOKENis set. It shares the coordinator's own transport, ledgerdb_path, andresume_queueput; on shutdown it calls the listener's newclose()and joins the thread in afinally. When Slack is not the transport or the app token is absent, no listener starts andservebehaves exactly as before — Slack is never made mandatory. The AUTHZ-01 owner allowlist + the open-status compare-and-set are untouched (still fail closed on an emptyAGENT_TEAM_SLACK_OWNER_IDS). - Tests:
tests/test_coordinator.py— start-when-Slack+app-token, no-start without the token, no-start when the transport is not Slack, idempotent start, clean shutdown, serve start/stop around the loop;tests/test_slack_listener.py—close()no-op + handler teardown.
✅ D-2 — coordinator unit loads ~/orchestrator/.env — RESOLVED (this PR)
- Location:
agent-team/systemd/agent-team-coordinator.service;run-team.py:_build_coordinator(always wiresdefault_review_wiring);coordinator.py:default_review_wiring→review_loop_llm.default_plan_reviewer(shells the local orchestratorrun.py→cross_reviewerGPT-4.1). - Was: the unit loaded only
~/secrev.env.run-team.py servebuilds the coordinator with the P2 review loop wired, and once a task reaches REVIEW the reviewer shells the orchestratorrun.py, whose GPT-4.1 call reads the non-Claude provider key from~/orchestrator/.env. The review path would fail to authenticate. - Now: the unit adds
EnvironmentFile=-/home/adam/orchestrator/.env(optional-, mirroring thesea-haven-secrevunit). Does not affect the P1 demo (P1 stops at PLAN before REVIEW); closes the latent P2 break.
🟠 D-4 — pip install list omits requests (provisioning note)
- Location:
agent_team/transport/github_live.py/github_intake.py(import requests). - Actual: the GitHub transport + intake require
requests; the Slack-first path does not hit it, but any--transport github/intake-githubuse fails with a clear RuntimeError without it. - Fix: PROVISIONING-RUNBOOK Step 4 installs
requestsinto the venv.requestsis also absent fromrequirements.txt(see D-5).
🟠 D-5 — agent-team runtime deps are not pinned in requirements.txt (provisioning note)
- Location:
requirements.txt(repo root). - Actual:
requirements.txtpinslanggraph==1.1.10andlanggraph-checkpoint-sqlite==3.1.0, but the agent-team runtime depsclaude-agent-sdk,slack_sdk,slack_bolt,requests(andanthropicfor api mode) are not inrequirements.txtat all — they are installed ad-hoc into the agent-team venv by the runbook. There is no pinned, reproducible source of truth for the box's runtime set. - Fix (deferred): add an
agent-team/requirements.txt(or extras group) pinning these, version-matched to the rootrequirements.txtlanggraph pin. Until then, PROVISIONING-RUNBOOK Step 4 pinslanggraph==1.1.10/langgraph-checkpoint-sqlite==3.1.0explicitly so the unpinnedpip installcannot pull a newer, untested major. Do NOT modifyrequirements.txtor the checkers in this PR (out of scope).
✅ D-6 — slack_bolt is now exercised by the daemon — RESOLVED (consequence of D-1)
- Location:
slack_listener.py:serve(the onlyslack_boltimport). - Now: with D-1 fixed,
Coordinator.serve()startsSlackListener.serve(), which imports + usesslack_boltfor the Socket Mode handler. The dep is right and now actually exercised on the live Slack path.
✅ D-SLACKVAR (clean) — SLACK_CHANNEL_ID matches
run-team.py:_build_transportreads exactlyos.environ.get("SLACK_CHANNEL_ID"). The runbook, the unit comment, and the code all useSLACK_CHANNEL_ID(notSLACK_CHANNEL). CLEAN.
✅ D-ENV-SLACKBOT / OAUTH / OWNERS / APPTOKEN (clean) — names match
SLACK_BOT_TOKEN↔slack_live.py+slack_listenerenv read. CLEAN.CLAUDE_CODE_OAUTH_TOKEN↔invoker.py. CLEAN.AGENT_TEAM_SLACK_OWNER_IDS↔slack_listener.py(name + fail-closed semantics). CLEAN — now read by the running daemon (D-1 fixed).SLACK_APP_TOKEN— now read in two places:coordinator._slack_listener_enabledgates the listener on its presence, anddefault_slack_listener_factory/SlackListener.servesource it to open the socket. CLEAN (read site exists now that D-1 is fixed).
✅ D-7 — ExecStart uses the venv interpreter — RESOLVED (this PR)
- Location:
agent-team/systemd/agent-team-coordinator.serviceExecStart. - Was:
ExecStart=/usr/bin/env python3 run-team.py serveresolved the system interpreter under systemd's PATH — not the venv where the deps were installed, so the daemon would fail at import. - Now:
ExecStart=/home/adam/orchestrator/agent-team/.venv/bin/python run-team.py serve(matches the runbook venv path +WorkingDirectory).
✅ D-SUBCMD (mostly clean) — run-team.py subcommands referenced exist
Cross-checked every run-team.py <sub> the deploy doc + demo name against
run-team.py:build_parser: init-db, serve, list (+ --all / --parked),
show, expire, answer, redeliver, supersede, force-resume, start,
intake-github, intake-checker, fix — all exist. No invented verbs.
Operator-CLI divergence (provisioning note)
Two operator CLIs exist with different verb names:
run-team.py(the entry CLI):init-db, list, show, redeliver, expire, answer, supersede, force-resume, start, serve, intake-github, intake-checker, fix. Hasshowand--parked;--db/--audit-logdefault sensibly.agent_team/operator_cli.py:list, redeliver, force-expire, answer-on-behalf, force-resume— noshow,--db/--audit-logare required, and itsforce-resumesupersedes (unlikerun-team.py's, which reopens an expired row and never supersedes).Use
run-team.pyfor provisioning + the demo + incident recovery. The docs reference onlyrun-team.py. Reconciling the two CLIs is a follow-up.
✅ D-PYTHONPKG (clean) — package import bootstrap is correct
run-team.py inserts its own dir into sys.path so the hyphenated script
imports the agent_team package without an editable install. CLEAN.
✅ D-HARDENING (clean, and matches the locked decision)
- Unit:
NoNewPrivileges=true,ProtectSystem=full,ProtectHome=read-only,ReadWritePaths=/home/adam/orchestrator/agent-team/state. Retained unchanged in this PR (locked decision — do not revert to secrev parity). - The
ReadWritePathscarve-out matches the ledger + audit-log location (state/agent_team.sqlite,state/audit.log.jsonl). CLEAN.
Summary table
| ID | Sev | Status | One-line |
|---|---|---|---|
| D-1 | 🔴 | ✅ RESOLVED (PR) | serve starts SlackListener (Slack + app-token gated; Slack stays optional) |
| D-2 | 🔴 | ✅ RESOLVED (PR) | unit loads ~/orchestrator/.env for the P2 GPT-4.1 review provider key |
| D-7 | 🟡 | ✅ RESOLVED (PR) | ExecStart points at the agent-team venv interpreter |
| D-6 | 🟡 | ✅ RESOLVED | slack_bolt now exercised by the daemon (consequence of D-1) |
| D-4 | 🟠 | NOTE | pip list omits requests — runbook Step 4 installs it |
| D-5 | 🟠 | NOTE | agent-team runtime deps not pinned in requirements.txt — runbook pins langgraph |
| operator-CLI | — | NOTE | run-team.py vs operator_cli.py divergent verbs — use run-team.py |
| D-SLACKVAR | ✅ | CLEAN | SLACK_CHANNEL_ID matches everywhere |
| D-ENV-* | ✅ | CLEAN | bot/oauth/owner/app-token env names match; all read sites now exist |
| D-SUBCMD | ✅ | CLEAN | every run-team.py verb/flag the docs cite exists |
| D-HARDENING | ✅ | CLEAN | unit hardening retained unchanged (locked decision) |