Add repository-local tutorial agent skills

This commit is contained in:
LinuxDev9002
2026-07-18 15:27:12 -07:00
parent c054586a39
commit b48a2381d7
11 changed files with 906 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
---
name: test-bpf-tutorial-kvm
description: Build and smoke-test bpf-developer-tutorial lessons without loading BPF programs into the host kernel. Use when creating, changing, reviewing, or debugging an eBPF tutorial and the runtime test should reuse the already-built x86 kernel under bpf-benchmark through virtme-ng/KVM. Also use for checking whether a tutorial needs a newer kernel feature. Do not use this workflow for performance benchmarking or for rebuilding the benchmark kernel.
---
# Test BPF Tutorials in KVM
Compile tutorial code on the host, but run every command that loads, attaches, or exercises BPF inside a short-lived KVM guest using the kernel artifact already built by `bpf-benchmark`.
## Safety boundary
- Never load or attach a tutorial BPF program on the host. This includes `bpftool prog load`, loaders, `make run`, XDP/TC attachment, sched_ext registration, and LSM/cgroup attachment.
- Do not rebuild or modify `bpf-benchmark`, its kernel tree, its Git state, caches, results, or conversation/agent-history data.
- Do not silently fall back to the host kernel or to TCG. Stop if `/dev/kvm`, `vng`, or the expected benchmark kernel is unavailable.
- Treat the tutorial directory as read-only in the guest by default. Use `--rwdir` only for a path that must return guest-generated files to the host.
- Use bounded commands. The wrapper applies a host-side timeout; add a shorter guest-side `timeout` to long-running loaders when useful.
- Report a KVM run as a functional smoke test, not as benchmark-quality evidence.
## Default locations
Resolve the tutorial checkout and skill from the current Git worktree:
```bash
TUTORIAL_ROOT="$(git rev-parse --show-toplevel)"
RUNNER="$TUTORIAL_ROOT/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh"
```
The helper reads `BPF_BENCHMARK_ROOT`, defaulting to a `bpf-benchmark` checkout next to the tutorial repository. Its `--cwd` defaults to the caller's current directory.
The reused kernel is:
```text
$BPF_BENCHMARK_ROOT/vendor/build/x86/linux/arch/x86/boot/bzImage
```
## Workflow
### 1. Validate the repositories and instructions
Read every applicable `AGENTS.md` or equivalent repository instruction before acting.
Require a real tutorial checkout before implementation or testing. A valid checkout has lesson sources and a working Git worktree, not only a `.git` fragment. If it is incomplete, stop and explain that the checkout must be restored; do not clone over it or delete the remnant without explicit authorization.
The top-level `bpf-benchmark` Git repository does not need to be healthy for artifact reuse. The wrapper validates the kernel image, build config, kernel source worktree, KVM device, and toolchain independently.
Run the preflight:
```bash
"$RUNNER" --check
```
### 2. Inspect before building
Read the lesson README, Makefile, source, and existing test commands. Identify separately:
1. a compile-only host command;
2. the userspace loader or runtime command;
3. the event or workload that proves the program works;
4. any required network, cgroup, tracefs, debugfs, or kernel-config feature.
Use `make -n` or inspect recipes before choosing a target. A target that invokes a loader is a guest command, even if it also compiles.
### 3. Build on the host
Run only compile and static-generation steps on the host. Prefer the lesson's documented build target. Preserve generated `.bpf.o`, skeletons, and userspace binaries in the lesson directory so the guest sees the exact host-built artifacts.
Do not use `sudo` for the build. Do not run a post-build loader on the host.
### 4. Run inside the benchmark-kernel guest
Pass the runtime command after `--`. Arguments are preserved without host re-evaluation.
```bash
TUTORIAL_ROOT="$(git rev-parse --show-toplevel)"
RUNNER="$TUTORIAL_ROOT/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh"
LESSON="$TUTORIAL_ROOT/src/<lesson>"
"$RUNNER" --cwd "$LESSON" -- ./loader --help
```
The wrapper boots the built benchmark `bzImage` with KVM, verifies the guest release, runs as guest root, mounts bpffs/debugfs/tracefs/cgroup2, prints provenance, and powers off when the command exits. It defaults to 2 vCPUs, 4G RAM, no network, and 120 seconds.
Use `--network user` only when the test needs an external/userspace-backed interface. Use `--network loop` for a guest-local loop network. Prefer `BPF_PROG_TEST_RUN` or deterministic guest-local traffic when either is sufficient.
Read [references/tutorial-patterns.md](references/tutorial-patterns.md) for tracer, network, writable-output, and failure-diagnosis patterns.
### 5. Verify the behavior, not only the exit code
Capture enough evidence to connect an input event to the expected BPF output: attach confirmation, a deterministic trigger in the same guest, expected map/event output, and clean detach or process exit. Negative tests should verify the expected verifier or attach failure.
For a new lesson, test at least:
- host compile from a clean lesson build state when safe;
- KVM preflight;
- one successful guest runtime path;
- one relevant error or cleanup path;
- documentation commands exactly as written.
Do not clean unrelated files or repository-wide caches to create a clean build state.
### 6. Report provenance and limitations
Include the guest `uname -r`, benchmark kernel source commit, kernel image SHA-256, lesson commit or dirty state, host build command, guest command, and the observed result. The wrapper prints kernel provenance automatically.
If the tutorial requires a feature absent from the built config or kernel, report the missing symbol/helper/attach type and stop. Propose a benchmark-kernel rebuild or upgrade separately; do not perform it as a fallback.
## Wrapper interface
```text
run-in-kvm.sh --check
run-in-kvm.sh [--cwd PATH] [--cpus N] [--memory SIZE]
[--timeout SECONDS] [--network none|user|loop]
[--rwdir PATH]... [--append KERNEL_ARGS]...
[--dry-run] [--verbose] -- COMMAND [ARG...]
```
Use `--dry-run` to inspect the generated `vng`/QEMU launch without booting. `--rwdir` is an explicit trust decision because guest root can change that host path.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Test BPF Tutorials in KVM"
short_description: "Build locally and test with the benchmark KVM kernel"
default_prompt: "Use $test-bpf-tutorial-kvm to build this BPF tutorial locally and run its smoke test with the bpf-benchmark KVM kernel."

View File

@@ -0,0 +1,89 @@
# BPF tutorial KVM test patterns
Resolve the wrapper from the tutorial worktree:
```bash
TUTORIAL_ROOT="$(git rev-parse --show-toplevel)"
RUNNER="$TUTORIAL_ROOT/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh"
```
All paths below are host paths exposed at the same location in the virtme-ng guest.
## One-shot loader or self-test
```bash
"$RUNNER" --cwd "$LESSON" -- ./loader --self-test
```
Prefer a self-test that loads the BPF object, exercises a deterministic input, checks the map or return value, and detaches before exiting.
## Long-running tracer plus a trigger
The loader, trigger, signal, and output assertion must all occur inside the same guest:
```bash
"$RUNNER" --cwd "$LESSON" --timeout 90 -- bash -lc '
set -euo pipefail
out=$(mktemp)
./tracer >"$out" 2>&1 &
pid=$!
trap '\''kill -INT "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true'\'' EXIT
sleep 1
true # replace with the deterministic traced event
sleep 1
kill -INT "$pid"
wait "$pid" || test $? -eq 130
trap - EXIT
grep -F "expected event" "$out"
'
```
Keep the host timeout longer than the guest-side lifecycle. Avoid an unbounded `timeout ./tracer` assertion because exit 124 proves only that it was killed.
## XDP or TC
For a userspace-backed interface and outbound guest traffic:
```bash
"$RUNNER" --cwd "$LESSON" --network user -- bash -lc '
set -euo pipefail
ip -brief link
./loader --interface eth0 &
pid=$!
trap '\''kill -INT "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true'\'' EXIT
ping -c 1 -W 2 10.0.2.2
# Assert counters or emitted events here.
'
```
Use `--network loop` for guest-local network testing. Prefer `BPF_PROG_TEST_RUN` when the lesson is about program behavior rather than attachment mechanics.
## sched_ext, LSM, and cgroup programs
- Verify the exact config symbol in the printed kernel config before booting, for example `CONFIG_SCHED_CLASS_EXT=y` or `CONFIG_BPF_LSM=y`.
- Keep sched_ext registration bounded and assert that the scheduler exits or unregisters cleanly.
- Create test cgroups only inside the guest's cgroup2 mount.
- Test LSM enforcement on guest-local temporary files or processes, never on host resources mounted read-write.
## Returning generated output
The tutorial directory is read-only by default. Create a dedicated output directory and expose only it when a test must retain logs or generated files:
```bash
mkdir -p "$LESSON/test-output"
"$RUNNER" --cwd "$LESSON" --rwdir "$LESSON/test-output" -- \
./loader --output "$LESSON/test-output/smoke.json"
```
Guest root can modify any `--rwdir` path. Never expose a repository root, `.git`, credentials, task histories, or agent traces just for convenience.
## Interpreting failures
- **Preflight fails:** repair KVM/tool/artifact availability; do not fall back to the host kernel.
- **Guest kernel mismatch:** the image and build metadata are inconsistent; rebuild or repair the benchmark artifact separately.
- **`bpftool` asks for `linux-tools-<guest-version>`:** the Ubuntu `/usr/sbin/bpftool` wrapper was selected. On this machine, use the real binary at `/usr/local/sbin/bpftool` after confirming it with `--version`.
- **Unknown helper, attach type, or kfunc:** compare lesson requirements with the built kernel release/config/BTF. Report a kernel gap rather than changing the host test boundary.
- **Permission error inside guest:** confirm the command is running as guest root and that bpffs/debugfs/tracefs/cgroup2 mounted. Do not add host `sudo`.
- **Read-only filesystem:** write under `/tmp`, or explicitly expose a narrow output directory with `--rwdir`.
- **Timeout 124:** inspect loader cleanup and the trigger. A timeout is not a passing tracer test.
- **Verifier rejection:** preserve the complete verifier log and distinguish an intentional negative test from a compatibility regression.

View File

@@ -0,0 +1,320 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
run-in-kvm.sh --check
run-in-kvm.sh [options] -- COMMAND [ARG...]
Build tutorial artifacts on the host, then use this wrapper only for the
runtime command that must execute in the bpf-benchmark KVM kernel.
Options:
--check Validate KVM, vng, and the benchmark kernel, then exit.
--cwd PATH Guest working directory (default: current directory).
--cpus N Guest vCPU count (default: 2).
--memory SIZE Guest RAM accepted by vng/QEMU (default: 4G).
--timeout SECONDS Host-side wall-clock limit (default: 120).
--network MODE none, user, or loop (default: none).
--rwdir PATH Explicit host path writable by guest root; repeatable.
--append ARGS Additional kernel command-line string; repeatable.
--dry-run Print vng's QEMU command without booting.
--verbose Show the guest boot console and vng details.
-h, --help Show this help.
Environment:
BPF_BENCHMARK_ROOT Default: bpf-benchmark next to this repository
EOF
}
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}
require_positive_integer() {
local name=$1 value=$2
[[ $value =~ ^[1-9][0-9]*$ ]] || die "$name must be a positive integer: $value"
}
quote_for_posix_shell() {
local value=$1
value=${value//\'/\'\\\'\'}
REPLY="'$value'"
}
benchmark_root=${BPF_BENCHMARK_ROOT:-}
cwd=$PWD
cpus=2
memory=4G
wall_timeout=120
network=none
check_only=false
dry_run=false
verbose=false
declare -a rwdirs=()
declare -a extra_append=()
while (($#)); do
case $1 in
--check)
check_only=true
shift
;;
--cwd)
(($# >= 2)) || die "--cwd requires a path"
cwd=$2
shift 2
;;
--cpus)
(($# >= 2)) || die "--cpus requires a value"
cpus=$2
shift 2
;;
--memory)
(($# >= 2)) || die "--memory requires a value"
memory=$2
shift 2
;;
--timeout)
(($# >= 2)) || die "--timeout requires seconds"
wall_timeout=$2
shift 2
;;
--network)
(($# >= 2)) || die "--network requires a mode"
network=$2
shift 2
;;
--rwdir)
(($# >= 2)) || die "--rwdir requires a path"
rwdirs+=("$2")
shift 2
;;
--append)
(($# >= 2)) || die "--append requires a kernel command-line string"
extra_append+=("$2")
shift 2
;;
--dry-run)
dry_run=true
shift
;;
--verbose)
verbose=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
*)
die "unknown option before --: $1"
;;
esac
done
require_positive_integer "--cpus" "$cpus"
require_positive_integer "--timeout" "$wall_timeout"
[[ $memory =~ ^[1-9][0-9]*([KkMmGgTt])?$ ]] || die "invalid --memory value: $memory"
case $network in
none|user|loop) ;;
*) die "--network must be none, user, or loop: $network" ;;
esac
for command_name in file flock git mktemp realpath sha256sum timeout vng; do
require_command "$command_name"
done
if [[ -z $benchmark_root ]]; then
skill_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
tutorial_root=$(git -C "$skill_dir" rev-parse --show-toplevel)
benchmark_root=$(dirname -- "$tutorial_root")/bpf-benchmark
fi
[[ $(uname -m) == x86_64 ]] || die "the reused artifact is x86_64, host is $(uname -m)"
[[ -c /dev/kvm && -r /dev/kvm && -w /dev/kvm ]] || \
die "/dev/kvm is not available with read/write permission; refusing a TCG fallback"
benchmark_root=$(realpath -e -- "$benchmark_root")
kernel_source=$benchmark_root/vendor/linux-framework
kernel_build=$benchmark_root/vendor/build/x86/linux
kernel_image=$kernel_build/arch/x86/boot/bzImage
kernel_config=$kernel_build/.config
kernel_release_file=$kernel_build/include/config/kernel.release
[[ -d $kernel_source ]] || die "kernel source directory is missing: $kernel_source"
[[ -s $kernel_image ]] || die "built benchmark kernel is missing: $kernel_image"
[[ -r $kernel_config ]] || die "kernel config is missing: $kernel_config"
[[ -r $kernel_release_file ]] || die "kernel release file is missing: $kernel_release_file"
file -- "$kernel_image" | grep -q 'Linux kernel x86 boot executable bzImage' || \
die "artifact is not an x86 bzImage: $kernel_image"
git -C "$kernel_source" rev-parse --verify HEAD >/dev/null 2>&1 || \
die "kernel source Git worktree is invalid: $kernel_source"
required_config=(
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_CGROUP_BPF=y
CONFIG_KPROBES=y
CONFIG_UPROBES=y
CONFIG_FTRACE=y
CONFIG_DEBUG_INFO_BTF=y
)
for symbol in "${required_config[@]}"; do
grep -qxF "$symbol" "$kernel_config" || die "required kernel config is absent: $symbol"
done
kernel_release=$(<"$kernel_release_file")
[[ -n $kernel_release ]] || die "kernel release file is empty: $kernel_release_file"
kernel_commit=$(git -C "$kernel_source" rev-parse HEAD)
kernel_subject=$(git -C "$kernel_source" show -s --format=%s HEAD)
kernel_dirty=no
if [[ -n $(git -C "$kernel_source" status --porcelain --untracked-files=normal) ]]; then
kernel_dirty=yes
fi
kernel_sha256=$(sha256sum "$kernel_image" | awk '{print $1}')
config_sha256=$(sha256sum "$kernel_config" | awk '{print $1}')
vng_version=$(vng --version | head -n 1)
printf '%s\n' \
'KVM preflight: OK' \
"benchmark_root=$benchmark_root" \
"kernel_image=$kernel_image" \
"kernel_release=$kernel_release" \
"kernel_sha256=$kernel_sha256" \
"kernel_config_sha256=$config_sha256" \
"kernel_source_commit=$kernel_commit" \
"kernel_source_dirty=$kernel_dirty" \
"kernel_source_subject=$kernel_subject" \
"vng_version=$vng_version"
if $check_only; then
(($# == 0)) || die "--check does not accept a guest command"
exit 0
fi
(($# > 0)) || die "missing guest command; place it after --"
[[ -d $cwd ]] || die "guest working directory does not exist: $cwd"
cwd=$(realpath -e -- "$cwd")
declare -a normalized_rwdirs=()
cwd_is_rw=false
for path in "${rwdirs[@]}"; do
[[ -d $path ]] || die "--rwdir is not a directory: $path"
path=$(realpath -e -- "$path")
normalized_rwdirs+=("$path")
if [[ $path == "$cwd" ]]; then
cwd_is_rw=true
fi
done
quote_for_posix_shell "$kernel_release"
quoted_release=$REPLY
quote_for_posix_shell "$cwd"
quoted_cwd=$REPLY
quoted_command=
for argument in "$@"; do
quote_for_posix_shell "$argument"
quoted_command+="$REPLY "
done
guest_script=$'#!/bin/sh\nset -eu\n'
guest_script+=$'mount_if_needed() {\n'
guest_script+=$' source=$1 type=$2 target=$3\n'
guest_script+=$' mkdir -p -- "$target"\n'
guest_script+=$' grep -qs " $target " /proc/mounts || mount -t "$type" "$source" "$target"\n'
guest_script+=$'}\n'
guest_script+=$'mount_if_needed bpf bpf /sys/fs/bpf\n'
guest_script+=$'mount_if_needed debugfs debugfs /sys/kernel/debug\n'
guest_script+=$'mount_if_needed tracefs tracefs /sys/kernel/tracing\n'
guest_script+=$'mount_if_needed none cgroup2 /sys/fs/cgroup\n'
guest_script+=$'ulimit -l unlimited\n'
guest_script+="expected_release=$quoted_release"$'\n'
guest_script+=$'actual_release=$(uname -r)\n'
guest_script+=$'if [ "$actual_release" != "$expected_release" ]; then\n'
guest_script+=$' echo "ERROR: guest kernel mismatch: expected=$expected_release actual=$actual_release" >&2\n'
guest_script+=$' exit 1\n'
guest_script+=$'fi\n'
guest_script+=$'printf "guest_kernel=%s\\nguest_identity=%s\\n" "$actual_release" "$(id)"\n'
guest_script+="cd -- $quoted_cwd"$'\n'
guest_script+="exec $quoted_command"$'\n'
lock_file=${TMPDIR:-/tmp}/test-bpf-tutorial-kvm.lock
exec {lock_fd}>"$lock_file"
flock -w 5 "$lock_fd" || die "another tutorial KVM run holds $lock_file"
host_script_dir=$(mktemp -d /tmp/test-bpf-tutorial-kvm.XXXXXX)
host_script_path=$host_script_dir/guest-command.sh
guest_script_dir=/tmp/${host_script_dir##*/}
guest_script_path=$guest_script_dir/guest-command.sh
cleanup() {
rm -f -- "$host_script_path"
rmdir -- "$host_script_dir" 2>/dev/null || true
}
trap cleanup EXIT
printf '%s' "$guest_script" >"$host_script_path"
chmod 0555 "$host_script_path"
declare -a vng_command=(
vng
--run "$kernel_image"
--cwd "$cwd"
--disable-monitor
--cpus "$cpus"
--memory "$memory"
--overlay-rwdir /tmp
'--qemu-opts=-machine accel=kvm'
--append 'loglevel=4 panic=30 oops=panic'
--rodir "$guest_script_dir=$host_script_dir"
--exec "$guest_script_path"
)
if ! $cwd_is_rw; then
vng_command+=(--rodir "$cwd")
fi
for path in "${normalized_rwdirs[@]}"; do
vng_command+=(--rwdir "$path")
done
if [[ $network != none ]]; then
vng_command+=(--network "$network")
fi
for args in "${extra_append[@]}"; do
vng_command+=(--append "$args")
done
if $verbose; then
vng_command+=(--verbose)
else
vng_command+=(--quiet)
fi
if $dry_run; then
vng_command+=(--dry-run)
fi
printf 'guest_cwd=%s\n' "$cwd"
printf 'guest_network=%s\n' "$network"
printf 'guest_command='
printf '%q ' "$@"
printf '\n'
if ((${#normalized_rwdirs[@]})); then
printf 'WARNING: guest root has host write access to:' >&2
printf ' %q' "${normalized_rwdirs[@]}" >&2
printf '\n' >&2
fi
if $dry_run; then
"${vng_command[@]}"
else
timeout --foreground --signal=TERM --kill-after=10s "${wall_timeout}s" \
"${vng_command[@]}"
fi

View File

@@ -0,0 +1,71 @@
---
name: write-bpf-production-tutorial
description: Create or revise bilingual bpf-developer-tutorial lessons as small, production-realistic eBPF tools with host builds, benchmark-kernel KVM tests, reproducible evidence, and prose authored only by the exact Claude model claude-opus-4-6[1m]. Use when proposing, implementing, documenting, reviewing, or preparing a PR for a new eBPF tutorial, especially when deciding between numbered lessons, features, networking, security, or xpu/gpu placement.
---
# Write BPF Production Tutorials
Build a useful tool around a kernel capability. Do not publish a wrapper around an upstream selftest or a feature-only API tour.
## Enforce the author boundary
- Let the implementation agent design, code, test, and collect technical evidence.
- Require Claude Code with the exact model string `claude-opus-4-6[1m]` to author or revise `README.md` and `README.zh.md`.
- Invoke Claude only through `scripts/run-claude-writer.sh`. Do not expose a model override and do not configure a fallback model.
- Do not write tutorial prose with Codex or another model. Send factual corrections back through the same wrapper instead of rewriting paragraphs manually.
- Permit other models to review code or prose, but never to author replacement tutorial text.
- Preserve every wrapper trace and manifest under `~/.local/state/bpf-tutorial-writer/runs/`. Never delete, truncate, rewrite, or clean that directory.
## Establish the tutorial contract
Before coding, write one sentence naming:
1. the production failure or operational need;
2. the user-visible command and result;
3. the kernel capability that makes it possible;
4. the positive and negative paths that prove it works.
Reject proposals whose only result is “the program loaded,” “the helper returned success,” or “the upstream selftest passed.”
Read [references/tutorial-acceptance.md](references/tutorial-acceptance.md) before selecting scope or placement. Read [references/writing-guide.md](references/writing-guide.md) before invoking the writer.
## Implement and verify
1. Read the repository contract, nearby lesson code, Makefiles, documentation generators, and CI workflows.
2. Keep one production scenario per PR. Start from the remote default branch for every PR.
3. Follow existing libbpf and CO-RE patterns. Add a deterministic workload, observable result, cleanup, and a relevant failure path.
4. Build on the host without loading BPF there.
5. Use `$test-bpf-tutorial-kvm` for every load, attach, and runtime test. Record guest kernel provenance and real output.
6. Add a compile CI step. Add a runtime feature probe or an explicit skip when ordinary CI lacks the required kernel.
7. Run documentation generators and link/unit tests.
## Delegate the prose
Create a task file outside the lesson with:
- the exact README paths Claude may edit;
- the production scenario and intended audience;
- the final code paths and CLI;
- verified build and guest commands;
- real captured output with private paths and identities removed;
- minimum kernel, libbpf, config, privilege, and hardware requirements;
- primary upstream references;
- facts or claims Claude must not add.
Then run:
```bash
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
```
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.
## Prepare the PR
- Include only one lesson and the minimal generated index/compatibility changes.
- Explain the production problem before the kernel feature in the PR body.
- Report the host build, KVM positive test, negative/cleanup test, guest kernel provenance, documentation tests, and model-trace manifest.
- Follow the repository's review, Copilot, and monitored CI gates. Do not merge unless the user asks.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "BPF Production Tutorial Writer"
short_description: "Build tested eBPF lessons with Claude Opus prose"
default_prompt: "Use $write-bpf-production-tutorial to turn this production eBPF scenario into a tested bilingual tutorial."

View File

@@ -0,0 +1,38 @@
# Tutorial acceptance and placement
## Acceptance gate
A numbered lesson must ship a small tool that a developer could plausibly run while operating or debugging a real system. Require all of the following:
- A concrete actor and problem, such as an SRE diagnosing queueing, a security agent quarantining a live connection, or an accelerator operator finding leaked shared buffers.
- A user-space CLI with useful output, filters, and actionable errors.
- A deterministic workload or fixture that recreates the scenario.
- An assertion connecting the workload to BPF output or behavior.
- A relevant negative path, unsupported-feature probe, or policy miss.
- Automatic cleanup of links, qdiscs, cgroups, maps, sockets, and test processes.
- A documented minimum kernel and required config or hardware.
- Host compile coverage and guest runtime evidence.
An upstream selftest may guide correctness, but do not copy its test harness as the lesson's product story.
## Placement
- Use `src/<number>-<name>/` for an end-to-end tracing, networking, security, scheduling, or operations tool.
- Use `src/features/<name>/` only when the reusable kernel primitive itself is the teaching goal and no honest production tool can be kept small.
- Use `src/xpu/<name>/` when the workload is specifically about GPU, NPU, DMA-BUF, accelerator drivers, or accelerator profiling. Do not use xpu merely because the host has a GPU.
- Keep cross-cutting deployment mechanisms such as program signing under `src/features/` unless the lesson implements a complete operational policy loader.
## Scenario guidance
- `fsession`: build function-latency/SLO diagnosis, not a cookie unit test.
- BPF qdisc: build a two-class latency-sensitive packet scheduler with queue/drop counters, not a bare `Qdisc_ops` registration test.
- Socket destroy: build a live connection quarantine CLI with an allowlisted control case.
- File dynptr plus task work: build deferred executable/header inspection with a direct-read failure demonstration.
- DMA-BUF iterator: build an accelerator/shared-buffer inventory and leak check under `src/xpu/`; avoid claiming per-process ownership without fd correlation.
- XDP multi-buffer: build a fragmented/jumbo-packet L4 parser with a truncated-packet failure case.
- Signed programs: build a signed policy deployment workflow with an untrusted/tampered rejection path.
- BPF crypto: defer unless the example has a defensible operational use and clear key-lifecycle boundaries.
## PR boundary
Keep each scenario in its own branch and PR. Generate README/SUMMARY/compatibility changes on that branch only. Rebase the next branch on current upstream `main`, not on another unmerged tutorial branch.

View File

@@ -0,0 +1,40 @@
# BPF tutorial writing guide
Write for developers who want to reproduce a small, useful eBPF tool. Use oral, direct English and natural Chinese. Prefer short, connected sentences over academic prose or marketing claims.
## Required narrative
1. Open with a production incident or operational task the reader recognizes. Explain the consequence and the signal the tool will expose or control.
2. Show the command and verified result early. Do not begin with a generic definition of eBPF.
3. Explain the end-to-end flow from workload to hook, maps or kfuncs, user-space processing, and final output.
4. Present complete source when it stays readable. Otherwise use copyable 15–40 line excerpts and link to the complete lesson.
5. Explain only the kernel mechanisms needed to understand the tool. State minimum kernel/libbpf versions, configs, privileges, limitations, and cleanup.
6. Provide exact build and run commands. Use only output captured from a real run. Never invent benchmark numbers, events, PIDs, paths, or errors.
7. Include the positive path and one meaningful miss, failure, or unsupported path.
8. Close with what the operator can now learn or control, plus the repository and tutorial-site links.
## Style rules
- Use a specific title such as `# eBPF Tutorial by Example: Quarantine an Established TCP Connection`.
- Build a progressive explanation around one running example instead of listing features.
- Use active voice and define technical terms through the example.
- Avoid throat clearing, repeated summaries, vague claims, paper-like language, em dashes, and stacked one-line paragraphs.
- Do not describe a demo as production-ready. State scope and missing hardening plainly.
- Preserve exact code, command, version, and output facts supplied in the task file.
- Link kernel claims to primary sources such as kernel commits, documentation, or selftests.
## Bilingual rules
- Keep English and Chinese headings, examples, commands, limitations, and claims aligned.
- Translate naturally rather than sentence by sentence.
- Keep code and verified console output identical.
- Do not use Chinese em dashes (`——`) or spaced English em dashes (` — `).
## Required ending links
Include direct links to:
- <https://github.com/eunomia-bpf/bpf-developer-tutorial>
- <https://eunomia.dev/tutorials/>
Do not add frontmatter unless nearby lessons use it.

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -euo pipefail
readonly MODEL='claude-opus-4-6[1m]'
readonly SKILL_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
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
}
repo=
task=
while (($#)); do
case "$1" in
--repo)
repo=${2:-}
shift 2
;;
--task)
task=${2:-}
shift 2
;;
--model|--fallback-model)
echo "model overrides and fallbacks are forbidden; required model: $MODEL" >&2
exit 2
;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 2
;;
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 python3 >/dev/null || { echo "python3 is required" >&2; exit 127; }
state_root=${XDG_STATE_HOME:-$HOME/.local/state}/bpf-tutorial-writer/runs
mkdir -p "$state_root"
chmod 700 "$state_root"
run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
run_dir="$state_root/$run_id"
mkdir -m 700 "$run_dir"
trace="$run_dir/claude-trace.jsonl"
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}')
prompt=$(printf '%s\n\n%s\n\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.' \
"$(<"$GUIDE")" \
"$(<"$task")")
set +e
(
cd "$repo"
claude -p \
--model "$MODEL" \
--effort high \
--permission-mode acceptEdits \
--allowedTools Read Edit Write \
--output-format stream-json \
--verbose \
"$prompt"
) >"$trace" 2>&1
claude_status=$?
set -e
trace_sha=$(sha256sum "$trace" | awk '{print $1}')
head_after=$(git -C "$repo" rev-parse HEAD)
status_after=$(git -C "$repo" status --porcelain=v1)
MODEL_VALUE="$MODEL" \
REPO_VALUE="$repo" \
TASK_VALUE="$task" \
TASK_SHA_VALUE="$task_sha" \
TRACE_VALUE="$trace" \
TRACE_SHA_VALUE="$trace_sha" \
CLAUDE_STATUS_VALUE="$claude_status" \
HEAD_BEFORE_VALUE="$head_before" \
HEAD_AFTER_VALUE="$head_after" \
STATUS_BEFORE_VALUE="$status_before" \
STATUS_AFTER_VALUE="$status_after" \
python3 - "$manifest" <<'PY'
import json
import os
import pathlib
path = pathlib.Path(__import__("sys").argv[1])
path.write_text(json.dumps({
"model": os.environ["MODEL_VALUE"],
"repo": os.environ["REPO_VALUE"],
"task": os.environ["TASK_VALUE"],
"task_sha256": os.environ["TASK_SHA_VALUE"],
"trace": os.environ["TRACE_VALUE"],
"trace_sha256": os.environ["TRACE_SHA_VALUE"],
"claude_exit_status": int(os.environ["CLAUDE_STATUS_VALUE"]),
"head_before": os.environ["HEAD_BEFORE_VALUE"],
"head_after": os.environ["HEAD_AFTER_VALUE"],
"status_before": os.environ["STATUS_BEFORE_VALUE"],
"status_after": os.environ["STATUS_AFTER_VALUE"],
}, indent=2, sort_keys=True) + "\n", encoding="utf-8")
PY
chmod 600 "$trace" "$manifest"
if ((claude_status != 0)); then
echo "Claude exited with status $claude_status; preserved trace: $trace" >&2
exit "$claude_status"
fi
python3 "$VERIFY" --trace "$trace" --model "$MODEL" --require-write
echo "manifest=$manifest"

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Verify that a persisted Claude trace used the required authoring model."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
def load_events(path: Path) -> list[dict[str, Any]]:
raw = path.read_text(encoding="utf-8")
try:
value = json.loads(raw)
except json.JSONDecodeError:
events = []
for number, line in enumerate(raw.splitlines(), 1):
if not line.strip():
continue
try:
value = json.loads(line)
except json.JSONDecodeError as error:
raise ValueError(f"invalid JSON on trace line {number}: {error}") from error
events.extend(value if isinstance(value, list) else [value])
return [event for event in events if isinstance(event, dict)]
values = value if isinstance(value, list) else [value]
return [event for event in values if isinstance(event, dict)]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--trace", required=True, type=Path)
parser.add_argument("--model", required=True)
parser.add_argument("--require-write", action="store_true")
args = parser.parse_args()
events = load_events(args.trace)
init_models = {
event.get("model")
for event in events
if event.get("type") == "system" and event.get("subtype") == "init"
}
if init_models != {args.model}:
raise SystemExit(f"expected init model {args.model!r}, found {sorted(init_models)!r}")
base_model = args.model.split("[", 1)[0]
authored_text = 0
write_calls = 0
assistant_models = set()
for event in events:
if event.get("type") != "assistant":
continue
message = event.get("message") or {}
model = message.get("model")
content = message.get("content") or []
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
)
if not authored_text:
raise SystemExit("trace contains no authored assistant text")
if assistant_models != {base_model}:
raise SystemExit(
f"expected authored text from {base_model!r}, found {sorted(assistant_models)!r}"
)
if args.require_write and write_calls == 0:
raise SystemExit("trace contains no Claude Edit or Write call")
results = [event for event in events if event.get("type") == "result"]
if not results:
raise SystemExit("trace contains no terminal result")
result = results[-1]
if result.get("is_error"):
raise SystemExit(f"Claude run failed: {result.get('subtype', 'unknown error')}")
usage = result.get("modelUsage") or {}
exact_usage = usage.get(args.model) or {}
if exact_usage.get("outputTokens", 0) <= 0:
raise SystemExit(f"terminal usage does not prove output from {args.model}")
print(
json.dumps(
{
"model": args.model,
"authored_text_events": authored_text,
"write_calls": write_calls,
"trace": str(args.trace),
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())