AgentOath 接上它
← 回首頁 · 原始碼 · English
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

只放不可還原的值

收據是公開的,而且沒有提供刪除的 API。

不要放

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

要放

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

自問一句

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

這保證什麼、不保證什麼

🔴 先看清界線再決定要不要靠它。下面「保證」那一欄是數學,任何人可以自己驗;「不保證」那一欄是目前的實作限制,不是承諾。

保證:內容沒被改過

收據裡任何一個位元組被動過,簽章就驗不過。Registry 自己也做不到—— 它沒有你的私鑰,而驗證端點不需要信任它。

保證:身分不能冒充

DID 由公鑰推導(sha256),任何人可以離線重算。 沒有你的私鑰就簽不出掛你 DID 的收據。

不保證:一定不會被刪

收據存在一般資料庫裡。沒有區塊鏈式的不可刪除保證,也沒有雜湊鏈或外部錨定。 「不會被刪」目前是信任 Registry 的營運者,不是數學。

不保證:時間是真的

收據裡的 timestamp簽章者自己填的—— 你可以簽一張標著三年前的收據。Registry 另外記了自己收到的時間, 但那一樣是「它說的」,不是可獨立驗證的證明。

不保證:清單是完整的

沒有辦法證明某張收據不是事後補插的,也沒辦法證明你看到的列表沒有被省略。

所以適合什麼

適合你自己要拿出來的證據——你想證明某件事按你說的方式發生過,而對方懷疑的是 「你事後改過」。不適合用來對抗 Registry 本身,也不適合當作可信時間戳記。 需要那些,請看下面。

需要可信時間或不可刪除? 那兩格已經有現成的補法,而且套件裡就有:

# 只送雜湊 —— 收據本身、檔案本身都不會離開你的機器
pip install "agentoath[timestamps]"

from agentoath.timestamps import stamp_receipt, verify, upgrade

proof = stamp_receipt(receipt)
report = verify(proof)
report["any_valid"]          # 時間戳記機構簽了這個雜湊和它自己的時鐘
report["bitcoin_confirmed"]   # …而且已經寫進 Bitcoin 區塊

proof = upgrade(proof)        # 幾小時後:pending → confirmed

兩種機制刻意並用,因為它們的失敗方式不同。RFC 3161 由時間戳記機構用自己的時鐘簽章 ——即時、法院與 eIDAS 認的格式,你信任的是那家機構而不是我們。OpenTimestamps 把雜湊 寫進 Bitcoin ——不需要信任任何一方,代價是要等幾小時才進區塊。

🔴 拿到 OpenTimestamps 證明、加上你自己保存的那份收據之後,就算這裡的收據全部被刪掉, 你依然證明得了那件事發生過、以及發生在什麼時候。 它不是把資料庫變成不可刪除, 而是讓資料庫**變得不重要** —— 那才是你要的性質。

JavaScript 套件沒有這個模組:那邊唯一長期存在的 OpenTimestamps 函式庫 2019 年就停止維護, 比較新的全是 v0.x、作者不明、發布不到兩個月。要別人安裝的套件不該綁那種相依。targetOfReceipt() 會給你要蓋章的雜湊,實際蓋章交給 Python 套件或 ots 指令 ——證明格式是語言中立的 JSON。

公證一份文件

沒有「文件收據」這種型態,這正是重點:文件就是一般收據加上一個 metadata 約定, 所以簽章、跨語言逐位元一致、隱私黑名單、Registry 的驗證,全部原封不動繼續生效。 新型態的話這些都要重新掙一次。

# Python
from agentoath.documents import build_document_receipt
receipt = build_document_receipt(identity, "scans/book-0417.pdf",
    refs={"source_ref": "isbn:9780000000001", "acquired_ref": "po-2026-0817"})

// JavaScript
import { buildDocumentReceipt } from 'agentoath/documents';

receipt_id 由檔案雜湊推導,所以同一份檔案公證兩次是同一張收據, 不是兩張互不相干的。檔案是串流讀的,幾 GB 的掃描檔不必塞進記憶體。

🔴 檔案本身永遠不會離開你的機器,進到收據裡的只有它的 sha256。 雜湊對「已經有那個檔案的人」證明得了「就是同一份」,對沒有的人什麼都證明不了 ——當你要公證的是一份不能散布的掃描檔時,那正是你要的性質。

參照欄位必須以 _ref 結尾且不超過 128 字元。收據是公開且永久的, 一句被貼進去的話是收不回來的。

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

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

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

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

客戶端

兩種語言的官方套件都內建了。零相依(Python 只多用 cryptography,JS 只用 Node 內建 crypto)、 canonical JSON 與 Registry 逐位元相同、三條鐵則本地先擋、黑名單本地先掃、預設關閉。

# Python
pip install agentoath
from agentoath.hosted import Identity, build_signed_receipt

// JavaScript(ESM;CJS 用 await import)
npm install agentoath
import { Identity, buildSignedReceipt } from 'agentoath/hosted';

不想加相依、或用的不是這兩種語言?這一份是同樣的東西,單檔零相依,可以直接放進專案:

⬇ 下載 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 由公鑰推導,換鑰匙 = 新身分、信譽歸零。這是設計,不是故障。私鑰請當成長期資產保管。