Skip to content

安全模型 ​

Familiars 的安全架构实现细节。涵盖风险分级、审批闸门、敏感信息检测管线、加密原语、设备认证、Bridge Token 鉴权、沙箱权限。 来源:src/tools/security.py、src/tools/sensitive_detector.py、src/base/crypto.py、src/base/auth/、src/base/bridge_token.py、src/tools/turn_secrets.py、SECURITY.md。


1 · 风险分级体系 ​

1.1 四级风险桶 ​

src/tools/security.py 定义四个风险等级,每个等级映射一个整数:

等级数值语义典型工具
low0纯查询,无副作用ls、get_file_info、read_text_file、find_files、grep_files、list_archive、list_my_processes、screen_size、clipboard_get_type、network、host_status、wait、*_status/*_list
medium1有副作用但可逆write、edit、compress、decompress、screenshot、monitor_start、browser_open、web_search、create_schedule、fetch_email_*、clipboard_paste_image、open_with_default_app、run_ffmpeg、run_pandoc、http_request、所有 generate_pdf/word/ppt/excel、所有 word_*/ppt_*/excel_* 细粒度构建器
high2不可逆或高危delete_file、execute_shell_script、execute_python_file、execute_node_file、run_pip、run_npm、run_git、open_app、kill_process、download、web_fetch、web_dive、move_or_rename、sqlite_query、ssh_exec、scp_upload、social_send、social_connect/disconnect、send_email、configure_email、generate_image/video、shut_down、lock_screen、operate、所有 mouse_*/keyboard_*(除 mouse_position)、所有 browser_click/type/eval_js/close_tab、clipboard_copy/paste/clear/copy_image/copy_files、start_screen_record、sign_pdf、hang_up、set_audio_routing、run_call_conversation
super99不可逆且对人影响——无论用户阈值如何都强制弹审批dial_number(真实电话呼叫)

super 等级的设计:故意远离 high(数值 99 vs 2),使得用户阈值(最高只能升到 high=2)永远无法覆盖。dial_number 每次呼叫都弹审批——即便 intense_mode=True、FAMILIARS_APPROVAL_DISABLED=1、本轮白名单——全部失效。

1.2 TOOL_RISK 字典 ​

TOOL_RISK: dict[str, str] 是单一真相源,约 150+ 工具方法名 → 风险等级。未列出的工具默认为 low。wrap_class_methods(cls) 在工具类加载时遍历类方法,自动给每个在 TOOL_RISK 中有名字的方法包上 @requires_approval 装饰器。

1.3 基于评分的动态风险升级 ​

静态分级之外,每次工具调用都经过 _compute_effective_risk(tool_name, base_risk, kwargs) 做动态升级:

  1. 取工具的 base score(来自 TOOL_BASE_SCORE 字典;未列出的用 _score_from_level(base_risk) 转为 {low:10, medium:40, high:70, super:95})
  2. 对 kwargs 里所有字符串值(递归至深度 4)跑一组轻量正则(_ARG_SCAN_PATTERNS,延迟编译并缓存),命中则累加 bonus score
  3. 总分映射:[0,30)=low、[30,60)=medium、[60,90)=high、90+=super

只升级、不降级。例如 write(path, content) 的静态 base=45(medium);若 content 含银行账号(cn-bank-card bonus=50),总分=95 → 升级为 super。

正则扫描规则及 bonus:

规则正则bonus
aws-access-keyAKIA[0-9A-Z]{16}50
github-patghp_[A-Za-z0-9]{36}50
openai-keysk-(proj-)?[A-Za-z0-9_-]{20,}50
anthropic-keysk-ant-[A-Za-z0-9_-]{20,}50
google-api-keyAIza[0-9A-Za-z_-]{35}50
private-key-pemBEGIN [A-Z ]*PRIVATE KEY50
jwteyJ...{10,}\..{10,}\..{10,}40
password-assign(password|pwd|secret)[:=]...{6,}40
cn-mobile1[3-9]\d{9}30
cn-id-card\d{17}[\dXx]45
cn-bank-card\d{16,19}50
enc-tokenENC::25

2 · 审批闸门机制 ​

2.1 装饰器 @requires_approval ​

requires_approval(level=None) 是装饰器工厂。包装后的函数执行流程:

工具调用
  │
  ├─ risk == "super" → 直接走 _ask_user("super") → 拒绝则 raise ToolDenied
  │
  ├─ demo gate 已装(匿名 demo bridge) → _demo_gate() 返回合成响应或 fall through
  │
  ├─ 全局禁用 / intense_mode / turbo_mode / operate_scope → 直接执行
  │
  ├─ skill-scope 已授权 → 直接执行
  │
  ├─ 本轮白名单("本轮总是允许") → 直接执行
  │
  └─ 正常路径:
       effective_risk = _compute_effective_risk(...)
       if effective_risk < user_threshold → 执行
       else → _ask_user(effective_risk) → 批准则执行 / 拒绝则 raise ToolDenied

2.2 用户阈值 security_level ​

UserConfig.security_level(即 DialogConfig.security_level,默认 "medium")决定阈值:

用户选择阈值含义
low0所有工具(low+medium+high+super)都弹审批
medium(默认)1medium+high+super 弹审批,low 直接执行
high2只有 high+super 弹审批(最宽松)

_user_threshold() 内部用 min(threshold, _LEVELS["high"]) 确保阈值永远不超过 high——super 是框架保留的,无法被任何用户设置覆盖。

2.3 审批请求通道 ​

_ask_user() 通过 CommClient.request_approval() 向进程内 CommHub 投递 ApprovalRequest,然后无限等待用户响应(timeout=None)。早期的 FAMILIARS_APPROVAL_TIMEOUT 已移除——超时不再默认拒绝(避免 ~10h word_new 卡死事件重演)。

_resolve_actor_for_approval() 有三级回退找 agent/dialogue id:

  1. security 模块的 ContextVar _current_agent / _current_dialogue
  2. observability 模块的 agent_id_var / dialogue_id_var
  3. 广播(agent_id=0)+ 日志 WARNING + 可选堆栈(FAMILIARS_DIAG_ACTOR_DRIFT 默认开启)

2.4 环境变量开关 ​

环境变量效果
FAMILIARS_APPROVAL_DISABLED=1全局短路审批门——所有工具直接执行(仅开发/批量用)
FAMILIARS_DIAG_ACTOR_DRIFT=0关闭 misrouted 审批的堆栈追踪(生产降噪)

2.5 intense_mode / turbo_mode ​

  • intense_mode:全局 / 单 agent / WeChat/QQ trigger 临时都可设为 True → 所有工具直接执行(包括 high),super 仍然弹。由 bind_actor() 在每个 turn 开始时从 config.json overlay 快照到 ContextVar _current_intense。
  • turbo_mode:跳过审批但保留 plan 验证和 ask_user_questions。与 intense_mode 互斥。
  • operate_scope():operate() 工具内部用 context manager 设 _in_operate=True,审批 operate 本身即授权其内部所有子工具调用。

2.6 skill-scope 授权 ​

authorize_skill(agent_id, skill_id) 记录用户一次性批准。之后该 skill 的所有步骤(通过 _current_skill_id ContextVar 识别)都绕过审批。ensure_skill_approved() 在 skill 启动时调用一次,展示步骤列表 + 聚合风险(max(step.tool_binding.risk))。revoke_skill_authorization() 在设置 UI 撤销授权或 skill 被删除时调用。

2.7 本轮白名单 ​

allow_tool_for_turn(agent_id, tool_name) 在用户点击"本轮总是允许"时调用。bind_actor() 在每个 turn 开始时清空该 agent 的白名单(_always_allow.pop(agent_id)),确保白名单不跨 turn 残留。

2.8 ToolDenied 异常 ​

ToolDenied(PermissionError) —— 继承 PermissionError,工具循环可以看到干净的异常类,格式化回模型作为 role:tool content 字符串。


3 · 敏感信息检测管线 ​

3.1 三层检测架构 ​

src/tools/sensitive_detector.py 的 SensitiveDetector 按顺序执行三层检测,每层的 findings 取并集后去重(_dedup 优先 layer 早 + score 高):

层引擎擅长延迟
Layer 1: gitleaks二进制 gitleaks detect(external/gitleaks)或内嵌正则回退高置信凭据/令牌/密钥(AWS key、GitHub PAT、JWT、PEM 私钥…)二进制 ~1-3s;正则 <10ms
Layer 2: presidioMicrosoft Presidio + spaCy NER(en+zh 双语)PII 实体(PERSON、EMAIL_ADDRESS、PHONE_NUMBER、CREDIT_CARD、IBAN_CODE、US_SSN…)首次 30s-10min(spaCy 冷启动);热路径 ~50-200ms
Layer 3: mini-LLM当前已禁用(no-op)。用户选择不跑本地小模型做检测。符号保留供 recipe override 接入项目代号、自定义 ID、雇主名等长尾N/A

Layer 1 回退正则:_GITLEAKS_FALLBACK 包含 12 条正则(aws-access-key、aws-secret-key、github-pat、github-fine-grained、openai-key、anthropic-key、slack-token、google-api-key、private-key-pem、jwt、generic-bearer、password-assignment)。

中文本地 PII 正则:_LOCALE_PII_FALLBACK 包含 3 条(cn-mobile、cn-id-card-18、cn-bank-card),各自有验证器(Luhn 校验银行账号、18 位身份证形状校验)抑制误报。

Layer 2 多语言引擎:优先尝试加载 zh_core_web_lg/md/sm/trf + en_core_web_lg/md/sm。若 zh 模型不可用则退到 en-only(不使用 AnalyzerEngine() 默认构造——它会硬编码触发 en_core_web_lg 自动下载)。_has_cjk() 检测输入文本决定默认语言。spaCy 自动下载被 _disable_spacy_auto_download() 全局屏蔽(spacy.cli.download 替换为 no-op)。

3.2 mask_secrets(快速脱敏) ​

mask_secrets(text) 是纯正则的快速通道——只跑 Layer 1 的 gitleaks 回退 + locale PII,不碰 Presidio 和 mini-LLM。用于:

  • memorizer prompt 输入
  • audit log 摘录
  • shadow dump
  • 任何进入云端 LLM 前的文本(ConversationSession.call(scrub=True))

命中 span 替换为 [REDACTED-<rule>],如 [REDACTED-github-pat]、[REDACTED-cn-mobile]。

3.3 scrub_text / unscrub_text(占位符替换管线) ​

scrub_text(text) → (scrubbed_text, [(placeholder, original_value), ...]) 把敏感 span 替换为 «SECRET_N» 占位符,返回映射表。unscrub_text 从占位符恢复原文。映射是 call-scoped(返回给调用者,用于 LLM 回复中的还原)。Mini-LLM 故意不在此路径运行——scrub 在 LLM 热路径上,必须同步且廉价。

3.4 commit 管线 ​

检测到 finding 后,SensitiveDetector.commit() 把每个 finding 通过 MemoryWriter._save_secrets_store 加密落盘到 workspace/SECRET.json:

  1. 命名:_name_secret() 是纯机械命名(无 LLM)——key = "<sanitized_rule>.<kind>"(如 github_pat.credential),附带 context_hint(局部摘录,原文用 <VALUE> 遮罩)
  2. 加密:crypto.encrypt_value(finding.value) → Fernet 加密
  3. 存储:store[key] = {type, aliases, secrets: {field_name: ciphertext}, context_hint, fingerprints: {field_name: {value_sha256, scope, source_url, cookie...}}}

commit_with_namings() 接受预计算的 namings(避免命名 LLM 付两次费),支持 extras 参数写入 fingerprints(浏览器凭据捕获用来记 cookie scope/domain/name/path)。

revert_commit() 回滚:删除字段→空条目则删 key。用于 Case-B undo toast。

3.5 辅助查询 ​

  • known_fingerprints() → 返回 SECRET.json 里所有 value_sha256,用于捕获流程跳过已存值
  • known_hosts() → 返回所有有存储凭据的 hostname(从 fingerprints[*].source_url 和 cookie.domain 解析)
  • iter_cookie_entries() → 解密所有 cookie-scoped 条目,返回 playwright.context.add_cookies() 可直接消费的格式

4 · 加密原语 ​

4.1 HKDF 密钥派生 ​

src/base/crypto.py 实现机器绑定的对称加密:

hostname + "|" + username + "|" + salt_bytes
    → HKDF-SHA256(salt=workspace/.salt, info=b"familiars-secrets-v1", length=32)
    → 32-byte raw key
    → base64url encode
    → Fernet key
  • HKDF info 字符串:b"familiars-secrets-v1" —— 版本化的加密原语。轮换它会使 workspace/SECRET.json 里所有密文永久失效。
  • salt:workspace/.salt,首次使用时 os.urandom(32) 生成并持久化。
  • 机器身份:platform.node() + os.getenv('USER', os.getenv('USERNAME', 'unknown'))。
  • Fernet:AES-128-CBC + HMAC-SHA256,每次加密自带 IV + timestamp + HMAC。
  • 密钥缓存:_cached_key 进程内派生一次后复用。

4.2 安全边界 ​

  • SECRET.json 单独拷给别人 → 解不开(缺机器身份)
  • 整机克隆但没拷 .salt → 解不开
  • SECRET.json + .salt 都拷但主机名/用户名变了 → 解不开
  • 重装系统 / 改用户名 / 换电脑 → 自己也解不开,须提前备份

4.3 encrypt_value / decrypt_value ​

python
from src.base.crypto import encrypt_value, decrypt_value

ciphertext = encrypt_value("my_github_password")  # → "gAAAAAB..."
plaintext = decrypt_value(ciphertext)               # → "my_github_password"

decrypt_value 抛出 cryptography.fernet.InvalidToken 如果密文损坏、key 不匹配或 TTL 过期(默认无 TTL)。


5 · 设备认证 ​

5.1 设备指纹生成 ​

src/base/auth/device.py:get_device_id() 跨平台生成稳定设备指纹:

平台硬件标识MAC主机名
macOSIOPlatformSerialNumber(ioreg 读)ifconfig en0socket.gethostname()
WindowsMachineGuid(HKLM\SOFTWARE\Microsoft\Cryptography,OS 安装时生成,跨升级存活)psutil.net_if_addrs() 首个非回环非全零 MAC同
Linux/etc/machine-id 或 /var/lib/dbus/machine-idpsutil.net_if_addrs()同

所有部分用 "|" 拼接 → SHA-256 取前 16 hex → dev_<16hex>。

所有硬件查询失败时回退到 hostname alone(明确权衡:两台共享 DESKTOP-XYZ 默认名的机器可能碰撞,但永远不在敌对机器上 raise)。

5.2 AuthManager ​

src/base/auth/manager.py:AuthManager 单例管理本地用户身份:

  • 存储:workspace/auth.json(目前明文 JSON)
  • register(nickname="") → 首次创建身份:user_id = "u_" + sha256(device_id)[:12],device_token = "dt_" + secrets.token_hex(16)
  • login() → 验证 auth.json 里的 device_id 与当前 get_device_id() 一致
  • 遗留 wallet key 被 load() 静默忽略

workspace/auth.json 结构:

json
{
  "identity": {
    "user_id": "u_a1b2c3...",
    "device_id": "dev_abcdef...",
    "device_token": "dt_0123456789abcdef...",
    "created_at": "2026-08-29T...",
    "nickname": ""
  }
}

6 · Bridge Token 鉴权 ​

6.1 生成与存储 ​

src/base/bridge_token.py:首次启动生成 256-bit token(secrets.token_hex(32)),写入 workspace/.bridge_token,尝试 chmod 0o600(Windows 上 best-effort 忽略 OSError)。进程内 _cached 缓存。

6.2 验证 ​

  • 每个 HTTP 请求必须携带 X-Bridge-Token header 或 ?token=... query 参数
  • verify_bridge_token(presented) 用 secrets.compare_digest 做常量时间比较
  • /health 是唯一免 token 的端点
  • WebSocket 连接在 accept() 之前校验 ?token=...,未带 token 或错误 → 4001 拒绝(避免触发 5GB VRAM TTS 加载)

6.3 安全边界 ​

浏览器里的随机标签页:

  • 无法读取 workspace/.bridge_token(不在任何 web-serving root 下)
  • 无法通过自签证书白名单(workspace/.bridge.crt)

剩余风险:能读 workspace 目录的本机进程(IDE、同步盘、备份客户端、恶意扩展)可读走 .bridge_token → 冒充 Electron 前端调用任意 API。缓解:.bridge_token 不要被 git add;workspace/ 不要设为同步盘目录。

6.4 HTTPS 传输 ​

Bridge 默认用 hypercorn + HTTPS/2 + 自签证书(workspace/.bridge.crt + .bridge.key,首次启动自动生成)。FAMILIARS_HTTP_INSECURE=1 回退到明文 HTTP + uvicorn(诊断/回退路径)。


7 · Turn Secrets(最后一公里替换) ​

src/tools/turn_secrets.py 解决"模型需要调用 get_user_secret 拿到明文但不让明文进模型上下文"的问题:

7.1 生命周期 ​

new turn → begin_turn(agent_id, dialogue_id, turn_id)
模型调用 get_user_secret(name) → register(ciphertext, plaintext) → 返回 "ENC::gAAAA..." 给模型
模型调用 foo(token="ENC::gAAAA...") → substitute_args → 替换为明文 → foo(plaintext)
turn 结束 → end_turn() → 清零 registry(覆盖值后 drop ref)

7.2 实现 ​

  • TurnSecrets 持有 mapping: dict[str, str](token → plaintext)
  • CIPHERTEXT_PREFIX = "ENC::" —— 所有注册 token 带此前缀,dispatcher 快速判断"本轮无 secret → 跳过 arg walk"
  • 按 (agent_id, dialogue_id, turn_id) 做 ContextVar key,并发生成不串台
  • substitute_in(value, mapping) 递归替换 list/dict/str 中的 ciphertext token,最长优先匹配避免部分重叠
  • clear() 先把所有 value 覆盖为 "" 再 drop——best-effort scrub

8 · 沙箱权限 ​

8.1 PERMANENT_DENY ​

src/tools/security.py 的 PERMANENT_DENY 列表包含真正的逃逸原语(ctypes/pickle/importlib 等),这些永远不可请求。

8.2 可请求权限 ​

os/subprocess/requests 等模块通过 capabilities.extra_modules 声明 + per-(recipe, agent) 审批后可用。ast_guard.py 和 worker.py 镜像 DENY 列表。

8.3 进程闸门 ​

kill_process 只允许 kill _spawned_pids 集合中的 PID(agent 自己通过 open_app 启动的进程)。用户自己开的 chrome → 拒绝。防止 agent 幻觉误关。


9 · 数据上云防线总结 ​

防线位置作用
mask_secrets每个进入 LLM 的文本之前gitleaks 正则 + locale PII → [REDACTED-*]
scrub_textConversationSession.call占位符替换 + call-scoped 还原
sensitive_detector对话内敏感信息发现三层检测 → 加密 commit 到 SECRET.json
turn_secretsToolDispatcher dispatchENC:: token → 明文,仅进程内替换
审批门每个工具函数入口风险分级 + 用户审批
Bridge TokenHTTP/WS 中间件256-bit token 阻止外部调用
HTTPS/2传输层自签证书防中间人
HKDF + Fernet磁盘存储机器绑定的 AES-128-CBC + HMAC

用户级文档见仓库根 SECURITY.md(11 章节)。安全漏洞报告流程见 SECURITY_REPORT.md。

Familiars · 多 Agent AI 桌面应用