Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
/target
/bin
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>`
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
Expand Down Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions bin/vta-agent-memory
Original file line number Diff line number Diff line change
@@ -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 <your checkout of OpenVTC/vta-agent-memory>

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
55 changes: 34 additions & 21 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
Loading