跳转到内容

数据授权 (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读用户公开 profileusers.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” 点”撤销”。后端立即:

  1. grants.status = "revoked"
  2. grant_access_logaction='revoked', revoked_by='user'
  3. 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 和 DELETE
CREATE 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.revoked event, 打印 cleaning up local cache for grant 01HZ...

下一步