Skip to content
17 changes: 8 additions & 9 deletions .agents/skills/verify-libghostty-vt-node/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@ S=.agents/skills/verify-libghostty-vt-node/scripts
## Launch

```sh
$S/launch.sh # makes a new RUN_ROOT with mktemp
$S/launch.sh "$RUN_ROOT" # or uses a directory that you name, outside the checkout
$S/launch.sh # makes a new RUN_ROOT with mktemp in ${TMPDIR:-/tmp}
```

`launch.sh` runs in the checkout: `npm ci` (when `node_modules/` was not built from the current `package-lock.json`), `npm run build:libghostty` (when `vendor/libghostty-vt/` was not built from the current `scripts/build-libghostty-vt.sh` and `GHOSTTY_*` / `LIBGHOSTTY_VT_*` variables), `npm run build` from an empty `dist/`, `npm run build:prebuild`, and `npm pack`. A stamp file with the hash of those inputs records each reuse decision, and `RUN_ROOT/instance/launch.log` lists the steps that ran. It records the commit and tracked diff before the build, and it fails if they change before the end. Then it installs the tarball into `RUN_ROOT/instance/consumer/` with `npm install`. It is ready when the last line is `READY RUN_ROOT=<dir>`. Set the shell variable `RUN_ROOT` to that path. It can differ from the path you gave, because `launch.sh` resolves symlinks. A warm launch takes about 15 seconds. The first one also builds libghostty-vt.
`launch.sh` runs in the checkout: `npm ci` (when `node_modules/` was not built from the current `package-lock.json`), `npm run build:libghostty` (when `vendor/libghostty-vt/` was not built from the current `scripts/build-libghostty-vt.sh` and `GHOSTTY_*` / `LIBGHOSTTY_VT_*` variables), `npm run build` from an empty `dist/`, `npm run build:prebuild`, and `npm pack`. A stamp file with the hash of those inputs records each reuse decision, and `RUN_ROOT/instance/launch.log` lists the steps that ran. It records the commit and tracked diff before the build, and it fails if they change before the end. Then it installs the tarball into `RUN_ROOT/instance/consumer/` with `npm install`. It is ready when the last line is `READY RUN_ROOT=<dir>`. Set the shell variable `RUN_ROOT` to that path. A warm launch takes about 15 seconds. The first one also builds libghostty-vt.

A `RUN_ROOT` that you name must be a real directory (not a symlink) that only you can write. Every directory above it must be owned by you or root, and group or others must not be able to write it, unless it has the sticky bit, like `/tmp`. `launch.sh` creates a missing one with mode `700`. It refuses anything else, because `npm install` runs package scripts from the tarball in `RUN_ROOT`.
`launch.sh` takes no arguments. To put `RUN_ROOT` somewhere else, set `TMPDIR` to a directory outside the checkout. `npm install` runs package scripts from the tarball in `RUN_ROOT`, and the compiler writes temp files to `TMPDIR`, so `launch.sh` exits `2` when another account can change `TMPDIR`: it, or a directory above it, is owned by another account (root excepted) or writable by group or others without the sticky bit that `/tmp` has. `launch.sh` builds and links its own libghostty-vt in `vendor/libghostty-vt/`. It exits `2` when `LIBGHOSTTY_VT_PREFIX`, `LIBGHOSTTY_VT_INCLUDE_DIR`, `LIBGHOSTTY_VT_STATIC_LIB`, or `LIBGHOSTTY_VT_LIB` is set: unset them.

`launch.sh` writes `build/`, `dist/`, and `prebuilds/` in the checkout, so only one launch can run per checkout. A second launch exits `2` and names the lock directory `vendor/.verify-launch.lock`. After launch, each `RUN_ROOT` is independent: many runs can drive their own instances at the same time.

Expand All @@ -46,10 +45,10 @@ $S/doctor.sh "$RUN_ROOT"
It is read-only. Exit `0` means that the instance is worth driving. Each line starts with `ok`, `WARN`, or `FAIL`. It checks that:

1. `instance.json` exists, `launch.sh` made it, and it names this checkout.
2. The checkout has the same `HEAD` and the same tracked diff as at launch. Otherwise it prints `STALE`: run `cleanup.sh`, then `launch.sh` with a new `RUN_ROOT`.
2. The checkout has the same `HEAD` and the same tracked diff as at launch. Otherwise it prints `STALE`: run `cleanup.sh`, then `launch.sh` again.
3. `node` on `PATH` is the version that launch used. It warns when that is not the `mise.toml` pin.
4. The package is a real copy in the consumer's `node_modules`, not a symlink to the checkout.
5. `node-gyp-build` picks the packed prebuild (`prebuilds/<platform>-<arch>/@coder+libghostty-vt-node.node`).
5. `node-gyp-build` picks the packed prebuild (`prebuilds/<platform>-<arch>/@coder+libghostty-vt-node.node`), and its sha256 is the `addonSha256` in `instance.json`.
6. The bare import resolves inside the consumer, and `getNativeInfo().packageVersion` matches `package.json`.

Run it first, and again whenever a result looks wrong.
Expand All @@ -70,13 +69,13 @@ t.dispose();
JS
```

Put node flags after the name, for example `$S/drive.sh "$RUN_ROOT" lifecycle-gc --expose-gc`. `drive.sh` prints the journey's stdout, then `--- exit N, evidence in <dir>` on stderr, and it exits with the journey's code. When stdout and stderr share one stream, the exit line can show up out of order: read `exit-code.txt` for the verdict. A crash gives `128 + signal`: `134` for SIGABRT, `139` for SIGSEGV.
Put node flags after the name, for example `$S/drive.sh "$RUN_ROOT" lifecycle-gc --expose-gc`. `drive.sh` prints the journey's stdout, then `--- exit N, evidence in <dir>` on stderr, and it exits with the journey's code. When stdout and stderr share one stream, the exit line can show up out of order: read `exit-code.txt` for the verdict. A crash gives `128 + signal`: `134` for SIGABRT, `139` for SIGSEGV. When `drive.sh` cannot save the journey or `command.txt`, it exits `2` before node runs, and the journey has no `exit-code.txt`.

The map in [`features/README.md`](features/README.md) has a literal journey for each sub-feature. Read the index, then use the matching feature file.

## Evidence

Each journey writes `RUN_ROOT/evidence/<name>/`: `journey.mjs`, `command.txt` (directory, node flags, time, node version), `stdout.txt`, `stderr.txt`, and `exit-code.txt`. `RUN_ROOT/evidence/instance.json` names the commit, the tracked-diff hash, and the sha256 of the tarball, of the loaded addon, and of the libghostty-vt static library with the hash of its build inputs. One `RUN_ROOT` holds the evidence of one launch: `launch.sh` refuses a `RUN_ROOT` whose `evidence/` is not empty. `drive.sh` refuses to overwrite a journey's directory.
Each journey writes `RUN_ROOT/evidence/<name>/`: `journey.mjs`, `command.txt` (directory, node flags, time, node version), `stdout.txt`, `stderr.txt`, and `exit-code.txt`. `RUN_ROOT/evidence/instance.json` names the commit, the tracked-diff hash, and the sha256 of the tarball, of the loaded addon, and of the libghostty-vt static library with the hash of its build inputs. One `RUN_ROOT` holds the evidence of one launch, because each launch makes a new `RUN_ROOT`. `drive.sh` refuses to overwrite a journey's directory.

Proof standards:

Expand All @@ -98,4 +97,4 @@ It removes `RUN_ROOT/instance/` (the tarball, the consumer project, and the `no-

- `launch.sh` packs whatever is in `prebuilds/`, including directories for other platforms from an earlier `mise run assemble-prebuilds`. `doctor.sh` checks that this platform's prebuild is the one that loads.
- `doctor.sh` hashes only tracked changes. A new untracked source file does not make the instance `STALE`. Relaunch after you add one.
- `RUN_ROOT` must be outside the checkout, so the instance never shows up in `git status` and no import can resolve to the checkout's own files. `launch.sh` refuses a `RUN_ROOT` inside it.
- `RUN_ROOT` must be outside the checkout, so the instance never shows up in `git status` and no import can resolve to the checkout's own files. `launch.sh` exits `2` when `TMPDIR` is inside it.
10 changes: 8 additions & 2 deletions .agents/skills/verify-libghostty-vt-node/scripts/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fail=0
ok() { echo "ok $*"; }
bad() { echo "FAIL $*"; fail=1; }
warn() { echo "WARN $*"; }
sha_stdin() { if command -v sha256sum >/dev/null; then sha256sum; else shasum -a 256; fi | cut -d' ' -f1; }

if [[ ! -f "$INSTANCE/instance.json" ]]; then
bad "no $INSTANCE/instance.json: run launch.sh first"
Expand All @@ -26,11 +27,11 @@ field() { node -e 'const i=require(process.argv[1]); process.stdout.write(String
[[ "$(field checkout)" == "$ROOT" ]] && ok "built from this checkout" || bad "built from $(field checkout), not $ROOT"

head_now="$(git -C "$ROOT" rev-parse HEAD)"
diff_now="$(git -C "$ROOT" diff HEAD | { if command -v sha256sum >/dev/null; then sha256sum; else shasum -a 256; fi; } | cut -d' ' -f1)"
diff_now="$(git -C "$ROOT" diff HEAD | sha_stdin)"
if [[ "$(field commit)" == "$head_now" && "$(field trackedDiffSha256)" == "$diff_now" ]]; then
ok "current: built from HEAD $head_now and the same uncommitted diff"
else
bad "STALE: the checkout changed after launch (HEAD or tracked diff). Run cleanup.sh, then launch.sh with a new RUN_ROOT"
bad "STALE: the checkout changed after launch (HEAD or tracked diff). Run cleanup.sh, then launch.sh again"
fi

[[ "$(field node)" == "$(node --version)" ]] && ok "node $(node --version) matches launch" \
Expand All @@ -47,6 +48,11 @@ case "$addon" in
"$pkg_dir"/prebuilds/*.node) ok "addon loads from the packed prebuild: ${addon#"$CONSUMER"/}" ;;
*) bad "node-gyp-build does not pick a packed prebuild: $addon" ;;
esac
addon_sha=""
[[ -f "$addon" ]] && addon_sha="$(sha_stdin < "$addon")"
want_sha="$(field addonSha256)"
[[ "$addon_sha" == "$want_sha" ]] && ok "addon sha256 $addon_sha matches instance.json" \
|| bad "addon sha256 is ${addon_sha:-unreadable}, instance.json has addonSha256 $want_sha: the installed addon changed after launch. Run cleanup.sh, then launch.sh again"

info="$(cd "$CONSUMER" && node --input-type=module -e '
const { fileURLToPath } = await import("node:url");
Expand Down
10 changes: 6 additions & 4 deletions .agents/skills/verify-libghostty-vt-node/scripts/drive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ OUT="$RUN_ROOT/evidence/$NAME"
[[ -f "$RUN_ROOT/instance/instance.json" ]] || { echo "error: no instance in $RUN_ROOT: run launch.sh" >&2; exit 2; }
[[ "$NAME" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || { echo "error: NAME must match [a-z0-9][a-z0-9._-]*" >&2; exit 2; }
# One mkdir reserves the name, so two runs with one NAME cannot share evidence.
mkdir -p "$RUN_ROOT/evidence" "$CONSUMER/journeys"
mkdir -p "$RUN_ROOT/evidence" "$CONSUMER/journeys" || exit 2
mkdir "$OUT" 2>/dev/null || { echo "error: $OUT exists: pick a new NAME, evidence is never overwritten" >&2; exit 2; }
cat > "$CONSUMER/journeys/$NAME.mjs"
cp "$CONSUMER/journeys/$NAME.mjs" "$OUT/journey.mjs"
echo "cd $CONSUMER && node $* journeys/$NAME.mjs # $(date -u +%Y-%m-%dT%H:%M:%SZ) $(node --version)" > "$OUT/command.txt"
# A failed write here would let node run a stale or partial journey.
{ cat > "$CONSUMER/journeys/$NAME.mjs" &&
cp "$CONSUMER/journeys/$NAME.mjs" "$OUT/journey.mjs" &&
echo "cd $CONSUMER && node $* journeys/$NAME.mjs # $(date -u +%Y-%m-%dT%H:%M:%SZ) $(node --version)" > "$OUT/command.txt"
} || { echo "error: could not save the journey and command.txt for $NAME: node did not run" >&2; exit 2; }

(cd "$CONSUMER" && node "$@" "journeys/$NAME.mjs") > "$OUT/stdout.txt" 2> "$OUT/stderr.txt"
code=$?
Expand Down
89 changes: 35 additions & 54 deletions .agents/skills/verify-libghostty-vt-node/scripts/launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,50 @@
# pack it with a prebuild for this platform, and install the tarball into a new
# consumer project, the way a user installs the package from npm.
#
# Usage: .agents/skills/verify-libghostty-vt-node/scripts/launch.sh [RUN_ROOT]
# RUN_ROOT default: a new mktemp dir. The last line is `READY RUN_ROOT=<dir>`.
# Usage: .agents/skills/verify-libghostty-vt-node/scripts/launch.sh
# Makes a new RUN_ROOT with mktemp in ${TMPDIR:-/tmp}. The last line is `READY RUN_ROOT=<dir>`.
# Makes: RUN_ROOT/instance/ (tarball, consumer project, instance.json, launch.log)
# RUN_ROOT/evidence/ (empty; drive.sh writes here, cleanup.sh keeps it)
set -euo pipefail

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ROOT="$(cd "$SKILL_DIR/../../.." && pwd -P)"
outside_checkout() {
case "$1/" in
"$ROOT"/*) echo "error: RUN_ROOT must be outside the checkout ($ROOT)." >&2; exit 2 ;;
esac
}
RUN_ROOT="${1:-}"
if [[ -z "$RUN_ROOT" ]]; then
RUN_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/verify-libghostty-vt-node.XXXXXX")"
elif [[ ! -e "$RUN_ROOT" && ! -L "$RUN_ROOT" ]]; then
parent="$(dirname -- "$RUN_ROOT")"
if [[ -d "$parent" ]]; then outside_checkout "$(cd "$parent" && pwd -P)/$(basename -- "$RUN_ROOT")"; fi
mkdir -p -- "$parent"
mkdir -m 700 -- "$RUN_ROOT"
if [[ $# -gt 0 ]]; then
echo "error: launch.sh takes no arguments. It makes a new RUN_ROOT with mktemp: set TMPDIR to choose its parent." >&2
exit 2
fi
# instance.json records vendor/libghostty-vt, so the build must link that copy.
supplied=""
for v in LIBGHOSTTY_VT_PREFIX LIBGHOSTTY_VT_INCLUDE_DIR LIBGHOSTTY_VT_STATIC_LIB LIBGHOSTTY_VT_LIB; do
[[ -z "${!v+set}" ]] || supplied+=" $v"
done
if [[ -n "$supplied" ]]; then
echo "error: launch.sh builds and links its own libghostty-vt in $ROOT/vendor/libghostty-vt. Unset${supplied} and launch again." >&2
exit 2
fi
# The build installs the tarball from RUN_ROOT with npm, which runs package
# scripts. Refuse a RUN_ROOT that another account can change or move: a
# symlink, a directory owned by someone else or writable by group or others,
# or one below any directory that another account owns (root excepted) or
# that group or others can write without the sticky bit. Check the ancestors
# of the path as given and of its physical path, and use the physical path
# that this check returns: never resolve RUN_ROOT again after it.
RUN_ROOT="$(node -e '
tmp="$(cd "${TMPDIR:-/tmp}" && pwd -P)"
case "$tmp/" in
"$ROOT"/*) echo "error: TMPDIR ($tmp) is inside the checkout ($ROOT): set it to a directory outside." >&2; exit 2 ;;
esac
# npm install runs package scripts from RUN_ROOT, and the compiler writes temp
# files to TMPDIR. Refuse a TMPDIR that another account can change: it, or a
# directory above it, is owned by another account (root excepted) or writable
# by group or others without the sticky bit. Children use the checked path.
node -e '
const fs = require("node:fs"), path = require("node:path");
const dir = process.argv[1], uid = process.getuid();
const fail = (why) => { console.error(`error: RUN_ROOT ${dir} ${why}`); process.exit(2); };
const st = fs.lstatSync(dir);
if (st.isSymbolicLink()) fail("is a symlink: pass the real path");
if (!st.isDirectory()) fail("is not a directory");
if (st.uid !== uid) fail("is not owned by you");
if (st.mode & 0o022) fail("is writable by group or others: chmod 700 it, or use a new path");
const real = fs.realpathSync(dir);
for (const start of [path.resolve(dir), real]) {
for (let up = path.dirname(start); ; up = path.dirname(up)) {
const a = fs.statSync(up);
if (a.uid !== uid && a.uid !== 0) fail(`is below ${up}, which another account owns`);
if ((a.mode & 0o022) && !(a.mode & 0o1000)) fail(`is below ${up}, which group or others can write`);
if (up === path.dirname(up)) break;
}
for (let dir = process.argv[1]; ; dir = path.dirname(dir)) {
const st = fs.statSync(dir);
const why = st.uid !== process.getuid() && st.uid !== 0 ? "another account owns"
: (st.mode & 0o022) && !(st.mode & 0o1000) ? "group or others can write" : "";
if (why) { console.error(`error: TMPDIR ${process.argv[1]} is unsafe: ${why} ${dir}`); process.exit(2); }
if (dir === path.dirname(dir)) break;
}
process.stdout.write(real);
' "$RUN_ROOT")"
outside_checkout "$RUN_ROOT"
' "$tmp"
export TMPDIR="$tmp"
RUN_ROOT="$(mktemp -d "$TMPDIR/verify-libghostty-vt-node.XXXXXX")"
INSTANCE="$RUN_ROOT/instance"
EVIDENCE="$RUN_ROOT/evidence"

if [[ -e "$INSTANCE" ]]; then
echo "error: $INSTANCE exists. Run cleanup.sh $RUN_ROOT first, then use a new RUN_ROOT." >&2
exit 2
fi
# Evidence belongs to the launch that made it: never mix two launches in one RUN_ROOT.
if [[ -d "$EVIDENCE" && -n "$(ls -A "$EVIDENCE")" ]]; then
echo "error: $EVIDENCE holds evidence from an earlier launch. Use a new RUN_ROOT." >&2
exit 2
fi

# One launch at a time per checkout: launch writes build/, dist/, and prebuilds/.
mkdir -p "$ROOT/vendor"
lock="$ROOT/vendor/.verify-launch.lock"
Expand Down Expand Up @@ -98,7 +79,7 @@ if [[ "$(cat "$deps_stamp" 2>/dev/null)" != "$deps_inputs" ]]; then
run npm ci
echo "$deps_inputs" > "$deps_stamp"
fi
lib_dir="${LIBGHOSTTY_VT_PREFIX:-$ROOT/vendor/libghostty-vt}"
lib_dir="$ROOT/vendor/libghostty-vt"
lib_stamp="$lib_dir/.verify-libghostty-vt-node.sha256"
lib_inputs="$( { cat scripts/build-libghostty-vt.sh; env | grep -E '^(GHOSTTY_|LIBGHOSTTY_VT_)' | sort || true; } | sha_stdin)"
if [[ ! -f "$lib_dir/lib/libghostty-vt.a" || "$(cat "$lib_stamp" 2>/dev/null)" != "$lib_inputs" ]]; then
Expand All @@ -120,10 +101,10 @@ JSON
(cd "$INSTANCE/consumer" && run npm install --no-audit --no-fund "$INSTANCE/$tarball")

if [[ "$(checkout_state)" != "$state_before" ]]; then
echo "error: the checkout changed during the launch (HEAD or tracked diff). Run cleanup.sh, then launch again with a new RUN_ROOT." >&2
echo "error: the checkout changed during the launch (HEAD or tracked diff). Run cleanup.sh, then launch again." >&2
exit 1
fi
static_lib="${LIBGHOSTTY_VT_STATIC_LIB:-$lib_dir/lib/libghostty-vt.a}"
static_lib="$lib_dir/lib/libghostty-vt.a"
addon="$(cd "$INSTANCE/consumer" && node -e 'process.stdout.write(require("node-gyp-build").path(process.argv[1]))' "$INSTANCE/consumer/node_modules/@coder/libghostty-vt-node")"
INSTANCE="$INSTANCE" TARBALL="$tarball" TARBALL_SHA="$(sha "$INSTANCE/$tarball")" \
LIB_INPUTS="$lib_inputs" STATIC_LIB="$static_lib" STATIC_LIB_SHA="$(sha "$static_lib")" \
Expand Down
Loading