diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml new file mode 100644 index 0000000..6b51801 --- /dev/null +++ b/.github/workflows/publish-crates.yml @@ -0,0 +1,161 @@ +name: Publish crates + +# Publishes agent-memory-core, then agent-memory-mcp, to crates.io when an +# exact semver tag is pushed (issue #9). +# +# Trigger: the same exact-vX.Y.Z filter as release.yml. GSD milestone tags +# (v1.0-milestone) and pre-release suffixes (v0.1.0-rc.1) do not match, and a +# fork cannot push a tag here, which is what makes `id-token: write` acceptable. +# +# ── WHY A SEPARATE FILE ──────────────────────────────────────────────────────── +# release.yml runs on GitHub-hosted runners under the issue #1 exception, which +# holds only while it uses no org secret. This workflow uses none either: it +# authenticates with crates.io Trusted Publishing. The runner's GitHub OIDC token +# is exchanged for a crates.io token that is scoped to this repo and workflow and +# expires after 30 minutes, and crates-io-auth-action revokes it in its post +# step. Keeping it in a separate file keeps release.yml's policy readable and lets +# a crates.io outage fail independently of the GitHub Release. +# +# Runner: GitHub-hosted, deliberately. This repo is public and the org runner +# group enforces allows_public_repositories: false, so a self-hosted label would +# queue until cancelled. Do not change it to a self-hosted label. +# +# ── IDEMPOTENT BY DESIGN ─────────────────────────────────────────────────────── +# Trusted Publishing can only be configured for a crate that already exists on +# crates.io, so the FIRST publish of each crate is done by hand from the owner's +# machine before the tag is pushed. This workflow therefore skips any crate whose +# version is already in the crates.io index. On the v0.1.0 tag both crates are +# expected to be skipped; from v0.1.1 on it publishes both. Re-running a failed +# run is safe: whatever already landed is skipped. +# +# Order: core first, then wait until the index serves the new core version, +# because agent-memory-mcp depends on agent-memory-core = "" and +# cargo resolves that dependency from the index during its verify build. +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +permissions: + contents: read + +# Never run two publishes at once. A queued run waits instead of cancelling the +# one in progress, because cancelling mid-publish can leave core published and +# mcp not. +concurrency: + group: publish-crates + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + +jobs: + publish: + name: Publish to crates.io (Trusted Publishing) + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # GitHub OIDC token for the crates.io Trusted Publishing exchange + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable + with: + toolchain: stable + + - name: Tag must match both crate versions + id: version + run: | + set -euo pipefail + TAG_VERSION="${GITHUB_REF_NAME#v}" + META="$(cargo metadata --format-version 1 --no-deps --locked)" + for crate in agent-memory-core agent-memory-mcp; do + v="$(jq -r --arg n "$crate" '.packages[] | select(.name == $n) | .version' <<<"$META")" + if [ "$v" != "$TAG_VERSION" ]; then + echo "::error::$crate is at '$v' but the tag is $GITHUB_REF_NAME" + exit 1 + fi + echo "$crate $v matches $GITHUB_REF_NAME" + done + echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT" + + # Tests before the token is minted, so the 30-minute token is not spent + # waiting on a test run. release.yml runs the full gate separately. + - name: cargo test + run: cargo test --workspace --locked + + - name: Which crates still need publishing + id: plan + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + # The sparse index cargo itself reads. Path for names of 4+ chars: + # first two chars / next two chars / name. A 404 means the crate does + # not exist yet. The file is one JSON object per version (NDJSON): + # slurp it so `-e` judges ONE boolean. A per-line `select` with `-e` + # exits 4 whenever the LAST line does not match (jq 1.6), which + # would report a published version as missing. + # Only a 404 means "crate absent". Any other failure stops the run: a + # transient error must never be read as "not published". + published() { + local name="$1" body="$RUNNER_TEMP/index-$1" code + code="$(curl -sS --retry 3 -o "$body" -w '%{http_code}' \ + "https://index.crates.io/${name:0:2}/${name:2:2}/${name}")" + case "$code" in + 200) jq -e -s --arg v "$VERSION" 'any(.[]; .vers == $v)' "$body" >/dev/null ;; + 404) return 1 ;; + *) echo "::error::index lookup for $name returned HTTP $code"; exit 1 ;; + esac + } + for crate in agent-memory-core agent-memory-mcp; do + key="${crate//-/_}" + if published "$crate"; then + echo "$crate $VERSION is already on crates.io; skipping it" + echo "$key=false" >> "$GITHUB_OUTPUT" + else + echo "$crate $VERSION will be published" + echo "$key=true" >> "$GITHUB_OUTPUT" + fi + done + + - name: Authenticate to crates.io (OIDC) + if: steps.plan.outputs.agent_memory_core == 'true' || steps.plan.outputs.agent_memory_mcp == 'true' + id: auth + uses: rust-lang/crates-io-auth-action@3268eb413133e2960fa4ae0e963ca76380c54cd9 # v1.0.6 + + - name: Publish agent-memory-core + if: steps.plan.outputs.agent_memory_core == 'true' + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: cargo publish -p agent-memory-core --locked + + - name: Wait for agent-memory-core in the index + if: steps.plan.outputs.agent_memory_core == 'true' && steps.plan.outputs.agent_memory_mcp == 'true' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + # cargo publish already waits for the index (since cargo 1.66). This + # loop is a guard so the next publish never races the index. + for attempt in $(seq 1 30); do + if curl -fsS "https://index.crates.io/ag/en/agent-memory-core" 2>/dev/null \ + | jq -e -s --arg v "$VERSION" 'any(.[]; .vers == $v)' >/dev/null; then + echo "agent-memory-core $VERSION is in the index" + exit 0 + fi + echo "attempt $attempt: not in the index yet; waiting 10 s" + sleep 10 + done + echo "::error::agent-memory-core $VERSION did not appear in the index within 5 minutes" + exit 1 + + - name: Publish agent-memory-mcp + if: steps.plan.outputs.agent_memory_mcp == 'true' + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + run: cargo publish -p agent-memory-mcp --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7354ded..da26d7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,9 @@ name: Release # # 1. It uses NO org secrets — only the built-in GITHUB_TOKEN, which # softprops/action-gh-release uses implicitly. There is no signing key, no -# Sonatype or npm token, no crates.io token. The Homebrew formula in +# Sonatype or npm token, no crates.io token. crates.io publishing lives in +# the separate publish-crates.yml (Trusted Publishing over OIDC, no +# secret either). The Homebrew formula in # UnityInFlow/homebrew-tap is updated by hand, outside this workflow. The # self-hosted policy exists to protect secrets this pipeline does not hold. # 2. This repo is public and the org runner group enforces @@ -179,8 +181,10 @@ jobs: "$HOME/.zig/${ZIG_DIR}/zig" version cargo install --locked --version "${CARGO_ZIGBUILD_VERSION}" cargo-zigbuild + # The package is agent-memory-mcp (crates.io name, #9); its [[bin]] is + # still `agent-memory`, so the binary path and asset names are unchanged. - name: Build (cargo zigbuild, cross-compiled from this Linux host) - run: cargo zigbuild --release --locked --target ${{ matrix.target }} -p agent-memory + run: cargo zigbuild --release --locked --target ${{ matrix.target }} -p agent-memory-mcp - name: Smoke test (host-arch-aware --version; foreign-arch/darwin = presence-only) run: | @@ -327,6 +331,15 @@ jobs: brew install unityinflow/tap/agent-memory ``` + ### Install via cargo + + ``` + cargo install agent-memory-mcp --locked + ``` + + The crate is `agent-memory-mcp`; the executable it installs is + `agent-memory`. + ### Manual install | Platform | Asset | diff --git a/.github/workflows/spike-cross-compile.yml b/.github/workflows/spike-cross-compile.yml index 182e9d5..fba64c6 100644 --- a/.github/workflows/spike-cross-compile.yml +++ b/.github/workflows/spike-cross-compile.yml @@ -110,9 +110,9 @@ jobs: run: | set -euo pipefail case "${{ matrix.tool }}" in - zigbuild) cargo zigbuild --release --locked --target ${{ matrix.target }} -p agent-memory ;; - xwin) cargo xwin build --release --locked --target ${{ matrix.target }} -p agent-memory ;; - mingw) cargo build --release --locked --target ${{ matrix.target }} -p agent-memory ;; + zigbuild) cargo zigbuild --release --locked --target ${{ matrix.target }} -p agent-memory-mcp ;; + xwin) cargo xwin build --release --locked --target ${{ matrix.target }} -p agent-memory-mcp ;; + mingw) cargo build --release --locked --target ${{ matrix.target }} -p agent-memory-mcp ;; esac - name: Smoke test (suffix- and host-arch-aware) diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 08e4719..1c712f4 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -34,14 +34,16 @@ Each maps to a roadmap phase. IDs continue from the v1.0 numbering (archived in ### Distribution (DIST) -- [ ] **DIST-05**: Linux musl binaries (x86_64/aarch64) build via a target-suffixed CFLAGS typedef shim (`-Du_int*_t=uint*_t`) on the existing zigbuild pipeline — removable when sqlite-vec PR #199 ships in a pinned release -- [ ] **DIST-03**: A Windows x86_64 binary is published — toolchain decided by an early spike (cargo-xwin/msvc preferred, mingw-w64/gnu fallback); artifact-presence smoke only (no Windows runner in the fleet), marked best-effort in release notes -- [ ] **DIST-06**: Product release v0.1.0 published: checksummed tarballs for all supported targets + updated Homebrew formula, with release notes covering the tag-match behavioral change +- [ ] **DIST-06**: Product release v0.1.0 published: checksummed tarballs for all supported targets + updated Homebrew formula, with release notes covering the tag-match behavioral change. *Re-scoped 2026-10-25 (issue #7): "all supported targets" = the six `release.yml` builds (four required + the best-effort musl pair); also covers the crates.io publish of `agent-memory-core` + `agent-memory-mcp` (#9) and CHANGELOG/SECURITY.md (#8).* + +*DIST-05 (hard musl) and DIST-03 (Windows) moved to v0.2.0 on 2026-10-25 by the owner decision on issue #7 — see Future Requirements.* ## Future Requirements Acknowledged but deferred — not in this milestone. +- **DIST-05** *(moved from v1.1 Phase 6 to v0.2.0, 2026-10-25, issue #7)*: Linux musl binaries (x86_64/aarch64) as a **hard** release target via a target-suffixed CFLAGS typedef shim (`-Du_int*_t=uint*_t`) on the existing zigbuild pipeline — removable when sqlite-vec PR #199 ships in a pinned release. Until then the musl legs stay best-effort, as in v0.0.1. Toolchain verdict already recorded (Phase 3 D-11). +- **DIST-03** *(moved from v1.1 Phase 6 to v0.2.0, 2026-10-25, issue #7)*: A Windows x86_64 binary is published — cargo-xwin 0.23.0 / `x86_64-pc-windows-msvc` (Phase 3 D-11 verdict; mingw-w64/gnu proven fallback); artifact-presence smoke only, best-effort label in release notes. - **SEARCH-06**: Request-level `mode` override (`keyword | semantic | hybrid`) — cheap additive, revisit if agents need deterministic keyword testing - **MCP-08**: LLM auto-arbitration on write (mem0 AUDN-style) — heavy, non-local - Bi-temporal invalidation (Zep-style) — complexity not justified at local scale @@ -58,7 +60,7 @@ Acknowledged but deferred — not in this milestone. ## Traceability -Mapped by roadmap creation 2026-07-12. Coverage: 12/12 v1.1 requirements mapped to Phases 3-6. +Mapped by roadmap creation 2026-07-12. Coverage: 12/12 v1.1 requirements mapped to Phases 3-6; re-scoped 2026-10-25 (issue #7): 10 stay in v1.1, DIST-05 and DIST-03 moved to v0.2.0. | Requirement | Phase | Status | |-------------|-------|--------| @@ -71,10 +73,11 @@ Mapped by roadmap creation 2026-07-12. Coverage: 12/12 v1.1 requirements mapped | SEARCH-05 | Phase 4 | Complete | | DIST-04 | Phase 5 | Complete | | INTEROP-02 | Phase 5 | Complete | -| DIST-05 | Phase 6 | Pending | -| DIST-03 | Phase 6 | Pending | -| DIST-06 | Phase 6 | Pending | +| DIST-05 | v0.2.0 (moved from Phase 6, 2026-10-25, #7) | Deferred | +| DIST-03 | v0.2.0 (moved from Phase 6, 2026-10-25, #7) | Deferred | +| DIST-06 | Phase 6 (v0.1.0 release, #7) | Pending | --- *Requirements defined: 2026-07-12 from approved v2 backlog + 4-dimension research (research/SUMMARY.md)* *Traceability filled: 2026-07-12 by roadmap creation (Phases 3-6)* +*Traceability updated: 2026-10-25 — Phase 6 re-scoped to the v0.1.0 release (owner decision on issue #7); DIST-05/DIST-03 → v0.2.0* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4daf72e..d05fa21 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -7,7 +7,7 @@ ## Overview -v1.1 closes the deferred v2 backlog on the shipped v0.0.1 codebase: better recall (decay-aware hybrid RRF search), a richer MCP surface (`memory_update`, flat typed relations), hardened input validation at the shared service seam, and wider distribution (musl + Windows binaries, portable JSONL export/import) — shipping as product release v0.1.0. The build order follows the research-verified dependency chain: the validation seam lands first (every later surface routes new inputs through it) alongside the milestone's only feasibility unknown, the Windows/musl cross-compile spikes; the relations schema and update semantics land next with hybrid RRF running as an independent parallel track; export freezes its format v1 only after relations exist so link edges are in the contract from day one; and distribution ships last on the toolchains the Phase 3 spikes already proved. +v1.1 closes the deferred v2 backlog on the shipped v0.0.1 codebase: better recall (decay-aware hybrid RRF search), a richer MCP surface (`memory_update`, flat typed relations), hardened input validation at the shared service seam, and wider distribution (musl + Windows binaries, portable JSONL export/import) — shipping as product release v0.1.0. The build order follows the research-verified dependency chain: the validation seam lands first (every later surface routes new inputs through it) alongside the milestone's only feasibility unknown, the Windows/musl cross-compile spikes; the relations schema and update semantics land next with hybrid RRF running as an independent parallel track; export freezes its format v1 only after relations exist so link edges are in the contract from day one; and distribution ships last on the toolchains the Phase 3 spikes already proved. *(2026-10-25: Phase 6 re-scoped to the v0.1.0 release per issue #7 — Windows binaries and hard musl move to v0.2.0.)* ## Phases @@ -29,7 +29,7 @@ Full phase details: [milestones/v1.0-ROADMAP.md](milestones/v1.0-ROADMAP.md) - [x] **Phase 3: API Hardening & Toolchain Spikes** - Out-of-bounds limit/ttl rejected as client errors and exact tag matching at the shared service seam, plus early Windows/musl cross-compile spikes answering the milestone's only feasibility unknown (completed 2026-07-12) - [x] **Phase 4: Memory Update, Relations & Hybrid Search** - `memory_update` with same-transaction re-embed, flat typed links with cascade, migration 0003 hygiene, and decay-aware hybrid RRF as a parallel track (completed 2026-10-23) - [x] **Phase 5: Portable Export & Import** - Versioned JSONL export (link edges included, embeddings excluded) and idempotent cross-machine import with sweep-backfill re-embedding (completed 2026-10-25) -- [ ] **Phase 6: Distribution & v0.1.0 Release** - musl + Windows binaries on the spike-chosen toolchains and the checksummed v0.1.0 release with Homebrew update and behavioral-change release notes +- [ ] **Phase 6: v0.1.0 Release (launch plan #7)** - *Re-scoped 2026-10-25 (owner decision on #7):* v0.1.0 ships from `main` on the six targets `release.yml` already builds, with CHANGELOG, SECURITY.md, the `agent-memory-mcp` crates.io rename + Trusted Publishing, and the Homebrew bump; Windows binaries and hard musl move to v0.2.0 ## Phase Details @@ -127,17 +127,26 @@ Plans: - [x] 05-05-PLAN.md — Gap wave 5: held-out race test for the 05-02 one-snapshot backstop truth (`a_concurrent_link_committed_mid_export_cannot_produce_a_dangling_key`: a memory + link committed mid-export are invisible to that snapshot and present in the next) + recorded live-Ollama realism run (export A → import B → first-sweep backfill → hybrid paraphrase search, `05-05-realism.sh`) (DIST-04, INTEROP-02) -### Phase 6: Distribution & v0.1.0 Release +### Phase 6: v0.1.0 Release (launch plan #7) -**Goal**: As a user on Windows or musl-based Linux, I want to install agent-memory v0.1.0 from a pre-built binary, so that the memory daemon runs on my platform without a Rust toolchain. +> **Re-scoped 2026-10-25** by the owner decision recorded on GitHub issue #7: v0.1.0 ships from +> today's `main` (Phases 3-5, including JSONL export/import) following the launch plan and issues +> #7-#15. **DIST-03 (Windows binaries) and DIST-05 (musl as a hard target) move to v0.2.0** — the +> two musl legs stay best-effort in `release.yml`, exactly as for v0.0.1. The Phase 3 toolchain +> verdicts (D-11) stay valid input for v0.2.0. Scope below is the pre-tag + tag work of #8 and #9 +> plus the Homebrew bump (#10); README rewrite/demo (#11/#12), directories (#13/#14) and launch +> posts (#15) follow the tag and are tracked on GitHub, not as Phase 6 plans. + +**Goal**: As a user of Claude Code, Codex or Cursor, I want to install agent-memory v0.1.0 from crates.io, Homebrew or a checksummed tarball, so that every v1.1 feature (update, relations, hybrid search, export/import) is available without building from source. **Mode:** mvp -**Depends on**: Phase 3 (spike toolchain verdicts), Phases 4-5 (shipped binary must contain all v1.1 features) -**Requirements**: DIST-05, DIST-03, DIST-06 +**Depends on**: Phases 4-5 (shipped binary must contain all v1.1 features) +**Requirements**: DIST-06 **Success Criteria** (what must be TRUE): - 1. Linux musl tarballs (x86_64/aarch64) build green on the existing zigbuild pipeline via the target-suffixed CFLAGS typedef shim (`-Du_int*_t=uint*_t`) and ship checksummed on the release — shim documented as removable when sqlite-vec PR #199 lands in a pinned release. - 2. A Windows x86_64 binary is published on the release using the Phase 3 spike-chosen toolchain, with artifact-presence smoke only (no Windows runner in the fleet) and a best-effort label in the release notes. - 3. Product release v0.1.0 is published: checksummed tarballs for all supported targets, `brew install unityinflow/tap/agent-memory` installs the new version, and release notes call out the behavioral changes (exact tag match, new 400s on bounds, `"hybrid"` search mode). + 1. Both crates are at 0.1.0; the binary crate is published as `agent-memory-mcp` (the executable stays `agent-memory`) and `agent-memory-core` is published beside it; packaging dry-runs are green before the tag, and `.github/workflows/publish-crates.yml` publishes both from the tag via crates.io Trusted Publishing (OIDC, no org secret), skipping a version already on crates.io (#9). + 2. `CHANGELOG.md` (Keep a Changelog, `v0.0.1..v0.1.0`) and `SECURITY.md` (GitHub private advisories) are on `main` before the tag (#8, closes #2). + 3. Product release v0.1.0 is published by `release.yml`: the four required tarballs (macOS arm64/x86_64, Linux gnu x86_64/aarch64) plus the musl pair when the best-effort legs succeed, `SHA256SUMS.txt` and a verifying SLSA attestation; release notes call out the behavioral changes (exact tag match, new 400s on bounds, `"hybrid"` search mode). + 4. `brew install unityinflow/tap/agent-memory` installs 0.1.0 (#10). **Plans**: TBD @@ -153,4 +162,4 @@ Phases execute in numeric order: 3 → 4 → 5 → 6 | 3. API Hardening & Toolchain Spikes | v1.1 | 2/2 | Complete | 2026-07-12 | | 4. Memory Update, Relations & Hybrid Search | v1.1 | 7/7 | Complete | 2026-10-23 | | 5. Portable Export & Import | v1.1 | 5/5 | Complete | 2026-10-25 | -| 6. Distribution & v0.1.0 Release | v1.1 | 0/? | Not started | - | +| 6. v0.1.0 Release (launch plan #7) | v1.1 | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 5428776..8cd8574 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,12 +3,12 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Hardening & Interop current_phase: 6 -current_phase_name: Distribution & v0.1.0 Release +current_phase_name: v0.1.0 Release (launch plan #7) status: planning -stopped_at: Phase 5 complete, ready to plan Phase 6 +stopped_at: Phase 6 re-scoped to the v0.1.0 release (#7); pre-tag work of #8/#9 in progress on local main last_updated: "2026-10-25T13:51:46.548Z" last_activity: 2026-10-25 -last_activity_desc: Phase 5 complete, transitioned to Phase 6 +last_activity_desc: Phase 5 secured (05-SECURITY.md, threats_open 0); Phase 6 re-scoped per issue #7 state_head: fe18db796014dc9d885859771ee089ce7ba895df progress: total_phases: 4 @@ -24,14 +24,14 @@ progress: See: .planning/PROJECT.md (updated 2026-07-12) **Core value:** An agent can persist a structured memory and retrieve the right one later — across sessions and tools — over a standard MCP interface, with no cloud. -**Current focus:** Phase 5 — Portable Export & Import +**Current focus:** Phase 6 — v0.1.0 Release (launch plan #7) ## Current Position -Phase: 6 — Distribution & v0.1.0 Release -Plan: Not started -Status: Ready to plan -Last activity: 2026-10-25 — Phase 5 complete, transitioned to Phase 6 +Phase: 6 — v0.1.0 Release (launch plan #7) +Plan: Not planned (pre-tag work of issues #8/#9 done directly on local main per owner instruction) +Status: Pre-tag work in progress +Last activity: 2026-10-25 - Completed quick task 261025-lbb: fix MCP serverInfo identity and date the 0.1.0 CHANGELOG entry Progress: [░░░░░░░░░░] 0% (0/4 v1.1 phases) @@ -141,6 +141,7 @@ Recent decisions affecting current work: - [Phase 05]: 05-04: README Export & import section is one ## with ten ### subsections in plan order; sweep cadence documented as the shipped 'then hourly' (SWEEP_INTERVAL 3600 s); tests/cli.rs is realism-only (zero coverage by design, like tests/rest.rs) and pins the exact report strings - [Phase 05]: 05-04: phase-end gates recorded — cargo llvm-cov 95.61% lines (floor 80), D-01 transport grep empty, sql/ + Cargo.lock + .github/ unchanged across all phase-5 commits - [Phase 5]: [05-05]: Gap 1 (DIST-04 backstop truth 2.8) closed with a channel-ordered race test driving Store::export_rows directly — WAL snapshot isolation proven, no production change; every wait bounded by recv_timeout +- [Phase 6 re-scope, 2026-10-25]: Owner decision on issue #7 — v0.1.0 ships from today's `main` (includes Phase 5 export/import) following `docs/reviews/2026-10-24-launch-plan.md` and issues #7-#15. Binary crate renamed `agent-memory-mcp` (executable stays `agent-memory`); DIST-03 (Windows) and DIST-05 (hard musl) move to v0.2.0, musl legs stay best-effort; REST code stays but leaves the README top and demo (#11/#12). Phase 6 requirement set is DIST-06 only. - [Phase 5]: [05-05]: Gap 2 (INTEROP-02 live re-embed) closed by a recorded run of the real binary against local nomic-embed-text (05-05-realism.sh, exit 0: backfilled=3 on first sweep tick, search_mode hybrid, target rank 0) — stays a script, never a cargo test ### Pending Todos @@ -153,7 +154,7 @@ None yet. None open. The v1.0 carried notes are now scheduled in this milestone: -- musl Linux binaries (sqlite-vec BSD typedefs) → DIST-05, Phase 6 (CFLAGS shim spiked in Phase 3). +- musl Linux binaries (sqlite-vec BSD typedefs) → DIST-05, moved to v0.2.0 on 2026-10-25 (#7); musl legs stay best-effort in v0.1.0 (CFLAGS shim spiked in Phase 3). - REST boundary-value hardening (limit/ttl_secs extremes, tag LIKE over-match; 02-REVIEW WR-01/WR-02/WR-07) → API-02/API-03, Phase 3. Watch items: @@ -167,6 +168,7 @@ Watch items: |---|-------------|------|--------|-----------| | 261009-hc1 | Release pipeline on GitHub-hosted runners with SLSA provenance, semver-only trigger, all actions SHA-pinned (closes #1); resolves the release-runner decision Phase 3 D-12 deferred to Phase 6 | 2026-10-09 | 9810e31 | [261009-hc1-move-release-yml-to-github-hosted-runner](./quick/261009-hc1-move-release-yml-to-github-hosted-runner/) | | 261023-v6w | WR-04: reject `memory_list` with `expand_links` and no `limit` as a client error (MCP `invalid_params`, REST 400); user decision option (a) | 2026-10-23 | 969e891 | [261023-v6w-wr-04-reject-memory-list-expand-links-wi](./quick/261023-v6w-wr-04-reject-memory-list-expand-links-wi/) | +| 261025-lbb | MCP `initialize` serverInfo now reports `agent-memory` + crate version, title and repository URL (was `rmcp` 1.8.0); CHANGELOG 0.1.0 dated 2026-10-25 | 2026-10-25 | 8c49f24, be9dc2d | [261025-lbb-fix-mcp-serverinfo-identity-and-date-the](./quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/) | ## Deferred Items @@ -174,7 +176,8 @@ Items acknowledged and carried forward: | Category | Item | Status | Deferred At | |----------|------|--------|-------------| -| Distribution | DIST-03 Windows binaries | Picked up in v1.1 — Phase 6 (spiked Phase 3) | Roadmap (2026-06-24) | +| Distribution | DIST-03 Windows binaries | Moved to v0.2.0 (spiked Phase 3; re-deferred by #7) | Roadmap (2026-06-24); re-deferred 2026-10-25 | +| Distribution | DIST-05 musl as a hard target | Moved to v0.2.0 (best-effort legs in v0.1.0) | Phase 6 re-scope (2026-10-25, #7) | | Search | SEARCH-04 hybrid RRF fusion | Picked up in v1.1 — Phase 4 | Roadmap (2026-06-24) | | Interface | MCP-06 memory_update / relation tools | Picked up in v1.1 — Phase 4 | Roadmap (2026-06-24) | | Distribution | DIST-04 portable export | Picked up in v1.1 — Phase 5 | Roadmap (2026-06-24) | @@ -184,9 +187,9 @@ Items acknowledged and carried forward: ## Session Continuity Last session: 2026-10-25T13:42:17.195Z -Stopped at: Phase 5 complete, ready to plan Phase 6 +Stopped at: Phase 6 re-scoped; pre-tag work of #8/#9 on local main (no push/tag) Resume file: None ## Operator Next Steps -- `/gsd-discuss-phase 3` to lock in Phase 3 decisions, then `/gsd-plan-phase 3` +- Owner: first manual crates.io publish of both crates, configure Trusted Publishing, then push `main` and tag `v0.1.0` (see issue #9) diff --git a/.planning/phases/05-portable-export-import/05-SECURITY.md b/.planning/phases/05-portable-export-import/05-SECURITY.md new file mode 100644 index 0000000..1d7e9c0 --- /dev/null +++ b/.planning/phases/05-portable-export-import/05-SECURITY.md @@ -0,0 +1,105 @@ +--- +phase: 05 +slug: portable-export-import +status: verified +# threats_open = count of OPEN threats at or above workflow.security_block_on severity (the blocking gate) +threats_open: 0 +asvs_level: 1 +block_on: high +created: 2026-10-25 +--- + +# Phase 05 — Security + +> Per-phase security contract: threat register, accepted risks, and audit trail. +> Register merged from the `` blocks of 05-01..05-05 PLAN.md (authored at plan +> time) and the `## Threat Flags` of every SUMMARY (none new). Verified at ASVS L1 (grep depth) +> against `main` @ `b1053c5`. + +--- + +## Trust Boundaries + +| Boundary | Description | Data Crossing | +|----------|-------------|---------------| +| MCP stdio / REST loopback client → service error text | untrusted reader of error bodies | storage-engine detail (table/column names) — must not cross | +| service → stderr log | trusted operator channel | full engine error text | +| database (0700 dir) → export file on disk | the corpus leaves the DB's protection | whole memory corpus (private) | +| store read pool ↔ concurrent writers | WAL snapshot per read transaction | memories + links consistency | +| user-supplied JSONL file → parser | untrusted bytes (hand-edited, truncated, hostile, skewed clock) | memories, links, timestamps | +| file-local keys ↔ target database ids | two id spaces that must never be confused | link endpoints | +| realism run → local Ollama / loopback REST daemon | test-only, synthetic fixtures | synthetic text | + +--- + +## Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation | Status | +|-----------|----------|-----------|----------|-------------|------------|--------| +| T-05-01 | Information Disclosure | engine text in service errors + `/health` | medium | mitigate | `redact_storage_error` (`service.rs:64`) wraps every public service op (store/list/update/link/unlink/forget/import/sweep/search/export); `tests/redaction.rs`; `health_storage_failure_is_500_without_sqlite_text` | closed | +| T-05-02 | Information Disclosure | export file world-readable / in shared tmp | high | mitigate | `export_file.rs:127-128` `create_new(true).mode(0o600)`; temp `{target}.export-tmp-{pid}` beside the target; unit test on mode | closed | +| T-05-03 | DoS (data loss) | half-written export target | medium | mitigate | `sync_all` + `rename` in `commit` (`export_file.rs:90-94`); temp removed on every failure path; commit/abort tests | closed | +| T-05-04 | Tampering | vectors travelling in the file | high | mitigate | `Record` has no vector field, `deny_unknown_fields` (`export.rs:114`); `export_rows` never reads `vec_memories` (`sqlite.rs:874`); `insert_imported` writes `embedding_status = 0`; `memory_records_carry_exactly_the_documented_keys` | closed | +| T-05-05 | Tampering (integrity) | file keys treated as target ids | high | mitigate | keys index only the pass-2 `HashMap` (`service.rs:646`); unmapped/collapsed endpoints counted `links_dropped`; overlapping-rowid kill-test (`edges_touching`) | closed | +| T-05-06 | Tampering | hand-edited `type`/`kind`/`tags` reaching SQL | medium | mitigate | `MemoryType::try_from` / `LinkKind::try_from` at the seam; `tags: Vec`; bound `params![]` only | closed | +| T-05-07 | DoS | parser panic on hostile input | medium | mitigate | serde_json recursion limit; no `unwrap` on untrusted data; `hostile_input_never_panics` (`jsonl.rs:628`); invalid UTF-8 refused in CLI | closed | +| T-05-08 | Tampering / DoS | half-restored corpus on malformed file | high | mitigate | `parse_jsonl` completes before `open_service` (`main.rs:282-285`); `nothing_is_written_when_validation_fails` | closed | +| T-05-09 | Repudiation (UX) | expired rows reported imported | low | mitigate | `expires_at <= now` → `skipped_expired` (`service.rs:651`); boundary test | closed | +| T-05-10 | DoS | pass 1 holds a huge file in RAM | low | accept | see Accepted Risks AR-05-01 | closed | +| T-05-11 | EoP / Information Disclosure | export/import as MCP tool or REST route | high | mitigate (by design) | CLI-only (D-01); grep for `memory_export`/`memory_import`/`/api/export`/`/api/import` in `crates/agent-memory/src` is empty; README states the rule | closed | +| T-05-12 | Information Disclosure | stdout export in scrollback | low | accept | see AR-05-02 | closed | +| T-05-13 | Tampering | source-clock skew | low | mitigate | `clock_skew_message` + `CLOCK_SKEW_TOLERANCE_SECS = 3600` (`service.rs:205-213`) with unit test | closed | +| T-05-14 | Information Disclosure | `SqliteStore::open` failures on CLI stderr | low | accept | see AR-05-03 | closed | +| T-05-15 | Tampering (integrity) | divergent `memories` INSERTs | low | mitigate | single private `insert_row` (`sqlite.rs:622`); exactly one `INSERT INTO memories` in `src` | closed | +| T-05-16 | Repudiation | restored row swept later | low | mitigate (contract) | `store/mod.rs:52-53` fixes `<= now` (import skip) vs `< now` (sweep) | closed | +| T-05-17 | Information Disclosure | engine text on import fault | medium | mitigate | `redact_storage_error("import", _)`; `import_jsonl_storage_failure_is_redacted` | closed | +| T-05-18 | Repudiation (UX) | misread report counts | low | mitigate | README count table (`README.md:711` "not an error"); CLI test pins wording | closed | +| T-05-19 | Tampering (integrity) | export straddling a concurrent `link` | medium | mitigate | one `unchecked_transaction` on one pooled connection spans counts and both scans (`sqlite.rs:837-843`); 05-05 race proof | closed | +| T-05-20 | Information Disclosure | engine text on export fault | medium | mitigate | `redact_storage_error("export", _)`; `export_storage_failure_is_redacted` | closed | +| T-05-21 | DoS (test hang) | race test deadlock | low | mitigate | `recv_timeout(10 s)` on both hand-offs (`tests/export_import.rs:473,504`) | closed | +| T-05-22 | EoP / Information Disclosure | realism run binds the REST daemon | low | mitigate | `127.0.0.1:0`, throwaway DB, `trap cleanup EXIT` (`05-05-realism.sh:70,107`) | closed | +| T-05-23 | Information Disclosure | fixture text to local Ollama / temp dir | low | accept | see AR-05-04 | closed | +| T-05-24 | Repudiation (evidence) | realism checks loosened to pass | low | mitigate | checks fixed in committed `05-05-realism.sh`; raw output in 05-05-SUMMARY | closed | +| T-05-SC | Tampering | cargo supply chain | low | accept | see AR-05-05 | closed | + +*Status: open · closed · open — below high threshold (non-blocking)* +*Severity: critical > high > medium > low — only open threats at or above workflow.security_block_on count toward threats_open* +*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)* + +Related hardening closed by the phase code review (05-REVIEW.md, all fixed before this audit): +WR-02 (unbounded `access_count` could make every later export fail — now rejected `< 0` on +import and the bump saturates, `3fdadad`), WR-03 (dedup probe matched expired-but-unswept rows, +`db11cc8`), IN-02 (redaction proofs for export/import, `1173fde`). + +--- + +## Accepted Risks Log + +| Risk ID | Threat Ref | Rationale | Accepted By | Date | +|---------|------------|-----------|-------------|------| +| AR-05-01 | T-05-10 | Inherent to validate-before-write (D-08); a 4 MiB content line is tested to parse; documented in README. A size cap is not in v0.1.0 scope. | plan 05-03 / 05-04 threat model | 2026-10-25 | +| AR-05-02 | T-05-12 | User-directed output; README points to `--output` (0600, atomic) as the private alternative. | plan 05-02 / 05-04 threat model | 2026-10-25 | +| AR-05-03 | T-05-14 | Startup, operator-facing local stderr outside the `MemoryService` seam; no transport carries it. README scopes redaction to served requests and CLI import/export. | plan 05-01 threat model (orchestrator resolution 4) | 2026-10-25 | +| AR-05-04 | T-05-23 | Synthetic engineering-note fixtures only; nothing user-private; temp dir OS-managed. | plan 05-05 threat model | 2026-10-25 | +| AR-05-05 | T-05-SC | Zero new dependencies in Phase 5; `--locked` builds fail on any `Cargo.lock` drift; rusqlite/libsqlite3-sys pin untouched. | plans 05-01..05-05 threat model | 2026-10-25 | + +*Accepted risks do not resurface in future audit runs.* + +--- + +## Security Audit Trail + +| Audit Date | Threats Total | Closed | Open | Run By | +|------------|---------------|--------|------|--------| +| 2026-10-25 | 25 | 25 | 0 | /gsd-secure-phase 5 (orchestrator, ASVS L1 grep-depth short-circuit; register authored at plan time, so no auditor spawn) | + +--- + +## Sign-Off + +- [x] All threats have a disposition (mitigate / accept / transfer) +- [x] Accepted risks documented in Accepted Risks Log +- [x] `threats_open: 0` confirmed +- [x] `status: verified` set in frontmatter + +**Approval:** verified 2026-10-25 diff --git a/.planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-PLAN.md b/.planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-PLAN.md new file mode 100644 index 0000000..ec4565b --- /dev/null +++ b/.planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-PLAN.md @@ -0,0 +1,262 @@ +--- +phase: quick-261025-lbb +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - crates/agent-memory/src/mcp.rs + - crates/agent-memory/tests/tools.rs + - CHANGELOG.md +autonomous: true +requirements: [QUICK-261025-lbb] +user_setup: [] + +estimate: + tokens: 35000 + raw_tokens: 35000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "A real JSON-RPC initialize over the stdio of the built `agent-memory serve` binary returns result.serverInfo.name == \"agent-memory\" and result.serverInfo.version == the agent-memory-mcp package version (env!(\"CARGO_PKG_VERSION\") = 0.1.0). It no longer reports the SDK identity (rmcp / 1.8.0)" + - "The same serverInfo also carries title \"Agent Memory\" and websiteUrl \"https://github.com/UnityInFlow/agent-memory\"" + - "Everything else in the initialize reply is unchanged: tools capability enabled, same instructions string, same protocolVersion negotiation, stdout still carries only JSON-RPC (stdio_purity passes)" + - "An integration test drives the real binary over stdio and pins serverInfo name, version, title and websiteUrl, so a regression back to the SDK identity fails CI" + - "CHANGELOG.md's 0.1.0 heading reads exactly `## [0.1.0] - 2026-10-25`, its ### Fixed list gains one serverInfo-identity bullet, and the [0.1.0] compare-link footer is unchanged" + - "cargo fmt --all -- --check, cargo clippy --workspace --all-targets --locked -- -D warnings, and cargo test --workspace --locked are all green on the final HEAD. Cargo.lock is unchanged" + - "SUMMARY.md records the exact serverInfo JSON captured from a live stdio initialize after the fix, the pre-fix value, both commit SHAs, and the fmt/clippy/test results with the real test counts" + artifacts: + - path: "crates/agent-memory/src/mcp.rs" + provides: "ServerHandler::get_info reporting the agent-memory identity" + contains: "Implementation::new(\"agent-memory\"" + - path: "crates/agent-memory/tests/tools.rs" + provides: "stdio handshake test pinning serverInfo; Server::initialize returns the initialize reply" + contains: "initialize_reports_agent_memory_server_info_over_stdio" + - path: "CHANGELOG.md" + provides: "dated 0.1.0 entry plus a Fixed bullet for the serverInfo identity" + contains: "## [0.1.0] - 2026-10-25" + key_links: + - from: "crates/agent-memory/src/mcp.rs (get_info)" + to: "crates/agent-memory/Cargo.toml [package] version" + via: "env!(\"CARGO_PKG_VERSION\") is evaluated in package agent-memory-mcp, the package that owns mcp.rs" + pattern: "env!\\(\"CARGO_PKG_VERSION\"\\)" + - from: "crates/agent-memory/tests/tools.rs" + to: "the agent-memory binary's initialize reply" + via: "Server::spawn + Server::initialize over piped stdio, asserting /result/serverInfo" + pattern: "/result/serverInfo" +--- + + +Make agent-memory's MCP `initialize` reply identify agent-memory itself, instead of the rmcp SDK it is built on. Pin the fix with a stdio test, and date the 0.1.0 CHANGELOG entry. + +Purpose: MCP clients show `serverInfo` in their server lists and write it to their logs. Today every client sees `{"name":"rmcp","version":"1.8.0"}`, because `get_info` takes the SDK's own build-env identity. That makes a v0.1.0 install impossible to tell apart from any other rmcp server, and it reports the wrong version. This fix has to land before the v0.1.0 tag (Phase 6, launch plan #7). + +Output: a corrected `get_info`, a stdio regression test, a dated CHANGELOG with a Fixed bullet, two local commits on `main`, and a SUMMARY.md that holds the live serverInfo capture and the gate counts. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@CLAUDE.md + + +These were taken from the code at HEAD adea39e. The line numbers are from before any change, so re-grep after editing. + +Crate and version facts (they decide what `env!("CARGO_PKG_VERSION")` resolves to): +- `crates/agent-memory/Cargo.toml` is package `agent-memory-mcp`, `version = "0.1.0"`, and has `repository.workspace = true`. The workspace `repository` is `https://github.com/UnityInFlow/agent-memory`. It has ONE target, `[[bin]] name = "agent-memory"` at `src/main.rs`, and no lib target. +- `crates/agent-memory/src/mcp.rs` is `mod mcp;` of `src/main.rs`, which puts it in package `agent-memory-mcp`. Inside it, `env!("CARGO_PKG_VERSION")` is agent-memory-mcp's version (0.1.0) and `env!("CARGO_PKG_REPOSITORY")` is the workspace repository URL. The core crate `agent-memory-core` is also 0.1.0, but it is not where this code lives. +- The integration tests in `crates/agent-memory/tests/*.rs` compile in the same package, so `env!("CARGO_PKG_VERSION")` in `tests/tools.rs` is the same value the server reports. + +crates/agent-memory/src/mcp.rs: +- Line 12: `use rmcp::model::{CallToolResult, Content, Implementation, ServerCapabilities, ServerInfo};`. `Implementation` is already imported. +- Lines 495-509: `#[tool_handler(router = self.tool_router)] impl ServerHandler for MemoryMcp { fn get_info(&self) -> ServerInfo { ... } }`. The body starts from `ServerInfo::default()`, with a comment explaining that ServerInfo/InitializeResult is `#[non_exhaustive]`. It sets `info.capabilities` (tools enabled), sets `info.server_info` with rmcp's build-env constructor on line 502 (THE BUG), sets `info.instructions = Some("Persistent typed agent memory: store, list, search, update, link/unlink, and forget typed memories.")`, and returns `info`. +- The inline `#[cfg(test)] mod tests` starts at line 511. It is not needed for this task. + +rmcp 1.8.0 (checked in `~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rmcp-1.8.0/src/model.rs:996-1066`): +- `#[non_exhaustive] pub struct Implementation { name: String, title: Option, version: String, description: Option, icons: Option>, website_url: Option }` uses `#[serde(rename_all = "camelCase")]`, and its `None` options are skipped when serializing. On the wire the fields are `name`, `title`, `version`, `websiteUrl`, in that order. +- Because it is `#[non_exhaustive]`, a struct literal is not allowed outside rmcp. Use the constructors: `Implementation::new(name: impl Into, version: impl Into)`, then the consuming builders `.with_title(..)`, `.with_website_url(..)`, `.with_description(..)` and `.with_icons(..)`. +- `impl Default for Implementation` delegates to the build-env constructor, which reads rmcp's OWN `CARGO_CRATE_NAME` and `CARGO_PKG_VERSION`. That is why `ServerInfo::default()` plus that constructor yields rmcp/1.8.0. + +crates/agent-memory/tests/tools.rs (the stdio harness): +- `fn binary_path()` uses `env!("CARGO_BIN_EXE_agent-memory")` (line 12). +- `struct Server { child, stdin, stdout }` provides `Server::spawn(&db_path)`, which runs `serve --db` with the dead Ollama URL `http://127.0.0.1:9` and nulls stderr. It also provides `send(&json)`, `read_response(id) -> serde_json::Value` and `shutdown(self)`. +- `fn initialize(&mut self)` (lines 69-87) sends the initialize frame (id 1, protocolVersion "2025-11-25", clientInfo tools-test), then throws away the reply via `let _ = self.read_response(1);` (line 80), then sends `notifications/initialized`. +- Seven existing tests call `server.initialize();` as a statement (lines 125, 179, 241, 306, 352, 423, 492). +- `fn tool_text` ends at about line 117. The first `#[test]` is at line 119. +- `crates/agent-memory/tests/stdio_purity.rs` is the MCP-05 gate (stdout is only JSON-RPC). Leave it unchanged. Its single purpose is the purity gate, and it must keep passing. + +CHANGELOG.md: +- Line 11 is the 0.1.0 release heading. It currently carries a placeholder status word instead of a date, and that is the only occurrence of the word in the file. +- Lines 13-14 hold the "Generated from `git log v0.0.1..main` (133 commits)" sentence. Leave it untouched. +- `### Fixed` is at line 80. Its bullets are plain `- ` sentences, with backticks for identifiers and no bold. The last bullet is "Pre-migration backups are race-safe across processes opening the same database." (about lines 92-93), followed by a blank line and then `### Security` at line 94. +- Footer line 132: `[0.1.0]: https://github.com/UnityInFlow/agent-memory/compare/v0.0.1...v0.1.0`. It is already consistent and must stay unchanged. + +Pre-fix evidence, captured by the planner on 2026-10-25 from target/debug/agent-memory over stdio: `{"name": "rmcp", "version": "1.8.0"}`. + +Working tree hazards: `.planning/config.json` is modified, and `.claude/`, `.gitignore`, `.gsd/`, `.mcp.json`, `.planning/research/.cache/`, `10-agent-memory.md`, `CLAUDE.md`, `claude-code-harness-engineering-guide-v2.md` and `docs/` are untracked. None of them may be staged. Stage by explicit path only. Never use `git add -A`, `git add .` or `git commit -a`. + + + + + + + Task 1: End-to-end serverInfo identity over real stdio: pin it in a test (RED), fix get_info (GREEN), commit + crates/agent-memory/tests/tools.rs, crates/agent-memory/src/mcp.rs + + - Test `initialize_reports_agent_memory_server_info_over_stdio`: spawn the real binary, run the initialize handshake, and read `/result/serverInfo` from the reply: + - `name` == "agent-memory" + - `version` == env!("CARGO_PKG_VERSION") + - `title` == "Agent Memory" + - `websiteUrl` == "https://github.com/UnityInFlow/agent-memory" + - Before the fix, the test FAILS because name is "rmcp" and version is "1.8.0". After the fix, it passes. + - The seven existing tools tests and stdio_purity still pass with no other changes. + + + RED step, in crates/agent-memory/tests/tools.rs: + 1. Change `Server::initialize` so it returns the initialize reply (`-> serde_json::Value`) and no longer discards it. Bind the result of `read_response(1)` to a local. Send the `notifications/initialized` frame exactly as now, then return the local. Leave the seven existing `server.initialize();` call sites as they are. A discarded `serde_json::Value` statement is fine, so do not rewrite them. + 2. Add `#[test] fn initialize_reports_agent_memory_server_info_over_stdio()` just before the first existing `#[test]` (after `tool_text`). It needs a doc comment saying it pins the MCP identity clients display and log, so the reply cannot fall back to the SDK's own name and version. + 3. Test steps: create a `tempfile::tempdir()` with `memory.db`, call `Server::spawn`, and call `let response = server.initialize();`. Take `response.pointer("/result/serverInfo")`, using `unwrap_or_else(|| panic!(...))` with the full response in the message (the file's existing panic-with-context idiom). Then assert the four fields from ``, each with `assert_eq!` against `Some(...)` of the `as_str()` value, and finish with `server.shutdown()`. Compare the version with `env!("CARGO_PKG_VERSION")`, never a "0.1.0" literal, so the test survives version bumps. + 4. Run the test and confirm it FAILS on name/version. Record the failure line for the SUMMARY. Do not commit the red state: the CLAUDE.md rule is never commit failing tests. + + GREEN step, in crates/agent-memory/src/mcp.rs `get_info` (about line 502): + 5. Replace the right-hand side of the `info.server_info =` assignment with `Implementation::new("agent-memory", env!("CARGO_PKG_VERSION"))`, chained with `.with_title("Agent Memory")` and `.with_website_url(env!("CARGO_PKG_REPOSITORY"))`. + - `Implementation` is `#[non_exhaustive]` like ServerInfo, so build it through rmcp's constructor and builders, never a struct literal. Keep the existing ServerInfo-from-Default plus field-by-field mutation pattern. + - `env!("CARGO_PKG_VERSION")` here resolves to package agent-memory-mcp (crates/agent-memory/Cargo.toml, 0.1.0), the package that owns mcp.rs. The test compares the same macro in the same package. + - The name is the literal "agent-memory", the binary and product name. Do not use `CARGO_PKG_NAME`, which would give "agent-memory-mcp", the crates.io package name. + - Title "Agent Memory" is a planner discretion choice: a human-readable display title, which MCP clients show in UI. + - `CARGO_PKG_REPOSITORY` keeps the URL single-sourced from the workspace manifest. + - Do not set description or icons. They are out of scope. + 6. Extend the comment above `get_info` by one sentence explaining that rmcp's default Implementation reports the SDK's own crate name and version, so ours is set explicitly. Describe the SDK default by concept only; do not name the old constructor in the comment. + 7. Leave capabilities, instructions and everything else in get_info unchanged. Add no `unwrap()` / `expect()` to production code. The `env!` macros resolve at compile time and cannot fail at runtime. + + Gates, then commit: + 8. Run `cargo fmt --all`, then the commands in ``. All must be green. + 9. Stage ONLY the two files with `git add -- crates/agent-memory/src/mcp.rs crates/agent-memory/tests/tools.rs`. Confirm `git diff --cached --name-only` lists exactly those two paths. + 10. Commit on local `main` with subject `fix(mcp): report agent-memory serverInfo identity`. The body is 1-3 lines: initialize reported rmcp 1.8.0; it now reports agent-memory with the package version, a title and the repository URL; it is pinned by a stdio test. End the message with the line `Co-Authored-By: Claude Opus 5.5 `. Do not push, tag or publish. + + + cd /Users/jirihermann/Documents/workspace-1-ideas/unity-in-flow-ai/10-agent-memory && cargo fmt --all -- --check && cargo clippy --workspace --all-targets --locked -- -D warnings && cargo test -p agent-memory-mcp --locked --test tools && cargo test -p agent-memory-mcp --locked --test stdio_purity && test "$(grep -vE '^\s*//' crates/agent-memory/src/mcp.rs | grep -c 'from_build_env')" -eq 0 && grep -c 'Implementation::new("agent-memory", env!("CARGO_PKG_VERSION"))' crates/agent-memory/src/mcp.rs && git diff --quiet -- Cargo.lock && git log -1 --format=%s | grep -qx 'fix(mcp): report agent-memory serverInfo identity' + + + - The new stdio test failed before the fix with a "rmcp" name mismatch, and passes after it. + - All `tests/tools.rs` tests and `stdio_purity` pass. + - fmt and clippy are clean. Cargo.lock is unchanged. + - mcp.rs non-comment lines contain no SDK build-env identity call. + - Exactly one new commit on local `main` touches only `crates/agent-memory/src/mcp.rs` and `crates/agent-memory/tests/tools.rs`, with the required subject and the Co-Authored-By trailer. + + + + + Task 2: Date the 0.1.0 CHANGELOG entry and add the serverInfo Fixed bullet, commit + CHANGELOG.md + + 1. On line 11 of CHANGELOG.md, replace the placeholder status word after `## [0.1.0] - ` with `2026-10-25`, so the line reads exactly `## [0.1.0] - 2026-10-25`. This matches the `## [0.0.1] - 2026-07-03` style below it. + 2. Append one bullet at the END of the `### Fixed` list: after the "Pre-migration backups are race-safe…" bullet, before the blank line that precedes `### Security`. Use the existing style: a plain `- ` sentence, backticks for identifiers, no bold, wrapped at about 80 columns like its neighbours. Content: the MCP `initialize` reply now identifies the server as `agent-memory` with the release's own version, a display title and the repository URL. Before, it reported the MCP SDK's identity (`rmcp` `1.8.0`). Suggested wording: "The MCP `initialize` reply now identifies the server as `agent-memory`, with this release's version, a display title and the repository URL, instead of the MCP SDK's own identity (`rmcp` `1.8.0`)." + 3. Change nothing else. Leave these exactly as they are: + - the "Generated from `git log v0.0.1..main` (133 commits)" sentence; + - the `[0.1.0]:` / `[0.0.1]:` link footer, which is already consistent with a v0.1.0 tag; + - the README. This is a Fixed item, not a **Changed** contract item, so the README's "Behavioral changes in v0.1.0" section does not apply. + 4. Stage ONLY `CHANGELOG.md` with `git add -- CHANGELOG.md`, and confirm that `git diff --cached --name-only` prints exactly `CHANGELOG.md`. + 5. Commit on local `main` with subject `docs(changelog): date 0.1.0 as 2026-10-25`. The body is one line noting the dated heading and the new serverInfo Fixed entry. End the message with the line `Co-Authored-By: Claude Opus 5.5 `. Do not push, tag or publish. + + + cd /Users/jirihermann/Documents/workspace-1-ideas/unity-in-flow-ai/10-agent-memory && test "$(grep -c '^## \[0\.1\.0\] - 2026-10-25$' CHANGELOG.md)" -eq 1 && test "$(grep -c 'Unreleased' CHANGELOG.md)" -eq 0 && grep -q '^\[0\.1\.0\]: https://github.com/UnityInFlow/agent-memory/compare/v0\.0\.1\.\.\.v0\.1\.0$' CHANGELOG.md && awk '/^### Fixed/{f=1;next} /^### /{f=0} f' CHANGELOG.md | grep -q 'serverInfo\|identifies the server as `agent-memory`' && git log -1 --format=%s | grep -qx 'docs(changelog): date 0.1.0 as 2026-10-25' && test "$(git show --name-only --format= HEAD)" = "CHANGELOG.md" + + + - CHANGELOG.md line 11 reads `## [0.1.0] - 2026-10-25`, and the placeholder status word is gone from the file. + - The 0.1.0 `### Fixed` section ends with the serverInfo identity bullet. + - The compare-link footer and the generated-from sentence are untouched. + - One new commit on local `main` touches only CHANGELOG.md, with the required subject and the Co-Authored-By trailer. + + + + + Task 3: Final gates on HEAD and live stdio serverInfo capture recorded in SUMMARY.md + .planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-SUMMARY.md + + 1. On the final HEAD (after both commits), run the three required gates in full and keep their output: + - `cargo fmt --all -- --check` + - `cargo clippy --workspace --all-targets --locked -- -D warnings` + - `cargo test --workspace --locked` + For tests, sum every `test result:` line (passed / failed / ignored, plus the number of test binaries). Report the actual numbers. Never estimate them, and never copy the 76/0/1 baseline from CLAUDE.md. For clippy, report the warning count, which must be 0, and the exit status. + The RTK hook may compress cargo output. If the `` awk reports `binaries=0`, the `test result:` lines were filtered, not missing. Re-run as `rtk proxy cargo test --workspace --locked` to get the raw lines. + 2. Rebuild the binary with `cargo build --locked -p agent-memory-mcp`. Confirm `target/debug/agent-memory` has an mtime later than the Task 1 commit. The planner's probe ran against a binary built at 15:15, before the fix, so a stale binary would silently report rmcp again (see the `gotchas` skill: "a rebuilt fix appears not to have taken effect"). + 3. Live capture: create a fresh `mktemp -d` directory. Pipe one initialize frame into the built binary: + `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.0"}}}` + Run it as `AGENT_MEMORY_OLLAMA_URL=http://127.0.0.1:9 target/debug/agent-memory serve --db /memory.db`, with stderr discarded. Take the first stdout line and extract `.result.serverInfo` with `jq -c`. Then remove the temp dir. + - Expected, byte for byte: `{"name":"agent-memory","title":"Agent Memory","version":"0.1.0","websiteUrl":"https://github.com/UnityInFlow/agent-memory"}`. + - If the capture shows `rmcp`, stop: the binary is stale, so rebuild and re-probe. Never record a value you did not observe. + 4. Write SUMMARY.md, following the summary template. It must contain: + a. the exact compact serverInfo JSON captured in step 3, plus the command used; + b. the pre-fix value `{"name":"rmcp","version":"1.8.0"}` (planner's probe) for contrast; + c. the RED failure line observed in Task 1; + d. both commit SHAs and subjects; + e. the fmt result, the clippy result (exit 0 / 0 warnings), and the test totals from step 1; + f. the files changed; + g. which crate `env!("CARGO_PKG_VERSION")` resolves in: package agent-memory-mcp (crates/agent-memory/Cargo.toml, 0.1.0), not agent-memory-core; + h. the title "Agent Memory" discretion choice; + i. a note that nothing was pushed, tagged or published. + Do not stage or commit SUMMARY.md together with any of the forbidden paths listed in the context. The quick-task workflow's docs commit owns `.planning/quick/` artifacts. + + + cd /Users/jirihermann/Documents/workspace-1-ideas/unity-in-flow-ai/10-agent-memory && cargo fmt --all -- --check && cargo clippy --workspace --all-targets --locked -- -D warnings && cargo test --workspace --locked 2>&1 | awk '/^test result:/{b++; p+=$4; f+=$6; i+=$8} END{printf "binaries=%d passed=%d failed=%d ignored=%d\n", b, p, f, i; exit (f>0 || b==0)}' && cargo build --locked -p agent-memory-mcp && D=$(mktemp -d) && printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.0"}}}' | AGENT_MEMORY_OLLAMA_URL=http://127.0.0.1:9 target/debug/agent-memory serve --db "$D/memory.db" 2>/dev/null | head -1 | jq -c '.result.serverInfo' | tee "$D/si.json" && grep -qx '{"name":"agent-memory","title":"Agent Memory","version":"0.1.0","websiteUrl":"https://github.com/UnityInFlow/agent-memory"}' "$D/si.json" && rm -rf "$D" && grep -q '"name":"agent-memory"' .planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-SUMMARY.md + + + - All three gates are green on the final HEAD. The test totals come from the real run and show 0 failed. + - The live stdio capture equals the expected serverInfo JSON. + - SUMMARY.md contains that JSON verbatim, the pre-fix value, the RED line, both SHAs, and the fmt/clippy/test results. + - Nothing is pushed, tagged or published. + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| MCP client → agent-memory stdio | The client sends `initialize`. The server's reply is what the client displays and logs as the server's identity. | +| Server stdout | It must carry only JSON-RPC frames (MCP-05). The new serverInfo fields serialize inside the existing initialize frame. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-261025-lbb-01 | Spoofing | `get_info` in crates/agent-memory/src/mcp.rs | low | mitigate | Report the true identity: name `agent-memory`, version from the owning package via `env!("CARGO_PKG_VERSION")`. `initialize_reports_agent_memory_server_info_over_stdio` pins it, so client logs and audit trails stop pointing at the SDK (rmcp 1.8.0). | +| T-261025-lbb-02 | Information Disclosure | serverInfo `version` / `websiteUrl` | low | accept | The exact version and the public repository URL are already public (GitHub Release, crates.io, CHANGELOG). The reply only reaches the local MCP client that launched the process over stdio. Before the fix, the reply already disclosed the SDK version. MCP defines these fields for exactly this purpose. | +| T-261025-lbb-03 | Tampering | stdout JSON-RPC channel | low | mitigate | No new output path: the fields are serde-serialized inside the existing initialize frame. Task 1 re-runs the unchanged `stdio_purity` gate, and Task 3 re-runs the full workspace suite. | +| T-261025-lbb-04 | Tampering | dependency graph | low | mitigate | There are no new dependencies or installs. Every cargo command uses `--locked`, and Task 1 asserts that `Cargo.lock` is unchanged. | + + + +- `cargo fmt --all -- --check`: clean. +- `cargo clippy --workspace --all-targets --locked -- -D warnings`: exit 0. +- `cargo test --workspace --locked`: 0 failed. The totals are summed from the real `test result:` lines and recorded in SUMMARY.md. +- A live stdio `initialize` against the freshly built binary returns `{"name":"agent-memory","title":"Agent Memory","version":"0.1.0","websiteUrl":"https://github.com/UnityInFlow/agent-memory"}`. +- `git log -2 --format=%s` shows `docs(changelog): date 0.1.0 as 2026-10-25` and then `fix(mcp): report agent-memory serverInfo identity`. Each commit touches only its own files, and both end with the Co-Authored-By trailer. +- `git status` still shows the pre-existing untracked and modified files, none of them committed. Nothing is pushed or tagged. + + + +- MCP clients see agent-memory 0.1.0, not rmcp 1.8.0, and a stdio test prevents a regression. +- The CHANGELOG 0.1.0 entry is dated 2026-10-25 and lists the fix under Fixed. The footer stays consistent. +- Every gate is green on local `main`, with two conventional commits and no push, tag or publish. +- SUMMARY.md holds the observed serverInfo JSON and the real gate counts. + + + +Create `.planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-SUMMARY.md` when done. It must include: +- the exact serverInfo JSON captured from the live stdio `initialize` after the fix, and the command used; +- the pre-fix value; +- the RED failure line; +- both commit SHAs; +- the fmt and clippy results; +- the `cargo test --workspace --locked` totals (binaries / passed / failed / ignored), as reported, not estimated. + diff --git a/.planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-SUMMARY.md b/.planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-SUMMARY.md new file mode 100644 index 0000000..f3d30cb --- /dev/null +++ b/.planning/quick/261025-lbb-fix-mcp-serverinfo-identity-and-date-the/261025-lbb-SUMMARY.md @@ -0,0 +1,172 @@ +--- +phase: quick-261025-lbb +plan: 01 +subsystem: mcp +tags: [mcp, serverInfo, initialize, changelog, v0.1.0] +status: complete +requires: [] +provides: + - "MCP initialize reports agent-memory's own identity (name, version, title, websiteUrl)" + - "stdio regression test initialize_reports_agent_memory_server_info_over_stdio" + - "CHANGELOG 0.1.0 dated 2026-10-25 with a serverInfo Fixed bullet" +affects: [phase-6-v0.1.0-release] +tech-stack: + added: [] + patterns: + - "rmcp #[non_exhaustive] Implementation built through Implementation::new + with_* builders" +key-files: + created: [] + modified: + - crates/agent-memory/src/mcp.rs + - crates/agent-memory/tests/tools.rs + - CHANGELOG.md +decisions: + - "serverInfo name is the literal product/binary name \"agent-memory\", not CARGO_PKG_NAME (which would be the crates.io package name agent-memory-mcp)" + - "serverInfo version and websiteUrl come from env!(CARGO_PKG_VERSION) / env!(CARGO_PKG_REPOSITORY) of package agent-memory-mcp, so they stay single-sourced from the manifests" + - "serverInfo title \"Agent Memory\" (planner discretion: human-readable display title)" +metrics: + duration: "~6 min" + completed: 2026-10-25 + tasks: 3 + files: 3 +actuals: + tokens: 1500 + tasks: 3 + commits: 2 +--- + +# Phase quick-261025-lbb Plan 01: MCP serverInfo identity + dated 0.1.0 CHANGELOG Summary + +agent-memory's MCP `initialize` reply now names agent-memory 0.1.0, with a title and the repository URL, where it used to report the rmcp SDK (`rmcp` / `1.8.0`). A real-binary stdio test pins the fix, and the CHANGELOG's 0.1.0 entry is dated 2026-10-25. + +## Live serverInfo capture + +**Before** (planner's probe of the pre-fix binary, 2026-10-25): + +```json +{"name":"rmcp","version":"1.8.0"} +``` + +**After**: observed from a live stdio `initialize` against the binary rebuilt on HEAD `be9dc2d`. The binary's mtime is 1792938446, later than the Task 1 commit time of 1792938423, so the binary was not stale. + +```json +{"name":"agent-memory","title":"Agent Memory","version":"0.1.0","websiteUrl":"https://github.com/UnityInFlow/agent-memory"} +``` + +Command used: + +```bash +D=$(mktemp -d) +printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.0"}}}' \ + | AGENT_MEMORY_OLLAMA_URL=http://127.0.0.1:9 target/debug/agent-memory serve --db "$D/memory.db" 2>/dev/null \ + | head -1 | jq -c '.result.serverInfo' +rm -rf "$D" +``` + +The rest of the same reply did not change: `protocolVersion` `"2025-11-25"`, `capabilities` `{"tools":{}}`, and the same `instructions` string. + +## Tasks and commits + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | serverInfo identity: RED test, GREEN fix (tracer) | `8c49f243b10af1ed1cf239ba5fe30329ed2f2538` (`fix(mcp): report agent-memory serverInfo identity`) | crates/agent-memory/src/mcp.rs, crates/agent-memory/tests/tools.rs | +| 2 | Date the 0.1.0 CHANGELOG entry and add a Fixed bullet | `be9dc2d62408d540c78d69e56e610a351ab0277a` (`docs(changelog): date 0.1.0 as 2026-10-25`) | CHANGELOG.md | +| 3 | Final gates and live capture | (no code commit; this SUMMARY is for the orchestrator's docs commit) | this file | + +Both commits end with `Co-Authored-By: Claude Opus 5.5 `. + +### RED (Task 1, before the fix) + +`cargo test -p agent-memory-mcp --locked --test tools initialize_reports` failed: + +``` +thread 'initialize_reports_agent_memory_server_info_over_stdio' panicked at crates/agent-memory/tests/tools.rs:134:5: +assertion `left == right` failed + left: Some("rmcp") + right: Some("agent-memory") +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 7 filtered out +``` + +The red state was not committed, per the CLAUDE.md rule that failing tests are never committed. + +### GREEN (Task 1, after the fix) + +- `tests/tools.rs`: 8 passed / 0 failed / 0 ignored. This is the 7 existing tests plus the new one. +- `tests/stdio_purity.rs`: 1 passed / 0 failed / 0 ignored. The file itself is unchanged. + +## What changed + +- `crates/agent-memory/src/mcp.rs` (`get_info`): `info.server_info = Implementation::new("agent-memory", env!("CARGO_PKG_VERSION")).with_title("Agent Memory").with_website_url(env!("CARGO_PKG_REPOSITORY"));`. The comment above it gained one sentence explaining that rmcp's default Implementation reports the SDK's own identity. Capabilities and instructions are unchanged. The change adds no `unwrap()`/`expect()`, because the `env!` macros resolve at compile time. +- `crates/agent-memory/tests/tools.rs`: + - `Server::initialize` now returns the initialize reply (`serde_json::Value`). The seven existing call sites are unchanged. + - New test `initialize_reports_agent_memory_server_info_over_stdio` asserts `name`, `version` (against `env!("CARGO_PKG_VERSION")`, not a literal), `title` and `websiteUrl` at `/result/serverInfo`. +- `CHANGELOG.md`: + - The heading now reads `## [0.1.0] - 2026-10-25`, and the placeholder word is gone from the file. + - One bullet was appended to the end of `### Fixed`. + - The "Generated from …" sentence and the `[0.1.0]:` compare-link footer are untouched. + +**Where `env!("CARGO_PKG_VERSION")` resolves:** `mcp.rs` is `mod mcp;` of `src/main.rs` in package **agent-memory-mcp** (`crates/agent-memory/Cargo.toml`, version 0.1.0), not agent-memory-core. The integration test compiles in the same package, so it compares the same value. `CARGO_PKG_REPOSITORY` is inherited from the workspace (`https://github.com/UnityInFlow/agent-memory`). + +**Title choice:** "Agent Memory" is a planner discretion choice, a human-readable display title for MCP client UIs. Description and icons were deliberately left unset. + +## Final gates (HEAD `be9dc2d`) + +| Gate | Result | +|------|--------| +| `cargo fmt --all -- --check` | clean, exit 0 | +| `cargo clippy --workspace --all-targets --locked -- -D warnings` | exit 0, 0 warnings | +| `cargo test --workspace --locked` | exit 0: **21 test binaries (incl. 1 doc-test), 288 passed / 0 failed / 2 ignored** | +| `cargo build --locked -p agent-memory-mcp` | exit 0 | +| `Cargo.lock` | unchanged (`git diff --quiet adea39e HEAD -- Cargo.lock`) | + +The test totals were summed from the raw `test result:` lines (run via `rtk proxy`). The two ignored tests are one in `migration_hygiene.rs` and one in `semantic.rs`, both pre-existing. Per binary: + +- agent_memory_core unit tests: 63 +- backfill_race: 7 +- decay: 4 +- export_import: 20 +- fallback: 5 +- hybrid: 22 +- import: 2 +- migration_hygiene: 8 + 1 ignored +- redaction: 10 +- relations: 27 +- semantic: 4 + 1 ignored +- store: 8 +- ttl: 4 +- update: 9 +- validation: 5 +- agent-memory unit tests (main.rs): 72 +- cli: 6 +- rest: 3 +- stdio_purity: 1 +- tools: 8 +- doc-tests: 0 + +## Deviations from Plan + +None. The plan was executed exactly as written. + +## Threat model + +- T-261025-lbb-01 (spoofing): mitigated. The reply now reports the true identity, pinned by the stdio test. +- T-261025-lbb-03 (stdout tampering): mitigated. `stdio_purity` still passes, and there is no new output path. +- T-261025-lbb-04 (dependency graph): mitigated. There are no new dependencies, every command used `--locked`, and Cargo.lock is unchanged. +- T-261025-lbb-02 (information disclosure): accepted, as planned. + +No new threat surface. + +## Known Stubs + +None. + +## Not done (by instruction) + +Nothing was pushed, tagged or published. STATE.md and ROADMAP.md were not updated, and no docs artifacts were committed (the orchestrator owns the docs commit). The pre-existing modified and untracked files (`.planning/config.json`, `.claude/`, `.gitignore`, `.gsd/`, `.mcp.json`, `.planning/research/.cache/`, `10-agent-memory.md`, `CLAUDE.md`, `claude-code-harness-engineering-guide-v2.md`, `docs/`) were left unstaged. + +## Self-Check: PASSED + +- FOUND: crates/agent-memory/src/mcp.rs contains `Implementation::new("agent-memory", env!("CARGO_PKG_VERSION"))` +- FOUND: crates/agent-memory/tests/tools.rs contains `initialize_reports_agent_memory_server_info_over_stdio` +- FOUND: CHANGELOG.md contains `## [0.1.0] - 2026-10-25` +- FOUND: commit 8c49f24, commit be9dc2d diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d360e0b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,136 @@ +# Changelog + +All notable changes to agent-memory are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +While the version is `0.x`, a minor bump (`0.0.x` → `0.1.0`) may change contracts; +every such change is listed under **Changed** and in the README's +*Behavioral changes in v0.1.0* section. + +## [0.1.0] - 2026-10-25 + +Generated from `git log v0.0.1..main` (133 commits). Highlights: update and +relations tools, hybrid search, and portable JSONL export/import. + +### Added + +- **`memory_update` MCP tool and `PATCH /api/memories/{id}`.** Merge-patch + semantics: an omitted field is unchanged, `null` clears it. A content change + re-embeds in the same writer transaction, and the edit counts as a fresh + access. Unknown or immutable fields are refused on both transports. +- **Relations: `memory_link` / `memory_unlink`** with three kinds: + `relates_to` (symmetric), `supersedes` and `caused_by`. Also exposed as + `POST /api/links` and `DELETE /api/links/{from_id}/{to_id}/{kind}`. Linking + twice is idempotent, a self-link is a client error, and a missing endpoint is + reported as data rather than a 500. `memory_forget` and the TTL sweep remove + a memory's links with it. +- **`expand_links`** on `memory_search` and `memory_list` attaches up to 10 + newest related memories per result, labelled by direction (`superseded_by`, + `caused` for incoming edges). `memory_list` with `expand_links` requires a + `limit`. +- **Hybrid search.** When Ollama is reachable, keyword (FTS5 bm25) and semantic + (KNN) rankings are fused with Reciprocal Rank Fusion (k = 60), then + decay-weighted, reported as `"search_mode": "hybrid"`. +- **Forgiving keyword leg.** The raw FTS5 query runs first. If FTS5 cannot parse + it, it is retried once with its words quoted and OR-joined, so punctuated, + hyphenated and colon-bearing queries (`sqlite-vec`, `TODO: auth`) are + answered instead of failing. +- **Portable JSONL export/import (format v1).** `agent-memory export [--output PATH]` + writes a header, then one record per memory, then one record per link, all + from one consistent read snapshot. Embedding vectors and other derived fields + are excluded. `agent-memory import --from jsonl FILE` validates the whole file + before it writes anything. It is idempotent on re-import, safe on a non-empty + database, and remaps file-local link keys to target ids. Expired rows are + skipped and counted. Restored rows are re-embedded against the importing + database's own model by the background sweep. Export and import are CLI + subcommands only (no MCP tool, no REST route). +- **Private, atomic `--output`.** The export is written to a sibling temp file + created with mode `0600`, then `sync_all`-ed and renamed over the target. No + failure path leaves a half-written file. +- **Schema migration 0003 (`memory_links`)** with an automatic pre-migration + online backup (`memory.db.backup-pre-NNNN-YYYYMMDD`). If the backup cannot be + written, the database is not migrated. Concurrent first opens are safe. +- **Newer-schema refusal.** A database written by a newer agent-memory is + refused at startup and left byte-untouched. The error names both schema + versions. +- **Release provenance.** Every release tarball and `SHA256SUMS.txt` carries a + signed SLSA build-provenance attestation, verifiable with + `gh attestation verify agent-memory-.tar.gz --repo UnityInFlow/agent-memory` (#1). + +### Changed + +- **The binary crate is now `agent-memory-mcp`.** The crates.io name + `agent-memory` belongs to an unrelated project. Install with + `cargo install agent-memory-mcp`. The executable, the Homebrew formula and + the release tarball names stay `agent-memory`. `agent-memory-core` is + published beside it (#9). +- **Tag filtering is an exact, case-sensitive match** instead of a substring + match: `tag=rust` no longer matches `rustling`. This applies to search (both + modes) and list. +- **Out-of-bounds `limit` (1–200) and `ttl_secs` (1–3155760000) are client + errors** (HTTP 400 / MCP `invalid_params`) carrying the valid range, instead + of being clamped or accepted. +- **`search_mode: "semantic"` is no longer emitted.** With an embedder, + searches are `"hybrid"`. `"keyword"` (embedder down) and the envelope shape + are unchanged. Result order can differ from v0.0.1's embedding-only ranking. +- **Release pipeline** moved to GitHub-hosted runners, triggered only by exact + `vX.Y.Z` tags. All actions are pinned to commit SHAs (#1). + +### Fixed + +- The keyword fallback (Ollama down) now honours the tag filter. +- A malformed or over-deep FTS5 expression is classified as client input, never + a server fault. +- Re-importing a GSD `STATE.md` with no source is idempotent (NULL-safe + dedup). +- The background embedding backfill never overwrites a row that was edited or + forgotten while its vector was being computed. +- `relates_to` is stored as one symmetric relation and can be unlinked from + either end. +- Pre-migration backups are race-safe across processes opening the same + database. +- The MCP `initialize` reply now identifies the server as `agent-memory`, with + this release's version, a display title and the repository URL, instead of + the MCP SDK's own identity (`rmcp` `1.8.0`). + +### Security + +- **Storage-engine errors are redacted on every surface.** `store`, `list` + (including `/health`), `search`, `update`, `link`/`unlink`, `forget`, + `import`, `export` and the sweep now report + ` failed due to a storage error (details are in the server log)` + instead of echoing SQLite text with table and column names. The detail is + only in the server's stderr log. +- The JSONL importer treats its input as untrusted. Hostile input (deep + nesting, NUL bytes, overflowing numbers, negative `access_count`) is either + rejected with a line-numbered error or stored as plain data. It never + panics. File keys can never address + existing rows. Model-mismatched vectors cannot be imported, because the format + has no vector field. +- Added [`SECURITY.md`](SECURITY.md) with a private vulnerability-reporting + route. + +## [0.0.1] - 2026-07-03 + +First release. + +### Added + +- MCP stdio server with `memory_store`, `memory_search`, `memory_list` and + `memory_forget` over embedded SQLite (no cloud, no account). +- Six memory types: `DECISION`, `PATTERN`, `ERROR`, `TODO`, `ARCHITECTURE`, + `CONSTRAINT`. +- Exponential decay scoring, which fades ranking without deleting anything. + Optional TTL expiry with a background sweep. +- Semantic search via local Ollama (`nomic-embed-text`), with automatic FTS5 + keyword fallback. +- `agent-memory import --from gsd-state .planning/STATE.md`. +- REST API (`agent-memory serve-rest`), bound to loopback by default. + Non-loopback addresses are refused unless `--allow-remote` is passed, because + the API has no authentication. +- Pre-built binaries for macOS (arm64/x86_64) and Linux gnu (x86_64/aarch64), + plus the Homebrew formula `unityinflow/tap/agent-memory`. + +[0.1.0]: https://github.com/UnityInFlow/agent-memory/compare/v0.0.1...v0.1.0 +[0.0.1]: https://github.com/UnityInFlow/agent-memory/releases/tag/v0.0.1 diff --git a/Cargo.lock b/Cargo.lock index 0167842..9275fad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,44 +3,44 @@ version = 4 [[package]] -name = "agent-memory" -version = "0.0.1" +name = "agent-memory-core" +version = "0.1.0" dependencies = [ "agent-memory-core", - "anyhow", - "axum", - "clap", - "dirs", + "bytemuck", + "chrono", + "r2d2", + "r2d2_sqlite", "reqwest", - "rmcp", "rusqlite", + "rusqlite_migration", "serde", "serde_json", + "sqlite-vec", "tempfile", + "thiserror", "tokio", "tracing", - "tracing-subscriber", ] [[package]] -name = "agent-memory-core" -version = "0.0.1" +name = "agent-memory-mcp" +version = "0.1.0" dependencies = [ "agent-memory-core", - "bytemuck", - "chrono", - "r2d2", - "r2d2_sqlite", + "anyhow", + "axum", + "clap", + "dirs", "reqwest", + "rmcp", "rusqlite", - "rusqlite_migration", "serde", "serde_json", - "sqlite-vec", "tempfile", - "thiserror", "tokio", "tracing", + "tracing-subscriber", ] [[package]] @@ -240,9 +240,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures", diff --git a/README.md b/README.md index 3124d53..6cd4fc6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Tool 10 in the [UnityInFlow](https://github.com/UnityInFlow) ecosystem. +- crates.io: [`agent-memory-mcp`](https://crates.io/crates/agent-memory-mcp) (installs the `agent-memory` executable) +- MCP Registry name: `mcp-name: io.github.UnityInFlow/agent-memory` + AI agents forget everything between sessions. `agent-memory` is a small [MCP](https://modelcontextprotocol.io) stdio server backed by an embedded SQLite database: any MCP-speaking runtime can store, search, list, update, and forget @@ -56,6 +59,17 @@ tar -xzf agent-memory-.tar.gz ./agent-memory --version ``` +### From crates.io + +Requires a stable Rust toolchain. The crate is `agent-memory-mcp` (the name +`agent-memory` on crates.io belongs to an unrelated project); the executable it +installs is still `agent-memory`: + +```bash +cargo install agent-memory-mcp --locked +agent-memory --version +``` + ### From source Requires a stable Rust toolchain (edition 2021). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..07753fd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,63 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +|---------|-----------| +| 0.1.x | Yes | +| < 0.1 | No. Please upgrade. | + +Fixes land on `main` and ship in the next 0.1.x release. + +## Reporting a vulnerability + +**Please do not open a public issue for a security problem.** + +Report it privately through GitHub's private vulnerability reporting: open the +repository's **Security** tab → **Report a vulnerability**, or go to +. + +Please include the agent-memory version (`agent-memory --version`), your +platform, the transport involved (MCP stdio, REST, or the CLI), and steps to +reproduce. We aim to acknowledge a report within 7 days. We will agree a +disclosure date with you and credit you in the advisory unless you ask us not +to. + +## Scope + +agent-memory is a **local** binary. It runs an MCP stdio server, an optional +REST daemon and a set of CLI subcommands over an embedded SQLite file on your +own machine. It has no cloud service and no accounts, and it makes no network +calls except to the Ollama URL you configure (`http://localhost:11434` by +default). + +In scope, for example: + +- Leaking storage-engine detail (SQLite text, table or column names) through + MCP or REST error responses. These are redacted by design. +- The REST daemon accepting a non-loopback bind without `--allow-remote`, or + any path that exposes it beyond loopback by default. +- A crafted file passed to `agent-memory import` (JSONL or GSD `STATE.md`) + causing a panic, memory corruption, writes outside the database, phantom + links to existing rows, or a partially applied import. +- `agent-memory export --output` creating a file readable by other users + (it must be `0600` on Unix) or leaving a half-written file behind. +- Data written by one MCP client being lost or silently altered by another + client sharing the same database. +- Tampering with a release artifact that still passes `SHA256SUMS.txt` or + `gh attestation verify`. + +Out of scope (by design): + +- **The REST API has no authentication.** It binds to loopback by default and + refuses other addresses unless `--allow-remote` is passed. Anyone who can + reach a daemon you exposed with `--allow-remote` can read and write every + memory. Put your own authenticating proxy in front of it if you need remote + access. +- Other processes running as your user. They can read the database file and + its backups directly, just as they can read any file you own. +- Export written to **stdout** ending up in terminal scrollback or shell logs. + Use `--output` for a private file. +- Content you store in a memory being sent to the Ollama instance you + configured. That is how semantic search works. +- Vulnerabilities in Ollama or in third-party MCP clients themselves. diff --git a/crates/agent-memory-core/Cargo.toml b/crates/agent-memory-core/Cargo.toml index 73d605b..a1cb239 100644 --- a/crates/agent-memory-core/Cargo.toml +++ b/crates/agent-memory-core/Cargo.toml @@ -1,11 +1,14 @@ [package] name = "agent-memory-core" -version = "0.0.1" +version = "0.1.0" edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true description = "Core domain, store, and decay logic for the agent-memory MCP server" +readme = "../../README.md" +keywords = ["mcp", "memory", "agent", "sqlite", "claude"] +categories = ["command-line-utilities", "database"] [features] # Exposes TestClock to dependents (e.g. the binary crate's integration tests) diff --git a/crates/agent-memory/Cargo.toml b/crates/agent-memory/Cargo.toml index 48c2a66..7fc7d4e 100644 --- a/crates/agent-memory/Cargo.toml +++ b/crates/agent-memory/Cargo.toml @@ -1,18 +1,23 @@ [package] -name = "agent-memory" -version = "0.0.1" +# crates.io name: `agent-memory` is taken by an unrelated project (#9). The +# executable stays `agent-memory` via the [[bin]] below. +name = "agent-memory-mcp" +version = "0.1.0" edition.workspace = true license.workspace = true authors.workspace = true repository.workspace = true description = "Persistent typed agent memory: an MCP stdio server over embedded SQLite" +readme = "../../README.md" +keywords = ["mcp", "memory", "agent", "sqlite", "claude"] +categories = ["command-line-utilities", "database"] [[bin]] name = "agent-memory" path = "src/main.rs" [dependencies] -agent-memory-core = { path = "../agent-memory-core" } +agent-memory-core = { version = "0.1.0", path = "../agent-memory-core" } axum.workspace = true rmcp.workspace = true tokio.workspace = true @@ -27,7 +32,7 @@ tracing-subscriber.workspace = true [dev-dependencies] # Test builds only: exposes the core crate's FakeEmbedder/TestClock so the # in-process MCP and REST tests can drive the hybrid (embedder-up) search path. -agent-memory-core = { path = "../agent-memory-core", features = ["test-clock"] } +agent-memory-core = { version = "0.1.0", path = "../agent-memory-core", features = ["test-clock"] } tempfile.workspace = true serde_json.workspace = true rusqlite.workspace = true diff --git a/crates/agent-memory/src/mcp.rs b/crates/agent-memory/src/mcp.rs index 073e255..81be05f 100644 --- a/crates/agent-memory/src/mcp.rs +++ b/crates/agent-memory/src/mcp.rs @@ -496,10 +496,13 @@ impl MemoryMcp { impl ServerHandler for MemoryMcp { fn get_info(&self) -> ServerInfo { // ServerInfo (InitializeResult) is #[non_exhaustive]; start from Default - // and set the fields we care about. + // and set the fields we care about. rmcp's default Implementation reports + // the SDK's own crate name and version, so ours is set explicitly. let mut info = ServerInfo::default(); info.capabilities = ServerCapabilities::builder().enable_tools().build(); - info.server_info = Implementation::from_build_env(); + info.server_info = Implementation::new("agent-memory", env!("CARGO_PKG_VERSION")) + .with_title("Agent Memory") + .with_website_url(env!("CARGO_PKG_REPOSITORY")); info.instructions = Some( "Persistent typed agent memory: store, list, search, update, link/unlink, and forget typed memories." .to_string(), diff --git a/crates/agent-memory/tests/tools.rs b/crates/agent-memory/tests/tools.rs index 06ef482..b5c1a36 100644 --- a/crates/agent-memory/tests/tools.rs +++ b/crates/agent-memory/tests/tools.rs @@ -66,7 +66,8 @@ impl Server { } } - fn initialize(&mut self) { + /// Run the initialize handshake and return the server's initialize reply. + fn initialize(&mut self) -> serde_json::Value { self.send(&serde_json::json!({ "jsonrpc": "2.0", "id": 1, @@ -77,12 +78,13 @@ impl Server { "clientInfo": { "name": "tools-test", "version": "0.0.0" } } })); - let _ = self.read_response(1); + let response = self.read_response(1); // The initialized notification has no id and expects no response. self.send(&serde_json::json!({ "jsonrpc": "2.0", "method": "notifications/initialized" })); + response } /// Call a tool by name and read its response, matching the given request id. @@ -116,6 +118,39 @@ fn tool_text(response: &serde_json::Value) -> String { .to_string() } +/// Pins the MCP identity that clients display and log: the initialize reply must +/// name agent-memory and its own version, never fall back to the SDK's identity. +#[test] +fn initialize_reports_agent_memory_server_info_over_stdio() { + let dir = tempfile::tempdir().expect("tempdir"); + let db_path = dir.path().join("memory.db"); + + let mut server = Server::spawn(&db_path); + let response = server.initialize(); + let server_info = response + .pointer("/result/serverInfo") + .unwrap_or_else(|| panic!("no serverInfo in initialize reply: {response}")); + + assert_eq!( + server_info.get("name").and_then(|v| v.as_str()), + Some("agent-memory") + ); + assert_eq!( + server_info.get("version").and_then(|v| v.as_str()), + Some(env!("CARGO_PKG_VERSION")) + ); + assert_eq!( + server_info.get("title").and_then(|v| v.as_str()), + Some("Agent Memory") + ); + assert_eq!( + server_info.get("websiteUrl").and_then(|v| v.as_str()), + Some("https://github.com/UnityInFlow/agent-memory") + ); + + server.shutdown(); +} + #[test] fn memory_store_returns_id_with_null_scope_then_list_returns_it() { let dir = tempfile::tempdir().expect("tempdir");