# Numu — AI Agents API Contract (Make ↔ Numu)

> **Final architecture (locked):** Make = orchestration + AI only. **Numu is the
> business execution engine** — after approval, **Numu executes the action itself**
> through `StartupLabelActionService` / `InvestorLabelActionService` (the single
> execution path). Make never executes business actions.

```
Make → AI Agent → Numu → Policy Engine → Approval Engine → Business Action Engine → Notifications / Automations / Audit
```

**Base:** `/api/v1/ai` · **Auth (every endpoint here):** `McpAuth:full` — `Authorization: Bearer <token>`
where the token is an OAuth 2.1 bearer with ability `numu:full` **or** a legacy Sanctum
`naat_…` full token. JSON in/out. Throttle 600/min. `actorId` (audit) = the token's user.
Responses are wrapped as `{ "data": … }`.

Postman: **`docs/postman/numu-ai-agents.postman_collection.json`** (9 folders, 7 scenarios).

### Conventions (apply to every endpoint)
- **Base URL / env:** dev `http://127.0.0.1:8080`; staging/prod `https://dashboard.numuangels.net` (same paths). **Deployment status: NOT deployed — all endpoints exist in code on branch `main` (uncommitted); none are live in production yet.**
- **Auth:** `Authorization: Bearer <token>` — OAuth 2.1 bearer with scope **`numu:full`** *or* a Sanctum token with the **`numu:full`** ability. Tier is enforced by `McpAuth:full`. (Read-only tier `numu:read` exists for other AI surfaces but every agent endpoint requires `full`.)
- **Required headers:** `Authorization`, `Content-Type: application/json` (writes), `Accept: application/json`.
- **Permissions/scopes:** `numu:full` (single scope gates the whole agent block).
- **Success envelope:** `{ "data": <payload>, "meta": { … } }`.
- **Error envelope:** `{ "error": { "code": "<machine_code>", "message": "<human>", "details": { … } } }`. Version conflicts add `current_version` / `expected_version`.
- **HTTP status codes:** `200` ok · `201` created · `400/422` validation/invalid payload · `401` unauthenticated · `403` forbidden/scope · `404` not found · `409` conflict (immutable/settled/version) · `429` throttled.
- **Shared enums:** `policy` = `always_allow|needs_approval|blocked` · `agent_type` = `startup|investor` · `environment` = `dev|test|prod` · `trigger_type` = `webhook|schedule|manual|retry` · `analysis_status` = `queued|running|completed|failed|cancelled` · `execution_status` = `not_started|executing|executed|blocked|cancelled|failed` · `approval status` = `pending|approved|rejected|cancelled|expired` · `risk_level` = `none|expected|high` · execution-request `status` = `processing|succeeded|failed|cancelled|duplicate|deferred`.
- **Subject rule:** runs / approvals / execution-requests / handoffs take **exactly one** of `startup_id | investor_id` (`required_without` + `prohibits`).

---

## 1. Agent registration

| Method | Path | Body | Notes |
|---|---|---|---|
| POST | `/agents` | `{agent_key*, agent_type?(startup\|investor), name*, description?, agent_version?, core_skill_version?, stage_skill_version?, environment?(dev\|test\|prod), is_active?}` | upsert; `registered_by=make_api`; 201 |
| GET | `/agents` | — `?environment=&is_active=` | list + effective status + stats |
| GET | `/agents/{agent}` | — | detail + recent_runs + policies |
| PATCH | `/agents/{agent}` | `{agent_type?, name?, description?, agent_version?, environment?, is_active?}` | **NEW** — update |

**Validation:** `agent_key` `^[a-z0-9_\-]+$`; `agent_type` in `startup,investor` (default `startup`);
`environment` in `dev,test,prod`. **Errors:** 422 validation, 409 conflict, 404 not-found.

```bash
curl -X POST $B/api/v1/ai/agents -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
 -d '{"agent_key":"prescreen","agent_type":"startup","name":"Pre-screen Agent","environment":"prod"}'
# → 201 { "data": { "id":2,"agent_key":"prescreen","agent_type":"startup","registered_by":"make_api", … } }
```

---

## 2. Runtime

| Method | Path | Returns |
|---|---|---|
| GET | `/agents/runtime-status` | global `*` switch + every agent's effective status |
| GET | `/agents/{agentKey}/runtime-status` | one agent's effective status (`enabled\|paused`, `source: agent\|global`) |
| POST | `/agents/{agentKey}/pause` · `/enable` | `{reason?, mode?}` |
| POST | `/agents/pause-all` · `/enable-all` | `{reason?}` (global, precedence over per-agent) |

Execution availability = effective status `enabled` AND the action policy resolves to a
non-blocked value. Current policy state per action: `GET /action-policies/{agentKey}`.

---

## 3. Agent runs

| Method | Path | Body |
|---|---|---|
| POST | `/agent-runs` | `{agent_key*, `**`startup_id`**`\|`**`investor_id`**` (exactly one)*, context?, trigger_type?(webhook), correlation_id?(auto), idempotency_key?(auto), agent_version?, expected_group?, expected_status?, …}` |
| GET | `/agent-runs` | `?startup_id=&agent_key=&limit=&cursor=` |
| GET | `/agent-runs/{run}` | + approvalRequests + executionRequests |

```bash
curl -X POST $B/api/v1/ai/agent-runs -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \
 -d '{"agent_key":"prescreen","startup_id":457,"context":{"source":"make","workflow":"startup_pipeline"}}'
# → 201 { "data": { "id": 31, "analysis_status":"queued","execution_status":"not_started", … } }
```
`idempotency_key` dedupes — same key returns the original run (200, not a new row). Omitting
it auto-generates a UUID (fine for testing; send your own in production).

---

## 4. Agent decision  (the orchestrator)

```
POST /agent-runs/{run}/decision
{ "action_slug":"request_meeting", "confidence":0.87, "reason":"passed prescreen",
  "payload":{"note":"AI recommendation"}, "environment":"prod", "risk_level":"expected" }
```
Records the proposal on the run (analysis=completed, immutable) → resolves the policy →
branches → audits → returns `{policy, policy_source, decision, executed, approval_id?, execution_request_id?, result?, run}`.

| Resolved policy | DB writes | Approval | Execution request | Execution result | Audit | Notifications / Automations |
|---|---|---|---|---|---|---|
| **always_allow** | run analysis+execution | — | 1 (succeeded) | run.execution=executed | run_created/analysis, decision, execution_claimed/result, run_execution | **fire** (via `apply()` → StartupLabelOptionChanged) |
| **needs_approval** | run analysis | 1 PENDING (+confidence, payload, policy_snapshot) | — (created on approve) | — (on approve) | …, decision, approval_created | on approve only |
| **blocked** | run analysis+execution(blocked) | — | — | run.execution=blocked | …, decision, run_execution(blocked) | none |
| **unassigned** | = blocked (`source=default`) | — | — | run.execution=blocked | …, decision | none |

---

## 5. Action policies

| Method | Path | Notes |
|---|---|---|
| GET | `/action-policies` | `?agent_key=&action_key=&environment=` — all active |
| GET | `/action-policies/{agentKey}` | **NEW** — one agent's assigned actions + policies |
| GET | `/action-policies/{agentKey}/{actionKey}` | **resolve** `?environment=&startup_id=&test_fixture=` → `{resolved:{policy,source,version?}}` |
| PUT | `/action-policies/{agentKey}/{actionKey}` | `{policy, environment, expected_version?, reason?}` (409 on version mismatch) |
| POST | `/action-policy-overrides` · `/{override}/revoke` | scoped temporary overrides |

```bash
curl $B/api/v1/ai/action-policies/prescreen/request_more_info?environment=prod -H "Authorization: Bearer $T"
# → { "data": { "resolved": { "policy":"needs_approval","source":"designed","version":2 } } }
```

---

## 6. Approvals  (Numu executes on approve)

| Method | Path | Body |
|---|---|---|
| GET | `/approvals` | `?status=&startup_id=&pending_only=1&limit=` |
| GET | `/approvals/{approval}` | detail (+run, startup, agent) |
| POST | `/approvals/{approval}/approve` | `{comment?}` → **Numu executes the native action** + records |
| POST | `/approvals/{approval}/reject` | `{comment?}` → close; nothing executes; run → blocked |
| POST | `/approvals/{approval}/decide` | `{decision:approve\|reject\|cancel, comment?}` (approve executes) |

**States:** `pending → approved | rejected | cancelled | expired`. Decision is **one-shot**
(re-deciding a settled approval → 409). **On approve:** claim `approval-{id}:{slug}` →
`AgentActionExecutor` → `apply()` → startup changes + notifications/automations → record
results. **Idempotency:** duplicate approve returns the stored result; the action never repeats.
**Rollback:** an executed agent action is a real business transition and is **not auto-reversible** —
reverse it with the corresponding inverse action (same as a dashboard action); approvals only
gate, they never silently mutate state.

---

## 7. Execution

| Method | Path | Notes |
|---|---|---|
| POST | `/execution-requests` | claim (durable idempotency/lock; one of `startup_id`\|`investor_id`) — used internally by the orchestrator |
| GET | `/execution-requests` | **NEW** — list `?startup_id=&status=&agent_run_id=&action_key=` |
| GET | `/execution-requests/{key}` | by idempotency_key |
| GET | `/execution-results` | **NEW** — same list (results view) |
| GET | `/execution-results/{id}` | **NEW** — by numeric id |
| POST | `/agent-runs/{run}/execution-results` | record outcome (legacy/external; Numu records internally now) |

**Status mapping** (the user's lifecycle → stored): `pending`→ execution_request `processing` /
run `executing`; `approved` → approval `approved`; `blocked` → run `blocked`; `executed` →
request `succeeded` + run `executed`; `failed` → request `failed` + run `failed`.

---

## 8. Testing endpoints (Postman, no Make)

Gated by `config('agents.testing_enabled')` (on in non-prod; `AI_AGENT_TESTING=true` to force).
Each runs the **full lifecycle in one call** and (default) **reverts** the startup afterward.

| Method | Path | Body |
|---|---|---|
| POST | `/testing/startup-scenario` | `{startup_id*, agent_key*, action_slug*, policy*, confidence?, reason?, payload?, auto_approve?, revert?(true)}` |
| POST | `/testing/approval-scenario` | same (forces `needs_approval`; `auto_approve` to also execute) |
| POST | `/testing/blocked-scenario` | same (forces `blocked`) |
| POST | `/testing/investor-scenario` | `{agent_key*, action_slug*, policy*}` — policy resolution only (execution is a gap) |

Returns `{scenario, run_id, decision, approval, startup_action_before, startup_action_after, reverted}`.

---

## 9. Postman scenarios (in the collection)

1 always_allow · 2 needs_approval · 3 blocked · 4 unassigned · 5 duplicate decision (idempotency) ·
6 approve (Numu executes) · 7 reject. Folders 1–7 = raw endpoints; 8 = one-call testing; 9 = raw scenarios.

---

## 10. Sequence diagrams (final architecture)

### always_allow
```
Make → AI Agent → POST /agent-runs ........................ Numu: agent_runs(queued)            ▶audit
Make → POST /agent-runs/{r}/decision
   Numu │ recordAnalysisResult ............................ proposed_action frozen             ▶audit
        │ Policy Engine.resolve ............................ "always_allow"                     ▶audit decision
        │ Approval Engine .................................. (skipped)
        │ Business Action Engine: ExecutionRequest.claim ... execution_requests(processing)     ▶audit
        │   → StartupLabelActionService.apply() ............ action_option_id changed
        │        → StartupLabelOptionChanged → Notifications / Automations / activity_logs
        │ recordExecutionResult(executed) + result(succeeded) .................................. ▶audit
   ← { policy:always_allow, executed:true }
```

### needs_approval
```
Make → POST /agent-runs/{r}/decision
   Numu │ recordAnalysisResult ............................ proposed_action                    ▶audit
        │ Policy Engine.resolve ............................ "needs_approval"                   ▶audit decision
        │ Approval Engine.createFromRun .................... agent_approval_requests(PENDING,
        │                                                     confidence, payload, snapshot)    ▶audit approval_created
   ← { policy:needs_approval, approval_id }      ── Business Action NOT run; startup unchanged ──
Admin/Make → POST /approvals/{id}/approve
   Numu │ Approval Engine.decide ........................... APPROVED (one-shot)               ▶audit approval_approve
        │ Business Action Engine: claim → apply() .......... startup changes + Notifications/Automations
        │ record execution + result ....................................................... ▶audit run_execution
   ← { approval:APPROVED, executed:true }        [reject → REJECTED, run blocked, nothing executes]
```

### blocked
```
Make → POST /agent-runs/{r}/decision
   Numu │ recordAnalysisResult ............................ proposed_action                    ▶audit
        │ Policy Engine.resolve ............................ "blocked" (designed OR default)    ▶audit decision
        │ recordExecutionResult(blocked) ....................................................  ▶audit run_execution(blocked)
   ← { policy:blocked, executed:false }          ── Business Action NOT run · no approval ──
```

---

## 11. Production-readiness checklist

**Cron (`routes/console.php` / scheduler):**
- `approvals:expire` — every 5–15 min (expires PENDING approvals past `expires_at`).
- (optional) a sweep for stale `execution_requests` whose `locked_until` passed.

**Queue workers:** notifications/automations dispatch on `database` queue → run
`php artisan queue:work --queue=notifications,default --tries=3 --max-time=3600` (supervisor).
The decision/approval responses are synchronous; only the downstream sends are queued.

**Env:**
- `AI_AGENT_TESTING=false` in production (disables the testing endpoints).
- `AI_AGENT_ENV=prod` (default environment for policy resolution).
- `QUEUE_CONNECTION=database` (or redis) with a running worker.
- Mail/SMS/WhatsApp creds for the existing notification channels (unchanged).

**Webhooks / Make config:**
- Make calls with a `Bearer naat_…` full token (or OAuth `numu:full`).
- Per run: send a stable `idempotency_key` + `correlation_id` for dedupe + tracing.
- Flow: `POST /agent-runs` → `POST /agent-runs/{run}/decision`; for needs_approval, poll
  `GET /approvals?pending_only=1` or rely on the admin to approve (Numu executes on approve).

**Indexes (already present via migrations):** `agent_runs.idempotency_key` (unique),
`agent_action_policies (agent_key, action_key, environment)`, `execution_requests.idempotency_key`
(unique), `agent_approval_requests.status`, `agents.agent_type`. Add a composite
`agent_approval_requests (status, created_at)` if the Approvals list grows large.

**Monitoring:** alert on `agent_runs.execution_status=failed`, `execution_requests.status=failed`,
PENDING approvals older than N hours, and audit-log gaps. Track decision→execution latency.

**Retry strategy:** decisions/approvals are idempotent (re-POST is safe — duplicate returns
the stored result). Native action `apply()` is a no-op if the action is unchanged. Notification
sends retry via the queue (`--tries=3`). No bespoke retry needed at the agent layer.

---

## 12. Remaining gaps

**✅ Complete**
- Agent registry (POST/GET/PATCH, typed startup/investor), runtime enable/pause.
- Agent runs (relaxed Make contract + `context`), decision orchestrator.
- Policy engine (designed + overrides + resolution), policy listing per agent.
- Approval engine (auto-create on needs_approval; approve **executes**; reject closes; one-shot; idempotent).
- Business action engine via the canonical `apply()` (notifications/automations/audit reused, zero duplication).
- Execution-request store + list/results read endpoints.
- Testing endpoints (4) + Postman collection (7 scenarios) + audit on every step.
- Verified: 21/21 lifecycle + 14/14 testing-endpoint assertions; 14/14 unit tests.

**🟡 Partially implemented**
- `GET /agents/{agentKey}/runtime-status` returns effective status; a single "execution availability"
  composite (status × policy) is derivable but not a dedicated field.
- `execution-results` are a read-view over `execution_requests` (no separate table) — fine for Make,
  but there's no independent results resource.

**✅ Investor parity — DONE**
- `agent_runs` / `agent_approval_requests` / `execution_requests` carry `investor_id` (startup_id
  now nullable; exactly one subject per row). Investor **runs, decisions, approvals, and execution**
  all work via `InvestorLabelActionService`. `POST /agent-runs` + `/execution-requests` accept
  `investor_id`; the testing endpoint runs the full investor lifecycle. See
  **`docs/AI_AGENTS_INVESTOR_PARITY.md`**. Only remaining: seed canonical investor agents once
  the investor pipeline is standardized (create-as-needed works today).

**🔴 Make-integration gaps**
- Make blueprints not built/connected (0 live traffic). The full contract above is ready to consume.
- Decide whether Make polls `/approvals` or relies on admin approval (Numu executes either way).

**🔴 Production-deployment gaps**
- Schedule `approvals:expire`; run a queue worker; set `AI_AGENT_TESTING=false`.
- Add the optional composite approval index if volume grows.
- Wire `agent_type` into Make's agent-registration payload for investor agents.
