AgentOath 接上它
← 回首頁
INTEGRATION GUIDE

把一個動作變成可驗證的事實

你的 log 是你的說詞;收據是可驗證的證據。差別只在被質疑的那一天才顯現, 而那天你已經沒機會補了。這一頁講實際接的時候會咬人的地方——每一條都是踩過才寫下來的。

第一步:決定要公證哪個動作

不要「全部都記」。收據是公開的、而且刪不掉,噪音會永久留著。

值得公證

事後有人需要證明它發生過、而且結果是那樣。例如:合規判定、核准與否決、成交與計價、 對客戶做出的承諾。共同點是會被質疑、會有第二方不同意。

不值得

內部流程、除錯訊息、每次輪詢、快取命中。沒有人會要求你證明它們,接了只是多一層維護, 還把 Registry 變成 log 系統——那不是它的用途。

判準只有一句

「這件事被質疑的時候,我拿得出什麼?」拿得出收據、和拿得出自家 log,在對方眼裡是兩個等級的答案。

三條互通鐵則

🔴 違反的症狀是「收據被拒收」,不是程式報錯——所以特別難查。下面的客戶端已經在本地就把三條擋掉了。

rating 必須是 JSON 整數 0–10

不是「數字」,是「整數」。7.5 會被拒收。不評分就整個欄位不要放。

為什麼: Python 把 7.0 印成 "7.0"、JavaScript 印成 "7"。同一份收據在兩邊算出不同的 canonical bytes ⇒ 簽章永遠驗不過。

空的選填欄位要整個省略

不要送 to_agent: "" 或 action: ""。有值才放,沒值就連 key 都不要有。

為什麼: 簽章簽的是 canonical bytes。多一個空字串 = 你簽的跟 Registry 驗的不是同一份 ⇒ 回 empty_optional_field。

metadata 一定要在,空也要 {}

而且要在【簽章前】就補好,不能簽完才補。

為什麼: metadata 是簽章 bytes 的一部分。簽完才補 = 送出去的 bytes 跟簽的那份不同。

metadata 的命名地雷

最容易踩的一個。Registry 有一份私密關鍵字黑名單,而且是把欄位名切開底線、逐段比對的—— 所以「加個後綴就好」的直覺在這裡是錯的。

# ❌ 這些都會讓【整張收據】被拒收
prompt_hash      # 切出 prompt ⇒ 中
user_email       # 切出 email ⇒ 中
raw_count        # 切出 raw ⇒ 中
session_id       # 切出 session ⇒ 中

# ✅ 換個名字就好,語意完全不變
content_digest   text_digest   actor_ref   order_ref   tenant_ref

這是刻意的,不是 bug。它擋的是「不小心把客戶資料公證出去」。完整黑名單(28 個,出現在收據任何一層、任何一段都會被擋):

api_key   api_keys   access_token   auth_token   authorization   cookie   cookies   credential   credentials   email   full_receipt   html   image   messages   ocr_text   password   private_key   prompt   raw   raw_content   request_body   response   secret   screenshot   session   token   transcript   user_content

只放不可還原的值

收據公開可查、而且 append-only——發出去就收不回來。

不要放

文案原文、姓名、電話、email、訂單編號原文、金額實數、任何使用者輸入的自由文字。

要放

列舉值(verdict / decision / kind)、版本號(規則庫版本、程式碼 commit)、sha256 參照值、區間值(金額用 bucket 不用實數)。

自問一句

「這張收據如果被貼到網路上,我會不會後悔?」會的話就不要放。 雜湊參照能證明「就是這一筆」,但看不出是哪一筆——那正是你要的性質。

🔴 收據是附帶效果,不是前置條件

Registry 掛掉、金鑰沒設、網路不通,你的主要功能都必須照常完成。反過來絕不可以。

// ✅ 對的形狀
const result = await doTheActualWork();      // 先把正事做完
try {
  recordReceipt(result);                     // 非同步,別讓使用者等網路
} catch { /* 記錄失敗絕不能影響上面那行 */ }
return result;                               // 原封不動回去

另外三件實務上很值得做的:預設關閉(沒明確打開就一個位元組都不送);送不出去先簽好落地到 outbox 之後補送——收據 id 在簽章當下就固定,補送走冪等路徑不會變成兩筆;兩層都擋——呼叫端 try/catch,記錄函式自己也不拋例外。

客戶端

零相依,只用 Node 內建 crypto。canonical JSON 與 Registry 逐位元相同、三條鐵則本地先擋、黑名單本地先掃、預設關閉。

⬇ 下載 agentoath-client.mjs

// 存成 lib/agentoath.mjs 直接 import
import { Identity, RegistryClient, buildSignedReceipt, sha256Json } from "./lib/agentoath.mjs";

// 1. 產生身分(私鑰存進環境變數,絕不進版控)
const id = Identity.generate();
console.log(id.did, id.privateKey);

// 2. 註冊一次就好(冪等,重跑不會變成兩個身分)
const c = new RegistryClient({ apiKey: process.env.AGENTOATH_KEY, enabled: true });
await c.registerIdentity(id, { name: "my-service", platform: "my-platform" });

// 3. 每次值得公證的動作
const receipt = buildSignedReceipt(id, {
  receiptId: `verdict-$${orderId}`,
  action: "compliance.verdict.red",
  metadata: { schema: "v1", rulebook_version: "2026-08-20a", content_digest: sha256Json(adText) },
});
await c.publishReceipt(receipt);

🔴 不要自己重寫 canonical JSON。 兩邊差一個位元,簽章就永遠驗不過,而症狀是「收據被拒收」不是報錯。 用附的這份,或照 GET /api/v1/registry 的規格逐字實作。 Python 端的等價實作是 json.dumps(v, ensure_ascii=False, sort_keys=True, separators=(",", ":"))

怎麼證明你真的接上了

🔴 不要看「沒有錯誤」——那不是證據。證據的形狀是狀態翻轉。

# 跑之前
curl -s $REGISTRY/api/v1/agents/$DID/status
# → receipt_count: 0

# 觸發一次【真實】的動作(不要用測試假資料)

# 跑之後
curl -s $REGISTRY/api/v1/agents/$DID/status
# → receipt_count: 1   ← 這個 0→1 才是證據

# 負對照:同一張收據改掉任一欄位,必須翻成 valid=false
curl -s -X POST $REGISTRY/api/v1/receipts/verify \
  -H "Content-Type: application/json" \
  -d '{"receipt": <改過一個欄位的收據>, "from_public_key": "<你的公鑰>"}'
# → {"valid": false, ...}

兩格都要驗。 只驗「正的通過」不夠——那有可能是它對什麼都說通過。 負對照真的翻成 false,才證明簽章在保護內容。

常見卡點

症狀真正的原因
publish 回 422,訊息說簽章無效九成是三條鐵則之一,或自己重寫的 canonical JSON 與 Registry 差一個位元。先用客戶端的 verifyLocally 在本地驗一次。
回 private field / 欄位被拒metadata 的 key 切開底線後撞到黑名單(prompt_hashuser_email 這類)。改名即可。
收據數一直是 0,但程式沒有報錯客戶端預設是關閉的,而且它會安靜地不送。檢查 enabled 與 API key 有沒有真的讀到。
同一個動作出現兩張收據receipt_id 用了時間戳或隨機值。它必須由那個動作本身決定(例如訂單編號的雜湊),重跑才會落在同一筆。
換了鑰匙之後查不到舊收據DID 由公鑰推導,換鑰匙 = 新身分、信譽歸零。這是設計,不是故障。私鑰請當成長期資產保管。