数据授权 (Data Grants)
Data Grant 是 Hashee 的”细粒度数据访问授权”机制:H2A 关系是”是否允许通讯”, Grant 是”是否允许 Agent 读取用户的某类数据”。两者解耦——用户可以让 Agent 通讯 但不让它读联系人;可以让 Agent 看历史消息但不让它看个人档案。
技术真源:E2EE 规范 §6 Grant Ledger
.claude/rules/security-e2ee.md§5 Grant ledger rules。
核心保证
- 用户发起:Grant 必须由用户在客户端明示授权,Agent 不能 implicit 拿权限。
- 范围明确:每个 Grant 标明 scope(“读联系人” vs “读最近 30 天消息”)+ 时间窗口。
- 可撤销:用户可在客户端的”Agent 详情 → 已授权数据”随时撤销。
- 可审计:每次 Agent 读 grant-scoped 数据,后端写
grant_access_log一行 (append-only trigger 强制;永远无法删除)。 - 盲管道兼容:Grant 通过
message_grants表 wrap CEK 给 Agent; 后端依然看不到明文。
Grant scope(V1 内置)
| scope id | 描述 | 数据来源 |
|---|---|---|
profile:read | 读用户公开 profile | users.public_profile |
profile:read.full | 读用户完整 profile(含联系方式、隐私字段) | users.profile |
contacts:read | 读联系人列表 | users.contacts |
conversations:read | 读对话历史(H2A 范围内) | messages 表(per H2A relation) |
conversations:read.all | 读 H2A 关系外的对话历史(极少见) | messages 表 |
knowledge:read | 读用户知识库(笔记 / 收藏 / bookmark) | knowledge_* 表 |
notification:read | 读未读通知数 / 通知摘要 | notifications 表 |
每个 scope 有 sensitivity 与权限提示文案 i18n key(V1 已预置)。
申请 Grant
V1 通过 Artifact 表单完成(用户可见、可拒绝、可定制):
import { ulid } from "ulid";
const requestId = ulid();await agent.sendArtifact(conversationId, { artifact: { artifact_id: requestId, subtype: "approval", title: "申请数据访问", payload: { request: { scopes: [ { id: "knowledge:read", duration_days: 30 }, { id: "conversations:read", duration_days: 7 }, ], reason: "我需要读取你的笔记和近 7 天对话来生成本周总结。", revocable: true, }, approve_label: "授权", reject_label: "拒绝", timeout_s: 86400, // 1 天内未回 → 自动失效 }, },});
agent.addEventHandler(async (event) => { if (event.type !== "artifact_response") return; if (event.payload.ref_artifact !== requestId) return;
if (event.payload.action === "approve") { // 用户同意;后端已建立 grant 行 console.log("granted, grant_id:", event.payload.payload.grant_id); await runWeeklyDigest(event.sender_id); } else { console.log("rejected:", event.payload.payload.reason); // 业务侧降级 }});使用 Grant 读数据
授权后用 agent.readUserData(),传 grant_id 与 scope:
const knowledge = await agent.readUserData({ user_id: targetUserId, grant_id: grantId, scope: "knowledge:read", filters: { tags: ["work", "important"] }, limit: 50,});
// knowledge 是已解密的明文数据数组console.log(`got ${knowledge.length} entries`);
// 后端自动写一行 grant_access_log:// { grant_id, agent_id, scope, accessed_at, item_count: 50 }每次调用 readUserData 后端写一条 grant_access_log 行。
该行通过 trigger 强制 append-only(既不能 UPDATE 也不能 DELETE)。
Grant 撤销
用户在客户端”Agent 详情 → 已授权数据 → DemoBot 的 knowledge:read” 点”撤销”。后端立即:
grants.status = "revoked"- 写
grant_access_log:action='revoked', revoked_by='user' - 推
grant.revoked事件给 Agent
agent.addEventHandler(async (event) => { if (event.type !== "grant.revoked") return; const { grant_id, scope, user_id } = event.payload; console.log(`grant ${grant_id} (${scope}) revoked by user ${user_id}`);
// 业务侧立即清理: // - 内存缓存 // - 已派生的衍生数据(向量化、指纹、AI 训练样本) await db.deleteGrantData({ grant_id });});Agent 主动放弃 Grant
如果 Agent 不再需要某个 grant(比如功能升级后用更小 scope):
import { restRequest } from "@hasheeai/agent-sdk-ts";
await restRequest({ method: "DELETE", baseUrl, path: `/agents/${AGENT_ID}/grants/${grantId}`, token,});// 后端写 grant_access_log: action='revoked', revoked_by='agent'// 同时给用户客户端推 system.grant_released 通知(让用户知道)主动放弃比”等 Grant 自然过期”更礼貌——用户会在通知里看到 “DemoBot 主动归还了 knowledge:read 权限”。
时间窗口
每个 Grant 在申请时声明 duration_days:
| duration | 说明 |
|---|---|
1-30 | 最常见,按业务需要 |
0 | 一次性(读完后自动撤销) |
-1 | 永久(用户可显式选择,不推荐) |
到期后:
- Agent 调
readUserData返回GRANT_EXPIRED - 后端推
grant.expired事件 - Agent 可重新申请(一次性 grant 不能续)
grant_access_log(永不删除)
为什么这么严格?— 防止”权限被授予但被偷偷读光后撤销 + Agent 删除访问记录” 攻击。审计完整性是 Hashee 数据主权模型的基石。
grant_access_log schema(简化):
CREATE TABLE grant_access_log ( id UUID PRIMARY KEY, grant_id UUID NOT NULL, agent_id UUID NOT NULL, user_id UUID NOT NULL, scope TEXT NOT NULL, action TEXT NOT NULL, -- 'read' / 'revoked' / 'expired' revoked_by TEXT, -- 'user' / 'agent' / 'system' item_count INT, accessed_at TIMESTAMPTZ NOT NULL, metadata JSONB);
-- Trigger: 拒绝 UPDATE 和 DELETECREATE TRIGGER grant_access_log_block_mutations BEFORE UPDATE OR DELETE ON grant_access_log FOR EACH ROW EXECUTE FUNCTION block_mutations();Trigger 在 migration 0014 创建;CI 校验它存在。
用户视角
用户在 Hashee 客户端 “Agent 详情 → DemoBot → 已授权数据” 看到:
DemoBot 当前持有的授权: ✓ 读知识库 (knowledge:read) 授权于 5 月 8 日,剩余 23 天 最近 7 天访问 12 次,最后访问 2 小时前 ✓ 读对话历史 (conversations:read) 授权于 5 月 8 日,剩余 5 天 最近 7 天访问 3 次,最后访问 昨天
[撤销 knowledge:read] [撤销 conversations:read] [全部撤销] [查看完整访问日志]点”查看完整访问日志”展示所有 grant_access_log 行(per scope,per call,
含访问数量与时间戳)。
与盲管道兼容
后端怎么把 grant-scoped 数据递给 Agent 而又不解密?
Agent 申请 grant + 用户同意 ↓后端建 grants 行 ↓后端为 grant 选择数据集(如最近 7 天 messages) ↓对每条数据,**用户客户端**预先做了"加密一份给 Agent X25519 公钥的 wrap",存在 `message_grants.wrap_envelope` ↓Agent readUserData 时后端只是把这些 wrap_envelope 发给 Agent ↓Agent 用自己的 X25519 私钥 unwrap → CEK → 解密内容也就是说,授权时用户客户端要重 wrap 已存在的消息 CEK 给 Agent 公钥。这是为什么 grant 申请有”延迟”——客户端要计算并上传 wraps。
V1 默认延迟 < 5 秒(小于 100 条消息)。大量 grant 范围(千条以上)的 Agent 应避免一次性大 scope,倾向多次小 scope。
最佳实践
- 请求最小 scope:按需申请;不要”先抓一把通用权限”。
- 说明清楚原因:用户看到 reason 字段决定是否授权;说明越具体接受率越高。
- 优雅降级拒绝:Agent 应该在没有 grant 时仍能提供基础服务,不要”没权限就 dead”。
- 及时清理:撤销 / 过期事件触发后立即清理本地数据 + 衍生数据。
- 不要堆积 grant:完成任务后主动
DELETE /agents/:id/grants/:gid归还。
演示视频
视频文字版逐节描述
- 00:00 – 00:10 — Hashee app 用户对 DemoBot 说 “帮我做本周工作总结”。
Agent 终端
inbound text=...。 - 00:10 – 00:25 — Agent 决定需要 grant;sendArtifact approval 卡片。 Hashee app 显示 “申请数据访问” 卡片,列出申请的 scope 和 reason。
- 00:25 – 00:35 — 用户点”授权”。Hashee app 显示 “处理中…”(客户端 在重新 wrap 最近消息 CEK);约 3 秒后回到 Agent。
- 00:35 – 00:45 — Agent 终端
grant approved, grant_id=01HZ..., 调readUserData三次(一次拉 knowledge,一次拉 messages,一次拉 profile)。 - 00:45 – 00:55 — Agent 跑总结流程;约 8 秒后发回总结消息。
- 00:55 – 01:10 — 演示撤销:用户进 Agent 详情 → 已授权数据 →
撤销 knowledge:read。Agent 终端立即收到
grant.revokedevent, 打印cleaning up local cache for grant 01HZ...。
下一步
- Artifact 高级 —
subtype: "approval"详细 - 更新与撤销 — 关系层撤销
- 安全与加密边界 — 盲管道与 grant ledger 关系