From 95225e6a646b528382024ba1c47c83e4591e8730 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sat, 7 Nov 2026 08:44:30 +0000 Subject: [PATCH 1/4] feat(personal): install pinned Paseo plugins at startup, starting with quote-notes Add a background startup command that runs paseo-plugins-install.sh. It waits for the Paseo daemon from the separate paseo kit, enables the global plugin switch, and installs each listed plugin from a source pinned to a full commit (git) or exact version (npm). The first plugin is quote-notes (Codex Style Annotations 0.2.2) at 5ce92527c99a328b28775c7d745f324f2cc84309. The kit gains its first commands block and one egress host, github.com. See personal/docs/decisions/0003. scripts/verify section 3d checks the installer. --- README.md | 2 +- personal/AGENTS.md | 8 +- personal/README.md | 58 ++++++- personal/TROUBLESHOOTING.md | 28 +++ ...-the-first-mate-skill-as-a-snapshot-kit.md | 2 +- .../0003-install-paseo-plugins-at-startup.md | 82 +++++++++ personal/files/home/paseo-plugins-install.sh | 160 ++++++++++++++++++ personal/scripts/verify | 72 +++++++- personal/spec.yaml | 52 +++++- 9 files changed, 439 insertions(+), 25 deletions(-) create mode 100644 personal/docs/decisions/0003-install-paseo-plugins-at-startup.md create mode 100755 personal/files/home/paseo-plugins-install.sh diff --git a/README.md b/README.md index 1a70219..1259bc4 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ An agent kit is a `hybrid/v1` mixin that: | [`antigravity/`](antigravity/) | Agent | Pinned Google agy + private Gemini-to-Chat-Completions bridge | **Experimental** — real ARM64 CLI and isolated Paseo verified against synthetic provider; live gateway and x64 pending | | [`opencode-go-provider/`](opencode-go-provider/) | Provider | OpenCode Go (`openai-compatible`) facts and a 19-model neutral catalog | **Preview** — blocked on the acq credential-binding addition | | [`pi-coding-agent/`](pi-coding-agent/) | Agent | Pinned pi CLI + `~/.pi/agent/models.json` renderer | **Preview** — live verification pending acq PR 611 | -| [`personal/`](personal/) | Skill | The author's personal layer, dropped into `~/.agents/skills`: the `paseo-first-mate` skill and 27 vendored skills by [Matt Pocock](https://github.com/mattpocock/skills) (MIT, pinned commit; his code review is installed as `pocock-code-review`). See [`personal/NOTICE`](personal/NOTICE) for credits | Offline-verified; no live sandbox run | +| [`personal/`](personal/) | Skill + startup step | The author's personal layer, dropped into `~/.agents/skills`: the `paseo-first-mate` skill and 27 vendored skills by [Matt Pocock](https://github.com/mattpocock/skills) (MIT, pinned commit; his code review is installed as `pocock-code-review`). See [`personal/NOTICE`](personal/NOTICE) for credits. Also installs pinned Paseo plugins at startup (one background command, `github.com` egress; needs the paseo kit) | Offline-verified; plugin installer exercised against a real Paseo 0.12.0 daemon; no live `acq` run | **Preview status.** diff --git a/personal/AGENTS.md b/personal/AGENTS.md index db8df87..2433080 100644 --- a/personal/AGENTS.md +++ b/personal/AGENTS.md @@ -20,5 +20,9 @@ An acq `hybrid/v1` skill kit. Start with [`README.md`](README.md). - **Check your work:** `scripts/verify` (offline) and, from the repository root, `python scripts/run-kit-tests.py personal`. Both need `python3`; `shellcheck` and `acq` are used when installed. -- The kit has no `commands`, no `caps.network` and no `agent:` block on purpose. Every - payload is dropped at its final path; do not add a startup step to link or fetch. +- The kit has one `commands` entry on purpose: a background startup step that runs + `files/home/paseo-plugins-install.sh` (ADR 0003). Skills are dropped at their final path; + do not add a step to link or fetch them. It has no `agent:` block, and `caps.network` + lists `github.com` only. A new Paseo plugin is one `install_plugin ID git:owner/repo <40-hex>` + (or `npm:pkg `) line in that script; the script refuses unpinned sources and + `scripts/verify` section 3d checks it. Read the plugin's source at the pin first. diff --git a/personal/README.md b/personal/README.md index 196346e..74fc25d 100644 --- a/personal/README.md +++ b/personal/README.md @@ -5,8 +5,9 @@ that holds the author's personal layer: the skills a coding agent should have in every sandbox, applied last on top of the global kits. It is an opt-in `--kit` mixin. It installs no agent, writes no provider -configuration, runs no commands, publishes no ports, and allow-lists no -network hosts. +configuration, and publishes no ports. It runs one background startup command that +installs Paseo plugins (see [Paseo plugins](#paseo-plugins)), and allow-lists one +network host, `github.com`, for that command. ## What it ships @@ -26,9 +27,46 @@ network hosts. `~/.agents/skills` is the cross-agent skills root that the supported agents scan, so no per-agent linking is needed. -The payload is written at its final path by the kit's `files[]` records, which -is why the spec has no `commands`. Nothing can fail at startup beyond the file -drop itself. +The skills are written at their final path by the kit's `files[]` records, so they +need no command. The one startup command is the plugin installer below; it is +detached and always exits 0, so nothing it does can fail a sandbox create. + +## Paseo plugins + +At startup a background script, `~/paseo-plugins-install.sh`, installs the author's +Paseo plugins into the sandbox's Paseo daemon. It needs the separate +[`paseo` kit](https://github.com/GSA-TTS/agentic-coding-patterns/tree/main/integrations/isolation/acq-kits/paseo) applied too: that +kit starts the daemon this script talks to. Without it the script waits up to ten +minutes, logs that the daemon never answered, and installs nothing. + +What it does, in order: + +1. Takes a run lock (one installer at a time) and waits for `paseo plugin ls` to answer. +2. If the daemon's global `pluginsEnabled` switch is off, sets it with + `paseo daemon config set` under the paseo kit's `config.json` lock and runs + `paseo reload`. **Plugins are trusted, unsandboxed code**: a plugin's server code + can reach the sandbox's files, processes, credentials and network, and its client + code runs inside the Paseo app. Using this kit is the consent to turn the switch + on. +3. Installs each listed plugin that is not already installed, from a source pinned to + a full commit (`git:owner/repo` plus the SHA) or an exact npm version. It does not + use a bare registry id such as `xzthemeofsss/quote-notes`, which would install + whatever the registry points at that day. `scripts/verify` fails on an unpinned + entry. Each install is retried up to three times, waiting for the daemon in between, + because the paseo kit can restart the daemon while it pins the worktree root. + +Log: `~/.local/state/paseo/paseo-plugins.log`. Check the result with `paseo plugin ls`. +Plugins live under the daemon's home, which is not persisted past the sandbox, so +each new sandbox installs them again. + +| Plugin | Source | Pin | Notes | +| --- | --- | --- | --- | +| `quote-notes` ("Codex Style Annotations", 0.2.2) by xzthemeofsss | `git:xzthemeofsss/paseo-plugins` | `5ce92527c99a328b28775c7d745f324f2cc84309` | Client plus a one-line settings registration; no server handlers. Needs Paseo 0.11.4 or later. Disable the older `quote` plugin first if you add it. Some labels are in Chinese. | + +To add or change one, edit the `install_plugin` lines at the bottom of +`files/home/paseo-plugins-install.sh`, read the plugin's source, and run +`scripts/verify`. The reasoning is in +[`docs/decisions/0003`](docs/decisions/0003-install-paseo-plugins-at-startup.md). ## Matt Pocock's skills @@ -162,7 +200,7 @@ empty state directory, that every vendored file still matches the sha256 in its lock file, that the renamed skill is consistent everywhere, that no credential-shaped string is shipped, and `acq kit validate` when available. `shellcheck` runs when installed. There is no live sandbox check: the kit only -drops files, so a live run would exercise acq's file drop rather than this kit. +drops files and starts one plugin installer that needs the separate paseo kit, so a live run would exercise acq and that kit rather than this one. The installer itself is exercised against a real Paseo daemon in an isolated home; see ADR 0003. ## Security notes @@ -172,6 +210,14 @@ drops files, so a live run would exercise acq's file drop rather than this kit. run commands and push code. Everything is plain text you can read in this directory; review the diff before pinning a new SHA, especially after re-vendoring. +- **`github.com` is open to every agent in the sandbox.** `caps.network` is sandbox-wide, + not scoped to the installer, so any agent or plugin there can also reach github.com + (clones, gists, pushes). It is the same trust you already give `gh`. +- **Plugins are code, not text.** A listed plugin runs unsandboxed. Review a plugin's + source at the exact commit before you add or move its pin, and check what it fetches + and writes. The installer reaches only `github.com` (and whatever the daemon's own + allow-list permits); the egress allow-list is the only network control, and the + plugin runs inside it. - **Agent-writable.** The files land under `/home/agent`, which the agent owns. They are a convenience payload, not a trust anchor. diff --git a/personal/TROUBLESHOOTING.md b/personal/TROUBLESHOOTING.md index 41ade6f..608ff6d 100644 --- a/personal/TROUBLESHOOTING.md +++ b/personal/TROUBLESHOOTING.md @@ -64,3 +64,31 @@ agent owns it) and starts nothing. The kit is applied from a pinned SHA. Push the change, pin the new SHA, and create a new sandbox. Editing the files inside a running sandbox changes that sandbox only. + +## A Paseo plugin is not installed + +- **Read the log.** + + ```bash + acq exec -- cat /home/agent/.local/state/paseo/paseo-plugins.log + acq exec -- paseo plugin ls + ``` + +- **"daemon not reachable ... (is the paseo kit applied?)".** The installer talks to the + Paseo daemon that the separate `paseo` kit starts. Apply both kits. If the daemon was + slow, run `sh ~/paseo-plugins-install.sh` again; it skips what is already installed. +- **"could not lock config.json" or "could not set pluginsEnabled".** The paseo kit holds + a lock while it writes `config.json`. Run the installer again. +- **"another installer is running".** A run lock at + `~/.local/state/paseo/paseo-plugins.run.lock` is held, or was left by a killed run. + If no installer is running, remove that directory and run again. A killed run can + likewise leave `~/.paseo/.config-json.lock`, which the paseo kit also waits on. +- **A `git clone` fails.** The sandbox must allow `github.com`. This kit's `caps.network` + lists it; confirm the kit was applied and the egress policy did not drop it. Behind an + HTTPS-inspecting proxy, git needs the proxy CA in the system store; that path is untested. +- **The plugin installed but the UI does not show it.** Reload the browser tab. Client + plugins load in the app, not the daemon, and the app checks the plugin's + `requirements.paseo` range against its own version. +- **It is installed but you want a newer version.** Change the pin in + `files/home/paseo-plugins-install.sh`, run `scripts/verify`, and recreate the sandbox. + Plugins are installed at create time and are not updated in place. diff --git a/personal/docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md b/personal/docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md index 1e25821..353a077 100644 --- a/personal/docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md +++ b/personal/docs/decisions/0001-ship-the-first-mate-skill-as-a-snapshot-kit.md @@ -1,6 +1,6 @@ # Decision: ship the First Mate skill as a snapshot kit at its final path -**Status:** accepted (2026-10-29); the kit was later renamed `personal` and widened, see [0002](0002-personal-kit-and-vendored-pocock-skills.md) +**Status:** accepted (2026-10-29); the kit was later renamed `personal` and widened, see [0002](0002-personal-kit-and-vendored-pocock-skills.md). The files-only, no-commands, no-egress shape described below was relaxed by [0003](0003-install-paseo-plugins-at-startup.md) ## Context diff --git a/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md b/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md new file mode 100644 index 0000000..6464661 --- /dev/null +++ b/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md @@ -0,0 +1,82 @@ +# Decision: install Paseo plugins at sandbox startup, pinned, from the personal kit + +**Status:** accepted (2026-11-07) + +## Context + +The author uses Paseo plugins in the Paseo web UI that the separate `paseo` kit runs +in the sandbox. A new sandbox starts with the daemon's `pluginsEnabled` switch unset +(treated as off) and no plugins configured, so each plugin had to be installed by hand +every time. [0001](0001-ship-the-first-mate-skill-as-a-snapshot-kit.md) and +[0002](0002-personal-kit-and-vendored-pocock-skills.md) kept this kit to files only: no +commands and no egress. A plugin install cannot be a file drop. The daemon owns its +plugin directory and config, and `paseo plugin install` needs a running daemon. + +## Decision + +1. **One startup command, in the background.** It runs + `~/paseo-plugins-install.sh` (a dropped file, mode 0755) detached, with the same + `user: "1000"` and `background: true` shape the paseo kit uses. The script waits up + to 600 seconds for the daemon (the paseo kit npm-installs the CLI first, which can be + slow behind a proxy), takes a run lock so two copies never install at once, then + installs, retrying each plugin up to three times because the paseo kit can restart + the daemon while it pins the worktree root. It always exits 0 and logs to + `~/.local/state/paseo/paseo-plugins.log`, so it cannot fail a sandbox create. This is + the kit's first `commands` block. + +2. **The kit turns the global switch on.** If `pluginsEnabled` is not true, the script + runs `paseo daemon config set pluginsEnabled true` under the paseo kit's + `config.json` lock directory, then `paseo reload`. Plugins are trusted, unsandboxed + code: server code can reach the sandbox's files, processes, credentials and network, + and client code runs inside the app. The author gave explicit consent to enabling the + switch in every sandbox that uses this kit (2026-11-07). The blast radius is the + sandbox, which is already the security boundary. + +3. **Pin every plugin to a full commit or exact version; do not install by registry + id.** `paseo plugin install xzthemeofsss/quote-notes` asks `plugins.paseo.sh` what to + install and takes the answer of the day. The script instead uses + `git:owner/repo` with `--ref <40-hex>` (or `npm:pkg@x.y.z`), which resolves nothing + remotely beyond the repository itself. The pin is the one the registry listed when it + was reviewed. The guard is in the script itself: `install_plugin` refuses, and logs, + anything but a git source with a lowercase 40-hex commit or an npm source with an + exact `x.y.z`. `scripts/verify` section 3d checks the guard is present, that no other + line calls `paseo plugin install`, and that every call is pinned. This also removes the dependency on `plugins.paseo.sh`, + which was not reachable from the development sandbox, so the registry route could + not be tested. + +4. **Egress: `github.com`.** For `git clone` of the pinned sources. `caps.network` is + sandbox-wide, so this also lets every other agent in the sandbox reach github.com. The first plugin is + a git source. An npm-sourced plugin would need `registry.npmjs.org`, which the paseo + kit already allows; it would be added to `caps.network` here too rather than relied + on, in the commit that adds such a plugin. + +5. **Idempotent and skip-if-present.** The script skips a plugin whose id is already + configured, so a second run, a daemon restart that re-runs startup, or a snapshot that + kept the plugin does not error or re-clone. + +6. **No in-place updates.** Plugins install at create time, like provider input. To move + a pin, change the script and recreate the sandbox. + +## What was tested + +Against a real Paseo 0.12.0 daemon in an isolated `PASEO_HOME`, with a real `git clone` +of the pinned commit (the registry was not used): starting with `pluginsEnabled` unset, the script +set it, reloaded, installed `quote-notes` from `git:xzthemeofsss/paseo-plugins` at the +pinned SHA, and `paseo plugin ls` reported it `running` at that SHA. A second run +logged "already installed; skipped". With no daemon it logged a clear message after the +wait and exited 0. The guard refused a branch, a short SHA, an uppercase SHA, an npm range +and a bare registry id; a second concurrent run exited at the run lock; a TERM left no +lock behind. Not tested: a real `acq run` with the paseo kit on msb or sbx (no `acq` in +the development sandbox), the paseo kit's newer CLI pins (0.12.0 here), a daemon restart +landing mid-install, and `git clone` behind an HTTPS-inspecting proxy whose CA is not in +the system store. + +## Consequences + +- The kit now depends on the paseo kit for the plugin step to do anything. Kits cannot + declare dependencies yet, so this is documented, and the script reports a missing + daemon instead of failing. +- The kit adds one egress host and one background process that lives for a few seconds + to a few minutes after sandbox start. +- Updating a plugin is a code-review task: read the diff between pins, then bump. +- `scripts/verify` gains section 3d and also shellchecks the installer. diff --git a/personal/files/home/paseo-plugins-install.sh b/personal/files/home/paseo-plugins-install.sh new file mode 100755 index 0000000..5cb1af3 --- /dev/null +++ b/personal/files/home/paseo-plugins-install.sh @@ -0,0 +1,160 @@ +#!/bin/sh +# paseo-plugins-install.sh: install the author's Paseo plugins into the sandbox's +# Paseo daemon. Started in the background by the personal kit's startup command. +# +# Needs the separate `paseo` kit (or any image with `paseo` on PATH and a daemon). +# It waits for the daemon, turns the global plugin switch on, then installs each +# plugin listed at the bottom that is not already installed. It never fails the +# sandbox: every problem is logged to $STATE_DIR/paseo-plugins.log and it exits 0. +# +# Sources are pinned on purpose. install_plugin refuses anything but a git source +# with a full 40-hex commit or an npm source with an exact version. A plain +# registry id such as xzthemeofsss/quote-notes would install whatever the registry +# points at on the day the sandbox is created. + +set -u + +PASEO_HOME_DIR="${PASEO_HOME:-$HOME/.paseo}" +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/paseo" +LOG="$STATE_DIR/paseo-plugins.log" +WAIT_SECONDS="${PASEO_PLUGINS_WAIT_SECONDS:-600}" +ATTEMPTS="${PASEO_PLUGINS_ATTEMPTS:-3}" +mkdir -p "$STATE_DIR" 2>/dev/null || true + +log() { printf '%s paseo-plugins: %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG"; } + +PATH="$HOME/.npm-global/bin:$(npm prefix -g 2>/dev/null)/bin:$PATH" +export PATH + +# One installer at a time: a second run (manual or a repeated startup) exits. +RUN_LOCK_DIR="$STATE_DIR/paseo-plugins.run.lock" +CONFIG_LOCK_DIR="$PASEO_HOME_DIR/.config-json.lock" +have_run_lock="" +have_config_lock="" +cleanup() { + [ -n "$have_config_lock" ] && rmdir "$CONFIG_LOCK_DIR" 2>/dev/null + have_config_lock="" + [ -n "$have_run_lock" ] && rmdir "$RUN_LOCK_DIR" 2>/dev/null + have_run_lock="" +} +trap cleanup EXIT +trap 'cleanup; exit 1' HUP INT TERM + +if mkdir "$RUN_LOCK_DIR" 2>/dev/null; then + have_run_lock=yes +else + log "another installer is running ($RUN_LOCK_DIR exists); this run did nothing" + exit 0 +fi + +paseo_ready() { paseo plugin ls --home "$PASEO_HOME_DIR" --json >/dev/null 2>&1; } + +deadline=$(( $(date +%s) + WAIT_SECONDS )) +until command -v paseo >/dev/null 2>&1 && paseo_ready; do + if [ "$(date +%s)" -ge "$deadline" ]; then + log "daemon not reachable after ${WAIT_SECONDS}s (is the paseo kit applied?); no plugins installed" + exit 0 + fi + sleep 3 +done + +plugins_enabled() { + paseo daemon config get --home "$PASEO_HOME_DIR" pluginsEnabled --json 2>/dev/null | + grep -q '"value": *true' +} + +# Same coarse lock directory the paseo kit takes around config.json writes. +enable_plugins() { + lock_deadline=$(( $(date +%s) + 30 )) + until mkdir "$CONFIG_LOCK_DIR" 2>/dev/null; do + if [ "$(date +%s)" -ge "$lock_deadline" ]; then + log "could not lock config.json; plugins left disabled" + return 1 + fi + sleep 1 + done + have_config_lock=yes + paseo daemon config set --home "$PASEO_HOME_DIR" pluginsEnabled true >>"$LOG" 2>&1 + status=$? + rmdir "$CONFIG_LOCK_DIR" 2>/dev/null + have_config_lock="" + [ "$status" -eq 0 ] || { log "could not set pluginsEnabled"; return 1; } + paseo reload --home "$PASEO_HOME_DIR" --json >>"$LOG" 2>&1 + plugins_enabled +} + +if ! plugins_enabled; then + enable_plugins || { log "plugins are not enabled; no plugins installed"; exit 0; } + log "enabled plugins on the daemon" +fi + +plugin_listed() { + paseo plugin ls --home "$PASEO_HOME_DIR" --json 2>/dev/null | grep -qF "\"id\": \"$1\"" +} + +plugin_status() { + paseo plugin ls "$1" --home "$PASEO_HOME_DIR" --json 2>/dev/null | + sed -n 's/.*"status": *"\([a-z]*\)".*/\1/p' | head -n 1 +} + +# install_plugin ID SOURCE PIN +# git:owner/repo PIN is a full 40-hex commit +# npm:package PIN is an exact x.y.z version +# Any other shape is refused, so an unpinned install cannot be added by accident. +install_plugin() { + id="$1"; source="$2"; pin="${3:-}" + case "$source" in + git:*) + case "$pin" in + *[!0-9a-f]*|"") log "$id REFUSED: git source needs a full lowercase commit SHA, got '$pin'"; return 0 ;; + esac + [ "${#pin}" -eq 40 ] || { log "$id REFUSED: commit pin must be 40 hex characters, got '$pin'"; return 0; } + target="$source" + ;; + npm:*) + case "$pin" in + [0-9]*.[0-9]*.[0-9]*) : ;; + *) log "$id REFUSED: npm source needs an exact x.y.z version, got '$pin'"; return 0 ;; + esac + case "$pin" in + *[!0-9.]*|*..*|.*|*.) log "$id REFUSED: npm version must be digits and dots only, got '$pin'"; return 0 ;; + esac + target="$source@$pin" + ;; + *) log "$id REFUSED: source must be git:owner/repo or npm:package, got '$source'"; return 0 ;; + esac + + if plugin_listed "$id"; then + log "$id already installed (status: $(plugin_status "$id")); skipped" + return 0 + fi + + n=1 + while [ "$n" -le "$ATTEMPTS" ]; do + # A daemon restart (the paseo kit does one when it pins the worktree root) can + # land mid-install; wait for it to answer again before each attempt. + until paseo_ready; do + [ "$(date +%s)" -ge "$deadline" ] && break + sleep 3 + done + case "$source" in + git:*) paseo plugin install --home "$PASEO_HOME_DIR" --id "$id" --ref "$pin" "$target" >>"$LOG" 2>&1 ;; + *) paseo plugin install --home "$PASEO_HOME_DIR" --id "$id" "$target" >>"$LOG" 2>&1 ;; + esac + install_status=$? + if plugin_listed "$id"; then + log "$id installed from $target${pin:+ ($pin)} on attempt $n (exit $install_status); status: $(plugin_status "$id")" + return 0 + fi + log "$id attempt $n of $ATTEMPTS failed (exit $install_status); see lines above" + n=$((n + 1)) + sleep 5 + done + log "$id FAILED to install from $target after $ATTEMPTS attempts" +} + +# Codex Style Annotations v0.2.2, the commit the Paseo registry lists for +# xzthemeofsss/quote-notes (https://paseo.sh/plugins/xzthemeofsss/quote-notes). +install_plugin quote-notes git:xzthemeofsss/paseo-plugins 5ce92527c99a328b28775c7d745f324f2cc84309 + +exit 0 diff --git a/personal/scripts/verify b/personal/scripts/verify index 02e75e7..7b1ae04 100755 --- a/personal/scripts/verify +++ b/personal/scripts/verify @@ -3,9 +3,10 @@ # verify — host-side offline gate for the personal acq mixin kit. # # Checks that the spec and the payload under files/ agree, that the skill is -# well-formed, that the helper scripts parse, and that nothing credential-shaped -# is shipped. If `acq kit validate` is available, the neutral hybrid/v1 spec is -# validated too. There is no live sandbox check: the kit only drops files. +# well-formed, that the helper scripts parse, that the Paseo plugin installer is +# pinned and wired, and that nothing credential-shaped is shipped. If `acq kit +# validate` is available, the neutral hybrid/v1 spec is validated too. There is +# no live sandbox check: a live run would exercise acq and the paseo kit. # # Usage (from anywhere): path/to/personal/scripts/verify @@ -18,6 +19,7 @@ SKILL_DIR="$KIT_DIR/files/home/.agents/skills/$SKILL_NAME" SKILL_MD="$SKILL_DIR/SKILL.md" CHECK_PY="$SKILL_DIR/assets/first-mate-check.py" WAIT_SH="$SKILL_DIR/assets/first-mate-wait.sh" +PLUGINS_SH="$KIT_DIR/files/home/paseo-plugins-install.sh" PYTHON_BIN="${PYTHON_BIN:-python3}" WORK="$(mktemp -d "${TMPDIR:-/tmp}/personal-verify.XXXXXX")" || { echo "mktemp failed" >&2; exit 1; } @@ -31,7 +33,7 @@ info() { printf '\n\033[1m%s\033[0m\n' "$1"; } trap 'rm -rf "$WORK" >/dev/null 2>&1 || true' EXIT INT TERM info "0. Preconditions" -for f in "$SPEC" "$SKILL_MD" "$CHECK_PY" "$WAIT_SH"; do +for f in "$SPEC" "$SKILL_MD" "$CHECK_PY" "$WAIT_SH" "$PLUGINS_SH"; do if [ -f "$f" ]; then ok "present: ${f#"$KIT_DIR"/}" else @@ -123,7 +125,7 @@ for rel, digest in sorted(locked.items()): errors.append("missing: " + rel) elif hashlib.sha256(open(path, "rb").read()).hexdigest() != digest: errors.append("differs from lock: " + rel) -own = "files/home/.agents/skills/paseo-first-mate/" +own = ("files/home/.agents/skills/paseo-first-mate/", "files/home/paseo-plugins-install.sh") for item in lock.get("thirdParty", []): path = os.path.join(kit, item["licenseFile"]) if not os.path.isfile(path): @@ -212,6 +214,58 @@ else printf '%s\n' "$leftover" | sed 's/^/ | /' >&2 fi +info "3d. Paseo plugin installer is wired and pinned" +# The installer enforces pins at run time (install_plugin refuses anything but a +# git source with a full 40-hex commit or an npm source with an exact version). +# This section checks that the guard is still there, that nothing bypasses it, and +# that every listed call is pinned. See docs/decisions/0003. +plug_bad=0 +spec_commands="$(awk '/^commands:/ {c=1; next} /^[A-Za-z]/ {c=0} c' "$SPEC" | grep -vE '^[[:space:]]*#')" +spec_caps="$(awk '/^caps:/ {c=1; next} /^[A-Za-z]/ {c=0} c' "$SPEC" | grep -vE '^[[:space:]]*#')" +rec="$(awk -v p="/home/agent/paseo-plugins-install.sh" '$0 == " - path: " p {f=1; next} f && /mode:/ {gsub(/"/,"",$2); print $2; exit}' "$SPEC")" +[ "$rec" = "0755" ] || { bad "spec record for paseo-plugins-install.sh is missing or not mode 0755 (got '${rec:-}')"; plug_bad=1; } +[ -x "$PLUGINS_SH" ] || { bad "paseo-plugins-install.sh lost its executable bit"; plug_bad=1; } +printf '%s\n' "$spec_commands" | grep -q 'sh /home/agent/paseo-plugins-install.sh' || { bad "no startup command runs paseo-plugins-install.sh"; plug_bad=1; } +printf '%s\n' "$spec_commands" | grep -qE '^[[:space:]]+background:[[:space:]]+true[[:space:]]*$' || { bad "the plugin startup command is not background: true"; plug_bad=1; } +printf '%s\n' "$spec_caps" | grep -qE '^[[:space:]]+-[[:space:]]+github\.com[[:space:]]*$' || { bad "caps.network.allow does not list github.com"; plug_bad=1; } +# The run-time guard must be present. +for needle in "*[!0-9a-f]*|\"\")" '"${#pin}" -eq 40' '*[!0-9.]*|*..*|.*|*.)' 'REFUSED: source must be git:owner/repo or npm:package'; do + grep -qF -- "$needle" "$PLUGINS_SH" || { bad "install_plugin guard missing: $needle"; plug_bad=1; } +done +# Only install_plugin may call `paseo plugin install`, and only with the checked $target and --ref "$pin". +installs="$(grep -nE 'paseo[[:space:]]+plugin[[:space:]]+(install|add)' "$PLUGINS_SH" | grep -vE '^[0-9]+:[[:space:]]*#' || true)" +if [ "$(printf '%s\n' "$installs" | grep -c .)" -ne 2 ] || printf '%s\n' "$installs" | grep -vqE '"\$target"[[:space:]]+>>'; then + bad "paseo plugin install appears outside install_plugin or with an unchecked target" + printf '%s\n' "$installs" | sed 's/^/ | /' >&2 + plug_bad=1 +fi +# Every call must be pinned. Calls are only accepted at the start of a line. +calls="$(grep -E '^[[:space:]]*install_plugin[[:space:]]' "$PLUGINS_SH" | grep -vE 'install_plugin\(\)' || true)" +other="$(grep -E 'install_plugin' "$PLUGINS_SH" | grep -vE '^[[:space:]]*(#|install_plugin[[:space:]]|install_plugin\(\))' | grep -vE 'REFUSED|^[[:space:]]*#' || true)" +if [ -n "$other" ]; then + bad "install_plugin is referenced somewhere other than a line-leading call" + printf '%s\n' "$other" | sed 's/^/ | /' >&2 + plug_bad=1 +fi +if [ -z "$calls" ]; then + bad "paseo-plugins-install.sh installs no plugins"; plug_bad=1 +fi +while IFS= read -r line; do + [ -n "$line" ] || continue + # shellcheck disable=SC2086 + set -- $line + src="${3:-}"; pin="${4:-}"; extra="${5:-}" + [ -z "$extra" ] || { bad "unexpected extra arguments: $line"; plug_bad=1; continue; } + case "$src" in + git:[A-Za-z0-9_.-]*/[A-Za-z0-9_.-]*) printf '%s' "$pin" | grep -qE '^[0-9a-f]{40}$' || { bad "not pinned to a full commit SHA: $line"; plug_bad=1; } ;; + npm:*) printf '%s' "$pin" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { bad "npm source not pinned to an exact version: $line"; plug_bad=1; } ;; + *) bad "source must be git:owner/repo or npm:package: $line"; plug_bad=1 ;; + esac +done </dev/null 2>&1; then if "$PYTHON_BIN" -c 'import ast,sys; ast.parse(open(sys.argv[1], encoding="utf-8").read(), sys.argv[1])' "$CHECK_PY" 2>"$WORK/py.log"; then @@ -229,8 +283,14 @@ else bad "first-mate-wait.sh has a syntax error" sed 's/^/ | /' "$WORK/sh.log" >&2 fi +if sh -n "$PLUGINS_SH" 2>"$WORK/plugins-sh.log"; then + ok "paseo-plugins-install.sh parses as POSIX sh" +else + bad "paseo-plugins-install.sh has a syntax error" + sed 's/^/ | /' "$WORK/plugins-sh.log" >&2 +fi if command -v shellcheck >/dev/null 2>&1; then - if shellcheck --severity=warning "$WAIT_SH" "$0" "$KIT_DIR/scripts/test-wait-lock" >"$WORK/shellcheck.log" 2>&1; then + if shellcheck --severity=warning "$WAIT_SH" "$PLUGINS_SH" "$0" "$KIT_DIR/scripts/test-wait-lock" >"$WORK/shellcheck.log" 2>&1; then ok "shellcheck --severity=warning passes" else bad "shellcheck --severity=warning failed" diff --git a/personal/spec.yaml b/personal/spec.yaml index 525be06..70b1836 100644 --- a/personal/spec.yaml +++ b/personal/spec.yaml @@ -3,7 +3,8 @@ # PERSONAL KIT: the author's own layer on top of the global kits, applied last # with --kit or ACQ_EXTRA_KITS. It drops Agent Skills into the cross-agent # skills root, ~/.agents/skills, which OpenCode, Codex, Docker Agent, and -# Copilot all scan. It carries no agent, no provider, and no configuration. +# Copilot all scan, and installs the author's Paseo plugins. It carries no +# agent and no provider. # # Skills today: # paseo-first-mate the author's own skill plus two helper scripts @@ -14,13 +15,18 @@ # playbook kit ships a different skill named code-review into the same # directory. # -# NO COMMANDS: every payload is dropped at its final path, so there is nothing -# to link, install, or render. Nothing here can fail a sandbox create beyond the -# file drop itself. +# ONE COMMAND, AT STARTUP, IN THE BACKGROUND: the skills are dropped at their +# final path, so they need no command. The one startup command runs +# paseo-plugins-install.sh, which waits for the Paseo daemon that the separate +# `paseo` kit starts, turns on the daemon's plugin switch, and installs the +# plugins listed in that script at pinned commits. It is detached, always exits +# 0, and logs to ~/.local/state/paseo/paseo-plugins.log, so it cannot fail a +# sandbox create. Without the paseo kit it logs that the daemon never came up and +# installs nothing. See docs/decisions/0003. # -# NO EGRESS: skills are instructions plus a few local scripts. The scripts call -# the `gh` and `paseo` CLIs when the agent runs them, and those CLIs' network -# needs belong to the sandbox's other kits, not to this one. +# EGRESS: github.com only, for `git clone` of the pinned plugin sources. The +# skills themselves need none; the first-mate scripts call the `gh` and `paseo` +# CLIs when the agent runs them, and those CLIs' needs belong to other kits. # # NO `agent:` BLOCK and NO published ports: this is an opt-in --kit mixin. # @@ -31,9 +37,16 @@ kind: mixin name: personal displayName: Personal kit description: > - The author's personal skills for coding agents in the sandbox: the Paseo + The author's personal layer for coding agents in the sandbox: the Paseo First Mate skill and a vendored copy of Matt Pocock's engineering skills, - dropped into ~/.agents/skills. Opt-in mixin; no egress, commands, or ports. + dropped into ~/.agents/skills, plus a startup step that installs pinned Paseo + plugins into the Paseo daemon when the paseo kit is also applied. Opt-in + mixin; egress to github.com only; no ports. + +caps: + network: + allow: + - github.com files: - path: /home/agent/.agents/skills/paseo-first-mate/SKILL.md @@ -52,6 +65,10 @@ files: mode: "0644" source: files/home/.agents/licenses/humanlayer-skills-LICENSE description: MIT notice for HumanLayer's show-me skill, adapted inside the pr skill + - path: /home/agent/paseo-plugins-install.sh + mode: "0755" + source: files/home/paseo-plugins-install.sh + description: Wait for the Paseo daemon, enable plugins, and install the pinned plugin list # BEGIN vendored:mattpocock-skills (generated by scripts/vendor-pocock-skills; do not edit) - path: /home/agent/.agents/licenses/mattpocock-skills-LICENSE mode: "0644" @@ -295,6 +312,19 @@ files: source: files/home/.agents/skills/writing-for-agents/agents/openai.yaml # END vendored:mattpocock-skills +commands: + - phase: startup + # The hybrid/v1 `user` field is a numeric uid pattern; the paseo kit uses the + # same value for the same reason. acq runs the startup phase as the agent user. + user: "1000" + background: true + description: Install the pinned Paseo plugins once the daemon is up (background, never fails the sandbox) + command: + - sh + - -c + - | + sh /home/agent/paseo-plugins-install.sh & + agentContext: | ## Personal skills @@ -304,6 +334,10 @@ agentContext: | agents, issues, or dependent pull requests. Copy its `assets/` scripts into the coordinator worktree's git-ignored `tmp/` as the skill describes rather than running them in place. + - Paseo plugins: at startup a background script enables plugins on the Paseo + daemon and installs the ones listed in `~/paseo-plugins-install.sh`. Its log + is `~/.local/state/paseo/paseo-plugins.log`; `paseo plugin ls` shows the + result. It does nothing when the paseo kit is not applied. - Matt Pocock's skills (`ask-matt`, `tdd`, `triage`, `wayfinder`, `handoff`, `teach`, and others). 16 of the 27 are user-invoked: run those only when the user types the name. `teach` fills the directory it runs in with lesson files, so From 948f8ad6f3288147705780175bfce0b665f501b2 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sat, 7 Nov 2026 08:54:34 +0000 Subject: [PATCH 2/4] feat(personal): install the Skills Paseo plugin (gpambrozio/skills 0.4.0) Add npm:@gpambrozio/paseo-skills at exact version 0.4.0, the version the Paseo registry lists. The plugin lists the skills each agent session can use, renders each SKILL.md and can invoke one. It reads ~/.claude, ~/.codex and ~/.agents/skills, writes nothing, and has no network or subprocess calls (reviewed at 0.4.0; tarball sha512 recorded in the README and checked against the registry). First npm-sourced plugin, so caps.network gains registry.npmjs.org, and scripts/verify now fails if an npm-sourced plugin is listed without that host. --- personal/README.md | 12 +++++++----- .../0003-install-paseo-plugins-at-startup.md | 11 ++++++----- personal/files/home/paseo-plugins-install.sh | 5 +++++ personal/scripts/verify | 5 ++++- personal/spec.yaml | 7 ++++--- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/personal/README.md b/personal/README.md index 74fc25d..ea1d09a 100644 --- a/personal/README.md +++ b/personal/README.md @@ -7,7 +7,7 @@ every sandbox, applied last on top of the global kits. It is an opt-in `--kit` mixin. It installs no agent, writes no provider configuration, and publishes no ports. It runs one background startup command that installs Paseo plugins (see [Paseo plugins](#paseo-plugins)), and allow-lists one -network host, `github.com`, for that command. +network hosts, `github.com` and `registry.npmjs.org`, for that command. ## What it ships @@ -62,6 +62,7 @@ each new sandbox installs them again. | Plugin | Source | Pin | Notes | | --- | --- | --- | --- | | `quote-notes` ("Codex Style Annotations", 0.2.2) by xzthemeofsss | `git:xzthemeofsss/paseo-plugins` | `5ce92527c99a328b28775c7d745f324f2cc84309` | Client plus a one-line settings registration; no server handlers. Needs Paseo 0.11.4 or later. Disable the older `quote` plugin first if you add it. Some labels are in Chinese. | +| `skills` ("Skills", 0.4.0) by gpambrozio | `npm:@gpambrozio/paseo-skills` | `0.4.0` (sha512 `zMgP2U/vrRsLkKhoQICkN52+YoDy2WkkYymEDmruduKxf4Dq99Ct8Pk3xqJyAzijq2oprMFYLbj/wT6XepBLeA==`) | Lists the skills each agent session can use, renders each `SKILL.md`, and can invoke one. Reads `~/.claude`, `~/.codex` and `~/.agents/skills`; writes nothing; no network or subprocess. Shows this kit's 28 skills in the Paseo UI. Needs Paseo 0.9 or later. | To add or change one, edit the `install_plugin` lines at the bottom of `files/home/paseo-plugins-install.sh`, read the plugin's source, and run @@ -210,12 +211,13 @@ drops files and starts one plugin installer that needs the separate paseo kit, s run commands and push code. Everything is plain text you can read in this directory; review the diff before pinning a new SHA, especially after re-vendoring. -- **`github.com` is open to every agent in the sandbox.** `caps.network` is sandbox-wide, - not scoped to the installer, so any agent or plugin there can also reach github.com - (clones, gists, pushes). It is the same trust you already give `gh`. +- **`github.com` and `registry.npmjs.org` are open to every agent in the sandbox.** + `caps.network` is sandbox-wide, not scoped to the installer, so any agent or plugin + there can also reach them (clones, gists, pushes, package fetches). The paseo kit + already allows the npm registry. - **Plugins are code, not text.** A listed plugin runs unsandboxed. Review a plugin's source at the exact commit before you add or move its pin, and check what it fetches - and writes. The installer reaches only `github.com` (and whatever the daemon's own + and writes. The installer reaches only `github.com` and `registry.npmjs.org` (and whatever the daemon's own allow-list permits); the egress allow-list is the only network control, and the plugin runs inside it. - **Agent-writable.** The files land under `/home/agent`, which the agent owns. diff --git a/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md b/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md index 6464661..e7f3633 100644 --- a/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md +++ b/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md @@ -44,11 +44,12 @@ plugin directory and config, and `paseo plugin install` needs a running daemon. which was not reachable from the development sandbox, so the registry route could not be tested. -4. **Egress: `github.com`.** For `git clone` of the pinned sources. `caps.network` is - sandbox-wide, so this also lets every other agent in the sandbox reach github.com. The first plugin is - a git source. An npm-sourced plugin would need `registry.npmjs.org`, which the paseo - kit already allows; it would be added to `caps.network` here too rather than relied - on, in the commit that adds such a plugin. +4. **Egress: `github.com` and `registry.npmjs.org`.** `github.com` is for `git clone` of + git-sourced plugins and `registry.npmjs.org` for npm-sourced ones. The paseo kit + already allows the npm registry; this kit lists it too rather than rely on another kit. + `caps.network` is sandbox-wide, so every other agent in the sandbox also reaches these + hosts. `scripts/verify` fails if an npm-sourced plugin is listed without the npm host. + The daemon installs npm plugins with `--ignore-scripts` (checked in its source). 5. **Idempotent and skip-if-present.** The script skips a plugin whose id is already configured, so a second run, a daemon restart that re-runs startup, or a snapshot that diff --git a/personal/files/home/paseo-plugins-install.sh b/personal/files/home/paseo-plugins-install.sh index 5cb1af3..31b9371 100755 --- a/personal/files/home/paseo-plugins-install.sh +++ b/personal/files/home/paseo-plugins-install.sh @@ -157,4 +157,9 @@ install_plugin() { # xzthemeofsss/quote-notes (https://paseo.sh/plugins/xzthemeofsss/quote-notes). install_plugin quote-notes git:xzthemeofsss/paseo-plugins 5ce92527c99a328b28775c7d745f324f2cc84309 +# Skills v0.4.0, the version the Paseo registry lists for gpambrozio/skills +# (https://paseo.sh/plugins/gpambrozio/skills). npm tarball sha512 reviewed: +# zMgP2U/vrRsLkKhoQICkN52+YoDy2WkkYymEDmruduKxf4Dq99Ct8Pk3xqJyAzijq2oprMFYLbj/wT6XepBLeA== +install_plugin skills npm:@gpambrozio/paseo-skills 0.4.0 + exit 0 diff --git a/personal/scripts/verify b/personal/scripts/verify index 7b1ae04..cc5969a 100755 --- a/personal/scripts/verify +++ b/personal/scripts/verify @@ -228,6 +228,9 @@ rec="$(awk -v p="/home/agent/paseo-plugins-install.sh" '$0 == " - path: " p {f= printf '%s\n' "$spec_commands" | grep -q 'sh /home/agent/paseo-plugins-install.sh' || { bad "no startup command runs paseo-plugins-install.sh"; plug_bad=1; } printf '%s\n' "$spec_commands" | grep -qE '^[[:space:]]+background:[[:space:]]+true[[:space:]]*$' || { bad "the plugin startup command is not background: true"; plug_bad=1; } printf '%s\n' "$spec_caps" | grep -qE '^[[:space:]]+-[[:space:]]+github\.com[[:space:]]*$' || { bad "caps.network.allow does not list github.com"; plug_bad=1; } +if grep -qE '^[[:space:]]*install_plugin[[:space:]]+[a-z][a-z0-9-]*[[:space:]]+npm:' "$PLUGINS_SH"; then + printf '%s\n' "$spec_caps" | grep -qE '^[[:space:]]+-[[:space:]]+registry\.npmjs\.org[[:space:]]*$' || { bad "an npm-sourced plugin is listed but caps.network.allow lacks registry.npmjs.org"; plug_bad=1; } +fi # The run-time guard must be present. for needle in "*[!0-9a-f]*|\"\")" '"${#pin}" -eq 40' '*[!0-9.]*|*..*|.*|*.)' 'REFUSED: source must be git:owner/repo or npm:package'; do grep -qF -- "$needle" "$PLUGINS_SH" || { bad "install_plugin guard missing: $needle"; plug_bad=1; } @@ -264,7 +267,7 @@ while IFS= read -r line; do done </dev/null 2>&1; then diff --git a/personal/spec.yaml b/personal/spec.yaml index 70b1836..ba8909e 100644 --- a/personal/spec.yaml +++ b/personal/spec.yaml @@ -24,8 +24,8 @@ # sandbox create. Without the paseo kit it logs that the daemon never came up and # installs nothing. See docs/decisions/0003. # -# EGRESS: github.com only, for `git clone` of the pinned plugin sources. The -# skills themselves need none; the first-mate scripts call the `gh` and `paseo` +# EGRESS: github.com for `git clone` of git-sourced plugins, registry.npmjs.org for +# npm-sourced ones. The skills themselves need none; the first-mate scripts call the `gh` and `paseo` # CLIs when the agent runs them, and those CLIs' needs belong to other kits. # # NO `agent:` BLOCK and NO published ports: this is an opt-in --kit mixin. @@ -41,12 +41,13 @@ description: > First Mate skill and a vendored copy of Matt Pocock's engineering skills, dropped into ~/.agents/skills, plus a startup step that installs pinned Paseo plugins into the Paseo daemon when the paseo kit is also applied. Opt-in - mixin; egress to github.com only; no ports. + mixin; egress to github.com and registry.npmjs.org only; no ports. caps: network: allow: - github.com + - registry.npmjs.org files: - path: /home/agent/.agents/skills/paseo-first-mate/SKILL.md From 031bd55d9881dccc08e2dd05cdca926503035eac Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sat, 7 Nov 2026 08:54:49 +0000 Subject: [PATCH 3/4] feat(personal): install the Mermaid Paseo plugin (dutchakdev/mermaid 0.5.0) Add npm:paseo-plugin-mermaid at exact version 0.5.0, the version the Paseo registry lists. The plugin draws mermaid code blocks in chat as diagrams and opens .mmd files as diagrams. It is client-only: no server code, no network calls, no subprocess (reviewed at 0.5.0; tarball sha512 recorded in the README and checked against the registry). --- personal/README.md | 1 + personal/files/home/paseo-plugins-install.sh | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/personal/README.md b/personal/README.md index ea1d09a..78cca6a 100644 --- a/personal/README.md +++ b/personal/README.md @@ -63,6 +63,7 @@ each new sandbox installs them again. | --- | --- | --- | --- | | `quote-notes` ("Codex Style Annotations", 0.2.2) by xzthemeofsss | `git:xzthemeofsss/paseo-plugins` | `5ce92527c99a328b28775c7d745f324f2cc84309` | Client plus a one-line settings registration; no server handlers. Needs Paseo 0.11.4 or later. Disable the older `quote` plugin first if you add it. Some labels are in Chinese. | | `skills` ("Skills", 0.4.0) by gpambrozio | `npm:@gpambrozio/paseo-skills` | `0.4.0` (sha512 `zMgP2U/vrRsLkKhoQICkN52+YoDy2WkkYymEDmruduKxf4Dq99Ct8Pk3xqJyAzijq2oprMFYLbj/wT6XepBLeA==`) | Lists the skills each agent session can use, renders each `SKILL.md`, and can invoke one. Reads `~/.claude`, `~/.codex` and `~/.agents/skills`; writes nothing; no network or subprocess. Shows this kit's 28 skills in the Paseo UI. Needs Paseo 0.9 or later. | +| `mermaid` (0.5.0) by dutchakdev | `npm:paseo-plugin-mermaid` | `0.5.0` (sha512 `vNVNE1b0z7HJ1093jT1Cz/f3QYXFKg6yTfdqTHeFqaa9Ks437b0nrmQRdY/tWC9XDdXvgjWCCS1v9CsYYsCOhg==`) | Draws `mermaid` code blocks in chat as diagrams (flowcharts and sequence diagrams itself, other types through Paseo's Markdown on 0.13 or later), and opens `.mmd` files as diagrams. Runs only in the app: no server code, network or subprocess. Needs Paseo 0.11 or later. If you install Better Paseo, turn off its "Plugin markdown for replies" setting. | To add or change one, edit the `install_plugin` lines at the bottom of `files/home/paseo-plugins-install.sh`, read the plugin's source, and run diff --git a/personal/files/home/paseo-plugins-install.sh b/personal/files/home/paseo-plugins-install.sh index 31b9371..5f686d1 100755 --- a/personal/files/home/paseo-plugins-install.sh +++ b/personal/files/home/paseo-plugins-install.sh @@ -162,4 +162,9 @@ install_plugin quote-notes git:xzthemeofsss/paseo-plugins 5ce92527c99a328b28775c # zMgP2U/vrRsLkKhoQICkN52+YoDy2WkkYymEDmruduKxf4Dq99Ct8Pk3xqJyAzijq2oprMFYLbj/wT6XepBLeA== install_plugin skills npm:@gpambrozio/paseo-skills 0.4.0 +# Mermaid v0.5.0, the version the Paseo registry lists for dutchakdev/mermaid +# (https://paseo.sh/plugins/dutchakdev/mermaid). npm tarball sha512 reviewed: +# vNVNE1b0z7HJ1093jT1Cz/f3QYXFKg6yTfdqTHeFqaa9Ks437b0nrmQRdY/tWC9XDdXvgjWCCS1v9CsYYsCOhg== +install_plugin mermaid npm:paseo-plugin-mermaid 0.5.0 + exit 0 From 95b65852fe944014bc38c8ddcee8273e6b941bdf Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sat, 7 Nov 2026 09:13:22 +0000 Subject: [PATCH 4/4] feat(personal): install the Beautiful Chat Paseo plugin (aborakati/beautiful-chat) Add git:ABorakati/beautiful-chat at commit 42b61d8301b5400a67821bbbfcf99fa8b681dc37, the commit the Paseo registry lists. It redraws chat timeline items as typed, syntax-highlighted cards. Its transformers do not gate on provider, so it applies to every agent, not only Oh My Pi. Its manifest runs `npm ci --include=dev` at install, unsandboxed. At the pinned commit all 361 lockfile entries resolve from registry.npmjs.org with integrity hashes and the only install script is fsevents (macOS only). Documented in the README and ADR 0003 as a point-in-time review. It conflicts with mermaid by default: its "Assistant markdown" setting must be turned off once per browser origin, which the kit cannot do because the setting lives in localStorage. Documented in the README. Hardening from adversarial review of the whole branch: - install_plugin now also refuses git sources that are not exactly owner/repo and npm sources that carry a version, tag or non-registry spec, at run time. - scripts/verify section 3d requires the git install to keep --ref "$pin" and anchors the source shapes. - The deadline is per plugin, no sleep after the last attempt, and an already installed plugin at a different revision is logged, not silently skipped. - README and ADR corrected: setting name, localStorage origin caveat, what was tested. --- personal/README.md | 27 ++++++++++-- .../0003-install-paseo-plugins-at-startup.md | 43 +++++++++++++------ personal/files/home/paseo-plugins-install.sh | 34 +++++++++++++-- personal/scripts/verify | 10 +++-- 4 files changed, 91 insertions(+), 23 deletions(-) diff --git a/personal/README.md b/personal/README.md index 78cca6a..ab787dd 100644 --- a/personal/README.md +++ b/personal/README.md @@ -59,11 +59,28 @@ Log: `~/.local/state/paseo/paseo-plugins.log`. Check the result with `paseo plug Plugins live under the daemon's home, which is not persisted past the sandbox, so each new sandbox installs them again. -| Plugin | Source | Pin | Notes | +| Plugin | Source | Pin (sha512s are recorded for reviewers; nothing checks them at install) | Notes | | --- | --- | --- | --- | | `quote-notes` ("Codex Style Annotations", 0.2.2) by xzthemeofsss | `git:xzthemeofsss/paseo-plugins` | `5ce92527c99a328b28775c7d745f324f2cc84309` | Client plus a one-line settings registration; no server handlers. Needs Paseo 0.11.4 or later. Disable the older `quote` plugin first if you add it. Some labels are in Chinese. | -| `skills` ("Skills", 0.4.0) by gpambrozio | `npm:@gpambrozio/paseo-skills` | `0.4.0` (sha512 `zMgP2U/vrRsLkKhoQICkN52+YoDy2WkkYymEDmruduKxf4Dq99Ct8Pk3xqJyAzijq2oprMFYLbj/wT6XepBLeA==`) | Lists the skills each agent session can use, renders each `SKILL.md`, and can invoke one. Reads `~/.claude`, `~/.codex` and `~/.agents/skills`; writes nothing; no network or subprocess. Shows this kit's 28 skills in the Paseo UI. Needs Paseo 0.9 or later. | +| `skills` ("Skills", 0.4.0) by gpambrozio | `npm:@gpambrozio/paseo-skills` | `0.4.0` (sha512 `zMgP2U/vrRsLkKhoQICkN52+YoDy2WkkYymEDmruduKxf4Dq99Ct8Pk3xqJyAzijq2oprMFYLbj/wT6XepBLeA==`) | Lists the skills each agent session can use, renders each `SKILL.md`, and can invoke one. Reads the skill directories under `~/.claude`, `~/.codex` (or `$CODEX_HOME`) and `~/.agents`, and the project-level ones for the agent's working directory; writes nothing; no network or subprocess. Shows this kit's 28 skills in the Paseo UI. Needs Paseo 0.9 or later. | | `mermaid` (0.5.0) by dutchakdev | `npm:paseo-plugin-mermaid` | `0.5.0` (sha512 `vNVNE1b0z7HJ1093jT1Cz/f3QYXFKg6yTfdqTHeFqaa9Ks437b0nrmQRdY/tWC9XDdXvgjWCCS1v9CsYYsCOhg==`) | Draws `mermaid` code blocks in chat as diagrams (flowcharts and sequence diagrams itself, other types through Paseo's Markdown on 0.13 or later), and opens `.mmd` files as diagrams. Runs only in the app: no server code, network or subprocess. Needs Paseo 0.11 or later. If you install Better Paseo, turn off its "Plugin markdown for replies" setting. | +| `beautiful-chat` (0.1.0) by ABorakati | `git:ABorakati/beautiful-chat` | `42b61d8301b5400a67821bbbfcf99fa8b681dc37` | Redraws chat timeline items (tool calls, reasoning, checklists, prompts, errors, replies) as typed, syntax-highlighted cards that follow the Paseo theme, with its own settings page and chat find. The code does not gate on provider, so it applies to every agent; only the OMP-specific parts (job and IRC envelopes) are provider specific. Its manifest runs `npm ci --include=dev` during install, so the daemon needs `npm` on its PATH and `registry.npmjs.org`. Its server spawns `xdg-open` for "reveal path", which does nothing useful in a headless sandbox. **Conflicts with `mermaid` by default, see below.** | + +**Mermaid and Beautiful Chat: switch one setting off, once per browser.** Beautiful Chat's +**Assistant markdown** setting is on by default and takes assistant replies before Mermaid +sees them, so Mermaid blocks show as plain code. In the Paseo app open +**Settings, Plugins, Beautiful Chat** and turn **Assistant markdown** off; Paseo then draws +replies itself and Mermaid draws the diagrams. (Mermaid's own overview calls the +equivalent setting in a different plugin, Better Paseo, "Plugin markdown for replies"; +Beautiful Chat's is named **Assistant markdown**.) + +Beautiful Chat keeps its settings in the browser's `localStorage` under +`paseo/beautiful-chat/preferences/v1`, not on the daemon, so the kit cannot set this for +you. `localStorage` belongs to the page origin. It carries across sandboxes only if you +reach them through the same origin, such as the hosted app at `app.paseo.sh` or the desktop +app. A daemon-served web UI on a per-sandbox published port is a new origin each time and +needs the toggle again. On a native mobile client `localStorage` may not exist, in which +case the choice is not saved and the conflict returns; that is untested. To add or change one, edit the `install_plugin` lines at the bottom of `files/home/paseo-plugins-install.sh`, read the plugin's source, and run @@ -218,8 +235,10 @@ drops files and starts one plugin installer that needs the separate paseo kit, s already allows the npm registry. - **Plugins are code, not text.** A listed plugin runs unsandboxed. Review a plugin's source at the exact commit before you add or move its pin, and check what it fetches - and writes. The installer reaches only `github.com` and `registry.npmjs.org` (and whatever the daemon's own - allow-list permits); the egress allow-list is the only network control, and the + and writes. Some plugins run commands at install: `beautiful-chat` declares + `build: npm ci --include=dev`, which executes with the daemon's privileges and the sandbox's + registry access (npm lifecycle scripts of its dependencies included). Read a plugin's `paseo-plugin.json` `build` list as well as its code. The installer reaches only `github.com` and `registry.npmjs.org` + (and whatever the daemon's own allow-list permits); the egress allow-list is the only network control, and the plugin runs inside it. - **Agent-writable.** The files land under `/home/agent`, which the agent owns. They are a convenience payload, not a trust anchor. diff --git a/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md b/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md index e7f3633..7b7439b 100644 --- a/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md +++ b/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md @@ -55,29 +55,46 @@ plugin directory and config, and `paseo plugin install` needs a running daemon. configured, so a second run, a daemon restart that re-runs startup, or a snapshot that kept the plugin does not error or re-clone. -6. **No in-place updates.** Plugins install at create time, like provider input. To move +6. **Plugins with a `build` step are allowed, reviewed by hand.** `beautiful-chat`'s + manifest runs `npm ci --include=dev` on install, unsandboxed, so its dev dependencies + (React Native, TypeScript, about 360 packages) are fetched and their lifecycle scripts + may run. Reviewed at the pinned commit: every lockfile entry resolves from + `registry.npmjs.org` with an integrity hash, and the only package with an install + script is `fsevents` (macOS only, skipped on Linux). That is a point-in-time review of a + lockfile, not a guarantee. The kit cannot see a plugin's `build` list from the installer, + so `scripts/verify` does not check it; the README tells whoever bumps a pin to read it. + +7. **No in-place updates.** Plugins install at create time, like provider input. To move a pin, change the script and recreate the sandbox. ## What was tested -Against a real Paseo 0.12.0 daemon in an isolated `PASEO_HOME`, with a real `git clone` -of the pinned commit (the registry was not used): starting with `pluginsEnabled` unset, the script -set it, reloaded, installed `quote-notes` from `git:xzthemeofsss/paseo-plugins` at the -pinned SHA, and `paseo plugin ls` reported it `running` at that SHA. A second run -logged "already installed; skipped". With no daemon it logged a clear message after the -wait and exited 0. The guard refused a branch, a short SHA, an uppercase SHA, an npm range -and a bare registry id; a second concurrent run exited at the run lock; a TERM left no -lock behind. Not tested: a real `acq run` with the paseo kit on msb or sbx (no `acq` in -the development sandbox), the paseo kit's newer CLI pins (0.12.0 here), a daemon restart -landing mid-install, and `git clone` behind an HTTPS-inspecting proxy whose CA is not in -the system store. +Against a real Paseo 0.12.0 daemon in an isolated `PASEO_HOME`, with real `git clone`s and +npm installs (the registry at `plugins.paseo.sh` was not used): starting with `pluginsEnabled` +unset, one run of the script set it, reloaded, and installed all four plugins. `paseo plugin +ls` reported each `running` at its pin: `quote-notes` and `beautiful-chat` at their commits, +`skills` 0.4.0 and `mermaid` 0.5.0 from npm (the lockfiles the daemon wrote carry the same +sha512s recorded in the README). `beautiful-chat`'s `npm ci` build took about 7 seconds. A +second run logged "already installed; skipped" for each. With no daemon it logged a clear +message after the wait and exited 0. The run-time guard refused a branch, a short SHA, an +uppercase SHA, an npm range or tag, a bare registry id, `git:` sources with extra path +segments or `@ref`, and `npm:github:` sources, all before calling `paseo`. A second +concurrent run exited at the run lock, and a TERM left no lock behind. + +Not tested: a real `acq run` with the paseo kit on msb or sbx (no `acq` in the development +sandbox), the paseo kit's newer CLI pins (0.12.0 here), a daemon restart landing mid-install, +a build that outlasts the CLI's own install timeout, `git clone` behind an HTTPS-inspecting +proxy whose CA is not in the system store, and how any of the plugins look in a browser. ## Consequences - The kit now depends on the paseo kit for the plugin step to do anything. Kits cannot declare dependencies yet, so this is documented, and the script reports a missing daemon instead of failing. -- The kit adds one egress host and one background process that lives for a few seconds +- The kit adds two egress hosts (`github.com`, `registry.npmjs.org`) and one background process that lives for a few seconds to a few minutes after sandbox start. - Updating a plugin is a code-review task: read the diff between pins, then bump. - `scripts/verify` gains section 3d and also shellchecks the installer. +- `mermaid` and `beautiful-chat` conflict until Beautiful Chat's **Assistant markdown** + setting is switched off in each browser origin. That setting is client-side + `localStorage`, so the kit cannot apply it. Documented in the README. diff --git a/personal/files/home/paseo-plugins-install.sh b/personal/files/home/paseo-plugins-install.sh index 5f686d1..12a222e 100755 --- a/personal/files/home/paseo-plugins-install.sh +++ b/personal/files/home/paseo-plugins-install.sh @@ -92,6 +92,11 @@ plugin_listed() { paseo plugin ls --home "$PASEO_HOME_DIR" --json 2>/dev/null | grep -qF "\"id\": \"$1\"" } +plugin_revision() { + paseo plugin ls "$1" --home "$PASEO_HOME_DIR" --json 2>/dev/null | + sed -n 's/.*"currentRevision": *"\([^"]*\)".*/\1/p' | head -n 1 +} + plugin_status() { paseo plugin ls "$1" --home "$PASEO_HOME_DIR" --json 2>/dev/null | sed -n 's/.*"status": *"\([a-z]*\)".*/\1/p' | head -n 1 @@ -105,6 +110,12 @@ install_plugin() { id="$1"; source="$2"; pin="${3:-}" case "$source" in git:*) + repo="${source#git:}" + case "$repo" in + ""|/*|*/|*/*/*|*..*|*[!A-Za-z0-9_./-]*) log "$id REFUSED: git source must be git:owner/repo, got '$source'"; return 0 ;; + */*) : ;; + *) log "$id REFUSED: git source must be git:owner/repo, got '$source'"; return 0 ;; + esac case "$pin" in *[!0-9a-f]*|"") log "$id REFUSED: git source needs a full lowercase commit SHA, got '$pin'"; return 0 ;; esac @@ -112,6 +123,10 @@ install_plugin() { target="$source" ;; npm:*) + pkg="${source#npm:}" + case "$pkg" in + ""|*[!A-Za-z0-9_./@-]*|?*@*|*..*|*/*/*) log "$id REFUSED: npm source must be npm:package or npm:@scope/package with no version, got '$source'"; return 0 ;; + esac case "$pin" in [0-9]*.[0-9]*.[0-9]*) : ;; *) log "$id REFUSED: npm source needs an exact x.y.z version, got '$pin'"; return 0 ;; @@ -125,16 +140,22 @@ install_plugin() { esac if plugin_listed "$id"; then - log "$id already installed (status: $(plugin_status "$id")); skipped" + have="$(plugin_revision "$id")" + if [ -n "$have" ] && [ "$have" != "$pin" ]; then + log "$id already installed at '$have', not the pin '$pin' (status: $(plugin_status "$id")); skipped, NOT changed" + else + log "$id already installed (status: $(plugin_status "$id")); skipped" + fi return 0 fi + install_deadline=$(( $(date +%s) + WAIT_SECONDS )) n=1 while [ "$n" -le "$ATTEMPTS" ]; do # A daemon restart (the paseo kit does one when it pins the worktree root) can # land mid-install; wait for it to answer again before each attempt. until paseo_ready; do - [ "$(date +%s)" -ge "$deadline" ] && break + [ "$(date +%s)" -ge "$install_deadline" ] && break sleep 3 done case "$source" in @@ -147,8 +168,8 @@ install_plugin() { return 0 fi log "$id attempt $n of $ATTEMPTS failed (exit $install_status); see lines above" + [ "$n" -lt "$ATTEMPTS" ] && sleep 5 n=$((n + 1)) - sleep 5 done log "$id FAILED to install from $target after $ATTEMPTS attempts" } @@ -167,4 +188,11 @@ install_plugin skills npm:@gpambrozio/paseo-skills 0.4.0 # vNVNE1b0z7HJ1093jT1Cz/f3QYXFKg6yTfdqTHeFqaa9Ks437b0nrmQRdY/tWC9XDdXvgjWCCS1v9CsYYsCOhg== install_plugin mermaid npm:paseo-plugin-mermaid 0.5.0 +# Beautiful Chat 0.1.0 by ABorakati, the commit the Paseo registry lists for +# aborakati/beautiful-chat (https://paseo.sh/plugins/aborakati/beautiful-chat). +# Its manifest runs `npm ci --include=dev` at install, from a lockfile whose +# packages all resolve from registry.npmjs.org with integrity hashes. The build runs +# only from this pinned commit's manifest. +install_plugin beautiful-chat git:ABorakati/beautiful-chat 42b61d8301b5400a67821bbbfcf99fa8b681dc37 + exit 0 diff --git a/personal/scripts/verify b/personal/scripts/verify index cc5969a..2906f75 100755 --- a/personal/scripts/verify +++ b/personal/scripts/verify @@ -237,7 +237,9 @@ for needle in "*[!0-9a-f]*|\"\")" '"${#pin}" -eq 40' '*[!0-9.]*|*..*|.*|*.)' 'RE done # Only install_plugin may call `paseo plugin install`, and only with the checked $target and --ref "$pin". installs="$(grep -nE 'paseo[[:space:]]+plugin[[:space:]]+(install|add)' "$PLUGINS_SH" | grep -vE '^[0-9]+:[[:space:]]*#' || true)" -if [ "$(printf '%s\n' "$installs" | grep -c .)" -ne 2 ] || printf '%s\n' "$installs" | grep -vqE '"\$target"[[:space:]]+>>'; then +if [ "$(printf '%s\n' "$installs" | grep -c .)" -ne 2 ] \ + || [ "$(printf '%s\n' "$installs" | grep -cF -- '--ref "$pin" "$target" >>')" -ne 1 ] \ + || [ "$(printf '%s\n' "$installs" | grep -cF -- '--id "$id" "$target" >>')" -ne 1 ]; then bad "paseo plugin install appears outside install_plugin or with an unchecked target" printf '%s\n' "$installs" | sed 's/^/ | /' >&2 plug_bad=1 @@ -260,8 +262,10 @@ while IFS= read -r line; do src="${3:-}"; pin="${4:-}"; extra="${5:-}" [ -z "$extra" ] || { bad "unexpected extra arguments: $line"; plug_bad=1; continue; } case "$src" in - git:[A-Za-z0-9_.-]*/[A-Za-z0-9_.-]*) printf '%s' "$pin" | grep -qE '^[0-9a-f]{40}$' || { bad "not pinned to a full commit SHA: $line"; plug_bad=1; } ;; - npm:*) printf '%s' "$pin" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { bad "npm source not pinned to an exact version: $line"; plug_bad=1; } ;; + git:*) printf '%s' "${src#git:}" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' || { bad "git source must be git:owner/repo: $line"; plug_bad=1; } + printf '%s' "$pin" | grep -qE '^[0-9a-f]{40}$' || { bad "not pinned to a full commit SHA: $line"; plug_bad=1; } ;; + npm:*) printf '%s' "${src#npm:}" | grep -qE '^(@[a-z0-9._-]+/)?[a-z0-9._-]+$' || { bad "npm source must be npm:package with no version or tag: $line"; plug_bad=1; } + printf '%s' "$pin" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { bad "npm source not pinned to an exact version: $line"; plug_bad=1; } ;; *) bad "source must be git:owner/repo or npm:package: $line"; plug_bad=1 ;; esac done <