From 3093c45b616e469fdc4f5086769426820e04eb1b Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Mon, 24 Aug 2026 22:31:00 +0800 Subject: [PATCH] fix: make a marketplace install actually work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things were broken for anybody installing this the documented way. **The install command could not work.** setup and install.sh both printed `claude plugin install vta-agent-memory`, which resolves only against *configured* marketplaces — so it fails with 'not found in any configured marketplace' until the marketplace is added. It needs two commands, and the qualified name: claude plugin marketplace add OpenVTC/vta-agent-memory claude plugin install vta-agent-memory@vta-agent-memory **And the install produced a plugin that could not run.** .mcp.json and hooks.json invoke ${CLAUDE_PLUGIN_ROOT}/bin/vta-agent-memory. When installed from a marketplace that directory is a fresh git clone — and bin/ was gitignored, because it held a copied build artifact. So the MCP server and the hook both pointed at a file that does not exist. Verified against the real installed copy at ~/.claude/plugins/marketplaces/vta-agent-memory: no bin/ at all. bin/vta-agent-memory is now a committed shell shim that execs the real binary — $VTA_AGENT_MEMORY_BIN, then ~/.cargo/bin, then a local target/, then PATH. install.sh does `cargo install` instead of copying. The shim's exit code differs by subcommand, because its two callers have opposite contracts: `recall` exits 0 so a missing binary never fails a session at start-up, everything else exits 1 so it fails loudly. CI gains three guards, since this is a class of breakage the Rust build cannot see: the shim is tracked, executable, and mode 100755; and every ${CLAUDE_PLUGIN_ROOT} path in the manifests resolves in a clean checkout. Signed-off-by: Glenn Gore --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++ .gitignore | 1 - CLAUDE.md | 14 +++++++++- README.md | 20 +++++++++++--- bin/vta-agent-memory | 57 ++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 55 +++++++++++++++++++++++--------------- src/main.rs | 6 +++-- 7 files changed, 156 insertions(+), 29 deletions(-) create mode 100755 bin/vta-agent-memory diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f90fa9d..e48891b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,38 @@ jobs: python3 -c "import json,sys; json.load(open('$f'))" done + # The shim at bin/ is what `.mcp.json` and the hook actually invoke. It + # must be committed and executable: a marketplace install is a fresh + # clone with no build artifacts, so if this file is ever gitignored + # again — as it was — the plugin installs and does nothing. + - name: Check the launcher shim is committed and executable + run: | + test -x bin/vta-agent-memory \ + || { echo "::error::bin/vta-agent-memory missing or not executable"; exit 1; } + git ls-files --error-unmatch bin/vta-agent-memory > /dev/null \ + || { echo "::error::bin/vta-agent-memory is not tracked by git"; exit 1; } + mode=$(git ls-files -s bin/vta-agent-memory | cut -d' ' -f1) + test "$mode" = "100755" \ + || { echo "::error::bin/vta-agent-memory has git mode $mode, expected 100755"; exit 1; } + + # The paths those manifests point at must exist in a fresh clone, which is + # exactly what a marketplace install gives you. + - name: Check manifest paths resolve in a clean checkout + run: | + fail=0 + for f in .mcp.json hooks/hooks.json; do + python3 - "$f" <<'PY' || fail=1 + import json, os, re, sys + raw = open(sys.argv[1]).read() + for path in re.findall(r'\$\{CLAUDE_PLUGIN_ROOT\}/([^"\s]+)', raw): + if not os.path.exists(path): + print(f"::error file={sys.argv[1]}::{path} does not exist in a clean checkout") + sys.exit(1) + print(f"ok {sys.argv[1]} -> {path}") + PY + done + exit $fail + # Every command and skill needs frontmatter to be loaded at all; a missing # `description` makes a command invisible rather than broken. - name: Check command and skill frontmatter diff --git a/.gitignore b/.gitignore index 4dc0091..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /target -/bin diff --git a/CLAUDE.md b/CLAUDE.md index b9a55d1..5d31b02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,8 @@ commands, and hook around it. hooks/hooks.json SessionStart -> `recall --format json` skills/agent-memory/ WHEN to save and recall — the policy layer commands/ /remember /recall /forget /memories -scripts/install.sh build + place bin/vta-agent-memory +bin/vta-agent-memory COMMITTED shim — execs the real cargo-installed binary +scripts/install.sh cargo install src/ lib.rs the crate is a library too, so tests/ can reach it enrol.rs two-phase enrolment (init/connect) — the primary path @@ -103,6 +104,17 @@ accepts either and normalises. endpoint, so preferring `resolve_vta_endpoint` would fail setup for precisely the deployments the operator had already described. +**`bin/vta-agent-memory` is a committed shell shim, not a build artifact.** +`.mcp.json` and `hooks.json` invoke `${CLAUDE_PLUGIN_ROOT}/bin/vta-agent-memory` +because Claude Code's launch environment need not match a shell — but when the +plugin is installed from a marketplace that directory is a fresh clone with no +compiled binary in it. The shim resolves the real one (`$VTA_AGENT_MEMORY_BIN`, +then `~/.cargo/bin`, then a local `target/`, then `PATH`). Do not gitignore it, +and do not replace it with a copied binary — that is what was broken. + +Its exit code differs by subcommand on purpose: `recall` exits 0 (the +SessionStart hook must never fail a session), everything else exits 1. + **Enrolment goes through `vti_secrets::IntegrationOnboarding`, never a hand-rolled key.** It is the shared ephemeral-`did:key` → ACL-grant → auto-rotate-on-first-connect flow the mediator, PNM and DID-hosting use. Two diff --git a/README.md b/README.md index 56f1d40..79b9790 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,24 @@ Needs Rust 1.95+, and a VTA you are already logged into with ```bash git clone https://github.com/OpenVTC/vta-agent-memory cd vta-agent-memory -scripts/install.sh # builds and places bin/vta-agent-memory +scripts/install.sh # cargo install, into ~/.cargo/bin ``` +Then add it to Claude Code. **Two steps** — `claude plugin install ` +resolves from *configured marketplaces*, so on its own it reports +"not found in any configured marketplace": + +```bash +claude plugin marketplace add OpenVTC/vta-agent-memory +claude plugin install vta-agent-memory@vta-agent-memory +``` + +The plugin ships Rust source, not a compiled binary, so `bin/vta-agent-memory` +in the repo is a **shim**: `.mcp.json` and the hook invoke it, and it execs +whichever real binary `cargo install` produced. That is what makes a +marketplace install work, where the plugin directory is a fresh clone with no +build artifacts in it. Override the lookup with `$VTA_AGENT_MEMORY_BIN`. + ### Enrol this machine Two phases, because **the machine holding your memories should not also hold an @@ -100,9 +115,6 @@ though, so prefer `init` + `connect` anywhere that matters. `--vta` also accepts name, but prefer the DID: a `pnm` name is a nickname chosen on one machine and means nothing on any other. -Then add the plugin to Claude Code (from a marketplace that lists this repo, or -by pointing at the checkout). - Check it any time: ```bash diff --git a/bin/vta-agent-memory b/bin/vta-agent-memory new file mode 100755 index 0000000..f4e2d10 --- /dev/null +++ b/bin/vta-agent-memory @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# Locate the real `vta-agent-memory` binary and exec it. +# +# `.mcp.json` and `hooks/hooks.json` both invoke +# `${CLAUDE_PLUGIN_ROOT}/bin/vta-agent-memory`, an absolute path inside the +# plugin directory, because Claude Code launches MCP servers and hooks with an +# environment that need not match an interactive shell — a plugin that works in +# the terminal but not when launched is the worst version of this to debug. +# +# But when the plugin is installed from a marketplace, that directory is a fresh +# git clone in Claude's cache, and a compiled binary is not something you commit. +# So this shim is what lives at that path, and it finds the binary that a +# `cargo install` put somewhere sensible. +# +# Search order, most specific first. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +candidates=( + "${VTA_AGENT_MEMORY_BIN:-}" # explicit override, wins outright + "${CARGO_HOME:-$HOME/.cargo}/bin/vta-agent-memory" + "$here/../target/release/vta-agent-memory" # a local checkout, release + "$here/../target/debug/vta-agent-memory" # a local checkout, debug +) + +for c in "${candidates[@]}"; do + if [[ -n "$c" && -x "$c" ]]; then + exec "$c" "$@" + fi +done + +# Last resort: whatever is on PATH, as long as it is not this shim. +if resolved="$(command -v vta-agent-memory 2>/dev/null)" \ + && [[ "$resolved" != "$here/vta-agent-memory" ]]; then + exec "$resolved" "$@" +fi + +cat >&2 <<'EOF' +vta-agent-memory: the binary is not installed. + +This plugin ships Rust source, not a compiled binary. Build it once: + + cargo install --path + +or set VTA_AGENT_MEMORY_BIN to an existing binary. +EOF + +# Exit code depends on who is asking, because the two callers have opposite +# contracts. The SessionStart hook must never fail a session — somebody just +# opened a terminal — so it exits 0 and injects nothing. Everything else, +# including `serve`, should fail loudly rather than pretend to have worked. +case "${1:-}" in + recall) exit 0 ;; + *) exit 1 ;; +esac diff --git a/scripts/install.sh b/scripts/install.sh index f14b4c2..77759c2 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -22,34 +22,47 @@ if [[ "${1:-}" == "--debug" ]]; then profile_flag="" fi -echo "Building vta-agent-memory (${profile})…" -# shellcheck disable=SC2086 # profile_flag is intentionally word-split (may be empty) -cargo build --manifest-path "${root}/Cargo.toml" ${profile_flag} - -mkdir -p "${root}/bin" -built="${root}/target/${profile}/vta-agent-memory" -if [[ ! -x "${built}" ]]; then - echo "error: expected a binary at ${built}" >&2 - exit 1 +echo "Installing vta-agent-memory (${profile})…" + +# `cargo install` rather than a copy into bin/. bin/ holds a committed shim that +# finds the binary wherever cargo put it — which is what makes the plugin work +# when Claude Code installs it from a marketplace, where bin/ is a fresh clone +# with no compiled artifacts in it. +if [[ "${profile}" == "debug" ]]; then + cargo install --debug --path "${root}" --force +else + cargo install --path "${root}" --force fi -# Copy rather than symlink: a symlink into `target/` breaks the moment someone -# runs `cargo clean`, and it breaks silently — the plugin just stops having -# memory. -install -m 0755 "${built}" "${root}/bin/vta-agent-memory" -echo "Installed ${root}/bin/vta-agent-memory" +installed="${CARGO_HOME:-${HOME}/.cargo}/bin/vta-agent-memory" +if [[ ! -x "${installed}" ]]; then + echo "error: expected a binary at ${installed}" >&2 + exit 1 +fi +echo "Installed ${installed}" -if [[ ! -f "${VTA_AGENT_MEMORY_CONFIG:-${XDG_CONFIG_HOME:-${HOME}/.config}/vta-agent-memory/config.json}" ]]; then +config="${VTA_AGENT_MEMORY_CONFIG:-${XDG_CONFIG_HOME:-${HOME}/Library/Application Support}/vta-agent-memory/config.json}" +if [[ ! -f "${config}" ]]; then cat <<'EOF' -Not configured yet. It bootstraps from a VTA you have already logged into -with `pnm` on this machine: +Enrol this machine. It mints a temporary identity for somebody with VTA admin +to authorize — they do not have to be on this machine: + + vta-agent-memory init --vta-did --context agent-memory - bin/vta-agent-memory setup # your default pnm VTA - bin/vta-agent-memory setup --vta did:webvh:... # a specific one, by DID +…then, once the printed grant has been run: -Then check it: + vta-agent-memory connect - bin/vta-agent-memory doctor +If you hold admin here, `vta-agent-memory setup` does both at once. EOF fi + +cat <<'EOF' + +Add it to Claude Code (two steps — `install` alone cannot find a plugin whose +marketplace has not been added): + + claude plugin marketplace add OpenVTC/vta-agent-memory + claude plugin install vta-agent-memory@vta-agent-memory +EOF diff --git a/src/main.rs b/src/main.rs index 6e5eb15..94b0ff2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -459,8 +459,10 @@ fn print_setup_outcome(o: &setup::SetupOutcome) { println!(" agent DID {did}"); } println!(" memories {} already stored", o.memories_found); - println!("\nEnable it in Claude Code:"); - println!(" claude plugin install vta-agent-memory"); + println!("\nEnable it in Claude Code (two steps — `install` alone cannot find a"); + println!("plugin whose marketplace has not been added):"); + println!(" claude plugin marketplace add OpenVTC/vta-agent-memory"); + println!(" claude plugin install vta-agent-memory@vta-agent-memory"); if let Some(did) = &o.agent_did { println!("\nTo revoke this machine's access later:"); println!(" pnm acl delete --did {did}");