diff --git a/.agents/skills/test-bpf-tutorial-kvm/SKILL.md b/.agents/skills/test-bpf-tutorial-kvm/SKILL.md new file mode 100644 index 0000000..5c39191 --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/SKILL.md @@ -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/" + +"$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. diff --git a/.agents/skills/test-bpf-tutorial-kvm/agents/openai.yaml b/.agents/skills/test-bpf-tutorial-kvm/agents/openai.yaml new file mode 100644 index 0000000..6f2b6f9 --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/agents/openai.yaml @@ -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." diff --git a/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md b/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md new file mode 100644 index 0000000..c001129 --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/references/tutorial-patterns.md @@ -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-`:** 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. diff --git a/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh b/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh new file mode 100755 index 0000000..fa51db8 --- /dev/null +++ b/.agents/skills/test-bpf-tutorial-kvm/scripts/run-in-kvm.sh @@ -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 diff --git a/.agents/skills/write-bpf-production-tutorial/SKILL.md b/.agents/skills/write-bpf-production-tutorial/SKILL.md new file mode 100644 index 0000000..5c7e91b --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/SKILL.md @@ -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. diff --git a/.agents/skills/write-bpf-production-tutorial/agents/openai.yaml b/.agents/skills/write-bpf-production-tutorial/agents/openai.yaml new file mode 100644 index 0000000..99e5b0f --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/agents/openai.yaml @@ -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." diff --git a/.agents/skills/write-bpf-production-tutorial/references/tutorial-acceptance.md b/.agents/skills/write-bpf-production-tutorial/references/tutorial-acceptance.md new file mode 100644 index 0000000..957eb9e --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/references/tutorial-acceptance.md @@ -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/-/` for an end-to-end tracing, networking, security, scheduling, or operations tool. +- Use `src/features//` only when the reusable kernel primitive itself is the teaching goal and no honest production tool can be kept small. +- Use `src/xpu//` 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. diff --git a/.agents/skills/write-bpf-production-tutorial/references/writing-guide.md b/.agents/skills/write-bpf-production-tutorial/references/writing-guide.md new file mode 100644 index 0000000..791d5b5 --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/references/writing-guide.md @@ -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: + +- +- + +Do not add frontmatter unless nearby lessons use it. diff --git a/.agents/skills/write-bpf-production-tutorial/scripts/run-claude-writer.sh b/.agents/skills/write-bpf-production-tutorial/scripts/run-claude-writer.sh new file mode 100755 index 0000000..24a743f --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/scripts/run-claude-writer.sh @@ -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" diff --git a/.agents/skills/write-bpf-production-tutorial/scripts/verify_claude_trace.py b/.agents/skills/write-bpf-production-tutorial/scripts/verify_claude_trace.py new file mode 100755 index 0000000..214b584 --- /dev/null +++ b/.agents/skills/write-bpf-production-tutorial/scripts/verify_claude_trace.py @@ -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()) diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file