diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d40f55a..8b3d66a86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to agentbox are documented here. Format inspired by [Keep a ## [Unreleased] +### Changed (2026-10-03 — ruflo memory governed on the sidecar, ADR-2123) + +- The image's `ruflo` / `claude-flow` bins are governed wrappers. `ruflo memory …` runs + `mcp/servers/ruflo-memory-cli.cjs`, which serves store/retrieve/search/list/delete/stats from + the ruvector-postgres sidecar through the governed memory library (Xinference bge-small + embeddings, the MCP server's entry ids) and emits ruflo 3.51.1's `--format json` shapes, so the + ruflo-console memory pane shows the sidecar corpus. `init`, `configure`, `export`, `import`, + `purge`, `distill`, `backup`, `compress`, `cleanup`, `classify`, `select-operator` and + `migrate` are refused (exit 2): ruflo's memory subsystem is local-only and would be a second + memory system. Every other subcommand runs with `RUFLO_DAEMON_AUTOSTART=0`, + `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH=~/.cache/ruflo/memory` as + overridable defaults (also exported at boot), so no invocation spawns a daemon or writes + `.swarm/`, `ruvector.db` or an ONNX model into the working directory. + ### Changed (2026-10-03 — pod trails in the blocktrails git-mark §5.2 shape) - `nostr-pod-bridge` writes the pod's `blocktrails.json` in the blocktrails/spec git-mark diff --git a/config/agentbox-aliases.sh b/config/agentbox-aliases.sh index fc942fbb1..f168adde5 100644 --- a/config/agentbox-aliases.sh +++ b/config/agentbox-aliases.sh @@ -22,7 +22,7 @@ alias cf-hive="claude-flow hive-mind spawn" alias cf-spawn="claude-flow hive-mind spawn" alias cf-status="claude-flow hive-mind status" alias cf-help="claude-flow --help" -alias cf-memory="claude-flow memory" # read/debug only — writes MUST use MCP memory_* (CLI bypasses embeddings) +alias cf-memory="claude-flow memory" # governed (ADR-2123): serves the ruvector-postgres sidecar via Xinference; init/configure/export refused alias cf-hooks="claude-flow hooks" alias cf-doctor="claude-flow doctor --fix" alias cf-daemon="claude-flow daemon start" diff --git a/config/custody/env-classes.json b/config/custody/env-classes.json index 5d5c670fc..3655acfc3 100644 --- a/config/custody/env-classes.json +++ b/config/custody/env-classes.json @@ -596,6 +596,9 @@ "RAW_BUS", "RES", "RUFLO_DAEMON_AI_WORKERS", + "RUFLO_DAEMON_AUTOSTART", + "CLAUDE_FLOW_DISABLE_BRIDGE", + "CLAUDE_FLOW_MEMORY_PATH", "RUNTIME_ENV_DURABLE", "RUNTIME_ENV_FILE", "RUSTUP_HOME", diff --git a/config/entrypoint-unified.sh b/config/entrypoint-unified.sh index f0e47f783..8f12dd23b 100644 --- a/config/entrypoint-unified.sh +++ b/config/entrypoint-unified.sh @@ -3374,6 +3374,17 @@ export EMBEDDING_MODEL="${EMBEDDING_MODEL}" # explicitly so an enabled plugin — ruflo-loop-workers / ruflo-autopilot — # can never flip headless Claude launches on without an operator export). export RUFLO_DAEMON_AI_WORKERS="${RUFLO_DAEMON_AI_WORKERS:-0}" +# ADR-2123 governed ruflo: the baked `ruflo`/`claude-flow` wrappers set these +# per invocation; exporting them here covers anything that reaches +# @claude-flow/cli directly (node …/cli.js, the console's subprocess env). +# No daemon autostart (every CLI call, even --help, spawned one into the CWD), +# no AgentDB bridge (its native engine writes ./ruvector.db wherever it runs +# and downloads MiniLM into the read-only store path), and the sql.js +# bookkeeping store under the cache dir instead of /.swarm. Durable +# memory is the ruvector-postgres sidecar via `ruflo memory …` / memory_* only. +export RUFLO_DAEMON_AUTOSTART="${RUFLO_DAEMON_AUTOSTART:-0}" +export CLAUDE_FLOW_DISABLE_BRIDGE="${CLAUDE_FLOW_DISABLE_BRIDGE:-1}" +export CLAUDE_FLOW_MEMORY_PATH="\${CLAUDE_FLOW_MEMORY_PATH:-\${HOME:-/tmp}/.cache/ruflo/memory}" # ADR-2080 model-router console knobs (empty unless [model_routing.neural].enabled) $_MRN_EXPORTS # Interaction plane (PRD-021 / ADR-042): expose the AoE daemon + NIP-98 proxy diff --git a/docs/adr/ADR-2002-aoe-token-auth-boundary.md b/docs/adr/ADR-2002-aoe-token-auth-boundary.md index 0b0a91f24..1141037fb 100644 --- a/docs/adr/ADR-2002-aoe-token-auth-boundary.md +++ b/docs/adr/ADR-2002-aoe-token-auth-boundary.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [config/nip98-proxy/proxy.mjs, scripts/aoe-curl.sh, flake.nix] owner: jjohare review_trigger: next image rebuild (activation), or any new consumer of :9095, or per-process isolation becoming available @@ -266,3 +266,7 @@ Tripped by the W10 gap fixes on `custody/integration`. `flake.nix` (`dc91e092a`) ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2002 — AoE interaction plane requires token auth — loopback is not a boundary) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). Nothing this record decides (ADR-2002 — AoE interaction plane requires token auth — loopback is not a boundary) changed: the nip98-proxy, the AoE daemon and the token check are untouched, and none of the new programs opens a listener (the seats and the coach dial the forum relay and the loopback producer). The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. diff --git a/docs/adr/ADR-2009-nip98-proxy-identity-boundary.md b/docs/adr/ADR-2009-nip98-proxy-identity-boundary.md index 3ae829e39..95068c84f 100644 --- a/docs/adr/ADR-2009-nip98-proxy-identity-boundary.md +++ b/docs/adr/ADR-2009-nip98-proxy-identity-boundary.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [config/nip98-proxy/proxy.mjs, flake.nix, docs/INGRESS-identity.md] owner: jjohare review_trigger: A second identity ingress is proposed, or aoe serve stops binding loopback @@ -274,3 +274,7 @@ Tripped by the W10 gap fixes on `custody/integration`. `flake.nix` (`dc91e092a`) ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2009 — The nip98-proxy is the fail-closed AoE identity boundary) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). Nothing this record decides (ADR-2009 — The nip98-proxy is the fail-closed AoE identity boundary) changed: `config/nip98-proxy/proxy.mjs` and `docs/INGRESS-identity.md` did not move and no change in `flake.nix` touches the proxy's package, program or environment. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. diff --git a/docs/adr/ADR-2012-relay-allowlist-only-ingress.md b/docs/adr/ADR-2012-relay-allowlist-only-ingress.md index c4058639a..bb0671777 100644 --- a/docs/adr/ADR-2012-relay-allowlist-only-ingress.md +++ b/docs/adr/ADR-2012-relay-allowlist-only-ingress.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [agentbox.toml, flake.nix] owner: jjohare review_trigger: ingress_policy changes from allowlist, or the ADR-040 D3 governance-publisher key-split lands @@ -346,3 +346,7 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2012 — Relay ingress is allowlist-only with no fallback and no auto-add) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). Nothing this record decides (ADR-2012 — Relay ingress is allowlist-only with no fallback and no auto-add) changed: the only relay URLs added (`[poker_citizen.dreamlab-txbt4].relay`, `[poker_coach].relay`) are the forum relay already named at `forum_relay_url` (`agentbox.toml:189`); no allowlist entry, fallback or auto-add appears. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. diff --git a/docs/adr/ADR-2013-loopback-publish-except-9096.md b/docs/adr/ADR-2013-loopback-publish-except-9096.md index 063d572bc..7a6c70580 100644 --- a/docs/adr/ADR-2013-loopback-publish-except-9096.md +++ b/docs/adr/ADR-2013-loopback-publish-except-9096.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [scripts/ci/check-ports-loopback.sh, .github/workflows/invariants.yml, flake.nix, docker-compose.yml] owner: jjohare review_trigger: Any new entry on the SANCTIONED list, or a new compose overlay file @@ -347,3 +347,7 @@ Tripped by the W10 gap fixes on `custody/integration`. `.github/workflows/invari ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2013 — Every compose publish binds 127.0.0.1 unless on the sanctioned-exposure list, CI-enforced across all overlays) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). `docker-compose.yml`, `scripts/ci/check-ports-loopback.sh` and the invariants workflow did not move. Nothing this record decides (ADR-2013 — Every compose publish binds 127.0.0.1 unless on the sanctioned-exposure list, CI-enforced across all overlays) changed: the `flake.nix` changes add supervisor programs and a package wrapper, not compose publishes. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. diff --git a/docs/adr/ADR-2014-memory-mcp-only-fail-closed.md b/docs/adr/ADR-2014-memory-mcp-only-fail-closed.md index 0a4758362..f490a56f8 100644 --- a/docs/adr/ADR-2014-memory-mcp-only-fail-closed.md +++ b/docs/adr/ADR-2014-memory-mcp-only-fail-closed.md @@ -118,3 +118,7 @@ structural rather than evidenced per row. Backup participation in TTL is still undefined: an expired row the sweep deletes may survive in a retained backup. `implementation_status` stays `partial` until a production repair run and a passing recall receipt exist together. + +## Re-affirmation — 2026-10-03 (ruflo 3.51.1 memory, ADR-2123) + +The `review_trigger` fired in substance: ruflo 3.51.1 (baked under the console gate) carries its own memory subsystem, and one `memory init` wrote three local stores into a repository. Tested, it is local-only (sql.js, AgentDB mirror, `./ruvector.db`, MiniLM) with no Postgres path, so it is not a new backend for this record to admit; it is a second system this record forbids. ADR-2123 closes the gap by routing `ruflo memory …` through `memStore`/`memSearch` of `lib/memory-tools.js` and refusing the local-store verbs, so the MCP-only, fail-closed mandate now also holds for the CLI and the ruflo-console memory pane. The upstream lineage (why ruvnet has an embedded line and a Postgres line, and why the Postgres line is current) is recorded in ADR-2123 so it is not re-derived. Nothing this record decides changed. diff --git a/docs/adr/ADR-2019-model-lifecycle-384-dim-freeze.md b/docs/adr/ADR-2019-model-lifecycle-384-dim-freeze.md index 824463d76..e639db505 100644 --- a/docs/adr/ADR-2019-model-lifecycle-384-dim-freeze.md +++ b/docs/adr/ADR-2019-model-lifecycle-384-dim-freeze.md @@ -7,7 +7,7 @@ implementation_status: none activation_status: inactive supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [mcp/servers/lib/aggregate-effectiveness.js, scripts/ruvector-sona-feeder.mjs, agentbox.toml] owner: jjohare review_trigger: A SONA binary with configurable embedding_dim (384-capable) ships, or a dimension migration is planned @@ -240,3 +240,7 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `4ea3181b5..451823ca8`: `agentbox.toml` adds `[toolchains].ruflo_console = false`. Nothing this record governs (ADR-2019 — Model-lifecycle freeze — 384-dim bge is the active column, SONA and attention-rerank stay off) changes meaning. The decision holds. Re-verified by `git log 4ea3181b5..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `mcp/servers/lib/aggregate-effectiveness.js` and `scripts/ruvector-sona-feeder.mjs` did not move. Nothing this record decides (ADR-2019 — Model-lifecycle freeze — 384-dim bge is the active column, SONA and attention-rerank stay off) changed: no embedding, SONA or rerank key changes. Outside the governed paths, `012bf98f5` routes `ruflo memory` to the ruvector-postgres sidecar and its 384-dim Xinference embedder instead of ruflo's own MiniLM store (ADR-2123), which is consistent with this freeze. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. diff --git a/docs/adr/ADR-2020-capability-gating.md b/docs/adr/ADR-2020-capability-gating.md index 588782716..b607fd0a3 100644 --- a/docs/adr/ADR-2020-capability-gating.md +++ b/docs/adr/ADR-2020-capability-gating.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: live supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [agentbox.toml, skills/tree-search-coder/SKILL.md, services/agentbox-ops/src/bin/tree-search-cap.rs] owner: jjohare review_trigger: any new optional skill/feature block added to agentbox.toml, or any change to the tree-search-coder spend/route posture @@ -247,3 +247,7 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `4ea3181b5..451823ca8`: `agentbox.toml` adds `[toolchains].ruflo_console = false`. Nothing this record governs (ADR-2020 — Optional capabilities are manifest-gated and byte-identical-when-off; execution-gated tools are spend-capped and never auto-routed) changes meaning. This record's rule is exercised directly: the gate defaults off, flake.nix bakes nothing under it when off, and the boot projection leaves `settings.json` and `installed_plugins.json` byte-identical when none of the three ids is present (asserted in `tests/config/ruflo-console.test.mjs`). The decision holds. Re-verified by `git log 4ea3181b5..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `skills/tree-search-coder/SKILL.md` and `tree-search-cap.rs` did not move. Each new table is a manifest gate in this record's shape: `[poker_citizen.]` and `[poker_coach]` are schema-declared, catalogued rebuild-class, and bake nothing when off; the `[sidechain.dreamlab-txbt4]` and `ruflo_console` flips are gate *states* changing in the shipped manifest, not the rule. The previous note's "the gate defaults off" for `ruflo_console` describes the manifest at `451823ca8`; since `a2ffa05eb` it ships on, and `tests/config/ruflo-console.test.mjs` asserts byte-identity when off and the three ids when on (15/15 at HEAD). Nothing this record decides (ADR-2020 — Optional capabilities are manifest-gated and byte-identical-when-off; execution-gated tools are spend-capped and never auto-routed) changed. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. diff --git a/docs/adr/ADR-2023-loom-facade.md b/docs/adr/ADR-2023-loom-facade.md index 0667e213b..22f6ecc3f 100644 --- a/docs/adr/ADR-2023-loom-facade.md +++ b/docs/adr/ADR-2023-loom-facade.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: live supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [agentbox.toml, mcp/servers/lib/ontology-retrieval.js] owner: jjohare review_trigger: model swap behind the Loom, or ADR-051 deferred-distillation MCP tools becoming a discrete server @@ -310,3 +310,7 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `4ea3181b5..451823ca8`: `agentbox.toml` adds `[toolchains].ruflo_console = false`. Nothing this record governs (ADR-2023 — The Loom is a façade — consumers hold the :8084 door and the model is a swappable URL behind it) changes meaning. The decision holds. Re-verified by `git log 4ea3181b5..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `mcp/servers/lib/ontology-retrieval.js` did not move; `loom_url`, `loom_model` and `loom_max_tokens` are unchanged. One new consumer appears: `[poker_coach].llm_url = "http://192.168.2.132:8084/v1"` holds the `:8084` door, with `[poker_coach].model` naming what the endpoint expects (`18a85577c`) — the façade contract this record requires, not a second door. Nothing this record decides (ADR-2023 — The Loom is a façade — consumers hold the :8084 door and the model is a swappable URL behind it) changed. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. diff --git a/docs/adr/ADR-2028-vault-manifest-path-authority.md b/docs/adr/ADR-2028-vault-manifest-path-authority.md index fc9d00372..7d709dca7 100644 --- a/docs/adr/ADR-2028-vault-manifest-path-authority.md +++ b/docs/adr/ADR-2028-vault-manifest-path-authority.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: live supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [agentbox.toml, setup/agentbox.default.toml, schema/agentbox.toml.schema.json, config/entrypoint-unified.sh, mcp/servers/lib/ontology-local.js, mcp/servers/lib/ontology-index-build.js, scripts/ontology-condense-scheduler.mjs, scripts/ontology-condense-refresh.sh, skills/podcast-knowledge-ingest/SKILL.md, skills/ontology-core/SKILL.md, skills/ontology-enrich/SKILL.md, skills/ontology-augment/SKILL.md, skills/web-summary/SKILL.md] owner: jjohare review_trigger: any new skill, MCP server, or supervised program that reads or writes authored markdown @@ -233,3 +233,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `4ea3181b5..451823ca8`: `agentbox.toml` adds `[toolchains].ruflo_console = false`; `config/entrypoint-unified.sh` adds the ruflo-console boot block after factrail's, and factrail's function-hook switch and `agentbox` marketplace removal also respect the new gate; with it off both behave as before; the schema declares `toolchains.ruflo_console` (boolean, default false). Nothing this record governs (ADR-2028 — `[vault]` in agentbox.toml is the single path authority for the authored corpus; no consumer hard-codes a Logseq path) changes meaning. The decision holds. Re-verified by `git log 4ea3181b5..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `schema/agentbox.toml.schema.json` declares `faucet_units`/`faucet_sats`, `peg_script`, the `[poker_citizen.]` sub-objects and `[poker_coach]` (`bde96a334`, `a2ffa05eb`, `18a85577c`). `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). No other governed path moved. Nothing this record decides (ADR-2028 — `[vault]` in agentbox.toml is the single path authority for the authored corpus; no consumer hard-codes a Logseq path) changed: `[vault]` is untouched and no change introduces a vault or Logseq path. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2029-rune-markdown-tui-notes-window.md b/docs/adr/ADR-2029-rune-markdown-tui-notes-window.md index dc6e02189..acb6002a0 100644 --- a/docs/adr/ADR-2029-rune-markdown-tui-notes-window.md +++ b/docs/adr/ADR-2029-rune-markdown-tui-notes-window.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [flake.nix, lib/rune.nix, config/tmux-autostart.sh, config/tmux.conf, agentbox.toml, setup/agentbox.default.toml, schema/agentbox.toml.schema.json] owner: jjohare review_trigger: a Rune release that changes its CLI (`-w`), its keyboard-protocol requirement, or its licence; or the AoE plane absorbing note editing @@ -272,3 +272,7 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2029 — Rune is the first-class markdown TUI; tmux window 9 \"Notes\" opens it at the vault root) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `schema/agentbox.toml.schema.json` declares `faucet_units`/`faucet_sats`, `peg_script`, the `[poker_citizen.]` sub-objects and `[poker_coach]` (`bde96a334`, `a2ffa05eb`, `18a85577c`). `lib/rune.nix`, `config/tmux-autostart.sh` and `config/tmux.conf` did not move. Nothing this record decides (ADR-2029 — Rune is the first-class markdown TUI; tmux window 9 "Notes" opens it at the vault root) changed: nothing touches the rune package, the tmux window or the vault root. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. diff --git a/docs/adr/ADR-2031-consultant-model-manifest-projection.md b/docs/adr/ADR-2031-consultant-model-manifest-projection.md index c5c4acb9c..f4c18c233 100644 --- a/docs/adr/ADR-2031-consultant-model-manifest-projection.md +++ b/docs/adr/ADR-2031-consultant-model-manifest-projection.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [config/entrypoint-unified.sh, services/agentbox-manifest/src/tui_write.rs, mcp/consultants/antigravity/server.js, skills/mcp.json] owner: jjohare review_trigger: any change to a consultant's default model, a Gemini model retirement, the 2027-01-01 Gemini tariff step, or a wizard that starts exposing the consultant model field @@ -176,3 +176,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `f93586b9e..451823ca8`: `config/entrypoint-unified.sh` adds the ruflo-console boot block after factrail's, and factrail's function-hook switch and `agentbox` marketplace removal also respect the new gate; with it off both behave as before. Nothing this record governs (ADR-2031 — Consultant model selection is projected from the manifest at boot; environment wins, TUI preserves the operator's choice, and tariffs are dated) changes meaning. The decision holds. Re-verified by `git log f93586b9e..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). No other governed path moved. Nothing this record decides (ADR-2031 — Consultant model selection is projected from the manifest at boot; environment wins, TUI preserves the operator's choice, and tariffs are dated) changed: the consultant projection block, its environment-wins rule and the tariff dates are untouched. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2033-deepsec-security-gate.md b/docs/adr/ADR-2033-deepsec-security-gate.md index 22b0fc3df..de1cf254a 100644 --- a/docs/adr/ADR-2033-deepsec-security-gate.md +++ b/docs/adr/ADR-2033-deepsec-security-gate.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [flake.nix, agentbox.toml, schema/agentbox.toml.schema.json, scripts/agentbox-config-validate.js, management-api/lib/system-manifest.js, skills/build-with-quality/scripts, skills/build-with-quality/references/deepsec-security-gate.md, .github/workflows/deepsec.yml] owner: jjohare review_trigger: a deepsec major version, a change to its CLI exit-code contract or model-route schema, any new model route, or the first paid full-repo run @@ -313,3 +313,7 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2033 — deepsec is the executed Security gate of build-with-quality, baked as a manifest-gated CLI under a names-only credential policy) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `schema/agentbox.toml.schema.json` declares `faucet_units`/`faucet_sats`, `peg_script`, the `[poker_citizen.]` sub-objects and `[poker_coach]` (`bde96a334`, `a2ffa05eb`, `18a85577c`). `management-api/lib/system-manifest.js` adds the rebuild-class catalogue entries `poker-citizen-dreamlab-txbt4` (requires `sidechain.dreamlab-txbt4.enabled`) and `poker-coach` (`bde96a334`, `18a85577c`). `scripts/agentbox-config-validate.js`, the build-with-quality scripts and reference, and `.github/workflows/deepsec.yml` did not move. Nothing this record decides (ADR-2033 — deepsec is the executed Security gate of build-with-quality, baked as a manifest-gated CLI under a names-only credential policy) changed: no deepsec package, gate, catalogue entry or credential-policy line changes. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. diff --git a/docs/adr/ADR-2082-orchestration-proxy-behind-governed-memory-server.md b/docs/adr/ADR-2082-orchestration-proxy-behind-governed-memory-server.md index 709f9d1bc..39d7e59b3 100644 --- a/docs/adr/ADR-2082-orchestration-proxy-behind-governed-memory-server.md +++ b/docs/adr/ADR-2082-orchestration-proxy-behind-governed-memory-server.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [mcp/servers/lib/orchestration-proxy.js, mcp/servers/ruvector-mcp.cjs, mcp/servers/lib/ruvector-gates.js, config/entrypoint-unified.sh] owner: jjohare review_trigger: next image rebuild (activation), a ruflo major bump that renames the swarm/agent/task/coordination tools, or any proposal to forward a memory_* tool @@ -158,3 +158,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `f93586b9e..451823ca8`: `config/entrypoint-unified.sh` adds the ruflo-console boot block after factrail's, and factrail's function-hook switch and `agentbox` marketplace removal also respect the new gate; with it off both behave as before. Nothing this record governs (ADR-2082 — The governed claude-flow server forwards orchestration tools to a filtered ruflo child; memory never crosses) changes meaning. The decision holds. Re-verified by `git log f93586b9e..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `orchestration-proxy.js`, `ruvector-mcp.cjs` and `ruvector-gates.js` did not move, so `DENIED_PREFIXES`, the alias table and fail-open-for-orchestration-only are untouched, and the projection of `RUVECTOR_ORCHESTRATION_PROXY` into the `claude-flow` env block is unchanged. The proxy child is still `claude-flow-mcp`, which `rufloGovernedPkg` links through unwrapped (`flake.nix`, `012bf98f5`); it inherits the three exports from the session environment, so ruflo's AgentDB bridge is off and its sql.js bookkeeping lands under `~/.cache/ruflo/memory` in the child as well. That narrows what the child writes locally and leaves the governed memory tools as the only durable memory path, which is what this record requires. Nothing this record decides (ADR-2082 — The governed claude-flow server forwards orchestration tools to a filtered ruflo child; memory never crosses) changed. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: one line in the runtime-env heredoc (`config/entrypoint-unified.sh`): `CLAUDE_FLOW_MEMORY_PATH` is now escaped so it resolves in the sourcing shell to `$HOME/.cache/ruflo/memory` instead of a root-side `/home/devuser` literal (RC-X1-01). The `claude-flow-mcp` proxy child still receives the same default; memory still never crosses the proxy. Nothing this record decides changed. diff --git a/docs/adr/ADR-2088-grill-before-acting-on-forum-suggestions.md b/docs/adr/ADR-2088-grill-before-acting-on-forum-suggestions.md index 30001b245..989140ffc 100644 --- a/docs/adr/ADR-2088-grill-before-acting-on-forum-suggestions.md +++ b/docs/adr/ADR-2088-grill-before-acting-on-forum-suggestions.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 275e12356319a9630846656580d497d53de3d38c +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [management-api/lib/junkiejarvis-clarify.js, management-api/lib/junkiejarvis-agent.js, management-api/server.js, scripts/dream-forum-suggestions.mjs, scripts/run-junkiejarvis.cjs, tests/sovereign/junkiejarvis-clarify.test.js, tests/sovereign/junkiejarvis-dm-send.test.js, tests/sovereign/dream-forum-suggestions-jj-gate.test.js] owner: jjohare review_trigger: any change to the clarity signals, MIN_SPECIFICITY, the 7-day expiry, or the forum-suggestions ingest path @@ -164,3 +164,7 @@ Tripped by the sidechain health and witness change. `management-api/server.js` c Tripped by custody X-1 step 1, W2 (`custody/w2-env-scrub`: `0965a9c8c`, `042115499`, `275e12356`; bypass 3, ROLE secrets out of PID 1's environment). `management-api/lib/junkiejarvis-agent.js` (`readPrivHex`) and `scripts/dream-forum-suggestions.mjs` read the JunkieJarvis key through the ROLE-secret loader. Flag off: the same read (the jj-gate suite passes 3/3, the agent and dm-send suites unchanged). **Consequence under the flag with W1:** the key file is `ab-identity` 0400, so the devuser agent finds no key and does not start (fail closed) until it signs through the identity port (`forum_event`, W3b). The grill-before-acting behaviour is untouched. The decision holds. Re-verified by `git diff e434a7a59..275e12356 -- `. No re-implementation was needed. The image is unverified until the owner's rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`275e12356..1fc26c786`: one governed change, `management-api/lib/junkiejarvis-agent.js` (`658085870`): the inner-rumor dedup becomes `rumor.id && this._dedup(rumor.id)`, so a rumor sealed without an `id` (the nostr-bbs kit before its rumor-id fix seals the bare unsigned template) is no longer counted as already seen; the wrap-id dedup still bounds replays. Before this, every such DM was dropped before the clarify gate saw it; now it reaches `_handleDm` and the grill-before-acting path like any other. The other seven governed paths did not move. The decision — grill the author before acting on an unclear item — is not altered; it now applies to kit-sent DMs that previously never arrived. The clarify, dm-send and jj-gate suites pass under Jest (3 suites, 70 tests) at HEAD. Re-verified by `git diff 275e12356..1fc26c786 -- `. diff --git a/docs/adr/ADR-2091-live-skill-router.md b/docs/adr/ADR-2091-live-skill-router.md index b98031bbd..f6b8e485a 100644 --- a/docs/adr/ADR-2091-live-skill-router.md +++ b/docs/adr/ADR-2091-live-skill-router.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [config/hooks/lib/skill-route.cjs, config/hooks/skill-route.cjs, skills/skill-router/scripts/route.mjs, config/entrypoint-unified.sh, tests/config/skill-route.test.js] owner: jjohare review_trigger: the first project that needs a per-project routing bypass (ADR-2090), a Jev model change, or a measured runtime-path accuracy below 85% on the 40-item set @@ -197,3 +197,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `f93586b9e..451823ca8`: `config/entrypoint-unified.sh` adds the ruflo-console boot block after factrail's, and factrail's function-hook switch and `agentbox` marketplace removal also respect the new gate; with it off both behave as before. Nothing this record governs (ADR-2091 — Route each turn to a skill with one typed judgement, failing open to the table) changes meaning. The decision holds. Re-verified by `git log f93586b9e..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `config/hooks/lib/skill-route.cjs`, `config/hooks/skill-route.cjs`, `skills/skill-router/scripts/route.mjs` and `tests/config/skill-route.test.js` did not move. Nothing this record decides (ADR-2091 — Route each turn to a skill with one typed judgement, failing open to the table) changed: the router's projection block and the fail-open rule are untouched. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2092-govern-the-agent-and-command-registries.md b/docs/adr/ADR-2092-govern-the-agent-and-command-registries.md index b57801e7c..4a3c3b2d7 100644 --- a/docs/adr/ADR-2092-govern-the-agent-and-command-registries.md +++ b/docs/adr/ADR-2092-govern-the-agent-and-command-registries.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [agents/registered-agents.txt, scripts/reconcile-agents.sh, scripts/reconcile-commands.sh, scripts/project-skill-roots.mjs, config/registered-commands.txt, config/entrypoint-unified.sh, flake.nix, tests/config/agent-reconcile.test.sh] owner: jjohare review_trigger: a new subagent worth always-loading, or evidence the router surfaces baked-but-unregistered skills too slowly @@ -263,3 +263,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2092 — Agents and slash-commands get the same manifest governance skills already have) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). `agents/registered-agents.txt`, `config/registered-commands.txt`, the reconcile scripts, `project-skill-roots.mjs` and the agent-reconcile test did not move. Nothing this record decides (ADR-2092 — Agents and slash-commands get the same manifest governance skills already have) changed: `rufloGovernedPkg` is a bin wrapper, not an agent or command registration, and the boot reconcile blocks are untouched. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2093-jev-verbatim-compaction.md b/docs/adr/ADR-2093-jev-verbatim-compaction.md index 4e1c0dd89..e84858f30 100644 --- a/docs/adr/ADR-2093-jev-verbatim-compaction.md +++ b/docs/adr/ADR-2093-jev-verbatim-compaction.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [lib/factrail.nix, config/entrypoint-unified.sh, lib/claude-code-binary.nix] owner: jjohare review_trigger: the first measured residency bill that exceeds the summary path's re-read savings, a Claude Code function-hook API change, a request to fence a class other than email, or a change to ADR-2121 (its implementation) @@ -341,3 +341,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `daba195e5..451823ca8`: `config/entrypoint-unified.sh` adds the ruflo-console boot block after factrail's, and factrail's function-hook switch and `agentbox` marketplace removal also respect the new gate; with it off both behave as before. Nothing this record governs (ADR-2093 — Compact context by Jev judgement, verbatim, with email fenced out and a switch) changes meaning. The compaction policy is untouched: CLAUDE_CODE_ENABLE_FUNCTION_HOOKS is still set whenever `[features.jev_compaction]` is on, and is now also kept on for the console. The decision holds. Re-verified by `git log daba195e5..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `lib/factrail.nix` and `lib/claude-code-binary.nix` did not move. Nothing this record decides (ADR-2093 — Compact context by Jev judgement, verbatim, with email fenced out and a switch) changed: the compaction block, its switch and the email fence are untouched. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2103-parent-chain-and-header-profile-are-configuration-behind-the-p21-gate.md b/docs/adr/ADR-2103-parent-chain-and-header-profile-are-configuration-behind-the-p21-gate.md index 8819ab817..2d76d47d5 100644 --- a/docs/adr/ADR-2103-parent-chain-and-header-profile-are-configuration-behind-the-p21-gate.md +++ b/docs/adr/ADR-2103-parent-chain-and-header-profile-are-configuration-behind-the-p21-gate.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: inactive supersedes: [] superseded_by: [] -verified_commit: f93586b9e52fda0d0b367881e2d2ff3014509faf +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [config/sidechain/dreamlab/chain.json, config/sidechain/dreamlab-txbt4/chain.json, config/sidechain/README.md, config/sidechain/run-producer.sh, config/sidechain/upstream-pins, lib/sidestr-upstream.nix, tests/config/sidechain-genesis.test.sh, tests/config/sidechain-producer-gates.test.sh, tests/config/sidechain-producer-baked.test.sh, management-api/lib/sidechain-health.js, scripts/activation/sidechain-demo-witness.sh, scripts/activation/sidechain-witness.cjs, scripts/activation/sidechain-witness-replay/src/main.rs] owner: jjohare review_trigger: sidestr/spec PR #4 and sidestr/explorer PR #2 merging or being declined; a new alias in the SPEC 3.2 parent table; any proposal to sign a chain document whose parent is a mainnet variant; a change to the Knots BLAKE2b fork's header format or activation; a BLAKE2b testnet4 node reachable from the container; upstream implementing assets between chains (assets-and-pools section 4) @@ -625,3 +625,7 @@ until the owner's rebuild. ## Re-verification — 2026-10-03 (`f93586b9e52fda0d0b367881e2d2ff3014509faf`, custody W4) Tripped by `f93586b9e`. `config/sidechain/run-producer.sh` changes only in where the block files live. Under `[security].role_isolation` the producer runs as its role with HOME on the `/run/secrets` tmpfs, so `SIDESTR_STATE` now defaults to `/var/lib/agentbox/events/sidestr/` on the agentbox-events volume (umask 027, so the devuser mirror reads it through the group), seeded once from the workspace state, which stays. With the flag off the default is unchanged, and the runner refuses to start (`CUSTODY-STATE-AHEAD`) when the custody `blocks.dat` has outgrown the workspace copy, because starting from the workspace would fork the chain. The parent check, the seal, the P21 gate, the checkpoint position and the baked upstream are unchanged; the producer gates pass 7/7 and the baked-upstream suite 8/8. The decision holds. Re-verified by `git log 3b5412963..f93586b9e -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`f93586b9e..1fc26c786`: `config/sidechain/upstream-pins` bumps `spec` from `e8deb63` to `83dd662` (SPEC 0.0.5 plus `--peg-script`) and `config/sidechain/run-producer.sh` passes `--peg-script "$SIDESTR_PEG_SCRIPT"` when set, documented as SPEC 6 Level 2 for a single-signer chain with no parent wallet: the announced output script is the peg, deposits paying it are credited, peg-outs are recorded, not paid (`aeca58df6`). `lib/sidestr-upstream.nix` moves its `spec` rev and hash to the same commit (`b78e414fb`) — this record's rule that the producer refuses a bake disagreeing with `upstream-pins`, exercised. `tests/config/sidechain-producer-baked.test.sh` adds a case that both producer PATHs carry `findutils` for the bake guard (`b83e0e5d7`); `tests/config/sidechain-producer-gates.test.sh` adds the per-chain poker-seat cases (`bde96a334`). The chain documents, README, genesis test, health module and witness tooling did not move. What this record decides — parent and header profile are manifest configuration bound at genesis, mainnet variants behind the P21 gate — is not affected: `peg_script` is a further per-chain manifest key, the parent stays `txbt4`, the seal and D3 check are untouched, and `checkpoint_every` is still `0` (the "not anchored" item stays open). One dated statement moves: the "Liquidity: none" paragraph's "coins arrive only by peg-in" now has a mechanism, since `sidestr:dreamlab-txbt4` is switched on with a peg script (`agentbox.toml`, outside this record's paths). Gates 10/10 and baked 9/9 pass at HEAD. `activation_status` stays `inactive`: a manifest flip is not a running-image receipt. Re-verified by `git diff f93586b9e..1fc26c786 -- `. diff --git a/docs/adr/ADR-2105-agentbox-kind-bands-and-the-colloquy-move.md b/docs/adr/ADR-2105-agentbox-kind-bands-and-the-colloquy-move.md index add9e7a52..bc9b50e95 100644 --- a/docs/adr/ADR-2105-agentbox-kind-bands-and-the-colloquy-move.md +++ b/docs/adr/ADR-2105-agentbox-kind-bands-and-the-colloquy-move.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [crates/colloquy/colloquy-nostr/src/kinds.rs, docs/PROTOCOL-registry.md, services/nostr-pod-bridge/src/colloquy_publish.rs, agentbox.toml] owner: jjohare review_trigger: the next agentbox Nostr kind allocation, or any change to the band table in docs/PROTOCOL-registry.md @@ -224,3 +224,7 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (ruflo-console gate, `451823ca8`) `4ea3181b5..451823ca8`: `agentbox.toml` adds `[toolchains].ruflo_console = false`. Nothing this record governs (ADR-2105 — The agentbox 38xxx bands below 38400 are all reserved, so colloquy and settlement move to 38400-38499) changes meaning. The decision holds. Re-verified by `git log 4ea3181b5..451823ca8 -- `. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `crates/colloquy/colloquy-nostr/src/kinds.rs`, `docs/PROTOCOL-registry.md` and `colloquy_publish.rs` did not move. Nothing this record decides (ADR-2105 — The agentbox 38xxx bands below 38400 are all reserved, so colloquy and settlement move to 38400-38499) changed: no kind number or band appears in the diff. The decision holds. Re-verified by `git diff 451823ca8..1fc26c786 -- `. diff --git a/docs/adr/ADR-2118-own-the-instruction-tiers-and-claude-home-in-the-repo.md b/docs/adr/ADR-2118-own-the-instruction-tiers-and-claude-home-in-the-repo.md index 2e119448f..c3e35bec7 100644 --- a/docs/adr/ADR-2118-own-the-instruction-tiers-and-claude-home-in-the-repo.md +++ b/docs/adr/ADR-2118-own-the-instruction-tiers-and-claude-home-in-the-repo.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [config/instructions, services/agentbox-manifest/src/instructions.rs, services/agentbox-manifest/src/cred_sync.rs, config/entrypoint-unified.sh, agentbox.sh, flake.nix, docker-compose.yml, docker-compose.override.yml, docker-compose.hp.yml, tests/config/claude-home-migration.test.sh, tests/config/compose-persistence.test.cjs] owner: jjohare review_trigger: the connected node runs migrate-claude-home; or Claude Code starts reading AGENTS.md natively (drop the @AGENTS.md wrappers and the embed); or a Claude Code release changes where credentials live @@ -164,3 +164,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2118 — Own the instruction tiers and the Claude home in the repo) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). `config/instructions`, `instructions.rs`, `cred_sync.rs`, `agentbox.sh`, the compose files and the two tests did not move. Nothing this record decides (ADR-2118 — Own the instruction tiers and the Claude home in the repo) changed: the tier projection is untouched, and `CLAUDE_FLOW_MEMORY_PATH` defaults under `/home/devuser/.cache/ruflo/memory`, not in the Claude home. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: one line in the runtime-env heredoc (`config/entrypoint-unified.sh`): `CLAUDE_FLOW_MEMORY_PATH` now resolves per user to `$HOME/.cache/ruflo/memory` (escaped) rather than a literal `/home/devuser` path. It stays outside the Claude home and outside every projected tier. Nothing this record decides changed. diff --git a/docs/adr/ADR-2119-remove-retired-outliner-ontology-runtime.md b/docs/adr/ADR-2119-remove-retired-outliner-ontology-runtime.md index beead1824..58a56e523 100644 --- a/docs/adr/ADR-2119-remove-retired-outliner-ontology-runtime.md +++ b/docs/adr/ADR-2119-remove-retired-outliner-ontology-runtime.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 1fc26c78639e3ab1dfd81c9b4284ea95bb5d731c verified_paths: [flake.nix, lib/ontology-tools.nix, services/ontology-tools, services/agentbox-mcp/src/web_summary, skills/ontology-core, skills/ontology-enrich, dream.config.json] owner: jjohare review_trigger: commit and rebuild the image; or introduce a corpus writer or output format @@ -130,3 +130,7 @@ Tripped by the W10 gap fixes on `custody/integration`. `flake.nix` (`dc91e092a`) ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2119 — Remove the retired outliner ontology runtime) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). `lib/ontology-tools.nix`, `services/ontology-tools`, `services/agentbox-mcp/src/web_summary`, the two ontology skills and `dream.config.json` did not move. Nothing this record decides (ADR-2119 — Remove the retired outliner ontology runtime) changed: nothing in `flake.nix` reintroduces an ontology package or program. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. diff --git a/docs/adr/ADR-2120-codex-daemon-package-volume.md b/docs/adr/ADR-2120-codex-daemon-package-volume.md index e75bfc725..be1a50fef 100644 --- a/docs/adr/ADR-2120-codex-daemon-package-volume.md +++ b/docs/adr/ADR-2120-codex-daemon-package-volume.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: live supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [agentbox.sh, config/entrypoint-unified.sh, flake.nix, docker-compose.yml, scripts/refresh-compose.sh, tests/config/compose-persistence.test.cjs, tests/config/refresh-compose.test.cjs] owner: jjohare review_trigger: commit verification and rebuild; or change Codex daemon packaging @@ -143,3 +143,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2120 — Give Codex daemon packages executable persistent storage) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). `agentbox.sh`, `docker-compose.yml`, `scripts/refresh-compose.sh` and the two compose tests did not move. Nothing this record decides (ADR-2120 — Give Codex daemon packages executable persistent storage) changed: no volume, mount option or Codex path changes. The decision holds. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2121-factrail-implements-jev-compaction.md b/docs/adr/ADR-2121-factrail-implements-jev-compaction.md index 6f51f8d4f..244db3c1d 100644 --- a/docs/adr/ADR-2121-factrail-implements-jev-compaction.md +++ b/docs/adr/ADR-2121-factrail-implements-jev-compaction.md @@ -7,7 +7,7 @@ implementation_status: complete activation_status: staged supersedes: [] superseded_by: [] -verified_commit: 451823ca8ec0b5452ceb8fdc52e777f77a2bbc43 +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [lib/factrail.nix, lib/lockfiles/factrail-57ac25b5.Cargo.lock, lib/claude-code-binary.nix, config/entrypoint-unified.sh, config/claude-plugins/.claude-plugin/marketplace.json, scripts/factrail-store-migrate.mjs, tests/config/factrail-store-migrate.test.mjs, tests/config/factrail-projection.test.sh, schema/agentbox.toml.schema.json, scripts/bake-ruflo-console.sh, scripts/ruflo-console-project.mjs, tests/config/ruflo-console.test.mjs] owner: jjohare review_trigger: a factrail rev bump in lib/factrail.nix, the end of the post-rebuild residency soak, a Claude Code function-hook API change, or a decision to train a local judge on recorded Jev decisions @@ -207,3 +207,11 @@ Tripped by `f93586b9e` (custody W2b and W4: the at-rest migrate/revert and the s 3. **A second registration pattern in one marketplace.** Factrail is still installed with `claude plugin install` into the persistent cache, with the content-digest and config-stamp reinstall. The ruflo mods follow the codex-plugin-cc pattern instead: `scripts/ruflo-console-project.mjs` registers them in `installed_plugins.json` at their stable `/opt/agentbox/config/claude-plugins/` paths, so no cache copy can go stale. It rewrites a wrong path or version every boot, writes `pluginConfigs` cli=`ruflo` (the baked bin; upstream's npx-offline default fails ENOTCACHED on a fresh npm cache), and removes the three ids when the gate is off. The schema and entrypoint diffs are otherwise limited to the new gate. Factrail's pin, projection and userConfig keys are unchanged (`tests/config/factrail-projection.test.sh` passes). The three new files join `verified_paths`. The decision holds. Re-verified by `git log daba195e5..451823ca8 -- `. Nix was not evaluated in this container (no `nix` binary); the image is unverified until the owner's rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`451823ca8..1fc26c786` (nothing in the governed paths moved before `09e6271e9`): `scripts/ruflo-console-project.mjs` (`2df10dc94`) replaces the `import.meta.url === file://argv[1]` main guard with a realpath comparison, so the projector runs when invoked through the image's symlinked path (before, the comparison failed through the symlink and the boot projection did nothing); `tests/config/ruflo-console.test.mjs` adds that symlink case and now asserts `[toolchains].ruflo_console = true` because `a2ffa05eb` set it on in the shipped manifest; `schema/agentbox.toml.schema.json` declares `faucet_units`/`faucet_sats`, `peg_script`, the `[poker_citizen.]` sub-objects and `[poker_coach]` (`bde96a334`, `a2ffa05eb`, `18a85577c`). `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `lib/factrail.nix`, the lockfile, `lib/claude-code-binary.nix`, `marketplace.json`, the store-migrate script and both factrail tests did not move. The decision — factrail implements Jev compaction, in Rust, at a pinned commit — is not affected: the pin, the shim and the projection are unchanged. One dated statement in the `451823ca8` note moves: `ruflo_console` is no longer "default off" in the shipped manifest, so in the running configuration factrail's gate-off branch does not remove the `agentbox` marketplace (consequence 1 of that note) because the console gate holds it. `tests/config/ruflo-console.test.mjs` passes 15/15 at HEAD. Re-verified by `git diff 451823ca8..1fc26c786 -- `. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2122-role-service-accounts-run-secrets-and-the-identity-port.md b/docs/adr/ADR-2122-role-service-accounts-run-secrets-and-the-identity-port.md index 47ea8f26e..1d3b4ddec 100644 --- a/docs/adr/ADR-2122-role-service-accounts-run-secrets-and-the-identity-port.md +++ b/docs/adr/ADR-2122-role-service-accounts-run-secrets-and-the-identity-port.md @@ -7,7 +7,7 @@ implementation_status: partial activation_status: inactive supersedes: [] superseded_by: [] -verified_commit: 09e6271e9d00e2a657e18766172e4cc87355d17d +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 verified_paths: [config/role-accounts.json, services/agentbox-manifest/src/role_accounts.rs, services/agentbox-manifest/src/main.rs, lib/agentbox-manifest.nix, config/lib/role-custody.sh, config/entrypoint-unified.sh, flake.nix, docker-compose.yml, agentbox.toml, setup/agentbox.default.toml, schema/agentbox.toml.schema.json, management-api/lib/system-manifest.js, tests/config/role-isolation-supervisor.test.sh, tests/config/role-secrets-delivery.test.sh, tests/config/role-isolation-boot.test.sh, tests/config/fixtures/role-isolation/supervisord.conf, config/custody/env-classes.json, scripts/ci/env-secret-inventory.js, management-api/lib/role-secret.js, services/nostr-pod-bridge/src/role_secret.rs, services/nostr-pod-bridge/src/bootstrap.rs, tests/runtime-contract/RC-X1-06.sh, config/custody/identity-port-acl.json, services/nostr-pod-bridge/src/identity_port/mod.rs, services/nostr-pod-bridge/src/identity_port/server.rs, management-api/lib/pod-signer.js, scripts/activation/role-isolation-rehearsal.sh, scripts/activation/role-isolation-rehearsal.host.sh, tests/config/role-isolation-rehearsal.test.sh, config/bake-devuser-privilege.sh, tests/runtime-contract/RC-X1-07.sh, tests/security/compose-role-env.test.mjs, docker-compose.override.yml, docker-compose.hp.yml, tests/config/role-custody-migrate.test.sh, config/sidechain/run-producer.sh, config/sidechain/run-faucet.sh, config/sidechain/mirror-sync.sh] owner: jjohare review_trigger: the role-isolation rehearsal (scripts/activation/role-isolation-rehearsal.sh) passing or failing on a rebuilt image; a new secret-bearing supervisor program; a new [sidechain.] chain; a change to the host docker gid; the identity port's consumer cutover (W3b: JunkieJarvis, the mirror hook, the gateway, dream-engine); a change to config/custody/identity-port-acl.json @@ -557,3 +557,11 @@ Tripped by `ad5d0b91a`, a shellcheck-only change to `config/lib/role-custody.sh` ### Re-verification — 2026-10-03 (web-researcher hold note) `cff75f7ea..09e6271e9`: `flake.nix` changes only a comment in `webResearcherMcpPkg` (v1.49.4 held: needs Go 1.27.1, beyond the pinned nixpkgs); no pin, hash or gate changes (`09e6271e9`). Nothing this record governs (ADR-2122 — Role service accounts, /run/secrets, and the identity port) changes meaning. The decision holds. Re-verified by `git log cff75f7ea..09e6271e9 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (ruflo memory governed, 1fc26c786) + +`09e6271e9..1fc26c786`: `config/role-accounts.json` adds `poker-citizen-*` and `poker-coach` to `secret_bearing_programs`, the roles `ab-poker-citizen-dreamlab-txbt4` (uid 971, `house.key` ← `POKER_CITIZEN_KEY_FILE`, at-rest source `/var/lib/agentbox/secrets/poker-citizen-dreamlab-txbt4.key`) and `ab-poker-coach` (uid 972, `coach.key` ← `POKER_COACH_KEY_FILE`; an identity only, no funds), and the BLAKES7 ledger dir `/var/lib/agentbox/events/sidestr/poker-citizen-dreamlab-txbt4` (`971:devuser 2750`, seeded once) (`bde96a334`, `18a85577c`). `flake.nix`: `34f322740` projects `[sidechain].faucet_units`/`faucet_sats` into `[program:sidestr-faucet]`'s environment; `aeca58df6` passes `SIDESTR_PEG_SCRIPT` to the per-chain producer; `bde96a334` adds the `pokerCitizenSeats` map and bakes `[program:poker-citizen-]` per `[poker_citizen.]` (the package gate now counts a seat); `18a85577c` bakes `[program:poker-coach]` under `[poker_coach]`; `b83e0e5d7` adds `findutils` to both producer PATHs; `012bf98f5` adds `rufloGovernedPkg` — `ruflo`/`claude-flow` wrappers that exec `mcp/servers/ruflo-memory-cli.cjs` for `memory` and otherwise run `rufloPkg` with `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` and `CLAUDE_FLOW_MEMORY_PATH` under `~/.cache` as overridable defaults, `claude-flow-mcp` symlinked through unchanged — and swaps it for `rufloPkg` in the gated package list (ADR-2123). Both new programs are `user=devuser` with their key in the `_KEY_FILE` form, the shape `isolate` rewrites under the flag. `agentbox.toml`: `34f322740` adds `[sidechain].faucet_units = 1000`/`faucet_sats = 2000`; `29bff6d94`, `5241b28e7` and `aeca58df6` switch `[sidechain.dreamlab-txbt4]` on (`enabled = true`, `interval = 60`, `peg_script`); `bde96a334`/`f2dfc5bfa` add `[poker_citizen.dreamlab-txbt4]` (the BLAKES7 seat on `:3451`, `asset_id`); `18a85577c` adds `[poker_coach]`; `a2ffa05eb` moves `[toolchains].ruflo_console` beside `ruflo` and sets it `true`. `schema/agentbox.toml.schema.json` declares `faucet_units`/`faucet_sats`, `peg_script`, the `[poker_citizen.]` sub-objects and `[poker_coach]` (`bde96a334`, `a2ffa05eb`, `18a85577c`). `management-api/lib/system-manifest.js` adds the rebuild-class catalogue entries `poker-citizen-dreamlab-txbt4` (requires `sidechain.dreamlab-txbt4.enabled`) and `poker-coach` (`bde96a334`, `18a85577c`). `lib/agentbox-manifest.nix` (`4645702a5`) copies `config/custody/env-classes.json` into the hermetic crate check and rewrites `role_accounts.rs`'s path to it, so the key-variable rule's test no longer reads outside the sandbox. `config/sidechain/run-producer.sh` passes `--peg-script` (`aeca58df6`): a public output script, not a secret. `config/entrypoint-unified.sh` changes in one place (`012bf98f5`): three exports in the runtime-env block after `RUFLO_DAEMON_AI_WORKERS` — `RUFLO_DAEMON_AUTOSTART` (default `0`), `CLAUDE_FLOW_DISABLE_BRIDGE` (default `1`) and `CLAUDE_FLOW_MEMORY_PATH` (default `/home/devuser/.cache/ruflo/memory`), each `${X:-default}` so an operator export wins (ADR-2123). `config/custody/env-classes.json` (`1fc26c786`) classifies those three switches NON_SECRET beside `RUFLO_DAEMON_AI_WORKERS` (paths and switches, no secret). The other new names (`SIDESTR_PEG_SCRIPT`, `SIDESTR_FAUCET_UNITS`/`_SATS`, `POKER_COACH_*`) are not in the table; `node scripts/ci/env-secret-inventory.js --check` passes at HEAD because none is credential-shaped and the two key paths are the `_KEY_FILE` form carried by a role. The decision holds and its `e3b06d688` rule is seen working: both key-holding programs arrived with a role in the same commits, so `isolate` has nothing to refuse. The role table in this record lists 970 as the last row; 971 and 972 are the two rows this note adds. `[security].role_isolation` still ships `false`. `isolate` was not run here. Re-verified by `git diff 09e6271e9..1fc26c786 -- `. Nix was not evaluated here; the image is unverified until the host rebuild. + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: `config/entrypoint-unified.sh` changes one line in the runtime-env heredoc — `CLAUDE_FLOW_MEMORY_PATH` is escaped so it resolves in the sourcing shell (`$HOME/.cache/ruflo/memory`) instead of as a root-side `/home/devuser` literal (RC-X1-01, `34f5e4254`). No path, gate, projection or program this record governs changed. Nothing this record decides changed. diff --git a/docs/adr/ADR-2123-govern-the-ruflo-memory-cli-on-the-sidecar.md b/docs/adr/ADR-2123-govern-the-ruflo-memory-cli-on-the-sidecar.md new file mode 100644 index 000000000..87efdbd67 --- /dev/null +++ b/docs/adr/ADR-2123-govern-the-ruflo-memory-cli-on-the-sidecar.md @@ -0,0 +1,50 @@ +--- +id: ADR-2123 +title: Govern the ruflo memory CLI on the ruvector-postgres sidecar +date: 2026-10-03 +decision_status: accepted +implementation_status: complete +activation_status: live +supersedes: [] +superseded_by: [] +verified_commit: 34f5e425403bde5b1c4f13375406d33fcbb22b22 +verified_paths: [mcp/servers/ruflo-memory-cli.cjs, tests/contract/ruflo-memory-cli.contract.spec.js, tests/config/ruflo-memory-governed.test.sh] +owner: jjohare +review_trigger: a ruflo release whose memory subsystem gains a Postgres backend or an external embedding provider, or a ruflo-console release whose memory pane stops shelling out to `ruflo memory stats|list --format json` +repo: agentbox +--- + +# ADR-2123 — Govern the ruflo memory CLI on the ruvector-postgres sidecar + +## Context + +Durable memory in this image is the ruvector-postgres sidecar, embedded by Xinference (bge-small-en-v1.5, 384-dim, GPU) and served by `mcp/servers/ruvector-mcp.cjs` under the `claude-flow` MCP name (ADR-015, ADR-2014, ADR-2019). ruflo 3.51.1 (ADR-2020 gate `ruflo_console`, baked 2026-10-03) ships its own memory subsystem, and it is local-only: `memory init` writes sql.js SQLite under `.swarm/`, an AgentDB mirror, the native engine's `./ruvector.db` in the working directory and a MiniLM ONNX embedder whose cache path is the read-only Nix store. Its `--backend` flag is a label written into a metadata table; no value reaches Postgres, no setting selects an embedding endpoint, and its MCP server given the sidecar's full `RUVECTOR_PG_*`/`PG*` env still searched an empty local store (tested 2026-10-03). Every `ruflo` invocation, `--help` included, also autostarted a worker daemon into the working directory. The ruflo-console memory pane reaches memory only by shelling out to `ruflo memory stats --format json` and `ruflo memory list --format json --limit 500`, so with the stock binary it shows an empty local store, never the governed corpus. One `memory init` run on 2026-10-03 left three local stores in a repository, one of them not gitignored. + +## Decision + +The `ruflo` and `claude-flow` bins the image puts on PATH are governed wrappers (`rufloGovernedPkg` in `flake.nix`), not the package's own. + +1. `ruflo memory …` execs `mcp/servers/ruflo-memory-cli.cjs`. It reuses `lib/memory-tools.js` with the same pool, embedding transport and `agentbox::` entry ids as the MCP server, so a key written on either path is the same row. It serves `store`, `retrieve`, `search`, `list`, `delete` and `stats` from the sidecar and emits ruflo 3.51.1's `--format json` shapes, so the console's parsers are unchanged and its memory pane shows the sidecar corpus. It fails closed: no Postgres is exit 1, an unembeddable write is rejected (ADR-2014), and `init`, `configure`, `cleanup`, `compress`, `export`, `import`, `purge`, `distill`, `backup`, `classify`, `select-operator` and `migrate` are refused with exit 2 and no file written. +2. Every other subcommand runs the real CLI with three env defaults, each overridable by an operator export and also exported at boot: `RUFLO_DAEMON_AUTOSTART=0`, `CLAUDE_FLOW_DISABLE_BRIDGE=1` (no AgentDB mirror, no `./ruvector.db`, no MiniLM download) and `CLAUDE_FLOW_MEMORY_PATH=$HOME/.cache/ruflo/memory` (ruflo's sql.js bookkeeping outside any repository). `claude-flow-mcp`, the ADR-2082 proxy child, passes through unchanged. +3. The gate is unchanged: the wrapper ships only when ruflo does (`toolchains.ruflo`, `toolchains.claude_flow` or `toolchains.ruflo_console`), so a gate-off image is byte-identical (ADR-2020). + +## Consequences + +- The console's memory pane, `cf-memory`, `aisp/init-aisp.sh` and any agent that types `ruflo memory store` all write to and read from the sidecar through the GPU embedder; there is one memory system. The cost is that ruflo's local-only memory verbs are gone from this image, and anything that needs them (a ruflo memory export, its distillation operators) must be done against the sidecar's own tooling instead. +- Disabling the AgentDB bridge is a behaviour change for the real CLI's non-memory paths (hooks, the proxy's swarm child): they keep sql.js and lose the AgentDB vector mirror. Memory tools never cross the proxy (ADR-2082), so no governed consumer is affected; an operator can re-enable it with `CLAUDE_FLOW_DISABLE_BRIDGE=0` for one invocation. +- The CLI is a second consumer of `lib/memory-tools.js`. A change to that library's contract must keep `tests/contract/ruflo-memory-cli.contract.spec.js` green alongside the server's suites. +- The review trigger above is the exit: when upstream ruflo can target Postgres and an external embedder natively, the wrapper's `memory` branch becomes configuration and this record is superseded. + +## Upstream lineage — why there are two memory systems in ruvnet's estate + +Recorded 2026-10-03 from the ruvnet-kb corpus so the next reader does not re-derive it or assume the two are one. `ruvector-core` (HNSW, SIMD) is a library packaged into two separate products that share algorithms, not storage or wire: the embedded engine (`ruvector` npm, `@ruvector/core`, NAPI, default file `./ruvector.db`) and the Postgres extension (`crates/ruvector-postgres`, a pgvector drop-in, image `ruvnet/ruvector-postgres`). ruflo's memory grew on the embedded side by design: claude-flow ADR-009 (SQLite plus AgentDB), ADR-017 (embedded ruvector into AgentDB), ADR-057 (RVF to replace sql.js), ADR-342 (ruvector as a flagged vector backend). Its target is zero-infrastructure `npx` use, browser and edge included, so memory must embed. Postgres was explicitly deferred (agentic-flow revised plan, 2025-12-30: "enterprise scale, not needed initially") and later scoped by ruflo ADR-027 (2026-01-16, still Proposed) as an optional plugin bridge plus the `ruflo ruvector` provisioning commands, never a memory backend. The extension itself is actively maintained: v2.0.0 (December 2025, security audit), ruvector ADR-044 "v0.3 extension upgrade" accepted and in progress, crate 2.0.6, images for Postgres 14 to 17 on amd64 and arm64. This image runs 2.0.5 with extension 0.3.0 on PostgreSQL 17.9: the current line. Lock-in is low: rows are ordinary tables with a pgvector-compatible `ruvector(384)` column, so the exit is a dump and a cast. + +**Rule this record adds.** Same vendor and same dimension do not mean same wire. Before treating two components as compatible, prove it at the wire with a live probe (here: ruflo's own MCP server given the sidecar's full connection env still searched an empty local store). ADR-015's choice of the Postgres shape is reaffirmed; the thing to watch is ruflo ADR-027, named in `review_trigger`. + +## Verification + +Live, 2026-10-03, against the running sidecar (213,332 rows, 464 namespaces, ruvector-postgres 0.3.0, Xinference bge-small-en-v1.5): `stats --format json` and `list --format json --limit 3` returned the governed corpus in the ruflo shapes; `search -q … -n project-state` returned HNSW results at 0.795 and 0.776 via `hnsw-xinference`; a `store` in namespace `cli-probe` embedded, was found by `search` at 0.769, was returned by `retrieve` and removed by `delete`; `init`, `configure` exit 2 with no file created; `git status` clean apart from the new file. Static and stub-backed: `tests/config/ruflo-memory-governed.test.sh` (wrapper, gate, env defaults, refusal list, no shipped `memory init`) and `tests/contract/ruflo-memory-cli.contract.spec.js` (shapes against the captured ruflo 3.51.1 output, refusals, fail-closed, protected-namespace refusal through the governed `memStore`, flag parsing). The baked wrapper itself is verified at the host rebuild (`ruflo memory help` prints the governed usage; `ruflo --help` in an empty directory writes nothing). + +### Re-verification — 2026-10-03 (runtime-env path escape, 34f5e4254) + +`1fc26c786..34f5e4254`: the boot export of `CLAUDE_FLOW_MEMORY_PATH` is escaped in the runtime-env heredoc so it resolves per user (`$HOME/.cache/ruflo/memory`), satisfying RC-X1-01; `tests/config/ruflo-memory-governed.test.sh` accepts the escaped form. The decision (governed `ruflo memory`, repo-safe defaults) is unchanged; the wrapper's own default was already `$HOME`-relative. Live checks unchanged. diff --git a/docs/adr/README.md b/docs/adr/README.md index f7fecdc19..9f2a137a9 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -59,7 +59,7 @@ detect changes to files it already lists — it is structurally blind to **addit is exactly how a new crate landed unlicensed under ADR-2030 while the record still read as verified and its `review_trigger` ("any new crate under `services/`") sat unactioned. -_114 record(s). Regenerate with_ `node scripts/adr-index-gen.js docs/adr`. +_115 record(s). Regenerate with_ `node scripts/adr-index-gen.js docs/adr`. | ID | Title | Domain | Date | Decision | Impl | Activation | Supersedes | Superseded by | Owner | Repo | |----|-------|--------|------|----------|------|------------|------------|---------------|-------|------| @@ -177,3 +177,4 @@ _114 record(s). Regenerate with_ `node scripts/adr-index-gen.js docs/adr`. | [ADR-2120](ADR-2120-codex-daemon-package-volume.md) | Give Codex daemon packages executable persistent storage | BASELINE-container | 2026-10-01 | accepted | complete | live | — | — | jjohare | agentbox | | [ADR-2121](ADR-2121-factrail-implements-jev-compaction.md) | Implement Jev compaction with factrail — fact rails in Rust, baked at a pinned commit | GOVERNANCE-capabilities | 2026-10-02 | accepted | complete | staged | — | — | jjohare | agentbox | | [ADR-2122](ADR-2122-role-service-accounts-run-secrets-and-the-identity-port.md) | Role service accounts, /run/secrets, and the identity port | SECURITY-profiles | 2026-10-03 | proposed | partial | inactive | — | — | jjohare | agentbox | +| [ADR-2123](ADR-2123-govern-the-ruflo-memory-cli-on-the-sidecar.md) | Govern the ruflo memory CLI on the ruvector-postgres sidecar | — | 2026-10-03 | accepted | complete | live | — | — | jjohare | agentbox | diff --git a/flake.nix b/flake.nix index d384d25f3..c248c48fe 100644 --- a/flake.nix +++ b/flake.nix @@ -461,6 +461,48 @@ }; }; + # 2+3 (governed). The `ruflo` / `claude-flow` bins the image puts on + # PATH are these wrappers, not rufloPkg's (ADR-2123). ruflo 3.51.1's + # memory subsystem is local-only: sql.js under .swarm/, an AgentDB + # mirror, the native engine's ./ruvector.db in the CWD and a MiniLM + # ONNX embedder; its --backend flag is a label and no setting reaches + # Postgres or an external embedder. Durable memory here is the + # ruvector-postgres sidecar embedded by Xinference (ADR-015/2014), so: + # * `ruflo memory …` execs mcp/servers/ruflo-memory-cli.cjs, which + # reuses the governed server's lib/memory-tools.js (same pool, + # embedder, entry ids) and emits ruflo-shaped --format json. The + # ruflo-console memory pane shells out to exactly these calls, so + # it shows the sidecar corpus; init/configure/export/… are refused. + # * every other subcommand runs the real CLI with defaults that keep + # its residual local state out of repositories: no daemon + # autostart (every `ruflo` invocation, even --help, spawned one), + # AgentDB bridge off (no ./ruvector.db, no agentdb-memory.db, no + # MiniLM download into the read-only store path), and the sql.js + # bookkeeping store under ~/.cache/ruflo/memory instead of + # /.swarm. Each is an env default: an operator export wins. + # claude-flow-mcp is passed through unchanged (ADR-2082 proxy child). + rufloGovernedPkg = pkgs.runCommand "ruflo-governed-3.51.1" { } '' + mkdir -p $out/bin + for b in ruflo claude-flow; do + cat > $out/bin/$b </dev/null || true + exec ${rufloPkg}/bin/$b "\$@" + WRAP + chmod +x $out/bin/$b + done + ln -s ${rufloPkg}/bin/claude-flow-mcp $out/bin/claude-flow-mcp + ln -s ${rufloPkg}/lib $out/lib + ''; + # 3b+3c. metaharness runtime binaries — gated by toolchains.metaharness # (ADR-062/063/064). Bakes the CLIs the ruflo-metaharness plugin # skills shell out to, at the versions the plugin tree pins @@ -681,7 +723,9 @@ # ruflo_console reaches ruflo through the `ruflo` bin on PATH (its # userConfig cli is baked to "ruflo"), so the console gate pulls the # closure in too. - lib.optionals ((toolchainCfg.ruflo or false) || (toolchainCfg.claude_flow or false) || rufloConsoleOn) [ rufloPkg ] + # rufloGovernedPkg wraps rufloPkg's bins (ADR-2123: `memory` → the + # governed CLI on the sidecar; everything else with repo-safe defaults). + lib.optionals ((toolchainCfg.ruflo or false) || (toolchainCfg.claude_flow or false) || rufloConsoleOn) [ rufloGovernedPkg ] ++ lib.optionals (toolchainCfg.metaharness or false) [ metaharnessPkg metaharnessDarwinPkg ] ++ lib.optionals (toolchainCfg.agentic_qe or false) [ agenticQePkg ] ++ lib.optionals (toolchainCfg.codebase_memory or false) [ codebaseMemoryPkg ] diff --git a/mcp/servers/ruflo-memory-cli.cjs b/mcp/servers/ruflo-memory-cli.cjs new file mode 100644 index 000000000..ccb04a7ae --- /dev/null +++ b/mcp/servers/ruflo-memory-cli.cjs @@ -0,0 +1,483 @@ +#!/usr/bin/env node +'use strict'; +/** + * ruflo-memory-cli.cjs — the governed `ruflo memory …` (ADR-2123). + * + * ruflo 3.51.1's own memory subsystem is a local store: sql.js SQLite under + * `.swarm/`, an AgentDB mirror, the native engine's `./ruvector.db` in the + * working directory, and a MiniLM ONNX embedder. Its `--backend` flag is a + * label written into a metadata table; no value reaches Postgres, and no + * setting selects an external embedding endpoint. In this image durable memory + * is the ruvector-postgres sidecar, embedded by Xinference (bge-small-en-v1.5, + * 384-dim, GPU) and served by `ruvector-mcp.cjs` (ADR-015, ADR-2014). + * + * The baked `ruflo` / `claude-flow` wrappers exec this file for the `memory` + * subcommand. It reuses `lib/memory-tools.js` — the same store/retrieve/search/ + * delete logic the MCP server runs, with the same pool, embedding transport and + * entry-id scheme — so a key written here is the same row the memory_* tools + * read, and vice versa. The ruflo-console memory pane (which shells out to + * `ruflo memory stats --format json` and `ruflo memory list --format json`) + * therefore shows the governed corpus. + * + * Output shapes with `--format json` match ruflo 3.51.1's so the console's + * parsers (plugins/ruflo-console/hooks/data/cli.ts) need no change: + * stats → { backend, entries:{total,vectors,text}, storage:{total,location}, + * version, oldestEntry, newestEntry } + * list → [ { id, key, namespace, size, accessCount, createdAt, updatedAt, + * hasEmbedding, provenanceType } ] + * + * Subcommands that would create or mutate a local store (init, configure, + * backup, compress, cleanup, export, import, purge, distill, migrate, …) are + * refused with exit 2: fail closed, never a second memory system. + */ + +const http = require('http'); +const path = require('path'); + +const { + createExternalPgBackend, + shapeSearchResponse, + resolveSearchLimit, + wildcardExcludedNamespaces, + DEFAULT_MIN_SCORE, +} = require(path.join(__dirname, 'lib', 'memory-tools.js')); +const { notExpiredPredicate } = require(path.join(__dirname, 'lib', 'memory-metadata.js')); + +// ── Constants mirrored from ruvector-mcp.cjs (single write-source so ids match) ── +const WRITE_SOURCE_TYPE = 'agentbox'; +const EMBEDDING_DIM = 384; +const XINFERENCE_URL = process.env.XINFERENCE_ENDPOINT || 'http://xinference:9997'; +const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL || 'bge-small-en-v1.5'; +const PG_SEARCH_PATHS = [ + '/home/devuser/workspace/.claude-pg/node_modules/pg', + '/opt/agentbox/management-api/node_modules/pg', + 'pg', +]; +const NOT_EXPIRED = notExpiredPredicate('metadata'); + +const ALLOWED = new Set(['store', 'retrieve', 'get', 'search', 'list', 'ls', 'delete', 'rm', 'stats', 'help', '--help', '-h']); +// Everything ruflo's memory command exposes that would build or mutate a local store. +const REFUSED = new Set([ + 'init', 'configure', 'config', 'cleanup', 'compress', 'export', 'import', 'purge', + 'distill', 'backup', 'classify', 'select-operator', 'migrate', +]); + +const EXIT_OK = 0; +const EXIT_FAIL = 1; +const EXIT_REFUSED = 2; +const EXIT_USAGE = 64; + +// ── Argument parsing (ruflo's flag spellings: -k/--key, --key=value, …) ───────── +const FLAG_ALIASES = { + k: 'key', v: 'value', n: 'namespace', q: 'query', l: 'limit', t: 'type', s: 'smart', +}; + +function parseArgs(argv) { + const flags = {}; + const positional = []; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--') { positional.push(...argv.slice(i + 1)); break; } + if (a.startsWith('--')) { + const eq = a.indexOf('='); + const name = eq > 0 ? a.slice(2, eq) : a.slice(2); + if (eq > 0) { flags[name] = a.slice(eq + 1); continue; } + const next = argv[i + 1]; + if (next !== undefined && !next.startsWith('-')) { flags[name] = next; i++; } + else flags[name] = true; + continue; + } + if (a.startsWith('-') && a.length > 1) { + const short = a.slice(1); + const name = FLAG_ALIASES[short] || short; + const next = argv[i + 1]; + if (next !== undefined && !next.startsWith('-')) { flags[name] = next; i++; } + else flags[name] = true; + continue; + } + positional.push(a); + } + return { flags, positional }; +} + +function wantJson(flags) { + return flags.format === 'json' || flags.json === true; +} + +function usage() { + return [ + 'ruflo memory — governed by agentbox: the ruvector-postgres sidecar, embedded by Xinference (ADR-2123)', + '', + 'SUBCOMMANDS:', + ' store -k -v [-n ] [--tags a,b] [--importance 0-1] [--type semantic|episodic|procedural|working|pattern] [--ttl ]', + ' retrieve -k [-n ] (alias: get)', + ' search -q [-n |*] [-l ] [--threshold 0-1]', + ' list [-n |*] [--limit ] (alias: ls; default: every namespace, newest first)', + ' delete -k [-n ] (alias: rm)', + ' stats', + '', + 'OPTIONS:', + ' --format json machine output (the shapes the ruflo-console memory pane reads)', + ' --verbose backend diagnostics on stderr', + '', + 'Refused here (they would create a second, local memory store): init, configure, cleanup, compress,', + 'export, import, purge, distill, backup, classify, select-operator, migrate.', + 'Durable memory is MCP-first: prefer the memory_* tools of the claude-flow MCP server (ADR-2014).', + ].join('\n'); +} + +// ── Backend wiring (the same shape ruvector-mcp.cjs injects) ───────────────────── +function makeLogger(verbose) { + return (level, msg) => { + if (level === 'DEBUG' && !verbose) return; + if (level === 'INFO' && !verbose) return; + process.stderr.write(`[ruflo-memory] [${level}] ${msg}\n`); + }; +} + +function loadPg() { + for (const p of PG_SEARCH_PATHS) { + try { return require(p); } catch { /* next */ } + } + return null; +} + +function parseConninfo(conninfo) { + const parsed = {}; + for (const pair of String(conninfo).split(/\s+/)) { + const eq = pair.indexOf('='); + if (eq > 0) parsed[pair.slice(0, eq)] = pair.slice(eq + 1); + } + return { + host: parsed.host || 'ruvector-postgres', + port: parseInt(parsed.port || '5432', 10), + database: parsed.dbname || parsed.database || 'ruvector', + user: parsed.user || parsed.username || 'ruvector', + password: parsed.password || 'ruvector', + }; +} + +function makePool(log) { + const PgModule = loadPg(); + if (!PgModule) { + log('ERROR', `pg module unavailable; searched ${PG_SEARCH_PATHS.join(', ')} and NODE_PATH=${process.env.NODE_PATH || '(unset)'}`); + return null; + } + const conn = parseConninfo(process.env.RUVECTOR_PG_CONNINFO || + 'host=ruvector-postgres port=5432 dbname=ruvector user=ruvector password=ruvector'); + return { + pool: new PgModule.Pool({ ...conn, max: 2, idleTimeoutMillis: 2000, connectionTimeoutMillis: 5000 }), + location: `${conn.host}:${conn.port}/${conn.database}`, + }; +} + +function getEmbedding(text) { + const body = JSON.stringify({ model: EMBEDDING_MODEL, input: text }); + return new Promise((resolve, reject) => { + const url = new URL(XINFERENCE_URL + '/v1/embeddings'); + const req = http.request({ + hostname: url.hostname, port: url.port, path: url.pathname, + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, + timeout: 10000, + }, res => { + let data = ''; + res.on('data', c => data += c); + res.on('end', () => { + try { + const j = JSON.parse(data); + const emb = j && j.data && j.data[0] && j.data[0].embedding; + if (!emb) return reject(new Error(`unexpected response: ${data.substring(0, 200)}`)); + if (emb.length !== EMBEDDING_DIM) return reject(new Error(`dimension mismatch: got ${emb.length}, expected ${EMBEDDING_DIM}`)); + resolve(emb); + } catch (e) { reject(new Error(`parse error: ${e.message}`)); } + }); + }); + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); + req.write(body); + req.end(); + }); +} + +function vecToSql(arr) { return '[' + arr.join(',') + ']'; } +function entryId(namespace, key) { return `${WRITE_SOURCE_TYPE}:${namespace}:${key}`; } +function parseVal(v) { + if (typeof v === 'string') { try { return JSON.parse(v); } catch { return v; } } + return v; +} + +/** + * Build the governed backend. `deps` lets tests inject a fake pool and embedder; + * production passes nothing and gets the real sidecar wiring. + */ +function buildBackend({ verbose = false, deps = {} } = {}) { + const log = deps.log || makeLogger(verbose); + let pool = deps.pool || null; + let location = deps.location || 'ruvector-postgres:5432/ruvector'; + // deps.pool === null is an explicit "no sidecar" (tests); undefined means + // build the real pool. + if (!pool && deps.pool !== null) { + const made = makePool(log); + if (made) { pool = made.pool; location = made.location; } + } + const pgOk = !!pool; + const embed = deps.getEmbedding || getEmbedding; + let xinfOk = null; + const xinfEnsure = deps.xinfEnsure || (async () => { + if (xinfOk !== null) return xinfOk; + try { await embed('probe'); xinfOk = true; } + catch (e) { xinfOk = false; log('WARN', `xinference unavailable at ${XINFERENCE_URL}: ${e.message}`); } + return xinfOk; + }); + const tools = createExternalPgBackend({ + pool, + getPgOk: () => pgOk, + getEmbedding: embed, + xinfEnsure, + vecToSql, + entryId, + parseVal, + notifyMemoryFlash: () => {}, + notifyMemoryFlashBatch: () => {}, + log, + writeSourceType: WRITE_SOURCE_TYPE, + }); + return { tools, pool, pgOk, location, log, close: async () => { if (pool && pool.end && !deps.pool) await pool.end(); } }; +} + +// ── Shapes (ruflo 3.51.1 `--format json` compatible) ──────────────────────────── +function toMs(v) { + if (v === null || v === undefined) return null; + const d = v instanceof Date ? v : new Date(v); + const t = d.getTime(); + return Number.isFinite(t) ? t : null; +} +function toIso(v) { const ms = toMs(v); return ms === null ? null : new Date(ms).toISOString(); } + +function shapeStats(row, { location, version, namespaces }) { + const total = Number(row.total) || 0; + const vectors = Number(row.vectors) || 0; + return { + backend: `ruvector-postgres (hnsw, xinference ${EMBEDDING_MODEL})`, + entries: { total, vectors, text: total - vectors }, + storage: { total: `${Number(row.bytes) || 0} B`, location }, + version: version || 'ruvector-postgres', + oldestEntry: toIso(row.oldest), + newestEntry: toIso(row.newest), + namespaces, + embedding: { model: EMBEDDING_MODEL, dimensions: EMBEDDING_DIM, endpoint: XINFERENCE_URL }, + }; +} + +function shapeListRow(r) { + return { + id: r.id, + key: r.key, + namespace: r.namespace, + size: Number(r.size) || 0, + accessCount: Number(r.access_count) || 0, + createdAt: toMs(r.created_at), + updatedAt: toMs(r.updated_at), + hasEmbedding: r.has_embedding === true, + provenanceType: 'unknown', + sourceType: r.source_type || null, + }; +} + +// ── Commands ──────────────────────────────────────────────────────────────────── +async function cmdStats(be) { + const { pool, location } = be; + const excluded = wildcardExcludedNamespaces(); + const q = await pool.query( + `SELECT count(*)::bigint AS total, + count(embedding)::bigint AS vectors, + min(created_at) AS oldest, + max(created_at) AS newest, + count(DISTINCT namespace)::int AS namespaces, + pg_total_relation_size('memory_entries')::bigint AS bytes + FROM memory_entries + WHERE ${NOT_EXPIRED} AND NOT (namespace = ANY($1::text[]))`, + [excluded], + ); + let version = null; + try { + const v = await pool.query(`SELECT extversion FROM pg_extension WHERE extname = 'ruvector' LIMIT 1`); + version = v.rows.length ? `ruvector-postgres ${v.rows[0].extversion}` : null; + } catch { /* extension catalogue optional */ } + return shapeStats(q.rows[0], { location, version, namespaces: Number(q.rows[0].namespaces) || 0 }); +} + +async function cmdList(be, flags) { + const { pool } = be; + const ns = flags.namespace ? String(flags.namespace) : '*'; + const limit = Math.max(1, Math.min(parseInt(flags.limit || '100', 10) || 100, 5000)); + const excluded = ns === '*' ? wildcardExcludedNamespaces() : []; + const q = await pool.query( + `SELECT id, key, namespace, length(value::text) AS size, access_count, + created_at, updated_at, (embedding IS NOT NULL) AS has_embedding, source_type + FROM memory_entries + WHERE (namespace = $1 OR $1 = '*') AND ${NOT_EXPIRED} + AND NOT (namespace = ANY($3::text[])) + ORDER BY created_at DESC + LIMIT $2`, + [ns, limit, excluded], + ); + return q.rows.map(shapeListRow); +} + +async function cmdSearch(be, flags) { + if (!flags.query) return { usage: 'search requires -q ' }; + const ns = flags.namespace ? String(flags.namespace) : '*'; + const limit = resolveSearchLimit(parseInt(flags.limit || '10', 10) || 10); + const minScore = flags.threshold !== undefined ? Number(flags.threshold) : DEFAULT_MIN_SCORE; + const t0 = Date.now(); + const raw = await be.tools.memSearch(String(flags.query), ns, limit, null, {}); + const shaped = shapeSearchResponse(raw, { minScore, limit }); + if (!shaped || shaped.success !== true) return { error: (shaped && shaped.error) || 'search failed', raw: shaped }; + return { + query: String(flags.query), + namespace: ns, + results: shaped.results, + total: shaped.results.length, + searchTime: `${Date.now() - t0}ms`, + backend: `ruvector-postgres (${shaped.method || 'hnsw-xinference'})`, + ...(shaped.degraded ? { degraded: true, warning: shaped.warning } : {}), + }; +} + +async function cmdStore(be, flags) { + if (!flags.key || flags.value === undefined || flags.value === true) return { usage: 'store requires -k -v ' }; + const ns = flags.namespace ? String(flags.namespace) : 'default'; + const options = {}; + if (flags.tags) options.tags = String(flags.tags).split(',').map(s => s.trim()).filter(Boolean); + if (flags.importance !== undefined) options.importance = Number(flags.importance); + if (flags.type && flags.type !== true) options.memory_type = String(flags.type); + if (flags.ttl !== undefined) options.ttl_seconds = Number(flags.ttl); + return be.tools.memStore(String(flags.key), String(flags.value), ns, options); +} + +async function cmdRetrieve(be, flags) { + if (!flags.key) return { usage: 'retrieve requires -k ' }; + const ns = flags.namespace ? String(flags.namespace) : 'default'; + return be.tools.memRetrieve(String(flags.key), ns); +} + +async function cmdDelete(be, flags) { + if (!flags.key) return { usage: 'delete requires -k ' }; + const ns = flags.namespace ? String(flags.namespace) : 'default'; + return be.tools.memDelete(String(flags.key), ns); +} + +// ── Human rendering ───────────────────────────────────────────────────────────── +function renderHuman(sub, out) { + if (sub === 'stats') { + return [ + `Backend: ${out.backend}`, + `Location: ${out.storage.location}`, + `Entries: ${out.entries.total} (${out.entries.vectors} embedded, ${out.entries.text} pending)`, + `Namespaces: ${out.namespaces}`, + `Storage: ${out.storage.total}`, + `Oldest: ${out.oldestEntry || 'n/a'}`, + `Newest: ${out.newestEntry || 'n/a'}`, + `Embedding: ${out.embedding.model} (${out.embedding.dimensions}d) via ${out.embedding.endpoint}`, + ].join('\n'); + } + if (sub === 'list' || sub === 'ls') { + if (!out.length) return '(no entries)'; + const w = Math.min(60, Math.max(...out.map(r => r.key.length))); + return out.map(r => `${r.namespace.padEnd(20)} ${r.key.padEnd(w)} ${r.hasEmbedding ? 'vec' : ' '} ${new Date(r.createdAt).toISOString()}`).join('\n'); + } + if (sub === 'search') { + if (out.error) return `search failed: ${out.error}`; + const head = `${out.total} result(s) for "${out.query}" in ${out.namespace} [${out.backend}, ${out.searchTime}]${out.degraded ? ' DEGRADED: ' + out.warning : ''}`; + const rows = out.results.map(r => ` ${(Number(r.score) || 0).toFixed(3)} ${r.namespace || ''}/${r.key}${r.snippet ? '\n ' + String(r.snippet).replace(/\s+/g, ' ').slice(0, 160) : ''}`); + return [head, ...rows].join('\n'); + } + if (sub === 'retrieve' || sub === 'get') { + if (!out.found) return `(not found) ${out.namespace}/${out.key}`; + return typeof out.value === 'string' ? out.value : JSON.stringify(out.value, null, 2); + } + if (sub === 'store') { + return out.success ? `stored ${out.namespace}/${out.key} (embedded=${out.embedded === true})` : `store failed: ${out.error}`; + } + if (sub === 'delete' || sub === 'rm') { + return out.success ? `deleted ${out.deleted} row(s) at ${out.namespace}/${out.key}` : `delete failed: ${out.error}`; + } + return JSON.stringify(out, null, 2); +} + +// ── Entry point ───────────────────────────────────────────────────────────────── +/** + * Run the CLI. Returns { code, stdout, stderr } instead of exiting so tests can + * drive it in-process with an injected backend. + */ +async function run(argv, { deps } = {}) { + const { flags, positional } = parseArgs(argv); + const sub = positional[0] || 'help'; + const json = wantJson(flags); + const emit = (obj, text) => (json ? JSON.stringify(obj, null, 2) : text); + + if (REFUSED.has(sub)) { + const msg = `ruflo memory ${sub} is refused in agentbox: it would create or mutate a local memory store. ` + + 'Durable memory is the ruvector-postgres sidecar (ADR-2014, ADR-2123); use the memory_* MCP tools, ' + + 'or this CLI\'s store/retrieve/search/list/delete/stats, which serve the sidecar.'; + return { code: EXIT_REFUSED, stdout: json ? JSON.stringify({ success: false, refused: sub, error: msg }) : '', stderr: json ? '' : msg + '\n' }; + } + if (!ALLOWED.has(sub)) { + return { code: EXIT_USAGE, stdout: '', stderr: `unknown subcommand: ${sub}\n\n${usage()}\n` }; + } + if (sub === 'help' || sub === '--help' || sub === '-h') { + return { code: EXIT_OK, stdout: usage() + '\n', stderr: '' }; + } + + const be = buildBackend({ verbose: flags.verbose === true, deps }); + if (!be.pgOk) { + const err = 'ruvector-postgres unavailable (pg module or connection missing) — fail closed (ADR-2014)'; + await be.close(); + return { code: EXIT_FAIL, stdout: json ? JSON.stringify({ success: false, error: err, backend: 'ruvector-postgres' }) : '', stderr: json ? '' : err + '\n' }; + } + + try { + let out; + switch (sub) { + case 'stats': out = await cmdStats(be); break; + case 'list': case 'ls': out = await cmdList(be, flags); break; + case 'search': out = await cmdSearch(be, flags); break; + case 'store': out = await cmdStore(be, flags); break; + case 'retrieve': case 'get': out = await cmdRetrieve(be, flags); break; + case 'delete': case 'rm': out = await cmdDelete(be, flags); break; + default: out = null; + } + if (out && out.usage) return { code: EXIT_USAGE, stdout: '', stderr: `${out.usage}\n\n${usage()}\n` }; + const failed = out && typeof out === 'object' && !Array.isArray(out) && out.success === false; + const code = failed ? EXIT_FAIL : EXIT_OK; + return { code, stdout: emit(out, renderHuman(sub, out)) + '\n', stderr: '' }; + } catch (e) { + const err = `${sub} failed: ${e && e.message ? e.message : String(e)}`; + return { code: EXIT_FAIL, stdout: json ? JSON.stringify({ success: false, error: err }) + '\n' : '', stderr: json ? '' : err + '\n' }; + } finally { + await be.close(); + } +} + +module.exports = { + run, parseArgs, shapeStats, shapeListRow, buildBackend, usage, + ALLOWED, REFUSED, EXIT_OK, EXIT_FAIL, EXIT_REFUSED, EXIT_USAGE, + WRITE_SOURCE_TYPE, EMBEDDING_DIM, +}; + +if (require.main === module) { + // The wrapper passes everything after `memory`; a direct invocation may + // still start with it. + const argv = process.argv.slice(2); + if (argv[0] === 'memory') argv.shift(); + run(argv).then(({ code, stdout, stderr }) => { + if (stdout) process.stdout.write(stdout); + if (stderr) process.stderr.write(stderr); + process.exitCode = code; + }).catch(e => { + process.stderr.write(`[ruflo-memory] fatal: ${e && e.stack ? e.stack : e}\n`); + process.exitCode = EXIT_FAIL; + }); +} diff --git a/tests/config/ruflo-memory-governed.test.sh b/tests/config/ruflo-memory-governed.test.sh new file mode 100644 index 000000000..257a2bab9 --- /dev/null +++ b/tests/config/ruflo-memory-governed.test.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# ADR-2123 — the image's `ruflo` / `claude-flow` bins are the governed wrappers: +# `memory` routes to mcp/servers/ruflo-memory-cli.cjs (the ruvector-postgres +# sidecar via lib/memory-tools.js), every other subcommand runs with no daemon +# autostart, no AgentDB bridge and the bookkeeping store outside the CWD. +# Static checks over the shipped sources, plus a live check of the wrapper's +# routing when `ruflo` on PATH is the governed one (inside the image). +set -euo pipefail +root="$(cd "$(dirname "$0")/../.." && pwd)" +fail=0 +note() { echo "FAIL: $*"; fail=1; } + +# 1. The wrapper derivation exists and is what the gated package list ships. +grep -q 'rufloGovernedPkg = pkgs.runCommand' "$root/flake.nix" || note 'flake.nix lost rufloGovernedPkg' +grep -q 'rufloConsoleOn) \[ rufloGovernedPkg \]' "$root/flake.nix" || note 'the ruflo gate must ship rufloGovernedPkg, not rufloPkg' +grep -q 'exec \${pkgs.nodejs_22}/bin/node /opt/agentbox/mcp/servers/ruflo-memory-cli.cjs' "$root/flake.nix" || note 'wrapper does not exec the governed memory CLI' +for v in RUFLO_DAEMON_AUTOSTART CLAUDE_FLOW_DISABLE_BRIDGE CLAUDE_FLOW_MEMORY_PATH; do + grep -q "export $v=" "$root/flake.nix" || note "wrapper does not default $v" + # The exports live in the runtime-env heredoc: a plain ${X:-…} resolves at boot, + # an escaped \${X:-…} resolves in the sourcing shell (per-user $HOME). Either + # form keeps the operator override; RC-X1-01 forbids a root-side literal + # /home/devuser path, which is why CLAUDE_FLOW_MEMORY_PATH is the escaped one. + grep -qE "^export $v=\"\\\\?\\\$\{$v:-" "$root/config/entrypoint-unified.sh" || note "entrypoint does not export $v with an operator-overridable default" +done + +# 2. The CLI ships, is CommonJS, reuses the governed library and refuses the local-store verbs. +cli="$root/mcp/servers/ruflo-memory-cli.cjs" +[[ -f "$cli" ]] || note 'mcp/servers/ruflo-memory-cli.cjs missing' +grep -q "require(path.join(__dirname, 'lib', 'memory-tools.js'))" "$cli" || note 'CLI must reuse lib/memory-tools.js (single memory implementation)' +for verb in init configure backup export import purge distill migrate; do + grep -qE "'$verb'" "$cli" || note "CLI no longer refuses '$verb'" +done +node -e "const c=require('$cli'); if (c.WRITE_SOURCE_TYPE!=='agentbox') process.exit(1)" || note 'CLI write source must match ruvector-mcp.cjs (agentbox) so entry ids coincide' + +# 3. Nothing the image ships runs a local `memory init` (it would be refused anyway; keep the sources honest). +set +e +hits="$(grep -rnE '(ruflo|claude-flow)[[:space:]]+memory[[:space:]]+init' "$root/config" "$root/scripts" "$root/aisp" 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#')" +set -e +[[ -z "$hits" ]] || note "shipped source runs ruflo memory init: $hits" + +# 4. Live, only inside the image: the governed wrapper routes `memory help` to the CLI +# and `--help` on the real CLI spawns no daemon. +if command -v ruflo >/dev/null 2>&1 && grep -q 'ADR-2123' "$(command -v ruflo)" 2>/dev/null; then + ruflo memory help 2>/dev/null | grep -q 'ADR-2123' || note 'live: `ruflo memory help` is not the governed CLI' + tmp="$(mktemp -d)"; ( cd "$tmp" && ruflo --help >/dev/null 2>&1 || true ) + [[ -e "$tmp/.claude-flow/daemon.pid" ]] && note 'live: `ruflo --help` still autostarted a daemon' + [[ -e "$tmp/ruvector.db" || -d "$tmp/.swarm" ]] && note 'live: `ruflo --help` still wrote a local store into the CWD' + rm -rf "$tmp" +fi + +[[ $fail -eq 0 ]] || exit 1 +echo 'PASS: ruflo memory is governed (sidecar via the wrapper); the real CLI runs repo-safe' diff --git a/tests/contract/ruflo-memory-cli.contract.spec.js b/tests/contract/ruflo-memory-cli.contract.spec.js new file mode 100644 index 000000000..b3d04fdf5 --- /dev/null +++ b/tests/contract/ruflo-memory-cli.contract.spec.js @@ -0,0 +1,197 @@ +'use strict'; + +/** + * Contract test suite — ADR-2123 governed `ruflo memory …`. + * + * ruflo 3.51.1's memory subsystem is local-only (sql.js under .swarm/, an + * AgentDB mirror, ./ruvector.db, a MiniLM embedder). In this image the baked + * `ruflo`/`claude-flow` wrappers exec mcp/servers/ruflo-memory-cli.cjs for the + * `memory` subcommand so the CLI — and the ruflo-console memory pane, which + * shells out to it — serves the ruvector-postgres sidecar through the governed + * memory library (lib/memory-tools.js, Xinference embeddings). + * + * Covers: + * 1. Output shapes with --format json are the ones ruflo 3.51.1 emits and the + * console parses (plugins/ruflo-console/hooks/data/cli.ts memoryProbe / + * namespacesProbe): stats.{backend,entries.total,entries.vectors, + * storage.total,oldestEntry,newestEntry}; list → array with + * {id,key,namespace,size,accessCount,createdAt,updatedAt,hasEmbedding, + * provenanceType}. + * 2. Every local-store subcommand is refused with exit 2 and touches nothing. + * 3. Fail-closed: no pg → exit 1, JSON error, no fallback store. + * 4. The write path is the governed memStore: a protected namespace is + * refused without RUVECTOR_ADMIN_WRITE, exactly as the MCP server does. + * 5. ruflo's flag spellings parse (-k/-v/-n, --key=value, --format json). + * + * No DB contact: the pg pool and embedder are stubs. + */ + +const path = require('path'); + +delete process.env.RUVECTOR_ADMIN_WRITE; +delete process.env.RUVECTOR_PROTECTED_NAMESPACES; + +const cli = require('../../mcp/servers/ruflo-memory-cli.cjs'); + +// Shapes captured from the real ruflo 3.51.1 CLI on 2026-10-03 +// (`ruflo memory stats --format json` / `ruflo memory list --format json`). +const RUFLO_STATS_KEYS = ['backend', 'entries', 'storage', 'version', 'oldestEntry', 'newestEntry']; +const RUFLO_LIST_KEYS = ['id', 'key', 'namespace', 'size', 'accessCount', 'createdAt', 'updatedAt', 'hasEmbedding', 'provenanceType']; + +function fakePool(handlers) { + const calls = []; + return { + calls, + async query(sql, params) { + calls.push({ sql, params }); + for (const [re, fn] of handlers) if (re.test(sql)) return fn(sql, params); + throw new Error(`unexpected SQL: ${sql.slice(0, 80)}`); + }, + async end() {}, + }; +} + +const vec = Array.from({ length: cli.EMBEDDING_DIM }, (_, i) => i / cli.EMBEDDING_DIM); +const fakeEmbed = async () => vec; + +describe('ADR-2123 governed ruflo memory CLI', () => { + test('stats --format json emits the ruflo 3.51.1 shape the console parses', async () => { + const pool = fakePool([ + [/pg_total_relation_size/, () => ({ rows: [{ total: '213332', vectors: '213330', oldest: new Date('2025-10-14T13:20:30Z'), newest: new Date('2026-10-03T18:29:12Z'), namespaces: 464, bytes: '2931515392' }] })], + [/pg_extension/, () => ({ rows: [{ extversion: '0.3.0' }] })], + ]); + const r = await cli.run(['stats', '--format', 'json'], { deps: { pool, getEmbedding: fakeEmbed, xinfEnsure: async () => true, log: () => {} } }); + expect(r.code).toBe(cli.EXIT_OK); + const out = JSON.parse(r.stdout); + for (const k of RUFLO_STATS_KEYS) expect(out).toHaveProperty(k); + expect(out.backend).toMatch(/^ruvector-postgres/); + expect(out.backend.length).toBeLessThanOrEqual(60); // console's stringOf(…, 60) + expect(out.entries).toEqual({ total: 213332, vectors: 213330, text: 2 }); + expect(out.storage.total).toBe('2931515392 B'); + expect(out.oldestEntry).toBe('2025-10-14T13:20:30.000Z'); + expect(out.newestEntry).toBe('2026-10-03T18:29:12.000Z'); + expect(out.version).toBe('ruvector-postgres 0.3.0'); + expect(out.embedding).toEqual(expect.objectContaining({ model: 'bge-small-en-v1.5', dimensions: 384 })); + }); + + test('list --format json emits ruflo-shaped rows, newest first, across namespaces by default', async () => { + const pool = fakePool([ + [/SELECT id, key, namespace/, (sql, params) => { + expect(params[0]).toBe('*'); + expect(params[1]).toBe(500); + expect(params[2]).toEqual(['governance-precedents']); // wildcard excludes protected namespaces + return { rows: [ + { id: 'agentbox:project-state:a', key: 'a', namespace: 'project-state', size: '12', access_count: 3, created_at: new Date(1791052152041), updated_at: new Date(1791052152041), has_embedding: true, source_type: 'agentbox' }, + { id: 'agentbox:personal-context:b', key: 'b', namespace: 'personal-context', size: '7', access_count: 0, created_at: new Date(1791051586246), updated_at: new Date(1791051586246), has_embedding: false, source_type: 'agentbox' }, + ] }; + }], + ]); + const r = await cli.run(['list', '--format', 'json', '--limit', '500'], { deps: { pool, getEmbedding: fakeEmbed, xinfEnsure: async () => true, log: () => {} } }); + expect(r.code).toBe(cli.EXIT_OK); + const out = JSON.parse(r.stdout); + expect(Array.isArray(out)).toBe(true); + expect(out).toHaveLength(2); + for (const k of RUFLO_LIST_KEYS) expect(out[0]).toHaveProperty(k); + expect(out[0]).toMatchObject({ key: 'a', namespace: 'project-state', size: 12, accessCount: 3, createdAt: 1791052152041, hasEmbedding: true, provenanceType: 'unknown' }); + expect(out[1].hasEmbedding).toBe(false); + }); + + test('list -n scopes the query and does not exclude anything', async () => { + const pool = fakePool([[/SELECT id, key, namespace/, (sql, params) => { + expect(params[0]).toBe('cli-probe'); + expect(params[2]).toEqual([]); + return { rows: [] }; + }]]); + const r = await cli.run(['ls', '-n', 'cli-probe', '--format', 'json'], { deps: { pool, getEmbedding: fakeEmbed, xinfEnsure: async () => true, log: () => {} } }); + expect(r.code).toBe(cli.EXIT_OK); + expect(JSON.parse(r.stdout)).toEqual([]); + }); + + test.each([...cli.REFUSED])('refuses `memory %s` with exit 2 and no backend contact', async (sub) => { + const pool = fakePool([]); + const r = await cli.run([sub, '--format', 'json'], { deps: { pool, getEmbedding: fakeEmbed, log: () => {} } }); + expect(r.code).toBe(cli.EXIT_REFUSED); + expect(JSON.parse(r.stdout)).toMatchObject({ success: false, refused: sub }); + expect(pool.calls).toHaveLength(0); + const human = await cli.run([sub], { deps: { pool, getEmbedding: fakeEmbed, log: () => {} } }); + expect(human.code).toBe(cli.EXIT_REFUSED); + expect(human.stderr).toMatch(/ADR-2014/); + }); + + test('fails closed without pg: exit 1, JSON error, nothing written', async () => { + const r = await cli.run(['stats', '--format', 'json'], { deps: { pool: null, getEmbedding: fakeEmbed, log: () => {} } }); + expect(r.code).toBe(cli.EXIT_FAIL); + expect(JSON.parse(r.stdout)).toMatchObject({ success: false, backend: 'ruvector-postgres' }); + }); + + test('store goes through the governed memStore: protected namespace refused without admin write', async () => { + const pool = fakePool([[/INSERT INTO memory_entries/, () => ({ rowCount: 1, rows: [] })]]); + const deps = { pool, getEmbedding: fakeEmbed, xinfEnsure: async () => true, log: () => {} }; + const refused = await cli.run(['store', '-k', 'x', '-v', 'y', '-n', 'governance-precedents', '--format', 'json'], { deps }); + expect(refused.code).toBe(cli.EXIT_FAIL); + expect(JSON.parse(refused.stdout).success).toBe(false); + expect(pool.calls).toHaveLength(0); + + const ok = await cli.run(['store', '--key=k1', '--value=hello world', '--namespace=cli-probe', '--tags', 'a,b', '--importance', '0.4', '--format', 'json'], { deps }); + expect(ok.code).toBe(cli.EXIT_OK); + const out = JSON.parse(ok.stdout); + expect(out).toMatchObject({ success: true, key: 'k1', namespace: 'cli-probe', embedded: true }); + const insert = pool.calls.find(c => /INSERT INTO memory_entries/.test(c.sql)); + expect(insert.params[0]).toBe(`${cli.WRITE_SOURCE_TYPE}:cli-probe:k1`); // same entry id the MCP server uses + expect(insert.sql).toMatch(/ruvector\(384\)/); + }); + + test('store without an embedding is rejected (ADR-2014 fail-closed), never stored unsearchable', async () => { + const pool = fakePool([[/INSERT INTO memory_entries/, () => ({ rowCount: 1, rows: [] })]]); + const deps = { pool, getEmbedding: async () => { throw new Error('xinference down'); }, xinfEnsure: async () => true, log: () => {} }; + const r = await cli.run(['store', '-k', 'k', '-v', 'v', '-n', 'cli-probe', '--format', 'json'], { deps }); + expect(r.code).toBe(cli.EXIT_FAIL); + expect(JSON.parse(r.stdout)).toMatchObject({ success: false, reason: 'embedding-unavailable' }); + expect(pool.calls.filter(c => /INSERT/.test(c.sql))).toHaveLength(0); + }); + + test('search uses the governed memSearch, shapes results and reports the backend', async () => { + const pool = fakePool([ + [/<=>|embedding/i, () => ({ rows: [ + { key: 'a', namespace: 'project-state', value: '"alpha text"', score: 0.81, source_type: 'agentbox' }, + { key: 'b', namespace: 'project-state', value: '"beta text"', score: 0.2, source_type: 'agentbox' }, + ] })], + ]); + const r = await cli.run(['search', '-q', 'alpha', '-n', 'project-state', '-l', '5', '--threshold', '0.5', '--format', 'json'], { deps: { pool, getEmbedding: fakeEmbed, xinfEnsure: async () => true, log: () => {} } }); + expect(r.code).toBe(cli.EXIT_OK); + const out = JSON.parse(r.stdout); + expect(out).toMatchObject({ query: 'alpha', namespace: 'project-state', total: 1 }); + expect(out.backend).toMatch(/^ruvector-postgres \(hnsw-xinference\)/); + expect(out.results[0].key).toBe('a'); + expect(out).toHaveProperty('searchTime'); + }); + + test('usage errors exit 64; help exits 0', async () => { + const deps = { pool: fakePool([]), getEmbedding: fakeEmbed, log: () => {} }; + expect((await cli.run(['bogus'], { deps })).code).toBe(cli.EXIT_USAGE); + expect((await cli.run(['search'], { deps })).code).toBe(cli.EXIT_USAGE); + expect((await cli.run(['store', '-k', 'only-key'], { deps })).code).toBe(cli.EXIT_USAGE); + const help = await cli.run(['help'], { deps }); + expect(help.code).toBe(cli.EXIT_OK); + expect(help.stdout).toMatch(/ADR-2123/); + }); + + test('parseArgs accepts ruflo spellings', () => { + const { flags, positional } = cli.parseArgs(['store', '-k', 'key1', '--value=v=1', '-n', 'ns', '--format', 'json', '--verbose']); + expect(positional).toEqual(['store']); + expect(flags).toEqual({ key: 'key1', value: 'v=1', namespace: 'ns', format: 'json', verbose: true }); + }); + + test('the shipped wrapper routes `memory` to this CLI and sets the repo-safe defaults', () => { + const fs = require('fs'); + const flake = fs.readFileSync(path.join(__dirname, '..', '..', 'flake.nix'), 'utf8'); + const block = flake.slice(flake.indexOf('rufloGovernedPkg = pkgs.runCommand')); + expect(block).toMatch(/if \[ "\\\$1" = "memory" \]/); + expect(block).toMatch(/ruflo-memory-cli\.cjs/); + for (const v of ['RUFLO_DAEMON_AUTOSTART', 'CLAUDE_FLOW_DISABLE_BRIDGE', 'CLAUDE_FLOW_MEMORY_PATH']) expect(block).toContain(`export ${v}=`); + expect(flake).toMatch(/rufloConsoleOn\) \[ rufloGovernedPkg \]/); + const entry = fs.readFileSync(path.join(__dirname, '..', '..', 'config', 'entrypoint-unified.sh'), 'utf8'); + expect(entry).toMatch(/export RUFLO_DAEMON_AUTOSTART="\$\{RUFLO_DAEMON_AUTOSTART:-0\}"/); + expect(entry).toMatch(/export CLAUDE_FLOW_DISABLE_BRIDGE="\$\{CLAUDE_FLOW_DISABLE_BRIDGE:-1\}"/); + }); +});