Quickstart: Agent Data Plane — integrate your backend

中文版:快速上手:Agent 数据面

This is the start-to-finish path for giving a Pouchy agent live, safe access to your system: reading your authoritative state (Views), changing it with user consent (Actions), and reacting when it changes (Events). The worked example is an e-commerce backend — an Order view, a refund_order action, an order.shipped event — but nothing here is domain-specific: the same seven steps fit a game server, a clinic scheduler, a logistics tracker, or an IoT fleet.

Works with any database — or none. The integration boundary is plain HTTPS: you host two endpoints Pouchy calls (a View GET, an Action POST) and you POST events to Pouchy. What sits behind your endpoints — Postgres, Mongo, Redis, a spreadsheet, another API — is invisible to the platform by construction. There are no connectors, no drivers, no query language: the agent selects a declared capability by name and never authors a query.

Prerequisites: a Pouchy project (dashboard access), a backend reachable at one HTTPS hostname you control (the audience), Node ≥18 if you use the kit. Full wire contract: API Reference (§Data capabilities). A runnable receiver lives in the repo's examples/data-plane-receiver/.

On-prem / intranet backend? Pouchy never connects inward — you place a thin verify-first shim where Pouchy can reach it (cloud/DMZ host, reverse tunnel, or your API gateway's edge) and it talks to the internal system over your private network; DB credentials never leave the intranet. Deployment shapes and the security posture: Integration FAQ §9.


0) The mental model, 60 seconds

  • You declare capabilities (dashboard → Data Capabilities, or headless via the Admin API / @pouchy_ai/admin-sdk). Published revisions are immutable — new content mints the next version, running sessions keep the revision they pinned. Publishing is never revocation; the per-agent data flags and the per-capability disable bit are the live levers.
  • Pouchy signs every request it makes to you (POUCHY-ACTOR-V1). You verify first, then trust the verified subject — never the other way.
  • Your HTTP answer is the outcome claim on Actions: 2xx = committed, 4xx = rejected, 5xx/timeout = unknown (may have applied; resolved only by reconciliation, never blind retry).

1) Provision the signing secret — BEFORE anything else

Dashboard → Data Capabilities → signing card → Provision (or POST /v1/projects/{id}/capabilities/signing {"action":"provision"}). Record the pcsk_… plaintext now — it is shown exactly once.

Order matters. If no keys exist, the platform mints them on the first capability call (test-read included) and auto-minted plaintext is never shown anywhere — provisioning afterwards answers 409. If you hit that, rotate (fresh plaintext, once), switch your receiver, retire_previous.

2) Declare and publish the three capabilities

Dashboard → Data Capabilities → Publish, or headless (pchy_admin_… key):

import { createAdminClient } from '@pouchy_ai/admin-sdk'; // ≥0.12.0 — typed declarations

const admin = createAdminClient({ adminKey: process.env.POUCHY_ADMIN_KEY! });

await admin.publishCapability({
  kind: 'view', name: 'Order',
  description: 'One customer order: status, total, delivery ETA.',
  audience: 'api.shop.example.com',
  endpoint: 'https://api.shop.example.com/pouchy/order',
  fields: ['orderId', 'status', 'total', 'eta'],
  filters: ['orderId']
});

await admin.publishCapability({
  kind: 'action', name: 'refund_order',
  description: 'Refund one order to the original payment method.',
  sensitivity: 'personal',
  audience: 'api.shop.example.com',
  endpoint: 'https://api.shop.example.com/pouchy/refund',
  args: ['orderId', 'reason'],
  idempotency: 'action_id',
  reconcile: { endpoint: 'https://api.shop.example.com/pouchy/refund-status' }
});

await admin.publishCapability({
  kind: 'event', name: 'order.shipped',
  description: 'An order left the warehouse.',
  source: 'shop-backend',
  schemaVersion: 1,
  subjectField: 'customerId'
});

Names are the agent's vocabulary (Order, not tbl_order_v2) — the model plans with them. fields is an output allowlist: anything else your endpoint returns is stripped before the agent sees it.

3) Implement the receiver (backend-kit does the wire)

npm install @pouchy_ai/backend-kit   # ≥0.3.1 — Node ≥18, zero deps, typed
import {
  verifyPouchyRequest, idempotentAction, reconcileFrom,
  ActionRejected, verifyWebhookSignature
} from '@pouchy_ai/backend-kit';

const SECRETS = { [process.env.POUCHY_KID]: process.env.POUCHY_SIGNING_SECRET };
const AUDIENCE = 'api.shop.example.com';

// Every route: verify the EXACT raw bytes first, trust claims only after.
function verified(req, rawBody) {
  return verifyPouchyRequest(
    { method: req.method, url: req.url, headers: req.headers, body: rawBody },
    { secrets: SECRETS, audience: AUDIENCE }
  );
}

// GET /pouchy/order — the View. Filters arrive as query params (signed).
//   → 200 flat JSON; Pouchy curates to the declared fields.
// POST /pouchy/refund — the Action. Body: {"actionId","action","args"}.
const refund = idempotentAction(store, async (args, ctx) => {
  const order = await db.orders.get(args.orderId);
  if (!order) throw new ActionRejected({ error: 'no such order' }, 422);
  if (order.status !== 'paid') throw new ActionRejected({ error: 'not refundable' }, 409);
  return await payments.refund(order, args.reason); // your durable receipt
});
// GET /pouchy/refund-status?actionId=… — reconciliation, three-state,
//   answered from the SAME store (never by re-reading business state).
const refundStatus = reconcileFrom(store);

// POST /webhooks/pouchy — platform webhooks (agent.event_reply etc.):
const w = verifyWebhookSignature({
  body: rawBody, header: req.headers['x-pouchy-signature'],
  secret: process.env.POUCHY_WEBHOOK_SECRET });

Three rules the kit cannot enforce for you:

  • Raw bytes. Every signature covers the exact wire bytes — mount these routes on the raw body, not a parsed-then-reserialized one.
  • Refuse with ActionRejected (a stored, replayed 4xx = rejected). Let unexpected crashes throw — a 500 honestly classifies unknown.
  • store is yours — any backing that satisfies { get(actionId), put(actionId, row) }, from a SQL table to a Map.

4) Prove it — headlessly, before any agent talks

await admin.testReadCapability('Order', { externalUserId: 'cust_42', filters: { orderId: 'o_1001' } });
// → the curated rows exactly as an agent would see them, plus the pinned revision

await admin.testActionCapability('refund_order', {
  confirmDuplicates: true,           // phases B/C intentionally re-deliver — real side effects
  args: { orderId: 'o_test', reason: 'integration test' }
});
// → four phases: connectivity, duplicate same-intent, intent mismatch, reconcile.
//   Protocol verdicts are reported SEPARATELY from outcomes — "Protocol: PASS,
//   outcome: unknown" is a valid result, resolved by phase D.

const { executions } = await admin.listActionExecutions({ limit: 10 }); // read back what ran

Use a scratch order — the action test makes real deliveries.

5) Enable the agent

Dashboard → Agents → your agent → Data tab, or headless:

await admin.updateAgent(agentId, {
  data: { enabled: true, actions: { enabled: true }, events: { enabled: true } }
});

enabled gives instances read_data + the pinned capability menu; actions.enabled adds the confirm-gated run_action; events.enabled lets your backend wake this agent. Every flag is live in the off direction — flipping one kills that plane on the next turn of running sessions.

6) Emit events

import { PouchyClient } from '@pouchy_ai/backend-kit';

const pouchy = new PouchyClient({ projectId, token: process.env.POUCHY_SECRET_KEY });

await pouchy.emitEvent({
  agentId,                                  // omit → subscription fan-out
  name: 'order.shipped',
  eventId: `shipped:${orderId}`,            // stable domain fact — Pouchy dedupes on it
  data: { customerId, orderId }
});

An event is a signal, not a payload transport: data is mined for the declared subjectField and discarded — the agent is told "order.shipped occurred" and reads your Views for current facts. Without an agent.event_reply webhook subscriber (step 3), accepted events defer to the user's next conversation instead of waking one — subscribe the webhook if you want proactive delivery. To require proof events come from the declared source, publish with signing: 'required' and provision a per-source pesk_ key (the kit signs when you pass source + sourceKeys).

7) Go-live checklist

  • Signing secret provisioned FIRST and stored (step 1) — rotation plan noted
  • test-read returns curated rows; test-action four phases green (step 4)
  • Refusals use ActionRejected / clean 4xx — never a catch-all 200 or 500
  • Reconcile endpoint answers by actionId from the idempotency store
  • agent.event_reply webhook subscribed + verified (proactive Events)
  • Agent data flags on; automation (autoRun) only if you mean it
  • You know your two ground-truth readers: the Action journal and the Event receipts (dashboard → Data Capabilities, or listActionExecutions / listEventReceipts — same rows, admin-key mirrored)

Deeper: API Reference — Data capabilities (exact canonical strings, refusal semantics, automation two-key, source signing vectors), Capabilities Map (how the Data plane sits among the other capability planes).