pi harness 与 CC/Codex 的资源接入不一致:项目级资源全关、user scope skill 路径错配、扩展点未接
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
> 基线:`17f2baa5`,Pi `v0.83.0`。下文所有行号以该 commit 为准。
> 本 issue 是 tracking issue,按小节拆成可独立提 PR 的任务,不需要一次做完。
## 背景
Pi 集成(#1265)把 `PI_CODING_AGENT_DIR` 指向**每会话新建、退出即删**的隔离目录:
```ts
// packages/maker-core/src/agents/pi/index.ts:716
const configHome = path.join(agentHome, 'run-tmp', randomBytes(8).toString('hex'));
// :1185
PI_CODING_AGENT_DIR: configHome,
```
这个隔离本身是对的,理由写在 `index.ts:709-713`:并发会话共写 `agentHome/models.json` 会互相截断,先启动的进程会读到半写入的 provider 快照(codex review P2)。
但 Pi 的 `PI_CODING_AGENT_DIR` 语义是「override the config directory; default is `~/.pi/agent`」——它同时决定了 **skills / prompts / extensions / packages / settings.json / trust.json 的全局发现根**。隔离之后没有做任何回桥,导致 Pi 看不到用户的全局与项目级资源。
Codex 撞过同一个问题并已有成套解法(`managed-dir-links.ts` + `codex-global-skills.ts` / `codex-global-plugins.ts` / `codex-global-rules.ts` / `shared-global-skills.ts`),Pi 没有复用。
### 三个 harness 的现状对比
| | 配置目录 | 生产环境隔离 | 用户全局/项目资源怎么进来 |
|---|---|---|---|
| **Claude Code** | `CLAUDE_CONFIG_DIR` | **否** —— `auth-adapters.ts:614` 只在 `XDT_USER_DATA_DIR && !app.isPackaged` 时重定向,仅 dev 多实例 | 天然全开;并且显式传 `settingSources: ['user','project','local']`(`claude-code/index.ts:2793` 本地 / `:2357` 远端) |
| **Codex** | `CODEX_HOME = userData/codex-home` | **是** | 受管 symlink 桥接(`codex-global-*.ts`);项目 trust 交给 codex app-server 自己写 `config.toml`,Cindy 不插手 |
| **Pi** | `PI_CODING_AGENT_DIR = agentHome/run-tmp/` | **是,且每会话销毁** | **无任何桥接** |
结论:Pi 是三者中唯一「隔离且失能」的。这不是安全上的更严,而是功能上的不一致 —— 详见 §2 的论证。
---
## 实测方法(后续每项都可用它验证)
用仓库 pin 的 Pi 二进制起真 RPC 进程,发 `get_commands` 读 Pi **实际加载**的命令清单:
```bash
SP=/tmp/pi-probe && mkdir -p "$SP/skills/probe-skill"
cat > "$SP/skills/probe-skill/SKILL.md" <<'EOF'
---
name: probe-skill
description: probe
---
probe
EOF
cat > "$SP/models.json" <<'EOF'
{"providers":{"probe":{"name":"Probe","baseUrl":"http://127.0.0.1:9","api":"anthropic-messages","apiKey":"dummy","models":[{"id":"probe-model","name":"Probe","reasoning":false,"input":["text"],"contextWindow":100000,"maxTokens":4096,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0}}]}}}
EOF
PI=~/Library/Application\ Support/Cindy/pi//pi
cd "$SP" && { echo '{"type":"get_commands"}'; sleep 6; } | \
PI_CODING_AGENT_DIR="$SP" PI_OFFLINE=1 "$PI" --mode rpc \
--provider probe --model probe-model --session-dir "$SP/sessions"
```
不需要真实模型凭证,启动即可查询。
---
## 1. `customization-scanner.ts` 的路径与 Pi 实际加载语义不一致
**类型:bug(用户可见 —— 列出来但用不了)**
`packages/maker-core/src/agents/pi/customization-scanner.ts`:
```ts
:30 { scope: 'user', dir: path.join(home, '.pi', 'agent', 'skills') },
:36 { scope: 'repo', dir: path.join(wd, '.pi', 'agent', 'skills'), workingDir: wd },
```
两条都不对:
1. **`~/.pi/agent/skills`(user scope)**:该目录是 `$PI_CODING_AGENT_DIR/skills` 的默认值。Cindy 把 `PI_CODING_AGENT_DIR` 重定向到 `run-tmp/` 后,Pi 的全局 skill 根跟着走,`~/.pi/agent/skills` **不会被读取**。
实测证据:把 probe skill 放进临时 `PI_CODING_AGENT_DIR/skills/`,`get_commands` 返回
```json
{"name":"skill:probe-skill","sourceInfo":{"scope":"user","baseDir":"/tmp/pi-probe"}}
```
`baseDir` 跟着 `PI_CODING_AGENT_DIR` 走,证实全局 skill 发现被重定向。
2. **`{workingDir}/.pi/agent/skills`(repo scope)**:Pi 的项目级路径是 `.pi/skills/`,不是 `.pi/agent/skills/`(见 Pi `docs/skills.md` Locations 一节)。当前扫的路径 Pi 从不读,同时漏掉了 Pi 真正读的 `.pi/skills/`。
另外 `.agents/skills` Pi 会从 `cwd` 沿祖先目录一路找到 git repo root,scanner 只看 `workingDir` 一层。
**唯一扫对的是 `~/.agents/skills`** —— 它是绝对 home 路径,不受 `PI_CODING_AGENT_DIR` 影响,实测正常加载。
### 建议
- [ ] user scope:移除 `~/.pi/agent/skills`,或改为扫当前会话真实的 configHome(但那里恒为空,实际等于移除)
- [ ] repo scope:`.pi/agent/skills` → `.pi/skills`
- [ ] `.agents/skills` 沿祖先目录向上找到 git root,与 Pi 行为一致
- [ ] 补测试:scanner 结果与 `get_commands` 的差集为空
---
## 2. 项目级资源全部静默失效 —— 建议默认传 `--approve`
**类型:bug + 一致性**
Pi `docs/settings.md` 原文:
> Non-interactive modes (`-p`, `--mode json`, and **`--mode rpc`**) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` **ignore those project resources**.
Cindy 三条路全断:
- argv 无 `--approve`(`pi/index.ts:1025-1031`)
- 不写 `settings.json`,`defaultProjectTrust` 落默认 `ask`
- `trust.json` 存在每会话新建的 configHome 里,恒为空
结果:项目 `.pi/settings.json`、`.pi/skills/`、项目 `.agents/skills/`、项目 packages、项目 extensions **全部被忽略**。
实测对照:
| 启动参数 | `get_commands` 里的项目级 skill |
|---|---|
| 当前实现(无 `--approve`) | 空 |
| 加 `--approve` | `skill:proj-pi-skill`、`skill:proj-agents-skill` |
### 为什么不该为此新增开关
第一版方案想给项目信任做一个用户可见开关,理由是「项目 extension 是加载期任意代码执行」。核对后这个理由不成立:
- **CC 已经无条件打开了同等风险面**:`settingSources: ['user','project','local']` 包含项目 `.claude/settings.json` 的 **hooks**,同样是加载期任意命令执行。注释里的理由是「不透传 SDK 默认不读,会丢用户配置」。
- **Codex 也没有这种开关**:项目 trust 由 codex app-server 自己写 `config.toml` 管(见 `codex-global-plugins.ts:42` 注释),Cindy 不插手,危险动作统一交给 `permissionMode` → `approvalPolicy`。
- **安全边界按工作目录划,不按 harness 划**:同一个 repo 下 CC 会话已经在执行项目 hooks 了。Pi 拒绝加载项目 extension 保护不了任何东西 —— 攻击者只要用户开一次 CC 会话即可。这道额外的严是纯功能损失、零安全收益。
所以对齐做法就是一个 argv 参数,不做 UI、不加开关。
### 需要单独决策的一点
`--approve` 除了让项目 skill / settings 生效,还会让 Pi **自动安装项目声明的 packages**(`docs/packages.md`:*pi installs any missing project packages automatically on startup after the project is trusted*),即会跑 `npm install`。
CC 最接近的等价物是项目 settings 声明的 MCP server 自动拉起,性质相近但 Pi 这条会真的往磁盘装 npm 包。
若判断这一条不可接受,应当**单独拦 packages 这一项**,而不是把整个项目信任面锁死。
### 建议
- [ ] 默认传 `--approve`,与 CC 的 `settingSources` 对齐
- [ ] 决策:是否接受「项目 packages 自动 npm install」;不接受则单独处理该项
- [ ] 补测试:项目级 skill 在 `get_commands` 中可见
---
## 3. 把 Pi 纳入 `shared-global-skills` 扇出
**类型:一致性 / enhancement**
`apps/desktop/src/main/maker-host/shared-global-skills.ts` 已有跨引擎 skill 扇出:
> `~/.agents/skills` is the shared index that Cindy Codex already scans.
> Existing `~/.claude/skills` entries are linked into `~/.agents/skills` so Codex can see them.
> `~/.agents/skills` and `~/.codex/skills` entries are linked into `~/.claude/skills` so Claude can see them.
但 `type SkillRootName = 'shared' | 'claude' | 'codex'` —— **没有 `'pi'`**。
Pi 目前是靠原生读取 `~/.agents/skills` 搭这套机制的顺风车,没有被正式纳入账本,后果:
- 没被扇出到 `~/.agents/skills` 的 CC-only skill,Pi 看不到(实测确认存在这种漏网条目)
- 项目级完全没接:`prepareSharedProjectSkillLinks` 只做 `.agents/skills` ↔ `.claude/skills`,不涉及 `.pi/skills`
- 扇出由 `learn-host/apply.ts` / `cindy-brain/skillSlot.ts` / `skillhub/importLocalSkill.ts` 按需触发,不是实时全覆盖
### 建议
- [ ] `SkillRootName` 加 `'pi'`,全局扇出把 `.pi/skills` 纳入双向链接
- [ ] `prepareSharedProjectSkillLinks` 项目级同步纳入 `.pi/skills`
- [ ] 或(更轻量的等价方案)启动时用 `--skill ` 显式喂目录 —— 该 flag 可重复且与隔离 configHome 零冲突
---
## 4. 用 `get_commands` 作为清单权威源
**类型:enhancement(根因治理)**
§1 那类「UI 列了但实际没加载」的脱节,根子是用文件系统扫描去**推断** Pi 加载了什么。Pi RPC 提供了权威接口 `get_commands`,当前未使用。
Cindy 已用的 RPC:`prompt` / `steer` / `abort` / `fork` / `clone` / `compact` / `export_html` / `get_tree` / `get_entries` / `get_session_stats` / `get_state` / `set_model` / `set_thinking_level` / `set_auto_compaction` / `switch_session` 等。
未使用且值得考虑的:`get_commands`(权威命令/skill 清单)、`set_steering_mode` / `set_follow_up_mode`(消息投递模式)、`set_auto_retry`。
### 建议
- [ ] 会话就绪后调 `get_commands`,校正 fs scanner 的结果(或直接以它为准)
- [ ] 保留 fs scanner 作为「会话未启动时的预览」,但标注为推断值
---
## 5. Extension 事件面只用了 1 个 hook
**类型:enhancement**
`cindy-bridge-source.ts` 当前只挂:
```
pi.on('tool_call') ×1
pi.registerTool() ×5
pi.registerCommand() ×1
```
Pi 的 hook 面(见 `docs/extensions.md` Lifecycle Overview)里,对 Cindy 直接有价值但未使用的:
- [ ] **`session_before_compact`** —— 可 cancel **或 customize** 压缩。目前 Cindy 的压缩完全等同裸跑 Pi(同一份二进制、同一套默认参数),这是唯一能让压缩质量真正区别于裸 Pi 的入口。可注入「保留文件路径 / 已定决策 / 未完成 TODO」等指令。
- [ ] **`context`** —— 可 modify messages。当前 compaction digest 只写不读:digest 进 FTS 可 `memory_search`,但排除出 `MEMORY.md` 和 system prompt(见 `memory/digest-type.test.ts:61-65`),模型不知道它存在。而模型最需要回捞的时刻,恰恰是它不知道自己丢了什么的时刻。用 `context` 事件主动回灌,才能把「压缩即记忆」从单向写入变成闭环。
- [ ] **`tool_result`** —— 可 modify。工具输出出站脱敏 + 大输出截断。当前凭证防线只有读侧路径拦截(`cindy-bridge-source.ts touchesCredentialPath`),没有出站内容过滤。
- [ ] `before_provider_headers` / `before_provider_request` / `after_provider_response` —— 路由、计量、诊断(优先级低)
---
## 6. settings.json 钉值防二进制升级漂移
**类型:chore**
`pi-remaining-work.md:246` 已列为「顺手可做的小项」。当前 `retry.*` / `httpIdleTimeoutMs` / `compaction.*` / `defaultProjectTrust` 全部放任 Pi 默认。
优先钉的一项:**`retry.provider.maxRetries: 0`**。Pi `docs/settings.md` 明确警告:
> Setting it above `0` can make SDK/provider retries handle out-of-usage-limit errors before Pi sees them, which may block the agent until the provider quota resets in some circumstances.
当前默认已经是 `0`,但没有任何东西阻止 Pi 升级后改掉它。
### 建议
- [ ] 加 `writeSettingsJson`(与 `writeModelsJson` 同机制,每次 startSession 覆写到 configHome)
- [ ] 钉 `retry.provider.maxRetries: 0`、`compaction.reserveTokens/keepRecentTokens`
---
## 不在本 issue 范围
**Pi package 生态整体不可用。** 隔离 configHome 使 `settings.json` 的 `packages` / `extensions` 配置面对用户完全关闭,社区包(`pi.dev/packages`)一个都装不了。Pi `docs/packages.md` 对此有明确安全警告:
> Pi packages run with **full system access**. Extensions execute **arbitrary code**.
这属于新增权限面,应当走 `docs/dev-rules/plugin-security-and-authoring.md` 级别的独立评审,不适合夹在本 issue 里顺手做。
---
## 优先级建议
| 优先级 | 项 | 理由 |
|---|---|---|
| P0 | §1 scanner 路径 | 用户可见:列出来但点不着 |
| P0 | §2 `--approve` | 与 CC/Codex 不一致,且当前的严格零安全收益 |
| P1 | §4 `get_commands` | 根因治理,防止 §1 类脱节复发 |
| P1 | §3 shared-global-skills 纳入 Pi | 与另外两个 harness 用同一套维护方式 |
| P2 | §5 三个 hook | 压缩质量、记忆闭环、出站脱敏 |
| P3 | §6 settings.json 钉值 | 防漂移 |
Contributor guide
Research direction
Choose one independently scoped section before starting. Read packages/maker-core/src/agents/pi/customization-scanner.ts and pi/index.ts, then run the issue's Pi RPC probe with get_commands; for shared skills, also inspect apps/desktop/src/main/maker-host/shared-global-skills.ts. Done means the selected resource is both discoverable and visible in the relevant tests or get_commands output.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- ai, developer-experience, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100