diff --git a/plugins/debug/copilot_sdk_research/GUIDE_ERRATA.md b/plugins/debug/copilot_sdk_research/GUIDE_ERRATA.md new file mode 100644 index 0000000..2443e4a --- /dev/null +++ b/plugins/debug/copilot_sdk_research/GUIDE_ERRATA.md @@ -0,0 +1,98 @@ +# 开发指南勘误与更新 + +## 权限控制章节修正(第 2.2 节) + +### ⚠️ 关键勘误 + +在实际测试中发现,Shell 权限请求使用的是 **`fullCommandText`** 字段,而非文档中提到的 `command` 字段。 + +### 需要修改的代码行 + +**第 89 行(错误):** + +```python +command = request.get("command", "") +``` + +**应改为(正确):** + +```python +command = request.get("fullCommandText", "") or request.get("command", "") +``` + +### 完整的正确实现 + +```python +async def on_user_permission_request(request, context): + """ + 统一权限审批网关 + """ + kind = request.get("kind") # shell, write, mcp, read, url + # ✅ 正确:使用 fullCommandText(shell)或 command(其他) + command = request.get("fullCommandText", "") or request.get("command", "") + + # 1. 超级模式:全部允许 + if self.valves.PERMISSIONS_ALLOW_ALL: + return {"kind": "approved"} + + # 2. 默认安全:始终允许 "读" 和 "Web浏览" + if kind in ["read", "url"]: + return {"kind": "approved"} + + # 3. 细粒度控制 + if kind == "shell": + if self.valves.PERMISSIONS_ALLOW_SHELL: + return {"kind": "approved"} + + pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN + if pattern and command: + try: + if re.match(pattern, command): + return {"kind": "approved"} + except re.error: + print(f"[Config Error] Invalid Regex: {pattern}") + + if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE: + return {"kind": "approved"} + + if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP: + return {"kind": "approved"} + + # 4. 默认拒绝 + print(f"[Permission Denied] Blocked: {kind} {command}") + return { + "kind": "denied-by-rules", + "rules": [{"kind": "check-openwebui-valves"}] + } +``` + +### Shell 权限请求的完整结构 + +```json +{ + "kind": "shell", + "toolCallId": "call_xxx", + "fullCommandText": "ls -la", // ← 关键字段 + "intention": "List all files and directories", + "commands": [ + { + "identifier": "ls -la", + "readOnly": false + } + ], + "possiblePaths": [], + "possibleUrls": [], + "hasWriteFileRedirection": false, + "canOfferSessionApproval": false +} +``` + +## 测试验证 + +已通过完整测试套件验证(8/8 通过),详见 [PERMISSION_TEST_REPORT.md](./PERMISSION_TEST_REPORT.md)。 + +--- + +**更新日期**: 2026-01-30 +**验证状态**: ✅ 已测试 +**影响范围**: 2.2 权限与确认章节 diff --git a/plugins/debug/copilot_sdk_research/PERMISSION_TEST_REPORT.md b/plugins/debug/copilot_sdk_research/PERMISSION_TEST_REPORT.md new file mode 100644 index 0000000..928ba43 --- /dev/null +++ b/plugins/debug/copilot_sdk_research/PERMISSION_TEST_REPORT.md @@ -0,0 +1,173 @@ +# GitHub Copilot SDK 权限控制测试报告 + +## 测试日期 + +2026-01-30 + +## 测试环境 + +- **Model**: gpt-4.1 +- **Python**: 3.12 +- **Copilot SDK**: Latest + +## 关键发现 + +### 1. Shell 权限请求结构 + +Shell 类型的权限请求使用 **`fullCommandText`** 字段,而非 `command` 字段。 + +**完整请求示例:** + +```json +{ + "kind": "shell", + "toolCallId": "call_JKLi7tz3uSDQWE3LgzCpvSVy", + "fullCommandText": "ls -la", + "intention": "List all files and directories with details in the current directory", + "commands": [ + { + "identifier": "ls -la", + "readOnly": false + } + ], + "possiblePaths": [], + "possibleUrls": [], + "hasWriteFileRedirection": false, + "canOfferSessionApproval": false +} +``` + +### 2. 正则匹配模式验证 + +正则白名单模式已验证有效,必须使用 `fullCommandText` 字段: + +```python +command = request.get("fullCommandText", "") or request.get("command", "") +pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN +if pattern and command: + if re.match(pattern, command): + return {"kind": "approved"} +``` + +## 测试结果 + +### 完整测试套件(8/8 通过 ✅) + +| # | 测试用例 | 配置 | 提示词 | 预期 | 结果 | 状态 | +|---|---------|------|--------|------|------|------| +| 1 | Default Deny Shell | 默认 | 请执行: ls -la | ❌ Denied | ❌ Denied | ✅ | +| 2 | Allow All | allow_all=True | 请执行: ls -la | ✅ Approved | ✅ Approved | ✅ | +| 3 | Allow Shell | allow_shell=True | 请执行: pwd | ✅ Approved | ✅ Approved | ✅ | +| 4 | Regex Match: ^ls | pattern='^ls' | 请执行: ls -la | ✅ Approved | ✅ Approved | ✅ | +| 5 | Regex No Match | pattern='^ls' | 请执行: pwd | ❌ Denied | ❌ Denied | ✅ | +| 6 | Regex Complex | pattern='^(ls\|pwd\|echo)' | 请执行: pwd | ✅ Approved | ✅ Approved | ✅ | +| 7 | Regex No Match: git | pattern='^(ls\|pwd\|echo)' | 请执行: git status | ❌ Denied | ❌ Denied | ✅ | +| 8 | Read Permission | 默认 | Read: README.md | ✅ Approved | ✅ Approved | ✅ | + +**总体通过率: 100%** 🎉 + +## 推荐配置示例 + +### 1. 安全模式(推荐生产环境) + +```python +PERMISSIONS_ALLOW_ALL: bool = False +PERMISSIONS_ALLOW_SHELL: bool = False +PERMISSIONS_SHELL_ALLOW_PATTERN: str = "^(ls|pwd|echo|cat).*" +PERMISSIONS_ALLOW_WRITE: bool = False +PERMISSIONS_ALLOW_MCP: bool = True +``` + +### 2. 开发模式 + +```python +PERMISSIONS_ALLOW_ALL: bool = False +PERMISSIONS_ALLOW_SHELL: bool = False +PERMISSIONS_SHELL_ALLOW_PATTERN: str = "^(ls|pwd|echo|cat|grep|git status|npm test).*" +PERMISSIONS_ALLOW_WRITE: bool = False +PERMISSIONS_ALLOW_MCP: bool = True +``` + +### 3. 完全信任模式(仅限受控环境) + +```python +PERMISSIONS_ALLOW_ALL: bool = True +``` + +## 实现建议 + +### 正确的权限处理代码 + +```python +import re +from typing import Any, Dict + +async def on_user_permission_request(request: Dict[str, Any], context: Dict[str, str]): + """ + 统一权限审批网关 + """ + kind = request.get("kind") + # 关键:使用 fullCommandText 而非 command + command = request.get("fullCommandText", "") or request.get("command", "") + + # 1. 超级模式 + if self.valves.PERMISSIONS_ALLOW_ALL: + return {"kind": "approved"} + + # 2. 默认安全(read、url) + if kind in ["read", "url"]: + return {"kind": "approved"} + + # 3. Shell 细粒度控制 + if kind == "shell": + if self.valves.PERMISSIONS_ALLOW_SHELL: + return {"kind": "approved"} + + pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN + if pattern and command: + try: + if re.match(pattern, command): + return {"kind": "approved"} + except re.error as e: + logger.error(f"Invalid regex: {pattern} - {e}") + + # 4. Write 权限 + if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE: + return {"kind": "approved"} + + # 5. MCP 权限 + if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP: + return {"kind": "approved"} + + # 6. 默认拒绝 + logger.warning(f"Permission Denied: {kind} {command}") + return { + "kind": "denied-by-rules", + "rules": [{"kind": "security-policy"}] + } +``` + +## 常见正则模式示例 + +| 用途 | 正则表达式 | 说明 | +|------|-----------|------| +| 只读命令 | `^(ls|pwd|cat|echo|grep).*` | 允许常见只读命令 | +| Git 只读 | `^git (status\|log\|diff\|show).*` | 允许 Git 只读操作 | +| npm/yarn 测试 | `^(npm\|yarn) (test\|run).*` | 允许测试脚本 | +| 完全 shell | `.*` | ⚠️ 危险:允许所有命令 | + +## 测试脚本位置 + +- 基础测试: [test_shell_permission_pattern.py](./test_shell_permission_pattern.py) +- 完整测试套件: [test_permission_comprehensive.py](./test_permission_comprehensive.py) + +## 结论 + +✅ **GitHub Copilot SDK 的权限控制机制完全有效** +✅ **正则白名单模式已验证可用** +⚠️ **必须使用 `fullCommandText` 字段获取命令内容** + +--- + +**测试执行者**: GitHub Copilot +**审核状态**: ✅ 已验证 diff --git a/plugins/debug/copilot_sdk_research/copilot_pipe_enhancement_guide.md b/plugins/debug/copilot_sdk_research/copilot_pipe_enhancement_guide.md new file mode 100644 index 0000000..12abd4c --- /dev/null +++ b/plugins/debug/copilot_sdk_research/copilot_pipe_enhancement_guide.md @@ -0,0 +1,238 @@ +# OpenWebUI GitHub Copilot Pipe Enhancement Guide + +基于 Copilot SDK 源码级研究的深度技术总结,旨在指导 OpenWebUI Pipe 的功能增强开发。 + +## 1. 认证机制 (Authentication) + +官方支持通过环境变量传递 Token。在 Pipe 中,只要确保 `GH_TOKEN` 或 `GITHUB_TOKEN` 存在于环境变量中,Copilot CLI 即可自动识别,无需在 `CopilotClient` 构造函数中重复注入。 + +### 核心实现 + +Pipe 应确保将 Token(来自 Valve 或 Env)正确设置到当前进程的环境变量中。 + +```python +import os +from copilot import CopilotClient + +# 1. 设置环境变量 (如果从 Valve 获取) +if self.valves.GH_TOKEN: + os.environ["GH_TOKEN"] = self.valves.GH_TOKEN + +# 2. 初始化客户端 +# CopilotClient 启动的 CLI 子进程会自动继承当前环境中的 GH_TOKEN +client = CopilotClient({ + # "cli_path": ..., + # 注意:无需在此处重复传入 github_token,CLI 会自动读取环境变量 +}) + +# 3. 启动前检查 (建议) +# status = await client.get_auth_status() +# if not status.isAuthenticated: ... +``` + +## 2. 权限与确认 (Permissions & Tools) - 核心控制点 + +这是用户最关心的部分:如何知道有哪些工具,以及如何控制它们的执行。 + +### 2.1 内置工具 (Built-in Tools) + +Copilot CLI 内部管理了一组标准工具,**Python SDK 目前没有直接的 API (`client.list_tools()`) 来列出这些工具**。 + +但是,根据 SDK 的 `PermissionRequest` 类型定义 (`copilot/types.py`),我们可以反推其能力类别: + +* **`shell`**: 执行终端命令 (对应 `run_terminal_command` 等) +* **`filesystem`** (对应 `read/write`): 文件读写 (对应 `read_file`, `edit_file`, `delete_file` 等) +* **`url`**: 网络访问 (对应 `fetch_url` 等) +* **`mcp`**: 连接的 MCP 服务器工具 + +> **提示**: `available_tools` 参数可以用来“隐藏”工具,让 Agent 根本不知道它有一把锤子。而 `on_permission_request` 是用来拦截 Agent 挥舞锤子的动作。通常我们建议**能力全开 (不设置 available_tools 限制)**,而在**权限层 (on_permission_request) 做拦截**。 + +### 2.2 实现“全部允许”与“按需允许” + +建议在 Valves 中增加权限控制字段,并在 `on_permission_request` 中实现逻辑。 + +```python +import re + +class Valves(BaseModel): + # ... 其他 Valve ... + # 权限控制开关 + PERMISSIONS_ALLOW_ALL: bool = Field(default=False, description="DANGER: Auto-approve ALL actions (shell, write, etc).") + PERMISSIONS_ALLOW_SHELL: bool = Field(default=False, description="Auto-approve shell commands.") + PERMISSIONS_SHELL_ALLOW_PATTERN: str = Field(default="", description="Regex for approved shell commands (e.g., '^ls|^grep').") + PERMISSIONS_ALLOW_WRITE: bool = Field(default=False, description="Auto-approve file write/edit/delete.") + PERMISSIONS_ALLOW_MCP: bool = Field(default=True, description="Auto-approve MCP tool execution.") + +# 权限处理 Hook 实现 +async def on_user_permission_request(request, context): + """ + 统一权限审批网关 + request keys: kind, toolCallId, ... (shell requests have 'command') + """ + kind = request.get("kind") # shell, write, mcp, read, url + + # 1. 超级模式:全部允许 + if self.valves.PERMISSIONS_ALLOW_ALL: + return {"kind": "approved"} + + # 2. 默认安全:始终允许 "读" 和 "Web浏览" (根据需求调整) + if kind in ["read", "url"]: + return {"kind": "approved"} + + # 3. 细粒度控制 + if kind == "shell": + # 3.1 完全允许 Shell + if self.valves.PERMISSIONS_ALLOW_SHELL: + return {"kind": "approved"} + + # 3.2 基于正则允许特定命令 + command = request.get("command", "") + pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN + if pattern and command: + try: + if re.match(pattern, command): + return {"kind": "approved"} + except re.error: + print(f"[Config Error] Invalid Regex: {pattern}") + + if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE: + return {"kind": "approved"} + + if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP: + return {"kind": "approved"} + + # 4. 默认拒绝 + print(f"[Permission Denied] Blocked request for: {kind} {request.get('command', '')}") + return { + "kind": "denied-by-rules", + "rules": [{"kind": "check-openwebui-valves"}] + } + +# 注册 Hook +session = await client.create_session({ + # ... + "on_permission_request": on_user_permission_request +}) +``` + +## 3. Agent 与 MCP 集成 (Agents & MCP) + +SDK 中的 Agent 和 MCP 并非独立文件,而是会话配置 (`SessionConfig`) 的一部分。Pipe 可以通过 Valves 动态构建这些配置。 + +### 关键映射关系 + +| SDK 概念 | OpenWebUI 对应 | 实现位置 | 关键参数 | +| :--- | :--- | :--- | :--- | +| **Custom Agent** | 自定义模型 / Persona | `create_session(custom_agents=[...])` | `name`, `prompt`, `tools` (仅名称) | +| **Agent Tools** | Valve 开关 / 预置工具 | `create_session(tools=[func1, func2])` | 必须先在 `tools` 注册函数,Agent 才能引用 | +| **MCP Server** | Valve 配置 (JSON) | `create_session(mcp_servers={...})` | `command`, `args`, `env` (本地) | + +### 代码范式:动态构建 Agent + +```python +async def create_agent_session(client, user_prompt, model_name): + # 1. 定义工具 (必须是函数引用) + # 假设已从 OpenWebUI Tools 转换或内置 + available_tools = [tool_web_search, tool_run_script] + + # 2. 构建 Agent Manifest (针对当前请求的虚拟 Agent) + agent_manifest = { + "name": "openwebui_agent", + "description": "Dynamic agent from OpenWebUI", + "prompt": "You are a helpful assistant...", # 这里注入 System Prompt + "tools": ["web_search", "run_script"], # 引用上方工具的 name + "mcp_servers": { + # 可以在这里为特定 Agent 绑定 MCP + } + } + + # 3. 创建会话 + session = await client.create_session({ + "model": "gpt-4", # 底层模型 + "custom_agents": [agent_manifest], + "tools": available_tools, # 注册实际代码 + "available_tools": ["web_search"], # 白名单控制当前可用工具 + # ... 权限配置 + }) +``` + +## 4. MCP 服务器配置 (Native MCP Support) + +Pipe 可以直接支持标准 MCP 协议(Stdio)。不需要额外的 MCP 客户端代理,SDK 原生支持。 + +### Valve 配置结构建议 + +建议在 Pipe 的 Valves 中增加一个 `MCP_CONFIG` 字段(JSON 字符串),解析后直接传给 SDK。 + +```python +# Valve 输入示例 (JSON) +# { +# "brave_search": { +# "type": "local", +# "command": "npx", +# "args": ["-y", "@modelcontextprotocol/server-brave-search"], +# "env": {"BRAVE_API_KEY": "..."} +# } +# } + +# 代码实现 +mcp_config = json.loads(self.valves.MCP_CONFIG) +session = await client.create_session({ + # ... + "mcp_servers": mcp_config, + # 注意:必须配合权限自动审批,否则 MCP 工具无法调用 + "on_permission_request": auto_approve_policy +}) +``` + +## 5. 会话管理:持久化 vs 重放 (Persistence) + +OpenWebUI 是无状态的,但 Copilot SDK 是有状态的(保留上下文窗口优化)。 + +### 最佳实践:以 `chat_id` 为锚点 + +利用 OpenWebUI 提供的 `chat_id` 来决定是 `resume` 还是 `start`。 + +1. **Map**: 维护 `Dict[chat_id, session_id]` (内存或数据库)。 +2. **Flow**: + * 请求进来 -> 检查 `chat_id` 是否有对应的 `session_id`。 + * **有**: 尝试 `client.resume_session(session_id)`。 + * *注意*:Resume 时必须重新传入 `tools`, `hooks`, `on_permission_request`,因为这些 Python 对象不会被序列化保存。 + * **无/失败**: 调用 `client.create_session()`,并将新 `session_id` 存入 Map。 +3. **Fallback**: 如果 Resume 失败(例如后端重启 SDK 进程丢失),回退到 Create 新会话,并可选地将 OpenWebUI 传来的 `messages` 历史以 System Message 或历史插入的方式“重放”进去(虽然 SDK 不直接支持 insert history,但可以通过连续的 `send` 模拟,但这很慢)。 + * *简易方案*:Resume 失败就作为新对话开始,只带入 System Prompt。 + +## 6. 高级 Hook:提示词增强 + +利用 `on_user_prompt_submitted` 钩子,可以在不修改用户可见内容的情况下,向 Copilot 注入隐式上下文(例如当前文件内容、Pipe 的元指令)。 + +```python +async def inject_context_hook(input_data, ctx): + user_prompt = input_data["prompt"] + + # 比如:检测到用户在问代码,自动附加上下文 + additional_context = "Current Language: Python. Framework: OpenWebUI." + + return { + "modifiedPrompt": user_prompt, # 可以在这里改写提示词 + "additionalContext": additional_context # 注入隐藏上下文 + } + +session = await client.create_session({ + # ... + "hooks": { + "on_user_prompt_submitted": inject_context_hook + } +}) +``` + +--- + +**总结开发清单:** + +1. [ ] **Env Auth**: 读取环境变量 -> `CopilotClient`。 +2. [ ] **Permission Valve**: 实现 `PERMISSIONS_ALLOW_ALL/SHELL` 等 Valves。 +3. [ ] **Auto-Approve Hook**: 实现 `on_permission_request` 逻辑。 +4. [ ] **MCP Valve**: 添加 JSON Valve -> `session.mcp_servers`。 +5. [ ] **Session Map**: 实现 `chat_id` <-> `session_id` 的简单的内存映射。 +6. [ ] **Resume Logic**: 优先 `resume_session`,并记得并在 resume 时重传 Hook 和 Tools。 diff --git a/plugins/debug/copilot_sdk_research/copilot_sdk_guide.py b/plugins/debug/copilot_sdk_research/copilot_sdk_guide.py new file mode 100644 index 0000000..2d647ff --- /dev/null +++ b/plugins/debug/copilot_sdk_research/copilot_sdk_guide.py @@ -0,0 +1,620 @@ +#!/usr/bin/env python3 +import argparse +import asyncio +import datetime as dt +import json +import logging +import os +import sys +import textwrap +from typing import Iterable, List, Optional + +from copilot import CopilotClient + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("copilot_sdk_guide") + +DEFAULT_CONTEXT_URLS = [ + "https://raw.githubusercontent.com/github/copilot-sdk/main/README.md", + "https://raw.githubusercontent.com/github/copilot-sdk/main/python/README.md", + "https://raw.githubusercontent.com/github/copilot-sdk/main/docs/getting-started.md", + "https://raw.githubusercontent.com/github/copilot-cli/main/README.md", + "https://raw.githubusercontent.com/github/copilot-cli/main/changelog.md", + "https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli", + "https://docs.github.com/en/copilot/concepts/agents/about-agent-skills", + "https://raw.githubusercontent.com/github/awesome-copilot/main/README.md", + "https://raw.githubusercontent.com/github/awesome-copilot/main/skills/copilot-sdk/SKILL.md", + "https://raw.githubusercontent.com/github/awesome-copilot/main/instructions/agent-skills.instructions.md", +] + +AWESOME_COPILOT_REPO = "github/awesome-copilot" +AWESOME_COPILOT_BRANCH = "main" +AWESOME_COPILOT_DOC_DIRS = ["docs/", "instructions/"] + +TOPICS = [ + "MCP Server Integration: JSON-RPC config and SDK hooks", + "Agent Manifests: Defining capabilities and permissions programmatically", + "Headless Auth: Device Code Flow and credential persistence", + "Session Replay vs Resume: Handling stateless frontend history", + "Advanced Session Hooks: Intercepting and modifying user prompts", + "Workspace Virtualization: Handling CWD for remote/virtual files", + "Error Recovery: Handling session disconnects and re-auth", + "Confirmation Events: programmatic handling of 'confirmation_required'", + "Skills: Conflict resolution and precedence defaults", + "Debugging: Tracing JSON-RPC traffic in the SDK", + "Billing & Policies: How seat management affects SDK features", +] + +QUESTION_TEMPLATES = [ + "Give a concise overview of {topic}.", + "Provide best practices and common pitfalls for {topic}.", + "Show a minimal example snippet for {topic}.", + "List recommended configuration defaults for {topic}.", + "How does {topic} relate to building a custom Agent?", +] + +CLI_FOCUS_QUESTIONS = [ + "How to configure MCP servers in ~/.copilot/config.json for SDK usage?", + "What CLI environment variables force 'Agent' mode vs 'Generic' mode?", + "Explain the 'confirmation' flow in CLI and how it maps to SDK events.", + "Does the CLI support 'dry-run' permission checks for tools?", + "What are the undocumented requirements for 'workspace' context updates?", + "How does the CLI handle 'device code' re-authentication automatically?", +] + + +def build_questions(max_questions: int) -> List[str]: + questions: List[str] = [] + + for topic in TOPICS: + for template in QUESTION_TEMPLATES: + questions.append(template.format(topic=topic)) + + questions.extend(CLI_FOCUS_QUESTIONS) + + # De-duplicate while preserving order + seen = set() + uniq: List[str] = [] + for q in questions: + if q in seen: + continue + seen.add(q) + uniq.append(q) + + return uniq[:max_questions] + + +def build_deep_dive_prompts() -> List[str]: + return [ + "Provide a python code example for configuring `CopilotClient` to connect to a local MCP server (e.g. Brave Search) via `CopilotClient` config.", + "Explain how to programmatically handle `tool.confirmation_required` events in a non-interactive stream using `session.on()`.", + "Show how to implement a 'Device Flow' login helper using SDK primitives (if available) or raw HTTP showing how to persist credentials.", + "Compare the pros and cons of 'Session Replay' (fast-forwarding history) vs 'Session Resume' (stateful ID) for a stateless web backend like OpenWebUI.", + "Detail the exact protocol for 'Virtual Workspace': how to implement a file system provider that feeds content to Copilot without physical files.", + "Create an 'Agent Manifest' example: how to define an Agent capable of specific high-privileged tools via SDK.", + "List all 'hidden' `SessionConfig` parameters relevant to Agent behavior and personality.", + ] + + +def load_questions(path: str) -> List[str]: + if path.lower().endswith(".json"): + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, list): + return [str(x).strip() for x in data if str(x).strip()] + raise ValueError("JSON must be an array of strings") + + with open(path, "r", encoding="utf-8") as f: + lines = [line.strip() for line in f.readlines()] + return [line for line in lines if line] + + +def fetch_url(url: str, headers: Optional[dict] = None) -> str: + import urllib.request + import time + + retries = 3 + if headers is None: + headers = {} + + req = urllib.request.Request(url, headers=headers) + + for i in range(retries): + try: + with urllib.request.urlopen(req, timeout=20) as response: + return response.read().decode("utf-8", errors="replace") + except Exception as exc: + if i == retries - 1: + logger.warning( + "Failed to fetch %s after %d attempts: %s", url, retries, exc + ) + return "" + time.sleep(1 * (i + 1)) + return "" + + +def list_repo_markdown_urls( + repo: str, + branch: str, + dir_prefixes: List[str], +) -> List[str]: + api_url = f"https://api.github.com/repos/{repo}/git/trees/{branch}?recursive=1" + headers = {} + if os.environ.get("GITHUB_TOKEN"): + headers["Authorization"] = f"token {os.environ.get('GITHUB_TOKEN')}" + + try: + content = fetch_url(api_url, headers=headers) + if not content: + return [] + data = json.loads(content) + except Exception as exc: + logger.warning("Failed to list repo tree: %s", exc) + return [] + + tree = data.get("tree", []) if isinstance(data, dict) else [] + urls: List[str] = [] + for item in tree: + if not isinstance(item, dict): + continue + path = item.get("path", "") + if not path or not path.endswith(".md"): + continue + if any(path.startswith(prefix) for prefix in dir_prefixes): + raw = f"https://raw.githubusercontent.com/{repo}/{branch}/{path}" + urls.append(raw) + return urls + + +def read_local_sdk_source(max_chars: int = 300000) -> str: + """ + Locates the installed 'copilot' package and reads its source code. + This ensures analysis is based on the actual installed version, not just docs. + """ + try: + import copilot + except ImportError: + logger.error("Could not import 'copilot' SDK. Is it installed?") + return "" + + package_dir = os.path.dirname(copilot.__file__) + logger.info(f"Reading SDK source from: {package_dir}") + + source_chunks = [] + total_chars = 0 + + # Prioritize key files that define core logic + priority_files = ["client.py", "session.py", "types.py", "events.py", "__init__.py"] + + # First pass: Recursively find all .py files + all_py_files = [] + for root, dirs, files in os.walk(package_dir): + if "__pycache__" in root: + continue + for file in files: + if file.endswith(".py"): + all_py_files.append(os.path.join(root, file)) + + # Sort files: priority files first, then alphabetical + def sort_key(path): + fname = os.path.basename(path) + if fname in priority_files: + return (0, priority_files.index(fname)) + return (1, path) + + all_py_files.sort(key=sort_key) + + for path in all_py_files: + rel_path = os.path.relpath(path, os.path.dirname(package_dir)) + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + # Add file delimiter for the model + header = f"\n\n# ==================================================\n# SOURCE CODE FILE: {rel_path}\n# ==================================================\n" + chunk = header + content + + if total_chars + len(chunk) > max_chars: + remaining = max_chars - total_chars + if remaining > len(header) + 100: + source_chunks.append( + chunk[:remaining] + "\n# [TRUNCATED DUE TO LENGTH LIMIT]" + ) + logger.warning(f"Context limit reached. Stopping at {rel_path}") + break + + source_chunks.append(chunk) + total_chars += len(chunk) + logger.info(f"Loaded source file: {rel_path} ({len(content)} chars)") + + except Exception as e: + logger.warning(f"Failed to read source file {path}: {e}") + + return "".join(source_chunks) + + +def build_context(urls: Iterable[str], max_chars: int) -> str: + chunks: List[str] = [] + remaining = max_chars + + for url in urls: + if remaining <= 0: + break + try: + content = fetch_url(url) + header = f"[Source: {url}]\n" + if len(header) >= remaining: + break + remaining -= len(header) + + if len(content) > remaining: + content = content[:remaining] + "\n[TRUNCATED]\n" + remaining = 0 + else: + remaining -= len(content) + + chunks.append(header + content) + logger.info("Fetched context: %s", url) + except Exception as exc: + logger.warning("Failed to fetch %s: %s", url, exc) + + return "\n\n".join(chunks) + + +def write_jsonl(path: str, item: dict) -> None: + with open(path, "a", encoding="utf-8") as f: + f.write(json.dumps(item, ensure_ascii=False) + "\n") + + +def write_markdown_header(path: str, title: str, meta: dict) -> None: + with open(path, "w", encoding="utf-8") as f: + f.write(f"# {title}\n\n") + for k, v in meta.items(): + f.write(f"- **{k}**: {v}\n") + f.write("\n---\n\n") + + +def append_markdown_qa(path: str, question: str, answer: str) -> None: + with open(path, "a", encoding="utf-8") as f: + f.write(f"## Q: {question}\n\n") + f.write(f"{answer}\n\n") + + +def clamp_questions(questions: List[str], max_questions: int) -> List[str]: + return questions[: max(1, min(max_questions, 400))] + + +def print_progress_bar( + iteration, + total, + prefix="", + suffix="", + decimals=1, + length=50, + fill="█", + printEnd="\r", +): + """ + Call in a loop to create terminal progress bar + """ + percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + filledLength = int(length * iteration // total) + bar = fill * filledLength + "-" * (length - filledLength) + # Clear line extension to handle shrinking suffixes + print(f"\r{prefix} |{bar}| {percent}% {suffix}\033[K", end=printEnd) + # Print New Line on Complete + if iteration == total: + print() + + +async def run_session( + model: str, + questions: List[str], + output_dir: str, + context: str, + session_id: Optional[str], + delay: float, + output_lang: str, + enable_infinite_sessions: bool, + timeout: int, +) -> None: + client = CopilotClient() + await client.start() + + session_config = {"model": model} + if session_id: + session_config["session_id"] = session_id + if enable_infinite_sessions: + session_config["infinite_sessions"] = { + "enabled": True, + "background_compaction_threshold": 0.8, + "buffer_exhaustion_threshold": 0.95, + } + + session = await client.create_session(session_config) + + timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S") + jsonl_path = os.path.join(output_dir, f"copilot_sdk_guide_{timestamp}.jsonl") + md_path = os.path.join(output_dir, f"copilot_sdk_guide_{timestamp}.md") + + write_markdown_header( + md_path, + "GitHub Copilot SDK & CLI 研究报告", + { + "model": model, + "questions": len(questions), + "timestamp": timestamp, + "language": output_lang, + }, + ) + + lang_instruction = "Chinese" if "zh" in output_lang.lower() else "English" + + system_prompt = textwrap.dedent( + f""" + You are an expert assistant. Focus on GitHub Copilot SDK and GitHub Copilot CLI. + + CRITICAL INSTRUCTION: SOURCE CODE FIRST. + You have been provided with the ACTUAL PYTHON SOURCE CODE of the `copilot` SDK in the context. + When answering questions: + 1. FIRST, analyze the provided source code (look for class definitions, type hints, methods). + 2. THEN, refer to documentation if source code is ambiguous. + 3. Do NOT hallucinate methods that do not exist in the source code. + 4. If a feature (like MCP) is not explicitly in the code, explain how to implement it using the available primitives (low-level hooks/events). + + Provide accurate, concise answers in {lang_instruction}. When relevant, include command names, + configuration keys, and pitfalls. Use bullet points where useful. + + Output requirements: + - Write in {lang_instruction}. + - Provide practical code snippets (Python/TypeScript/CLI) when helpful. + - Include a short "建议/落地" section for integration into a pipe. + - If citing facts from provided context, briefly mention the source URL. + """ + ).strip() + + if context: + system_prompt += "\n\nAdditional context:\n" + context + + await session.send_and_wait({"prompt": system_prompt}, timeout=timeout) + + total_q = len(questions) + print_progress_bar(0, total_q, prefix="Progress:", suffix="Starting...", length=30) + + for idx, question in enumerate(questions, start=1): + # Update progress bar (Asking...) + q_short = (question[:40] + "...") if len(question) > 40 else question.ljust(43) + print_progress_bar( + idx - 1, total_q, prefix="Progress:", suffix=f"Asking: {q_short}", length=30 + ) + + # Log to file/debug only + logger.debug("[%s/%s] Asking: %s", idx, total_q, question) + + answer = "" + max_retries = 3 + for attempt in range(max_retries): + try: + response = await session.send_and_wait( + {"prompt": question}, timeout=timeout + ) + answer = response.data.content if response and response.data else "" + break + except Exception as e: + logger.error( + f"Error asking question (Attempt {attempt+1}/{max_retries}): {e}" + ) + if attempt < max_retries - 1: + await asyncio.sleep(2) + else: + answer = f"Error retrieving answer: {e}" + + write_jsonl( + jsonl_path, + { + "index": idx, + "question": question, + "answer": answer, + "model": model, + }, + ) + append_markdown_qa(md_path, question, answer) + + # Update progress bar (Done...) + print_progress_bar( + idx, total_q, prefix="Progress:", suffix=f"Done: {q_short}", length=30 + ) + + if delay > 0: + await asyncio.sleep(delay) + + await session.destroy() + await client.stop() + + logger.info("Saved output to %s and %s", jsonl_path, md_path) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Ask up to 100 Copilot SDK questions via GitHub Copilot SDK", + ) + parser.add_argument("--model", default="gpt-5.2-codex", help="Model to use") + parser.add_argument( + "--max-questions", + type=int, + default=100, + help="Max number of questions (1-400)", + ) + parser.add_argument( + "--questions-file", + default="", + help="Path to .txt or .json list of questions", + ) + parser.add_argument( + "--context-url", + action="append", + default=[], + help="Additional context URL (repeatable)", + ) + parser.add_argument( + "--no-default-context", + action="store_true", + help="Disable default Copilot SDK context URLs", + ) + parser.add_argument( + "--include-awesome-copilot-docs", + action="store_true", + help="Include all markdown files from awesome-copilot/docs", + ) + parser.add_argument( + "--include-awesome-copilot-instructions", + action="store_true", + help="Include all markdown files from awesome-copilot/instructions", + ) + parser.add_argument( + "--no-sdk-source", + action="store_true", + help="Do NOT read local SDK source code (default: reads source)", + ) + parser.add_argument( + "--session-id", + default="", + help="Optional custom session ID", + ) + parser.add_argument( + "--output-dir", + default="", + help="Directory to save outputs", + ) + parser.add_argument( + "--delay", + type=float, + default=0.5, + help="Delay between questions (seconds)", + ) + parser.add_argument( + "--max-context-chars", + type=int, + default=400000, + help="Max characters of aggregated context (default: 400000)", + ) + parser.add_argument( + "--disable-infinite-sessions", + action="store_true", + help="Disable infinite sessions (default: enabled)", + ) + parser.add_argument( + "--output-lang", + default="zh-CN", + help="Output language (default: zh-CN)", + ) + parser.add_argument( + "--deep-dive", + action="store_true", + help="Append deep-dive prompts for more detailed research", + ) + parser.add_argument( + "--timeout", + type=int, + default=3600, + help="Session request timeout in seconds (default: 3600)", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + + if args.questions_file: + questions = load_questions(args.questions_file) + else: + # Generate enough questions to cover everything + questions = build_questions(9999) + + if args.deep_dive: + # Prepend deep dive questions to ensure they are prioritized + questions = build_deep_dive_prompts() + questions + + questions = clamp_questions(questions, args.max_questions) + if not questions: + logger.error("No questions to ask") + sys.exit(1) + + context_urls = [] if args.no_default_context else list(DEFAULT_CONTEXT_URLS) + + if args.include_awesome_copilot_docs: + context_urls.extend( + list_repo_markdown_urls( + AWESOME_COPILOT_REPO, + AWESOME_COPILOT_BRANCH, + ["docs/"], + ) + ) + + if args.include_awesome_copilot_instructions: + context_urls.extend( + list_repo_markdown_urls( + AWESOME_COPILOT_REPO, + AWESOME_COPILOT_BRANCH, + ["instructions/"], + ) + ) + + context_urls.extend(args.context_url or []) + + # 1. Read local source code first (Priority: High) + # We allocate up to max_context_chars to source code initially. + # The actual usage will likely be less for a typical SDK. + source_context = "" + source_chars_count = 0 + if not args.no_sdk_source: + source_context = read_local_sdk_source(args.max_context_chars) + source_chars_count = len(source_context) + logger.info(f"Source context usage: {source_chars_count} chars") + + # 2. Calculate remaining budget for Web Docs (Priority: Secondary) + # We ensure we don't exceed the global limit. + remaining_chars = max(10000, args.max_context_chars - source_chars_count) + logger.info(f"Remaining budget for web docs: {remaining_chars} chars") + + # 3. Fetch remote docs + web_context = build_context(context_urls, remaining_chars) + + combined_context = "" + # Assemble context in order of authority (Source > Docs) + if source_context: + combined_context += ( + "# PRIMARY SOURCE: LOCAL SDK CODE (AUTHORITATIVE)\n" + + source_context + + "\n\n" + ) + if web_context: + combined_context += ( + "# SECONDARY SOURCE: WEB DOCUMENTATION & AWESOME-COPILOT\n" + web_context + ) + + output_dir = args.output_dir or os.path.join( + os.getcwd(), "plugins", "debug", "copilot_sdk_research", "outputs" + ) + os.makedirs(output_dir, exist_ok=True) + + asyncio.run( + run_session( + model=args.model, + questions=questions, + output_dir=output_dir, + context=combined_context, + session_id=args.session_id or None, + delay=args.delay, + output_lang=args.output_lang, + enable_infinite_sessions=not args.disable_infinite_sessions, + timeout=args.timeout, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/plugins/debug/copilot_sdk_research/dummy_test_file.txt b/plugins/debug/copilot_sdk_research/dummy_test_file.txt new file mode 100644 index 0000000..ef6a5b2 --- /dev/null +++ b/plugins/debug/copilot_sdk_research/dummy_test_file.txt @@ -0,0 +1,2 @@ +This is a dummy test file for verifying permission controls. +You can delete this file safely. \ No newline at end of file diff --git a/plugins/debug/copilot_sdk_research/inspect_permissions.py b/plugins/debug/copilot_sdk_research/inspect_permissions.py new file mode 100644 index 0000000..4d02a17 --- /dev/null +++ b/plugins/debug/copilot_sdk_research/inspect_permissions.py @@ -0,0 +1,26 @@ +import inspect +import json + +try: + import copilot + from copilot import types + + print(f"Copilot location: {copilot.__file__}") + + print("\n=== Inspecting PermissionRequest types ===") + # Look for definition of PermissionRequest or similar + + # In the absence of direct access to the CLI output structure documentation, + # we can check if there are type hints or typed dicts in copilot.types + + for name, obj in inspect.getmembers(types): + if "Permission" in name or "Request" in name: + print(f"\nType: {name}") + try: + if hasattr(obj, "__annotations__"): + print(obj.__annotations__) + except: + pass + +except ImportError: + print("copilot module not installed") diff --git a/plugins/debug/copilot_sdk_research/outputs/copilot_sdk_guide_20260130_114642.jsonl b/plugins/debug/copilot_sdk_research/outputs/copilot_sdk_guide_20260130_114642.jsonl new file mode 100644 index 0000000..d1d3905 --- /dev/null +++ b/plugins/debug/copilot_sdk_research/outputs/copilot_sdk_guide_20260130_114642.jsonl @@ -0,0 +1,68 @@ +{"index": 1, "question": "Provide a python code example for configuring `CopilotClient` to connect to a local MCP server (e.g. Brave Search) via `CopilotClient` config.", "answer": "SDK 源码显示 MCP 服务器通过 `create_session`/`resume_session` 的 `mcp_servers` 配置传入并映射为 `mcpServers`,不是 CopilotClient 构造参数(源:本地源码 copilot/types.py / copilot/client.py)。下面是本地 stdio MCP(以 Brave Search 为例,命令/密钥请替换)的 Python 示例。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave_search\": {\n \"type\": \"local\", # 或 \"stdio\"\n \"command\": \"/path/to/brave-search-mcp\", # 替换为你的 MCP server 启动命令\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n \"timeout\": 30000, # 毫秒\n }\n },\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 查询 OpenAI 最新公告\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n要点/坑:\n- `tools`: `[\"*\"]` 表示全量,`[]` 表示不启用\n- `timeout`: 毫秒\n- `command/args/env`: 按你的 MCP server 实际启动方式替换\n\n建议/落地:\n- pipe 内复用同一 session,结束时统一 `client.stop()`", "model": "gpt-5.2-codex"} +{"index": 2, "question": "Explain how to programmatically handle `tool.confirmation_required` events in a non-interactive stream using `session.on()`.", "answer": "SDK 源码未定义 `tool.confirmation_required` 事件,权限确认由 JSON‑RPC `permission.request` 触发并交给 `on_permission_request` 回调处理,`session.on()` 只能订阅 `tool.user_requested`/`tool.execution_start` 等事件做记录关联(来源:本地源码 copilot/client.py、copilot/session.py、copilot/types.py)。 \n在非交互流里必须提供 `on_permission_request` 自动审批/拒绝,否则默认拒绝且回调异常也会被拒绝。\n\n要点/坑 \n- 配置键: `on_permission_request`(SessionConfig) \n- 权限类型: `shell` / `write` / `mcp` / `read` / `url` \n- 回调异常或缺失 -> `denied-no-approval-rule-and-could-not-request-from-user`\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n def on_event(ev):\n if ev.type == SessionEventType.TOOL_USER_REQUESTED:\n print(f\"tool requested: {ev.data.tool_name} {ev.data.tool_call_id}\")\n elif ev.type == SessionEventType.TOOL_EXECUTION_START:\n print(f\"tool start: {ev.data.tool_name}\")\n\n async def on_permission_request(req, ctx):\n kind = req.get(\"kind\")\n if kind in (\"read\", \"mcp\"):\n return {\"kind\": \"approved\"}\n return {\"kind\": \"denied-by-rules\", \"rules\": [{\"kind\": \"deny-non-read\"}]}\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": on_permission_request,\n })\n session.on(on_event)\n\n await session.send_and_wait({\"prompt\": \"请调用需要权限的工具\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地 \n- pipe 启动时注册审批策略 + `session.on` 记录 `tool_call_id` 做审计", "model": "gpt-5.2-codex"} +{"index": 3, "question": "Show how to implement a 'Device Flow' login helper using SDK primitives (if available) or raw HTTP showing how to persist credentials.", "answer": "SDK 本身没有 device‑flow 登录 API;可用鉴权原语只有 CopilotClient 的 `github_token`/`use_logged_in_user` 与 `get_auth_status`(源:本地源码 copilot/client.py、copilot/types.py)。 \n下面用 GitHub OAuth Device Flow 原始 HTTP 获取 token、持久化本地并注入 `github_token`(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。\n\n```python\nimport asyncio\nimport json\nimport os\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\n\nfrom copilot import CopilotClient\n\nCLIENT_ID = \"YOUR_OAUTH_APP_CLIENT_ID\"\nSCOPE = \"read:user\" # 按你的 OAuth App 需求调整\nTOKEN_PATH = Path.home() / \".config\" / \"myapp\" / \"copilot_token.json\"\n\ndef http_post(url, data):\n body = urllib.parse.urlencode(data).encode()\n req = urllib.request.Request(url, data=body, headers={\"Accept\": \"application/json\"})\n with urllib.request.urlopen(req) as resp:\n return json.loads(resp.read().decode())\n\ndef load_token():\n if TOKEN_PATH.exists():\n return json.loads(TOKEN_PATH.read_text()).get(\"access_token\")\n return None\n\ndef save_token(token):\n TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)\n TOKEN_PATH.write_text(json.dumps(token))\n os.chmod(TOKEN_PATH, 0o600)\n\ndef device_flow():\n code = http_post(\n \"https://github.com/login/device/code\",\n {\"client_id\": CLIENT_ID, \"scope\": SCOPE},\n )\n print(f\"Open {code['verification_uri']} and enter {code['user_code']}\")\n interval = int(code.get(\"interval\", 5))\n\n while True:\n token = http_post(\n \"https://github.com/login/oauth/access_token\",\n {\n \"client_id\": CLIENT_ID,\n \"device_code\": code[\"device_code\"],\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n },\n )\n if \"access_token\" in token:\n return token\n if token.get(\"error\") == \"slow_down\":\n interval += 5\n elif token.get(\"error\") not in (\"authorization_pending\",):\n raise RuntimeError(token)\n time.sleep(interval)\n\nasync def main():\n access_token = load_token()\n if not access_token:\n token = device_flow()\n save_token(token)\n access_token = token[\"access_token\"]\n\n client = CopilotClient({\"github_token\": access_token})\n await client.start()\n # ... create_session / send ...\n await client.stop()\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n```\n\n要点/坑 \n- `cli_url` 与 `github_token` 互斥 \n- SDK 不保存凭据 需自行持久化 \n- 轮询需处理 `authorization_pending` / `slow_down`\n\n建议/落地 \n- pipe 启动时读取 token 文件 不存在则走 device flow \n- 用 `github_token` 注入 保持非交互运行", "model": "gpt-5.2-codex"} +{"index": 4, "question": "Compare the pros and cons of 'Session Replay' (fast-forwarding history) vs 'Session Resume' (stateful ID) for a stateless web backend like OpenWebUI.", "answer": "对无状态后端(OpenWebUI)而言,SDK 只支持通过 `resume_session(session_id)` 的状态续接;所谓“Session Replay”需要你自行重放历史 prompt(来源:本地 SDK 源码 copilot/client.py、copilot/session.py)。 \n对比要点(短语): \n- Replay:无状态/易扩展;缺点是耗时耗 tokens、易截断/丢工具状态、可能重复副作用 \n- Resume:省 tokens、保留工具/compaction/`workspace_path` 状态;缺点是需持久化 session_id、依赖会话存活/跨实例一致性。 \n建议/落地:优先持久化 chat_id→session_id 并 resume_session,失效时回退为重放最近 N 轮并重建工具上下文。", "model": "gpt-5.2-codex"} +{"index": 5, "question": "Detail the exact protocol for 'Virtual Workspace': how to implement a file system provider that feeds content to Copilot without physical files.", "answer": "SDK 源码里没有“Virtual Workspace”协议或文件系统 provider 接口;会话只识别真实路径(`SessionConfig.working_directory`、`MessageOptions.attachments`)。要在无物理文件情况下供给内容,只能用自定义工具或 MCP 服务器把“读写文件”变成 tool 调用(来源:本地 SDK 源码 copilot/types.py / copilot/client.py)。\n\n可用原语(源码)\n- `SessionConfig.tools` / `available_tools`:自定义工具与白名单\n- `MessageOptions.attachments`:仅支持真实路径文件/目录\n\n方案 A:自定义工具实现 VFS(推荐)\n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nVFS = {\"README.md\": \"# Virtual README\\n内容来自内存\\n\"}\n\nclass VfsReadParams(BaseModel):\n path: str = Field(description=\"virtual path\")\n\n@define_tool(name=\"vfs_read\", description=\"Read a virtual file\")\ndef vfs_read(params: VfsReadParams) -> str:\n return VFS.get(params.path, \"\")\n\n@define_tool(name=\"vfs_list\", description=\"List virtual files\")\ndef vfs_list() -> list[str]:\n return sorted(VFS.keys())\n\nasync def main():\n client = CopilotClient()\n await client.start()\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [vfs_read, vfs_list],\n \"available_tools\": [\"vfs_read\", \"vfs_list\"],\n \"system_message\": {\"content\": \"Use vfs_* tools for all file access.\"},\n })\n await session.send_and_wait({\"prompt\": \"打开 README.md\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n方案 B:MCP 服务器(本地 stdio,VFS 由你实现)\n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"vfs\": {\n \"type\": \"local\",\n \"command\": \"python\",\n \"args\": [\"-m\", \"my_vfs_mcp_server\"],\n \"tools\": [\"*\"],\n }\n },\n})\n```\n\n注意/坑 \n- `available_tools` 只保留你的 VFS 工具以强制使用 \n- 内置文件工具只读真实文件;要兼容只能落地临时文件 \n- 大文件需分块返回,避免上下文爆炸 \n\n建议/落地 \n- pipe 内维护 VFS 存储(DB/Redis/内存) \n- 启动 session 时注入 `system_message` + `available_tools` 强制 VFS 入口", "model": "gpt-5.2-codex"} +{"index": 6, "question": "Create an 'Agent Manifest' example: how to define an Agent capable of specific high-privileged tools via SDK.", "answer": "SDK 没有独立“Agent Manifest”文件格式,等价于 `create_session` 的 `custom_agents`(CustomAgentConfig),并用 `tools`/`available_tools` 约束高权限工具,权限由 `on_permission_request` 处理(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \n示例如下(manifest 为 Python dict,可序列化为 JSON 配置)。\n\n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nclass AdminShellParams(BaseModel):\n cmd: str = Field(description=\"approved shell command\")\n\n@define_tool(name=\"admin_shell\", description=\"Run an approved shell command\")\ndef admin_shell(params: AdminShellParams) -> str:\n # TODO: 执行受控命令(白名单/沙箱)\n return f\"approved: {params.cmd}\"\n\nclass AdminWriteParams(BaseModel):\n path: str = Field(description=\"target path\")\n content: str = Field(description=\"content\")\n\n@define_tool(name=\"admin_write\", description=\"Write to privileged store\")\ndef admin_write(params: AdminWriteParams) -> str:\n # TODO: 受控写入\n return \"ok\"\n\nasync def on_permission_request(req, _ctx):\n if req.get(\"kind\") in (\"shell\", \"write\"):\n return {\"kind\": \"approved\"}\n return {\"kind\": \"denied-by-rules\", \"rules\": [{\"kind\": \"deny\"}]}\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n agent_manifest = {\n \"name\": \"ops-admin\",\n \"display_name\": \"Ops Admin\",\n \"description\": \"Privileged ops agent\",\n \"prompt\": \"Use only admin_* tools for privileged ops.\",\n \"tools\": [\"admin_shell\", \"admin_write\"],\n \"infer\": True,\n }\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [admin_shell, admin_write],\n \"custom_agents\": [agent_manifest],\n \"available_tools\": [\"admin_shell\", \"admin_write\"],\n \"on_permission_request\": on_permission_request,\n })\n\n await session.send_and_wait({\"prompt\": \"使用 ops-admin 执行受控操作\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n要点/坑 \n- session.tools 未注册 -> agent.tools 不可用 \n- custom_agents.tools 仅写名称 \n- 非交互缺少 on_permission_request -> 默认拒绝 \n- SDK 无显式 agent 选择参数 -> 需在 prompt 指定 \n\n建议/落地 \n- pipe 启动时加载 manifest 注入 custom_agents + available_tools + on_permission_request", "model": "gpt-5.2-codex"} +{"index": 7, "question": "List all 'hidden' `SessionConfig` parameters relevant to Agent behavior and personality.", "answer": "基于本地 SDK 源码 `copilot/types.py` 的 SessionConfig,影响 Agent 行为/人格的相关参数如下(来源:本地 SDK 源码)。 \n- `system_message`: `append`/`replace`;`replace` 会移除 SDK guardrails(风险) \n- `custom_agents`: `prompt`/`description`/`display_name`/`tools`/`mcp_servers`/`infer`;`tools` 仅名称且必须在 `tools` 注册 \n- `tools` / `available_tools` / `excluded_tools`: 能力与行为边界;`available_tools` 优先于 `excluded_tools` \n- `hooks`: `on_user_prompt_submitted`/`on_pre_tool_use`/`on_post_tool_use`/`on_session_start`/`on_session_end`/`on_error_occurred`,可改写 prompt/工具参数/结果 \n- `on_permission_request`: 非交互审批策略;缺失或异常 => 拒绝 \n- `on_user_input_request`: 启用/处理 `ask_user` \n- `mcp_servers`: 注入外部工具集 \n- `skill_directories` / `disabled_skills`: 技能启用/禁用 \n- `model` / `provider`: 模型与 BYOK 影响输出风格与能力 \n- `working_directory`: 上下文范围 \n- `infinite_sessions`: compaction 阈值影响记忆保留 \n- `config_dir`: CLI 配置/状态目录,行为取决于 CLI 侧配置 \n\n建议/落地:在 pipe 启动时固定 `system_message` + `available_tools/excluded_tools` + `hooks`,并持久化 session_id 以便 `resume_session`。", "model": "gpt-5.2-codex"} +{"index": 8, "question": "Give a concise overview of MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "SDK 中 MCP 通过 create_session/resume_session 的 mcp_servers 传入(映射到 JSON‑RPC 的 mcpServers),支持 local/stdio 与 http/sse,两类配置字段分别为 type/command/args/env/cwd/timeout/tools 或 type/url/headers/timeout/tools(来源:本地 SDK 源码)。 \nSDK hooks 使用 hooks(on_pre_tool_use/on_post_tool_use/on_user_prompt_submitted/on_session_start/on_session_end/on_error_occurred) 进行拦截与附加上下文,权限需 on_permission_request 对 kind='mcp' 明确批准,且 session.on 可审计 tool.execution_* 事件中的 mcp_server_name/mcp_tool_name。 \n示例见下,建议/落地:pipe 中启用 on_permission_request、限制 tools 白名单并记录 MCP 事件。\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def on_permission_request(req, _):\n return {\"kind\": \"approved\"} if req.get(\"kind\") == \"mcp\" else {\n \"kind\": \"denied-by-rules\",\n \"rules\": [{\"kind\": \"deny\"}],\n }\n\nasync def on_pre_tool_use(input, _):\n if input.get(\"toolName\", \"\").startswith(\"mcp.\"):\n return {\"additionalContext\": \"MCP tool call observed\"}\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n \"timeout\": 30000,\n }\n },\n \"hooks\": {\"on_pre_tool_use\": on_pre_tool_use},\n \"on_permission_request\": on_permission_request,\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 查询 Copilot SDK 新闻\"})\n await client.stop()\n\nasyncio.run(main())\n```", "model": "gpt-5.2-codex"} +{"index": 9, "question": "Provide best practices and common pitfalls for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "基于本地 SDK 源码,MCP 配置通过 `create_session`/`resume_session` 的 `mcp_servers` 进入 JSON‑RPC `mcpServers`,并通过 `on_permission_request` 与 `hooks` 控制工具审批与行为(来源:本地 SDK 源码) \n- 最佳实践:local/stdio 用 `type/command/args/env/cwd/timeout/tools`;remote http/sse 用 `type/url/headers/timeout/tools`;`tools` 尽量白名单、必要时 `[\"*\"]`;`timeout` 毫秒;`hooks.on_pre_tool_use` 做改参/拒绝,`session.on` 记录 `tool.execution_*` 与 `mcp_server_name`/`mcp_tool_name` \n- 常见坑:把 MCP 配置放在 CopilotClient;缺少 `on_permission_request`(kind=\"mcp\")导致默认拒绝;`tools: []` 等于禁用全部;设置 `available_tools` 却未包含 MCP 工具名导致被过滤 \n\n建议/落地:pipe 中固定审批策略 + 统一 MCP 配置模板 + 审计日志", "model": "gpt-5.2-codex"} +{"index": 10, "question": "Show a minimal example snippet for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "基于本地 SDK 源码,MCP 通过 `mcp_servers` 传入并用 `hooks` 处理工具调用(来源:本地 SDK 源码)。最小示例: \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n }\n },\n \"hooks\": {\n \"on_pre_tool_use\": lambda input, _: {\"additionalContext\": \"MCP tool call\"},\n },\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 搜索 Copilot SDK\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地:固定 `on_permission_request`(kind=\"mcp\")并限制 `tools` 白名单。", "model": "gpt-5.2-codex"} +{"index": 11, "question": "List recommended configuration defaults for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "推荐默认值(MCP JSON‑RPC 配置,来源:本地 SDK 源码):local/stdio 服务器用 `type=\"local\"/\"stdio\"`(远程用 `http`/`sse`)、`tools` 白名单(无则 `\"*\"`)、`timeout=30000` 毫秒、`env` 仅放密钥、`cwd` 指向受控目录。 \nHooks 默认:`on_pre_tool_use` 追加上下文并可设 `permissionDecision`(ask/allow)、必要时改参,`on_post_tool_use` 记录结果,`on_error_occurred` 设置 retry/skip。 \n建议/落地:非交互场景配置 `on_permission_request` 仅放行 `kind=\"mcp\"`,否则拒绝;工具名不确定时先不设 `available_tools`,避免误过滤。", "model": "gpt-5.2-codex"} +{"index": 12, "question": "How does MCP Server Integration: JSON-RPC config and SDK hooks relate to building a custom Agent?", "answer": "SDK 源码中,自定义 Agent 通过 `SessionConfig.custom_agents` 定义,`CustomAgentConfig` 支持 `mcp_servers`,因此“Agent 绑定 MCP”就是在 agent config 里声明 `mcp_servers`,底层在 `create_session` 时转成 JSON‑RPC 的 `mcpServers`(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \nHooks 是会话级拦截器(`on_pre_tool_use`/`on_post_tool_use` 等),对 MCP 工具同样生效;权限确认需 `on_permission_request` 审批 `kind=\"mcp\"`。 \n建议/落地:pipe 中为目标 Agent 绑定 MCP + 审批钩子,示例: \n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"custom_agents\": [{\n \"name\": \"search\",\n \"prompt\": \"Use MCP for search.\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n }\n },\n }],\n \"hooks\": {\"on_pre_tool_use\": lambda i, _: {\"additionalContext\": \"mcp\"}},\n \"on_permission_request\": lambda r, _: {\"kind\": \"approved\"},\n})\n```", "model": "gpt-5.2-codex"} +{"index": 13, "question": "Give a concise overview of Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "SDK 没有独立的 Agent Manifest 文件,等价做法是用 `SessionConfig.custom_agents` 描述 agent,并用 `tools`/`available_tools`/`excluded_tools` 限定能力。 \n权限通过 `on_permission_request` 与 `hooks`(如 `on_pre_tool_use`)在会话级控制,且 `custom_agents.tools` 只能写工具名并需在 `SessionConfig.tools` 注册。 \n建议/落地:pipe 启动时加载 manifest→注入 `custom_agents` + `available_tools` + `on_permission_request` 固化权限边界。", "model": "gpt-5.2-codex"} +{"index": 14, "question": "Provide best practices and common pitfalls for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "SDK 没有独立 Manifest 文件,等价于 `SessionConfig.custom_agents` + `tools/available_tools/excluded_tools` + `on_permission_request` + `hooks` 来定义能力与权限(来源:本地 SDK 源码)。 \n最佳实践:`custom_agents.tools` 只写已在 `SessionConfig.tools` 注册的名称,配合 `available_tools` 白名单与 `on_permission_request` 精准放行,必要时用 `hooks.on_pre_tool_use` 做参数校验/审计,`system_message` 选 append 以保留 guardrails。 \n常见坑:工具未注册或名称不一致、缺少 `on_permission_request` 导致默认拒绝、误用 `system_message` replace 去掉安全限制、期待 agent 自动被选中或跨实例不持久 `session_id`;建议/落地:pipe 启动时加载 manifest→注入 `custom_agents/available_tools/on_permission_request` 并持久化 `session_id`。", "model": "gpt-5.2-codex"} +{"index": 15, "question": "Show a minimal example snippet for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "最小示例(本地 SDK 源码:`custom_agents` + `tools` + `available_tools` + `on_permission_request`): \n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nclass PingParams(BaseModel):\n text: str = Field(description=\"text to echo\")\n\n@define_tool(name=\"priv_ping\", description=\"Privileged echo\")\ndef priv_ping(params: PingParams) -> str:\n return f\"ok: {params.text}\"\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n agent_manifest = {\n \"name\": \"priv-agent\",\n \"prompt\": \"Only use priv_ping for actions.\",\n \"tools\": [\"priv_ping\"],\n \"infer\": True,\n }\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [priv_ping],\n \"custom_agents\": [agent_manifest],\n \"available_tools\": [\"priv_ping\"],\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n })\n\n await session.send_and_wait({\"prompt\": \"调用 priv_ping\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地:保持工具名一致并白名单化,非交互务必提供 `on_permission_request`。", "model": "gpt-5.2-codex"} +{"index": 16, "question": "List recommended configuration defaults for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "基于本地 SDK 源码(copilot/types.py / copilot/client.py),推荐默认值如下: \n- custom_agents:name 必填、prompt 必填、tools 仅名称、infer=True,display_name/description 可选 \n- available_tools=白名单、excluded_tools=空、system_message=append \n- on_permission_request:除明确允许的 kind 其余拒绝;hooks.on_pre_tool_use 做审计/参数校验(其余 hooks 按需) \n建议/落地:pipe 启动时加载 manifest→注册 tools + available_tools + on_permission_request,并持久化 session_id。", "model": "gpt-5.2-codex"} +{"index": 17, "question": "How does Agent Manifests: Defining capabilities and permissions programmatically relate to building a custom Agent?", "answer": "“Agent Manifest”在 SDK 里就是 `SessionConfig.custom_agents` 的配置对象,用它定义自定义 Agent 的 `name/prompt/tools/mcp_servers/infer`,并在同一 session 里用 `tools` 注册实际工具、用 `available_tools/excluded_tools` 限定能力(来源:本地 SDK 源码)。 \n权限不在 manifest 内,而是由会话级 `on_permission_request` 和 hooks(如 `on_pre_tool_use`)统一控制,且对所有 Agent 生效。 \n建议/落地:pipe 启动时把 manifest 转成 `custom_agents` 并同步注册工具 + 白名单,再配置 `on_permission_request` 作为权限策略。", "model": "gpt-5.2-codex"} +{"index": 18, "question": "Give a concise overview of Headless Auth: Device Code Flow and credential persistence.", "answer": "SDK 仅提供 `CopilotClient` 的 `github_token/use_logged_in_user` 与 `get_auth_status()`,没有内置 device‑flow API(源码:copilot/client.py、copilot/types.py)。 \nHeadless Device Flow 需走 GitHub OAuth Device Flow HTTP 端点获取 token(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),将 token 持久化(权限 600)并通过 `CopilotClient({\"github_token\": token})` 注入,且与 `cli_url` 互斥。 \n建议/落地:启动时读 token 文件,不存在则执行 device flow 并保存,再启动客户端。\n\n```python\ntoken = load_token() or device_flow() # 调用 device code + access_token 端点\nsave_token(token) # chmod 600\nclient = CopilotClient({\"github_token\": token[\"access_token\"]})\nawait client.start()\n```", "model": "gpt-5.2-codex"} +{"index": 19, "question": "Provide best practices and common pitfalls for Headless Auth: Device Code Flow and credential persistence.", "answer": "SDK 不内置 device flow,只能用 GitHub OAuth Device Flow HTTP 换取 token 并通过 CopilotClient({github_token}) 注入,且与 cli_url 互斥(源码:copilot/client.py、copilot/types.py)。 \n最佳实践/坑:- token 文件 chmod 600 或 OS keychain;- 记录到期与刷新策略,启动时用 get_auth_status 验证;- 轮询需处理 authorization_pending/slow_down;- 未持久化、use_logged_in_user 与 github_token 同时使用或与 cli_url 混用会失败(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。 \n建议/落地:pipe 启动时读 token 文件,不存在则走 device flow 并保存,失败时回退重新授权。", "model": "gpt-5.2-codex"} +{"index": 20, "question": "Show a minimal example snippet for Headless Auth: Device Code Flow and credential persistence.", "answer": "最小示例:用 GitHub Device Flow(端点说明见 https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)拿 token,持久化后注入 CopilotClient({github_token}). \n```python\nimport asyncio\nimport json\nimport os\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\nfrom copilot import CopilotClient\n\nCLIENT_ID = \"YOUR_OAUTH_APP_CLIENT_ID\"\nTOKEN_PATH = Path.home() / \".config\" / \"myapp\" / \"copilot_token.json\"\n\ndef post(url, data):\n body = urllib.parse.urlencode(data).encode()\n req = urllib.request.Request(url, data=body, headers={\"Accept\": \"application/json\"})\n with urllib.request.urlopen(req) as resp:\n return json.loads(resp.read().decode())\n\ndef device_flow():\n code = post(\"https://github.com/login/device/code\", {\"client_id\": CLIENT_ID, \"scope\": \"read:user\"})\n print(f\"Open {code['verification_uri']} and enter {code['user_code']}\")\n interval = int(code.get(\"interval\", 5))\n while True:\n tok = post(\n \"https://github.com/login/oauth/access_token\",\n {\n \"client_id\": CLIENT_ID,\n \"device_code\": code[\"device_code\"],\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n },\n )\n if \"access_token\" in tok:\n return tok\n if tok.get(\"error\") == \"slow_down\":\n interval += 5\n elif tok.get(\"error\") != \"authorization_pending\":\n raise RuntimeError(tok)\n time.sleep(interval)\n\ndef load_token():\n return json.loads(TOKEN_PATH.read_text()).get(\"access_token\") if TOKEN_PATH.exists() else None\n\ndef save_token(tok):\n TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)\n TOKEN_PATH.write_text(json.dumps(tok))\n os.chmod(TOKEN_PATH, 0o600)\n\nasync def main():\n access_token = load_token()\n if not access_token:\n tok = device_flow()\n save_token(tok)\n access_token = tok[\"access_token\"]\n\n client = CopilotClient({\"github_token\": access_token})\n await client.start()\n # ... create_session / send ...\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地 \n- 启动先读 token 文件;缺失再 device flow \n- token 文件权限 600 \n- 失效则重新授权", "model": "gpt-5.2-codex"} +{"index": 21, "question": "List recommended configuration defaults for Headless Auth: Device Code Flow and credential persistence.", "answer": "基于本地 SDK 源码(`github_token` 与 `cli_url` 互斥)与 GitHub Device Flow 文档(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),推荐默认值: \n- Token 持久化:`~/.config//copilot_token.json` 或系统密钥链,文件权限 600 \n- Client 选项:仅 `github_token`,`use_logged_in_user` 留空/False,禁用 `cli_url` \n- 轮询:使用 device code 返回的 `interval`(缺省 5s),处理 `authorization_pending`/`slow_down` \n- 启动校验:`get_auth_status()` 失败即重新授权 \n\n建议/落地:启动先读 token 文件 → 无则 device flow → 保存 → `client.start()`", "model": "gpt-5.2-codex"} +{"index": 22, "question": "How does Headless Auth: Device Code Flow and credential persistence relate to building a custom Agent?", "answer": "Headless Auth 只负责拿到并持久化 `github_token`,让 `CopilotClient` 能在无交互环境启动会话并加载 `custom_agents`;因此它是自定义 Agent 能稳定运行的前置条件,而不是 Agent 定义本身(来源:本地 SDK 源码)。 \n建议/落地:在无状态后端启动时读取 token 文件(不存在就走 device flow)→ `CopilotClient({\"github_token\": token})` → `create_session(custom_agents=...)`。", "model": "gpt-5.2-codex"} +{"index": 23, "question": "Give a concise overview of Session Replay vs Resume: Handling stateless frontend history.", "answer": "基于本地 SDK 源码(copilot/client.py、copilot/session.py),Resume 是 `resume_session(session_id)` 保留会话状态,Replay 只能由应用重放历史 prompt。优缺点:Resume 省 tokens/保工具与 compaction 状态但依赖持久化 session_id;Replay 无状态易扩展但成本高且易丢副作用控制。建议/落地:无状态前端请保存 session_id 并优先 Resume,失效时回放最近 N 轮。", "model": "gpt-5.2-codex"} +{"index": 24, "question": "Provide best practices and common pitfalls for Session Replay vs Resume: Handling stateless frontend history.", "answer": "最佳实践:在无状态前端每次请求携带 session_id 并优先 `resume_session`,同时恢复 tools/hooks/on_permission_request/system_message 以保持行为一致(来源:本地 SDK 源码)。 \n常见坑:Replay 会重复执行工具且耗 tokens;Resume 未重新注册工具或权限处理器会出现 tool not supported/默认拒绝;会话失效未回退。 \n建议/落地:持久化 chat_id→session_id,resume 失败时仅回放最近 N 轮并禁用/幂等化有副作用的工具。", "model": "gpt-5.2-codex"} +{"index": 25, "question": "Show a minimal example snippet for Session Replay vs Resume: Handling stateless frontend history.", "answer": "来源:本地 SDK 源码(copilot/client.py, copilot/session.py),最小示例如下(优先 Resume,失败则 Replay)。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def handle_request(session_id: str | None, history: list[str], latest: str):\n client = CopilotClient()\n await client.start()\n\n try:\n # Resume: 复用会话状态\n if not session_id:\n raise RuntimeError(\"no session_id\")\n session = await client.resume_session(session_id)\n except Exception:\n # Replay: 逐条重放历史\n session = await client.create_session({\"model\": \"gpt-5\"})\n session_id = session.session_id\n for prompt in history:\n await session.send_and_wait({\"prompt\": prompt})\n\n response = await session.send_and_wait({\"prompt\": latest})\n await client.stop()\n return response.data.content, session_id\n\n# asyncio.run(handle_request(session_id, history, latest_prompt))\n```\n建议/落地:持久化 session_id,resume 失败时仅回放最近 N 轮。", "model": "gpt-5.2-codex"} +{"index": 26, "question": "List recommended configuration defaults for Session Replay vs Resume: Handling stateless frontend history.", "answer": "基于本地 SDK 源码,推荐默认配置如下(Resume / Replay): \n- Resume:持久化 session_id;`resume_session({...})` 传回 tools、hooks、on_permission_request、working_directory、streaming、provider、mcp_servers、custom_agents、skill_directories、disabled_skills;`disable_resume` 保持默认 False \n- Replay:`create_session({...})` 传回 model、system_message、tools、available_tools/excluded_tools、hooks、on_permission_request、working_directory、provider、mcp_servers、custom_agents、infinite_sessions(默认启用);仅回放最近 N 轮;默认拒绝 write/shell 或用只读白名单", "model": "gpt-5.2-codex"} +{"index": 27, "question": "How does Session Replay vs Resume: Handling stateless frontend history relate to building a custom Agent?", "answer": "Resume 会保留 session_id 关联的 custom_agents/工具状态,Replay 需要每次 create_session 时重新注入 custom_agents + tools,否则 persona/能力会丢失(来源:本地 SDK 源码)。对于无状态前端,Resume 更可靠且省 token,但要持久化 session_id 并在 resume 时重新注册 tools/hooks;Replay 则需回放历史并保证相同 custom_agents 配置。建议/落地:pipe 优先 resume_session,失败再 replay 最近 N 轮并重建 custom_agents/tools/权限策略。", "model": "gpt-5.2-codex"} +{"index": 28, "question": "Give a concise overview of Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "拦截/修改用户提示要用 `SessionHooks.on_user_prompt_submitted`(不是 `session.on` 事件订阅),`input.prompt` 可改为 `output.modifiedPrompt` 并附加 `additionalContext`/`suppressOutput`,且需在 `create_session`/`resume_session` 传入 `hooks` 才会触发 `hooks.invoke`(来源:本地 SDK 源码 copilot/types.py、copilot/session.py、copilot/client.py)。 \n```python\nfrom copilot import CopilotClient\n\nasync def on_user_prompt_submitted(input, _ctx):\n return {\"modifiedPrompt\": input[\"prompt\"].strip(), \"additionalContext\": \"normalize whitespace\"}\n\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"hooks\": {\"on_user_prompt_submitted\": on_user_prompt_submitted},\n})\n```\n建议/落地:在 pipe 入口统一做 prompt 清洗/拼接上下文,并记录修改前后用于审计。", "model": "gpt-5.2-codex"} +{"index": 29, "question": "Provide best practices and common pitfalls for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "最佳实践:在 create_session/resume_session 传入 hooks.on_user_prompt_submitted,返回 {modifiedPrompt, additionalContext, suppressOutput},修改保持幂等、最小化、可审计(来源:本地 SDK 源码 copilot/types.py、copilot/client.py、copilot/session.py)。 \n常见坑:忘记传 hooks 或 resume 时未重新传、handler 抛异常会被吞并返回 None、返回非 dict 或过度改写提示导致无效/偏航。 \n建议/落地:在 pipe 入口统一注册该 hook 并记录修改前后,保持 system_message 为 append。", "model": "gpt-5.2-codex"} +{"index": 30, "question": "Show a minimal example snippet for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "最小示例(来源:本地 SDK 源码): \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def on_user_prompt_submitted(hook_input, _ctx):\n return {\n \"modifiedPrompt\": hook_input[\"prompt\"].strip(),\n \"additionalContext\": \"normalized\",\n }\n\nasync def main():\n client = CopilotClient()\n await client.start()\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"hooks\": {\"on_user_prompt_submitted\": on_user_prompt_submitted},\n })\n await session.send_and_wait({\"prompt\": \" hello \"})\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:在 pipe 入口统一注册该 hook,并记录修改前后以便审计。", "model": "gpt-5.2-codex"} +{"index": 31, "question": "List recommended configuration defaults for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "- 推荐默认值(来源:本地 SDK 源码 copilot/types.py):`hooks={\"on_user_prompt_submitted\": handler}`,handler 默认返回 `{\"modifiedPrompt\": input[\"prompt\"], \"additionalContext\": \"\", \"suppressOutput\": False}` 或 `None`(不改写)。 \n- `create_session`/`resume_session` 都要传同一 hooks;`system_message` 保持 append 以保留 guardrails。 \n- 建议/落地:pipe 入口统一注册该 hook 并记录原/改提示用于审计。", "model": "gpt-5.2-codex"} +{"index": 32, "question": "How does Advanced Session Hooks: Intercepting and modifying user prompts relate to building a custom Agent?", "answer": "Hooks 是会话级拦截器,对所有自定义 Agent 都生效;因此它是定义 Agent 行为边界和输入规范化的“外层控制”,而不是 Agent Manifest 本身(来源:本地 SDK 源码 copilot/types.py、copilot/session.py)。 \n在自定义 Agent 场景下,`on_user_prompt_submitted` 可统一清洗/增强用户指令,再由 custom_agents 的 prompt 决定具体风格。 \n建议/落地:pipe 启动时同时注入 custom_agents + hooks,确保 resume 时同样注册 hooks。", "model": "gpt-5.2-codex"} +{"index": 33, "question": "Give a concise overview of Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 只有 `SessionConfig.working_directory` 来约束 CWD,`attachments` 也要求真实路径,没有“虚拟工作区/FS provider”协议(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。要处理远程/虚拟文件,需用自定义工具或 MCP 服务器实现 VFS 读写,或把内容落地到 working_directory 下的临时文件再交给模型。建议/落地:pipe 固定 working_directory 映射到会话工作区,统一走 vfs_* 工具/MCP,且在 on_permission_request 限制 write/shell。", "model": "gpt-5.2-codex"} +{"index": 34, "question": "Provide best practices and common pitfalls for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 没有虚拟工作区/FS provider,仅支持 `SessionConfig.working_directory` 与真实路径 `attachments`(来源:本地 SDK 源码),因此远程/虚拟文件需落地或用自定义工具/MCP 提供内容。 \n最佳实践:固定 working_directory 到可控目录/会话 workspace,VFS 工具分块返回,配合 `available_tools`/`on_permission_request` 限权;常见坑:直接传虚拟路径/URL、working_directory 不存在、resume 未重注册工具/权限、一次性返回过大内容导致上下文截断。 \n建议/落地:pipe 入口做 remote→临时文件或 vfs_* 映射,并持久化路径索引。", "model": "gpt-5.2-codex"} +{"index": 35, "question": "Show a minimal example snippet for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 仅提供 `SessionConfig.working_directory` 与真实路径附件,没有虚拟 FS/provider(来源:本地 SDK 源码 copilot/types.py)。最小做法是把远程内容落地到 CWD 下再作为附件传入: \n```python\nimport asyncio\nimport pathlib\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n workdir = \"/tmp/owui\"\n pathlib.Path(workdir).mkdir(parents=True, exist_ok=True)\n remote_content = \"...\"\n path = f\"{workdir}/remote.txt\"\n pathlib.Path(path).write_text(remote_content)\n\n session = await client.create_session({\"model\": \"gpt-5\", \"working_directory\": workdir})\n await session.send_and_wait({\n \"prompt\": \"Summarize remote.txt\",\n \"attachments\": [{\"type\": \"file\", \"path\": path}],\n })\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:pipe 入口固定 working_directory,落地临时文件后立即处理,并用 `on_permission_request` 限制 write/shell。", "model": "gpt-5.2-codex"} +{"index": 36, "question": "List recommended configuration defaults for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "推荐默认值(来源:本地 SDK 源码 copilot/types.py): \n- `working_directory`: 指向可控的会话目录(如 session.workspace_path 或 /tmp//) \n- `available_tools`: 只允许 vfs_* 或只读工具,避免任意 shell/write \n- `on_permission_request`: 默认拒绝 write/shell,仅放行 read/mcp \n- `attachments`: 仅真实路径文件;远程内容先落地 \n\n建议/落地:pipe 入口统一映射 remote→临时文件或 VFS 工具,并持久化映射表。", "model": "gpt-5.2-codex"} +{"index": 37, "question": "How does Workspace Virtualization: Handling CWD for remote/virtual files relate to building a custom Agent?", "answer": "SDK 没有虚拟文件系统协议,custom agent 只能依赖 `working_directory` 和真实路径附件,因此处理远程/虚拟文件需通过自定义工具/MCP 并在 agent prompt/available_tools 中强制使用(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \n这使“工作区虚拟化”成为 agent 能力与权限设计的一部分,需要 `available_tools`/`on_permission_request`/hooks 来限定访问范围。 \n建议/落地:pipe 中固定 working_directory 到会话目录,注入 vfs_* 工具白名单并在 prompt 明确使用 VFS。", "model": "gpt-5.2-codex"} +{"index": 38, "question": "Give a concise overview of Error Recovery: Handling session disconnects and re-auth.", "answer": "本地 SDK 源码中没有显式“重连”API,断线通常表现为请求异常或 `SessionEventType.SESSION_ERROR`,需要 `stop()/force_stop()` 后重新 `start()` 并 `resume_session`(失败则 replay)(来源:本地 SDK 源码)。 \n认证可用 `get_auth_status()` 检测,未认证时重新创建 `CopilotClient({\"github_token\": token})`;若 `cli_url` 连接外部服务器则认证由外部处理(来源:本地 SDK 源码)。\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nasync def safe_send(session_id, prompt, token):\n client = CopilotClient({\"github_token\": token})\n await client.start()\n try:\n session = await (client.resume_session(session_id) if session_id else client.create_session({\"model\": \"gpt-5\"}))\n session.on(lambda ev: ev.type == SessionEventType.SESSION_ERROR and print(ev.data.message))\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp, session.session_id\n except Exception:\n await client.stop()\n client = CopilotClient({\"github_token\": token})\n await client.start()\n session = await client.create_session({\"model\": \"gpt-5\"})\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp, session.session_id\n finally:\n await client.stop()\n```\n\n建议/落地:持久化 session_id 与 token,优先 resume,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"} +{"index": 39, "question": "Provide best practices and common pitfalls for Error Recovery: Handling session disconnects and re-auth.", "answer": "最佳实践(基于本地 SDK 源码):监听 `SESSION_ERROR`/捕获请求异常后 `stop()/force_stop()`→重新 `start()`→`resume_session`(失败再 replay),启动时用 `get_auth_status()` 校验并在失效时重建 `CopilotClient({\"github_token\": ...})`,同时在 resume 时重新注册 tools/hooks/权限处理器。 \n常见坑:把 `cli_url` 与 `github_token` 混用、忘记重注册工具导致 `tool not supported`、缺少 `on_permission_request` 导致默认拒绝、以及不回退到 replay 造成会话中断。 \n建议/落地:无状态后端持久化 `session_id+token`、实现幂等的重试/回放策略并记录错误上下文用于告警。", "model": "gpt-5.2-codex"} +{"index": 40, "question": "Show a minimal example snippet for Error Recovery: Handling session disconnects and re-auth.", "answer": "最小示例(本地 SDK 源码:copilot/client.py、copilot/session.py): \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def send_with_recovery(session_id, prompt, token):\n client = CopilotClient({\"github_token\": token})\n await client.start()\n try:\n session = await (client.resume_session(session_id) if session_id else client.create_session({\"model\": \"gpt-5\"}))\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp.data.content, session.session_id\n except Exception:\n await client.stop()\n client = CopilotClient({\"github_token\": token})\n await client.start()\n session = await client.create_session({\"model\": \"gpt-5\"})\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp.data.content, session.session_id\n finally:\n await client.stop()\n```\n建议/落地:持久化 token+session_id,resume 失败再重建并回放必要历史。", "model": "gpt-5.2-codex"} +{"index": 41, "question": "List recommended configuration defaults for Error Recovery: Handling session disconnects and re-auth.", "answer": "推荐默认值(字段来源:本地 SDK 源码):CopilotClientOptions—auto_start=True、log_level='info'、use_stdio=True(非 cli_url)、github_token=持久化 token;SessionConfig/ResumeSessionConfig—hooks.on_error_occurred=handler、on_permission_request=非交互审批、working_directory 固定、mcp_servers/custom_agents/streaming 与首次一致、可固定 config_dir 并持久化 session_id。 \n常见坑:cli_url 与 github_token 互斥、resume 时未重传 tools/hooks 导致 tool not supported 或默认拒绝。 \n建议/落地:pipe 保存 token+session_id,断线后先 resume_session,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"} +{"index": 42, "question": "How does Error Recovery: Handling session disconnects and re-auth relate to building a custom Agent?", "answer": "错误恢复与自定义 Agent 强相关:Agent 定义与工具/权限处理器都是 session 级配置,断线或重认证后必须重建客户端并重新注入 `custom_agents`、`tools`、`hooks`、`on_permission_request`,否则能力/人格会丢失(来源:本地 SDK 源码)。 \n建议/落地:持久化 session_id+token,重连优先 `resume_session` 并重新注册上述配置,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"} +{"index": 43, "question": "Give a concise overview of Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 源码里没有独立的 `confirmation_required` 事件;权限确认是 JSON‑RPC `permission.request`,通过 `SessionConfig.on_permission_request` 回调处理(缺失或异常会默认拒绝)。 \n`session.on()` 只能用于审计/关联工具事件(如 `tool.user_requested`、`tool.execution_start`),不能替代确认逻辑。", "model": "gpt-5.2-codex"} +{"index": 44, "question": "Provide best practices and common pitfalls for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 源码未定义 `confirmation_required` 事件,确认流程必须用 `SessionConfig.on_permission_request` 处理,缺失或异常会默认拒绝(来源:本地 SDK 源码)。 \n最佳实践/坑: \n- ✅ 对 `kind` 建立白名单策略(如仅允许 `read/mcp`),记录决策并用 `session.on` 仅做审计关联 `tool.user_requested` \n- ✅ `resume_session` 时重新注册 `on_permission_request`/hooks \n- ❌ 误以为 `session.on()` 可完成确认、返回非规范 dict 或抛异常导致全部拒绝", "model": "gpt-5.2-codex"} +{"index": 45, "question": "Show a minimal example snippet for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 里没有 `confirmation_required` 事件,需用 `SessionConfig.on_permission_request` 处理权限确认。 \n最小示例: \n```python\nfrom copilot import CopilotClient\n\nclient = CopilotClient()\nawait client.start()\n\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n})\n```\n建议/落地:在 pipe 中统一注册 on_permission_request,并在 resume_session 时重复注册。", "model": "gpt-5.2-codex"} +{"index": 46, "question": "List recommended configuration defaults for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "推荐默认值(SDK 无 confirmation_required 事件,改用 on_permission_request,来源:本地 SDK 源码):- 对 kind 建白名单(常见只放行 read/mcp)- 未命中默认返回 denied-by-rules 并给 rules - resume_session 时重注册 on_permission_request/hooks。 \n建议/落地:pipe 启动即注入 on_permission_request 并记录审批日志。", "model": "gpt-5.2-codex"} +{"index": 47, "question": "How does Confirmation Events: programmatic handling of 'confirmation_required' relate to building a custom Agent?", "answer": "SDK 没有 `confirmation_required` 事件,权限确认由 `create_session`/`resume_session` 的 `on_permission_request` 处理,且对同一 session 的所有 custom agent 生效(来源:本地 SDK 源码)。 \n建议/落地:在 pipe 启动时注入 `on_permission_request` 白名单策略并在 resume 时重注册,以约束高权限工具。", "model": "gpt-5.2-codex"} +{"index": 48, "question": "Give a concise overview of Skills: Conflict resolution and precedence defaults.", "answer": "在本地 SDK 源码中,Skills 只通过 SessionConfig 的 `skill_directories` 与 `disabled_skills` 暴露,未定义冲突解析/优先级规则,因此行为取决于 CLI 运行时实现(来源:本地 SDK 源码 copilot/types.py)。 \n为可控性,建议确保技能名唯一、仅配置单一目录或由你自行按顺序拼接目录,并用 `disabled_skills` 明确屏蔽同名/不应启用的技能。 \n建议/落地:在无状态后端每次 `create_session`/`resume_session` 时显式传入同一份 `skill_directories` 与 `disabled_skills`,避免因默认加载顺序差异导致行为漂移。", "model": "gpt-5.2-codex"} +{"index": 49, "question": "Provide best practices and common pitfalls for Skills: Conflict resolution and precedence defaults.", "answer": "SDK 源码仅暴露 SessionConfig/ResumeSessionConfig 的 `skill_directories` 和 `disabled_skills`,未定义冲突解析或优先级,行为由 CLI 侧决定(来源:本地 SDK 源码 copilot/types.py)。 \n最佳实践:技能名全局唯一、只配置单一目录或显式排序,并用 `disabled_skills` 禁用冲突项;常见坑是依赖默认加载顺序或 resume 未重传这两个字段导致行为漂移。 \n建议/落地:在 pipe 启动与 resume 时统一注入同一份 `skill_directories`/`disabled_skills` 并记录变更。", "model": "gpt-5.2-codex"} +{"index": 50, "question": "Show a minimal example snippet for Skills: Conflict resolution and precedence defaults.", "answer": "SDK 里技能只通过 `skill_directories` 与 `disabled_skills` 配置,冲突/优先级未在源码定义,需自行避免同名。 \n最小示例: \n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"skill_directories\": [\"/opt/skills\"],\n \"disabled_skills\": [\"legacy_skill\"],\n})\n``` \n建议:技能名唯一并在 `resume_session` 时重传这两项。", "model": "gpt-5.2-codex"} +{"index": 51, "question": "List recommended configuration defaults for Skills: Conflict resolution and precedence defaults.", "answer": "推荐默认值(基于 SDK 仅暴露 `skill_directories`/`disabled_skills`): \n- `skill_directories`: 明确设置为单一目录(或你自己控制顺序的列表),不要依赖 CLI 默认搜索顺序 \n- `disabled_skills`: 默认空列表;如存在同名/旧技能则显式禁用 \n- Resume/Replay:始终复用同一份配置以避免“隐式优先级”漂移", "model": "gpt-5.2-codex"} +{"index": 52, "question": "How does Skills: Conflict resolution and precedence defaults relate to building a custom Agent?", "answer": "Skills 是会话级配置(`skill_directories`/`disabled_skills`),会影响所有自定义 Agent 的可用能力与提示行为;SDK 不定义冲突优先级,因此需要你在构建自定义 Agent 时确保技能命名唯一并显式禁用冲突项(来源:本地 SDK 源码)。 \n建议/落地:为 custom agent 固定同一份 skills 配置,并在 resume 时重复传入,避免行为漂移。", "model": "gpt-5.2-codex"} +{"index": 53, "question": "Give a concise overview of Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "本地 SDK 源码里 JsonRpcClient 没有内置 JSON‑RPC trace,只能通过 `CopilotClient({\"log_level\":\"debug\"})` 打开 CLI 侧日志(非原始包)(来源:本地 SDK 源码)。 \n要抓实际 JSON‑RPC 流量,可 monkey‑patch `JsonRpcClient._send_message`/`_read_message` 或包装 stdio 进行 tee,最小示例如下。 \n建议/落地:在 pipe 中开启 patch 并对日志做脱敏与采样。 \n```python\nfrom copilot.jsonrpc import JsonRpcClient\n\norig_send = JsonRpcClient._send_message\nasync def _send(self, message):\n print(\">>\", message)\n return await orig_send(self, message)\nJsonRpcClient._send_message = _send\n\norig_read = JsonRpcClient._read_message\ndef _read(self):\n msg = orig_read(self)\n if msg:\n print(\"<<\", msg)\n return msg\nJsonRpcClient._read_message = _read\n```", "model": "gpt-5.2-codex"} +{"index": 54, "question": "Provide best practices and common pitfalls for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "源码里 JsonRpcClient 没有内置 trace,只能用 `CopilotClient({\"log_level\":\"debug\"})` 查看 CLI 侧日志或 monkey‑patch `JsonRpcClient._send_message/_read_message` 做 tee(来源:本地 SDK 源码)。 \n最佳实践:仅在调试启用、对 github_token/API key/内容脱敏、采样/限流、只读不改消息并注意线程安全;常见坑:记录完整 prompt/工具参数导致泄露,或同步打印阻塞 IO 造成超时/断连。", "model": "gpt-5.2-codex"} +{"index": 55, "question": "Show a minimal example snippet for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "最小示例(monkey‑patch `JsonRpcClient` 打印收发): \n```python\nfrom copilot.jsonrpc import JsonRpcClient\n\norig_send = JsonRpcClient._send_message\nasync def _send(self, msg):\n print(\">>\", msg)\n return await orig_send(self, msg)\nJsonRpcClient._send_message = _send\n\norig_read = JsonRpcClient._read_message\ndef _read(self):\n msg = orig_read(self)\n if msg:\n print(\"<<\", msg)\n return msg\nJsonRpcClient._read_message = _read\n```\n建议:仅在调试启用并脱敏日志。", "model": "gpt-5.2-codex"} +{"index": 56, "question": "List recommended configuration defaults for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "推荐默认值(基于 SDK 源码):- 生产环境关闭 trace;- 调试时 `CopilotClient({\"log_level\":\"debug\"})`;- 如需 JSON‑RPC 报文,采用临时 monkey‑patch 并启用脱敏/采样;- 不修改消息内容,避免线程阻塞。 \n建议/落地:pipe 中通过环境变量开关调试与采样率。", "model": "gpt-5.2-codex"} +{"index": 57, "question": "How does Debugging: Tracing JSON-RPC traffic in the SDK relate to building a custom Agent?", "answer": "JSON‑RPC trace 能看到 custom_agents 在 `session.create/session.resume` 的实际 payload,以及后续 `tool.call`/`permission.request`/`hooks.invoke` 是否按预期触发,从而验证自定义 Agent 的能力与权限边界(来源:本地 SDK 源码)。 \n建议/落地:仅在开发环境开启并脱敏日志;用它核对 customAgents/tools/权限决策与 MCP 配置。", "model": "gpt-5.2-codex"} +{"index": 58, "question": "Give a concise overview of Billing & Policies: How seat management affects SDK features.", "answer": "- 访问与政策:SDK 依赖 Copilot CLI,需有效 Copilot 订阅且组织/企业未禁用 CLI,否则无法使用(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md) \n- 计费与认证:每次 prompt 计入 Copilot premium requests 配额,PAT 需 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)\n\n建议/落地:启动先 `get_auth_status()`,失败时提示分配席位/启用 CLI 或更新 token", "model": "gpt-5.2-codex"} +{"index": 59, "question": "Provide best practices and common pitfalls for Billing & Policies: How seat management affects SDK features.", "answer": "最佳实践:确认用户/组织席位允许 Copilot CLI/SDK 使用,启动前用 `get_auth_status()` 预检,使用 PAT 时确保具备 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n常见坑:未分配席位或组织/企业策略禁用 CLI 导致 SDK 失效、PAT 权限不足或 premium requests 用尽引发认证/配额错误(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:在 pipe 启动时做席位与鉴权检测并给出明确提示/自助链接,记录每次请求计费与配额告警阈值以便限流。", "model": "gpt-5.2-codex"} +{"index": 60, "question": "Show a minimal example snippet for Billing & Policies: How seat management affects SDK features.", "answer": "基于 SDK README,最小示例是在启动时用 `get_auth_status()` 检查是否已分配席位/可用认证(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md)。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n auth = await client.get_auth_status()\n if not auth.isAuthenticated:\n raise RuntimeError(\"Copilot 未认证/未分配席位,请先登录或分配权限\")\n\n # ...正常创建 session...\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:pipe 启动先做该检查,失败时提示分配席位或更新 PAT 权限。", "model": "gpt-5.2-codex"} +{"index": 61, "question": "List recommended configuration defaults for Billing & Policies: How seat management affects SDK features.", "answer": "推荐默认值:CopilotClient 用 `use_logged_in_user=True`、`auto_start=True`、`log_level=\"info\"`;如用 PAT 则用 `github_token` 注入并确保具备 Copilot Requests 权限,且启动前 `get_auth_status()` 失败即按席位/策略限制处理(来源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:pipe 启动先做 auth 预检并给出续订/分配席位提示,避免会话中途才触发权限或配额错误。", "model": "gpt-5.2-codex"} +{"index": 62, "question": "How does Billing & Policies: How seat management affects SDK features relate to building a custom Agent?", "answer": "席位与组织策略决定 Copilot CLI/SDK 是否可用,因此自定义 Agent 只是同一会话上的配置,前提仍是有有效订阅与允许的策略,否则 Agent 无法运行(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:在创建自定义 Agent 前先 `get_auth_status()` 预检并对配额/政策限制做降级提示。", "model": "gpt-5.2-codex"} +{"index": 63, "question": "How to configure MCP servers in ~/.copilot/config.json for SDK usage?", "answer": "我在 CLI 文档里未看到 `~/.copilot/config.json` 的 MCP 配置结构;CLI 仅提供 `/mcp add|edit|delete|show` 命令管理配置(来源:Copilot CLI 文档)。 \n因此建议用 CLI 写入配置,并在 SDK 中用 `config_dir` 指向该配置目录;或直接在 SDK 的 `create_session(mcp_servers=...)` 传入。 \n如果你确认存在 `~/.copilot/config.json` 的具体 schema,请提供示例或路径,我可按你的文件格式给出精确配置。", "model": "gpt-5.2-codex"} +{"index": 64, "question": "What CLI environment variables force 'Agent' mode vs 'Generic' mode?", "answer": "CLI 文档未提到任何“通过环境变量强制 Agent 模式/Generic 模式”的开关(源:Copilot CLI 文档)。 \n已知与模式相关的是命令/参数,例如 `--agent` 选择自定义 agent,或交互内 `/agent` 切换;但不是环境变量。 \n如果你指的是内部实验开关,请提供具体变量名或版本线索,我可继续追索。", "model": "gpt-5.2-codex"} +{"index": 65, "question": "Explain the 'confirmation' flow in CLI and how it maps to SDK events.", "answer": "- CLI:工具权限弹窗确认,可用 `/allow-all` 或 `/yolo` 自动批准。(源: https://raw.githubusercontent.com/github/copilot-cli/main/README.md) \n- SDK:对应 JSON‑RPC `permission.request`→`on_permission_request` 返回 `approved/denied-*`,`session.on()` 只用于审计 tool.execution_*,无独立 confirmation 事件。(源: 本地 SDK 源码 copilot/client.py、copilot/session.py、copilot/types.py)\n\n示例:\n```python\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nclient = CopilotClient()\nawait client.start()\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n})\nsession.on(lambda ev: ev.type == SessionEventType.TOOL_EXECUTION_START and print(ev.data.tool_name))\n```\n\n建议/落地:非交互模式实现 `on_permission_request` 并记录 `tool.execution_*` 事件。", "model": "gpt-5.2-codex"} +{"index": 66, "question": "Does the CLI support 'dry-run' permission checks for tools?", "answer": "CLI 文档未提到“dry-run”权限检查,只有正常确认与 `/allow-all`、`/yolo` 自动批准(源:https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。SDK 侧仅有 `permission.request`→`on_permission_request` 与 `hooks.on_pre_tool_use` 的拦截/拒绝(本地 SDK 源码),不提供不执行的试运行;建议/落地:pipe 中用 `on_permission_request` 记录/拒绝高风险工具并在 hooks 做审计。", "model": "gpt-5.2-codex"} +{"index": 67, "question": "What are the undocumented requirements for 'workspace' context updates?", "answer": "CLI 文档与本地 SDK 源码都未提到“workspace context updates”的未公开要求;SDK 只暴露 `working_directory` 与 `session.workspace_path`,以及 `attachments` 真实路径(来源:本地 SDK 源码)。 \n如果你指的是 CLI 的“工作区/会话文件”更新规则,请提供具体页面或日志片段,我可以进一步定位。", "model": "gpt-5.2-codex"} +{"index": 68, "question": "How does the CLI handle 'device code' re-authentication automatically?", "answer": "CLI 文档未说明“自动 device code 重新认证”的具体机制;只提到未登录时使用 `/login`,PAT 可通过 `GH_TOKEN/GITHUB_TOKEN` 提供(源:Copilot CLI 文档)。因此无法确认是否有自动 re-auth 或缓存刷新策略。 \n若需无交互场景,请使用 SDK 的 `github_token` 注入并自行实现 device flow 轮询与持久化。", "model": "gpt-5.2-codex"} diff --git a/plugins/debug/copilot_sdk_research/outputs/copilot_sdk_guide_20260130_114642.md b/plugins/debug/copilot_sdk_research/outputs/copilot_sdk_guide_20260130_114642.md new file mode 100644 index 0000000..d189381 --- /dev/null +++ b/plugins/debug/copilot_sdk_research/outputs/copilot_sdk_guide_20260130_114642.md @@ -0,0 +1,1105 @@ +# GitHub Copilot SDK & CLI 研究报告 + +- **model**: gpt-5.2-codex +- **questions**: 68 +- **timestamp**: 20260130_114642 +- **language**: zh-CN + +--- + +## Q: Provide a python code example for configuring `CopilotClient` to connect to a local MCP server (e.g. Brave Search) via `CopilotClient` config. + +SDK 源码显示 MCP 服务器通过 `create_session`/`resume_session` 的 `mcp_servers` 配置传入并映射为 `mcpServers`,不是 CopilotClient 构造参数(源:本地源码 copilot/types.py / copilot/client.py)。下面是本地 stdio MCP(以 Brave Search 为例,命令/密钥请替换)的 Python 示例。 +```python +import asyncio +from copilot import CopilotClient + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "model": "gpt-5", + "mcp_servers": { + "brave_search": { + "type": "local", # 或 "stdio" + "command": "/path/to/brave-search-mcp", # 替换为你的 MCP server 启动命令 + "args": [], + "env": {"BRAVE_API_KEY": "YOUR_KEY"}, + "tools": ["*"], + "timeout": 30000, # 毫秒 + } + }, + }) + + await session.send_and_wait({"prompt": "用 Brave Search 查询 OpenAI 最新公告"}) + await client.stop() + +asyncio.run(main()) +``` + +要点/坑: +- `tools`: `["*"]` 表示全量,`[]` 表示不启用 +- `timeout`: 毫秒 +- `command/args/env`: 按你的 MCP server 实际启动方式替换 + +建议/落地: +- pipe 内复用同一 session,结束时统一 `client.stop()` + +## Q: Explain how to programmatically handle `tool.confirmation_required` events in a non-interactive stream using `session.on()`. + +SDK 源码未定义 `tool.confirmation_required` 事件,权限确认由 JSON‑RPC `permission.request` 触发并交给 `on_permission_request` 回调处理,`session.on()` 只能订阅 `tool.user_requested`/`tool.execution_start` 等事件做记录关联(来源:本地源码 copilot/client.py、copilot/session.py、copilot/types.py)。 +在非交互流里必须提供 `on_permission_request` 自动审批/拒绝,否则默认拒绝且回调异常也会被拒绝。 + +要点/坑 +- 配置键: `on_permission_request`(SessionConfig) +- 权限类型: `shell` / `write` / `mcp` / `read` / `url` +- 回调异常或缺失 -> `denied-no-approval-rule-and-could-not-request-from-user` + +```python +import asyncio +from copilot import CopilotClient +from copilot.generated.session_events import SessionEventType + +async def main(): + client = CopilotClient() + await client.start() + + def on_event(ev): + if ev.type == SessionEventType.TOOL_USER_REQUESTED: + print(f"tool requested: {ev.data.tool_name} {ev.data.tool_call_id}") + elif ev.type == SessionEventType.TOOL_EXECUTION_START: + print(f"tool start: {ev.data.tool_name}") + + async def on_permission_request(req, ctx): + kind = req.get("kind") + if kind in ("read", "mcp"): + return {"kind": "approved"} + return {"kind": "denied-by-rules", "rules": [{"kind": "deny-non-read"}]} + + session = await client.create_session({ + "model": "gpt-5", + "on_permission_request": on_permission_request, + }) + session.on(on_event) + + await session.send_and_wait({"prompt": "请调用需要权限的工具"}) + await client.stop() + +asyncio.run(main()) +``` + +建议/落地 +- pipe 启动时注册审批策略 + `session.on` 记录 `tool_call_id` 做审计 + +## Q: Show how to implement a 'Device Flow' login helper using SDK primitives (if available) or raw HTTP showing how to persist credentials. + +SDK 本身没有 device‑flow 登录 API;可用鉴权原语只有 CopilotClient 的 `github_token`/`use_logged_in_user` 与 `get_auth_status`(源:本地源码 copilot/client.py、copilot/types.py)。 +下面用 GitHub OAuth Device Flow 原始 HTTP 获取 token、持久化本地并注入 `github_token`(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。 + +```python +import asyncio +import json +import os +import time +import urllib.parse +import urllib.request +from pathlib import Path + +from copilot import CopilotClient + +CLIENT_ID = "YOUR_OAUTH_APP_CLIENT_ID" +SCOPE = "read:user" # 按你的 OAuth App 需求调整 +TOKEN_PATH = Path.home() / ".config" / "myapp" / "copilot_token.json" + +def http_post(url, data): + body = urllib.parse.urlencode(data).encode() + req = urllib.request.Request(url, data=body, headers={"Accept": "application/json"}) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + +def load_token(): + if TOKEN_PATH.exists(): + return json.loads(TOKEN_PATH.read_text()).get("access_token") + return None + +def save_token(token): + TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True) + TOKEN_PATH.write_text(json.dumps(token)) + os.chmod(TOKEN_PATH, 0o600) + +def device_flow(): + code = http_post( + "https://github.com/login/device/code", + {"client_id": CLIENT_ID, "scope": SCOPE}, + ) + print(f"Open {code['verification_uri']} and enter {code['user_code']}") + interval = int(code.get("interval", 5)) + + while True: + token = http_post( + "https://github.com/login/oauth/access_token", + { + "client_id": CLIENT_ID, + "device_code": code["device_code"], + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + }, + ) + if "access_token" in token: + return token + if token.get("error") == "slow_down": + interval += 5 + elif token.get("error") not in ("authorization_pending",): + raise RuntimeError(token) + time.sleep(interval) + +async def main(): + access_token = load_token() + if not access_token: + token = device_flow() + save_token(token) + access_token = token["access_token"] + + client = CopilotClient({"github_token": access_token}) + await client.start() + # ... create_session / send ... + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) +``` + +要点/坑 +- `cli_url` 与 `github_token` 互斥 +- SDK 不保存凭据 需自行持久化 +- 轮询需处理 `authorization_pending` / `slow_down` + +建议/落地 +- pipe 启动时读取 token 文件 不存在则走 device flow +- 用 `github_token` 注入 保持非交互运行 + +## Q: Compare the pros and cons of 'Session Replay' (fast-forwarding history) vs 'Session Resume' (stateful ID) for a stateless web backend like OpenWebUI. + +对无状态后端(OpenWebUI)而言,SDK 只支持通过 `resume_session(session_id)` 的状态续接;所谓“Session Replay”需要你自行重放历史 prompt(来源:本地 SDK 源码 copilot/client.py、copilot/session.py)。 +对比要点(短语): +- Replay:无状态/易扩展;缺点是耗时耗 tokens、易截断/丢工具状态、可能重复副作用 +- Resume:省 tokens、保留工具/compaction/`workspace_path` 状态;缺点是需持久化 session_id、依赖会话存活/跨实例一致性。 +建议/落地:优先持久化 chat_id→session_id 并 resume_session,失效时回退为重放最近 N 轮并重建工具上下文。 + +## Q: Detail the exact protocol for 'Virtual Workspace': how to implement a file system provider that feeds content to Copilot without physical files. + +SDK 源码里没有“Virtual Workspace”协议或文件系统 provider 接口;会话只识别真实路径(`SessionConfig.working_directory`、`MessageOptions.attachments`)。要在无物理文件情况下供给内容,只能用自定义工具或 MCP 服务器把“读写文件”变成 tool 调用(来源:本地 SDK 源码 copilot/types.py / copilot/client.py)。 + +可用原语(源码) +- `SessionConfig.tools` / `available_tools`:自定义工具与白名单 +- `MessageOptions.attachments`:仅支持真实路径文件/目录 + +方案 A:自定义工具实现 VFS(推荐) +```python +import asyncio +from pydantic import BaseModel, Field +from copilot import CopilotClient, define_tool + +VFS = {"README.md": "# Virtual README\n内容来自内存\n"} + +class VfsReadParams(BaseModel): + path: str = Field(description="virtual path") + +@define_tool(name="vfs_read", description="Read a virtual file") +def vfs_read(params: VfsReadParams) -> str: + return VFS.get(params.path, "") + +@define_tool(name="vfs_list", description="List virtual files") +def vfs_list() -> list[str]: + return sorted(VFS.keys()) + +async def main(): + client = CopilotClient() + await client.start() + session = await client.create_session({ + "model": "gpt-5", + "tools": [vfs_read, vfs_list], + "available_tools": ["vfs_read", "vfs_list"], + "system_message": {"content": "Use vfs_* tools for all file access."}, + }) + await session.send_and_wait({"prompt": "打开 README.md"}) + await client.stop() + +asyncio.run(main()) +``` + +方案 B:MCP 服务器(本地 stdio,VFS 由你实现) +```python +session = await client.create_session({ + "model": "gpt-5", + "mcp_servers": { + "vfs": { + "type": "local", + "command": "python", + "args": ["-m", "my_vfs_mcp_server"], + "tools": ["*"], + } + }, +}) +``` + +注意/坑 +- `available_tools` 只保留你的 VFS 工具以强制使用 +- 内置文件工具只读真实文件;要兼容只能落地临时文件 +- 大文件需分块返回,避免上下文爆炸 + +建议/落地 +- pipe 内维护 VFS 存储(DB/Redis/内存) +- 启动 session 时注入 `system_message` + `available_tools` 强制 VFS 入口 + +## Q: Create an 'Agent Manifest' example: how to define an Agent capable of specific high-privileged tools via SDK. + +SDK 没有独立“Agent Manifest”文件格式,等价于 `create_session` 的 `custom_agents`(CustomAgentConfig),并用 `tools`/`available_tools` 约束高权限工具,权限由 `on_permission_request` 处理(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 +示例如下(manifest 为 Python dict,可序列化为 JSON 配置)。 + +```python +import asyncio +from pydantic import BaseModel, Field +from copilot import CopilotClient, define_tool + +class AdminShellParams(BaseModel): + cmd: str = Field(description="approved shell command") + +@define_tool(name="admin_shell", description="Run an approved shell command") +def admin_shell(params: AdminShellParams) -> str: + # TODO: 执行受控命令(白名单/沙箱) + return f"approved: {params.cmd}" + +class AdminWriteParams(BaseModel): + path: str = Field(description="target path") + content: str = Field(description="content") + +@define_tool(name="admin_write", description="Write to privileged store") +def admin_write(params: AdminWriteParams) -> str: + # TODO: 受控写入 + return "ok" + +async def on_permission_request(req, _ctx): + if req.get("kind") in ("shell", "write"): + return {"kind": "approved"} + return {"kind": "denied-by-rules", "rules": [{"kind": "deny"}]} + +async def main(): + client = CopilotClient() + await client.start() + + agent_manifest = { + "name": "ops-admin", + "display_name": "Ops Admin", + "description": "Privileged ops agent", + "prompt": "Use only admin_* tools for privileged ops.", + "tools": ["admin_shell", "admin_write"], + "infer": True, + } + + session = await client.create_session({ + "model": "gpt-5", + "tools": [admin_shell, admin_write], + "custom_agents": [agent_manifest], + "available_tools": ["admin_shell", "admin_write"], + "on_permission_request": on_permission_request, + }) + + await session.send_and_wait({"prompt": "使用 ops-admin 执行受控操作"}) + await client.stop() + +asyncio.run(main()) +``` + +要点/坑 +- session.tools 未注册 -> agent.tools 不可用 +- custom_agents.tools 仅写名称 +- 非交互缺少 on_permission_request -> 默认拒绝 +- SDK 无显式 agent 选择参数 -> 需在 prompt 指定 + +建议/落地 +- pipe 启动时加载 manifest 注入 custom_agents + available_tools + on_permission_request + +## Q: List all 'hidden' `SessionConfig` parameters relevant to Agent behavior and personality. + +基于本地 SDK 源码 `copilot/types.py` 的 SessionConfig,影响 Agent 行为/人格的相关参数如下(来源:本地 SDK 源码)。 +- `system_message`: `append`/`replace`;`replace` 会移除 SDK guardrails(风险) +- `custom_agents`: `prompt`/`description`/`display_name`/`tools`/`mcp_servers`/`infer`;`tools` 仅名称且必须在 `tools` 注册 +- `tools` / `available_tools` / `excluded_tools`: 能力与行为边界;`available_tools` 优先于 `excluded_tools` +- `hooks`: `on_user_prompt_submitted`/`on_pre_tool_use`/`on_post_tool_use`/`on_session_start`/`on_session_end`/`on_error_occurred`,可改写 prompt/工具参数/结果 +- `on_permission_request`: 非交互审批策略;缺失或异常 => 拒绝 +- `on_user_input_request`: 启用/处理 `ask_user` +- `mcp_servers`: 注入外部工具集 +- `skill_directories` / `disabled_skills`: 技能启用/禁用 +- `model` / `provider`: 模型与 BYOK 影响输出风格与能力 +- `working_directory`: 上下文范围 +- `infinite_sessions`: compaction 阈值影响记忆保留 +- `config_dir`: CLI 配置/状态目录,行为取决于 CLI 侧配置 + +建议/落地:在 pipe 启动时固定 `system_message` + `available_tools/excluded_tools` + `hooks`,并持久化 session_id 以便 `resume_session`。 + +## Q: Give a concise overview of MCP Server Integration: JSON-RPC config and SDK hooks. + +SDK 中 MCP 通过 create_session/resume_session 的 mcp_servers 传入(映射到 JSON‑RPC 的 mcpServers),支持 local/stdio 与 http/sse,两类配置字段分别为 type/command/args/env/cwd/timeout/tools 或 type/url/headers/timeout/tools(来源:本地 SDK 源码)。 +SDK hooks 使用 hooks(on_pre_tool_use/on_post_tool_use/on_user_prompt_submitted/on_session_start/on_session_end/on_error_occurred) 进行拦截与附加上下文,权限需 on_permission_request 对 kind='mcp' 明确批准,且 session.on 可审计 tool.execution_* 事件中的 mcp_server_name/mcp_tool_name。 +示例见下,建议/落地:pipe 中启用 on_permission_request、限制 tools 白名单并记录 MCP 事件。 + +```python +import asyncio +from copilot import CopilotClient + +async def on_permission_request(req, _): + return {"kind": "approved"} if req.get("kind") == "mcp" else { + "kind": "denied-by-rules", + "rules": [{"kind": "deny"}], + } + +async def on_pre_tool_use(input, _): + if input.get("toolName", "").startswith("mcp."): + return {"additionalContext": "MCP tool call observed"} + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "model": "gpt-5", + "mcp_servers": { + "brave": { + "type": "local", + "command": "/path/to/brave-mcp", + "args": [], + "env": {"BRAVE_API_KEY": "YOUR_KEY"}, + "tools": ["*"], + "timeout": 30000, + } + }, + "hooks": {"on_pre_tool_use": on_pre_tool_use}, + "on_permission_request": on_permission_request, + }) + + await session.send_and_wait({"prompt": "用 Brave Search 查询 Copilot SDK 新闻"}) + await client.stop() + +asyncio.run(main()) +``` + +## Q: Provide best practices and common pitfalls for MCP Server Integration: JSON-RPC config and SDK hooks. + +基于本地 SDK 源码,MCP 配置通过 `create_session`/`resume_session` 的 `mcp_servers` 进入 JSON‑RPC `mcpServers`,并通过 `on_permission_request` 与 `hooks` 控制工具审批与行为(来源:本地 SDK 源码) +- 最佳实践:local/stdio 用 `type/command/args/env/cwd/timeout/tools`;remote http/sse 用 `type/url/headers/timeout/tools`;`tools` 尽量白名单、必要时 `["*"]`;`timeout` 毫秒;`hooks.on_pre_tool_use` 做改参/拒绝,`session.on` 记录 `tool.execution_*` 与 `mcp_server_name`/`mcp_tool_name` +- 常见坑:把 MCP 配置放在 CopilotClient;缺少 `on_permission_request`(kind="mcp")导致默认拒绝;`tools: []` 等于禁用全部;设置 `available_tools` 却未包含 MCP 工具名导致被过滤 + +建议/落地:pipe 中固定审批策略 + 统一 MCP 配置模板 + 审计日志 + +## Q: Show a minimal example snippet for MCP Server Integration: JSON-RPC config and SDK hooks. + +基于本地 SDK 源码,MCP 通过 `mcp_servers` 传入并用 `hooks` 处理工具调用(来源:本地 SDK 源码)。最小示例: +```python +import asyncio +from copilot import CopilotClient + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "model": "gpt-5", + "mcp_servers": { + "brave": { + "type": "local", + "command": "/path/to/brave-mcp", + "args": [], + "env": {"BRAVE_API_KEY": "YOUR_KEY"}, + "tools": ["*"], + } + }, + "hooks": { + "on_pre_tool_use": lambda input, _: {"additionalContext": "MCP tool call"}, + }, + "on_permission_request": lambda req, _: {"kind": "approved"}, + }) + + await session.send_and_wait({"prompt": "用 Brave Search 搜索 Copilot SDK"}) + await client.stop() + +asyncio.run(main()) +``` + +建议/落地:固定 `on_permission_request`(kind="mcp")并限制 `tools` 白名单。 + +## Q: List recommended configuration defaults for MCP Server Integration: JSON-RPC config and SDK hooks. + +推荐默认值(MCP JSON‑RPC 配置,来源:本地 SDK 源码):local/stdio 服务器用 `type="local"/"stdio"`(远程用 `http`/`sse`)、`tools` 白名单(无则 `"*"`)、`timeout=30000` 毫秒、`env` 仅放密钥、`cwd` 指向受控目录。 +Hooks 默认:`on_pre_tool_use` 追加上下文并可设 `permissionDecision`(ask/allow)、必要时改参,`on_post_tool_use` 记录结果,`on_error_occurred` 设置 retry/skip。 +建议/落地:非交互场景配置 `on_permission_request` 仅放行 `kind="mcp"`,否则拒绝;工具名不确定时先不设 `available_tools`,避免误过滤。 + +## Q: How does MCP Server Integration: JSON-RPC config and SDK hooks relate to building a custom Agent? + +SDK 源码中,自定义 Agent 通过 `SessionConfig.custom_agents` 定义,`CustomAgentConfig` 支持 `mcp_servers`,因此“Agent 绑定 MCP”就是在 agent config 里声明 `mcp_servers`,底层在 `create_session` 时转成 JSON‑RPC 的 `mcpServers`(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 +Hooks 是会话级拦截器(`on_pre_tool_use`/`on_post_tool_use` 等),对 MCP 工具同样生效;权限确认需 `on_permission_request` 审批 `kind="mcp"`。 +建议/落地:pipe 中为目标 Agent 绑定 MCP + 审批钩子,示例: +```python +session = await client.create_session({ + "model": "gpt-5", + "custom_agents": [{ + "name": "search", + "prompt": "Use MCP for search.", + "mcp_servers": { + "brave": { + "type": "local", + "command": "/path/to/brave-mcp", + "args": [], + "env": {"BRAVE_API_KEY": "YOUR_KEY"}, + "tools": ["*"], + } + }, + }], + "hooks": {"on_pre_tool_use": lambda i, _: {"additionalContext": "mcp"}}, + "on_permission_request": lambda r, _: {"kind": "approved"}, +}) +``` + +## Q: Give a concise overview of Agent Manifests: Defining capabilities and permissions programmatically. + +SDK 没有独立的 Agent Manifest 文件,等价做法是用 `SessionConfig.custom_agents` 描述 agent,并用 `tools`/`available_tools`/`excluded_tools` 限定能力。 +权限通过 `on_permission_request` 与 `hooks`(如 `on_pre_tool_use`)在会话级控制,且 `custom_agents.tools` 只能写工具名并需在 `SessionConfig.tools` 注册。 +建议/落地:pipe 启动时加载 manifest→注入 `custom_agents` + `available_tools` + `on_permission_request` 固化权限边界。 + +## Q: Provide best practices and common pitfalls for Agent Manifests: Defining capabilities and permissions programmatically. + +SDK 没有独立 Manifest 文件,等价于 `SessionConfig.custom_agents` + `tools/available_tools/excluded_tools` + `on_permission_request` + `hooks` 来定义能力与权限(来源:本地 SDK 源码)。 +最佳实践:`custom_agents.tools` 只写已在 `SessionConfig.tools` 注册的名称,配合 `available_tools` 白名单与 `on_permission_request` 精准放行,必要时用 `hooks.on_pre_tool_use` 做参数校验/审计,`system_message` 选 append 以保留 guardrails。 +常见坑:工具未注册或名称不一致、缺少 `on_permission_request` 导致默认拒绝、误用 `system_message` replace 去掉安全限制、期待 agent 自动被选中或跨实例不持久 `session_id`;建议/落地:pipe 启动时加载 manifest→注入 `custom_agents/available_tools/on_permission_request` 并持久化 `session_id`。 + +## Q: Show a minimal example snippet for Agent Manifests: Defining capabilities and permissions programmatically. + +最小示例(本地 SDK 源码:`custom_agents` + `tools` + `available_tools` + `on_permission_request`): +```python +import asyncio +from pydantic import BaseModel, Field +from copilot import CopilotClient, define_tool + +class PingParams(BaseModel): + text: str = Field(description="text to echo") + +@define_tool(name="priv_ping", description="Privileged echo") +def priv_ping(params: PingParams) -> str: + return f"ok: {params.text}" + +async def main(): + client = CopilotClient() + await client.start() + + agent_manifest = { + "name": "priv-agent", + "prompt": "Only use priv_ping for actions.", + "tools": ["priv_ping"], + "infer": True, + } + + session = await client.create_session({ + "model": "gpt-5", + "tools": [priv_ping], + "custom_agents": [agent_manifest], + "available_tools": ["priv_ping"], + "on_permission_request": lambda req, _: {"kind": "approved"}, + }) + + await session.send_and_wait({"prompt": "调用 priv_ping"}) + await client.stop() + +asyncio.run(main()) +``` + +建议/落地:保持工具名一致并白名单化,非交互务必提供 `on_permission_request`。 + +## Q: List recommended configuration defaults for Agent Manifests: Defining capabilities and permissions programmatically. + +基于本地 SDK 源码(copilot/types.py / copilot/client.py),推荐默认值如下: +- custom_agents:name 必填、prompt 必填、tools 仅名称、infer=True,display_name/description 可选 +- available_tools=白名单、excluded_tools=空、system_message=append +- on_permission_request:除明确允许的 kind 其余拒绝;hooks.on_pre_tool_use 做审计/参数校验(其余 hooks 按需) +建议/落地:pipe 启动时加载 manifest→注册 tools + available_tools + on_permission_request,并持久化 session_id。 + +## Q: How does Agent Manifests: Defining capabilities and permissions programmatically relate to building a custom Agent? + +“Agent Manifest”在 SDK 里就是 `SessionConfig.custom_agents` 的配置对象,用它定义自定义 Agent 的 `name/prompt/tools/mcp_servers/infer`,并在同一 session 里用 `tools` 注册实际工具、用 `available_tools/excluded_tools` 限定能力(来源:本地 SDK 源码)。 +权限不在 manifest 内,而是由会话级 `on_permission_request` 和 hooks(如 `on_pre_tool_use`)统一控制,且对所有 Agent 生效。 +建议/落地:pipe 启动时把 manifest 转成 `custom_agents` 并同步注册工具 + 白名单,再配置 `on_permission_request` 作为权限策略。 + +## Q: Give a concise overview of Headless Auth: Device Code Flow and credential persistence. + +SDK 仅提供 `CopilotClient` 的 `github_token/use_logged_in_user` 与 `get_auth_status()`,没有内置 device‑flow API(源码:copilot/client.py、copilot/types.py)。 +Headless Device Flow 需走 GitHub OAuth Device Flow HTTP 端点获取 token(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),将 token 持久化(权限 600)并通过 `CopilotClient({"github_token": token})` 注入,且与 `cli_url` 互斥。 +建议/落地:启动时读 token 文件,不存在则执行 device flow 并保存,再启动客户端。 + +```python +token = load_token() or device_flow() # 调用 device code + access_token 端点 +save_token(token) # chmod 600 +client = CopilotClient({"github_token": token["access_token"]}) +await client.start() +``` + +## Q: Provide best practices and common pitfalls for Headless Auth: Device Code Flow and credential persistence. + +SDK 不内置 device flow,只能用 GitHub OAuth Device Flow HTTP 换取 token 并通过 CopilotClient({github_token}) 注入,且与 cli_url 互斥(源码:copilot/client.py、copilot/types.py)。 +最佳实践/坑:- token 文件 chmod 600 或 OS keychain;- 记录到期与刷新策略,启动时用 get_auth_status 验证;- 轮询需处理 authorization_pending/slow_down;- 未持久化、use_logged_in_user 与 github_token 同时使用或与 cli_url 混用会失败(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。 +建议/落地:pipe 启动时读 token 文件,不存在则走 device flow 并保存,失败时回退重新授权。 + +## Q: Show a minimal example snippet for Headless Auth: Device Code Flow and credential persistence. + +最小示例:用 GitHub Device Flow(端点说明见 https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)拿 token,持久化后注入 CopilotClient({github_token}). +```python +import asyncio +import json +import os +import time +import urllib.parse +import urllib.request +from pathlib import Path +from copilot import CopilotClient + +CLIENT_ID = "YOUR_OAUTH_APP_CLIENT_ID" +TOKEN_PATH = Path.home() / ".config" / "myapp" / "copilot_token.json" + +def post(url, data): + body = urllib.parse.urlencode(data).encode() + req = urllib.request.Request(url, data=body, headers={"Accept": "application/json"}) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + +def device_flow(): + code = post("https://github.com/login/device/code", {"client_id": CLIENT_ID, "scope": "read:user"}) + print(f"Open {code['verification_uri']} and enter {code['user_code']}") + interval = int(code.get("interval", 5)) + while True: + tok = post( + "https://github.com/login/oauth/access_token", + { + "client_id": CLIENT_ID, + "device_code": code["device_code"], + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + }, + ) + if "access_token" in tok: + return tok + if tok.get("error") == "slow_down": + interval += 5 + elif tok.get("error") != "authorization_pending": + raise RuntimeError(tok) + time.sleep(interval) + +def load_token(): + return json.loads(TOKEN_PATH.read_text()).get("access_token") if TOKEN_PATH.exists() else None + +def save_token(tok): + TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True) + TOKEN_PATH.write_text(json.dumps(tok)) + os.chmod(TOKEN_PATH, 0o600) + +async def main(): + access_token = load_token() + if not access_token: + tok = device_flow() + save_token(tok) + access_token = tok["access_token"] + + client = CopilotClient({"github_token": access_token}) + await client.start() + # ... create_session / send ... + await client.stop() + +asyncio.run(main()) +``` + +建议/落地 +- 启动先读 token 文件;缺失再 device flow +- token 文件权限 600 +- 失效则重新授权 + +## Q: List recommended configuration defaults for Headless Auth: Device Code Flow and credential persistence. + +基于本地 SDK 源码(`github_token` 与 `cli_url` 互斥)与 GitHub Device Flow 文档(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),推荐默认值: +- Token 持久化:`~/.config//copilot_token.json` 或系统密钥链,文件权限 600 +- Client 选项:仅 `github_token`,`use_logged_in_user` 留空/False,禁用 `cli_url` +- 轮询:使用 device code 返回的 `interval`(缺省 5s),处理 `authorization_pending`/`slow_down` +- 启动校验:`get_auth_status()` 失败即重新授权 + +建议/落地:启动先读 token 文件 → 无则 device flow → 保存 → `client.start()` + +## Q: How does Headless Auth: Device Code Flow and credential persistence relate to building a custom Agent? + +Headless Auth 只负责拿到并持久化 `github_token`,让 `CopilotClient` 能在无交互环境启动会话并加载 `custom_agents`;因此它是自定义 Agent 能稳定运行的前置条件,而不是 Agent 定义本身(来源:本地 SDK 源码)。 +建议/落地:在无状态后端启动时读取 token 文件(不存在就走 device flow)→ `CopilotClient({"github_token": token})` → `create_session(custom_agents=...)`。 + +## Q: Give a concise overview of Session Replay vs Resume: Handling stateless frontend history. + +基于本地 SDK 源码(copilot/client.py、copilot/session.py),Resume 是 `resume_session(session_id)` 保留会话状态,Replay 只能由应用重放历史 prompt。优缺点:Resume 省 tokens/保工具与 compaction 状态但依赖持久化 session_id;Replay 无状态易扩展但成本高且易丢副作用控制。建议/落地:无状态前端请保存 session_id 并优先 Resume,失效时回放最近 N 轮。 + +## Q: Provide best practices and common pitfalls for Session Replay vs Resume: Handling stateless frontend history. + +最佳实践:在无状态前端每次请求携带 session_id 并优先 `resume_session`,同时恢复 tools/hooks/on_permission_request/system_message 以保持行为一致(来源:本地 SDK 源码)。 +常见坑:Replay 会重复执行工具且耗 tokens;Resume 未重新注册工具或权限处理器会出现 tool not supported/默认拒绝;会话失效未回退。 +建议/落地:持久化 chat_id→session_id,resume 失败时仅回放最近 N 轮并禁用/幂等化有副作用的工具。 + +## Q: Show a minimal example snippet for Session Replay vs Resume: Handling stateless frontend history. + +来源:本地 SDK 源码(copilot/client.py, copilot/session.py),最小示例如下(优先 Resume,失败则 Replay)。 +```python +import asyncio +from copilot import CopilotClient + +async def handle_request(session_id: str | None, history: list[str], latest: str): + client = CopilotClient() + await client.start() + + try: + # Resume: 复用会话状态 + if not session_id: + raise RuntimeError("no session_id") + session = await client.resume_session(session_id) + except Exception: + # Replay: 逐条重放历史 + session = await client.create_session({"model": "gpt-5"}) + session_id = session.session_id + for prompt in history: + await session.send_and_wait({"prompt": prompt}) + + response = await session.send_and_wait({"prompt": latest}) + await client.stop() + return response.data.content, session_id + +# asyncio.run(handle_request(session_id, history, latest_prompt)) +``` +建议/落地:持久化 session_id,resume 失败时仅回放最近 N 轮。 + +## Q: List recommended configuration defaults for Session Replay vs Resume: Handling stateless frontend history. + +基于本地 SDK 源码,推荐默认配置如下(Resume / Replay): +- Resume:持久化 session_id;`resume_session({...})` 传回 tools、hooks、on_permission_request、working_directory、streaming、provider、mcp_servers、custom_agents、skill_directories、disabled_skills;`disable_resume` 保持默认 False +- Replay:`create_session({...})` 传回 model、system_message、tools、available_tools/excluded_tools、hooks、on_permission_request、working_directory、provider、mcp_servers、custom_agents、infinite_sessions(默认启用);仅回放最近 N 轮;默认拒绝 write/shell 或用只读白名单 + +## Q: How does Session Replay vs Resume: Handling stateless frontend history relate to building a custom Agent? + +Resume 会保留 session_id 关联的 custom_agents/工具状态,Replay 需要每次 create_session 时重新注入 custom_agents + tools,否则 persona/能力会丢失(来源:本地 SDK 源码)。对于无状态前端,Resume 更可靠且省 token,但要持久化 session_id 并在 resume 时重新注册 tools/hooks;Replay 则需回放历史并保证相同 custom_agents 配置。建议/落地:pipe 优先 resume_session,失败再 replay 最近 N 轮并重建 custom_agents/tools/权限策略。 + +## Q: Give a concise overview of Advanced Session Hooks: Intercepting and modifying user prompts. + +拦截/修改用户提示要用 `SessionHooks.on_user_prompt_submitted`(不是 `session.on` 事件订阅),`input.prompt` 可改为 `output.modifiedPrompt` 并附加 `additionalContext`/`suppressOutput`,且需在 `create_session`/`resume_session` 传入 `hooks` 才会触发 `hooks.invoke`(来源:本地 SDK 源码 copilot/types.py、copilot/session.py、copilot/client.py)。 +```python +from copilot import CopilotClient + +async def on_user_prompt_submitted(input, _ctx): + return {"modifiedPrompt": input["prompt"].strip(), "additionalContext": "normalize whitespace"} + +session = await client.create_session({ + "model": "gpt-5", + "hooks": {"on_user_prompt_submitted": on_user_prompt_submitted}, +}) +``` +建议/落地:在 pipe 入口统一做 prompt 清洗/拼接上下文,并记录修改前后用于审计。 + +## Q: Provide best practices and common pitfalls for Advanced Session Hooks: Intercepting and modifying user prompts. + +最佳实践:在 create_session/resume_session 传入 hooks.on_user_prompt_submitted,返回 {modifiedPrompt, additionalContext, suppressOutput},修改保持幂等、最小化、可审计(来源:本地 SDK 源码 copilot/types.py、copilot/client.py、copilot/session.py)。 +常见坑:忘记传 hooks 或 resume 时未重新传、handler 抛异常会被吞并返回 None、返回非 dict 或过度改写提示导致无效/偏航。 +建议/落地:在 pipe 入口统一注册该 hook 并记录修改前后,保持 system_message 为 append。 + +## Q: Show a minimal example snippet for Advanced Session Hooks: Intercepting and modifying user prompts. + +最小示例(来源:本地 SDK 源码): +```python +import asyncio +from copilot import CopilotClient + +async def on_user_prompt_submitted(hook_input, _ctx): + return { + "modifiedPrompt": hook_input["prompt"].strip(), + "additionalContext": "normalized", + } + +async def main(): + client = CopilotClient() + await client.start() + session = await client.create_session({ + "model": "gpt-5", + "hooks": {"on_user_prompt_submitted": on_user_prompt_submitted}, + }) + await session.send_and_wait({"prompt": " hello "}) + await client.stop() + +asyncio.run(main()) +``` +建议/落地:在 pipe 入口统一注册该 hook,并记录修改前后以便审计。 + +## Q: List recommended configuration defaults for Advanced Session Hooks: Intercepting and modifying user prompts. + +- 推荐默认值(来源:本地 SDK 源码 copilot/types.py):`hooks={"on_user_prompt_submitted": handler}`,handler 默认返回 `{"modifiedPrompt": input["prompt"], "additionalContext": "", "suppressOutput": False}` 或 `None`(不改写)。 +- `create_session`/`resume_session` 都要传同一 hooks;`system_message` 保持 append 以保留 guardrails。 +- 建议/落地:pipe 入口统一注册该 hook 并记录原/改提示用于审计。 + +## Q: How does Advanced Session Hooks: Intercepting and modifying user prompts relate to building a custom Agent? + +Hooks 是会话级拦截器,对所有自定义 Agent 都生效;因此它是定义 Agent 行为边界和输入规范化的“外层控制”,而不是 Agent Manifest 本身(来源:本地 SDK 源码 copilot/types.py、copilot/session.py)。 +在自定义 Agent 场景下,`on_user_prompt_submitted` 可统一清洗/增强用户指令,再由 custom_agents 的 prompt 决定具体风格。 +建议/落地:pipe 启动时同时注入 custom_agents + hooks,确保 resume 时同样注册 hooks。 + +## Q: Give a concise overview of Workspace Virtualization: Handling CWD for remote/virtual files. + +SDK 只有 `SessionConfig.working_directory` 来约束 CWD,`attachments` 也要求真实路径,没有“虚拟工作区/FS provider”协议(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。要处理远程/虚拟文件,需用自定义工具或 MCP 服务器实现 VFS 读写,或把内容落地到 working_directory 下的临时文件再交给模型。建议/落地:pipe 固定 working_directory 映射到会话工作区,统一走 vfs_* 工具/MCP,且在 on_permission_request 限制 write/shell。 + +## Q: Provide best practices and common pitfalls for Workspace Virtualization: Handling CWD for remote/virtual files. + +SDK 没有虚拟工作区/FS provider,仅支持 `SessionConfig.working_directory` 与真实路径 `attachments`(来源:本地 SDK 源码),因此远程/虚拟文件需落地或用自定义工具/MCP 提供内容。 +最佳实践:固定 working_directory 到可控目录/会话 workspace,VFS 工具分块返回,配合 `available_tools`/`on_permission_request` 限权;常见坑:直接传虚拟路径/URL、working_directory 不存在、resume 未重注册工具/权限、一次性返回过大内容导致上下文截断。 +建议/落地:pipe 入口做 remote→临时文件或 vfs_* 映射,并持久化路径索引。 + +## Q: Show a minimal example snippet for Workspace Virtualization: Handling CWD for remote/virtual files. + +SDK 仅提供 `SessionConfig.working_directory` 与真实路径附件,没有虚拟 FS/provider(来源:本地 SDK 源码 copilot/types.py)。最小做法是把远程内容落地到 CWD 下再作为附件传入: +```python +import asyncio +import pathlib +from copilot import CopilotClient + +async def main(): + client = CopilotClient() + await client.start() + + workdir = "/tmp/owui" + pathlib.Path(workdir).mkdir(parents=True, exist_ok=True) + remote_content = "..." + path = f"{workdir}/remote.txt" + pathlib.Path(path).write_text(remote_content) + + session = await client.create_session({"model": "gpt-5", "working_directory": workdir}) + await session.send_and_wait({ + "prompt": "Summarize remote.txt", + "attachments": [{"type": "file", "path": path}], + }) + await client.stop() + +asyncio.run(main()) +``` +建议/落地:pipe 入口固定 working_directory,落地临时文件后立即处理,并用 `on_permission_request` 限制 write/shell。 + +## Q: List recommended configuration defaults for Workspace Virtualization: Handling CWD for remote/virtual files. + +推荐默认值(来源:本地 SDK 源码 copilot/types.py): +- `working_directory`: 指向可控的会话目录(如 session.workspace_path 或 /tmp//) +- `available_tools`: 只允许 vfs_* 或只读工具,避免任意 shell/write +- `on_permission_request`: 默认拒绝 write/shell,仅放行 read/mcp +- `attachments`: 仅真实路径文件;远程内容先落地 + +建议/落地:pipe 入口统一映射 remote→临时文件或 VFS 工具,并持久化映射表。 + +## Q: How does Workspace Virtualization: Handling CWD for remote/virtual files relate to building a custom Agent? + +SDK 没有虚拟文件系统协议,custom agent 只能依赖 `working_directory` 和真实路径附件,因此处理远程/虚拟文件需通过自定义工具/MCP 并在 agent prompt/available_tools 中强制使用(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 +这使“工作区虚拟化”成为 agent 能力与权限设计的一部分,需要 `available_tools`/`on_permission_request`/hooks 来限定访问范围。 +建议/落地:pipe 中固定 working_directory 到会话目录,注入 vfs_* 工具白名单并在 prompt 明确使用 VFS。 + +## Q: Give a concise overview of Error Recovery: Handling session disconnects and re-auth. + +本地 SDK 源码中没有显式“重连”API,断线通常表现为请求异常或 `SessionEventType.SESSION_ERROR`,需要 `stop()/force_stop()` 后重新 `start()` 并 `resume_session`(失败则 replay)(来源:本地 SDK 源码)。 +认证可用 `get_auth_status()` 检测,未认证时重新创建 `CopilotClient({"github_token": token})`;若 `cli_url` 连接外部服务器则认证由外部处理(来源:本地 SDK 源码)。 + +```python +import asyncio +from copilot import CopilotClient +from copilot.generated.session_events import SessionEventType + +async def safe_send(session_id, prompt, token): + client = CopilotClient({"github_token": token}) + await client.start() + try: + session = await (client.resume_session(session_id) if session_id else client.create_session({"model": "gpt-5"})) + session.on(lambda ev: ev.type == SessionEventType.SESSION_ERROR and print(ev.data.message)) + resp = await session.send_and_wait({"prompt": prompt}) + return resp, session.session_id + except Exception: + await client.stop() + client = CopilotClient({"github_token": token}) + await client.start() + session = await client.create_session({"model": "gpt-5"}) + resp = await session.send_and_wait({"prompt": prompt}) + return resp, session.session_id + finally: + await client.stop() +``` + +建议/落地:持久化 session_id 与 token,优先 resume,失败再回放最近 N 轮。 + +## Q: Provide best practices and common pitfalls for Error Recovery: Handling session disconnects and re-auth. + +最佳实践(基于本地 SDK 源码):监听 `SESSION_ERROR`/捕获请求异常后 `stop()/force_stop()`→重新 `start()`→`resume_session`(失败再 replay),启动时用 `get_auth_status()` 校验并在失效时重建 `CopilotClient({"github_token": ...})`,同时在 resume 时重新注册 tools/hooks/权限处理器。 +常见坑:把 `cli_url` 与 `github_token` 混用、忘记重注册工具导致 `tool not supported`、缺少 `on_permission_request` 导致默认拒绝、以及不回退到 replay 造成会话中断。 +建议/落地:无状态后端持久化 `session_id+token`、实现幂等的重试/回放策略并记录错误上下文用于告警。 + +## Q: Show a minimal example snippet for Error Recovery: Handling session disconnects and re-auth. + +最小示例(本地 SDK 源码:copilot/client.py、copilot/session.py): +```python +import asyncio +from copilot import CopilotClient + +async def send_with_recovery(session_id, prompt, token): + client = CopilotClient({"github_token": token}) + await client.start() + try: + session = await (client.resume_session(session_id) if session_id else client.create_session({"model": "gpt-5"})) + resp = await session.send_and_wait({"prompt": prompt}) + return resp.data.content, session.session_id + except Exception: + await client.stop() + client = CopilotClient({"github_token": token}) + await client.start() + session = await client.create_session({"model": "gpt-5"}) + resp = await session.send_and_wait({"prompt": prompt}) + return resp.data.content, session.session_id + finally: + await client.stop() +``` +建议/落地:持久化 token+session_id,resume 失败再重建并回放必要历史。 + +## Q: List recommended configuration defaults for Error Recovery: Handling session disconnects and re-auth. + +推荐默认值(字段来源:本地 SDK 源码):CopilotClientOptions—auto_start=True、log_level='info'、use_stdio=True(非 cli_url)、github_token=持久化 token;SessionConfig/ResumeSessionConfig—hooks.on_error_occurred=handler、on_permission_request=非交互审批、working_directory 固定、mcp_servers/custom_agents/streaming 与首次一致、可固定 config_dir 并持久化 session_id。 +常见坑:cli_url 与 github_token 互斥、resume 时未重传 tools/hooks 导致 tool not supported 或默认拒绝。 +建议/落地:pipe 保存 token+session_id,断线后先 resume_session,失败再回放最近 N 轮。 + +## Q: How does Error Recovery: Handling session disconnects and re-auth relate to building a custom Agent? + +错误恢复与自定义 Agent 强相关:Agent 定义与工具/权限处理器都是 session 级配置,断线或重认证后必须重建客户端并重新注入 `custom_agents`、`tools`、`hooks`、`on_permission_request`,否则能力/人格会丢失(来源:本地 SDK 源码)。 +建议/落地:持久化 session_id+token,重连优先 `resume_session` 并重新注册上述配置,失败再回放最近 N 轮。 + +## Q: Give a concise overview of Confirmation Events: programmatic handling of 'confirmation_required'. + +SDK 源码里没有独立的 `confirmation_required` 事件;权限确认是 JSON‑RPC `permission.request`,通过 `SessionConfig.on_permission_request` 回调处理(缺失或异常会默认拒绝)。 +`session.on()` 只能用于审计/关联工具事件(如 `tool.user_requested`、`tool.execution_start`),不能替代确认逻辑。 + +## Q: Provide best practices and common pitfalls for Confirmation Events: programmatic handling of 'confirmation_required'. + +SDK 源码未定义 `confirmation_required` 事件,确认流程必须用 `SessionConfig.on_permission_request` 处理,缺失或异常会默认拒绝(来源:本地 SDK 源码)。 +最佳实践/坑: +- ✅ 对 `kind` 建立白名单策略(如仅允许 `read/mcp`),记录决策并用 `session.on` 仅做审计关联 `tool.user_requested` +- ✅ `resume_session` 时重新注册 `on_permission_request`/hooks +- ❌ 误以为 `session.on()` 可完成确认、返回非规范 dict 或抛异常导致全部拒绝 + +## Q: Show a minimal example snippet for Confirmation Events: programmatic handling of 'confirmation_required'. + +SDK 里没有 `confirmation_required` 事件,需用 `SessionConfig.on_permission_request` 处理权限确认。 +最小示例: +```python +from copilot import CopilotClient + +client = CopilotClient() +await client.start() + +session = await client.create_session({ + "model": "gpt-5", + "on_permission_request": lambda req, _: {"kind": "approved"}, +}) +``` +建议/落地:在 pipe 中统一注册 on_permission_request,并在 resume_session 时重复注册。 + +## Q: List recommended configuration defaults for Confirmation Events: programmatic handling of 'confirmation_required'. + +推荐默认值(SDK 无 confirmation_required 事件,改用 on_permission_request,来源:本地 SDK 源码):- 对 kind 建白名单(常见只放行 read/mcp)- 未命中默认返回 denied-by-rules 并给 rules - resume_session 时重注册 on_permission_request/hooks。 +建议/落地:pipe 启动即注入 on_permission_request 并记录审批日志。 + +## Q: How does Confirmation Events: programmatic handling of 'confirmation_required' relate to building a custom Agent? + +SDK 没有 `confirmation_required` 事件,权限确认由 `create_session`/`resume_session` 的 `on_permission_request` 处理,且对同一 session 的所有 custom agent 生效(来源:本地 SDK 源码)。 +建议/落地:在 pipe 启动时注入 `on_permission_request` 白名单策略并在 resume 时重注册,以约束高权限工具。 + +## Q: Give a concise overview of Skills: Conflict resolution and precedence defaults. + +在本地 SDK 源码中,Skills 只通过 SessionConfig 的 `skill_directories` 与 `disabled_skills` 暴露,未定义冲突解析/优先级规则,因此行为取决于 CLI 运行时实现(来源:本地 SDK 源码 copilot/types.py)。 +为可控性,建议确保技能名唯一、仅配置单一目录或由你自行按顺序拼接目录,并用 `disabled_skills` 明确屏蔽同名/不应启用的技能。 +建议/落地:在无状态后端每次 `create_session`/`resume_session` 时显式传入同一份 `skill_directories` 与 `disabled_skills`,避免因默认加载顺序差异导致行为漂移。 + +## Q: Provide best practices and common pitfalls for Skills: Conflict resolution and precedence defaults. + +SDK 源码仅暴露 SessionConfig/ResumeSessionConfig 的 `skill_directories` 和 `disabled_skills`,未定义冲突解析或优先级,行为由 CLI 侧决定(来源:本地 SDK 源码 copilot/types.py)。 +最佳实践:技能名全局唯一、只配置单一目录或显式排序,并用 `disabled_skills` 禁用冲突项;常见坑是依赖默认加载顺序或 resume 未重传这两个字段导致行为漂移。 +建议/落地:在 pipe 启动与 resume 时统一注入同一份 `skill_directories`/`disabled_skills` 并记录变更。 + +## Q: Show a minimal example snippet for Skills: Conflict resolution and precedence defaults. + +SDK 里技能只通过 `skill_directories` 与 `disabled_skills` 配置,冲突/优先级未在源码定义,需自行避免同名。 +最小示例: +```python +session = await client.create_session({ + "model": "gpt-5", + "skill_directories": ["/opt/skills"], + "disabled_skills": ["legacy_skill"], +}) +``` +建议:技能名唯一并在 `resume_session` 时重传这两项。 + +## Q: List recommended configuration defaults for Skills: Conflict resolution and precedence defaults. + +推荐默认值(基于 SDK 仅暴露 `skill_directories`/`disabled_skills`): +- `skill_directories`: 明确设置为单一目录(或你自己控制顺序的列表),不要依赖 CLI 默认搜索顺序 +- `disabled_skills`: 默认空列表;如存在同名/旧技能则显式禁用 +- Resume/Replay:始终复用同一份配置以避免“隐式优先级”漂移 + +## Q: How does Skills: Conflict resolution and precedence defaults relate to building a custom Agent? + +Skills 是会话级配置(`skill_directories`/`disabled_skills`),会影响所有自定义 Agent 的可用能力与提示行为;SDK 不定义冲突优先级,因此需要你在构建自定义 Agent 时确保技能命名唯一并显式禁用冲突项(来源:本地 SDK 源码)。 +建议/落地:为 custom agent 固定同一份 skills 配置,并在 resume 时重复传入,避免行为漂移。 + +## Q: Give a concise overview of Debugging: Tracing JSON-RPC traffic in the SDK. + +本地 SDK 源码里 JsonRpcClient 没有内置 JSON‑RPC trace,只能通过 `CopilotClient({"log_level":"debug"})` 打开 CLI 侧日志(非原始包)(来源:本地 SDK 源码)。 +要抓实际 JSON‑RPC 流量,可 monkey‑patch `JsonRpcClient._send_message`/`_read_message` 或包装 stdio 进行 tee,最小示例如下。 +建议/落地:在 pipe 中开启 patch 并对日志做脱敏与采样。 +```python +from copilot.jsonrpc import JsonRpcClient + +orig_send = JsonRpcClient._send_message +async def _send(self, message): + print(">>", message) + return await orig_send(self, message) +JsonRpcClient._send_message = _send + +orig_read = JsonRpcClient._read_message +def _read(self): + msg = orig_read(self) + if msg: + print("<<", msg) + return msg +JsonRpcClient._read_message = _read +``` + +## Q: Provide best practices and common pitfalls for Debugging: Tracing JSON-RPC traffic in the SDK. + +源码里 JsonRpcClient 没有内置 trace,只能用 `CopilotClient({"log_level":"debug"})` 查看 CLI 侧日志或 monkey‑patch `JsonRpcClient._send_message/_read_message` 做 tee(来源:本地 SDK 源码)。 +最佳实践:仅在调试启用、对 github_token/API key/内容脱敏、采样/限流、只读不改消息并注意线程安全;常见坑:记录完整 prompt/工具参数导致泄露,或同步打印阻塞 IO 造成超时/断连。 + +## Q: Show a minimal example snippet for Debugging: Tracing JSON-RPC traffic in the SDK. + +最小示例(monkey‑patch `JsonRpcClient` 打印收发): +```python +from copilot.jsonrpc import JsonRpcClient + +orig_send = JsonRpcClient._send_message +async def _send(self, msg): + print(">>", msg) + return await orig_send(self, msg) +JsonRpcClient._send_message = _send + +orig_read = JsonRpcClient._read_message +def _read(self): + msg = orig_read(self) + if msg: + print("<<", msg) + return msg +JsonRpcClient._read_message = _read +``` +建议:仅在调试启用并脱敏日志。 + +## Q: List recommended configuration defaults for Debugging: Tracing JSON-RPC traffic in the SDK. + +推荐默认值(基于 SDK 源码):- 生产环境关闭 trace;- 调试时 `CopilotClient({"log_level":"debug"})`;- 如需 JSON‑RPC 报文,采用临时 monkey‑patch 并启用脱敏/采样;- 不修改消息内容,避免线程阻塞。 +建议/落地:pipe 中通过环境变量开关调试与采样率。 + +## Q: How does Debugging: Tracing JSON-RPC traffic in the SDK relate to building a custom Agent? + +JSON‑RPC trace 能看到 custom_agents 在 `session.create/session.resume` 的实际 payload,以及后续 `tool.call`/`permission.request`/`hooks.invoke` 是否按预期触发,从而验证自定义 Agent 的能力与权限边界(来源:本地 SDK 源码)。 +建议/落地:仅在开发环境开启并脱敏日志;用它核对 customAgents/tools/权限决策与 MCP 配置。 + +## Q: Give a concise overview of Billing & Policies: How seat management affects SDK features. + +- 访问与政策:SDK 依赖 Copilot CLI,需有效 Copilot 订阅且组织/企业未禁用 CLI,否则无法使用(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md) +- 计费与认证:每次 prompt 计入 Copilot premium requests 配额,PAT 需 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md) + +建议/落地:启动先 `get_auth_status()`,失败时提示分配席位/启用 CLI 或更新 token + +## Q: Provide best practices and common pitfalls for Billing & Policies: How seat management affects SDK features. + +最佳实践:确认用户/组织席位允许 Copilot CLI/SDK 使用,启动前用 `get_auth_status()` 预检,使用 PAT 时确保具备 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 +常见坑:未分配席位或组织/企业策略禁用 CLI 导致 SDK 失效、PAT 权限不足或 premium requests 用尽引发认证/配额错误(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 +建议/落地:在 pipe 启动时做席位与鉴权检测并给出明确提示/自助链接,记录每次请求计费与配额告警阈值以便限流。 + +## Q: Show a minimal example snippet for Billing & Policies: How seat management affects SDK features. + +基于 SDK README,最小示例是在启动时用 `get_auth_status()` 检查是否已分配席位/可用认证(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md)。 +```python +import asyncio +from copilot import CopilotClient + +async def main(): + client = CopilotClient() + await client.start() + + auth = await client.get_auth_status() + if not auth.isAuthenticated: + raise RuntimeError("Copilot 未认证/未分配席位,请先登录或分配权限") + + # ...正常创建 session... + await client.stop() + +asyncio.run(main()) +``` +建议/落地:pipe 启动先做该检查,失败时提示分配席位或更新 PAT 权限。 + +## Q: List recommended configuration defaults for Billing & Policies: How seat management affects SDK features. + +推荐默认值:CopilotClient 用 `use_logged_in_user=True`、`auto_start=True`、`log_level="info"`;如用 PAT 则用 `github_token` 注入并确保具备 Copilot Requests 权限,且启动前 `get_auth_status()` 失败即按席位/策略限制处理(来源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 +建议/落地:pipe 启动先做 auth 预检并给出续订/分配席位提示,避免会话中途才触发权限或配额错误。 + +## Q: How does Billing & Policies: How seat management affects SDK features relate to building a custom Agent? + +席位与组织策略决定 Copilot CLI/SDK 是否可用,因此自定义 Agent 只是同一会话上的配置,前提仍是有有效订阅与允许的策略,否则 Agent 无法运行(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 +建议/落地:在创建自定义 Agent 前先 `get_auth_status()` 预检并对配额/政策限制做降级提示。 + +## Q: How to configure MCP servers in ~/.copilot/config.json for SDK usage? + +我在 CLI 文档里未看到 `~/.copilot/config.json` 的 MCP 配置结构;CLI 仅提供 `/mcp add|edit|delete|show` 命令管理配置(来源:Copilot CLI 文档)。 +因此建议用 CLI 写入配置,并在 SDK 中用 `config_dir` 指向该配置目录;或直接在 SDK 的 `create_session(mcp_servers=...)` 传入。 +如果你确认存在 `~/.copilot/config.json` 的具体 schema,请提供示例或路径,我可按你的文件格式给出精确配置。 + +## Q: What CLI environment variables force 'Agent' mode vs 'Generic' mode? + +CLI 文档未提到任何“通过环境变量强制 Agent 模式/Generic 模式”的开关(源:Copilot CLI 文档)。 +已知与模式相关的是命令/参数,例如 `--agent` 选择自定义 agent,或交互内 `/agent` 切换;但不是环境变量。 +如果你指的是内部实验开关,请提供具体变量名或版本线索,我可继续追索。 + +## Q: Explain the 'confirmation' flow in CLI and how it maps to SDK events. + +- CLI:工具权限弹窗确认,可用 `/allow-all` 或 `/yolo` 自动批准。(源: https://raw.githubusercontent.com/github/copilot-cli/main/README.md) +- SDK:对应 JSON‑RPC `permission.request`→`on_permission_request` 返回 `approved/denied-*`,`session.on()` 只用于审计 tool.execution_*,无独立 confirmation 事件。(源: 本地 SDK 源码 copilot/client.py、copilot/session.py、copilot/types.py) + +示例: +```python +from copilot import CopilotClient +from copilot.generated.session_events import SessionEventType + +client = CopilotClient() +await client.start() +session = await client.create_session({ + "model": "gpt-5", + "on_permission_request": lambda req, _: {"kind": "approved"}, +}) +session.on(lambda ev: ev.type == SessionEventType.TOOL_EXECUTION_START and print(ev.data.tool_name)) +``` + +建议/落地:非交互模式实现 `on_permission_request` 并记录 `tool.execution_*` 事件。 + +## Q: Does the CLI support 'dry-run' permission checks for tools? + +CLI 文档未提到“dry-run”权限检查,只有正常确认与 `/allow-all`、`/yolo` 自动批准(源:https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。SDK 侧仅有 `permission.request`→`on_permission_request` 与 `hooks.on_pre_tool_use` 的拦截/拒绝(本地 SDK 源码),不提供不执行的试运行;建议/落地:pipe 中用 `on_permission_request` 记录/拒绝高风险工具并在 hooks 做审计。 + +## Q: What are the undocumented requirements for 'workspace' context updates? + +CLI 文档与本地 SDK 源码都未提到“workspace context updates”的未公开要求;SDK 只暴露 `working_directory` 与 `session.workspace_path`,以及 `attachments` 真实路径(来源:本地 SDK 源码)。 +如果你指的是 CLI 的“工作区/会话文件”更新规则,请提供具体页面或日志片段,我可以进一步定位。 + +## Q: How does the CLI handle 'device code' re-authentication automatically? + +CLI 文档未说明“自动 device code 重新认证”的具体机制;只提到未登录时使用 `/login`,PAT 可通过 `GH_TOKEN/GITHUB_TOKEN` 提供(源:Copilot CLI 文档)。因此无法确认是否有自动 re-auth 或缓存刷新策略。 +若需无交互场景,请使用 SDK 的 `github_token` 注入并自行实现 device flow 轮询与持久化。 + diff --git a/plugins/debug/copilot_sdk_research/test_multi_rule_permission.py b/plugins/debug/copilot_sdk_research/test_multi_rule_permission.py new file mode 100644 index 0000000..22c2a7d --- /dev/null +++ b/plugins/debug/copilot_sdk_research/test_multi_rule_permission.py @@ -0,0 +1,186 @@ +""" +Test multi-rule permission control pattern (VSCode-style) +Tests ordered rule matching like VSCode's chat.tools.terminal.autoApprove +SAFE VERSION: Uses harmless commands (echo/ls) only. No rm, no git. +""" + +import argparse +import asyncio +import json +import logging +import re +from typing import Any, Dict, List, Tuple + +from copilot import CopilotClient + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +def build_multi_rule_handler(rules_json: str): + """ + Build permission handler with ordered rules (VSCode-style) + """ + try: + rules = json.loads(rules_json) if rules_json else {} + except json.JSONDecodeError as e: + logger.error("Invalid rules JSON: %s", e) + rules = {} + + async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]): + kind = request.get("kind") + command = request.get("fullCommandText", "") or request.get("command", "") + + # Always approve read and url + if kind in ("read", "url"): + return {"kind": "approved"} + + # For shell commands, apply ordered rules + if kind == "shell" and command: + for pattern, approved in rules.items(): + try: + if re.match(pattern, command): + if approved: + logger.info( + "✅ Approved (rule match): pattern=%r command=%r", + pattern, + command, + ) + return {"kind": "approved"} + else: + logger.warning( + "❌ Denied (rule match): pattern=%r command=%r", + pattern, + command, + ) + return { + "kind": "denied-by-rules", + "rules": [ + {"kind": "multi-rule-deny", "pattern": pattern} + ], + } + except re.error as exc: + logger.error("Invalid pattern %r: %s", pattern, exc) + continue + + # Default deny for shell without matching rule + logger.warning("❌ Denied (no matching rule): command=%r", command) + return {"kind": "denied-by-rules", "rules": [{"kind": "no-rule-match"}]} + + return on_permission_request + + +async def run_test(model: str, rules_json: str, prompt: str) -> Tuple[bool, str]: + """Run a single test and return (approved, response)""" + try: + client = CopilotClient() + await client.start() + + session = await client.create_session( + { + "model": model, + "on_permission_request": build_multi_rule_handler(rules_json), + } + ) + + # Set a short timeout + try: + response = await asyncio.wait_for( + session.send_and_wait({"prompt": prompt}), timeout=15.0 + ) + except asyncio.TimeoutError: + logger.error("Test Timed Out") + return (False, "Timeout") + finally: + await client.stop() + + content = response.data.content + # Heuristics to detect denial in response + denied_keywords = [ + "不允许", + "无法", + "对不起", + "Sorry", + "can't", + "cannot", + "not have permission", + "denied", + ] + is_denied = any(kw in content for kw in denied_keywords) + + return (not is_denied, content) + except Exception as e: + logger.error("Test failed: %s", e) + return (False, str(e)) + + +async def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--model", default="gpt-4.1", help="Model ID") + args = parser.parse_args() + + # LOGIC TEST RULES + # 1. Deny "echo secret" explicitly (Specific Deny) + # 2. Allow "echo" anything else (General Allow) + # 3. Allow "ls" (General Allow) + # 4. Deny everything else (Default Deny) + logic_test_rules = { + "^echo\\s+secret": False, # Higher priority: Deny specific echo + "^echo": True, # Lower priority: Allow general echo + "^ls": True, # Allow ls + ".*": False, # Deny everything else (e.g. whoami) + } + + rules_json = json.dumps(logic_test_rules) + + test_cases = [ + # 1. Matches Rule 2 (^echo) -> Should be Approved + ("Allowed: Normal Echo", "请执行: echo 'hello world'", True), + # 2. Matches Rule 3 (^ls) -> Should be Approved + ("Allowed: LS", "请执行: ls -la", True), + # 3. Matches Rule 1 (^echo\s+secret) -> Should be DENIED + # This proves the ORDER matters. If it matched Rule 2 first, it would be allowed. + ("Denied: Restricted Echo", "请执行: echo secret data", False), + # 4. Matches Rule 4 (.*) -> Should be DENIED + ("Denied: Unknown Command", "请执行: whoami", False), + ] + + logger.info("=" * 80) + logger.info("Safe Multi-Rule Logic Test (Proving Precedence)") + logger.info("Rules: %s", json.dumps(logic_test_rules, indent=2)) + logger.info("=" * 80) + + results = [] + for i, (name, prompt, expected) in enumerate(test_cases, 1): + logger.info("\n[Test %d/%d] %s", i, len(test_cases), name) + logger.info(" Prompt: %s", prompt) + + approved, response = await run_test(args.model, rules_json, prompt) + passed = approved == expected + + status = "✅ PASS" if passed else "❌ FAIL" + results.append((name, passed)) + + logger.info( + " Expected: %s, Got: %s - %s", + "Approved" if expected else "Denied", + "Approved" if approved else "Denied", + status, + ) + logger.info(" Response: %s", response[:100].replace("\n", " ")) + + # Summary + logger.info("\n" + "=" * 80) + logger.info("Test Summary") + logger.info("=" * 80) + passed_count = sum(1 for _, passed in results if passed) + for name, passed in results: + logger.info("%s %s", "✅" if passed else "❌", name) + logger.info("Total: %d/%d tests passed", passed_count, len(results)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/plugins/debug/copilot_sdk_research/test_permission_comprehensive.py b/plugins/debug/copilot_sdk_research/test_permission_comprehensive.py new file mode 100644 index 0000000..72b6e94 --- /dev/null +++ b/plugins/debug/copilot_sdk_research/test_permission_comprehensive.py @@ -0,0 +1,202 @@ +""" +Comprehensive Permission Control Test Suite +Tests all permission control scenarios for GitHub Copilot SDK +""" + +import argparse +import asyncio +import logging +import re +from typing import Any, Dict, List, Tuple + +from copilot import CopilotClient + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +def build_permission_handler(allow_all: bool, allow_shell: bool, pattern: str): + async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]): + kind = request.get("kind") + # Shell requests use 'fullCommandText' not 'command' + command = request.get("fullCommandText", "") or request.get("command", "") + + if allow_all: + logger.info("✅ Approved (allow-all): kind=%s command=%r", kind, command) + return {"kind": "approved"} + + if kind in ("read", "url"): + logger.info("✅ Approved (safe): kind=%s", kind) + return {"kind": "approved"} + + if kind == "shell": + if allow_shell: + logger.info("✅ Approved (allow-shell): command=%r", command) + return {"kind": "approved"} + + if pattern and command: + try: + if re.match(pattern, command): + logger.info( + "✅ Approved (regex match): pattern=%r command=%r", + pattern, + command, + ) + return {"kind": "approved"} + except re.error as exc: + logger.error("Invalid regex pattern: %s (%s)", pattern, exc) + + logger.warning("❌ Denied: kind=%s command=%r", kind, command) + return {"kind": "denied-by-rules", "rules": [{"kind": "test-suite"}]} + + return on_permission_request + + +async def run_test( + model: str, allow_all: bool, allow_shell: bool, pattern: str, prompt: str +) -> Tuple[bool, str]: + """Run a single test and return (success, response)""" + try: + client = CopilotClient() + await client.start() + + session = await client.create_session( + { + "model": model, + "on_permission_request": build_permission_handler( + allow_all=allow_all, + allow_shell=allow_shell, + pattern=pattern, + ), + } + ) + + response = await session.send_and_wait({"prompt": prompt}) + await client.stop() + + content = response.data.content + # Check if response indicates success or denial + denied_keywords = [ + "不允许", + "无法", + "对不起", + "Sorry", + "can't", + "cannot", + "not have permission", + ] + is_denied = any(kw in content for kw in denied_keywords) + + return (not is_denied, content) + except Exception as e: + logger.error("Test failed with exception: %s", e) + return (False, str(e)) + + +async def main(): + parser = argparse.ArgumentParser( + description="Comprehensive permission control test suite." + ) + parser.add_argument("--model", default="gpt-4.1", help="Model ID for testing.") + + args = parser.parse_args() + + # Test cases: (name, allow_all, allow_shell, pattern, prompt, expected_approved) + test_cases = [ + ("Default Deny Shell", False, False, "", "请执行: ls -la", False), + ("Allow All", True, False, "", "请执行: ls -la", True), + ("Allow Shell", False, True, "", "请执行: pwd", True), + ("Regex Match: ^ls", False, False, "^ls", "请执行: ls -la", True), + ("Regex No Match: ^ls vs pwd", False, False, "^ls", "请执行: pwd", False), + ( + "Regex Complex: ^(ls|pwd|echo)", + False, + False, + "^(ls|pwd|echo)", + "请执行: pwd", + True, + ), + ( + "Regex Complex No Match: git", + False, + False, + "^(ls|pwd|echo)", + "请执行: git status", + False, + ), + ( + "Read Permission (Always Allow)", + False, + False, + "", + "Read the file: README.md", + True, + ), + ] + + results = [] + logger.info("=" * 80) + logger.info("Starting Comprehensive Permission Control Test Suite") + logger.info("Model: %s", args.model) + logger.info("=" * 80) + + for i, (name, allow_all, allow_shell, pattern, prompt, expected) in enumerate( + test_cases, 1 + ): + logger.info("\n[Test %d/%d] %s", i, len(test_cases), name) + logger.info( + " Config: allow_all=%s, allow_shell=%s, pattern=%r", + allow_all, + allow_shell, + pattern, + ) + logger.info(" Prompt: %s", prompt) + + approved, response = await run_test( + args.model, allow_all, allow_shell, pattern, prompt + ) + passed = approved == expected + + status = "✅ PASS" if passed else "❌ FAIL" + results.append((name, passed)) + + logger.info( + " Expected: %s, Got: %s - %s", + "Approved" if expected else "Denied", + "Approved" if approved else "Denied", + status, + ) + logger.info( + " Response: %s", + response[:100] + "..." if len(response) > 100 else response, + ) + + # Summary + logger.info("\n" + "=" * 80) + logger.info("Test Summary") + logger.info("=" * 80) + passed_count = sum(1 for _, passed in results if passed) + total_count = len(results) + + for name, passed in results: + logger.info("%s %s", "✅" if passed else "❌", name) + + logger.info("-" * 80) + logger.info( + "Total: %d/%d tests passed (%.1f%%)", + passed_count, + total_count, + 100 * passed_count / total_count, + ) + + if passed_count == total_count: + logger.info("🎉 All tests passed!") + else: + logger.warning("⚠️ Some tests failed. Please review the logs.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/plugins/debug/copilot_sdk_research/test_shell_permission_pattern.py b/plugins/debug/copilot_sdk_research/test_shell_permission_pattern.py new file mode 100644 index 0000000..e0a80e3 --- /dev/null +++ b/plugins/debug/copilot_sdk_research/test_shell_permission_pattern.py @@ -0,0 +1,90 @@ +import argparse +import asyncio +import logging +import re +from typing import Any, Dict + +from copilot import CopilotClient + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +def build_permission_handler(allow_all: bool, allow_shell: bool, pattern: str): + async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]): + kind = request.get("kind") + # Shell requests use 'fullCommandText' not 'command' + command = request.get("fullCommandText", "") or request.get("command", "") + logger.info("permission.request FULL: %s", request) + logger.info("permission.request kind=%s command=%r", kind, command) + + if allow_all: + return {"kind": "approved"} + + if kind in ("read", "url"): + return {"kind": "approved"} + + if kind == "shell": + if allow_shell: + return {"kind": "approved"} + + if pattern and command: + try: + if re.match(pattern, command): + return {"kind": "approved"} + except re.error as exc: + logger.error("Invalid regex pattern: %s (%s)", pattern, exc) + + return {"kind": "denied-by-rules", "rules": [{"kind": "debug-shell-pattern"}]} + + return on_permission_request + + +async def main(): + parser = argparse.ArgumentParser( + description="Test shell permission regex with GitHub Copilot SDK." + ) + parser.add_argument( + "--pattern", default="", help="Regex pattern for auto-approving shell commands." + ) + parser.add_argument( + "--allow-shell", action="store_true", help="Auto-approve all shell commands." + ) + parser.add_argument( + "--allow-all", action="store_true", help="Auto-approve all permission requests." + ) + parser.add_argument( + "--prompt", + default="请执行: ls -la", + help="Prompt to trigger a shell tool request.", + ) + parser.add_argument("--model", default="gpt-5-mini", help="Model ID for testing.") + + args = parser.parse_args() + + client = CopilotClient() + await client.start() + + session = await client.create_session( + { + "model": args.model, + "on_permission_request": build_permission_handler( + allow_all=args.allow_all, + allow_shell=args.allow_shell, + pattern=args.pattern, + ), + } + ) + + logger.info("Sending prompt: %s", args.prompt) + response = await session.send_and_wait({"prompt": args.prompt}) + logger.info("Response: %s", response.data.content) + + await client.stop() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/plugins/debug/github-copilot-sdk/guides/COPILOT_TOOLS_QUICKSTART.md b/plugins/debug/legacy_docs/guides/COPILOT_TOOLS_QUICKSTART.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/COPILOT_TOOLS_QUICKSTART.md rename to plugins/debug/legacy_docs/guides/COPILOT_TOOLS_QUICKSTART.md diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE.md b/plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE.md rename to plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE.md diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md b/plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md rename to plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE.md b/plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE.md rename to plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE.md diff --git a/plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md b/plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md rename to plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md diff --git a/plugins/debug/github-copilot-sdk/guides/OPENWEBUI_FUNCTION_INTEGRATION.md b/plugins/debug/legacy_docs/guides/OPENWEBUI_FUNCTION_INTEGRATION.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/OPENWEBUI_FUNCTION_INTEGRATION.md rename to plugins/debug/legacy_docs/guides/OPENWEBUI_FUNCTION_INTEGRATION.md diff --git a/plugins/debug/github-copilot-sdk/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md b/plugins/debug/legacy_docs/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md rename to plugins/debug/legacy_docs/guides/SESSIONCONFIG_INTEGRATION_GUIDE.md diff --git a/plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md b/plugins/debug/legacy_docs/guides/TOOLS_USAGE.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/TOOLS_USAGE.md rename to plugins/debug/legacy_docs/guides/TOOLS_USAGE.md diff --git a/plugins/debug/github-copilot-sdk/guides/TOOL_IMPLEMENTATION_GUIDE.md b/plugins/debug/legacy_docs/guides/TOOL_IMPLEMENTATION_GUIDE.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/TOOL_IMPLEMENTATION_GUIDE.md rename to plugins/debug/legacy_docs/guides/TOOL_IMPLEMENTATION_GUIDE.md diff --git a/plugins/debug/github-copilot-sdk/guides/WORKFLOW.md b/plugins/debug/legacy_docs/guides/WORKFLOW.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/WORKFLOW.md rename to plugins/debug/legacy_docs/guides/WORKFLOW.md diff --git a/plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md b/plugins/debug/legacy_docs/guides/WORKFLOW_CN.md similarity index 100% rename from plugins/debug/github-copilot-sdk/guides/WORKFLOW_CN.md rename to plugins/debug/legacy_docs/guides/WORKFLOW_CN.md diff --git a/plugins/debug/github-copilot-sdk/test_capabilities.py b/plugins/debug/legacy_docs/test_capabilities.py similarity index 100% rename from plugins/debug/github-copilot-sdk/test_capabilities.py rename to plugins/debug/legacy_docs/test_capabilities.py diff --git a/plugins/debug/github-copilot-sdk/test_injection.py b/plugins/debug/legacy_docs/test_injection.py similarity index 100% rename from plugins/debug/github-copilot-sdk/test_injection.py rename to plugins/debug/legacy_docs/test_injection.py diff --git a/plugins/debug/mcp_test.py b/plugins/debug/mcp_test.py new file mode 100644 index 0000000..a5fd8af --- /dev/null +++ b/plugins/debug/mcp_test.py @@ -0,0 +1,47 @@ +import asyncio +import os +from copilot import CopilotClient + + +async def main(): + token = os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN") + if not token: + print( + "Error: GH_TOKEN (or GITHUB_TOKEN) environment variable not set. Please export GH_TOKEN=... before running." + ) + return + + client = CopilotClient() + await client.start() + + async def on_permission_request(request, _ctx): + if request.get("kind") == "mcp": + return {"kind": "approved"} + return {"kind": "approved"} + + session = await client.create_session( + { + "model": "gpt-5-mini", + "mcp_servers": { + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": {"Authorization": f"Bearer {token}"}, + "tools": ["*"], + } + } + } + ) + + result = await session.send_and_wait( + { + "prompt": "Use GitHub MCP tools to find the owner of the 'awesome-openwebui' repository.", + },timeout=1000 + ) + print(result.data.content) + + await client.stop() + + +if __name__ == "__main__": + asyncio.run(main())