# Managed Agents Runtime — Implementation Report

**Repo:** `numu angels` · **TDs:** TD-001 → TD-005 + Parts 1–11 · **Completed:** 2026-06-26
**Scope decision (locked):** Numu owns durable **storage + REST APIs + admin UI + audit**, reusing the existing Native-Action execution. **Make** owns the expected-state / transition / idempotency / locking orchestration (TD-005). PKs are **bigint** per repo convention (no UUIDs).

---

## 1. What was delivered (by phase)

### Phase 1 — Data layer
- **8 migrations / tables** (bigint PKs, indexes on `startup_id`/`run_id`/`correlation_id`/`idempotency_key`/`agent_key`): `agents`, `agent_runtime_config`, `agent_runs`, `handoffs`, `agent_action_policies`, `policy_runtime_overrides`, `execution_requests`, `agent_approval_requests`. Plus a 9th migration adding `agent_run_id` to `activity_logs` + `entity_notes` (Part 8).
- **8 Eloquent models** with `const` enums, casts, relationships; **Handoff** immutability guard; **AgentRun** `isAnalysisLocked()` + `DECISION_FIELDS`/`EXECUTION_FIELDS`.
- **`AgentPermissionsSeeder`** (Spatie, guard `admin`): `agents.manage`, `approvals.manage`, `policies.manage` + write perms `agent-runs.write`, `handoffs.write`, `execution-requests.write`.

### Phase 2 — Services + REST APIs
- **8 services** (`app/Services/Agents/`) + `AgentAuditLogger`. **31 REST routes** under `/api/v1/ai/...` (`McpAuth:full`), exactly per each TD §7, via 7 controllers + an envelope/error-mapping base (`409 version_conflict`, `409 conflict`, `422`, `404`).

### Phase 3 — Admin UI
- **Sidebar:** `Approvals` (top-level) + **Settings → AI Agents** + **Settings → Startup Actions** (en + ar nav keys).
- **6 views** + 3 permission-gated web controllers calling the **same services**: Approvals (index + show **with Approval Timeline**), Agents (index with global pause + show with Overview/Runtime/Statistics/Policies/Recent-Runs), Startup-Actions (policy controls for AI-enabled actions only), Agent-Run details.
- **Logs/Notes `[AI]` badge** with a clickable Run ID → Agent Run details (server-side on Logs, Alpine on Notes; best-effort reciprocal stamping on `recordExecutionResult`).

### Phase 4 — Tests + report
- **13 feature tests** (`ManagedAgentsInvariantsTest`) on an isolated sqlite schema (`BuildsAgentSchema` trait) — all green. This report.

---

## 2. Acceptance criteria → enforcing mechanism (all verified)

| Criterion (TD/Part) | Mechanism | Proof |
|---|---|---|
| Unknown agent+action / transition → **blocked** | `ActionPolicyService::resolve` default | test ✓ |
| Designed policy vs runtime override kept **separate**; overrides **auto-expire** | distinct tables; `scopeActive` (revoked/expiry filter) | tests ✓ (override beats designed; expired ignored) |
| Resolution order **override → designed → default** | `resolve()` | test ✓ |
| **Optimistic concurrency** (409 on stale version) | `version` check-and-increment in runtime + policy services | tests ✓ (both) |
| **Global pause** overrides individual; one agent ≠ others; unrelated automations untouched | `AgentRuntimeService::effectiveStatus` (`*` wins); pause only gates run creation | test ✓ |
| **Idempotency** — duplicate key never re-executes | unique `idempotency_key` + return-existing in run create + execution claim | tests ✓ (run + execution) |
| **Agent decision immutable** after analysis terminal | `isAnalysisLocked()` guard in `recordAnalysisResult` | test ✓ |
| Agent **never** writes execution result; failed execution preserves decision | execution fields written only by `recordExecutionResult` (separate method + `execution-requests.write` perm) | test ✓ (execution recorded, decision intact) |
| **Handoffs immutable + versioned** | model `updating` guard (only `superseded_by` mutable) + monotonic sequence | test ✓ |
| **Approval one-shot**; human decision recorded | `decide()` rejects a settled request | test ✓ |
| **Human changes override AI** | approvals never write startup state; Make re-validates expected-state on approve (documented on the approval page) | by design / UI copy |
| **No silent updates** — every change audited | `AgentAuditLogger` → `activity_logs` (module=`agents`) | test ✓ |

---

## 3. API surface (`/api/v1/ai/...`, `McpAuth:full`)

- **Runs:** `POST agent-runs` · `GET agent-runs[/{run}]` · `PATCH agent-runs/{run}/analysis-result` · `POST agent-runs/{run}/execution-results` · `POST agent-runs/{run}/cancel`
- **Handoffs:** `POST handoffs` · `GET handoffs[/{id}]` · `GET handoffs/latest` · `POST handoffs/{id}/supersede`
- **Runtime:** `GET agents/runtime-status` · `GET agents/{agentKey}/runtime-status` · `POST agents/{agentKey}/{pause|enable}` · `POST agents/{pause-all|enable-all}`
- **Policies:** `GET action-policies` · `GET|PUT action-policies/{agent}/{action}` · `POST action-policy-overrides` · `POST action-policy-overrides/{id}/revoke`
- **Execution requests:** `POST execution-requests` · `GET execution-requests/{key}` · `POST execution-requests/{key}/result`
- **Approvals:** `GET approvals[/{id}]` · `POST approvals/{id}/decide`
- **Registry:** `GET agents[/{id}]` · `POST agents`

Envelope: `{data, meta:{request_id, correlation_id, actor}}` on success, `{error:{code,message,details}}` on failure.

---

## 4. Integration boundary with Make (TD-005)

Numu provides the **durable records + APIs**; Make performs the **orchestration**:
1. Make creates an Agent Run (`POST agent-runs`), the Agent records analysis (`PATCH …/analysis-result`).
2. Make resolves policy (`GET action-policies/{agent}/{action}`) → `needs_approval` creates an approval (held until decided); `always_allow`/approved proceeds.
3. Make claims an execution slot (`POST execution-requests`, idempotent + lock), runs expected-state/transition/double-read validation **itself**, then executes the **existing Native Action** (`StartupLabelActionService` via the `startup_set_action` MCP tool / REST), and records the outcome (`POST agent-runs/{run}/execution-results`, `POST execution-requests/{key}/result`).
4. Numu audits everything and surfaces it in the Approvals + Agents consoles and the Logs/Notes `[AI]` badges.

**Not built (by the locked decision):** server-side expected-state/transition evaluation, locking acquisition logic, post-execution verification — these stay in Make. The transition table is Make-owned.

---

## 5. Files

**Migrations (9):** `2026_06_26_110001…110009`.
**Models (8 + 2 edited):** `app/Models/{Agent,AgentRuntimeConfig,AgentRun,Handoff,AgentActionPolicy,PolicyRuntimeOverride,ExecutionRequest,AgentApprovalRequest}.php`; `ActivityLog` + `EntityNote` (`agentRun` relation).
**Services (9):** `app/Services/Agents/*` + `app/Exceptions/VersionConflictException.php`.
**API (8):** `app/Http/Controllers/Api/V1/Ai/Agents/*`.
**Web (3):** `app/Http/Controllers/Admin/Agents/*`.
**Views (6 + 3 edited):** `resources/views/admin/{approvals,agents,startup-actions}/*` + sidebar, logs/index, partials/entity-notes.
**Routes:** `routes/api.php` (31), `routes/web.php` (12). **Seeder:** `AgentPermissionsSeeder`. **Lang:** en/ar nav + `agents.*`. **Tests:** `tests/Feature/Agents/*`.

---

## 6. Verification

- **Migrations:** all applied on the dev MySQL DB; 8 + 2 columns confirmed.
- **Services:** every invariant smoke-verified live (Phase 2) **and** covered by 13 green feature tests (Phase 4).
- **UI:** all 6 views compile + render with real data; 12 web routes registered + resolve; permission-gated.
- **Full suite:** **71 passed** (only the pre-existing unrelated `ExampleTest` fails: `GET / → 302`).

---

## 7. Operational follow-ups

- ✅ **Done** — `approvals:expire` console command (`app/Console/Commands/Agents/ExpireApprovalsCommand.php`) scheduled every 5 min in `routes/console.php` (`withoutOverlapping`). Verified: a past-due pending approval is marked `expired`.
- ✅ **Done** — the AI note API (`POST /api/v1/ai/notes` + `POST /api/v1/investors/{id}/notes`) now accepts an optional `agent_run_id` and stamps it on the note, so the Notes `[AI]` badge lights up for agent-authored notes (logs already auto-link on execution-result). Verified: note created with `agent_run_id` set.
- Remaining (ops-only): grant the 3 console permissions to non-super-admin roles as required.
