# 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` + new `close`). - **Was:** `Coordinator.serve()` did only `bind_subscription_invoker()`, `setup()`, `recover()`, then an infinite tick/sleep loop — it never constructed or started `SlackListener`, so a deployed daemon posted clarifier questions and expired them on deadline but could **not hear Slack answers**. - **Now:** `serve()` starts the `SlackListener` on a background **daemon thread**, concurrently with the tick/drain loop, **when** the live transport is a `SlackTransport` AND `SLACK_APP_TOKEN` is set. It shares the coordinator's own transport, ledger `db_path`, and `resume_queue` put; on shutdown it calls the listener's new `close()` and joins the thread in a `finally`. When Slack is not the transport or the app token is absent, no listener starts and `serve` behaves 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 empty `AGENT_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 wires `default_review_wiring`); `coordinator.py:default_review_wiring` → `review_loop_llm.default_plan_reviewer` (shells the local orchestrator `run.py` → `cross_reviewer` GPT-4.1). - **Was:** the unit loaded only `~/secrev.env`. `run-team.py serve` builds the coordinator with the P2 review loop wired, and once a task reaches REVIEW the reviewer shells the orchestrator `run.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 the `sea-haven-secrev` unit). 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-github` use fails with a clear RuntimeError without it. - **Fix:** PROVISIONING-RUNBOOK Step 4 installs `requests` into the venv. `requests` is also absent from `requirements.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.txt` pins `langgraph==1.1.10` and `langgraph-checkpoint-sqlite==3.1.0`, but the agent-team runtime deps `claude-agent-sdk`, `slack_sdk`, `slack_bolt`, `requests` (and `anthropic` for api mode) are **not in `requirements.txt` at 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 root `requirements.txt` langgraph pin. Until then, PROVISIONING-RUNBOOK Step 4 pins `langgraph==1.1.10` / `langgraph-checkpoint-sqlite==3.1.0` explicitly so the unpinned `pip install` cannot pull a newer, untested major. **Do NOT modify `requirements.txt` or 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 only `slack_bolt` import). - **Now:** with D-1 fixed, `Coordinator.serve()` starts `SlackListener.serve()`, which imports + uses `slack_bolt` for 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_transport` reads exactly `os.environ.get("SLACK_CHANNEL_ID")`. The runbook, the unit comment, and the code all use `SLACK_CHANNEL_ID` (not `SLACK_CHANNEL`). **CLEAN.** --- ## ✅ D-ENV-SLACKBOT / OAUTH / OWNERS / APPTOKEN (clean) — names match - **`SLACK_BOT_TOKEN`** ↔ `slack_live.py` + `slack_listener` env 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_enabled` gates the listener on its presence, and `default_slack_listener_factory` / `SlackListener.serve` source 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.service` ExecStart. - **Was:** `ExecStart=/usr/bin/env python3 run-team.py serve` resolved 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 ` 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`. Has `show` and `--parked`; `--db` / `--audit-log` default sensibly. > - `agent_team/operator_cli.py`: `list, redeliver, force-expire, > answer-on-behalf, force-resume` — **no `show`**, `--db` / `--audit-log` are > **required**, and its `force-resume` **supersedes** (unlike `run-team.py`'s, > which reopens an expired row and never supersedes). > > **Use `run-team.py` for provisioning + the demo + incident recovery.** The > docs reference only `run-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 `ReadWritePaths` carve-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) |