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
@@ -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:-}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user