mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-10-29 03:34:44 +08:00
Enforce tutorial writer safety boundaries
This commit is contained in:
@@ -58,7 +58,9 @@ Then run:
|
||||
TUTORIAL_ROOT="$(git rev-parse --show-toplevel)"
|
||||
"$TUTORIAL_ROOT/.agents/skills/write-bpf-production-tutorial/scripts/run-claude-writer.sh" \
|
||||
--repo "$TUTORIAL_ROOT" \
|
||||
--task /absolute/path/to/writer-task.md
|
||||
--task /absolute/path/to/writer-task.md \
|
||||
--readme "$TUTORIAL_ROOT/src/<lesson>/README.md" \
|
||||
--readme "$TUTORIAL_ROOT/src/<lesson>/README.zh.md"
|
||||
```
|
||||
|
||||
The wrapper must finish with a verified exact-model trace. Inspect both READMEs for matching structure and technical meaning, then rerun build and documentation commands exactly as written.
|
||||
|
||||
@@ -7,11 +7,12 @@ readonly VERIFY="$SKILL_DIR/scripts/verify_claude_trace.py"
|
||||
readonly GUIDE="$SKILL_DIR/references/writing-guide.md"
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 --repo ABSOLUTE_REPO_PATH --task ABSOLUTE_TASK_FILE" >&2
|
||||
echo "usage: $0 --repo ABSOLUTE_REPO_PATH --task ABSOLUTE_TASK_FILE --readme ABSOLUTE_README_PATH --readme ABSOLUTE_README_PATH" >&2
|
||||
}
|
||||
|
||||
repo=
|
||||
task=
|
||||
declare -a readmes=()
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--repo)
|
||||
@@ -22,6 +23,10 @@ while (($#)); do
|
||||
task=${2:-}
|
||||
shift 2
|
||||
;;
|
||||
--readme)
|
||||
readmes+=("${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--model|--fallback-model)
|
||||
echo "model overrides and fallbacks are forbidden; required model: $MODEL" >&2
|
||||
exit 2
|
||||
@@ -37,10 +42,43 @@ while (($#)); do
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $repo = /* && -d $repo/.git ]] || { echo "--repo must be an absolute Git checkout" >&2; exit 2; }
|
||||
[[ $task = /* && -f $task ]] || { echo "--task must be an absolute readable file" >&2; exit 2; }
|
||||
command -v claude >/dev/null || { echo "Claude Code is required" >&2; exit 127; }
|
||||
command -v git >/dev/null || { echo "git is required" >&2; exit 127; }
|
||||
command -v python3 >/dev/null || { echo "python3 is required" >&2; exit 127; }
|
||||
command -v realpath >/dev/null || { echo "realpath is required" >&2; exit 127; }
|
||||
[[ $repo = /* && -d $repo ]] || { echo "--repo must be an absolute directory" >&2; exit 2; }
|
||||
repo=$(realpath -e -- "$repo")
|
||||
[[ $(git -C "$repo" rev-parse --is-inside-work-tree 2>/dev/null) == true ]] || {
|
||||
echo "--repo must be a Git worktree" >&2
|
||||
exit 2
|
||||
}
|
||||
[[ $task = /* && -f $task ]] || { echo "--task must be an absolute readable file" >&2; exit 2; }
|
||||
task=$(realpath -e -- "$task")
|
||||
((${#readmes[@]} == 2)) || { echo "pass exactly two --readme paths" >&2; exit 2; }
|
||||
|
||||
declare -a allowed_readmes=()
|
||||
english_readme=
|
||||
chinese_readme=
|
||||
for readme in "${readmes[@]}"; do
|
||||
[[ $readme = /* ]] || { echo "--readme must be absolute: $readme" >&2; exit 2; }
|
||||
readme=$(realpath -m -- "$readme")
|
||||
case "$readme" in
|
||||
"$repo"/src/*/README.md)
|
||||
[[ -z $english_readme ]] || { echo "pass only one README.md" >&2; exit 2; }
|
||||
english_readme=$readme
|
||||
;;
|
||||
"$repo"/src/*/README.zh.md)
|
||||
[[ -z $chinese_readme ]] || { echo "pass only one README.zh.md" >&2; exit 2; }
|
||||
chinese_readme=$readme
|
||||
;;
|
||||
*) echo "--readme must name a tutorial README inside $repo/src: $readme" >&2; exit 2 ;;
|
||||
esac
|
||||
allowed_readmes+=("$readme")
|
||||
done
|
||||
[[ -n $english_readme && -n $chinese_readme && $(dirname -- "$english_readme") == $(dirname -- "$chinese_readme") ]] || {
|
||||
echo "--readme paths must be the English and Chinese pair from one lesson" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
state_root=${XDG_STATE_HOME:-$HOME/.local/state}/bpf-tutorial-writer/runs
|
||||
mkdir -p "$state_root"
|
||||
@@ -54,9 +92,11 @@ manifest="$run_dir/manifest.json"
|
||||
head_before=$(git -C "$repo" rev-parse HEAD)
|
||||
status_before=$(git -C "$repo" status --porcelain=v1)
|
||||
task_sha=$(sha256sum "$task" | awk '{print $1}')
|
||||
printf -v allowed_paths 'Allowed README path: %s\n' "${allowed_readmes[@]}"
|
||||
|
||||
prompt=$(printf '%s\n\n%s\n\n%s\n' \
|
||||
prompt=$(printf '%s\n\n%s\n%s\n%s\n' \
|
||||
'You are the sole prose author for an eBPF production tutorial. Read the repository code and evidence named in the task. Edit only the README.md and README.zh.md paths explicitly allowed by the task. Do not edit code, build files, generated indexes, or tests. Do not invent output or claims. Finish by stating which README files you edited.' \
|
||||
"$allowed_paths" \
|
||||
"$(<"$GUIDE")" \
|
||||
"$(<"$task")")
|
||||
|
||||
@@ -64,10 +104,11 @@ set +e
|
||||
(
|
||||
cd "$repo"
|
||||
claude -p \
|
||||
--safe-mode \
|
||||
--model "$MODEL" \
|
||||
--effort high \
|
||||
--permission-mode acceptEdits \
|
||||
--allowedTools Read Edit Write \
|
||||
--tools "Read,Edit,Write" \
|
||||
--output-format stream-json \
|
||||
--verbose \
|
||||
"$prompt"
|
||||
@@ -116,6 +157,14 @@ if ((claude_status != 0)); then
|
||||
echo "Claude exited with status $claude_status; preserved trace: $trace" >&2
|
||||
exit "$claude_status"
|
||||
fi
|
||||
if [[ $head_after != "$head_before" ]]; then
|
||||
echo "Claude changed Git HEAD; preserved trace: $trace" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 "$VERIFY" --trace "$trace" --model "$MODEL" --require-write
|
||||
verify_args=(--trace "$trace" --model "$MODEL" --repo "$repo" --require-write)
|
||||
for readme in "${allowed_readmes[@]}"; do
|
||||
verify_args+=(--allowed-path "$readme")
|
||||
done
|
||||
python3 "$VERIFY" "${verify_args[@]}"
|
||||
echo "manifest=$manifest"
|
||||
|
||||
@@ -33,9 +33,14 @@ def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--trace", required=True, type=Path)
|
||||
parser.add_argument("--model", required=True)
|
||||
parser.add_argument("--repo", required=True, type=Path)
|
||||
parser.add_argument("--allowed-path", action="append", required=True, type=Path)
|
||||
parser.add_argument("--require-write", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo = args.repo.resolve()
|
||||
allowed_paths = {path.resolve() for path in args.allowed_path}
|
||||
|
||||
events = load_events(args.trace)
|
||||
init_models = {
|
||||
event.get("model")
|
||||
@@ -49,6 +54,7 @@ def main() -> int:
|
||||
authored_text = 0
|
||||
write_calls = 0
|
||||
assistant_models = set()
|
||||
tool_names = set()
|
||||
for event in events:
|
||||
if event.get("type") != "assistant":
|
||||
continue
|
||||
@@ -58,10 +64,23 @@ def main() -> int:
|
||||
if any(item.get("type") == "text" and item.get("text") for item in content):
|
||||
authored_text += 1
|
||||
assistant_models.add(model)
|
||||
write_calls += sum(
|
||||
item.get("type") == "tool_use" and item.get("name") in {"Edit", "Write"}
|
||||
for item in content
|
||||
)
|
||||
for item in content:
|
||||
if item.get("type") != "tool_use":
|
||||
continue
|
||||
tool_name = item.get("name")
|
||||
tool_names.add(tool_name)
|
||||
if tool_name not in {"Edit", "Write"}:
|
||||
continue
|
||||
write_calls += 1
|
||||
raw_path = (item.get("input") or {}).get("file_path")
|
||||
if not raw_path:
|
||||
raise SystemExit(f"{tool_name} call has no file_path")
|
||||
target = Path(raw_path)
|
||||
if not target.is_absolute():
|
||||
target = repo / target
|
||||
target = target.resolve()
|
||||
if target not in allowed_paths:
|
||||
raise SystemExit(f"{tool_name} call targeted unauthorized path: {target}")
|
||||
|
||||
if not authored_text:
|
||||
raise SystemExit("trace contains no authored assistant text")
|
||||
@@ -71,6 +90,9 @@ def main() -> int:
|
||||
)
|
||||
if args.require_write and write_calls == 0:
|
||||
raise SystemExit("trace contains no Claude Edit or Write call")
|
||||
unexpected_tools = tool_names - {"Read", "Edit", "Write"}
|
||||
if unexpected_tools:
|
||||
raise SystemExit(f"trace contains unauthorized tools: {sorted(unexpected_tools)!r}")
|
||||
|
||||
results = [event for event in events if event.get("type") == "result"]
|
||||
if not results:
|
||||
|
||||
Reference in New Issue
Block a user