架构与实践)
Friend 项目内存权限管控app/key 级内存授权Memory App/Key Grants架构与实践【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend导读本文围绕 Friend 开源仓库中 memory_app_key_memory_grants_readiness.md 这一技术纪事文档展开深入解析服务端持有server-owned的、按应用app与按密钥key维度的内存memory授权体系。文章会完整覆盖规范化的 Firestore 存储路径与文档契约、授权链路从 API Key 认证到authorize_app_key_scope_memory_grant门禁、客户端自授权被规则拒绝的验证方式、Developer API 默认内存读取的窄缝接入以及安全默认的管理员授权就绪脚本。读完本文你将掌握如何在多消费者Developer API / MCP / 第三方场景下为每个用户、每个应用、每个密钥持久化并校验内存读写授权并理解缺省拒绝、显式授权、Archive 永不默认可见这一安全模型在 Friend 中的具体实现。背景与设计目标Friend 是一个AI 看你的屏幕、听你的对话并告诉你该做什么的开源项目。其内存子系统memories沉淀了大量用户侧语义数据因此对内存的读取与写入必须实施服务端强管控而不是信任客户端请求体中的任何声明。本纪事文档Scope: Oracle P0-1/P0-6要解决的问题是外部内存消费者external memory consumers如何获得按用户—按应用—按密钥的细粒度内存访问授权。设计上坚持三条核心原则服务端持有server-owned授权状态持久化在 Firestore 中由后端/Admin SDK 通过 IAM 读写客户端规则firestore.rules一律拒绝直连。缺省失败关闭fail closed认证身份、所需 scope、持久化授权文档任一缺失或畸形都按拒绝处理。Archive 永不默认可见即使显式授予了archive_read能力默认读取策略也绝不把 Archive 内容默认暴露必须叠加显式 Archive 意图与 Archive 路由能力。服务端持有的 Firestore 存储路径规范化文档位置授权状态的唯一规范化持久化文档为users/{uid}/memory_control/app_key_memory_grants对应的路径构造逻辑实现在 memory_app_key_grants.pyAPP_KEY_MEMORY_GRANTS_COLLECTION memory_control APP_KEY_MEMORY_GRANT_DOC_ID app_key_memory_grants APP_KEY_MEMORY_GRANT_SUBPATH f{APP_KEY_MEMORY_GRANTS_COLLECTION}/{APP_KEY_MEMORY_GRANT_DOC_ID} def app_key_memory_grants_document_path(uid: str) - str: return fusers/{uid}/{APP_KEY_MEMORY_GRANT_SUBPATH}读取辅助函数后端/Admin SDK 侧通过read_app_key_memory_grants_state(uid, db_client)读取该文档见 memory_app_key_grants.py。该 helper 刻意设计为可注入的db_client调用方必须显式传入 Firestore 客户端路由代码与测试不依赖请求体字段也方便在测试中 fake 注入。返回结构化的读取决策AppKeyMemoryGrantStateRead通过reason字段区分三种情况场景presentmalformedreason文档不存在falsefalsemissing_app_key_memory_grants_state文档存在但顶层不是包含grants的 maptruetruemalformed_app_key_memory_grants_state契约结构合法truefalseok并携带原始嵌套状态其中顶层契约合法的判定_looks_like_grants_contract只检查state是 dict 且grants键对应 dictmemory_app_key_grants.py。对应单元测试见 test_memory_app_key_grants.py其中test_missing_app_key_memory_grants_state_returns_absent_state断言reason missing_app_key_memory_grants_statetest_malformed_app_key_memory_grants_state_is_detected_and_fails_closed断言畸形状态会级联导致授权allowed is False且reason malformed_app_key_scope_grant。授权消费的契约形状文档内嵌套契约存储文档刻意保持恰好是authorize_app_key_scope_memory_grant(...)消费的嵌套结构无需转换grants: developer_api: apps: app_123: keys: key_456: enabled: true scopes: - memories.read default_read: true archive_read: false write: false路径换算关系Firestore 文档路径users/{uid}/memory_control/app_key_memory_grants文档内授权路径grants.consumer.apps.app_id.keys.key_id授权辅助示例grants.developer_api.apps.app_123.keys.key_456代码中build_app_key_scope_grant_contract_state(...)memory_app_key_grants.py正是这一形状的纯函数构造器供测试与管理工具复用参数包括consumer、app_id、key_id、scopes、default_read、archive_read、write、enabled默认True。双重要求路由 scope 与持久化授权 scope 缺一不可外部消费者developer_api、mcp、third_party想要访问内存必须同时满足认证后的路由 scope例如 API Key 认证后携带memories:readDeveloper API 词汇或memories.read授权词汇。持久化的授权 scopeusers/{uid}/memory_control/app_key_memory_grants文档中对应grants.consumer.apps.app_id.keys.key_id下存在匹配的 scope 与能力标志。且默认读取不会让 Archive 默认可见。Archive 的暴露需要更强的memories.archive.read操作授权并且必须与显式 Archive 意图、Archive 路由能力组合后才可能放行——这一点在_grant_observability中始终写入archive_default_visible: Falseproduct_authorization.py。客户端自授权拒绝与本地规则验证firestore.rules 全面拒绝客户端直连firestore.rules 明确users/{uid}/memory_control/{document}包括app_key_memory_grants属于服务端持有状态移动端/Web 客户端必须走后端 API禁止绕过内存写入网关直接读写内部文档match /users/{uid}/{collectionId}/{document**} { allow read, create, update, delete: if false; } match /{document**} { allow read, write: if false; }Admin SDK/IAM 后端写入不受客户端规则限制形成客户端不可达、服务端可控的双层边界。本地模拟器断言纪事文档记录本地模拟器 harness 专门断言一个已登录客户端不能对以下路径进行 read/create/update/deleteusers/memory-emulator-user/memory_control/app_key_memory_grants并尝试在如下位置进行自授权self-grantgrants.developer_api.apps.client-app.keys.client-key本地证明命令本次 slice 中执行npm run test:memory-app-key-grants-rules:emulator该命令在根目录 package.json 中定义为test:memory-app-key-grants-rules:emulator: firebase emulators:exec --only firestore --project demo-memory \node backend/scripts/firestore_rules_emulator_test.mjs\文档明确标注结果为本地 Firebase Firestore 模拟器下的 PASS这不等同于线上cloud规则或 IAM 证明。这是测试通过 ≠ 生产已证明边界意识的具体体现。窄缝接入Developer API 认证输出与内存授权契约的衔接本 slice 新增了一条刻意保持窄的组合缝composition seam把 Developer API 认证输出进一步贴近内存 app/key 授权契约1. 认证数据面扩展database.dev_api_key.get_user_and_scopes_by_api_key(...)实现见 dev_api_key.py其核心get_api_key_auth_result在 dev_api_key.py现在额外返回key_idFirestore 文档 id以及稳定的app_idkey 元数据中存在则取其值否则落到显式 Developer API 应用桶developer_api加上原有的user_id/scopes字段。dependencies.ApiKeyAuthdependencies.py现在携带可选的app_id与key_id仅需要历史用户 id 的旧路由仍可通过 uid-only helper如get_uid_from_dev_api_key取auth.uid保持向后兼容。2. 认证上下文构造新增get_developer_memory_default_memory_read_context(...)dependencies.py校验认证 scope 中包含memories:read否则直接 403。校验app_id与key_id存在否则 403理由为Missing Developer API app/key identity for memory memory authorization。执行限流检查策略名dev:memories_read。将 Developer API scope 词汇memories:read、memories:write通过_memory_memory_scopes_from_developer_scopes换算为内存授权词汇memories.read、memories.write仅在 uid、app id、key id、memories:read全部就绪时返回ProductAuthorizationContextconsumer 固定为developer_apisurface 为developer_default_memory_read。ProductAuthorizationContext定义在 product_authorization.py字段包括uid、consumer、surface、app_id、key_id、scopes、explicit_archive_request、requires_archive_capability。3. 组合授权缝新增authorize_memory_external_default_memory_read(...)product_authorization.py把上述认证上下文与read_app_key_memory_grants_state(...)、authorize_app_key_scope_memory_grant(...)组合为DEFAULT_READ操作做最终判定。同类写入缝authorize_memory_external_default_memory_write(...)以WRITE操作委托同一契约product_authorization.py。authorize_app_key_scope_memory_grant(...)product_authorization.py是核心判定器其拒绝理由覆盖以下全部分支场景reason消费者不在EXTERNAL_MEMORY_CONSUMERS首方走 rollout 路径first_party_rollout_authorization放行缺少app_id或key_idmissing_app_or_key_identity认证 scope 缺少所需操作 scopemissing_authenticated_scope_scope持久化状态结构不合法malformed_app_key_scope_grant对应 grant 不存在missing_app_key_scope_grantgrant 中enabled/scopes类型不合法malformed_app_key_scope_grantgrant 被禁用app_key_scope_grant_disabled持久化 scopes 缺少所需 scopemissing_persisted_scope_scope对应操作能力标志缺失missing_operation_grant全部通过ok携带MemoryAccessPolicystatus 2004. 路由接线本 slice 已把 Developer API 默认内存列表路径GET /v1/dev/user/memories无 category 过滤时接入该组合缝路由在读取内存列表前先调用authorize_memory_external_default_memory_read(auth_context, db_clientdb)未通过则按app_key_grant.status_code返回并携带reason、consumer、archive_default_visibleFalse、archive_capabilityFalse、app_id、key_id等明细developer.py。而 category 过滤的 Developer API 列表仍保持 legacy 兼容不宣称内存默认读强制。该缝在认证身份、所需 scope、存储的 app/key 授权状态或匹配的持久化操作授权缺失/畸形/错误时全部失败关闭放行的默认读策略始终保持archive_capabilityfalse未暴露任何 Archive 路由/路径。剩余路由依赖与未完成项纪事文档明确列出宽泛的路由强制仍未完成属于已知边界Developer API 向量搜索内存向量读取前仍需接入同样的 app/key 授权组合对应路由 developer.py 已做类似门禁但文档撰写时点的广义强制仍未铺开。Developer API category 过滤列表仍为 legacy 兼容路径等待 T22/T23 的 category/read/write 收敛。MCP REST/SSE API Key 认证当时仅返回uidMCP key 模型尚未持久化 scopes/key scope 元数据。MCP SSE 的 OAuth 广告 scope路由执行必须把验证过的 scopes/app/key 身份带到内存授权缝相关演进见姊妹篇 memory_mcp_app_key_scope_readiness.md。产品/memory路由保持首方omi_chat身份首方 rollout/默认授权路径刻意不变。这些非主张non-claims的诚实记录恰是评审者判断该功能成熟度的关键依据。安全默认的管理员授权就绪脚本脚本入口与默认行为仓库新增 app_key_memory_grant_assignment_readiness.py 作为安全默认的授权分配就绪 runner用于确定性的服务端/Admin 授权分配规划。默认命令python3 backend/scripts/app_key_memory_grant_assignment_readiness.py默认行为见run_assignment_readiness的not execute分支脚本 L188-L203statusNOT_RUN、read_onlytrue、mutation_allowedfalse不执行任何 Firestore 读或写。它只是一个 readiness 产物。Dry-run 校验命令python3 backend/scripts/app_key_memory_grant_assignment_readiness.py \ --execute \ --assignment-file /secure/admin/memory-app-key-memory-grants.json该模式会执行normalize_assignments全部校验产出statusDRY_RUN的规划planned_writes仍不写库。正式写入命令仅当处于刻意的服务端/Admin 上下文、且输入文件是确定性的时才可执行python3 backend/scripts/app_key_memory_grant_assignment_readiness.py \ --execute \ --allow-write \ --assignment-file /secure/admin/memory-app-key-memory-grants.json紧凑形式为python3 backend/scripts/app_key_memory_grant_assignment_readiness.py --execute --allow-write --assignment-file /secure/admin/memory-app-key-memory-grants.jsonCLI 参数还支持通过环境变量MEMORY_APP_KEY_MEMORY_GRANT_ASSIGNMENTS提供--assignment-file脚本 L12、L269-L273。分配文件形状[ { uid: uid, consumer: developer_api, app_id: developer-api, key_id: existing-key-id, scopes: [memories.read], default_read: true, archive_read: false, write: false, archive_default_visible: false } ]load_assignments接受 JSON 列表或{assignments: [...]}对象脚本 L234-L246。校验/分配契约拒绝一切非确定性输入结合源码可得到完整的校验规则集脚本 L13-L30、L61-L142写入不可达除非同时提供--execute、--allow-write与--assignment-file缺文件时返回statusDENIED且退出码 2。写入目标唯一只允许写users/{uid}/memory_control/app_key_memory_grants。文档内路径确定性grants.consumer.apps.app_id.keys.key_id。允许的消费者白名单developer_api、mcp、third_partyALLOWED_CONSUMERS。允许的持久化 scope 白名单memories.read、memories.write、memories.archive.readALLOWED_PERSISTED_SCOPES。能力—scope 强绑定default_readtrue必须含memories.readwritetrue必须含memories.writearchive_readtrue必须含memories.archive.read。Archive 不可默认可见archive_default_visible必须为false即使显式分配了archive_read能力Archive 也永不默认可见。字段白名单必需字段为uid/consumer/app_id/key_id/scopes/default_read/archive_read/write/archive_default_visible可选字段仅enabled、reason未知字段、未知消费者、未知 scope、畸形布尔值一律拒绝错误码如unknown_capability_or_field:field、unknown_consumer:consumer、unknown_scope:scope、field_must_be_boolean等。拒绝客户端/工具风格 scope如tool.search_memories这类 scope 会被拒绝不在ALLOWED_PERSISTED_SCOPES。禁止推断授权绝不从 MCP 广告元数据、客户端请求字段、或 MCP/Developer key scope 单独推断得出——持久化授权必须是显式分配的。写路径通过apply_assignments使用mergeTrue进行嵌套 patch 合并保留同一文档下其他 key 的既有授权脚本 L165-L171。诚实边界纪事文档明确该 runner 在本次 slice 中未对生产执行生产或本地都没有通过真实 Firestore 客户端分配过任何 app/key 授权。base_non_claims脚本 L174-L185也把未对生产执行、未分配授权、未声称线上规则/IAM 证明、Archive 永不默认可见、生产发布仍被 P0 门禁阻塞等作为默认输出的一部分防止误读。生命周期联动密钥创建/删除与授权种子除就绪 runner 外memory_app_key_grants.py 还提供了授权种子的编程式写入/删除接口保证密钥生命周期与授权状态一致seed_developer_api_key_memory_grant(uid, key_id, default_read..., write..., db_client...)为新建的 Developer API 密钥写入grants.developer_api.apps.developer_api.keys.key_id使用 merge 写保留其他密钥授权返回写入的文档路径L134-L173。seed_mcp_api_key_memory_grant(...)对应 MCP 消费者mcp/ 默认 appmcp-api的等价种子L176-L205。remove_developer_api_key_memory_grant(...)密钥删除时按字段路径删除嵌套条目对不存在授权文档的 legacy 密钥会先探测doc_ref.get().exists再决定是否更新避免update()抛NotFound把一次成功的密钥删除变成 500且 UUID key id 含连字符因此通过FieldPath(...).to_api_repr()转义嵌套路径L208-L246。这一点与 dev_api_key.py 中的密钥删除流程联动撤销 Developer API 密钥时同步调用remove_developer_api_key_memory_grant(user_id, key_id, db_clientfirestore_client)确保已删除密钥无法再通过内存授权门禁。总结与安全模型速览Friend 的 memory app/key grants 机制可以浓缩为一张判定路径图客户端请求 │ ▼ 认证层API Key → uid app_id key_id verified scopes │ ▼ 路由 scope 校验如 memories:read │ ▼ 服务端读取 users/{uid}/memory_control/app_key_memory_grants │ ▼ authorize_app_key_scope_memory_grantconsumer/app/key 三级匹配 │ │ │ 缺任一条件 │ 全部满足 ▼ ▼ 403fail closed 放行policy 不带 Archive 能力安全要点总结存储归一一个文档路径users/{uid}/memory_control/app_key_memory_grants承载全部外部消费者的按 app/key 授权读取 helper 可 fake 注入、显式传db_client。双闸门认证后的路由 scope 与持久化的授权 scope 缺一不可客户端请求字段、广告元数据、密钥 scope 都不能单独构成授权。规则隔离firestore.rules拒绝客户端读写自授权在本地模拟器测试中被断言拒绝。确定性分配就绪 runner 默认NOT_RUN写入需--execute --allow-write --assignment-file三重条件输入经过白名单与能力—scope 强绑定校验。Archive 收敛Archive 永不默认可见必须显式 Archive 意图 memories.archive.read操作授权 Archive 路由能力组合。诚实边界文档与脚本都明确记录未完成项与非主张避免把本地模拟器验证误认为生产证明。若你正为外部应用/MCP 客户端接入 Friend 的内存能力建议按以下顺序落地先在本地模拟器跑通npm run test:memory-app-key-grants-rules:emulator验证规则隔离再用就绪 runner 的 dry-run 模式校验分配文件最后在明确的 Admin 上下文中执行写入而对于仅需历史 uid 的旧路由ApiKeyAuth的向后兼容设计让你无需改动即可平滑过渡。【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考