# PRD: App-Agent Integration (Webhook / Hooks Gateway)

**Status:** Draft
**Created:** 2026-03-15
**Phase:** 5 (post Phase 4.5)

---

## Problem

Tenant-deployed apps have no way to call their agent programmatically. The only trigger path today is Discord → SQLite messages table → agent run. An app running in the tenant's container environment cannot invoke its agent, cannot ask it to perform a task, and cannot receive a structured result.

This limits apps to being passive UIs. Any AI capability has to be hacked through Discord messages or scheduled polling — both unsuitable for real-time, request-response interactions.

---

## Goal

Give every tenant app a clean HTTP API to trigger agent runs and call tools, with results returned synchronously or delivered back to a channel. No Discord required. Modelled directly on the equivalent feature in OpenClaw (`~/openclaw`).

---

## User Story

> As a tenant, I want my deployed web app to call my agent with a prompt and get a result back, so I can add AI functionality to my app without building or hosting any AI infrastructure myself.

---

## What We're Building

Three endpoints, added to the NanoClaw platform and exposed per-tenant via Caddy at `{tenant}.devitltd.uk`:

### 1. `POST /hooks/agent` — async agent trigger

Fire-and-forget agent run. The app sends a prompt; the agent runs in isolation; result is optionally delivered to a Discord channel or posted back as a summary.

**Request:**
```json
{
  "message": "Summarise the uploaded CSV and reply in #general",
  "deliver": true,
  "channel": "discord"
}
```

**Response:** `200 OK` immediately (run is async).

**Use cases:** background processing, long-running tasks, report generation, anything where the app doesn't need to wait.

---

### 2. `POST /hooks/wake` — heartbeat / event injection

Enqueue a system event into the agent's main session. Used to notify the agent of something that happened without starting a full agent run.

**Request:**
```json
{
  "text": "User submitted a new support ticket: #4821",
  "mode": "now"
}
```

**Response:** `200 OK`.

**Use cases:** event-driven notifications, "something happened, agent should know", deferred processing triggers.

---

### 3. `POST /tools/invoke` — synchronous tool call

Call a single agent tool directly and get the result back inline. No full agent turn. Filtered by the same policy chain as agent runs.

**Request:**
```json
{
  "tool": "read_file",
  "args": { "path": "/workspace/group/data/report.csv" }
}
```

**Response:**
```json
{ "ok": true, "result": "..." }
```

**Use cases:** reading workspace data, running a quick computation, anything where the app needs a structured result synchronously.

---

## Authentication

Each tenant gets a **hooks token** — a static bearer token stored in the `tenants` table (new column: `hooks_token`). Apps authenticate with:

```
Authorization: Bearer <hooks_token>
```

MC provisioning generates this token automatically. It is shown in the tenant edit modal and can be rotated. Rate limiting applies per client IP to prevent brute-force token enumeration.

---

## Routing

All three endpoints are mounted per-tenant behind Caddy:

```
POST https://{tenant}.devitltd.uk/hooks/agent
POST https://{tenant}.devitltd.uk/hooks/wake
POST https://{tenant}.devitltd.uk/tools/invoke
```

Caddy routes these to NanoClaw (same port as the tenant dashboard, `19100`). NanoClaw resolves the tenant from the `Host` header and enforces tenant isolation — a request for `tenanta.devitltd.uk` cannot trigger `tenantb`'s agent.

---

## Security Constraints

- Token is tenant-scoped. Leaking it exposes only that tenant's agent, not the platform.
- `tools/invoke` has a **default deny list**: `sessions_spawn`, `sessions_send`, `gateway`, anything that could escalate or call other agents. Tenants cannot override this.
- Hook payloads are wrapped with safety boundaries before injection (same as direct messages).
- `hooks/agent` runs in an isolated session key (`hook:ingress`) separate from the main Discord conversation, so hook runs don't pollute the tenant's chat history.
- Rate limiting: per-IP, same threshold as existing auth rate limiting.
- Cross-tenant calls are blocked at the Host header resolution layer.

---

## Tenant Configuration

New `tenants` table column:

| Column | Type | Default | Description |
|---|---|---|---|
| `hooks_token` | TEXT | NULL | Bearer token for hooks endpoints; NULL = hooks disabled |
| `hooks_enabled` | INTEGER | 0 | Master on/off switch |

MC tenant edit modal gains:
- "App Integration" section with `hooks_enabled` toggle
- Hooks token field (read-only + rotate button)
- Copy-to-clipboard for the token
- Links to the three endpoint URLs for this tenant

---

## Implementation Plan

### Phase A — NanoClaw: hooks server
1. Add `hooks_token` + `hooks_enabled` columns to `tenants` table and DB CRUD.
2. Add HTTP server in `src/hooks-server.ts` (or extend existing server if one exists) with the three routes.
3. Add bearer token middleware (constant-time comparison, rate limiting per IP).
4. Implement `POST /hooks/agent`: write a message to the tenant's main group IPC `messages/` dir with `kind: user_message`, let the existing pipeline pick it up.
5. Implement `POST /hooks/wake`: write a system event to the IPC dir (new event type or reuse progress kind).
6. Implement `POST /tools/invoke`: resolve the tool from the tenant's available tools, run it within a tenant-scoped sandbox, return result inline.
7. Resolve tenant from `Host` header; reject unknown/disabled tenants.

### Phase B — Caddy: route hooks endpoints
1. In `ipc.ts` (or MC provisioning), include hooks route blocks in each tenant's Caddyfile:
   ```
   handle /hooks/* {
     reverse_proxy localhost:19100
   }
   handle /tools/* {
     reverse_proxy localhost:19100
   }
   ```
2. NanoClaw receives the request with `Host: {tenant}.devitltd.uk` and routes by tenant.

### Phase C — MC UI
1. Add `hooks_token` generation to `POST /api/tenants` provisioning flow.
2. Add "App Integration" section to tenant edit modal:
   - Enable/disable toggle
   - Token display + rotate
   - Endpoint URLs
3. `GET /api/tenants/:id` response includes hooks config.

### Phase D — Documentation / Skill
1. Write a base skill (`container/skills/app-integration/SKILL.md`) so agents know to reference the hooks endpoints when helping tenants build apps.
2. Add a section to AGENTS.md.

---

## OpenClaw Reference

The full upstream implementation to adapt from:

| Feature | OpenClaw source |
|---|---|
| `/hooks/agent` + `/hooks/wake` | `~/openclaw/src/gateway/server-cron.ts` |
| `/tools/invoke` | `~/openclaw/src/gateway/tools-invoke-http.ts` |
| Hooks config schema | `~/openclaw/docs/automation/webhook.md` |
| Tools invoke docs | `~/openclaw/docs/gateway/tools-invoke-http-api.md` |

The key adaptation is: OpenClaw is single-tenant (one hooks config per instance). NanoClaw is multi-tenant, so the token, enabled flag, and routing must be per-tenant, resolved from `Host` header.

---

## Out of Scope (this phase)

- `/v1/responses` (OpenResponses-compatible LLM API) — valuable but larger scope; separate PRD
- WebSocket streaming of agent responses back to the calling app
- Per-tool allowlist configuration via the tenant dashboard (use platform defaults for now)
- Webhooks from agent to app (agent calling a URL when something happens) — separate feature

---

## Success Criteria

- A tenant app can `POST /hooks/agent` with a prompt and the agent runs and delivers a response to Discord.
- A tenant app can `POST /tools/invoke` and get a structured tool result in <2s.
- Hooks are disabled by default; enabling requires explicit opt-in in MC.
- A compromised hooks token cannot affect any other tenant.
- Existing agent pipeline (Discord → agent) is unaffected.
