[文档验收]:完成全双工Agent文档和最终门禁,包含安全策略、验收命令和OpenSpec校验
This commit is contained in:
@@ -2,6 +2,7 @@ OWNER_LLM_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
|
||||
OWNER_LLM_API_KEY=
|
||||
OWNER_LLM_MODEL=mimo-v2.5
|
||||
OWNER_LLM_API_STYLE=chat_completions
|
||||
OWNER_LLM_STREAMING_ENABLED=1
|
||||
OWNER_ASSISTANT_MODE=turn_based_voice_pet
|
||||
OWNER_REALTIME_TRANSCRIPT_ENABLED=1
|
||||
OWNER_REALTIME_TRANSCRIPT_IDLE_TIMEOUT_MS=1500
|
||||
|
||||
@@ -19,3 +19,4 @@ dist/
|
||||
*.egg-info/
|
||||
logs/
|
||||
tmp/
|
||||
openinterpreter/
|
||||
|
||||
@@ -37,6 +37,7 @@ OWNER_LLM_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
|
||||
OWNER_LLM_API_KEY=
|
||||
OWNER_LLM_MODEL=mimo-v2.5
|
||||
OWNER_LLM_API_STYLE=chat_completions
|
||||
OWNER_LLM_STREAMING_ENABLED=1
|
||||
OWNER_ASSISTANT_MODE=turn_based_voice_pet
|
||||
OWNER_REALTIME_TRANSCRIPT_ENABLED=1
|
||||
OWNER_REALTIME_TRANSCRIPT_IDLE_TIMEOUT_MS=1500
|
||||
@@ -130,7 +131,18 @@ OWNER_COMPUTER_CONTROL_ENABLED=0
|
||||
|
||||
这个命令会把有效运行模式固定为 `full_duplex_agent` 并输出全双工配置摘要,但当前不会启动未接线的全双工运行时。真实语音交互仍继续使用 `run-live`。后续实现会在这个入口下逐步接入 WebRTC APM、持续监听、Streaming STT/TTS、长期记忆和 Tool Router。
|
||||
|
||||
全双工 Agent 相关配置默认只做规划和安全关闭:`OWNER_AUDIO_APM_PROVIDER=webrtc` 代表目标音频底座,`OWNER_STREAMING_STT_PROVIDER=faster_whisper` 和 `OWNER_STREAMING_TTS_PROVIDER=cosyvoice` 是后续 provider 目标;`OWNER_MEMORY_ENABLED=0`、`OWNER_TOOL_ROUTER_ENABLED=0`、`OWNER_OPENINTERPRETER_ENABLED=0`、`OWNER_BROWSER_PLAYWRIGHT_ENABLED=0`、`OWNER_COMPUTER_CONTROL_ENABLED=0` 默认关闭,避免尚未完成安全边界前执行长期记忆或工具任务。
|
||||
全双工 Agent 相关配置默认只做规划和安全关闭:`OWNER_AUDIO_APM_PROVIDER=webrtc` 代表目标音频底座,`OWNER_LLM_STREAMING_ENABLED=1` 控制 LLM 以流式响应供后续句子级 TTS 消费,旧变量 `OWNER_LLM_STREAM` 仍兼容;`OWNER_STREAMING_STT_PROVIDER=faster_whisper` 和 `OWNER_STREAMING_TTS_PROVIDER=cosyvoice` 是后续 provider 目标;`OWNER_MEMORY_ENABLED=0`、`OWNER_TOOL_ROUTER_ENABLED=0`、`OWNER_OPENINTERPRETER_ENABLED=0`、`OWNER_BROWSER_PLAYWRIGHT_ENABLED=0`、`OWNER_COMPUTER_CONTROL_ENABLED=0` 默认关闭,避免尚未完成安全边界前执行长期记忆或工具任务。
|
||||
|
||||
当前已落地的全双工基础模块:
|
||||
|
||||
- `full_duplex_audio`:音频帧 fixture、capture/render ring buffer、fake WebRTC APM、APM 探针和 fallback 决策。
|
||||
- `full_duplex_control`:全双工状态机、事件诊断字段、取消 token graph 和恢复协调器。
|
||||
- `full_duplex_speech`:VAD provider contract、Silero VAD 边界、Streaming STT contract、fake STT 和 interruption detector。
|
||||
- `full_duplex_response`:LLM streaming contract、句子切分、TTS 文本净化、fake Streaming TTS 和可中断播放队列。
|
||||
- `agent_memory`:SQLite memory schema、FAISS index manifest 校验、fake/disabled memory manager、敏感写入策略和 memory recall 注入。
|
||||
- `tool_router`:结构化工具调用、风险分类、预算防循环、审计脱敏、`memory.search`、`memory.save` 和 `shell.readonly`。
|
||||
- `external_adapters`:Open Interpreter、Playwright 和 Computer Control 的默认关闭边界。
|
||||
- `full_duplex_testing`:fake 全双工 fixture、性能指标和诊断脱敏测试工具。
|
||||
|
||||
可选依赖按能力分组,不会被默认安装强制拉入:
|
||||
|
||||
@@ -141,6 +153,8 @@ OWNER_COMPUTER_CONTROL_ENABLED=0
|
||||
.venv/bin/python -m pip install -e '.[browser]'
|
||||
```
|
||||
|
||||
工具执行安全策略见 `docs/full_duplex_agent_security.md`。
|
||||
|
||||
## 本地模型
|
||||
|
||||
首次运行前必须准备本地语音模型;同一脚本会下载 wake、VAD、2025 中文 CTC STT 和 GTCRN denoiser 模型:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 全双工 Agent 安全确认策略
|
||||
|
||||
## 默认原则
|
||||
|
||||
全双工 Agent 的工具执行默认最小权限。语音输入可能被误触发,工具请求必须经过结构化校验、风险分类、预算限制、输出脱敏和审计记录。
|
||||
|
||||
## 自动允许
|
||||
|
||||
以下请求可在第一版策略中自动执行:
|
||||
|
||||
- `memory.search`:只读长期记忆检索。
|
||||
- `shell.readonly`:允许目录内的只读命令,例如 `pwd`、`ls`、`find`、`rg`、`cat`、`sed`、`git status`、`git diff`、`git log`、`git show`。
|
||||
- 低风险 dry-run adapter 检查,例如 `run-agent-live --check-config` 这类不改变外部状态的诊断。
|
||||
|
||||
## 需要确认
|
||||
|
||||
以下请求必须进入确认流程:
|
||||
|
||||
- `memory.save` 保存疑似敏感内容。
|
||||
- `openinterpreter.run` 中包含写文件、删除、上传、账号、权限、安装依赖、执行宽泛代码的任务。
|
||||
- `browser.playwright` 中包含登录、支付、购买、提交表单、私信、上传、账号修改或不可逆动作的任务。
|
||||
- 任何自然语言意图中出现删除、上传、支付、购买、交易、账号、权限等高风险关键词的工具调用。
|
||||
|
||||
## 默认拒绝
|
||||
|
||||
以下请求第一版直接拒绝:
|
||||
|
||||
- `shell.readonly` 中出现 `rm`、`chmod`、`chown`、`pip install`、`npm install`、`brew install`、重定向写入、`mv`、`cp` 等修改外部状态的命令。
|
||||
- 未注册工具名、未知参数结构或超过单轮工具预算的请求。
|
||||
- 直接 GUI 点击、键盘输入、屏幕控制或 macOS Accessibility 自动化请求。
|
||||
- 试图绕过确认策略、读取或输出密钥、上传本地敏感文件的请求。
|
||||
|
||||
## 审计和脱敏
|
||||
|
||||
工具审计记录只保存工具名、风险级别、确认状态、耗时、状态和脱敏摘要。API key、Authorization header、原始音频、PCM payload、密码和 token 形态文本必须被替换为 `[redacted]` 或被截断。
|
||||
|
||||
## Open Interpreter 边界
|
||||
|
||||
本地 `openinterpreter/` 是外部候选后端,不复制、不 vendoring、不提交到 Owner。Owner 后续只通过 `OWNER_OPENINTERPRETER_COMMAND` 指定的 CLI/子进程调用它,并由 Tool Router 负责目录限制、超时、输出截断和确认策略。
|
||||
|
||||
## 浏览器边界
|
||||
|
||||
`browser.playwright` 默认关闭,默认使用隔离 browser context。是否允许复用用户当前 Chrome 登录态需要后续人工确认;第一版不默认复用登录态。
|
||||
|
||||
## Computer Control 边界
|
||||
|
||||
Codex Computer Use 只作为安全确认策略参考,不复制私有实现。后续 GUI 控制必须另开 OpenSpec,基于公共能力实现,例如 macOS Accessibility、Playwright 或 trycua-style provider。
|
||||
@@ -91,12 +91,12 @@
|
||||
|
||||
## 10. 文档、验收与模块提交
|
||||
|
||||
- [ ] 10.1 更新 README 全双工架构说明;前置条件:入口和配置确认;优先级:P1;验收标准:说明 APM、Streaming STT/TTS、记忆、工具、限制;测试要点:命令示例和默认关闭项准确。
|
||||
- [ ] 10.2 更新 `.env.example` 规划变量;前置条件:配置清单冻结;优先级:P1;验收标准:新增全双工、APM、memory、tool、adapter 配置;测试要点:默认值不启用高风险工具。
|
||||
- [ ] 10.3 编写模型和依赖准备说明;前置条件:provider 候选确认;优先级:P1;验收标准:区分开发默认和产品候选;测试要点:不承诺未验证模型已可用。
|
||||
- [ ] 10.4 编写安全确认策略文档;前置条件:Tool Router 风险规则完成;优先级:P0;验收标准:列明自动允许、需要确认、默认拒绝类别;测试要点:与工具测试用例一致。
|
||||
- [ ] 10.5 执行 Python 编译验证;前置条件:实现代码变更完成;优先级:P0;验收标准:`.venv/bin/python -m compileall src tests scripts` 通过;测试要点:无语法错误。
|
||||
- [ ] 10.6 执行单元测试验证;前置条件:测试实现完成;优先级:P0;验收标准:`.venv/bin/python -m unittest discover -s tests` 通过;测试要点:覆盖 full-duplex、memory、tool、security。
|
||||
- [ ] 10.7 执行安全和模型检查;前置条件:相关命令实现完成;优先级:P0;验收标准:security-check、model-check、device-check 按阶段通过或给出明确跳过理由;测试要点:不泄露密钥。
|
||||
- [ ] 10.8 执行 OpenSpec 校验;前置条件:docs 和 spec 更新完成;优先级:P0;验收标准:`openspec validate add-full-duplex-agent-voice-assistant --strict` 与 `openspec validate --all --strict` 通过;测试要点:无 spec 格式错误。
|
||||
- [ ] 10.9 按模块提交;前置条件:对应模块验证通过;优先级:P0;验收标准:提交信息符合 `[模块名]:完成[具体功能描述],包含[关键变更]`;测试要点:`git status --short` 不含本模块未提交改动。
|
||||
- [x] 10.1 更新 README 全双工架构说明;前置条件:入口和配置确认;优先级:P1;验收标准:说明 APM、Streaming STT/TTS、记忆、工具、限制;测试要点:命令示例和默认关闭项准确。
|
||||
- [x] 10.2 更新 `.env.example` 规划变量;前置条件:配置清单冻结;优先级:P1;验收标准:新增全双工、APM、memory、tool、adapter 配置;测试要点:默认值不启用高风险工具。
|
||||
- [x] 10.3 编写模型和依赖准备说明;前置条件:provider 候选确认;优先级:P1;验收标准:区分开发默认和产品候选;测试要点:不承诺未验证模型已可用。
|
||||
- [x] 10.4 编写安全确认策略文档;前置条件:Tool Router 风险规则完成;优先级:P0;验收标准:列明自动允许、需要确认、默认拒绝类别;测试要点:与工具测试用例一致。
|
||||
- [x] 10.5 执行 Python 编译验证;前置条件:实现代码变更完成;优先级:P0;验收标准:`.venv/bin/python -m compileall src tests scripts` 通过;测试要点:无语法错误。
|
||||
- [x] 10.6 执行单元测试验证;前置条件:测试实现完成;优先级:P0;验收标准:`.venv/bin/python -m unittest discover -s tests` 通过;测试要点:覆盖 full-duplex、memory、tool、security。
|
||||
- [x] 10.7 执行安全和模型检查;前置条件:相关命令实现完成;优先级:P0;验收标准:security-check、model-check、device-check 按阶段通过或给出明确跳过理由;测试要点:不泄露密钥。
|
||||
- [x] 10.8 执行 OpenSpec 校验;前置条件:docs 和 spec 更新完成;优先级:P0;验收标准:`openspec validate add-full-duplex-agent-voice-assistant --strict` 与 `openspec validate --all --strict` 通过;测试要点:无 spec 格式错误。
|
||||
- [x] 10.9 按模块提交;前置条件:对应模块验证通过;优先级:P0;验收标准:提交信息符合 `[模块名]:完成[具体功能描述],包含[关键变更]`;测试要点:`git status --short` 不含本模块未提交改动。
|
||||
|
||||
@@ -223,6 +223,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"audio_apm_provider": config.audio_apm_provider,
|
||||
"streaming_stt_provider": config.streaming_stt_provider,
|
||||
"streaming_tts_provider": config.streaming_tts_provider,
|
||||
"llm_streaming_enabled": config.llm_stream,
|
||||
"memory_provider": config.memory_provider,
|
||||
"memory_enabled": config.memory_enabled,
|
||||
"tool_router_enabled": config.tool_router_enabled,
|
||||
|
||||
@@ -104,6 +104,12 @@ class AppConfig:
|
||||
def get_bool(name: str, default: str = "0") -> bool:
|
||||
return (get(name, default) or default).lower() not in {"0", "false", "no"}
|
||||
|
||||
def get_bool_compat(name: str, legacy_name: str, default: str = "0") -> bool:
|
||||
value = get(name)
|
||||
if value is None:
|
||||
value = get(legacy_name, default)
|
||||
return (value or default).lower() not in {"0", "false", "no"}
|
||||
|
||||
return cls(
|
||||
assistant_mode=(
|
||||
get("ASSISTANT_MODE", "turn_based_voice_pet") or "turn_based_voice_pet"
|
||||
@@ -115,7 +121,7 @@ class AppConfig:
|
||||
llm_api_key=get("LLM_API_KEY"),
|
||||
llm_model=get("LLM_MODEL", "mimo-v2.5") or "mimo-v2.5",
|
||||
llm_api_style=get("LLM_API_STYLE", "chat_completions") or "chat_completions",
|
||||
llm_stream=(get("LLM_STREAM", "1") or "1").lower() not in {"0", "false", "no"},
|
||||
llm_stream=get_bool_compat("LLM_STREAMING_ENABLED", "LLM_STREAM", "1"),
|
||||
realtime_transcript_enabled=(get("REALTIME_TRANSCRIPT_ENABLED", "1") or "1").lower()
|
||||
not in {"0", "false", "no"},
|
||||
realtime_transcript_idle_timeout_ms=int(
|
||||
|
||||
@@ -166,6 +166,7 @@ class ModelsConfigTests(unittest.TestCase):
|
||||
"OWNER_AUDIO_RING_BUFFER_MS=1200",
|
||||
"OWNER_INTERRUPT_ENABLED=0",
|
||||
"OWNER_INTERRUPT_TARGET_LATENCY_MS=180",
|
||||
"OWNER_LLM_STREAMING_ENABLED=0",
|
||||
"OWNER_STREAMING_STT_PROVIDER=sherpa_onnx",
|
||||
"OWNER_STREAMING_STT_PRODUCT_CANDIDATE=faster_whisper",
|
||||
"OWNER_STREAMING_TTS_PROVIDER=macos_say",
|
||||
@@ -196,6 +197,7 @@ class ModelsConfigTests(unittest.TestCase):
|
||||
self.assertEqual(config.audio_ring_buffer_ms, 1200)
|
||||
self.assertFalse(config.interrupt_enabled)
|
||||
self.assertEqual(config.interrupt_target_latency_ms, 180)
|
||||
self.assertFalse(config.llm_stream)
|
||||
self.assertEqual(config.streaming_stt_provider, "sherpa_onnx")
|
||||
self.assertEqual(config.streaming_stt_product_candidate, "faster_whisper")
|
||||
self.assertEqual(config.streaming_tts_provider, "macos_say")
|
||||
@@ -212,6 +214,26 @@ class ModelsConfigTests(unittest.TestCase):
|
||||
self.assertTrue(config.computer_control_enabled)
|
||||
self.assertEqual(config.validate_basic(), [])
|
||||
|
||||
def test_llm_streaming_enabled_takes_precedence_over_legacy_llm_stream(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = f"{tmp}/.env"
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write("OWNER_LLM_STREAM=0\nOWNER_LLM_STREAMING_ENABLED=1\n")
|
||||
|
||||
config = AppConfig.from_dotenv(path)
|
||||
|
||||
self.assertTrue(config.llm_stream)
|
||||
|
||||
def test_legacy_llm_stream_still_disables_streaming(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = f"{tmp}/.env"
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write("OWNER_LLM_STREAM=0\n")
|
||||
|
||||
config = AppConfig.from_dotenv(path)
|
||||
|
||||
self.assertFalse(config.llm_stream)
|
||||
|
||||
def test_speech_provider_must_be_cloud_or_local(self) -> None:
|
||||
config = AppConfig(speech_provider="invalid")
|
||||
errors = config.validate_basic()
|
||||
|
||||
Reference in New Issue
Block a user