mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat: 组合包配置以包内 config/ 为唯一来源,并新增受保护写入人工审批
配置统一:权限策略、硬防御、Tool Search、提示词封面以及新迁入的 RTK、pi-compaction、pi-worktree、pi-hermes-memory 配置全部由扩展 直接读取已安装组合包内的 config/,pi install / pi update 即完成部署; 删除 rebuild-bundle-config.mjs 与 bundle-manifest.json,my-pi/ 只保留 日志与缓存。update.sh 成功升级后将默认 git 来源的已安装副本 reset 回发布版本。AGENTS.md 新增「组合包配置原则」。 脚本整合:search_config.sh / ssh_config.sh 合并为 configure.sh search|ssh;install.sh / update.sh 共享函数抽到 scripts/lib.sh,远程 安装器改为先下载到临时文件再执行,下载失败可被检测。 权限:hard-guards 新增 protectedWrites,写入 Pi 凭据/设置、已安装 组合包与扩展目录、pi-ssh 凭据库时一律人工确认(跳过 AutoReview、 yolo 与 Session grant,无 UI 时拒绝);放行 ls、LSP 查询、ctx 只读、 web_search、Chrome 只读与 git 只读命令,skill_manage 改为 ask。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
a0ee9389c7
commit
2aa1be0b54
@@ -22,15 +22,15 @@
|
||||
- 该目录从明确 tag 导入并由本仓库直接维护,不使用 npm 预编译产物,不是 submodule,也不保留上游 `.pi` 状态;源码、测试、wrapper 与权限集成实现继续保留,但当前不属于根组合包运行时依赖,`extensions/subagents.ts` 也不在默认扩展加载列表中。
|
||||
- `extensions/fff-override.ts`:强制使用 FFF 官方 `override` 模式,统一接管 `find`、`grep`、`multi_grep` 和 FFF 的 `@` 补全;显式 CLI flag 仍遵循 FFF 官方优先级。
|
||||
- `pi-web-search/`:完全自维护的统一在线搜索扩展。对模型只暴露 `web_search` / `web_fetch`,schema 与提示词不含厂商名;`web_search` 由当前主模型(`ctx.model` + `ctx.modelRegistry.complete`)按内置路由规则在 Tavily / Exa / Keenable 间选择并经统一参数翻译调用,`details` 记录实际 provider 与路由方式,决策按会话内 LRU 缓存,路由失败或被选厂商报错按可用性顺序降级;`web_fetch` 固定走 Exa `web_fetch_exa`,失败时静默降级 Keenable → Tavily。`clients/` 为纯 `fetch` 直连:Tavily 走 `api.tavily.com`(必须有 key),Keenable 复刻上游 client 保留 keyless `/public` 端点与 SSRF 防护,Exa 直接对 `mcp.exa.ai/mcp` 发 JSON-RPC(缓存 `mcp-session-id`,404 自动重建会话,key 只走 `x-api-key` 头),不使用 `pi-mcp-adapter` 或任何上游扩展包。
|
||||
- `search_config.sh` 与 `scripts/search-auth.mjs`:以交互或参数方式把 `tavily` / `exa` / `keenable` 三条 `api_key` 凭据写入 Pi 的 `auth.json`(`$PI_CODING_AGENT_DIR/auth.json`,默认 `~/.pi/agent/auth.json`);加锁后原子写、权限 `600`、保留全部其他条目,支持 `--migrate` 从旧 `search.env` 导入。`pi-web-search/auth.ts` 每次调用读取并复刻上游解析语义(`!command` 执行、`$VAR`/`${VAR}` 插值、credential `env` 优先于进程 env、`$$`/`$!` 转义);显式进程环境变量不再直接读取,env 注入通过 `"key": "$VAR"` 表达。
|
||||
- `configure.sh search` 与 `scripts/search-auth.mjs`:以交互或参数方式把 `tavily` / `exa` / `keenable` 三条 `api_key` 凭据写入 Pi 的 `auth.json`(`$PI_CODING_AGENT_DIR/auth.json`,默认 `~/.pi/agent/auth.json`);加锁后原子写、权限 `600`、保留全部其他条目,支持 `--migrate` 从旧 `search.env` 导入。`pi-web-search/auth.ts` 每次调用读取并复刻上游解析语义(`!command` 执行、`$VAR`/`${VAR}` 插值、credential `env` 优先于进程 env、`$$`/`$!` 转义);显式进程环境变量不再直接读取,env 注入通过 `"key": "$VAR"` 表达。
|
||||
- `extensions/codegraph.ts`:注册原生 `codegraph_explore` 工具,以 `execFile` argv 形式调用机器级 `codegraph` CLI(每次调用为短命进程,无常驻 server/daemon);未初始化项目先经 `ctx.ui.confirm` 确认再 `codegraph init` 并自动重试,拒绝或失败则把结果打回让模型改用 `find`/`grep`/`read`/LSP;结果按 `codegraph.db` mtime 做有界缓存,索引一变即失效。
|
||||
- `extensions/plugin-init.ts`:已移除;原 `/plugin_init` 的 CodeGraph 初始化职责由 `extensions/codegraph.ts` 在首次 `codegraph_explore` 调用内按需确认完成。
|
||||
- `context-mode`:加载上游 Pi adapter 与 skills,提供 `ctx_*` 工具、隔离式大输出处理和会话连续性。
|
||||
- `extensions/hashline.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`,以行哈希锚点覆盖内置 `read` / `edit`;Hashline 自带 `grep` 强制关闭。
|
||||
- `extensions/permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,自身不部署、不播种、不迁移任何配置。`config/pi-permission-policy.jsonc` / `config/pi-permission-hard-guards.jsonc` 由 `install.sh` / `update.sh` 经 `scripts/rebuild-bundle-config.mjs` 部署到 `<agentDir>/my-pi/permission-system/`(policy.jsonc 与 hard-guards.jsonc 且权限 owner-only);旧 `extensions/pi-permission-system/` 位置的残留文件被忽略,不迁移也不清理。项目级 policy 不读取。
|
||||
- `extensions/permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,自身不部署、不播种、不迁移任何配置。扩展运行时直接读取已安装组合包内的 `config/pi-permission-policy.jsonc` / `config/pi-permission-hard-guards.jsonc`;日志保留在 `<agentDir>/my-pi/permission-system/logs/`。旧 `extensions/pi-permission-system/` 位置的残留文件被忽略,不迁移也不清理。项目级 policy 不读取。
|
||||
- `extensions/lsp.ts`:把组合包内 TypeScript Language Server CLI 物化为绝对命令路径,部署 TypeScript/Kotlin/JDT LS 全局配置并加载 `pi-lsp`。
|
||||
- `extensions/tool-routing.ts`:保留 Pi 默认系统提示词,在每轮开始前按激活工具追加简短路由规则,并提供 `/dump-system-prompt` 将扩展所见的有效提示词写入 `.pi-debug/effective-system-prompt.md`。
|
||||
- `extensions/system-prompt-cover.ts`:把 Pi 默认系统提示词的开头替换为组合包权威 `config/system-prompt-cover.md`(部署到 `~/.pi/agent/my-pi/system-prompt-cover.md`),并用文本拼接保留其余全部分区及其原始顺序。必须注册在 `tool-routing.ts` **之前**:`tool-routing` 会以 `forceSystemPrompt` 压平提示词,在其后改 `systemPromptOptions` 将不再影响本轮请求。封面文件缺失或空白时不干预。`install.sh` / `update.sh` 经 `scripts/rebuild-bundle-config.mjs` 与全部组合包配置一起整体重建该文件,不保留备份。
|
||||
- `extensions/system-prompt-cover.ts`:把 Pi 默认系统提示词的开头替换为组合包权威 `config/system-prompt-cover.md`(扩展运行时直接读取已安装组合包内文件,不部署到 agent 目录),并用文本拼接保留其余全部分区及其原始顺序。必须注册在 `tool-routing.ts` **之前**:`tool-routing` 会以 `forceSystemPrompt` 压平提示词,在其后改 `systemPromptOptions` 将不再影响本轮请求。封面文件缺失或空白时不干预。
|
||||
- `pi-chrome/`:从上游源码导入并由本仓库直接维护,通过用户安装的 unpacked MV3 扩展与 `127.0.0.1:17318` loopback bridge 接管当前已登录 Chrome;控制默认锁定,每个 Pi session 必须显式 `/chrome authorize`。
|
||||
- 上游来源:<https://github.com/tianrendong/pi-chrome>
|
||||
- 初始导入快照:`017ff4b9a639f0b8b213e58a3f30613fc38edcc8`(`0.15.46`,该 commit 无对应 tag)。
|
||||
@@ -45,7 +45,7 @@
|
||||
- 该目录由本仓库直接维护,保留上游 MIT 许可证与来源说明;根组合包直接加载源码并通过本地 `file:` 依赖打包。
|
||||
- `pi-worktree/`:完全自维护(无上游)的斜杠命令 git worktree 管理扩展,提供 `/wt`(别名 `/worktree`)的 `new` / `merge` / `switch` / `list` / `remove` / `prune` 子命令,实现「一键拆分」与「一键合并」;不注册模型可调用工具。
|
||||
- 状态登记位于 `<agentDir>/pi-worktree/state.json`(目录 `700`、文件 `600`、原子写入),不放 `<agentDir>/my-pi/` 下;Pi 不能修改进程 cwd,会话切换通过 `SessionManager.forkFrom`(或为空会话写入最小 header)生成 cwd 指向目标目录的新会话文件再 `ctx.switchSession` 重建运行时,切换后的收尾只能在新 `ctx` 的 `withSession` 回调中执行。
|
||||
- 合并流程固定为先确认、再 rebase 到来源分支、最后默认 `--ff-only` 合并(`--squash` 可选压缩),冲突时可选择中止或保留后用 `/wt merge --continue` / `--abort` 处理,来源改动用 stash 兜底,永远不会 push;登记文件写入走 `state.json.lock` 锁文件串行化;worktree 根目录与分支前缀由 `settings.json.piWorktree` 的 `root` / `branchPrefix` 配置。
|
||||
- 合并流程固定为先确认、再 rebase 到来源分支、最后默认 `--ff-only` 合并(`--squash` 可选压缩),冲突时可选择中止或保留后用 `/wt merge --continue` / `--abort` 处理,来源改动用 stash 兜底,永远不会 push;登记文件写入走 `state.json.lock` 锁文件串行化;worktree 根目录与分支前缀由组合包 `config/pi-worktree.json` 的 `root` / `branchPrefix` 配置。
|
||||
- 来源 checkout 的 `.pi/worktree.json`(`version: 1`)声明环境复刻:`copy` 只复制未跟踪文件、`link` 只软链 gitignore 目标、`env` 渲染为 `600` 权限环境文件、`postCreate` 仅在 `ctx.isProjectTrusted()` 时以 argv 形式 `execFile`(绝不走 shell),任一失败即停并可用 `/wt reopen` 增量重跑;未被 `.gitignore` 覆盖的软链/环境文件写入 `.git/info/exclude`(仓库各 worktree 共享),保证 `/wt merge` 不会提交它们。
|
||||
- `pi-lsp@0.1.7`:提供声明式 LSP 接入;组合包内置 `typescript-language-server@5.3.0` + `typescript@6.0.3`,并配置机器级 `kotlin-lsp --stdio` 与 `jdtls`。
|
||||
- `pi-hermes-memory/`:从官方 `pi-hermes-memory` 仓库导入并由本仓库直接维护的 Hermes 风格持久记忆扩展;提供全局/项目/用户/失败四类记忆、SQLite FTS5 trigram 会话与记忆检索、`SKILL.md` 过程记忆、correction 检测、自动 consolidation、`STANDING.md` 常驻指令和 6 个记忆工具。
|
||||
@@ -63,10 +63,10 @@
|
||||
- `pi-compaction/`:统一管理增量 Session segment、确定性工具结果压缩、后台语义摘要、effective token 口径、`context_tree_query`、portable checkpoint 与 Codex native compaction;普通 Provider、native compaction、footer 和本地 context observer 必须消费同一个不可变 projection snapshot。
|
||||
- 上游参考一:<https://github.com/jjuraszek/pi-condense>,快照 `59c95110aacd8131db71e21296765dd80b72fae8`(`v2.9.1`)。
|
||||
- 上游来源二:<https://github.com/ogulcancelik/pi-extensions/tree/main/packages/pi-codex-compaction>,导入快照 `c4d6ca8fb484ba51684296ba8b4715f8c7b3ac91`(`0.1.4`)。
|
||||
- `pi-compaction` 采用全新的 `pi-compaction` version 1 Session 记录和 `settings.json.piCompaction` 配置,不读取或迁移旧 `context-prune-*`、`contextPrune` 或 `openai-codex-native-compaction` 状态;切换后应使用新 Session。两份上游 MIT 许可证及来源差异保留在包内。
|
||||
- `pi-compaction` 采用全新的 `pi-compaction` version 1 Session 记录和组合包 `config/pi-compaction.json` 配置,不读取或迁移旧 `context-prune-*`、`contextPrune` 或 `openai-codex-native-compaction` 状态;切换后应使用新 Session。两份上游 MIT 许可证及来源差异保留在包内。
|
||||
- 根包还固定安装 `pi-hashline-edit@0.8.3`、`typescript-language-server@5.3.0` 和 `typescript@6.0.3`;`@gotgenes/pi-permission-system`、`@smoose/pi-notify`、`pi-ask-user`、`pi-chrome`、`pi-compaction`、`pi-hermes-memory`、`pi-minimal-footer`、`pi-ssh`、`pi-tool-search`、`pi-web-search` 与 `pi-worktree` 分别指向仓库内对应目录的本地 `file:` 依赖;`pi-subagents/` 仅保留源码,不作为根运行时依赖。
|
||||
- `install.sh`:先安装根组合包,再通过 `scripts/rebuild-bundle-config.mjs` 整体重建 `<agentDir>/my-pi/` 受管配置目录,然后交互检查 Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。
|
||||
- `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。不得在 `pi update` 前预清理安装缓存;`pi update` 失败时 fail-fast 停止——Pi 1.3.0 会在下一次 `pi update` 时依据其 `.pi-update-incomplete` 标记自行重跑 `git clean -fdx` 与依赖安装完成自愈,本脚本不做人工恢复。升级成功后调用可定位的更新后组合包 Node helper `scripts/rebuild-bundle-config.mjs` 整体重建 `<agentDir>/my-pi/`:先按 `config/bundle-manifest.json` 校验全部来源与目标,在 agent 目录内构建临时树,再删除旧 `my-pi/`(符号链接只删链接不跟随)并原子替换,不备份、不迁移、不合并不属于组合包的内容。无法从 `PI_PACKAGE_SOURCE` 定位更新后 helper 时必须报告失败,不能回退到可能陈旧的脚本目录 helper。其他组件升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。
|
||||
- `install.sh`:先安装根组合包(组合包配置即随包内的 `config/` 就位,不做额外部署),然后交互检查 Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。
|
||||
- `update.sh`:先通过 `pi update` 升级根组合包,成功升级后把默认 git 来源的已安装组合包 checkout `git reset --hard` 还原到已发布版本(本地路径来源不 reset,不做 `git clean` 以保留 `node_modules`),再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。不得在 `pi update` 前预清理安装缓存;`pi update` 失败时 fail-fast 停止——Pi 1.3.0 会在下一次 `pi update` 时依据其 `.pi-update-incomplete` 标记自行重跑 `git clean -fdx` 与依赖安装完成自愈,本脚本不做人工恢复。其他组件升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。
|
||||
- `uninstall.sh`:先移除根组合包,其他共享终端环境和机器工具不卸载。不得删除 `.pi/agent/pi-hermes-memory/`、`projects-memory/`、`.hippo/` 或任何用户记忆数据。
|
||||
|
||||
## 当前职责与默认行为
|
||||
@@ -81,7 +81,7 @@
|
||||
- Hashline 默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 和 `replaceText` 均关闭;它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。
|
||||
- 工具路由规则不得替换 Pi 默认系统提示词:代码结构、调用关系和待修改 symbol 优先 CodeGraph,定义/引用/类型/诊断优先 LSP;工作区内大文件探索、分析和总结优先 `ctx_execute_file`,日志、构建与不可预测命令输出优先 Context Mode;字面搜索先用 FFF `find` 收敛文件位置,再在收敛路径内用 `grep`/`multi_grep` 获取行号;真正修改前才用小范围 Hashline `read` 获取新鲜锚点并用锚定 `edit`。在线搜索由 `pi-web-search` 内部路由,模型不感知厂商:路由规则为广泛发现、新闻和候选来源偏 Tavily,官方/技术/论文和高确定性来源偏 Exa,中文、站点限定和日期筛选偏 Keenable;路由决策由当前主模型做出,确定性逻辑只处理可用性与失败兜底,不得改写成启发式断路。遇到大量结果、截断或上限时继续缩小范围,不得靠提高 limit 或倾倒全部结果解决。
|
||||
- `ask_user_question` 只用于模型继续任务所必需的用户决定、偏好、确认或澄清;不得重复询问直接用户消息中已有的信息。工具支持交互式 TUI 与具备 Pi extension UI bridge 的 RPC 客户端、顺序执行和最多 8 题;RPC 必须使用标准 `select` / `input` 对话框逐题收集,JSON/print 等无 UI 模式保持拒绝。取消或 abort 必须返回 `cancelled: true`,不得伪造答案,且 `/qna` 不属于该扩展。其成功 `details.answers` 必须保留 `question` / `answer` 字段,以匹配 AutoReview 已有的可信结构化用户交互边界。
|
||||
- Tool Search 默认常驻核心 `read` / `write` / `edit` / `bash` / `grep` / `find`、`codegraph_explore`、`lsp_diagnostics`、`ctx_execute` / `ctx_execute_file` / `ctx_batch_execute`、六个记忆工具与 `context_tree_query`,以及 `tool_search`;Windows 上内置 `powershell` 必须与 `bash` 同属固定本地 Shell 核心工具,非 Windows 平台必须从目录中过滤。配置从 `<agentDir>/my-pi/tool-search/config.json` 读取(由 rebuild 脚本部署 `config/tool-search.json`),文件缺失或无效时回退 `BUNDLE_TOOL_SEARCH_DEFAULTS`;扩展自身不播种、不迁移,也不再读写 `settings.json` 的 `toolSearch` 段。其余组合包工具必须优先使用 `pi-tool-search/extensions/bundle-groups.ts` 中提交的权威工作流分组;精确已有分组必须直接激活,不得等待无关未知工具的模型增强。默认最多 5 个动态组、模型组最多 8 个工具、动态工具总数最多 28;`groups` 可原子激活最多 5 个精确组,完整请求超限时不得改变当前状态;加载或组内调用更新 LRU,超限先移除最久未用的非请求组。自然语言查询必须优先精确工具名和已提交 workflow alias,弱匹配或近似同分只能返回候选,不得任意激活;`/tool-search-status` 必须显示当前容量、LRU 顺序与最近淘汰原因。
|
||||
- Tool Search 默认常驻核心 `read` / `write` / `edit` / `bash` / `grep` / `find`、`codegraph_explore`、`lsp_diagnostics`、`ctx_execute` / `ctx_execute_file` / `ctx_batch_execute`、六个记忆工具与 `context_tree_query`,以及 `tool_search`;Windows 上内置 `powershell` 必须与 `bash` 同属固定本地 Shell 核心工具,非 Windows 平台必须从目录中过滤。配置从已安装组合包内的 `config/tool-search.json` 直接读取,文件缺失或无效时回退 `BUNDLE_TOOL_SEARCH_DEFAULTS`;扩展自身不播种、不迁移,也不再读写 `settings.json` 的 `toolSearch` 段。其余组合包工具必须优先使用 `pi-tool-search/extensions/bundle-groups.ts` 中提交的权威工作流分组;精确已有分组必须直接激活,不得等待无关未知工具的模型增强。默认最多 5 个动态组、模型组最多 8 个工具、动态工具总数最多 28;`groups` 可原子激活最多 5 个精确组,完整请求超限时不得改变当前状态;加载或组内调用更新 LRU,超限先移除最久未用的非请求组。自然语言查询必须优先精确工具名和已提交 workflow alias,弱匹配或近似同分只能返回候选,不得任意激活;`/tool-search-status` 必须显示当前容量、LRU 顺序与最近淘汰原因。
|
||||
- `pi-chrome` 在未授权时不注册 Chrome 工具;显式授权后出现的 21 个 `chrome_*` 工具必须由 Tool Search 分入 `chrome-navigation`、`chrome-interaction`、`chrome-debugging`,不得落入 unknown-tools 模型增强路径。授权撤销或过期时必须停用全部 21 个工具,包括 `chrome_find` / `chrome_inspect`;Tool Search 重新激活 schema 不能替代每次执行时的授权校验。
|
||||
- 标准组合包工具全部命中预置目录时 Tool Search 不得调用模型、创建用户缓存或发送隐藏 schema。只有存在未识别第三方工具时才允许惰性模型增强;生成结果必须保留全部预置 assignment,模型元数据不得替代真实名称、参数或 schema。缓存必须位于 agent 目录且权限 `600`;模型失败必须继续使用预置目录 + 确定性未知工具分组。`groupOverrides` 优先级高于预置分组。
|
||||
- Tool Search 必须直接使用当前 `@earendil-works/pi-coding-agent`、`typebox`、`ModelRegistry.complete()` 和 `setActiveTools()`;首次纯增加载使用当前 Pi 的增量结果传播,LRU 替换允许走宿主安全 fallback。不得安装或映射旧 `@mariozechner` runtime,不得恢复 provider payload 改写、代理分发或隐藏 `sendMessage` steer/retry 兼容循环。Tool Search 是唯一可调用 `getActiveTools()` / `setActiveTools()` 管理工具显隐的组合包扩展;`pi-permission-system` 只能在调用时裁决,不能读写 active tool 列表。
|
||||
@@ -91,20 +91,21 @@
|
||||
- `codegraph_explore` 为原生 Pi 工具而非 MCP 工具:每次调用通过 `execFile`/`pi.exec` 以 argv 形式运行 `codegraph explore`(带超时、不经 shell),结果按 `.codegraph/codegraph.db` mtime 缓存;CLI 缺失、init 被拒绝或执行失败都不得抛出或阻止其他工具,必须以文本结果打回并引导模型改用 `find`/`grep`/`read`/LSP。
|
||||
- 组合包不加载任何 MCP adapter;Exa 由 `pi-web-search/clients/exa.ts` 以纯 `fetch` JSON-RPC 直连托管端点 `https://mcp.exa.ai/mcp`:`initialize` 一次并缓存 `mcp-session-id`,随后 `tools/call`,HTTP 404 视为会话失效自动重建并重试一次。`exa` 凭据通过 `x-api-key` 请求头发送,不得放入 URL、仓库文件或日志。
|
||||
- 在线搜索对模型只暴露 `web_search` / `web_fetch` 两个统一工具名,不得重新引入按厂商命名的工具;`web_search` 必须经 `router.ts` 的主模型分类选择 provider,`web_fetch` 固定优先 Exa。Exa 搜索统一走 `web_search_advanced_exa` 并显式约束 `numResults` 与 `textMaxCharacters`;Tavily 默认 `search_depth: basic` 且必须在 auth.json 存在 `tavily` api_key 凭据才参与路由;Keenable keyless 可用,`keenable` 凭据仅提高速率限制。
|
||||
- 搜索 key 统一保存在 Pi 的 `auth.json`(`tavily`/`exa`/`keenable` 各一条 `api_key` 凭据,文件权限 `600` 且不得提交);key 支持 `!command` 与 `$VAR` 间接寻址,`search_config.sh` / `search-auth.mjs` 只输出配置状态,禁止回显 key。旧的 `search.env` 不再被读取,仅由 `--migrate` 一次性导入。
|
||||
- 搜索 key 统一保存在 Pi 的 `auth.json`(`tavily`/`exa`/`keenable` 各一条 `api_key` 凭据,文件权限 `600` 且不得提交);key 支持 `!command` 与 `$VAR` 间接寻址,`configure.sh search` / `search-auth.mjs` 只输出配置状态,禁止回显 key。旧的 `search.env` 不再被读取,仅由 `--migrate` 一次性导入。
|
||||
- `extensions/context-view.ts` 是只读的本地 effective-context observer,只读取 `pi-compaction/src/service.ts` 发布的 projection metrics,不注册生产 `context` writer;`pi-minimal-footer` 优先显示同一 effective usage,snapshot 尚未建立时才回退宿主原始 usage。
|
||||
- `pi-compaction` 负责已经进入 Session 的历史投影,与 Context Mode 的输入隔离职责互补。首次 Session hydration 允许一次完整扫描;后续普通 `context` hot path 必须复用不可变 snapshot 或只处理 delta,不得调用 summarizer、写 Session 或重新构建完整 branch。
|
||||
- 已闭合工具结果先同步生成保留工具名、参数、状态、首尾证据和 source key 的有界确定性 stub;大于 `semantic.minInputChars` 的结果再进入单 Session 有界后台语义 worker。后台结果只在安全事件边界提交,陈旧 hash 必须丢弃,Provider 请求不得等待后台摘要。
|
||||
- effective token 默认使用 Provider usage-backed baseline 加 projected delta;无可靠 baseline 时使用带上下界的估算。Pi Core raw threshold 只负责唤醒,只有 effective lower bound 达到 `hardRatio` 才执行 threshold compaction;manual 与 overflow 不得被阻断。
|
||||
- Codex native compaction 必须直接消费当前 projection 生成的 ResponseItems,不得从 raw branch 重建输入;native checkpoint 同时保存有界 portable summary 供模型或 Provider 切换。配置只从 `settings.json.piCompaction` 读取且不写回,默认 soft/hard/target 比例为 72%/90%/55%。
|
||||
- Codex native compaction 必须直接消费当前 projection 生成的 ResponseItems,不得从 raw branch 重建输入;native checkpoint 同时保存有界 portable summary 供模型或 Provider 切换。配置只从组合包 `config/pi-compaction.json` 读取且不写回 `settings.json`,默认 soft/hard/target 比例为 72%/90%/55%。
|
||||
- Kitty 完成通知只在 TUI 模式的 `agent_settled` 后发送;自动重试、自动 compaction 和 follow-up 期间不得提前通知或重置总耗时。权限通知只订阅 `permissions:ui_prompt`,在人类确认界面即将出现时立即发送,不得为策略或 AutoReview 已直接决定的请求通知,也不受完成通知时长门槛影响。默认 `o=always`、`a=focus`,每个 Pi Session 使用独立稳定 ID,标题和正文必须 Base64 编码。
|
||||
- `/notify on|off` 只覆盖当前 Session;持久默认来自 `PI_NOTIFY_*` 环境变量。`PI_NOTIFY_MESSAGE_SOURCE=none` 必须继续提供不泄露回复正文、权限值或请求者细节的隐私模式,非 TUI 模式不得写入 OSC 序列。
|
||||
- 权限策略默认允许常规工具,允许 FFF 工具;拒绝 Bash 直搜,硬防御默认只保留确定性的破坏性 Shell 命令;Git 非只读操作、包管理、文件/系统/网络高风险操作与内置 MCP 的 `mcp__<server>__<tool>` 调用按唯一全局 policy 判定。项目外路径不再产生独立 `external_directory` 判断,只走配置中的工具、`path` 与 Bash pattern。
|
||||
- 权限策略默认允许常规工具,允许 FFF 工具,并放行 `ls`、LSP 查询(`lsp_definition`/`lsp_references`/`lsp_hover`/`lsp_symbols`)、`ctx_search`/`ctx_index`/`ctx_stats`/`ctx_doctor`、`web_search` 与 Chrome 只读检查工具;`web_fetch`、`ctx_fetch_and_index` 与 `skill_manage` 保持 `ask`(模型自选的 URL 可能外传数据,skill_manage 写入的 skill 会进入所有后续会话);拒绝 Bash 直搜,硬防御默认只保留确定性的破坏性 Shell 命令;Git 只读管道命令(`blame`/`describe`/`rev-list`/`merge-base`/`ls-tree`/`cat-file`/`name-rev`/`shortlog`/`stash list`/`worktree list`/`tag -l|--list`/`config --get|--list`)放行,其余非只读操作、包管理、文件/系统/网络高风险操作与内置 MCP 的 `mcp__<server>__<tool>` 调用按唯一全局 policy 判定。项目外路径不再产生独立 `external_directory` 判断,只走配置中的工具、`path` 与 Bash pattern。
|
||||
- SSH 权限沿用同一条 `pi-permission-system` gate 与 `auto-review` authorizer chain:`ssh_connect`、`ssh_cd`、`ssh_read`、`ssh_write`、`ssh_edit`、`ssh_find`、`ssh_grep` 默认 `ask`;连接前的证据解析已导入 host ID 为非秘密 target、port 与请求/default cwd,AutoReview 只可依据用户直接消息中的明确目标授权连接。`ssh_bash` 通过 `shellTools` 映射到完整 Bash 策略并设置 `decisionFloor: "ask"`,把普通 Bash `allow` 提升为复核请求,同时保留原有 `ask` 与硬 `deny`。全部远端路径不得送入基于本机 cwd 的默认 `input.path` 归一化。
|
||||
- `auto-review` 是 `pi-permission-system` 内置 authorizer,不是独立扩展或第二个 `tool_call` gate;只复核普通策略产生的 `ask`,不会重复处理普通策略已 `allow` / `deny` 或硬防御已 `deny` 的请求。
|
||||
- reviewer 是对话式 Codex Guardian 风格审查:以独立请求(不写入主会话)把有界 active-branch 对话记录与权限请求交给模型,模型只能通过 `approve` / `reject` 两个工具给出带 `reason` 的结论,不强制 `toolChoice`。`approve` 自动批准(包括 `path`);`reject` 以及配置、模型、超时、Provider 错误、截断、未调用工具、多次调用或参数无效等一切异常都必须 `defer` 到人工提示,并在弹窗中显示原因(模型理由标注为模型生成)。正文文本即使是合法 JSON 也不得作为结论;只有用户按 Esc 中止当前操作时才直接拒绝。不得在 authorizer chain 外增加第二层内置授权范围限制,也不得恢复 annotator 机制。
|
||||
- reviewer 是对话式 Codex Guardian 风格审查:以独立请求(不写入主会话)把有界 active-branch 对话记录与权限请求交给模型,模型只能通过 `approve` / `reject` 两个工具给出带 `reason` 的结论,不强制 `toolChoice`。`approve` 自动批准(包括 `path`);`reject` 以及配置、模型、超时、Provider 错误、截断、未调用工具、多次调用或参数无效等一切异常都必须 `defer` 到人工提示,并在弹窗中显示原因(模型理由标注为模型生成)。正文文本即使是合法 JSON 也不得作为结论;只有用户按 Esc 中止当前操作时才直接拒绝。不得在 authorizer chain 外增加第二层内置授权范围限制,也不得恢复 annotator 机制。唯一例外是 `protectedWrites`:硬防御文件里配置的确定性 human-only 写路径——它不经 authorizer chain(含 AutoReview)、yolo 或 Session grant,无确认界面时直接拒绝,本身不产生裁决,只是永远把人拉进回路。
|
||||
- reviewer 默认跟随会话当前主模型(每次审查实时读取 `ctx.model`),经 `modelRegistry.streamSimple(...).result()` 调用;`autoReview.provider` / `autoReview.model` 必须同时设置(固定审查模型)或同时留空,只设其一视为配置错误并 `defer`。默认 `reasoning: "low"`(仅对支持推理的模型生效)、总超时 30 秒、最多 2 次尝试(仅 Provider 错误重试)、`maxOutputTokens` 4000。只把 active branch 中的直接用户消息与已识别、未取消的结构化问答作为授权证据,assistant/tool/compaction 内容不能自行授权;工具结果可替换为更短的 pi-compaction 摘要,但仍属不可信证据。同一条用户消息内完全相同的请求只缓存 `approve` 结论。
|
||||
- `config/pi-permission-policy.jsonc` 必须显式配置 `authorizerChain: ["auto-review"]`,普通未匹配请求和需要自动复核的 Git 非只读、包管理等类别声明为 `ask`;普通 pattern 必须按具体度优先(精确匹配、字面字符更多、通配符更少、pattern 更长,最后才以声明顺序破同级);`autoReview` 段只保留 `enabled`,不设 provider/model 种子值,使审查默认跟随主模型;`config/pi-permission-hard-guards.jsonc` 独立保存可由用户调整但不能被普通策略、Session grant、per-agent 规则或 reviewer 覆盖的高危 `deny`。
|
||||
- `config/pi-permission-policy.jsonc` 必须显式配置 `authorizerChain: ["auto-review"]`,普通未匹配请求和需要自动复核的 Git 非只读、包管理等类别声明为 `ask`;普通 pattern 必须按具体度优先(精确匹配、字面字符更多、通配符更少、pattern 更长,最后才以声明顺序破同级);`autoReview` 段只保留 `enabled`,不设 provider/model 种子值,使审查默认跟随主模型;`config/pi-permission-hard-guards.jsonc` 独立保存可由用户调整但不能被普通策略、Session grant、per-agent 规则或 reviewer 覆盖的高危 `deny`,以及 `protectedWrites` 人工写保护清单。
|
||||
- `protectedWrites`(`config/pi-permission-hard-guards.jsonc`)是确定性的 human-only 写路径,不属于普通策略也不是第二个授权范围:命中目标的 `write`/`edit` 与本地 `bash` 写入(redirect-out、写操作程序参数、`bash -c`/`eval` 等包装载荷中的受保护路径)一律弹出人工确认,跳过 AutoReview、yolo 与既有 Session grant,弹窗不提供「本会话允许」选项;没有可用确认界面(无 UI、子代理转发终端)时直接拒绝。读取不受影响;`ssh_bash` 等远端 shell 工具不参与本机路径保护。
|
||||
- `pi-lsp` 的 TypeScript/JavaScript 后端由根包固定依赖提供,`extensions/lsp.ts` 使用当前 Node 可执行文件直接启动包内 `typescript-language-server` CLI,不得依赖或调用 VS Code GUI;Kotlin/JDT LS 仍使用组合包部署的 `kotlin-lsp --stdio` 与 `jdtls` 配置,根包不安装这两个系统可执行文件,便捷脚本可在用户逐项明确确认后通过 Homebrew 安装 Kotlin LSP、Java 21 和 JDT LS。
|
||||
- 终端环境配置只属于便捷脚本:Kitty 可选安装后可通过官方 kitten 启用 Solarized Dark;Oh My Zsh 使用不切换 shell 的 unattended 安装;Powerlevel10k 将仓库内置的 `config/p10k.zsh`(Rainbow/ASCII 单行紧凑主题)部署到 `~/.config/my-pi/p10k.zsh`(遵循 `XDG_CONFIG_HOME`),不得覆盖用户自己的 `~/.p10k.zsh`。`.zshrc` 中脚本拥有的内容必须使用 `# >>> my-pi:<id> >>>` / `# <<< my-pi:<id> <<<` 受管块,更新时只替换块内内容;标记不完整或重复时拒绝修改。实际修改前必须创建带时间戳的备份。`git`、`zsh-autosuggestions`、`zsh-syntax-highlighting` 在安装流程中逐项询问后才安装或启用。
|
||||
- 原生远程压缩限定为 `openai-codex` + `openai-codex-responses` 与 `sub-api` + `openai-responses`,默认在 turn boundary 达到 effective 下界 90% 时触发。`pi-compaction/src/native-provider.ts` 统一资格与传输选择:Codex 保留原 endpoint/JWT/account ID;SubAPI 从显式 `baseUrl` 解析公共 `/responses`,使用配置的 API key 和请求头,不解析 ChatGPT account ID、不硬编码网关域名,也不调用独立 `/responses/compact`。两者复用 projection、`compaction_trigger`、SSE 结果、native checkpoint 恢复和 portable 模型切换摘要;SubAPI 部署须支持原生 compaction v2。普通聊天支持不代表压缩支持,失败回退 Pi,不自动改写用户模型配置。
|
||||
@@ -116,14 +117,22 @@
|
||||
- `skill_manage` 是 `pi-hermes-memory` 提供的能力,不是 Pi 宿主能力:Pi 只发现并加载 skill 目录(`~/.pi/agent/skills/` 加扩展通过 `resources_discover` 注册的目录),没有创建 skill 的工具。该扩展同时承担 skill 写入与目录注册;不加载它,`projects-memory/<项目>/skills/` 下的既有 skill 也不会被 Pi 发现。
|
||||
- `install.sh` 默认从 `git:git@bitbucket.org:siakitem/my-pi.git` 安装,并允许用 `PI_PACKAGE_SOURCE` 覆盖来源;组合包安装失败时立即停止,机器级依赖安装失败时继续检查其余依赖并最终返回非零状态。
|
||||
- CodeGraph 的可选安装使用官方远程安装脚本;macOS Kitty 使用 Homebrew cask,其他系统使用 Kitty 官方安装器;Kotlin LSP、Java 21 和 JDT LS 使用 Homebrew;Oh My Zsh 使用官方安装器,Powerlevel10k 与两个第三方 Zsh 插件从各自官方 Git 仓库克隆。升级时 Git checkout、Homebrew 包、Kitty 官方安装和 CodeGraph 都必须先比较本地与远端版本,版本一致时不得重新下载安装;无法可靠比较时保留现状并报告。缺少 Homebrew、curl 或 git 时保留已经完成的组合包安装并报告失败。
|
||||
- `install.sh` 不检查默认不需要的 `rtk` CLI;`update.sh` 和 `uninstall.sh` 使用同一个 `PI_PACKAGE_SOURCE` 规则分别调用 `pi update` 和 `pi remove`。运行 `update.sh` 视为明确授权升级已安装组件并整体重建 `<agentDir>/my-pi/` 受管配置目录:重建按 `config/bundle-manifest.json` 全量替换,不逐字段迁移或合并用户改动;需要长期保留的自定义必须改仓库 `config/` 源文件。不得安装缺失组件或改写受管块之外的用户配置;卸载不得删除用户记忆数据,其他共享机器级工具保持不变。
|
||||
- `<agentDir>/my-pi/` 只存放组合包拥有、可随时重建的配置/日志/缓存;`install.sh` / `update.sh` 每次运行都整体删除后按 `config/bundle-manifest.json` 重建且不备份。需要长期保留的配置修改必须改仓库 `config/` 中的源文件;用户数据(记忆、会话、`auth.json`、`models.json`、`settings.json`、`search.env` 等)不得放入该目录。外部 npm 扩展固定路径的 `hashline.json`、`lsp.json` 位于 agent 根目录,是唯一例外;旧位置(`extensions/pi-permission-system/`、`settings.json` 的 `toolSearch` 段、agent 根 `SYSTEM_COVER.md`)的残留不迁移也不清理。
|
||||
- `install.sh` 不检查默认不需要的 `rtk` CLI;`update.sh` 和 `uninstall.sh` 使用同一个 `PI_PACKAGE_SOURCE` 规则分别调用 `pi update` 和 `pi remove`。运行 `update.sh` 视为明确授权升级已安装组件;单独的 `pi update` 只在新版本存在时整体覆盖已安装副本(含 `config/`),`update.sh` 每次成功升级后都会把受管 checkout 还原到已发布版本,两者都不逐字段迁移或合并本机对已安装副本的改动;需要长期保留的自定义必须改仓库 `config/` 源文件。不得安装缺失组件或改写受管块之外的用户配置;卸载不得删除用户记忆数据,其他共享机器级工具保持不变。
|
||||
- `<agentDir>/my-pi/` 只存放可随时丢弃的运行时状态(日志、缓存),不存放配置;组合包配置一律以仓库 `config/` 为唯一来源并由扩展直接读取。用户数据(记忆、会话、`auth.json`、`models.json`、`settings.json`、`search.env` 等)不得放入该目录。外部 npm 扩展固定路径的 `hashline.json`、`lsp.json` 位于 agent 根目录,是唯一例外(扩展加载时从 `config/` 覆盖写入);旧位置(`extensions/pi-permission-system/`、`settings.json` 的 `toolSearch` 段、agent 根 `SYSTEM_COVER.md`)的残留不迁移也不清理。
|
||||
|
||||
## 组合包配置原则
|
||||
|
||||
- 本组合包是私有组合包,所有机器上的组合包配置必须一致。组合包自有扩展的全部配置只以仓库 `config/` 为唯一来源,扩展运行时直接读取已安装组合包内的 `config/` 文件,不复制、不播种、不迁移到 agent 目录。
|
||||
- 配置变更一律修改仓库 `config/` 并随组合包发布;`pi install` / `pi update` 即完成部署,便捷脚本不得再承担配置部署。某台机器上对已安装副本的临时修改(包括扩展设置界面的保存)不做保护、合并或提示:单独运行 `pi update` 只在有新版本时覆盖,`update.sh` 每次成功升级后都会把受管 checkout 还原到已发布版本。
|
||||
- 新增或导入扩展时,其可配置项必须放进 `config/` 下的独立文件;不得读写 Pi `settings.json` 的自定义段、agent 目录下的独立配置文件,也不得以环境变量作为持久默认值。只有外部 npm 扩展固定读取 agent 目录路径时(当前为 `hashline.json`、`lsp.json`),才允许在扩展加载时从 `config/` 覆盖写入该路径。
|
||||
- `config/` 只放与机器无关的值;依赖本机路径的默认值(home 下的目录、可执行文件绝对路径等)保留在代码中运行时计算。
|
||||
- `<agentDir>/my-pi/` 只存放可随时丢弃的运行时状态(日志、缓存),不得存放配置。用户数据(记忆、会话、`auth.json`、`models.json`、凭据库)不属于组合包配置,不受本节约束,也不得放进 `config/`。
|
||||
|
||||
## 修改边界
|
||||
|
||||
- 优先在目标扩展目录内完成改动;不要让一个扩展依赖另一个扩展的未公开内部实现。
|
||||
- 保留原项目的 `LICENSE`、版权信息和必要的来源说明。
|
||||
- agent 目录下扩展运行时产生的数据——`<agentDir>/my-pi/` 受管重建目录、旧 `extensions/pi-permission-system/` 残留、日志、构建产物、覆盖率目录和依赖目录——属于本地状态,不应提交;`config/bundle-manifest.json`、`config/pi-permission-policy.jsonc`、`config/pi-permission-hard-guards.jsonc`、`config/tool-search.json`、`config/system-prompt-cover.md`、`config/pi-hashline-edit.json` 与 `config/lsp.json` 是组合包权威源配置,必须提交并维护。
|
||||
- agent 目录下扩展运行时产生的数据——`<agentDir>/my-pi/` 可丢弃运行时目录、旧 `extensions/pi-permission-system/` 残留、日志、构建产物、覆盖率目录和依赖目录——属于本地状态,不应提交;`config/` 下的全部文件(`pi-permission-policy.jsonc`、`pi-permission-hard-guards.jsonc`、`tool-search.json`、`rtk-optimizer.json`、`pi-compaction.json`、`pi-worktree.json`、`pi-hermes-memory.json`、`system-prompt-cover.md`、`pi-hashline-edit.json`、`lsp.json`、`p10k.zsh` 等)是组合包权威源配置,必须提交并维护。
|
||||
- 外部 Pi 扩展依赖必须在根 `package.json` 中使用精确版本,自维护扩展必须使用本地 `file:` 依赖,并更新根 `package-lock.json`;不要用仓库级 `.pi/settings.json` 代替组合包依赖。
|
||||
- 需要原生构建的依赖只按锁定版本加入根 `allowScripts`;当前 `better-sqlite3@12.11.1` 仍由 Context Mode 使用并保持精确放行。`pi-hermes-memory` 的 `better-sqlite3@13` 运行时使用包内 N-API 预编译二进制,npm 因 `binding.gyp` 推断出的 `node-gyp rebuild` 无需执行,不得为此新增放行条目。
|
||||
- Pi 核心包只作为宿主 peer dependencies,不得在组合包内再安装或打包一套 Pi runtime;保留根 `.npmrc` 的 peer 安装策略。
|
||||
@@ -133,8 +142,8 @@
|
||||
- 每个自维护扩展目录必须保留 `UPSTREAM.md` 来源记录,并且**必须用中文维护**:记录官方仓库、tag/commit、导入版本、导入日期、许可证归属、本地改动清单,以及同步上游时的约束。`UPSTREAM.md` 是仓库自有的维护文档,不是上游产物;上游自带的源码、测试、文档、README、CHANGELOG 与 `LICENSE` 一律保持上游原文,不翻译、不改写。新增扩展或同步上游导致来源信息变化时,必须同步更新该文件。
|
||||
- `pi-notify/` 的根依赖必须保持 `file:./pi-notify`,组合包直接加载 `pi-notify/src/index.ts`;不得同时安装或加载 npm 预编译入口,否则会重复通知。
|
||||
- 上游仅作为参考来源。同步上游改动时先核对本仓库已有修改,再按明确范围移植;不要直接覆盖本地实现。
|
||||
- `install.sh`、`update.sh`、`uninstall.sh` 和 `search_config.sh` 必须保持 POSIX `sh` 兼容和可执行权限,并包含在根 `package.json` 的 `files` 中;修改脚本行为时同步更新 README 和本文件。
|
||||
- 安装流程中的机器级依赖和用户终端配置必须保持逐项询问且默认拒绝,不得在没有用户明确确认的情况下自动安装或改写。用户主动运行 `update.sh` 只授权升级已安装项和整体重建 `my-pi/` 受管配置目录;重建必须先完成全部校验并在临时目录构建成功后一次性替换,任何失败不得留下半重建状态。缺失项仍必须跳过。`.zshrc` 修改必须局限于受管块并保留备份,卸载不得顺带删除或还原共享工具和用户终端配置。
|
||||
- `install.sh`、`update.sh`、`uninstall.sh`、`configure.sh` 和 `scripts/lib.sh` 必须保持 POSIX `sh` 兼容,前者包含在根 `package.json` 的 `files` 中;修改脚本行为时同步更新 README 和本文件。
|
||||
- 安装流程中的机器级依赖和用户终端配置必须保持逐项询问且默认拒绝,不得在没有用户明确确认的情况下自动安装或改写。用户主动运行 `update.sh` 只授权升级已安装项;缺失项仍必须跳过。`.zshrc` 修改必须局限于受管块并保留备份,卸载不得顺带删除或还原共享工具和用户终端配置。
|
||||
- 未经明确要求,不执行发布、提交、推送、运行安装/卸载脚本或安装到用户 Pi 运行目录等外部写操作。
|
||||
|
||||
## 根目录 TypeScript 与 LSP 验证约定
|
||||
@@ -167,7 +176,7 @@
|
||||
- 根包必须通过 `file:./pi-permission-system` 提供运行时依赖,包装入口必须直接加载仓库源码;不得同时加载 npm 预编译入口或第二个 permission-system 实例。
|
||||
- 内置 AutoReview 实现位于 `pi-permission-system/src/auto-review/`;同步原 AutoReview 上游时必须从源码仓库明确 tag/commit 移植,保留 `LICENSES/pi-permission-auto-review-MIT.txt`、作者和 `docs/auto-review/UPSTREAM.md`,不得恢复顶层独立扩展、npm 预编译入口或平行 gate。
|
||||
- reviewer 只能处理普通策略已判定为 `ask` 且未被 hard guard 拒绝的请求;不得绕过硬 `deny`。
|
||||
- `HardGuardPolicyLoader` 必须保持 global-only、无 SessionRules、无项目覆盖;用户文件和 bundled fallback 都不可用时 fail closed 到 deny-all,并报告配置问题。普通 policy 与 hard guard 配置不得合并成一份文件。
|
||||
- `HardGuardPolicyLoader` 必须保持 global-only、无 SessionRules、无项目覆盖;唯一的组合包 hard-guards 文件不可用时 fail closed 到 deny-all,并报告配置问题。普通 policy 与 hard guard 配置不得合并成一份文件。
|
||||
- 修改 reviewer 策略、可信证据边界、配置结构、hard-guard precedence、模型解析或 authorizer/lifecycle 注册时,必须同步更新 README、配置文档、架构文档、schema 和相应测试。
|
||||
|
||||
|
||||
@@ -179,14 +188,14 @@
|
||||
- 六个记忆工具与 `context_tree_query` 必须在 `pi-tool-search/extensions/config.ts` 的 `BUNDLE_TOOL_SEARCH_DEFAULTS.alwaysEnabled` 中常驻,并由 `pi-tool-search/extensions/bundle-groups.ts` 的 `memory-recall` / `memory-management` 分组提供目录元数据(`context_tree_query` 留在 `memory-recall`)。新增、删除或重命名这些工具时必须同步 `bundle-groups.ts`、`BUNDLE_TOOL_SEARCH_DEFAULTS`、`config/tool-search.json`(二者必须完全一致)、Tool Search 测试、权限策略与 `pi-tool-search` 的 README / CHANGELOG。
|
||||
- 依赖必须保持 `better-sqlite3@^13.0.3`:13.x 运行时使用包内 N-API 预编译二进制 `prebuilds/<platform>.node`,不依赖 node-gyp 编译产物,必须能由 npm 嵌套安装并与根 `better-sqlite3@12.11.1` 并存。不得把它降级回 12.x(Node 24 下会在进程退出时 SIGABRT),也不得让根包提升为单一版本。
|
||||
- `devDependencies` 中的 `@earendil-works/*` 必须与根包当前版本精确一致。若写成 `^0.80.2` 这类与根不兼容的范围,npm 会在本目录嵌套安装第二套 Pi runtime,违反"不得在组合包内再安装一套 Pi runtime"的纪律;`typebox`、`tsx`、`typescript`、`@types/better-sqlite3` 同理,必须钉到根已有版本。
|
||||
- 全局记忆、项目记忆与 skill 位于 `~/.pi/agent/pi-hermes-memory/` 与 `~/.pi/agent/projects-memory/`,配置位于 `~/.pi/agent/hermes-memory-config.json`;这些属于用户数据,安装、升级与卸载脚本不得创建、迁移或删除。
|
||||
- 全局记忆、项目记忆与 skill 位于 `~/.pi/agent/pi-hermes-memory/` 与 `~/.pi/agent/projects-memory/`,属于用户数据,安装、升级与卸载脚本不得创建、迁移或删除;扩展配置位于已安装组合包的 `config/pi-hermes-memory.json`(其中路径类默认值由代码运行时计算),旧 `~/.pi/agent/hermes-memory-config.json` 被忽略。
|
||||
- 修改工具注册、`before_agent_start` 注入策略、`resources_discover` 目录注册、SQLite 迁移或 consolidation 后,必须同步包内 README/CHANGELOG/docs,并运行根扩展联合加载与 Tool Search 测试。
|
||||
|
||||
## `pi-compaction` 开发约定
|
||||
|
||||
- 扩展入口是 `pi-compaction/index.ts`,增量索引、投影、后台摘要、服务与 Codex native 实现在 `pi-compaction/src/`,测试位于 `pi-compaction/test/`,双上游来源记录是 `pi-compaction/UPSTREAM.md`。
|
||||
- 根包必须通过 `file:./pi-compaction` 提供依赖并直接加载源码;不得重新声明或加载 `pi-condense`、npm `@ogulcancelik/pi-codex-compaction` 或 npm `pi-context-view`。
|
||||
- `pi-compaction` 只支持自己的 version 1 记录和 `piCompaction` 配置,不得恢复旧状态兼容、自动迁移或加载时写回 `settings.json`。
|
||||
- `pi-compaction` 只支持自己的 version 1 记录和 `config/pi-compaction.json` 配置,不得恢复旧状态兼容、自动迁移或读写 `settings.json` 自定义段。
|
||||
- Context Authority 是 compaction 唯一生产投影者。普通 Provider、Codex native、`/context`、`/compaction-status` 和 footer 必须消费同一 snapshot;background worker 不得在 Provider 请求中被 await。
|
||||
- Segment custom entry 只能保存摘要、hash 和 source reference,不得复制完整原始工具输出。`context_tree_query` 恢复结果必须有界,native checkpoint 的 portable summary 也必须有界。
|
||||
- 修改 projection、token accounting、threshold、checkpoint、模型切换、后台并发或恢复索引后,至少运行 `npm test`、`npm run check`、`npm pack --dry-run --json`、根扩展联合加载、packed 根 tarball 隔离加载和性能回归。性能门禁为 cache-hit P95 <2ms、delta projection P95 <10ms,普通 hot path 不得扫描完整 branch。
|
||||
@@ -224,7 +233,7 @@
|
||||
|
||||
## `pi-ssh` 开发约定
|
||||
|
||||
- 扩展入口是 `pi-ssh/index.ts`,`ssh2` transport 与 SFTP 实现在 `pi-ssh/src/ssh2-transport.ts`,有界自适应远程搜索位于 `pi-ssh/src/remote-search.ts`,AES-GCM vault 位于 `pi-ssh/src/vault.ts`,选择性导入入口是根 `ssh_config.sh` 与 `pi-ssh/scripts/ssh-config.mjs`,权限桥接位于 `pi-ssh/permission-integration.ts`,测试保留在 `pi-ssh/test/`,来源记录是 `pi-ssh/UPSTREAM.md`。
|
||||
- 扩展入口是 `pi-ssh/index.ts`,`ssh2` transport 与 SFTP 实现在 `pi-ssh/src/ssh2-transport.ts`,有界自适应远程搜索位于 `pi-ssh/src/remote-search.ts`,AES-GCM vault 位于 `pi-ssh/src/vault.ts`,选择性导入入口是根 `configure.sh ssh` 与 `pi-ssh/scripts/ssh-config.mjs`,权限桥接位于 `pi-ssh/permission-integration.ts`,测试保留在 `pi-ssh/test/`,来源记录是 `pi-ssh/UPSTREAM.md`。
|
||||
- 同步上游必须从明确 tag/commit 移植并保留 `LICENSE`、作者和来源;参考其他实现时记录明确 commit,不得导入嵌套 `.git`、`node_modules` 或构建产物。
|
||||
- 运行时必须保持纯 `ssh2`,不得回退到系统 OpenSSH、`sshpass`、`SSH_ASKPASS` 或 ControlMaster;认证可使用密码、私钥文件或 `ssh2` 直连的 SSH Agent socket(包括 1Password),Agent 模式只保存 socket 路径且不得导出私钥。OpenSSH 只允许由显式配置脚本通过 `ssh -G` 解析用户选中的 alias,已导入配置变化必须由用户显式更新。第一版遇到 `ProxyJump` / `ProxyCommand` 必须拒绝,不能静默忽略。
|
||||
- 根包必须直接固定安装 `ssh2@1.17.0` 与配置 CLI 所需的 `jiti@2.7.0`,因为 packed bundle 直接加载其内置 `pi-ssh/` 源码;`ssh2` 与 `cpu-features` 的 install scripts 只构建可选加速绑定,当前不得加入根 `allowScripts`,纯 JavaScript fallback 必须可运行。
|
||||
@@ -244,14 +253,14 @@
|
||||
|
||||
## `extensions/system-prompt-cover.ts` 开发约定
|
||||
|
||||
- 扩展入口是 `extensions/system-prompt-cover.ts`;纯 helper(`spliceCover` / `readCoverFile` / `resolveCoverPath` / `COVER_FILE_NAME`)位于 `extensions/system-prompt-cover-config.ts` 以便 `node --test` 单测;部署由 `scripts/rebuild-bundle-config.mjs` 按 `config/bundle-manifest.json` 统一完成;组合包权威源是 `config/system-prompt-cover.md`。
|
||||
- 扩展入口是 `extensions/system-prompt-cover.ts`;纯 helper(`spliceCover` / `readCoverFile` / `resolveCoverPath` / `COVER_FILE_NAME`)位于 `extensions/system-prompt-cover-config.ts` 以便 `node --test` 单测;组合包权威源是 `config/system-prompt-cover.md`,扩展运行时直接读取已安装组合包内文件,不经任何脚本部署。
|
||||
- 只能使用 Pi 的 `forceSystemPrompt` 投影路径(`before_agent_start` 返回 `{ systemPrompt }`)配合文本拼接来保持分区顺序;不得改用 `customPrompt` 选项。后者会抑制 `tools` / `rules` / `docs` 三个分区,而回填只能追加到末尾,无法恢复原顺序。
|
||||
- 不得在 handler 中写 `customPrompt` 或依赖其生效:`forceSystemPrompt` 优先级更高,二者同时存在时 `customPrompt` 会被静默忽略而不报错。
|
||||
- 分割边界必须是独占一行的裸标签(带 backreference 同名闭合),不能匹配带属性的标签:`<project_instructions path="...">` 嵌套在 `<project_context>` 内,只有裸标签才是分区边界。
|
||||
- 封面文件是 `<agentDir>/my-pi/system-prompt-cover.md`,不得改名为 `SYSTEM.md`:后者会被 Pi 先当成 `customPrompt` 读入,封面扩展拿到的提示词会缺少可回填的分区。
|
||||
- 封面文件是已安装组合包内的 `config/system-prompt-cover.md`,不得改名为 `SYSTEM.md` 或移到 agent 根目录:后者会被 Pi 先当成 `customPrompt` 读入,封面扩展拿到的提示词会缺少可回填的分区。
|
||||
- 文件缺失、不可读或为空白时必须静默 no-op,保持 Pi 默认封面;不得写入空 preamble,也不得因读取失败阻止其他扩展加载。
|
||||
- `install.sh` / `update.sh` 必须从**当前升级后**组合包内的 helper 重建 `my-pi/`(`update.sh` 只定位 `updated_bundle_dir` 内的 `scripts/rebuild-bundle-config.mjs`,禁止回退到可能陈旧的脚本目录;`install.sh` 使用自身脚本目录副本),helper 缺失时报告失败但继续处理其他组件。重建不保留备份,与 `pi-hashline-edit.json` 的组合包权威配置语义一致。
|
||||
- 修改 `spliceCover` 语义、注册顺序、部署路径或脚本钩子后,必须运行 `node --test tests/system-prompt-cover.test.ts`、`node --test tests/bundle-config-rebuild.test.ts`、`node --test tests/extension-load.test.ts`、`sh -n install.sh`、`sh -n update.sh`,并确认 `npm pack --dry-run` 包含 `config/system-prompt-cover.md`、`config/bundle-manifest.json` 与 `scripts/rebuild-bundle-config.mjs`。
|
||||
- 封面文件随组合包发布,`pi install` / `pi update` 后即就位,无需任何部署 helper;对已安装副本的临时修改由 `pi update` 覆盖。
|
||||
- 修改 `spliceCover` 语义、注册顺序或封面路径后,必须运行 `node --test tests/system-prompt-cover.test.ts`、`node --test tests/bundle-config.test.ts`、`node --test tests/extension-load.test.ts`,并确认 `npm pack --dry-run` 包含 `config/system-prompt-cover.md`。
|
||||
|
||||
|
||||
## `pi-ask-user` 开发约定
|
||||
@@ -266,7 +275,7 @@
|
||||
|
||||
## `pi-tool-search` 开发约定
|
||||
|
||||
- 扩展入口是 `pi-tool-search/extensions/index.ts`,组合包权威分组位于 `pi-tool-search/extensions/bundle-groups.ts`,目录生成/校验/缓存位于 `pi-tool-search/extensions/catalog.ts`,配置读取与路径解析位于 `pi-tool-search/extensions/config.ts`(`<agentDir>/my-pi/tool-search/config.json`,由 rebuild 脚本部署),测试保留在 `pi-tool-search/test/`。
|
||||
- 扩展入口是 `pi-tool-search/extensions/index.ts`,组合包权威分组位于 `pi-tool-search/extensions/bundle-groups.ts`,目录生成/校验/缓存位于 `pi-tool-search/extensions/catalog.ts`,配置读取与路径解析位于 `pi-tool-search/extensions/config.ts`(直接读取组合包 `config/tool-search.json`;目录缓存仍在 `<agentDir>/my-pi/tool-search/catalog-v1.json`),测试保留在 `pi-tool-search/test/`。
|
||||
- 同步上游必须记录明确 tag/commit,保留 README、CHANGELOG、文档、作者和许可证说明;不得导入上游 `.pi`、嵌套 `.git`、`node_modules` 或构建产物。
|
||||
- 修改 Tool Card 提示词、目录校验、分组、缓存、active-tool/LRU 生命周期、配置或工具结果时,必须同步更新包内 README、当前动态加载文档、CHANGELOG 和测试。
|
||||
- 新增、删除或重命名组合包暴露工具时必须同步维护 `BUNDLE_GROUP_DEFINITIONS`;日常只读工具、provider 工具和高风险管理工具不得因名称相似而混入同一大组,根验证应确认标准组合包没有落入 unknown-tools 路径。
|
||||
@@ -275,9 +284,9 @@
|
||||
|
||||
## 验证
|
||||
|
||||
组合包依赖或加载入口变化时,至少验证根 `npm install` 幂等、锁文件有效,以及全部扩展可在隔离的临时 Pi agent 目录加载。权限配置变化时使用仓库内固定快照的 `pi-permission-system` schema 校验,并验证包装入口部署后的文件与仓库源配置一致。
|
||||
组合包依赖或加载入口变化时,至少验证根 `npm install` 幂等、锁文件有效,以及全部扩展可在隔离的临时 Pi agent 目录加载。权限配置变化时使用仓库内固定快照的 `pi-permission-system` schema 校验,并验证扩展运行时解析到的 `config/` 路径确实位于组合包内。
|
||||
|
||||
安装、升级、卸载、搜索或 SSH 配置脚本变化时,至少运行 `sh -n install.sh`、`sh -n update.sh`、`sh -n uninstall.sh`、`sh -n search_config.sh`、`sh -n ssh_config.sh` 和 ShellCheck,并核对脚本仍具有可执行权限、仍包含在根 `package.json` 的 `files` 中、README 描述与实际流程一致。组合包配置重建必须用隔离 agent dir 覆盖:`my-pi/` 全量替换且内容字节一致、权限 `600`/`700`、目录外文件不受影响、空或相对 agentDir 拒绝且不删除、manifest 目标越界拒绝且不删除、`my-pi/` 为符号链接时只替换链接且不触碰目标目录;`update.sh` 中 helper 无法定位时必须记录失败且不回退脚本目录。搜索配置测试只能使用虚拟 key 和隔离 HOME;SSH 配置测试必须使用隔离 HOME、虚拟凭据与 mock transport,不得连接真实服务器或把真实密码写入测试输出。涉及真实 `pi install`、`pi update`、`pi remove`、Homebrew、远程安装器、Git 克隆、真实 SSH 连接或真实用户终端配置的端到端验证属于外部写操作,未经明确要求不得执行;可以使用隔离的临时 HOME 和 mock 命令验证分支行为。
|
||||
安装、升级、卸载、搜索或 SSH 配置脚本变化时,至少运行 `sh -n install.sh`、`sh -n update.sh`、`sh -n uninstall.sh`、`sh -n configure.sh`、`sh -n scripts/lib.sh` 和 ShellCheck,并核对脚本仍具有可执行权限、仍包含在根 `package.json` 的 `files` 中、README 描述与实际流程一致。搜索配置测试只能使用虚拟 key 和隔离 HOME;SSH 配置测试必须使用隔离 HOME、虚拟凭据与 mock transport,不得连接真实服务器或把真实密码写入测试输出。涉及真实 `pi install`、`pi update`、`pi remove`、Homebrew、远程安装器、Git 克隆、真实 SSH 连接或真实用户终端配置的端到端验证属于外部写操作,未经明确要求不得执行;可以使用隔离的临时 HOME 和 mock 命令验证分支行为。
|
||||
|
||||
记忆扩展变化必须额外验证:`pi-hermes-memory` 在隔离临时 Pi agent 目录中可加载且注册 6 个工具、`pi.extensions` 中排在 `pi-compaction` 之前、Tool Search 目录把六个工具归入 `memory-recall` / `memory-management` 且全部常驻、权限策略默认 `allow`、`better-sqlite3@13` 嵌套安装与根 `12.11.1` 并存。不得在测试中连接真实数据库或修改真实 `~/.pi/agent/` 记忆数据。
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ Git checkout 在 `fetch` 后比较本地与上游提交,Homebrew 包在刷新
|
||||
日志中的“Zsh 插件未安装,升级流程跳过”不是升级失败:升级脚本不会新增用户此前未选择的
|
||||
插件。需要补装时重新运行 `./install.sh`,并在对应的逐项询问中明确选择 `Y`。
|
||||
|
||||
升级成功后,脚本会使用**更新后**组合包中的 `scripts/rebuild-bundle-config.mjs` 整体重建 `<agentDir>/my-pi/` 受管配置目录:按 `config/bundle-manifest.json` 校验来源与目标、在临时目录完成构建后一次性替换,不备份也不保留目录内的本地改动。Powerlevel10k 受管副本只在内容不同时备份并原子同步;`.zshrc` 也只更新已有的 my-pi 受管块,不触碰块外配置。因此安装流程负责补齐缺失项,升级流程只升级已安装项并整体刷新组合包拥有的配置。
|
||||
`pi update` 在有新版本时用更新后的组合包整体覆盖已安装副本;`update.sh` 在每次成功升级后还会把默认 git 来源的已安装 checkout `git reset --hard` 还原到已发布版本(本地路径来源不 reset,不做 `git clean` 以保留 `node_modules`),因此对已安装副本(含 `config/`)的本地修改不会保留。扩展运行时直接读取已安装组合包内的 `config/` 文件,脚本不再做任何配置部署。Powerlevel10k 受管副本只在内容不同时备份并原子同步;`.zshrc` 也只更新已有的 my-pi 受管块,不触碰块外配置。因此安装流程负责补齐缺失项,升级流程只升级已安装项。
|
||||
|
||||
卸载组合包:
|
||||
|
||||
@@ -116,8 +116,8 @@ prompt 前缀、对前缀缓存友好。模型需要历史上下文时主动调
|
||||
Tool Search 常驻,保证 policy 指示与模型可见工具一致。
|
||||
|
||||
记忆与 skill 数据位于 `~/.pi/agent/pi-hermes-memory/`(全局)与 `~/.pi/agent/projects-memory/<项目>/`
|
||||
(项目级),可选配置为 `~/.pi/agent/hermes-memory-config.json`。这些是用户数据,安装、升级与卸载脚本
|
||||
不会创建、迁移或删除。
|
||||
(项目级)。这些是用户数据,安装、升级与卸载脚本不会创建、迁移或删除;扩展配置来自已安装组合包的
|
||||
`config/pi-hermes-memory.json`。
|
||||
|
||||
CodeGraph 采用惰性初始化:`codegraph_explore` 在未建立索引的项目上被调用时会先向用户确认,
|
||||
确认后才运行 `codegraph init` 并自动重试本次探索;不需要手动执行任何初始化命令。
|
||||
@@ -147,13 +147,13 @@ CodeGraph 的更新或遥测设置。
|
||||
|
||||
### 系统提示词封面
|
||||
|
||||
`extensions/system-prompt-cover.ts` 把 Pi 默认系统提示词的**开头**替换为组合包权威的 `config/system-prompt-cover.md`(部署到 `~/.pi/agent/my-pi/system-prompt-cover.md`),同时**保留其余全部分区及其原始顺序**:`<tools>`、`<rules>`、`<docs>`、`<skills>`、`<project_context>`、`<cwd>` 都不会被删除或重新排序。
|
||||
`extensions/system-prompt-cover.ts` 把 Pi 默认系统提示词的**开头**替换为组合包权威的 `config/system-prompt-cover.md`(扩展直接读取已安装组合包内的该文件,不复制到 agent 目录),同时**保留其余全部分区及其原始顺序**:`<tools>`、`<rules>`、`<docs>`、`<skills>`、`<project_context>`、`<cwd>` 都不会被删除或重新排序。
|
||||
|
||||
实现上使用 Pi 的 `forceSystemPrompt` 投影路径(每轮把渲染结果作为强制定本发送)而不是 `customPrompt` 选项:后者会连带抑制 `tools`/`rules`/`docs` 三个分区,且回填只能追加到末尾,无法保持顺序。封面只在**请求**时应用,不修改 transcript 中已持久化的结构化分区。
|
||||
|
||||
封面每轮重新读取,因此修改 `~/.pi/agent/my-pi/system-prompt-cover.md` 后在**下一轮**即生效,不需要重开会话;已经结束的历史对话不受影响。文件缺失或为空白时不干预,Pi 继续使用自身默认封面。
|
||||
封面每轮重新读取,因此修改已安装组合包内的 `config/system-prompt-cover.md` 后在**下一轮**即生效,不需要重开会话;已经结束的历史对话不受影响。文件缺失或为空白时不干预,Pi 继续使用自身默认封面。
|
||||
|
||||
该文件由组合包直接维护,`install.sh` 与 `update.sh` 会在组合包安装/升级后连同整个 `my-pi/` 目录一起重建,**不保留备份并且会覆盖**本地修改;要自定义请直接编辑仓库中的 `config/system-prompt-cover.md`。
|
||||
该文件由组合包直接维护并随组合包发布;对已安装副本的本地修改不做保护,`pi update` 会直接覆盖;要长期自定义请直接编辑仓库中的 `config/system-prompt-cover.md`。
|
||||
|
||||
`system-prompt-cover.md` 必须使用这个名字:不能改名为 `SYSTEM.md`,否则 Pi 会先把它当作 `customPrompt` 读入,封面扩展拿到的提示词就已经缺少可供回填的分区了。
|
||||
|
||||
@@ -177,7 +177,7 @@ CodeGraph 的更新或遥测设置。
|
||||
|
||||
动态组默认最多同时加载 5 个、每个模型生成组最多 8 个工具、全部动态组最多 28 个工具。`tool_search.groups` 可在一次调用中原子加载最多 5 个精确组;完整请求本身超限时保持原状态。加载或调用组内工具会更新 LRU,超限时先卸载最久未使用且不在本次请求内的组。自然语言 query 对精确工具名和已提交 workflow alias 加权,弱匹配或近似同分只返回候选。首次纯增加载继续使用 Pi 原生增量传播;发生卸载和加载的替换由 Pi 自动走安全 fallback。
|
||||
|
||||
Tool Search 的用户级配置文件是 `~/.pi/agent/my-pi/tool-search/config.json`(准确位置为 Pi agent dir 下的 `my-pi/tool-search/config.json`),由 `install.sh` / `update.sh` 从仓库的 `config/tool-search.json` 部署:
|
||||
Tool Search 的配置文件是已安装组合包内的 `config/tool-search.json`(即本仓库的 `config/tool-search.json`,随 `pi install` / `pi update` 就位,扩展运行时直接读取):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -203,7 +203,7 @@ Tool Search 的用户级配置文件是 `~/.pi/agent/my-pi/tool-search/config.js
|
||||
}
|
||||
```
|
||||
|
||||
扩展只读取该文件,从不写入:文件缺失、不可读或结构无效时回退内置的 `BUNDLE_TOOL_SEARCH_DEFAULTS`;每次运行 `install.sh` / `update.sh` 都会随整个 `my-pi/` 目录重建该文件,本地手改会被覆盖,长期自定义应改仓库 `config/tool-search.json`。`alwaysEnabled` 中的精确工具名属于固定工具,不占动态组额度;`groupOverrides` 可把指定工具强制归入命名组。默认关闭状态行,避免与 minimal footer 叠加。
|
||||
扩展只读取该文件,从不写入:文件缺失、不可读或结构无效时回退内置的 `BUNDLE_TOOL_SEARCH_DEFAULTS`;对已安装副本的手改不做保护,`pi update` 会覆盖,长期自定义应改仓库 `config/tool-search.json`。`alwaysEnabled` 中的精确工具名属于固定工具,不占动态组额度;`groupOverrides` 可把指定工具强制归入命名组。默认关闭状态行,避免与 minimal footer 叠加。
|
||||
|
||||
### 精确读写归 Hashline
|
||||
|
||||
@@ -238,21 +238,21 @@ FFF 仍负责精确字面搜索;RTK 继续压缩未走 Context Mode 的 Bash
|
||||
`pi-ssh` 运行时使用纯 `ssh2`,不依赖系统 `ssh` 或 `sshpass`。首次使用前显式导入需要的 OpenSSH alias:
|
||||
|
||||
```bash
|
||||
./ssh_config.sh import packaging-server
|
||||
./configure.sh ssh import packaging-server
|
||||
```
|
||||
|
||||
脚本只在导入阶段调用 `ssh -G` 解析用户选中的 alias,随后交互选择 SSH Agent(包括 1Password)、私钥文件或密码认证、显示并确认服务器 Host Key、测试连接,再把完整配置写入 `${XDG_CONFIG_HOME:-$HOME/.config}/my-pi/pi-ssh/hosts.enc`。Agent 模式只保存 socket 路径,私钥仍留在 Agent 中;邻接的随机 `vault.key` 用于 AES-256-GCM 解密,目录与两个文件在 POSIX 上分别是 `700` / `600`。该加密只避免误看或单独泄漏密文,能读取两文件的同一用户仍可解密。
|
||||
|
||||
可用 `./ssh_config.sh list|update|remove|rotate-key` 管理已导入主机。第一版拒绝 `ProxyJump` / `ProxyCommand`,且不会自动读取远端 `AGENTS.md` 或 `CLAUDE.md`。连接不再通过 `/ssh`、`--ssh` 或 session resume 建立:用户在具体远端任务中明确指定已导入 host ID 后,Agent 把受复核、串行且可取消的 `ssh_connect` 作为独立步骤调用并等待成功,再使用其他 `ssh_*` 工具;连接动作及后续操作均进入 `ask → auto-review`,相对远端路径会在预览中显示请求值和解析后的绝对路径,会话结束自动断开。SSH transport 在会话内持久,独立命令最多使用 4 路有界并发 exec channel,但每次 `ssh_bash` 都启动独立的非交互 Shell;需要持续改变后续远端操作的工作目录时,Agent 同样必须把受复核且串行执行的 `ssh_cd` 作为独立步骤调用并等待成功,而不是依赖某次 Shell 中临时执行的 `cd`。
|
||||
可用 `./configure.sh ssh list|update|remove|rotate-key` 管理已导入主机。第一版拒绝 `ProxyJump` / `ProxyCommand`,且不会自动读取远端 `AGENTS.md` 或 `CLAUDE.md`。连接不再通过 `/ssh`、`--ssh` 或 session resume 建立:用户在具体远端任务中明确指定已导入 host ID 后,Agent 把受复核、串行且可取消的 `ssh_connect` 作为独立步骤调用并等待成功,再使用其他 `ssh_*` 工具;连接动作及后续操作均进入 `ask → auto-review`,相对远端路径会在预览中显示请求值和解析后的绝对路径,会话结束自动断开。SSH transport 在会话内持久,独立命令最多使用 4 路有界并发 exec channel,但每次 `ssh_bash` 都启动独立的非交互 Shell;需要持续改变后续远端操作的工作目录时,Agent 同样必须把受复核且串行执行的 `ssh_cd` 作为独立步骤调用并等待成功,而不是依赖某次 Shell 中临时执行的 `cd`。
|
||||
|
||||
远端文件搜索使用独立的 `ssh_find` / `ssh_grep`:在每次获批调用内按服务器现有能力选择 `fd/fdfind → git ls-files → find` 或 `rg → git grep → find+grep`,不安装远端软件,并以最大 200 条结果、单行截断和 `truncated` 标记约束返回。搜索目标作为绝对参数与当前远端执行 cwd 分离,因此 `ssh_grep` 可直接搜索单个文件而不会尝试把文件当目录进入;30 秒超时会报告解析后的 root 并提示先用 `ssh_find` 缩小范围。远端 HOME/cwd 通过可取消、有界随机标记探测获得,畸形结果不会更新连接状态。RTK 只压缩 `ssh_bash` 的非搜索输出,不改写远端命令,也不处理 `ssh_find`、`ssh_grep` 或 `ssh_read`。详细说明见 [`pi-ssh/README.md`](pi-ssh/README.md)。
|
||||
|
||||
### 在线搜索服务
|
||||
|
||||
运行 `./search_config.sh` 可用隐藏输入依次配置 Tavily、Exa 和 Keenable;也可一次无交互配置:
|
||||
运行 `./configure.sh search` 可用隐藏输入依次配置 Tavily、Exa 和 Keenable;也可一次无交互配置:
|
||||
|
||||
```bash
|
||||
./search_config.sh --tavily '...' --exa '...' --keenable '...'
|
||||
./configure.sh search --tavily '...' --exa '...' --keenable '...'
|
||||
```
|
||||
|
||||
只传部分参数时保留其他服务的已有值。凭据写入 Pi 自己的
|
||||
@@ -293,16 +293,17 @@ codegraph status
|
||||
|
||||
`pi-permission-system/` 从上游 `pi-permission-system-v26.2.1` tag 导入,初始源码快照为 `ec4fdb11343dc94f7185b113e559a4cf9f8dc035`。原 `pi-permission-auto-review` 的 reviewer、可信证据边界和 Codex Guardian 风格策略已整合进同一个扩展;其 MIT 许可和来源记录保留在 `pi-permission-system/LICENSES/` 与 `docs/auto-review/`。根组合包不再加载第二个 AutoReview 扩展。
|
||||
|
||||
`extensions/permission-system.ts` 只加载扩展本身,不做任何配置播种或迁移;两份用户可编辑 JSONC 由 `install.sh` / `update.sh` 经 `scripts/rebuild-bundle-config.mjs` 以 `0600` 部署:
|
||||
`extensions/permission-system.ts` 只加载扩展本身,不做任何配置播种或迁移;扩展运行时直接读取已安装组合包内的两份 JSONC:
|
||||
|
||||
- `~/.pi/agent/my-pi/permission-system/policy.jsonc`:唯一的全局普通 allow/ask/deny 策略,同时保存运行参数与 AutoReview 配置。项目级 `.pi/extensions/pi-permission-system/policy.jsonc` 不再读取。
|
||||
- `~/.pi/agent/my-pi/permission-system/hard-guards.jsonc`:仅全局生效的硬防御。硬 `deny` 先于 Session grant、per-agent 规则和 AutoReview,不能被后续层放宽。
|
||||
- `config/pi-permission-policy.jsonc`:唯一的全局普通 allow/ask/deny 策略,同时保存运行参数与 AutoReview 配置。项目级 `.pi/extensions/pi-permission-system/policy.jsonc` 不再读取。
|
||||
- `config/pi-permission-hard-guards.jsonc`:仅全局生效的硬防御。硬 `deny` 先于 Session grant、per-agent 规则和 AutoReview,不能被后续层放宽。
|
||||
- 同一文件内的 `protectedWrites`:Pi 凭据/全局设置/模型配置、已安装组合包与扩展目录、pi-ssh 凭据库等受保护文件的写入永远走人工确认——不经 AutoReview、yolo 或 Session grant,弹窗没有「本会话允许」选项,没有可用确认界面时直接拒绝。读取不受影响,`ssh_bash` 等远端 shell 工具不参与本机路径保护。
|
||||
|
||||
普通 pattern 不再按“最后一条命中”覆盖,而按具体度选择:精确匹配优先,其次字面字符更多、通配符更少、pattern 更长,只有完全同级时才以后声明者为准。因此具体 deny 会稳定压过宽泛的 `"*": "allow"`。
|
||||
|
||||
系统不再产生 `external_directory` 判断:项目外路径与项目内路径一样,只走用户配置的工具、`path` 和 Bash 规则。Tool Search 是唯一的工具显隐/激活控制者;权限系统只在工具被调用时做裁决。
|
||||
|
||||
`my-pi/` 目录由组合包整体拥有:每次 `install.sh` / `update.sh` 都会按 `config/bundle-manifest.json` 删除后重建,不备份也不迁移目录内的改动;要长期保留的自定义必须改仓库 `config/` 下的权威源。旧位置 `~/.pi/agent/extensions/pi-permission-system/` 的残留文件被忽略,不迁移也不清理。
|
||||
`<agentDir>/my-pi/` 只保留可随时丢弃的运行时状态(如权限日志、Tool Search 目录缓存);组合包配置一律位于仓库 `config/` 并随组合包发布,对已安装副本的本地改动由 `pi update` 覆盖,要长期保留的自定义必须改仓库 `config/` 下的权威源。旧位置 `~/.pi/agent/extensions/pi-permission-system/` 的残留文件被忽略,不迁移也不清理。
|
||||
|
||||
默认普通策略对未匹配请求返回 `ask`,并通过 `authorizerChain: ["auto-review"]` 先交给内置 reviewer。高危 Hard Guard 直接终止;AutoReview 的 `allow` / `deny` 对任一普通策略 `ask`(包括 `path`)直接结束当前审批;模型、配置、认证、超时或输出异常统一 `defer` 到人工界面。
|
||||
|
||||
@@ -354,8 +355,9 @@ JDT LS 需要 Java 21 或更高版本;可使用带 `jdtls` wrapper 的官方
|
||||
skill 也不会出现在 Pi 的 skill 索引中。
|
||||
|
||||
记忆与 skill 位于 `~/.pi/agent/pi-hermes-memory/`(全局)和 `~/.pi/agent/projects-memory/<项目>/`
|
||||
(项目级),配置位于 `~/.pi/agent/hermes-memory-config.json`。这些是用户数据:安装、升级和卸载脚本都
|
||||
不会创建、迁移或删除它们,也不会自动转换旧 Hippo `.hippo/` 数据。
|
||||
(项目级)。这些是用户数据:安装、升级和卸载脚本都
|
||||
不会创建、迁移或删除它们,也不会自动转换旧 Hippo `.hippo/` 数据;扩展配置来自已安装组合包的
|
||||
`config/pi-hermes-memory.json`。
|
||||
|
||||
### Kitty 完成与权限确认通知
|
||||
|
||||
@@ -379,7 +381,7 @@ compaction 和排队 follow-up 完成前不会提前通知;同时监听 `pi-pe
|
||||
|
||||
普通 Provider、阈值判断、Codex native compaction、`/context`、`/compaction-status` 和 minimal footer 都读取同一个 effective projection snapshot。Pi Core 的 raw threshold 只负责唤醒:只有 effective token 下界达到默认 90% 才执行 threshold compaction;manual 与 overflow 始终允许继续并在 native 失败时回退 Pi 默认 compaction。Codex native API 直接消费 effective ResponseItems,同时保存有界 portable summary 供模型或 Provider 切换。
|
||||
|
||||
唯一配置位于 Pi agent `settings.json` 的 `piCompaction` 字段,默认 soft/hard/target 比例为 72%/90%/55%,扩展只在内存中归一化且不写回配置。`/compaction-status` 显示 raw/effective/reclaimed token、置信度、segment 和后台任务。完整配置、Session version 1 格式和双上游来源见 [`pi-compaction/README.md`](pi-compaction/README.md) 与 [`pi-compaction/UPSTREAM.md`](pi-compaction/UPSTREAM.md)。
|
||||
唯一配置位于已安装组合包的 `config/pi-compaction.json`,默认 soft/hard/target 比例为 72%/90%/55%,扩展只在内存中归一化且不写回配置。`/compaction-status` 显示 raw/effective/reclaimed token、置信度、segment 和后台任务。完整配置、Session version 1 格式和双上游来源见 [`pi-compaction/README.md`](pi-compaction/README.md) 与 [`pi-compaction/UPSTREAM.md`](pi-compaction/UPSTREAM.md)。
|
||||
|
||||
原生远程压缩同时支持 `openai-codex` + `openai-codex-responses` 和 `sub-api` + `openai-responses`,使用触发时当前会话模型。SubAPI 保留现有模型配置,从 `baseUrl` 调用公共 `/responses` 并使用 `compaction_trigger` 流式协议;优先保留非空显式 `Authorization`,否则使用 API key,也支持仅凭非空 `cf-aig-authorization` 的代理认证。缺失或空白凭据在请求前拒绝。SubAPI 不要求客户端解析 ChatGPT account ID,也不调用独立 `/responses/compact`;Codex 的 JWT 要求保持不变。网关部署须支持 Sub2API 原生压缩 v2;请求失败沿用 Pi 回退,切换模型或 Provider 时使用 portable summary。
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"directory": "my-pi",
|
||||
"files": [
|
||||
{ "source": "config/pi-permission-policy.jsonc", "target": "permission-system/policy.jsonc" },
|
||||
{ "source": "config/pi-permission-hard-guards.jsonc", "target": "permission-system/hard-guards.jsonc" },
|
||||
{ "source": "config/tool-search.json", "target": "tool-search/config.json" },
|
||||
{ "source": "config/system-prompt-cover.md", "target": "system-prompt-cover.md" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"softRatio": 0.72,
|
||||
"hardRatio": 0.9,
|
||||
"targetRatio": 0.55,
|
||||
"deterministic": {
|
||||
"enabled": true,
|
||||
"maxStubTokens": 1024
|
||||
},
|
||||
"semantic": {
|
||||
"enabled": true,
|
||||
"model": "default",
|
||||
"thinking": "low",
|
||||
"concurrency": 1,
|
||||
"minInputChars": 4000,
|
||||
"maxInputChars": 24000,
|
||||
"maxRetries": 2
|
||||
},
|
||||
"native": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"lazyInitialization": false,
|
||||
"memoryMode": "policy-only",
|
||||
"memoryPolicyStyle": "full",
|
||||
"memoryCharLimit": 5000,
|
||||
"userCharLimit": 5000,
|
||||
"projectCharLimit": 5000,
|
||||
"nudgeInterval": 10,
|
||||
"reviewRecentMessages": 0,
|
||||
"reviewEnabled": true,
|
||||
"reviewTransport": "direct",
|
||||
"flushOnCompact": true,
|
||||
"flushOnShutdown": true,
|
||||
"flushMinTurns": 6,
|
||||
"flushRecentMessages": 0,
|
||||
"flushCompactTimeoutMs": 60000,
|
||||
"memoryOverflowStrategy": "auto-consolidate",
|
||||
"overflowGraceMs": 180000,
|
||||
"autoConsolidate": true,
|
||||
"correctionDetection": true,
|
||||
"failureInjectionEnabled": true,
|
||||
"failureInjectionMaxAgeDays": 7,
|
||||
"failureInjectionMaxEntries": 5,
|
||||
"consolidationChunking": false,
|
||||
"consolidationChunkChars": 4000,
|
||||
"consolidationTimeoutMs": 180000,
|
||||
"usageHitTrackingEnabled": true,
|
||||
"consolidationUsageSignals": true,
|
||||
"autoConsolidationWarnOnFailure": true,
|
||||
"nudgeToolCalls": 15,
|
||||
"standingInstructionsEnabled": true,
|
||||
"sessionSearch": { "variant": "legacy" },
|
||||
"quickCheckOnOpen": true,
|
||||
"sessionRetentionDays": 0
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
// Bundle default. The installed user-level copy is seeded once and may then
|
||||
// be edited by the user; project configuration cannot override it.
|
||||
// Bundle-owned hard guards, read directly from the installed bundle's
|
||||
// config/ directory. pi update overwrites this file; project configuration
|
||||
// cannot override it.
|
||||
"version": 1,
|
||||
"permission": {
|
||||
"bash": {
|
||||
@@ -13,5 +14,22 @@
|
||||
"mkfs*": "deny",
|
||||
"dd * of=/dev/*": "deny"
|
||||
}
|
||||
}
|
||||
},
|
||||
// Writes to the files below always reach a human prompt — never AutoReview,
|
||||
// never yolo, never an existing session grant — and are denied outright when
|
||||
// no confirmation UI is available. Reads are unaffected. `*` crosses `/`, so
|
||||
// `/**` covers a whole subtree. `{agentDir}` is the Pi agent directory,
|
||||
// `{configHome}` is $XDG_CONFIG_HOME or ~/.config; a leading `~/` also works.
|
||||
"protectedWrites": [
|
||||
{ "pattern": "{agentDir}/auth.json", "label": "Pi 凭据(模型与搜索 API key)" },
|
||||
{ "pattern": "{agentDir}/settings.json", "label": "Pi 全局设置" },
|
||||
{ "pattern": "{agentDir}/models.json", "label": "Pi 模型配置" },
|
||||
{ "pattern": "{agentDir}/trust.json", "label": "项目信任记录" },
|
||||
{ "pattern": "{agentDir}/hashline.json", "label": "Hashline 配置" },
|
||||
{ "pattern": "{agentDir}/lsp.json", "label": "LSP 配置" },
|
||||
{ "pattern": "{agentDir}/git/**", "label": "已安装的组合包(含 config/ 与权限系统源码)" },
|
||||
{ "pattern": "{agentDir}/npm/**", "label": "已安装的 npm 扩展" },
|
||||
{ "pattern": "{agentDir}/extensions/**", "label": "Pi 自动加载的扩展目录" },
|
||||
{ "pattern": "{configHome}/my-pi/**", "label": "pi-ssh 凭据库与 p10k 配置" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -38,13 +38,31 @@
|
||||
"grep": "allow",
|
||||
"find": "allow",
|
||||
"multi_grep": "allow",
|
||||
"ls": "allow",
|
||||
"codegraph_explore": "allow",
|
||||
"lsp_diagnostics": "allow",
|
||||
"lsp_definition": "allow",
|
||||
"lsp_references": "allow",
|
||||
"lsp_hover": "allow",
|
||||
"lsp_symbols": "allow",
|
||||
"ctx_execute": "allow",
|
||||
"ctx_execute_file": "allow",
|
||||
"ctx_batch_execute": "allow",
|
||||
"ctx_search": "allow",
|
||||
"ctx_index": "allow",
|
||||
"ctx_stats": "allow",
|
||||
"ctx_doctor": "allow",
|
||||
"tool_search": "allow",
|
||||
"ask_user_question": "allow",
|
||||
"web_search": "allow",
|
||||
"chrome_snapshot": "allow",
|
||||
"chrome_find": "allow",
|
||||
"chrome_inspect": "allow",
|
||||
"chrome_wait_for": "allow",
|
||||
"chrome_screenshot": "allow",
|
||||
"chrome_list_console_messages": "allow",
|
||||
"chrome_list_network_requests": "allow",
|
||||
"chrome_get_network_request": "allow",
|
||||
|
||||
// Hermes memory tools write only to the extension's own agent-directory
|
||||
// stores and never touch the workspace, so they stay allowed. memory_search
|
||||
@@ -55,12 +73,18 @@
|
||||
"memory_add": "allow",
|
||||
"memory_replace": "allow",
|
||||
"memory_remove": "allow",
|
||||
"skill_manage": "allow",
|
||||
// Skills skill_manage writes are loaded into every future session, so
|
||||
// their creation stays human-reviewed.
|
||||
"skill_manage": "ask",
|
||||
// pi-compaction's evidence recovery reads only the extension's own segment
|
||||
// index and never touches the workspace. It is pinned in Tool Search's
|
||||
// alwaysEnabled set, so it must not pay an AutoReview round trip per turn.
|
||||
"context_tree_query": "allow",
|
||||
|
||||
// Model-chosen URLs can exfiltrate data — same reason curl/wget ask.
|
||||
"web_fetch": "ask",
|
||||
"ctx_fetch_and_index": "ask",
|
||||
|
||||
"ssh_connect": "ask",
|
||||
"ssh_cd": "ask",
|
||||
"ssh_read": "ask",
|
||||
@@ -91,6 +115,20 @@
|
||||
"git branch --show-current*": "allow",
|
||||
"git remote -v": "allow",
|
||||
"git remote get-url*": "allow",
|
||||
"git blame*": "allow",
|
||||
"git describe*": "allow",
|
||||
"git rev-list*": "allow",
|
||||
"git merge-base*": "allow",
|
||||
"git ls-tree*": "allow",
|
||||
"git cat-file*": "allow",
|
||||
"git name-rev*": "allow",
|
||||
"git shortlog*": "allow",
|
||||
"git stash list*": "allow",
|
||||
"git worktree list*": "allow",
|
||||
"git tag -l*": "allow",
|
||||
"git tag --list*": "allow",
|
||||
"git config --get*": "allow",
|
||||
"git config --list*": "allow",
|
||||
"npm install *": "ask",
|
||||
"npm add *": "ask",
|
||||
"npm uninstall *": "ask",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"root": "~/.pi/worktrees",
|
||||
"branchPrefix": "wt/"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"commandRewritingEnabled": false,
|
||||
"mode": "rewrite",
|
||||
"guardWhenRtkMissing": true,
|
||||
"showRewriteNotifications": true,
|
||||
"outputCompaction": {
|
||||
"enabled": true,
|
||||
"stripAnsi": true,
|
||||
"readCompaction": {
|
||||
"enabled": false
|
||||
},
|
||||
"truncate": {
|
||||
"enabled": true,
|
||||
"maxChars": 12000
|
||||
},
|
||||
"sourceCodeFilteringEnabled": false,
|
||||
"preserveExactSkillReads": false,
|
||||
"sourceCodeFiltering": "none",
|
||||
"smartTruncate": {
|
||||
"enabled": false,
|
||||
"maxLines": 220
|
||||
},
|
||||
"aggregateTestOutput": true,
|
||||
"filterBuildOutput": true,
|
||||
"compactGitOutput": true,
|
||||
"aggregateLinterOutput": true,
|
||||
"trackSavings": true
|
||||
}
|
||||
}
|
||||
Executable
+145
@@ -0,0 +1,145 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR=$(CDPATH=; cd "$(dirname "$0")" && pwd) || exit 1
|
||||
|
||||
say() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '错误: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
用法:
|
||||
./configure.sh search [选项] 配置搜索 API key(tavily / exa / keenable)
|
||||
./configure.sh ssh [选项] 配置 SSH 主机凭据
|
||||
EOF
|
||||
}
|
||||
|
||||
search_usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./configure.sh search
|
||||
./configure.sh search --tavily KEY --exa KEY --keenable KEY
|
||||
./configure.sh search --migrate [search.env 路径]
|
||||
./configure.sh search --list
|
||||
|
||||
选项:
|
||||
--tavily KEY 配置 tavily api_key 凭据
|
||||
--exa KEY 配置 exa api_key 凭据
|
||||
--keenable KEY 配置 keenable api_key 凭据
|
||||
--migrate 从旧 search.env 导入缺失的 key
|
||||
--list 显示各 provider 配置状态(不输出 key)
|
||||
--auth FILE 覆盖 auth.json 路径
|
||||
-h, --help 显示帮助
|
||||
|
||||
不传 key 参数时进入隐藏输入的交互模式;传入任意 key 参数时不交互,
|
||||
未指定的 provider 保留已有值。凭据写入 Pi 的 auth.json
|
||||
($PI_CODING_AGENT_DIR/auth.json 或 ~/.pi/agent/auth.json,权限 600)。
|
||||
key 支持 auth.json 原生语义:字面量、$ENV_VAR / ${ENV_VAR} 插值、
|
||||
以及 !command(执行命令取 stdout,如 "!op read op://Vault/Tavily/key")。
|
||||
EOF
|
||||
}
|
||||
|
||||
configure_search() {
|
||||
SEARCH_AUTH_MJS="$SCRIPT_DIR/scripts/search-auth.mjs"
|
||||
[ -f "$SEARCH_AUTH_MJS" ] || { warn "找不到 ${SEARCH_AUTH_MJS}"; exit 1; }
|
||||
|
||||
SEARCH_AUTH_FILE=
|
||||
search_mode=interactive
|
||||
search_did_work=0
|
||||
|
||||
run_search_auth() {
|
||||
if [ -n "$SEARCH_AUTH_FILE" ]; then
|
||||
node "$SEARCH_AUTH_MJS" --auth "$SEARCH_AUTH_FILE" "$@"
|
||||
else
|
||||
node "$SEARCH_AUTH_MJS" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
search_has_key() {
|
||||
run_search_auth --has "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
search_prompt_key() {
|
||||
prompt_label=$1
|
||||
prompt_configured=$2
|
||||
if [ ! -t 0 ]; then
|
||||
warn "交互模式需要终端;请改用 --tavily/--exa/--keenable 参数。"
|
||||
return 2
|
||||
fi
|
||||
if [ "$prompt_configured" -eq 1 ]; then
|
||||
printf '%s API key(回车保留现有值): ' "$prompt_label" >&2
|
||||
else
|
||||
printf '%s API key(回车跳过): ' "$prompt_label" >&2
|
||||
fi
|
||||
stty -echo
|
||||
IFS= read -r prompt_value || prompt_status=$?
|
||||
stty echo
|
||||
printf '\n' >&2
|
||||
[ "${prompt_status:-0}" -eq 0 ] || return "$prompt_status"
|
||||
printf '%s\n' "$prompt_value"
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case $1 in
|
||||
--tavily|--exa|--keenable)
|
||||
[ "$#" -ge 2 ] || { warn "$1 缺少 KEY"; exit 2; }
|
||||
search_mode=noninteractive; search_did_work=1
|
||||
run_search_auth --set "${1#--}=$2" || exit $?
|
||||
shift 2 ;;
|
||||
--migrate)
|
||||
search_mode=noninteractive; search_did_work=1
|
||||
if [ "$#" -ge 2 ] && [ "${2#--}" = "$2" ]; then
|
||||
run_search_auth --migrate "$2" || exit $?
|
||||
shift 2
|
||||
else
|
||||
run_search_auth --migrate || exit $?
|
||||
shift
|
||||
fi
|
||||
say "如确认 auth.json 可用,可手动删除旧 search.env 文件。" ;;
|
||||
--list) search_mode=noninteractive; search_did_work=1; run_search_auth --list || exit $?; shift ;;
|
||||
--auth)
|
||||
[ "$#" -ge 2 ] || { warn "--auth 缺少 FILE"; exit 2; }
|
||||
SEARCH_AUTH_FILE=$2; shift 2 ;;
|
||||
-h|--help) search_usage; exit 0 ;;
|
||||
*) warn "未知参数:$1"; search_usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$search_mode" = interactive ]; then
|
||||
for provider in tavily exa keenable; do
|
||||
case $provider in
|
||||
tavily) label=Tavily ;;
|
||||
exa) label=Exa ;;
|
||||
keenable) label=Keenable ;;
|
||||
esac
|
||||
if search_has_key "$provider"; then configured=1; else configured=0; fi
|
||||
value=$(search_prompt_key "$label" "$configured") || exit $?
|
||||
if [ -n "$value" ]; then
|
||||
run_search_auth --set "$provider=$value" || exit $?
|
||||
search_did_work=1
|
||||
fi
|
||||
done
|
||||
[ "$search_did_work" -eq 1 ] || say "未提供任何 key,auth.json 保持不变。"
|
||||
fi
|
||||
}
|
||||
|
||||
case ${1:-} in
|
||||
search)
|
||||
shift
|
||||
configure_search "$@"
|
||||
;;
|
||||
ssh)
|
||||
shift
|
||||
exec node "$SCRIPT_DIR/pi-ssh/scripts/ssh-config-launcher.mjs" "$@"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -4,9 +4,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
/**
|
||||
* Load the bundled permission system directly.
|
||||
*
|
||||
* Its bundle-owned config (policy.jsonc, hard-guards.jsonc) is deployed by
|
||||
* install.sh / update.sh into `<agentDir>/my-pi/permission-system/`; the
|
||||
* extension itself never seeds or migrates configuration.
|
||||
* Its bundle-owned config (pi-permission-policy.jsonc, pi-permission-hard-guards.jsonc)
|
||||
* is read directly from the installed bundle's `config/` directory; the
|
||||
* extension itself never seeds, deploys, or migrates configuration.
|
||||
*/
|
||||
export default function bundledPermissionSystemExtension(pi: ExtensionAPI): void {
|
||||
permissionSystemExtension(pi);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/** File name of the bundle-managed system prompt cover inside `<agentDir>/my-pi/`. */
|
||||
/** File name of the bundle-managed system prompt cover inside the bundle's `config/` directory. */
|
||||
export const COVER_FILE_NAME = "system-prompt-cover.md";
|
||||
|
||||
/**
|
||||
@@ -14,9 +14,12 @@ export const COVER_FILE_NAME = "system-prompt-cover.md";
|
||||
*/
|
||||
const FIRST_SECTION_BOUNDARY = /\n<[a-z][a-z0-9_-]*>\n/;
|
||||
|
||||
/** Resolve the cover path inside a given agent directory's bundle-owned `my-pi/` tree. */
|
||||
export function resolveCoverPath(agentDir: string): string {
|
||||
return join(agentDir, "my-pi", COVER_FILE_NAME);
|
||||
/**
|
||||
* Resolve the cover path inside the installed bundle's `config/` directory —
|
||||
* the single source of truth, read directly at runtime with no deployment step.
|
||||
*/
|
||||
export function resolveCoverPath(): string {
|
||||
return fileURLToPath(new URL(`../config/${COVER_FILE_NAME}`, import.meta.url));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
import { readCoverFile, resolveCoverPath, spliceCover } from "./system-prompt-cover-config.ts";
|
||||
|
||||
@@ -19,7 +19,7 @@ export { COVER_FILE_NAME, readCoverFile, resolveCoverPath, spliceCover } from ".
|
||||
*/
|
||||
export default function systemPromptCoverExtension(pi: ExtensionAPI): void {
|
||||
pi.on("before_agent_start", (event) => {
|
||||
const cover = readCoverFile(resolveCoverPath(getAgentDir()));
|
||||
const cover = readCoverFile(resolveCoverPath());
|
||||
if (cover === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
+22
-97
@@ -3,13 +3,13 @@
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR=$(CDPATH=; cd "$(dirname "$0")" && pwd) || exit 1
|
||||
MANAGED_CONFIG_HELPERS=$SCRIPT_DIR/scripts/managed-config.sh
|
||||
if [ ! -f "$MANAGED_CONFIG_HELPERS" ]; then
|
||||
printf '错误: 找不到受管配置辅助脚本:%s\n' "$MANAGED_CONFIG_HELPERS" >&2
|
||||
MY_PI_LIB=$SCRIPT_DIR/scripts/lib.sh
|
||||
if [ ! -f "$MY_PI_LIB" ]; then
|
||||
printf '错误: 找不到共享辅助脚本:%s\n' "$MY_PI_LIB" >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=scripts/managed-config.sh
|
||||
. "$MANAGED_CONFIG_HELPERS"
|
||||
# shellcheck source=scripts/lib.sh
|
||||
. "$MY_PI_LIB"
|
||||
|
||||
PACKAGE_SOURCE=${PI_PACKAGE_SOURCE:-git:git@bitbucket.org:siakitem/my-pi.git}
|
||||
POWERLEVEL10K_PRESET_FILE=$SCRIPT_DIR/config/p10k.zsh
|
||||
@@ -21,14 +21,6 @@ ZSH_AUTOSUGGESTIONS_REPOSITORY=https://github.com/zsh-users/zsh-autosuggestions.
|
||||
ZSH_SYNTAX_HIGHLIGHTING_REPOSITORY=https://github.com/zsh-users/zsh-syntax-highlighting.git
|
||||
install_failed=0
|
||||
|
||||
say() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '警告: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
ask_yes_no() {
|
||||
prompt=$1
|
||||
printf '%s [y/N] ' "$prompt"
|
||||
@@ -64,42 +56,6 @@ record_install_failure() {
|
||||
install_failed=1
|
||||
}
|
||||
|
||||
resolve_agent_dir() {
|
||||
if [ -n "${PI_CODING_AGENT_DIR:-}" ]; then
|
||||
case $PI_CODING_AGENT_DIR in
|
||||
~/*) printf '%s\n' "$HOME/${PI_CODING_AGENT_DIR#~/}" ;;
|
||||
*) printf '%s\n' "$PI_CODING_AGENT_DIR" ;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$HOME/.pi/agent"
|
||||
}
|
||||
|
||||
# 整体删除并重建组合包拥有的 <agentDir>/my-pi/ 配置目录;脚本目录内的组合包副本即为来源。
|
||||
rebuild_bundle_config() {
|
||||
rebuild_helper=$SCRIPT_DIR/scripts/rebuild-bundle-config.mjs
|
||||
if [ ! -f "$rebuild_helper" ]; then
|
||||
warn "找不到组合包配置重建辅助脚本,已跳过配置重建。"
|
||||
return 1
|
||||
fi
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
warn "找不到 node,无法重建组合包配置目录。"
|
||||
return 1
|
||||
fi
|
||||
rebuild_agent_dir=$(resolve_agent_dir) || return 1
|
||||
rebuild_status=$(node "$rebuild_helper" "$rebuild_agent_dir" 2>&1) || {
|
||||
warn "组合包配置目录重建失败:$rebuild_status"
|
||||
return 1
|
||||
}
|
||||
case $rebuild_status in
|
||||
rebuilt:*) say "✓ 已重建组合包配置目录:${rebuild_status#rebuilt:}" ;;
|
||||
*)
|
||||
warn "组合包配置重建返回未知状态:$rebuild_status"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
java_major_version() {
|
||||
java_version=$(java -version 2>&1 | awk -F '"' 'NR == 1 { print $2 }')
|
||||
case $java_version in
|
||||
@@ -117,7 +73,13 @@ install_codegraph() {
|
||||
warn "安装 CodeGraph 需要 curl。"
|
||||
return 1
|
||||
fi
|
||||
curl -fsSL "$CODEGRAPH_INSTALL_URL" | sh || return 1
|
||||
codegraph_installer=$(mktemp "${TMPDIR:-/tmp}/my-pi-codegraph.XXXXXX") || return 1
|
||||
if ! curl -fsSL "$CODEGRAPH_INSTALL_URL" -o "$codegraph_installer" || \
|
||||
! sh "$codegraph_installer"; then
|
||||
rm -f "$codegraph_installer"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$codegraph_installer"
|
||||
|
||||
# 官方安装器默认使用 ~/.local/bin;让本次检查立即看到新命令。
|
||||
if [ -d "$HOME/.local/bin" ]; then
|
||||
@@ -152,21 +114,6 @@ install_jdtls() {
|
||||
command -v jdtls >/dev/null 2>&1
|
||||
}
|
||||
|
||||
detect_kitty() {
|
||||
kitty_bin=
|
||||
if command -v kitty >/dev/null 2>&1; then
|
||||
kitty_bin=$(command -v kitty)
|
||||
return 0
|
||||
fi
|
||||
for candidate in /Applications/kitty.app/Contents/MacOS/kitty "$HOME/.local/kitty.app/bin/kitty"; do
|
||||
if [ -x "$candidate" ]; then
|
||||
kitty_bin=$candidate
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
install_kitty() {
|
||||
case $(uname -s) in
|
||||
Darwin)
|
||||
@@ -178,16 +125,18 @@ install_kitty() {
|
||||
warn "安装 Kitty 需要 curl。"
|
||||
return 1
|
||||
fi
|
||||
curl -L "$KITTY_INSTALL_URL" | sh /dev/stdin || return 1
|
||||
installer_file=$(mktemp "${TMPDIR:-/tmp}/my-pi-kitty.XXXXXX") || return 1
|
||||
if ! curl -fsSL "$KITTY_INSTALL_URL" -o "$installer_file" || \
|
||||
! sh "$installer_file"; then
|
||||
rm -f "$installer_file"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$installer_file"
|
||||
;;
|
||||
esac
|
||||
detect_kitty
|
||||
}
|
||||
|
||||
resolve_kitty_config_dir() {
|
||||
kitty_config_dir=${KITTY_CONFIG_DIRECTORY:-${XDG_CONFIG_HOME:-$HOME/.config}/kitty}
|
||||
}
|
||||
|
||||
kitty_solarized_dark_configured() {
|
||||
resolve_kitty_config_dir
|
||||
[ -f "$kitty_config_dir/current-theme.conf" ] || return 1
|
||||
@@ -201,14 +150,6 @@ configure_kitty_solarized_dark() {
|
||||
"$kitty_bin" +kitten themes --reload-in=none "Solarized Dark"
|
||||
}
|
||||
|
||||
resolve_zsh_paths() {
|
||||
oh_my_zsh_dir=${ZSH:-$HOME/.oh-my-zsh}
|
||||
zsh_custom_dir=${ZSH_CUSTOM:-$oh_my_zsh_dir/custom}
|
||||
zshrc_file=${ZDOTDIR:-$HOME}/.zshrc
|
||||
my_pi_config_dir=${XDG_CONFIG_HOME:-$HOME/.config}/my-pi
|
||||
p10k_managed_config_file=$my_pi_config_dir/p10k.zsh
|
||||
}
|
||||
|
||||
oh_my_zsh_installed() {
|
||||
resolve_zsh_paths
|
||||
[ -f "$oh_my_zsh_dir/oh-my-zsh.sh" ]
|
||||
@@ -238,20 +179,6 @@ prepare_zshrc() {
|
||||
[ -f "$zshrc_file" ] || : > "$zshrc_file" || return 1
|
||||
}
|
||||
|
||||
replace_zsh_managed_block() {
|
||||
managed_id=$1
|
||||
managed_placement=$2
|
||||
managed_anchor=$3
|
||||
shift 3
|
||||
managed_content=$(mktemp "${TMPDIR:-/tmp}/my-pi-zsh-block.XXXXXX") || return 1
|
||||
printf '%s\n' "$@" > "$managed_content"
|
||||
my_pi_replace_managed_block "$zshrc_file" "$managed_id" "$managed_content" \
|
||||
"$managed_placement" "$managed_anchor"
|
||||
managed_status=$?
|
||||
rm -f "$managed_content"
|
||||
return "$managed_status"
|
||||
}
|
||||
|
||||
zshrc_has_oh_my_zsh_loader() {
|
||||
[ -f "$zshrc_file" ] || return 1
|
||||
grep -E '^[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh' "$zshrc_file" >/dev/null 2>&1
|
||||
@@ -270,7 +197,7 @@ ensure_oh_my_zsh_loader() {
|
||||
set_powerlevel10k_theme() {
|
||||
prepare_zshrc || return 1
|
||||
replace_zsh_managed_block "powerlevel10k-theme" "before" \
|
||||
'^(# >>> my-pi:oh-my-zsh-loader >>>|[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh)' \
|
||||
"$MY_PI_ZSH_LOADER_ANCHOR" \
|
||||
'ZSH_THEME="powerlevel10k/powerlevel10k"'
|
||||
}
|
||||
|
||||
@@ -327,7 +254,7 @@ add_zsh_plugin() {
|
||||
prepare_zshrc || return 1
|
||||
ensure_oh_my_zsh_loader || return 1
|
||||
replace_zsh_managed_block "zsh-plugin-$plugin" "before" \
|
||||
'^(# >>> my-pi:oh-my-zsh-loader >>>|[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh)' \
|
||||
"$MY_PI_ZSH_LOADER_ANCHOR" \
|
||||
"plugins+=($plugin)"
|
||||
}
|
||||
|
||||
@@ -369,9 +296,7 @@ if ! pi install "$PACKAGE_SOURCE"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
say "Pi 组合包安装完成,开始部署受管配置。"
|
||||
|
||||
rebuild_bundle_config || record_install_failure "组合包配置重建"
|
||||
say "Pi 组合包安装完成。"
|
||||
|
||||
say "开始检查终端环境和机器级 CLI 依赖。"
|
||||
|
||||
|
||||
+1
-3
@@ -31,7 +31,6 @@
|
||||
"pi-permission-system/LICENSES",
|
||||
"pi-permission-system/docs/auto-review",
|
||||
"pi-permission-system/src",
|
||||
"pi-permission-system/config",
|
||||
"pi-permission-system/schemas",
|
||||
"pi-permission-system/package.json",
|
||||
"pi-permission-system/README.md",
|
||||
@@ -42,8 +41,7 @@
|
||||
"install.sh",
|
||||
"update.sh",
|
||||
"uninstall.sh",
|
||||
"search_config.sh",
|
||||
"ssh_config.sh",
|
||||
"configure.sh",
|
||||
"README.md",
|
||||
"AGENTS.md"
|
||||
],
|
||||
|
||||
@@ -17,18 +17,17 @@ Context Mode remains responsible for preventing large external data from enterin
|
||||
|
||||
## Configuration
|
||||
|
||||
Set `piCompaction` in the Pi agent `settings.json`:
|
||||
Configuration lives in the installed bundle's `config/pi-compaction.json` (the bundle repository's `config/` directory is the single source of truth; `pi update` refreshes the installed copy). The extension never reads `settings.json` and never writes config at runtime:
|
||||
|
||||
```json
|
||||
{
|
||||
"piCompaction": {
|
||||
"enabled": true,
|
||||
"softRatio": 0.72,
|
||||
"hardRatio": 0.9,
|
||||
"targetRatio": 0.55,
|
||||
"deterministic": {
|
||||
"enabled": true,
|
||||
"maxStubTokens": 256
|
||||
"maxStubTokens": 1024
|
||||
},
|
||||
"semantic": {
|
||||
"enabled": true,
|
||||
@@ -43,14 +42,13 @@ Set `piCompaction` in the Pi agent `settings.json`:
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The extension never writes settings. Invalid or missing fields are normalized in memory.
|
||||
Invalid or missing fields are normalized in memory.
|
||||
|
||||
## Native providers
|
||||
|
||||
Native compaction is enabled by `piCompaction.native.enabled` and uses the model selected in the session when compaction starts. It does not select a separate summarization model.
|
||||
Native compaction is enabled by the `native.enabled` config key and uses the model selected in the session when compaction starts. It does not select a separate summarization model.
|
||||
|
||||
| Provider | API | Native transport |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { PiCompactionConfig } from "./types.js";
|
||||
|
||||
export const SETTINGS_KEY = "piCompaction" as const;
|
||||
// Bundle-level `config/` is the single source of truth: the extension reads
|
||||
// `config/pi-compaction.json` from inside the installed bundle (resolved
|
||||
// relative to this source file). It never reads `settings.json` and never
|
||||
// writes config at runtime.
|
||||
export const CONFIG_PATH = fileURLToPath(
|
||||
new URL("../../config/pi-compaction.json", import.meta.url),
|
||||
);
|
||||
|
||||
export const DEFAULT_CONFIG: PiCompactionConfig = {
|
||||
enabled: true,
|
||||
@@ -78,10 +83,12 @@ export function normalizeConfig(value: unknown): PiCompactionConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadConfig(): Promise<PiCompactionConfig> {
|
||||
export async function loadConfig(
|
||||
configPath: string = CONFIG_PATH,
|
||||
): Promise<PiCompactionConfig> {
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(join(getAgentDir(), "settings.json"), "utf8"));
|
||||
return normalizeConfig(object(parsed)[SETTINGS_KEY]);
|
||||
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
||||
return normalizeConfig(parsed);
|
||||
} catch {
|
||||
return structuredClone(DEFAULT_CONFIG);
|
||||
}
|
||||
|
||||
@@ -511,7 +511,7 @@ Move behavior:
|
||||
|
||||
## Configuration
|
||||
|
||||
Create `~/.pi/agent/hermes-memory-config.json`:
|
||||
Configuration is read from the installed bundle's `config/pi-hermes-memory.json` — the bundle repository's `config/` directory is the single source of truth, refreshed by `pi update`. The shipped file only contains machine-independent values; `memoryDir`, `projectsMemoryDir`, and `childExtensionPaths` are machine-dependent and omitted so the code computes them at runtime. A leftover `~/.pi/agent/hermes-memory-config.json` is ignored. All supported keys:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -708,7 +708,6 @@ native install scripts are disabled because the fixture writes no memories.
|
||||
│ │ └── SKILL.md
|
||||
│ └── another-project/
|
||||
│ └── MEMORY.md
|
||||
├── hermes-memory-config.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
|
||||
- 全局记忆与 skill:`~/.pi/agent/pi-hermes-memory/`
|
||||
- 项目记忆与 skill:`~/.pi/agent/projects-memory/<项目>/`
|
||||
- 配置:`~/.pi/agent/hermes-memory-config.json`(可选,缺省时使用内置默认值)
|
||||
|
||||
这些路径是用户数据,不属于组合包,也不应提交或自动清理。
|
||||
以上路径是用户数据,不属于组合包,也不应提交或自动清理。组合包配置位于已安装组合包的 `config/pi-hermes-memory.json`,随 `pi update` 刷新;旧 `~/.pi/agent/hermes-memory-config.json` 被忽略。
|
||||
|
||||
同步上游时,请从明确 tag/commit 移植改动,并与本地改动对照审查,不得整体覆盖本目录。
|
||||
|
||||
@@ -24,9 +24,10 @@ const emit = async (event, data = {}) => {
|
||||
|
||||
try {
|
||||
process.env.PI_CODING_AGENT_DIR = root;
|
||||
process.env.PI_HERMES_MEMORY_CONFIG_PATH = join(root, "hermes-memory-config.json");
|
||||
await mkdir(globalDir);
|
||||
await mkdir(ctx.cwd);
|
||||
await writeFile(join(root, "hermes-memory-config.json"), JSON.stringify({
|
||||
await writeFile(process.env.PI_HERMES_MEMORY_CONFIG_PATH, JSON.stringify({
|
||||
lazyInitialization: lazy, memoryMode: "policy-only",
|
||||
reviewEnabled: false, correctionDetection: false,
|
||||
flushOnShutdown: false, flushOnCompact: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { MemoryConfig, MemoryOverflowStrategy, ReviewTransport, SessionSearchVariant, ThinkingLevel } from "./types.js";
|
||||
import {
|
||||
DEFAULT_MEMORY_CHAR_LIMIT,
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
DEFAULT_FAILURE_INJECTION_MAX_ENTRIES,
|
||||
DEFAULT_SESSION_RETENTION_DAYS,
|
||||
} from "./constants.js";
|
||||
import { AGENT_ROOT, normalizeConfiguredMemoryDir, normalizeProjectsMemoryDir } from "./paths.js";
|
||||
import { normalizeConfiguredMemoryDir, normalizeProjectsMemoryDir } from "./paths.js";
|
||||
|
||||
const MEMORY_OVERFLOW_STRATEGIES: readonly MemoryOverflowStrategy[] = ["auto-consolidate", "reject", "fifo-evict"];
|
||||
const SESSION_SEARCH_VARIANTS: readonly SessionSearchVariant[] = ["legacy", "anchors"];
|
||||
@@ -81,12 +81,24 @@ const DEFAULT_CONFIG: MemoryConfig = {
|
||||
sessionRetentionDays: DEFAULT_SESSION_RETENTION_DAYS,
|
||||
};
|
||||
|
||||
export const DEFAULT_CONFIG_PATH = path.join(
|
||||
AGENT_ROOT,
|
||||
"hermes-memory-config.json",
|
||||
// Bundle-level `config/` is the single source of truth: the extension reads
|
||||
// `config/pi-hermes-memory.json` from inside the installed bundle (resolved
|
||||
// relative to this source file). Memory, sessions, and credentials stay under
|
||||
// the agent dir — they are user data, not bundle config.
|
||||
export const DEFAULT_CONFIG_PATH = fileURLToPath(
|
||||
new URL("../../config/pi-hermes-memory.json", import.meta.url),
|
||||
);
|
||||
|
||||
export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
||||
/**
|
||||
* Resolve the effective config path. `PI_HERMES_MEMORY_CONFIG_PATH` relocates
|
||||
* the file (tests point it at a temp file; it is never a persisted config
|
||||
* value).
|
||||
*/
|
||||
export function resolveConfigPath(): string {
|
||||
return process.env.PI_HERMES_MEMORY_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
|
||||
}
|
||||
|
||||
export function loadConfig(configPath = resolveConfigPath()): MemoryConfig {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const raw = fs.readFileSync(configPath, "utf-8");
|
||||
|
||||
@@ -8,7 +8,9 @@ import { after, beforeEach, afterEach, describe, it } from "node:test";
|
||||
// Bind the entry point to a disposable agent root, never the user's memory.
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "pi-lazy-startup-"));
|
||||
const previousRoot = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousConfigPath = process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
||||
process.env.PI_CODING_AGENT_DIR = root;
|
||||
process.env.PI_HERMES_MEMORY_CONFIG_PATH = path.join(root, "hermes-memory-config.json");
|
||||
const { default: registerExtension } = await import("../src/index.js");
|
||||
const { MemoryStore } = await import("../src/store/memory-store.js");
|
||||
const { DatabaseManager } = await import("../src/store/db.js");
|
||||
@@ -85,6 +87,8 @@ afterEach(async () => {
|
||||
after(async () => {
|
||||
if (previousRoot === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previousRoot;
|
||||
if (previousConfigPath === undefined) delete process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
||||
else process.env.PI_HERMES_MEMORY_CONFIG_PATH = previousConfigPath;
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("session project memory rebinding", () => {
|
||||
const launchDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-project-rebind-launch-"));
|
||||
const targetDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-project-rebind-target-"));
|
||||
const previousAgentRoot = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousConfigPath = process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
||||
const previousCwd = process.cwd();
|
||||
try {
|
||||
await fs.writeFile(
|
||||
@@ -46,6 +47,7 @@ describe("session project memory rebinding", () => {
|
||||
);
|
||||
|
||||
process.env.PI_CODING_AGENT_DIR = agentRoot;
|
||||
process.env.PI_HERMES_MEMORY_CONFIG_PATH = path.join(agentRoot, "hermes-memory-config.json");
|
||||
process.chdir(launchDir);
|
||||
// Import after setting PI_CODING_AGENT_DIR so AGENT_ROOT is test-local.
|
||||
const { default: registerExtension } = await import("../src/index.js");
|
||||
@@ -101,6 +103,8 @@ describe("session project memory rebinding", () => {
|
||||
process.chdir(previousCwd);
|
||||
if (previousAgentRoot === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previousAgentRoot;
|
||||
if (previousConfigPath === undefined) delete process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
||||
else process.env.PI_HERMES_MEMORY_CONFIG_PATH = previousConfigPath;
|
||||
await fs.rm(agentRoot, { recursive: true, force: true });
|
||||
await fs.rm(launchDir, { recursive: true, force: true });
|
||||
await fs.rm(targetDir, { recursive: true, force: true });
|
||||
|
||||
@@ -22,7 +22,9 @@ import { after, beforeEach, describe, it } from "node:test";
|
||||
// Bind the entry point to a disposable agent root, never the user's memory.
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "pi-prompt-injection-"));
|
||||
const previousRoot = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousConfigPath = process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
||||
process.env.PI_CODING_AGENT_DIR = root;
|
||||
process.env.PI_HERMES_MEMORY_CONFIG_PATH = path.join(root, "hermes-memory-config.json");
|
||||
const { default: registerExtension } = await import("../src/index.js");
|
||||
const globalDir = path.join(root, "pi-hermes-memory");
|
||||
const cwd = path.join(root, "workspace");
|
||||
@@ -111,6 +113,8 @@ beforeEach(async () => {
|
||||
after(async () => {
|
||||
if (previousRoot === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previousRoot;
|
||||
if (previousConfigPath === undefined) delete process.env.PI_HERMES_MEMORY_CONFIG_PATH;
|
||||
else process.env.PI_HERMES_MEMORY_CONFIG_PATH = previousConfigPath;
|
||||
try {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
} catch {
|
||||
|
||||
@@ -19,6 +19,7 @@ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) codin
|
||||
- **Uses one global ordinary policy file**; project `policy.jsonc` files are not loaded
|
||||
- **Chooses the most specific matching pattern**, so a specific deny wins over a broad `"*": "allow"` regardless of declaration order
|
||||
- **Applies a separate hard-guard policy first** so session grants, per-agent rules, and model review cannot override high-risk denies
|
||||
- **Keeps protected writes human-only**: files listed in `protectedWrites` (Pi credentials, settings, the installed bundle itself, …) always reach the local human prompt — never AutoReview, never yolo, never a session grant — and are denied when no confirmation UI exists. Reads are unaffected.
|
||||
- **Controls Bash, paths, MCP, skills, and tool surfaces** while leaving active-tool ownership entirely to Tool Search
|
||||
- **Reviews ordinary `ask` requests automatically** with integrated AutoReview; its `allow` verdict is final for ordinary tools and `path`, while `defer` reaches the next authorizer or human terminal
|
||||
- **Follows the session's current model**: AutoReview asks the model you are already chatting with to judge each ask, so the review always reflects the model the operator actually selected; set `autoReview.provider` and `autoReview.model` together to pin a different reviewer
|
||||
@@ -34,7 +35,7 @@ pi install npm:@gotgenes/pi-permission-system
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Create ordinary policy at `~/.pi/agent/my-pi/permission-system/policy.jsonc`:
|
||||
1. Ordinary policy lives in the installed bundle at `config/pi-permission-policy.jsonc` (the bundle repository's `config/` directory — edit it there for durable changes):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -51,7 +52,7 @@ pi install npm:@gotgenes/pi-permission-system
|
||||
}
|
||||
```
|
||||
|
||||
2. Create independent hard guards at `~/.pi/agent/my-pi/permission-system/hard-guards.jsonc`:
|
||||
2. Independent hard guards live beside it at `config/pi-permission-hard-guards.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -62,7 +63,10 @@ pi install npm:@gotgenes/pi-permission-system
|
||||
"*.env.*": "deny",
|
||||
"*.env.example": "allow",
|
||||
"~/.ssh/**": "deny"
|
||||
}
|
||||
},
|
||||
"protectedWrites": [
|
||||
{ "pattern": "{agentDir}/auth.json", "label": "Pi credentials" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -86,12 +90,12 @@ The integrated extension uses two independent global JSONC files:
|
||||
|
||||
| Purpose | Scope | Path |
|
||||
| ------- | ----- | ---- |
|
||||
| Ordinary policy, runtime knobs, AutoReview | Global only | `~/.pi/agent/my-pi/permission-system/policy.jsonc` |
|
||||
| Non-overridable hard guards | Global only | `~/.pi/agent/my-pi/permission-system/hard-guards.jsonc` |
|
||||
| Ordinary policy, runtime knobs, AutoReview | Global only | `config/pi-permission-policy.jsonc` inside the installed bundle |
|
||||
| Non-overridable hard guards | Global only | `config/pi-permission-hard-guards.jsonc` inside the installed bundle |
|
||||
|
||||
Project-level `policy.jsonc` files are ignored. Project agent Markdown remains a separate per-agent restriction mechanism.
|
||||
|
||||
The bundle deploys both files with mode `0600` via `scripts/rebuild-bundle-config.mjs`: every `install.sh` / `update.sh` run rebuilds the whole `<agentDir>/my-pi/` tree, overwriting local edits without a backup — keep durable changes in the repository's `config/` sources. Files left at the former `extensions/pi-permission-system/` location are ignored.
|
||||
Both files are read directly from the bundle's `config/` directory — the single source of truth. Nothing is deployed to, seeded into, or migrated from the agent directory; `pi update` refreshes them with the bundle. Files left at the former `~/.pi/agent/my-pi/permission-system/` location are ignored.
|
||||
|
||||
Within a pattern map, the most specific match wins: exact patterns, then more literal characters, fewer wildcards, and longer patterns. Declaration order is only a tie-breaker.
|
||||
|
||||
@@ -99,7 +103,7 @@ There is no `external_directory` gate. Outside-CWD paths use the same configured
|
||||
|
||||
Tool Search alone owns Pi's active tool list. Permission System only decides whether an invoked tool call is allowed, denied, or requires confirmation.
|
||||
|
||||
See [docs/configuration.md](docs/configuration.md) for the complete reference and [docs/hard-guards.md](docs/hard-guards.md) for the independent hard-guard format.
|
||||
See [docs/configuration.md](docs/configuration.md) for the complete reference, including the [protected writes](docs/configuration.md#protected-writes) format in the hard-guard file.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
// User-configurable, global-only hard boundary. Project policy, session
|
||||
// grants and AutoReview cannot override a matching deny.
|
||||
"version": 1,
|
||||
"permission": {
|
||||
"bash": {
|
||||
"rm -rf /": "deny",
|
||||
"rm -rf /*": "deny",
|
||||
"rm -rf ~": "deny",
|
||||
"rm -rf ~/*": "deny",
|
||||
"sudo rm -rf /": "deny",
|
||||
"sudo rm -rf /*": "deny",
|
||||
"mkfs*": "deny",
|
||||
"dd * of=/dev/*": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,6 +517,8 @@ The public authorizer registry remains available for other named authorities. Co
|
||||
|
||||
Authorizers are named capabilities registered on `permissions:ready`; the integrated `auto-review` authorizer uses the same chain contract as third-party links. Once configured, a link's `allow` or `deny` is final for any ordinary-policy `ask`, including `path`; `defer` advances to the next link or terminal.
|
||||
|
||||
One configured path bypasses the chain entirely: `protectedWrites` in the hard-guard file marks write targets as human-only. The protected-write gate produces its `ask` last in the tool-call pipeline (a hard deny still blocks first); the descriptor's `humanOnly` marker skips the runner's session-approval and yolo fast paths, and `AuthorizerSelection` composes it with zero links so no link — including `auto-review` — is consulted. A `LocalUserAuthorizer` terminal prompts without a "for this session" option; any other terminal (`DenyingAuthorizer`, a `ParentAuthorizer` relay) denies without prompting. Reads never reach the gate.
|
||||
|
||||
This is the principled successor to the per-command argument-position work deferred from [#509].
|
||||
The bash path projection surfaces a bare token that names a real file ([#645]) and deliberately accepts a fail-safe false positive (`grep id_rsa secrets.txt` prompts when an `id_rsa` file happens to exist); that false positive lives on the *ask-producing* side of `evaluate()`, and the model link dismisses it on the *ask-consuming* side without hard-coding per-command file-argument tables.
|
||||
This split is the layering principle of [ADR 0009](../decisions/0009-bash-path-projection-completeness-contract.md): the deterministic layer biases toward surfacing because over-suppression is unrecoverable, and the judge absorbs the surplus.
|
||||
@@ -578,7 +580,7 @@ src/
|
||||
├── index.ts Composition root and Pi event registration
|
||||
├── config-loader.ts Global ordinary policy loading
|
||||
├── policy-loader.ts Global policy + active-agent frontmatter gateway
|
||||
├── hard-guard-policy-loader.ts Independent global-only hard-guard loading
|
||||
├── hard-guard-loader.ts Independent global-only hard-guard + protectedWrites loading
|
||||
├── rule.ts Pattern matching and specificity ranking
|
||||
├── permission-manager.ts Unified intent evaluation
|
||||
├── access-intent/ Resolved intent types and resolver
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## Config File Locations
|
||||
|
||||
Pi Permission System reads two user-level JSONC files:
|
||||
Pi Permission System reads two JSONC files from the installed bundle's `config/` directory (the single source of truth; `pi update` refreshes them with the bundle):
|
||||
|
||||
| Purpose | Scope | Path |
|
||||
| ------- | ----- | ---- |
|
||||
| Ordinary policy + runtime | Global only | `~/.pi/agent/my-pi/permission-system/policy.jsonc` |
|
||||
| Hard guards | Global only | `~/.pi/agent/my-pi/permission-system/hard-guards.jsonc` |
|
||||
| Ordinary policy + runtime | Global only | `config/pi-permission-policy.jsonc` |
|
||||
| Hard guards | Global only | `config/pi-permission-hard-guards.jsonc` |
|
||||
|
||||
`PI_CODING_AGENT_DIR` replaces `~/.pi/agent` when set. Project-level `policy.jsonc` files are not loaded. Project agent Markdown may still provide per-agent restrictions; it is not a second ordinary policy file.
|
||||
Project-level `policy.jsonc` files are not loaded. Project agent Markdown may still provide per-agent restrictions; it is not a second ordinary policy file.
|
||||
|
||||
Hard guards are evaluated first. Their matching `deny` decisions cannot be overridden by ordinary policy, session approvals, per-agent rules, or AutoReview.
|
||||
Hard guards are evaluated first. Their matching `deny` decisions cannot be overridden by ordinary policy, session approvals, per-agent rules, or AutoReview. The same file's `protectedWrites` list marks files whose *writes* always reach a human prompt — see [Protected writes](#protected-writes).
|
||||
|
||||
Tool Search is the only extension that controls Pi's active tool list. Permission System never hides, activates, or replaces tools; it only evaluates a tool when Pi calls it.
|
||||
|
||||
@@ -37,6 +37,27 @@ This makes a broad `"*": "allow"` a fallback rather than an ordering trick. For
|
||||
|
||||
There is no separate `external_directory` decision. A path outside the current working directory is evaluated by the same configured tool, `path`, and Bash rules as any other path.
|
||||
|
||||
## Protected writes
|
||||
|
||||
`protectedWrites` in the hard-guard file lists files and subtrees whose **writes** must always be confirmed by a human:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"version": 1,
|
||||
"permission": { "bash": { "rm -rf /": "deny" } },
|
||||
"protectedWrites": [
|
||||
{ "pattern": "{agentDir}/auth.json", "label": "Pi credentials" },
|
||||
{ "pattern": "{configHome}/my-pi/**", "label": "pi-ssh credential store" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each entry is `{ "pattern": string, "label": string }`. `pattern` uses the same wildcard syntax as the `path` surface (`*` crosses `/`, so a trailing `/**` covers the whole subtree) and is matched against both the lexical and canonical (symlink-resolved) forms of a write target. `{agentDir}` expands to the Pi agent directory (`PI_CODING_AGENT_DIR`), `{configHome}` to `$XDG_CONFIG_HOME` or `~/.config`, and a leading `~/` to the home directory. `label` is the name shown in the prompt and review log.
|
||||
|
||||
Counts as a write: a `write`/`edit` tool call's `input.path`, and a local `bash` command's redirect-out destinations and non-read-only program arguments — including a `bash -c`/`eval`-style opaque payload that textually contains a protected path, an unparseable command, and a relative write after `cd` into a protected directory. Reads of protected paths are unaffected: `cat` and friends are not writes, and neither is a redirect-in (`<`). Aliased shell tools (`shellTools`, e.g. `ssh_bash`) address remote filesystems and are never matched.
|
||||
|
||||
A protected write produces a human-only `ask`: it skips the session-approval and yolo fast paths, composes with zero `authorizerChain` links (AutoReview is never consulted), and the dialog offers no "for this session" option. With no local confirmation UI — a headless session or a subagent relay terminal — the write is denied without prompting. A malformed `protectedWrites` entry makes the whole hard-guard file invalid and follows its fail-closed semantics.
|
||||
|
||||
## Full Example
|
||||
|
||||
```jsonc
|
||||
@@ -244,6 +265,8 @@ Three invariants govern the chain:
|
||||
|
||||
A named authorizer may return a final `allow` or `deny` for any request that ordinary policy resolved to `ask`, including `path`. There is no additional surface-based delegation cap. `defer` advances to the next configured authorizer and ultimately the human terminal.
|
||||
|
||||
The one exception is a [protected write](#protected-writes): its `ask` is human-only and never reaches the chain — `authorizerChain` links are not consulted, and a terminal that is not the local user prompt denies without prompting.
|
||||
|
||||
Extension authors register a link from `permissions:ready` via `getPermissionsService().registerAuthorizer(name, authorize)`. For integration details, see [Cross-Extension API](cross-extension-api.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -49,7 +49,7 @@ If your OpenCode config uses these features, the equivalent works in this extens
|
||||
| `mcp` surface | Not a documented permission surface | First-class with server/tool-level granularity |
|
||||
| Top-level string shorthand | `"permission": "allow"` sets all surfaces | Not supported; must use an object |
|
||||
| Per-agent config location | `agent` key in config JSON or YAML frontmatter | YAML frontmatter in agent `.md` files only |
|
||||
| Config file paths | `~/.config/opencode/opencode.json` | `~/.pi/agent/my-pi/permission-system/policy.jsonc` plus global `hard-guards.jsonc` |
|
||||
| Config file paths | `~/.config/opencode/opencode.json` | `config/pi-permission-policy.jsonc` plus `config/pi-permission-hard-guards.jsonc` inside the installed bundle |
|
||||
| Subagent prompt forwarding | Not documented | `ask` policies work in non-UI subagent contexts |
|
||||
| Infrastructure auto-allow | N/A | Read-only tools to Pi infra dirs bypass the gate |
|
||||
| Permission review log | No equivalent documented | Writes decisions to a JSONL audit log |
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
| Problem | Cause | Solution |
|
||||
| ----------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Config not applied (everything asks) | File not found or parse error | Verify the global policy at `~/.pi/agent/my-pi/permission-system/policy.jsonc` (respects `PI_CODING_AGENT_DIR`); check JSONC syntax |
|
||||
| Config not applied (everything asks) | File not found or parse error | Verify the global policy at `config/pi-permission-policy.jsonc` inside the installed bundle; check JSONC syntax |
|
||||
| Per-agent override not applied | Frontmatter parsing issue | Ensure `---` delimiters at file top; keep YAML simple; restart session |
|
||||
| Tool blocked as unregistered | Unknown tool name | Server tools arrive as `mcp__<server>__<tool>` under Pi's built-in MCP (check `/mcp`); a unified `mcp` proxy tool uses `{ "tool": "server:tool" }` |
|
||||
| `/skill:<name>` blocked | Deny policy or confirmation unavailable | Check merged `skill` policy (global/project/agent layers). `ask` still requires UI or forwarded confirmation. |
|
||||
| External file path blocked | `external_directory` is `ask` without UI or `deny` | Allow/ask the permission or keep file tools inside the active working directory. |
|
||||
| Spurious external-path prompt for `cd <subdir> && grep … ../path` | Relative path was resolved against cwd instead of the `cd` target | Fixed in current version — paths after a leading `cd <subdir> &&` are resolved against the cd target, matching actual shell behavior. |
|
||||
| Permission prompt is too verbose | Generic extension tool input is large | Built-in file tools are summarized automatically; third-party tools are capped to a bounded one-line JSON preview. |
|
||||
| A file write keeps prompting despite a session grant or yolo | The target is in `protectedWrites` | Working as designed — protected writes are human-only and never take a session grant. Remove the entry from `protectedWrites` in `config/pi-permission-hard-guards.jsonc` to lift it. |
|
||||
| A file write is denied without any prompt | `protectedWrites` target, no confirmation UI (headless or subagent relay) | Working as designed — a human-only ask cannot be answered without a local UI. Run the write from an interactive session instead. |
|
||||
|
||||
## Diagnostic Logging
|
||||
|
||||
@@ -22,17 +24,12 @@ This makes it easy to verify which files the extension actually loaded:
|
||||
```jsonc
|
||||
{
|
||||
"event": "config.resolved",
|
||||
"globalConfigPath": "/…/.pi/agent/my-pi/permission-system/policy.jsonc",
|
||||
"globalConfigPath": "/…/bundle/config/pi-permission-policy.jsonc",
|
||||
"globalConfigExists": true,
|
||||
"projectConfigPath": "/…/my-project/.pi/extensions/pi-permission-system/policy.jsonc",
|
||||
"projectConfigExists": false,
|
||||
"agentsDir": "/…/.pi/agent/agents",
|
||||
"agentsDirExists": true,
|
||||
"projectAgentsDir": "/…/my-project/.pi/agents",
|
||||
"projectAgentsDirExists": false,
|
||||
"legacyGlobalPolicyDetected": false,
|
||||
"legacyProjectPolicyDetected": false,
|
||||
"legacyExtensionConfigDetected": false
|
||||
"projectAgentsDirExists": false
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"src",
|
||||
"dist",
|
||||
"config/config.example.json",
|
||||
"config/hard-guards.default.jsonc",
|
||||
"schemas/permissions.schema.json",
|
||||
"schemas/hard-guards.schema.json",
|
||||
"docs/*.md",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://raw.githubusercontent.com/siakitem/my-pi/main/pi-permission-system/schemas/hard-guards.schema.json",
|
||||
"title": "Pi Permission System Hard Guards",
|
||||
"description": "Global-only allow exceptions and non-overridable deny rules evaluated before ordinary policy, session grants, and AutoReview.",
|
||||
"description": "Global-only allow exceptions and non-overridable deny rules evaluated before ordinary policy, session grants, and AutoReview, plus protectedWrites whose writes always escalate to a human prompt.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": { "type": "string" },
|
||||
@@ -18,6 +18,19 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"protectedWrites": {
|
||||
"type": "array",
|
||||
"description": "Files and subtrees whose writes are human-only: every write goes to a local human prompt, skipping AutoReview, yolo, and session grants, and is denied when no UI is available. Patterns use the path surface wildcard syntax; {agentDir}, {configHome} and a leading ~/ are expanded at load.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": { "type": "string", "minLength": 1 },
|
||||
"label": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["pattern", "label"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["version", "permission"],
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
"additionalProperties": false,
|
||||
"title": "PI Permission System Configuration",
|
||||
"description": "Ordinary JSONC policy combining runtime knobs, AutoReview settings, and flat permission rules for pi-permission-system.",
|
||||
"markdownDescription": "Global ordinary JSONC policy for [pi-permission-system](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-permission-system).\n\nPlace at `~/.pi/agent/my-pi/permission-system/policy.jsonc`. Project-level policy files are not loaded. Global-only hard guards use the separate `hard-guards.jsonc` schema.",
|
||||
"markdownDescription": "Global ordinary JSONC policy for [pi-permission-system](https://github.com/gotgenes/pi-packages/tree/main/packages/pi-permission-system).\n\nRead directly from the installed bundle's `config/pi-permission-policy.jsonc`. Project-level policy files are not loaded. Global-only hard guards use the separate `config/pi-permission-hard-guards.jsonc` schema.",
|
||||
"$defs": {
|
||||
"permissionState": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
classifyTokenAsRuleCandidate,
|
||||
} from "#src/access-intent/bash/token-classification";
|
||||
import {
|
||||
collectCommandTokens,
|
||||
collectPathCandidateTokens,
|
||||
collectRedirectTokens,
|
||||
type BashPathTokenRole,
|
||||
type CollectedPathToken,
|
||||
collectCommandTokensDetailed,
|
||||
collectPathCandidateTokensDetailed,
|
||||
collectRedirectTokensDetailed,
|
||||
extractCommandName,
|
||||
} from "#src/access-intent/bash/token-collection";
|
||||
import { normalizePathPolicyLiteral } from "#src/access-intent/path-normalization";
|
||||
@@ -40,6 +42,9 @@ type EffectiveBase =
|
||||
interface PathCandidate {
|
||||
readonly token: string;
|
||||
readonly base: EffectiveBase;
|
||||
readonly role: BashPathTokenRole;
|
||||
readonly program: string | null;
|
||||
readonly wrapped: boolean;
|
||||
}
|
||||
|
||||
// ── Public output types ──────────────────────────────────────────────────────
|
||||
@@ -49,13 +54,41 @@ export interface BashPathRuleCandidate {
|
||||
readonly token: string;
|
||||
/** The path's lexical and canonical forms for permission policy matching. */
|
||||
readonly path: AccessPath;
|
||||
/**
|
||||
* How the command uses the token — `redirect-out`/`redirect-in` for
|
||||
* redirect destinations, `argument` otherwise. Only the protected-write
|
||||
* gate reads this; the ordinary path gate is role-agnostic.
|
||||
*/
|
||||
readonly role: BashPathTokenRole;
|
||||
/** The enclosing command's program name, or `null` outside a command. */
|
||||
readonly program: string | null;
|
||||
/**
|
||||
* True inside a wrapper payload (`bash -c`, `eval`, `sudo`, `xargs`, …) or a
|
||||
* command/process substitution — the enclosing program cannot vouch for the
|
||||
* token's use.
|
||||
*/
|
||||
readonly wrapped: boolean;
|
||||
}
|
||||
|
||||
/** Cd-aware path-rule candidates extracted from a bash program. */
|
||||
export interface ResolvedBashPaths {
|
||||
readonly ruleCandidates: readonly BashPathRuleCandidate[];
|
||||
/**
|
||||
* The protected-write slice: every `ruleCandidates` entry plus every
|
||||
* `redirect-out` destination, which is admitted unconditionally — a redirect
|
||||
* target need not exist nor look like a path to still be written (`cd dir
|
||||
* && echo 1 > new.json`). Role metadata is what the gate classifies on.
|
||||
*/
|
||||
readonly writeCandidates: readonly BashPathRuleCandidate[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect targets that are really file-descriptor plumbing, not files:
|
||||
* `>&1`, `2>&-` and friends write to a descriptor, so they never enter the
|
||||
* write slice.
|
||||
*/
|
||||
const FD_DUPLICATE_TARGET = /^&?[\d-]+$/;
|
||||
|
||||
// ── Walk-time constants ──────────────────────────────────────────────────────
|
||||
|
||||
/** The working directory in force at the start of a program (`cwd`). */
|
||||
@@ -101,9 +134,7 @@ export class BashPathResolver {
|
||||
? CWD_BASE
|
||||
: this.deriveBaseFromCdTarget(CWD_BASE, this.workdir);
|
||||
const candidates = this.collectPathCandidates(rootNode, initialBase);
|
||||
return {
|
||||
ruleCandidates: this.projectRuleCandidates(candidates),
|
||||
};
|
||||
return this.projectCandidates(candidates);
|
||||
}
|
||||
|
||||
// ── AST walk — collect PathCandidates ──────────────────────────────────
|
||||
@@ -145,7 +176,7 @@ export class BashPathResolver {
|
||||
case "redirected_statement":
|
||||
return this.walkCurrentShellSequence(node, base, out);
|
||||
case "command":
|
||||
tagTokens(collectCommandTokens(node), base, out);
|
||||
tagTokens(collectCommandTokensDetailed(node), base, out);
|
||||
return this.foldCd(node, base);
|
||||
case "pipeline":
|
||||
// tree-sitter-bash mis-groups a redirect-bearing `&&`/`;` list as the
|
||||
@@ -170,7 +201,7 @@ export class BashPathResolver {
|
||||
// substitution interiors: collect every candidate in the subtree tagged
|
||||
// with the enclosing base and do not fold their internal `cd`s. (Folding
|
||||
// inside substitutions is deferred — conservative, never under-flags.)
|
||||
tagTokens(collectPathCandidateTokens(node), base, out);
|
||||
tagTokens(collectPathCandidateTokensDetailed(node), base, out);
|
||||
return base;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +261,7 @@ export class BashPathResolver {
|
||||
}
|
||||
// Downstream stage (after a `|`): subshell — collect against the folded
|
||||
// base, do not fold.
|
||||
tagTokens(collectPathCandidateTokens(child), current, out);
|
||||
tagTokens(collectPathCandidateTokensDetailed(child), current, out);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -260,7 +291,7 @@ export class BashPathResolver {
|
||||
if (child.type === "file_redirect") {
|
||||
// Redirect destinations are part of the piped stage; collect them
|
||||
// against the folded base without folding.
|
||||
tagTokens(collectRedirectTokens(child), current, out);
|
||||
tagTokens(collectRedirectTokensDetailed(child), current, out);
|
||||
continue;
|
||||
}
|
||||
// The inner statement is the `list`/`command` being redirected; fold its
|
||||
@@ -270,7 +301,7 @@ export class BashPathResolver {
|
||||
return current;
|
||||
}
|
||||
// Bare `command` or any other shape: a true subshell first stage.
|
||||
tagTokens(collectPathCandidateTokens(node), base, out);
|
||||
tagTokens(collectPathCandidateTokensDetailed(node), base, out);
|
||||
return base;
|
||||
}
|
||||
|
||||
@@ -298,7 +329,7 @@ export class BashPathResolver {
|
||||
current = this.walkForCandidates(child, current, out);
|
||||
} else {
|
||||
// Terminal child = the real pipe stage; collect without folding.
|
||||
tagTokens(collectPathCandidateTokens(child), current, out);
|
||||
tagTokens(collectPathCandidateTokensDetailed(child), current, out);
|
||||
}
|
||||
}
|
||||
return current;
|
||||
@@ -363,34 +394,67 @@ export class BashPathResolver {
|
||||
/**
|
||||
* Project candidates into policy lookup values while preserving literal
|
||||
* values after a non-literal `cd`, where no reliable absolute base exists.
|
||||
*
|
||||
* `ruleCandidates` keeps the long-standing admission rule — path-shaped
|
||||
* tokens plus bare tokens an existence probe confirms. `writeCandidates`
|
||||
* additionally admits every `redirect-out` destination: a redirect writes
|
||||
* its target whether or not the file exists yet, so existence and shape
|
||||
* cannot gate it. The write slice is deduplicated per role so the same
|
||||
* path appearing as both a read argument and a write destination keeps
|
||||
* both classifications.
|
||||
*/
|
||||
private projectRuleCandidates(
|
||||
private projectCandidates(
|
||||
candidates: readonly PathCandidate[],
|
||||
): BashPathRuleCandidate[] {
|
||||
const seen = new Set<string>();
|
||||
const result: BashPathRuleCandidate[] = [];
|
||||
): ResolvedBashPaths {
|
||||
const seenRules = new Set<string>();
|
||||
const seenWrites = new Set<string>();
|
||||
const ruleCandidates: BashPathRuleCandidate[] = [];
|
||||
const writeCandidates: BashPathRuleCandidate[] = [];
|
||||
|
||||
for (const { token, base } of candidates) {
|
||||
const pushWrite = (
|
||||
candidate: BashPathRuleCandidate,
|
||||
matchValues: readonly string[],
|
||||
): void => {
|
||||
const key = `${candidate.role} | ||||