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:
云服务部-叶林立
2026-10-20 01:05:31 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent a0ee9389c7
commit 2aa1be0b54
105 changed files with 2337 additions and 1413 deletions
+58 -2
View File
@@ -1,7 +1,63 @@
#!/bin/sh
# Variables assigned here are consumed by the sourcing install.sh/update.sh.
# shellcheck disable=SC2034
# Shared helpers for files that contain my-pi-owned blocks.
# Callers must provide say() and warn().
# Shared helpers for the bundle's POSIX sh scripts.
# Source this file; it defines say(), warn(), the Kitty/Zsh resolution
# helpers, and the my-pi managed-block machinery used by install.sh and
# update.sh.
say() {
printf '%s\n' "$*"
}
warn() {
printf '警告: %s\n' "$*" >&2
}
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
}
resolve_kitty_config_dir() {
kitty_config_dir=${KITTY_CONFIG_DIRECTORY:-${XDG_CONFIG_HOME:-$HOME/.config}/kitty}
}
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
}
# Anchor regex shared by every "before the Oh My Zsh loader" managed block.
MY_PI_ZSH_LOADER_ANCHOR='^(# >>> my-pi:oh-my-zsh-loader >>>|[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh)'
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"
}
MY_PI_MANAGED_BACKUPS=${MY_PI_MANAGED_BACKUPS:-}
-174
View File
@@ -1,174 +0,0 @@
#!/usr/bin/env node
/**
* Rebuilds the bundle-owned config directory inside the Pi agent directory.
*
* Every piece of bundle-owned, rebuildable state lives under
* `<agentDir>/my-pi/` (policy files, Tool Search config, the system prompt
* cover, permission logs). `config/bundle-manifest.json` is the authoritative
* source→target list: this helper builds the full tree in a sibling temp
* directory, deletes any existing `my-pi/` wholesale — no backup — and renames
* the fresh tree into place, so a rebuild can never half-apply.
*
* Nothing outside `my-pi/` is read or removed; leftovers at the old locations
* are ignored rather than migrated.
*/
import { realpathSync } from "node:fs";
import { chmod, copyFile, lstat, mkdir, readFile, rename, rm, unlink } from "node:fs/promises";
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const scriptPath = fileURLToPath(import.meta.url);
const bundleRoot = dirname(dirname(scriptPath));
export const BUNDLE_ROOT = bundleRoot;
export const MANIFEST_PATH = join(bundleRoot, "config", "bundle-manifest.json");
/** Thrown for an unusable agentDir argument; maps to CLI exit code 2. */
export class RebuildUsageError extends Error {}
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
/**
* Load and validate the manifest.
*
* Every source must be a readable repo-relative file and every target a
* relative path that stays inside the bundle directory — a `..` or absolute
* target would escape `my-pi/` during the rebuild, so it is rejected before
* anything is touched.
*
* @param {string} manifestPath
* @returns {Promise<{ directory: string, files: Array<{ source: string, target: string }> }>}
*/
export async function loadManifest(manifestPath) {
let manifest;
try {
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
} catch (error) {
throw new Error(
`cannot read bundle manifest at ${manifestPath}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
if (!isObject(manifest) || manifest.directory !== "my-pi" || !Array.isArray(manifest.files)) {
throw new Error(`bundle manifest at ${manifestPath} is malformed`);
}
const files = manifest.files.map((entry, index) => {
if (!isObject(entry) || typeof entry.source !== "string" || typeof entry.target !== "string") {
throw new Error(`bundle manifest entry ${index} lacks source/target strings`);
}
const directoryBase = resolve(manifest.directory);
const resolved = resolve(directoryBase, entry.target);
if (
isAbsolute(entry.target) ||
resolved === directoryBase ||
!resolved.startsWith(directoryBase + sep)
) {
throw new Error(
`bundle manifest target ${entry.target} escapes the ${manifest.directory}/ directory`,
);
}
const sourcePath = join(bundleRoot, entry.source);
return { source: entry.source, sourcePath, target: entry.target };
});
// Fail before deleting anything: a missing source must not wipe the live tree.
for (const file of files) {
try {
await readFile(file.sourcePath);
} catch (error) {
throw new Error(
`cannot read manifest source ${file.source}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
return { directory: manifest.directory, files };
}
/**
* Rebuild `<agentDir>/my-pi` from the bundle manifest.
*
* The tree is staged in `<agentDir>/.my-pi.rebuild-<pid>-<rand>` (dirs 0700,
* files 0600), then the existing `my-pi` is removed — a symlink is unlinked,
* never followed — and the stage is renamed into place. Any failure removes
* the stage and throws.
*
* @param {string} agentDir - Absolute Pi agent directory.
* @param {{ manifestPath?: string }} [options]
* @returns {Promise<{ targetDir: string }>}
*/
export async function rebuildBundleConfig(agentDir, { manifestPath = MANIFEST_PATH } = {}) {
if (typeof agentDir !== "string" || agentDir.length === 0 || !isAbsolute(agentDir)) {
throw new RebuildUsageError("rebuild-bundle-config requires an absolute agentDir argument");
}
const manifest = await loadManifest(manifestPath);
const targetDir = join(agentDir, manifest.directory);
const stageDir = join(
agentDir,
`.${manifest.directory}.rebuild-${process.pid}-${Math.random().toString(16).slice(2)}`,
);
try {
await mkdir(agentDir, { recursive: true, mode: 0o700 });
await mkdir(stageDir, { mode: 0o700 });
for (const file of manifest.files) {
const destination = join(stageDir, file.target);
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
// copyFile keeps the source mode on some platforms; pin owner-only.
await copyFile(file.sourcePath, destination);
await chmod(destination, 0o600);
}
// Never follow a symlink: unlink the link itself, never its target.
try {
const stats = await lstat(targetDir);
if (stats.isSymbolicLink()) {
await unlink(targetDir);
} else {
await rm(targetDir, { recursive: true, force: true });
}
} catch (error) {
if (error?.code !== "ENOENT") throw error;
}
await rename(stageDir, targetDir);
return { targetDir };
} catch (error) {
await rm(stageDir, { recursive: true, force: true }).catch(() => {});
throw error;
}
}
function isMainModule() {
if (!process.argv[1]) return false;
try {
return fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
} catch {
return false;
}
}
async function main() {
const agentDir = process.argv[2] ?? "";
if (agentDir.length === 0 || !isAbsolute(agentDir)) {
console.error("usage: rebuild-bundle-config.mjs <absolute-agent-dir>");
process.exitCode = 2;
return;
}
const { targetDir } = await rebuildBundleConfig(agentDir);
process.stdout.write(`rebuilt:${targetDir}\n`);
}
if (isMainModule()) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = error instanceof RebuildUsageError ? 2 : 1;
});
}