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..ab787dd 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 hosts, `github.com` and `registry.npmjs.org`, for that command. ## What it ships @@ -26,9 +27,65 @@ 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 (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 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 +`scripts/verify`. The reasoning is in +[`docs/decisions/0003`](docs/decisions/0003-install-paseo-plugins-at-startup.md). ## Matt Pocock's skills @@ -162,7 +219,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 +229,17 @@ 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` 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. 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/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..7b7439b --- /dev/null +++ b/personal/docs/decisions/0003-install-paseo-plugins-at-startup.md @@ -0,0 +1,100 @@ +# 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` 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 + kept the plugin does not error or re-clone. + +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 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 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 new file mode 100755 index 0000000..12a222e --- /dev/null +++ b/personal/files/home/paseo-plugins-install.sh @@ -0,0 +1,198 @@ +#!/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_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 +} + +# 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:*) + 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 + [ "${#pin}" -eq 40 ] || { log "$id REFUSED: commit pin must be 40 hex characters, got '$pin'"; return 0; } + 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 ;; + 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 + 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 "$install_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" -lt "$ATTEMPTS" ] && sleep 5 + n=$((n + 1)) + 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 + +# 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 + +# 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 + +# 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 02e75e7..2906f75 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,65 @@ 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; } +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; } +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 -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 +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:*) 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 </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 +290,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..ba8909e 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 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. # @@ -31,9 +37,17 @@ 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 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 @@ -52,6 +66,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 +313,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 +335,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