快速上手:Agent 数据面——接入你的后端(中文版)

English version: Quickstart: Agent Data Plane。 两份内容同步维护;以英文版为准绳,中文版跟随更新。

这是让 Pouchy Agent 实时、安全地访问你的系统的端到端路径:读你的权威 状态(View)、经用户同意改变它(Action)、在它变化时被通知 (Event)。示例用电商后端——Order 视图、refund_order 动作、 order.shipped 事件——但没有任何领域限定:同样七步适用于游戏服务器、 门诊排班、物流跟踪或 IoT 集群。

兼容任何数据库——或者没有数据库。 集成边界是纯 HTTPS:你托管两个 Pouchy 会调用的端点(View 走 GET、Action 走 POST),事件由你 POST 给 Pouchy。端点背后是什么——Postgres、Mongo、Redis、SQL Server、一张 Excel、另一个 API——平台在结构上看不见。没有 connector、没有驱动、没有 查询语言:Agent 只按名字选用你声明过的能力,从不自己写查询。

前置:一个 Pouchy 项目(dashboard 权限)、一个你可控的 HTTPS 域名下可达的 后端(即 audience)、用 kit 的话 Node ≥18。完整线协议: API Reference(§Data capabilities)。可运行 的接收端在仓库 examples/data-plane-receiver/

系统在内网(ERP/内部数据库)? Pouchy 从不向内连——你把一层先验签的 薄 shim 放在 Pouchy 够得着的地方(云/DMZ 主机、反向隧道、或你 API 网关的 公网边缘),由它经你的私有网络与内部系统通信;数据库凭据一步不出内网。 部署形态与安全姿势见 Integration FAQ §9


0)心智模型,60 秒

  • 声明能力(dashboard → 数据能力,或用 Admin API / @pouchy_ai/admin-sdk 无头操作)。已发布的修订不可变——新内容铸下 一个版本,运行中的会话继续用各自钉定的修订。发布从来不是撤销;每 Agent 的 data 开关和每能力的停用位才是实时闸门。
  • Pouchy 对发给你的每个请求签名(POUCHY-ACTOR-V1)。先验签、后信任 已验证的主体——顺序绝不能反。
  • Action 上,你的 HTTP 应答就是结果声明:2xx = committed, 4xx = rejected,5xx/超时 = unknown(可能已生效;只能靠对账解决, 永不盲目重试)。

1)先铸签名密钥——先于其它一切

Dashboard → 数据能力 → 签名卡片 → 签发(或 POST /v1/projects/{id}/capabilities/signing {"action":"provision"})。 立刻记下 pcsk_… 明文——只显示这一次。

顺序有讲究。 若尚无密钥,平台会在第一次能力调用(包括 test-read) 时自动铸一对——而自动铸出的明文任何地方都不会展示,之后再签发只会得到 409。真踩了这个坑:rotate(拿到一次新明文)→ 切换你的接收端 → retire_previous

2)声明并发布三种能力

Dashboard → 数据能力 → 发布,或无头(pchy_admin_… 密钥):

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'
});

名字是 Agent 的词汇(Order,不是 tbl_order_v2)——模型用它们做 计划。fields 是输出白名单:你端点多返回的任何字段都在到达 Agent 前被 裁掉。

3)实现接收端(线协议交给 backend-kit)

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 });

三条 kit 替你守不住的规矩:

  • 原始字节。 每个签名都覆盖线上的精确字节——这些路由要挂在 raw body 上,不能是解析再序列化过的。
  • 拒绝用 ActionRejected(持久化、可重放的 4xx = rejected)。意外 崩溃就让它抛——500 会诚实地归类为 unknown
  • store 是你的——满足 { get(actionId), put(actionId, row) } 的任何 存储都行,SQL 表到 Map 皆可。

不用 Node? 线协议语言中立,随包发布的 vectors 让任何移植都能自证。 仓库 examples/csharp-receiver/ 有零依赖的 C#/.NET 参考实现 (POUCHY-ACTOR-V1 + webhook 验签 + 来源签名 + intent hash,内置向量自测—— dotnet run 全绿再上线)。

4)先无头自证——在任何 Agent 开口之前

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

用一张草稿订单——action 测试是真实投递。

5)在 Agent 上开启

Dashboard → Agents → 你的 agent → 数据 tab,或无头:

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

enabled 给实例 read_data + 钉定的能力菜单;actions.enabled 加上 确认门控的 run_action;events.enabled 允许你的后端唤醒这个 agent。 每个开关关向都是实时的——关掉一个,运行中的会话下一轮就失去那个平面。

6)发事件

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 }
});

事件是信号,不是载荷运输:data 被挖出声明的 subjectField 后即被 丢弃——Agent 只被告知"order.shipped 发生了",当前事实仍从你的 View 读。 没有订阅 agent.event_reply webhook(第 3 步)时,已接受的事件顺延到 用户下次会话,而不是唤醒一次——要主动触达就把 webhook 订上。要证明事件 确实来自声明的来源,发布时加 signing: 'required' 并签发按来源的 pesk_ 密钥(kit 传 source + sourceKeys 即自动签名)。

7)上线检查单

  • 签名密钥最先签发并妥存(第 1 步)——轮换预案记录在案
  • test-read 返回裁剪后的行;test-action 四相全绿(第 4 步)
  • 拒绝一律 ActionRejected / 干净 4xx——绝不用兜底 200 或 500
  • 对账端点按 actionId 从幂等存储作答
  • agent.event_reply webhook 已订阅并验签(事件主动触达)
  • Agent data 开关就位;自动化(autoRun)只在真需要时开
  • 知道两个 ground-truth 读口:Action 执行日志与事件回执 (dashboard → 数据能力,或 listActionExecutions / listEventReceipts——同样的行,admin-key 镜像可读)

更深入:API Reference — Data capabilities (精确的规范化字符串、拒绝语义、自动化双钥、来源签名向量)、 Capabilities Map(数据面在其它能力平面中的 位置)。