# Managed Agents Runtime — Implementation Plan (TD-001 → TD-005 + Parts 1–11)

**Repo:** `numu angels` (confirmed) · **Date:** 2026-06-26 · **Status:** awaiting sign-off before Phase 1

## 0. Locked decisions (from you)

1. **Target repo:** `numu angels` (this app) — it already has the MCP, `StartupLabelActionService` (Native Action), startup status/group/action, activity logs, and notifications the TDs drive.
2. **Validation ownership:** **Numu = durable storage + REST APIs + admin UI + audit, reusing the existing Native-Action execution. Make owns** expected-state / transition / idempotency / locking orchestration (TD-005 §2/§11). So `execution_requests` is the **durable idempotency/lock store Make writes to** — *not* a server-side validation engine. We expose claim/record/read APIs over it; we do **not** re-implement the transition-evaluation logic server-side.

## 0b. Locked adjustments (sign-off 2026-06-26)

1. **PKs: bigint auto-increment for ALL 8 new tables** — NO UUID, NO `HasUuids` (preserve repo convention). Global references use `correlation_id`, `idempotency_key`, and `external_reference_id` (string) where needed.
2. **Validation ownership:** Make owns expected-state/transition/idempotency/locking/post-exec verification (TD-005). Numu = storage + APIs + UI + audit + execution records only.
3. **Keep** `agents` + `agent_approval_requests`.
4. **Agents UI:** NOT top-level — placed under **Settings → AI Agents** (Agent List + Agent Details: Overview / Runtime Controls / Policies / Statistics / Recent Runs).
5. **Approvals UI:** top-level sidebar item; details page adds an **Approval Timeline** (analyzed → created → viewed → approved/rejected → execution started → completed) + Run ID, Correlation ID, Startup, Agent, Policy Version, Execution Status.
6. **Startup Actions:** under **Settings → Startup Actions**; Agent-Policy controls shown **only** for AI-agent-enabled actions (with Allowed Agents + Notification Risk + Transition); non-agent actions show no policy controls.
7. **Logs/Notes badges:** the `[AI]` badge block with a **clickable Run ID** linking to the Agent Run details page.
8. **Invariants:** all preserved (default-blocked, immutable handoffs + agent decisions, append-only execution results, override expiry, optimistic concurrency, idempotency, human-overrides-AI, global-pause precedence).

## 1. Convention reconciliations (decisions baked into this plan)

| Conflict | Repo today | TD wants | Decision |
|---|---|---|---|
| Primary keys | bigint auto-increment everywhere; no `HasUuids` | UUID (run_id, handoff_id, policy id, override id) | **bigint auto-increment PKs for ALL 8 new tables** (locked 2026-06-26 — preserve repo convention; no `HasUuids`). Cross-system references use the string columns `correlation_id` / `idempotency_key` / `external_reference_id`. FKs to existing tables are bigint (`startup_id`→startups, `*_by`→admins). |
| Optimistic concurrency | none | `version` + `expected_version` on config/policy updates | Add an integer **`version`** column to `agent_runtime_config` + `agent_action_policies`. The service does **check-and-increment**; a mismatch returns **HTTP 409 `version_conflict`**. New, localized to these tables. |
| Immutability / append-only | `ai_activity_logs` is append-only (no `updated_at`) | run decision fields immutable after completion; handoffs immutable; no hard delete of policies | Enforce in the **service layer + model guards**: `agent_runs` decision fields locked once `analysis_status` is terminal; `execution_result` append-only; `handoffs` immutable (corrections create a new row + `superseded_by`); policies/overrides use `effective_to` / `expires_at` / `revoke` — **no hard delete**. |
| Tables beyond the "6 required" | — | — | The UI parts need two more: **`agents`** (registry — Part 7 list/columns) and **`agent_approval_requests`** (Part 6 console). Total **8 tables**. |

## 2. Scope boundary (what we build vs what Make owns)

**Numu builds:** 8 tables + models + repositories + services + REST APIs + admin UI (Approvals, Agents, Logs/Notes badges, Action-policy controls) + Spatie permissions + audit + tests.
**Numu reuses (no new logic):** `StartupLabelActionService` (Native Action execution), `ActivityLogger`/`AiActivityLogger` (audit), `AssignRequestCorrelationId` middleware (correlation_id), Spatie permissions, the existing sidebar/CSS/Blade component system.
**Make owns (out of scope, we only persist/expose):** expected-state compare, transition-table evaluation, startup locking acquisition logic, double-read, post-execution verification orchestration. We provide the durable records + APIs Make reads/writes.

## 3. Data model (8 tables, aligned to TD schemas + repo conventions)

> All status-like columns are `string` with model `const`s (repo convention). `timestamps()` unless append-only. Indexes on `startup_id`, `correlation_id`, `idempotency_key`, `agent_key`, `run_id` per Part 11.

1. **`agents`** (registry — bigint id) — `agent_key` (unique), `name`, `description`, `agent_version`, `core_skill_version`, `stage_skill_version`, `environment` (dev|test|prod), `is_active`, timestamps, softDeletes.
2. **`agent_runtime_config`** (TD-003 — bigint id) — `agent_key` (unique; `*` = global), `status` (enabled|paused), `pause_mode` (pause_new_only|cancel_queued|cancel_running_if_safe), `reason?`, `changed_by`→admins, `changed_at`, `effective_at`, **`version`**, `expires_at?`, timestamps. Global `*` row overrides individuals (enforced in service).
3. **`agent_runs`** (TD-001 — bigint id) — `startup_id`→startups, `agent_key`, `agent_version`, `core_skill_version`, `stage_skill_version`, `trigger_type`, `trigger_event_id?`, **`correlation_id`** (idx), **`idempotency_key`** (unique), `expected_group?`, `expected_status?`, `expected_record_version?`, `analysis_status` (queued|running|completed|failed|cancelled), `decision_payload` json, `proposed_action?`, `proposed_status_update?`, `note_payload?` json, `handoff_id?`→handoffs, `execution_status` (not_started|executing|executed|blocked|cancelled|failed), `execution_result?` json (**Execution-Layer-only**), `activity_log_id?`, `notification_result?` json, `error_code?`, `error_message?`, `parent_run_id?` (retry link), `started_at`, `completed_at?`, timestamps.
4. **`handoffs`** (TD-002 — bigint handoff_id) — `startup_id`, `source_run_id`→agent_runs, `from_agent`, `to_agent?`, `stage`, `schema_version`, `sequence_number` (monotonic per startup), `current_group?`, `current_status?`, `proposed_action?`, `resulting_group?`, `resulting_status?`, `facts` json, `findings` json, `risks` json, `open_questions` json, `documents_used` json, `decision_summary` json, `created_at`, `superseded_by?`→handoffs, `retention_class`. **Immutable** (no `updated_at`).
5. **`agent_action_policies`** (TD-004 — bigint id) — `agent_key`, `action_key`, `policy` (always_allow|needs_approval|blocked), `environment`, `effective_from`, `effective_to?`, **`version`**, `reason`, `created_by`/`updated_by`→admins, timestamps. Unique active `(agent_key, action_key, environment)` where `effective_to is null`.
6. **`policy_runtime_overrides`** (TD-004 — bigint id) — `scope_type` (startup|environment|test_fixture), `scope_id`, `agent_key?`, `action_key?`, `override_policy`, **`expires_at`** (mandatory), `revoked_at?`, `reason` (mandatory), `created_by`→admins, `created_at`. No hard delete (revoke).
7. **`execution_requests`** (TD-005 — bigint id) — **`idempotency_key`** (unique), `startup_id`, `agent_run_id?`→agent_runs, `source_event_id`, `correlation_id`, `action_key`, `transition_rule_version?`, `expected_group?`, `expected_status?`, `expected_record_version?`, `expected_updated_at?`, `status` (processing|succeeded|failed|cancelled|duplicate|deferred), `attempt_count`, `execution_result?` json, `locked_until?`, timestamps. Durable store Make uses for idempotency + lock.
8. **`agent_approval_requests`** (Part 6 — bigint id) — `agent_run_id`→agent_runs, `startup_id`, `agent_key`, `action_key`, `proposed_action`, `proposed_group?`, `proposed_status?`, `reason`, `risk_level` (none|expected|high), `current_group?`, `current_status?`, `correlation_id`, `policy_version?`, `status` (pending|approved|rejected|cancelled|expired), `expires_at`, `decided_by?`→admins, `decided_at?`, `decision_comment?`, timestamps.

## 4. Models / repositories / services

- **Models** (`app/Models/`): one per table, repo conventions (`$fillable`, `casts()`, `const` enums, `HasUuids` on the 5 UUID tables, `SoftDeletes` on `agents`, relationships with return types). Immutability guards in `saving`/`updating` hooks where required.
- **Repositories** (`app/Repositories/Eloquent/`): query objects per aggregate (cursor lists, filters) — mirrors `EloquentStartupRepository`.
- **Services** (`app/Services/Agents/`):
  - `AgentRegistryService` — register/list agents, stats rollups (run counts, failure rate).
  - `AgentRuntimeService` — enable/pause/global, version-checked, audited; the "global `*` overrides individual" rule + `pause_mode` semantics.
  - `AgentRunService` — create run, append analysis result (locks decision), append execution result (Execution-Layer-only), cancel; idempotency-key guard.
  - `HandoffService` — create (immutable), supersede, retrieve-latest (stage + sequence + schema compatible).
  - `ActionPolicyService` — resolve policy (override → designed → default **blocked**), update with version check, list; **"always_allow never bypasses safety"** is a documented contract (safety lives in Make; our resolver returns the policy only).
  - `PolicyOverrideService` — create (mandatory expiry + reason), revoke, auto-expire scope.
  - `ApprovalService` — create from `needs_approval`, decide (approve/reject/cancel), expire; on approve, return the run context Make re-validates (we do **not** re-run validation server-side — Make does, per the boundary).
  - `ExecutionRequestService` — claim (insert idempotency row + lock), record result, read.
  - All writes audited via `ActivityLogger`/`AiActivityLogger` (old/new/actor/reason/correlation/version).

## 5. REST APIs (under `/api/v1/ai`, behind `McpAuth`/Sanctum + the new permissions)

Exactly the TD §7 surfaces + the Part 6/7 needs:
- **Agent runs:** `POST /agent-runs`, `PATCH /agent-runs/{id}/analysis-result`, `POST /agent-runs/{id}/execution-results`, `GET /agent-runs/{id}`, `GET /agent-runs?startup_id=`, `POST /agent-runs/{id}/cancel`.
- **Handoffs:** `POST /handoffs`, `GET /handoffs/{id}`, `GET /handoffs/latest?startup_id=&from_agent=&schema_version=`, `GET /handoffs?startup_id=`, `POST /handoffs/{id}/supersede`.
- **Runtime:** `GET /agents/runtime-status`, `GET /agents/{key}/runtime-status`, `POST /agents/{key}/pause`, `POST /agents/{key}/enable`, `POST /agents/pause-all`, `POST /agents/enable-all`.
- **Policies:** `GET /action-policies`, `GET /action-policies/{agent}/{action}`, `PUT /action-policies/{agent}/{action}` (with `expected_version` → 409 on conflict), `POST /action-policy-overrides`, `POST /action-policy-overrides/{id}/revoke`.
- **Execution requests:** `POST /execution-requests` (claim), `POST /execution-requests/{key}/result`, `GET /execution-requests/{key}`.
- **Approvals:** `GET /approvals`, `GET /approvals/{id}`, `POST /approvals/{id}/decide`.

Reuse the `AiResponseFactory` `{data, meta}` envelope, `AiSessionResolver` actor, and `AssignRequestCorrelationId` for `correlation_id`.

## 6. Admin UI (custom CSS + x-admin.* + Alpine — matches existing)

- **Sidebar:** add **Approvals** (direct) + **Agents** (group) to `resources/views/components/admin/sidebar.blade.php` (new `'agents'` group), gated by the new permissions.
- **Approvals console** (`admin/approvals/index` + `show`) — `.data-table` list (Part 6 columns) + detail page (decision payload, generated note, handoff summary, execution preview, transition result, expected vs current state, policy version) + Approve/Reject/Cancel.
- **Agents console** (`admin/agents/index` + `show`) — list (Part 7 columns + filters) + detail tabs (Overview, Runtime Controls enable/pause, Policies matrix, Statistics, Recent Runs).
- **Logs + Notes badges** (Part 8) — extend `admin/logs/index.blade.php` + `admin/partials/entity-notes.blade.php` with the `[AI] Agent / Run# / Policy / Approval / Execution` badge block (custom `.badge` pills).
- **Action policy controls** (Part 9) — extend `admin/labels/option-edit.blade.php`: for **action** label options only, show `Policy: Always Allow / Needs Approval / Blocked` radios + allowed agents + transition target + notification risk + policy version. Persisted via `agent_action_policies` (not on the label option).

## 7. Permissions, audit, concurrency (Parts 10–11)

- **Permissions** (Spatie, guard `admin`): `agents.manage`, `approvals.manage`, `policies.manage`, `agent-runs.write` (Make/MCP), `handoffs.write`. New `AgentPermissionsSeeder` granting to `super_admin`. Routes gated with `permission:NAME,admin`.
- **Audit:** every config/policy/runtime/approval change → `ActivityLogger::recordFieldChange` (old/new/actor/reason) + `version` + `correlation_id`; AI/Make writes → `AiActivityLogger`. No silent updates.
- **Concurrency:** `version` check-and-increment on `agent_runtime_config` + `agent_action_policies`; unique `idempotency_key` on `agent_runs` + `execution_requests`; sequence uniqueness on handoffs.
- **Indexes:** `startup_id`, `run_id`/`agent_run_id`, `correlation_id`, `idempotency_key`, `agent_key`.

## 8. Phasing (each phase verified + reported before the next)

- **Phase 1 — Data layer:** 8 migrations + 8 models (+ enums, casts, relationships, immutability/UUID/version traits) + the `AgentPermissionsSeeder`. Verify: migrate, model resolution, a tinker round-trip per table.
- **Phase 2 — Services + repositories + REST APIs:** all services, repos, controllers, routes, the version/idempotency/immutability guards. Verify: feature smoke per endpoint (create run → analysis → execution append; handoff create → supersede → latest; policy resolve order; override expiry; runtime global-override; approval decide).
- **Phase 3 — Admin UI:** sidebar, Approvals + Agents consoles, Logs/Notes badges, Action-policy controls. Verify: pages render, permission-gated, actions wired.
- **Phase 4 — Tests + report:** feature/unit tests for invariants (immutability, default-blocked, override-expiry, version-conflict 409, global-pause precedence, idempotency dedupe, human-override precedence note) + the implementation report. Full `php artisan test` green.

## 9. Invariants enforced (acceptance criteria → mechanism)

- Unknown agent+action policy / transition → **blocked** (resolver default).
- Designed policy vs runtime override stored **separately**; overrides **auto-expire** (`expires_at` + scope filter).
- **Always Allow never bypasses safety** — documented; safety is Make's; our resolver returns policy only.
- Agent **never** writes `execution_result` (separate endpoint + permission + service guard; `agent-runs.write` cannot hit the execution endpoint).
- Failed execution **preserves** the original decision (append-only `execution_result`).
- Duplicate `idempotency_key` prevented (unique constraint + service `duplicate_no_reexecution`).
- **Human changes override AI** — overrides/approvals never overwrite human state; documented + enforced by Make (we record mismatch outcomes).
- Global pause overrides individual; pausing one agent never pauses others; unrelated Numu automations untouched (we only gate agent-run creation).

## 10. Open items to confirm (non-blocking — defaults chosen)

1. **`agents` + `agent_approval_requests`** (the 2 tables beyond the "6 required") — included because Parts 6–7 need them. *(Default: include.)*
2. **UUID PKs** on the 5 externally-referenced tables (vs repo's bigint default). *(Default: UUID per TD.)*
3. **Transition-validation table** stays **Make-side** (per the validation-ownership decision) — not built in Numu. *(Default: Make-owned.)*
4. APIs live under the existing `/api/v1/ai` + `McpAuth` surface (so Make authenticates with a Sanctum connector token + the new write permissions). *(Default: yes.)*

---

**Next step:** on your go-ahead I start **Phase 1** (migrations + models + seeder), verify, and report before moving to services/APIs.
