Melmonster13/ai-assistant-demo

GitHub: Melmonster13/ai-assistant-demo

一个以安全第一为原则的 Jarvis 风格个人 AI 助手,通过权限网关、单次使用 JWT、MCP 工具指纹验证和 pgvector 记忆系统实现可信的工具调用与语音交互。

Stars: 0 | Forks: 0

# ai-assistant-demo 一个个人的、Jarvis 风格的 AI 助手 —— 语音和交互式 UI 作为平等的交互端,以安全第一的原则构建:权限网关、token 代理和审计日志存在于它们可能无法保护的一切之前。 ![架构:三个客户端终止于一个 orchestrator,该 orchestrator 生成 Ed25519 权限凭条,并在信任边界内仅通过每个工具各自的验证代理来访问它们。](https://static.pigsec.cn/wp-content/uploads/repos/cas/13/13966fad99086d59c94e42fb1e4a61d0fde791af6d871835b1eb91abe765cacf.svg) ## 状态:第 5/6 阶段(语音)—— 已完成 语音输入和输出,完全本地的音频:唤醒词(通过 openWakeWord 识别“hey jarvis”)→ faster-whisper STT → 浏览器使用的同一个 web API → Piper(或 macOS `say`)TTS 回传。这是一个已经在运行的系统的第四个客户端,而不是一个新的子系统 —— 语音客户端只需要助手的 URL,因此它可以运行在与 orchestrator 不同的机器上。 | 阶段 | 构建 | 状态 | |---|---|---| | 1 —— 安全主线 | Orchestrator 循环、模型适配器、JWT 验证的模拟工具、CLI 确认、审计日志 | ✅ | | 2 —— 通过 MCP 接入真实工具 | MCP 客户端、工具指纹注册表(TOFU + 漂移检测)、分级 token | ✅ | | 3 —— 记忆 | pgvector 事实存储(RLS)、persona 加载器、embedding 适配器 | ✅ | | 4 —— UI | Web 聊天、确认卡片、只读的 persona/记忆浏览器 | ✅ | | 5 —— 语音 | 唤醒词 → 本地 STT → orchestrator → 本地 TTS,语音确认 | ✅ | | 6 —— 部署 | 拆分至目标拓扑;开发环境设计为单机 | — | ## 网关的工作原理 三个导入隔离的包构成了一个真正的信任边界: - **`src/assistant`** —— orchestrator 端:手动控制的工具调用循环(唯一的执行点)、模型适配器、CLI 确认、指纹注册表,以及一个生成按风险分级的 Ed25519 签名 JWT 权限凭条的代理:破坏性调用获取的是一次性的、绑定参数的、寿命仅有几秒的 token,且仅在用户确认后生成;只读调用使用的是寿命较长的、限定于特定工具的 token。每一步都会审计其触发因素(`user_request` 还是 `tool_output` —— 工具输出是不可信的,会重新进入同一个网关)。 - **`src/toolwrapper`** —— 一个验证型的 HTTP 代理,它生成并拥有一个 stdio MCP server 作为子进程,仅持有公钥。没有任何路径可以绕过它访问工具:发现和每次调用都通过 wrapper,它会验证签名、过期时间、级别和工具绑定 —— 此外,在高层级还会验证参数绑定并消耗原子的、单次使用的 `jti`。每个边界都强制执行其各自配置的级别,因此无论 orchestrator 生成了什么,低级别 token 永远无法授权破坏性调用。它可以验证但在结构上无法生成;orchestrator 可以生成但从不验证自己的 token。 - **`src/mcpservers`** —— 实际的 stdio MCP server(`notes`:只读;`files`:破坏性的,沙盒限制在根目录下),使用明确的环境变量允许列表生成,而不是继承 wrapper 的环境。 工具定义在首次使用时受信任:每个工具的名称 + 描述 + input schema 都会被指纹化并需要一次性批准。每次重新连接时都会重新检查指纹;更改后的定义(即 MCP 的“rug pull”式攻击)会记录新旧对比,需要明确重新批准,并且即使重新批准,在整个会话期间也会强制要求确认。 ## 记忆 记忆是一个直接集成(`src/assistant/memory/`),而不是一个 MCP 工具 —— 它是内部组件,没有单独的信任边界。分为两个层级: - **事实召回(Fact-recall)** —— 一个以 `user_id` 为键的 Postgres + pgvector 存储。强制执行行级安全(Row-level security),而不仅仅是声明:记忆连接时使用专用的非超级用户角色,并且每个操作都会绑定 `app.current_user_id`,因此即使 SQL 省略了 `WHERE`,RLS 策略也会在数据库本身中将每个查询限制为该用户的行。相关事实通过语义相似度在每轮对话中自动召回,并注入到 system prompt 中;模型使用 `remember_fact` 工具持久化新事实(这是一种内部低风险的写入 —— 会进行审计,但无需确认或 token)。 - **Persona/配置(profile)** —— 存放在文件夹中手工编写的 markdown(这是同步保险库的开发替代品),拼接到 system prompt 中以塑造语调。 Embedding 是由配置选择的适配器:`local` 使用一个小型本地 sentence-transformers 模型(私密,无需 API key;`uv sync --extra local-embeddings`),`hashing` 是零依赖的备选方案。两者都会生成 384 维的向量,因此该 schema 是独立于后端的。 ## 语音 `src/voiceclient` 与其他所有内容都是导入隔离的,并且仅通过 HTTP 访问 orchestrator —— 也就是浏览器使用的同一个 `/api/chat`。所有音频都保留在设备上:openWakeWord 在本地对唤醒词进行评分,faster-whisper 在本地转录,Piper 在本地合成(`say` 是 macOS 上零配置的备选方案)。后端属于配置,而非架构 —— 每一个都是可替换的适配器。 确认机制在构造上与模态无关:破坏性工具调用会落入共享的决策队列,浏览器按钮和语音客户端都会轮询同一个待处理列表 —— 语音客户端大声读出请求并监听 yes/no,无论哪个交互端先回答即为最终决定。对于有歧义的语音回答,会再询问一次,然后拒绝;未回答的请求将在超时后被拒绝。网关始终保持失败即关闭。 ``` uv sync --extra voice uv run assistant-web # the voice client talks to this uv run assistant-voice # dev defaults: push-to-talk + `say` # 目标 stack: WAKE_BACKEND=openwakeword, TTS_BACKEND=piper (参见 .env.example) ``` ## 设置说明 需要 [uv](https://docs.astral.sh/uv/) 和 Docker。 ``` uv sync # add --extra local-embeddings for the local embedding model uv run python scripts/generate_keys.py # dev Ed25519 keypair → keys/ (gitignored) docker compose up -d # Postgres on host port 5433, schema auto-applied cp .env.example .env # then add your ANTHROPIC_API_KEY ``` 默认的 `EMBEDDING_BACKEND=local` 需要 `local-embeddings` 扩展;将其设置为 `hashing` 即可在没有额外依赖项的情况下运行。 ## 运行说明 ``` uv run tool-wrapper notes # terminal 1: read-only notes server (low tier) uv run tool-wrapper files # terminal 2: destructive files server (high tier) uv run assistant # terminal 3: CLI chat — or: uv run assistant-web # web UI on http://127.0.0.1:8080 (localhost only) ``` 首次运行时会提示对每个发现的工具进行一次性批准(TOFU)。破坏性工具调用在生成 token 之前需要确认 —— 在 CLI 中是 `Allow? [y/N]`,在 Web UI 中是一张 Allow/Deny 卡片;读取操作则不需要。在 Web UI 中,未回答的确认请求会在超时后被拒绝。 要在没有 API key 的情况下尝试 Web UI:`uv run python scripts/preview_ui.py` 会在 8090 端口上提供服务,并配备一个脚本化模型和真实的 wrapper/记忆。 ## 测试 ``` uv run pytest ``` [tests/test_bypass.py](tests/test_bypass.py) 证明了破坏性工具在没有确认过的、单次使用的、未过期的 token 的情况下无法运行 —— 针对接入真实 MCP server 的在线 wrapper 发起了七次绕过尝试(无 token、伪造签名、已过期、重放 `jti`、篡改参数、错误的工具 token、在高级别边界使用低级别 token),每一次尝试都断言没有任何东西到达文件系统。[tests/test_registry.py](tests/test_registry.py) 涵盖了 TOFU 和 rug-pull 漂移;[tests/test_tiering.py](tests/test_tiering.py) 涵盖了两个层级的 token 规则;[tests/test_orchestrator.py](tests/test_orchestrator.py) 进行了端到端的循环运行测试。[tests/test_memory.py](tests/test_memory.py) 涵盖了语义召回和 RLS 用户隔离(包括原始的、未过滤的 `SELECT` 仍然无法跨用户);[tests/test_memory_orchestrator.py](tests/test_memory_orchestrator.py) 涵盖了 persona 注入、自动召回和 `remember_fact` 路径。[tests/test_webui.py](tests/test_webui.py) 涵盖了决策队列的失败即关闭语义,以及通过 HTTP 进行的端到端 allow/deny 确认流程。[tests/test_voice.py](tests/test_voice.py) 使用脚本化的音频后端,在同一个网关下驱动了语音循环 —— 语音 yes 会执行,语音 no 不会执行,而歧义答案会失败即关闭。如果 Postgres 未运行,测试将被跳过。 ## 致谢 在代码生成和项目脚手架方面使用了 Claude Code (Anthropic) 的协助。 所有的架构、安全设计和威胁模型决策均为本人完成。
标签:AI助手, MCP工具调用, 安全架构, 本地语音, 权限控制, 测试用例, 语音助手, 请求拦截, 逆向工具