mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Protect jangbu wrapper installs from incomplete payloads
The promoted upstream subskills need to be discoverable without making home installs destructive or incomplete. The wrapper installer now copies its support payload into both home skill roots, allows installed-wrapper reruns, and refuses to overwrite unrelated top-level jangbu-* skills unless explicitly overridden. Constraint: PR #181 review requires top-level subskill discovery under both Claude and agents roots. Constraint: Home installs must remain re-runnable without a source checkout. Rejected: Continue using generic sync_dir for promoted skills | it silently deletes unrelated user-authored skills. Confidence: high Scope-risk: narrow Directive: Do not bypass the promoted-skill ownership check without preserving unrelated home skill directories. Tested: node --test scripts/skill-docs.test.js --test-name-pattern='korean-jangbu-for' Tested: bash -n korean-jangbu-for/scripts/install.sh Tested: bash korean-jangbu-for/scripts/install.sh plus installed ~/.claude and ~/.agents wrapper reruns Tested: bash ~/.claude/skills/korean-jangbu-for/upstream/scripts/install.sh and verify.sh with Python 3.11 shim Tested: npm run ci Tested: Architect verification APPROVED Not-tested: Live CODEF collection requiring user BYOK credentials and external authentication
This commit is contained in:
parent
5a4ff0759f
commit
11b1150110
4 changed files with 141 additions and 3 deletions
|
|
@ -32,6 +32,8 @@
|
|||
|
||||
`~/.claude/skills/korean-jangbu-for/upstream/` 와 `~/.agents/skills/korean-jangbu-for/upstream/` 양쪽에 pinned SHA 로 업스트림을 설치한다. 동시에 업스트림 `skills/jangbu-*` 를 `~/.claude/skills/<skill-name>` 및 `~/.agents/skills/<skill-name>` top-level 경로로 등록해 slash skill discovery 가 중첩 `upstream/` 탐색에 의존하지 않게 한다. `/korean-jangbu-for` 는 wrapper top-level skill 로 유지한다.
|
||||
|
||||
홈 디렉터리 wrapper 에는 재실행 가능한 `scripts/install.sh`, `scripts/upstream.pin`, `LICENSE.upstream`, `DISCLAIMER.md`, `NOTICE` 까지 함께 복사된다. Promoted `jangbu-*` top-level 스킬은 wrapper 가 이전에 설치한 managed copy 만 자동 갱신하며, 같은 이름의 unrelated/user-authored 스킬이 있으면 덮어쓰지 않고 중단한다. 의도적으로 교체해야 하는 경우에만 `KOREAN_JANGBU_FOR_OVERWRITE_SKILLS=1` 로 재실행한다.
|
||||
|
||||
```bash
|
||||
bash korean-jangbu-for/scripts/install.sh
|
||||
```
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ metadata:
|
|||
|
||||
업스트림을 `~/.claude/skills/korean-jangbu-for/upstream/` 와 `~/.agents/skills/korean-jangbu-for/upstream/` 양쪽에 pinned SHA 로 체크아웃한다. 또한 업스트림 `skills/jangbu-*` 를 양쪽 홈 디렉터리의 top-level skill 로 등록해 `/jangbu-connect`, `/jangbu-import`, `/jangbu-tag`, `/jangbu-tax`, `/jangbu-dash`, `/jangbu-jongso` 라우팅이 agent-compatible 런타임에서도 발견되게 한다. `/korean-jangbu-for` 는 이 wrapper 의 top-level skill 로 유지한다. 레포 내부에는 업스트림 payload 를 커밋하지 않는다.
|
||||
|
||||
홈 디렉터리 wrapper 에는 `SKILL.md`, `scripts/install.sh`, `scripts/upstream.pin`, `LICENSE.upstream`, `DISCLAIMER.md`, `NOTICE` 를 함께 설치한다. Promoted `jangbu-*` top-level 경로에 사용자가 만든 다른 스킬이 이미 있으면 installer 는 덮어쓰지 않고 중단한다. wrapper 가 이전에 설치한 managed 스킬만 자동 갱신하며, 의도적으로 교체해야 할 때만 `KOREAN_JANGBU_FOR_OVERWRITE_SKILLS=1` 을 설정한다.
|
||||
|
||||
```bash
|
||||
bash korean-jangbu-for/scripts/install.sh
|
||||
```
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ set -euo pipefail
|
|||
|
||||
UPSTREAM_REPO="${KOREAN_JANGBU_FOR_UPSTREAM_REPO:-https://github.com/kimlawtech/korean-jangbu-for.git}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WRAPPER_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
PIN_FILE="${SCRIPT_DIR}/upstream.pin"
|
||||
SKILL_NAME="korean-jangbu-for"
|
||||
MANAGED_MARKER="k-skill wrapper attribution and disclaimer"
|
||||
|
||||
if [[ ! -f "${PIN_FILE}" ]]; then
|
||||
echo "[korean-jangbu-for] upstream.pin not found at ${PIN_FILE}" >&2
|
||||
|
|
@ -58,6 +60,52 @@ sync_dir() {
|
|||
fi
|
||||
}
|
||||
|
||||
copy_file_if_different() {
|
||||
local source_file="$1"
|
||||
local target_file="$2"
|
||||
|
||||
if [[ -e "${target_file}" ]] && [[ "$(cd "$(dirname "${source_file}")" && pwd -P)/$(basename "${source_file}")" == "$(cd "$(dirname "${target_file}")" && pwd -P)/$(basename "${target_file}")" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
cp "${source_file}" "${target_file}"
|
||||
}
|
||||
|
||||
install_wrapper_payload() {
|
||||
local target_dir="$1"
|
||||
|
||||
mkdir -p "${target_dir}/scripts"
|
||||
copy_file_if_different "${WRAPPER_DIR}/SKILL.md" "${target_dir}/SKILL.md"
|
||||
copy_file_if_different "${WRAPPER_DIR}/LICENSE.upstream" "${target_dir}/LICENSE.upstream"
|
||||
copy_file_if_different "${WRAPPER_DIR}/DISCLAIMER.md" "${target_dir}/DISCLAIMER.md"
|
||||
copy_file_if_different "${WRAPPER_DIR}/NOTICE" "${target_dir}/NOTICE"
|
||||
copy_file_if_different "${WRAPPER_DIR}/scripts/install.sh" "${target_dir}/scripts/install.sh"
|
||||
copy_file_if_different "${WRAPPER_DIR}/scripts/upstream.pin" "${target_dir}/scripts/upstream.pin"
|
||||
chmod +x "${target_dir}/scripts/install.sh"
|
||||
}
|
||||
|
||||
is_managed_promoted_skill() {
|
||||
local target_dir="$1"
|
||||
local skill_file="${target_dir}/SKILL.md"
|
||||
|
||||
[[ -f "${skill_file}" ]] && grep -q "${MANAGED_MARKER}" "${skill_file}"
|
||||
}
|
||||
|
||||
sync_promoted_skill() {
|
||||
local source_dir="$1"
|
||||
local target_dir="$2"
|
||||
|
||||
if [[ -e "${target_dir}" || -L "${target_dir}" ]]; then
|
||||
if [[ "${KOREAN_JANGBU_FOR_OVERWRITE_SKILLS:-}" != "1" ]] && ! is_managed_promoted_skill "${target_dir}"; then
|
||||
echo "[korean-jangbu-for] refusing to overwrite unrelated skill: ${target_dir}" >&2
|
||||
echo " Set KOREAN_JANGBU_FOR_OVERWRITE_SKILLS=1 to replace this top-level skill." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
sync_dir "${source_dir}" "${target_dir}"
|
||||
}
|
||||
|
||||
append_response_policy() {
|
||||
local skill_file="$1"
|
||||
|
||||
|
|
@ -110,8 +158,7 @@ for HOME_SKILL_DIR in "${HOME_DIRS[@]}"; do
|
|||
if [[ -L "${HOME_SKILL_DIR}" ]]; then
|
||||
rm -f "${HOME_SKILL_DIR}"
|
||||
fi
|
||||
mkdir -p "${HOME_SKILL_DIR}"
|
||||
cp "${SCRIPT_DIR}/../SKILL.md" "${HOME_SKILL_DIR}/SKILL.md"
|
||||
install_wrapper_payload "${HOME_SKILL_DIR}"
|
||||
|
||||
sync_dir "${CLONE_DIR}" "${HOME_UPSTREAM}"
|
||||
|
||||
|
|
@ -133,7 +180,7 @@ for HOME_SKILL_DIR in "${HOME_DIRS[@]}"; do
|
|||
exit 1
|
||||
fi
|
||||
|
||||
sync_dir "${UPSTREAM_SKILL_DIR}" "${HOME_UPSTREAM_SKILL_DIR}"
|
||||
sync_promoted_skill "${UPSTREAM_SKILL_DIR}" "${HOME_UPSTREAM_SKILL_DIR}"
|
||||
append_response_policy "${HOME_UPSTREAM_SKILL_DIR}/SKILL.md"
|
||||
|
||||
echo "[korean-jangbu-for] registered upstream skill /${UPSTREAM_SKILL} -> ${HOME_UPSTREAM_SKILL_DIR}"
|
||||
|
|
|
|||
|
|
@ -3070,6 +3070,19 @@ test("korean-jangbu-for installer registers upstream subskills for Claude and ag
|
|||
`${root} should replace conflicting upstream korean-jangbu-for symlinks with a wrapper directory`,
|
||||
);
|
||||
|
||||
for (const requiredPath of [
|
||||
"scripts/install.sh",
|
||||
"scripts/upstream.pin",
|
||||
"LICENSE.upstream",
|
||||
"DISCLAIMER.md",
|
||||
"NOTICE",
|
||||
]) {
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(skillRoot, "korean-jangbu-for", requiredPath)),
|
||||
`${root} should install wrapper support payload ${requiredPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const skillName of upstreamSubskills) {
|
||||
const installedSubskillPath = path.join(skillRoot, skillName, "SKILL.md");
|
||||
assert.ok(
|
||||
|
|
@ -3087,6 +3100,80 @@ test("korean-jangbu-for installer registers upstream subskills for Claude and ag
|
|||
assert.match(installedSubskill, /세무신고/);
|
||||
}
|
||||
}
|
||||
|
||||
for (const installedRoot of [".claude", ".agents"]) {
|
||||
childProcess.execFileSync("bash", [path.join(homeDir, installedRoot, "skills", "korean-jangbu-for", "scripts", "install.sh")], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: homeDir,
|
||||
KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir,
|
||||
KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha,
|
||||
},
|
||||
stdio: "pipe",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("korean-jangbu-for installer refuses to overwrite unrelated promoted subskills", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "korean-jangbu-for-collision-"));
|
||||
const homeDir = path.join(tmpDir, "home");
|
||||
const upstreamDir = path.join(tmpDir, "upstream");
|
||||
const installPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "install.sh");
|
||||
const upstreamSubskills = [
|
||||
"jangbu-connect",
|
||||
"jangbu-dash",
|
||||
"jangbu-import",
|
||||
"jangbu-jongso",
|
||||
"jangbu-tag",
|
||||
"jangbu-tax",
|
||||
];
|
||||
|
||||
for (const skillName of upstreamSubskills) {
|
||||
fs.mkdirSync(path.join(upstreamDir, "skills", skillName), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(upstreamDir, "skills", skillName, "SKILL.md"),
|
||||
`---\nname: ${skillName}\n---\n\n# ${skillName}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.join(upstreamDir, "scripts"), { recursive: true });
|
||||
fs.writeFileSync(path.join(upstreamDir, "scripts", "verify.sh"), "#!/usr/bin/env bash\nexit 0\n");
|
||||
|
||||
childProcess.execFileSync("git", ["init"], { cwd: upstreamDir, stdio: "ignore" });
|
||||
childProcess.execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: upstreamDir });
|
||||
childProcess.execFileSync("git", ["config", "user.name", "Test"], { cwd: upstreamDir });
|
||||
childProcess.execFileSync("git", ["add", "."], { cwd: upstreamDir });
|
||||
childProcess.execFileSync("git", ["commit", "-m", "seed upstream skills"], { cwd: upstreamDir, stdio: "ignore" });
|
||||
const upstreamSha = childProcess.execFileSync("git", ["rev-parse", "HEAD"], { cwd: upstreamDir, encoding: "utf8" }).trim();
|
||||
|
||||
const unrelatedSkillDir = path.join(homeDir, ".claude", "skills", "jangbu-tax");
|
||||
fs.mkdirSync(unrelatedSkillDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(unrelatedSkillDir, "SKILL.md"),
|
||||
"---\nname: jangbu-tax\n---\n\n# user-authored jangbu-tax\n",
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
childProcess.execFileSync("bash", [installPath], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: homeDir,
|
||||
KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir,
|
||||
KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha,
|
||||
},
|
||||
stdio: "pipe",
|
||||
}),
|
||||
/refusing to overwrite unrelated skill/,
|
||||
);
|
||||
|
||||
assert.match(
|
||||
fs.readFileSync(path.join(unrelatedSkillDir, "SKILL.md"), "utf8"),
|
||||
/user-authored jangbu-tax/,
|
||||
"unrelated existing subskill should be preserved after installer refusal",
|
||||
);
|
||||
});
|
||||
|
||||
test("korean-jangbu-for feature doc documents source-first use and mandatory attribution", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue