Quickstart — two nodes federating
What you'll do
Run two Stigmem nodes on one machine and watch a fact asserted on Node A replicate automatically to Node B. End-to-end smoke test including peer handshake, replication, and audit log inspection.
The fastest path is make demo.
It starts two local nodes, registers them as peers, asserts a fact on
node A, waits for pull replication, checks node B, prints recent
audit entries, and tears the stack down. Everything below explains
what make demo does and how to drive it manually.
Prerequisites
curl + jqhelmNo Python installation required — the nodes run in containers.
Default deployments enforce direct garden_id reads and writes, but
tenant-wide fact queries, recall ranking, push subscriptions, OIDC permission
ceilings, and graph traversal do not filter by garden membership unless
stigmem-plugin-memory-garden-acl is installed, registered, and enabled with
the relevant STIGMEM_MEMORY_GARDEN_ACL_* flags. Check /v1/doctor for the
current memory_garden_acl_filtering state before relying on advanced garden
filtering. The endpoint is unauthenticated in v0.9.0a10 and exposes only this
coarse ops posture, not garden names, memberships, tenants, or policy subjects.
Step 1 — Clone and start the stack
git clone https://github.com/eidetic-labs/stigmem
cd stigmem
make demo
make demo runs scripts/quickstart-verify.sh. By default it uses
the pinned GHCR image from docker-compose.yml for speed.
Contributors who need to prove the local working tree can set
DEMO_BUILD=1 make demo to force a local image build first.
The quickstart starts two services:
node-anode-bLocal-dev defaults.
The compose file sets STIGMEM_FEDERATION_INSECURE=1 plus
STIGMEM_LOCAL_DEV_ALLOW_INSECURE_NON_LOOPBACK=1 because
containers address each other with Docker service DNS names instead
of localhost. Do not carry those flags into production —
use mTLS for any deployed federation.
If ports 8765 / 8766 are busy, the demo script automatically selects free host ports starting at 18765.
Troubleshooting make demo
Ports 8765/8766 are already in use
The containers still listen on port 8765 internally, but the host
ports can move. When 8765 or 8766 are busy, make demo automatically
selects available host ports starting at 18765 and prints the Node A
and Node B URLs it used.
Use those printed URLs for manual curl commands, or set explicit
ports before running the demo:
STIGMEM_NODE_A_HOST_PORT=18765 STIGMEM_NODE_B_HOST_PORT=18766 make demo
If a browser or curl command still points at localhost:8765 or
localhost:8766, it may be talking to another local process instead
of the demo stack.
Docker image pull failures
By default, the demo pulls the pinned GHCR image from
docker-compose.yml. Transient network, registry, or local Docker
cache failures are usually safe to retry. Contributors validating
local changes — or anyone blocked by a published image pull — can
build the local image instead:
DEMO_BUILD=1 make demo
Failure before teardown
make demo prints each step before it runs it. When a failure happens
before teardown, the first failing step and the Docker error output
are the most useful diagnostics.
To keep containers available for inspection, rerun with:
KEEP_UP=1 make demo
Then inspect the stack with:
docker compose ps
docker compose logs node-a node-b
When you are done, clean up with
docker compose down -v --remove-orphans.
Step 2 — Verify federation
The demo prints each step as it runs:
Step 0 — docker compose up -d
Step 2 — Inspecting /.well-known/stigmem
Step 3 — Peer handshake (both directions)
Step 4 — Asserting fact on node-a
Step 5 — Waiting 35s for federation pull
Step 6 — Federation audit log on node-b
quickstart smoke test PASSED confirms three things:
Peer declaration accepted
The receiving node verified the signed peer declaration.
Pull replication moved the fact
Node B observed the fact written on Node A within the pull interval.
Audit endpoint readable
The federation audit log returned the expected pull and peer events.
To keep the demo stack running for manual inspection:
KEEP_UP=1 make demo
Step 3 — Manual fact assertion
With KEEP_UP=1, assert another fact on node A:
curl -s -X POST "${NODE_A:-http://localhost:8765}/v1/facts" \
-H 'Content-Type: application/json' \
-d '{
"entity": "user:alice",
"relation": "memory:prefers",
"value": {"type": "string", "v": "dark mode"},
"source": "agent:settings",
"confidence": 1.0,
"scope": "company"
}' | jq '{id, entity, relation, value, scope}'
Record the id from the response — you'll use it to confirm
replication.
Step 4 — Verify cross-node replication
Node B pulls facts from Node A on the background pull interval (default 30 s):
sleep 35
curl -s "${NODE_B:-http://localhost:8766}/v1/facts?entity=user:alice&scope=company" \
-H 'Content-Type: application/json' | jq '.facts[] | {id, entity, value, scope, source_node}'
The fact asserted on Node A (localhost:8765) should appear on Node B
(localhost:8766) with a source_node field pointing to Node A's
node_id.
The default pull interval is 30 s
(STIGMEM_FEDERATION_PULL_INTERVAL_S). Lower it in
docker-compose.yml for testing.
Step 5 — Federation audit log
Every pull event and peer action is recorded:
curl -s "${NODE_B:-http://localhost:8766}/v1/federation/audit" | jq '.entries[-3:]'
Makefile targets
make demoDEMO_BUILD=1 make demoKEEP_UP=1 make demomake demo-attackDEMO_ATTACK_TRANSCRIPT=/tmp/demo-attack.json make demo-attackFor ad hoc compose work, use docker compose up -d and
docker compose down -v.
Malicious-Peer Rejection Demo
make demo-attack runs the focused malicious-peer acceptance gate
without leaving a demo cluster running. It validates two rejection
paths:
Unauthorized scope write
A peer attempts to write into a scope outside its allowed scopes.
Source-forged public write
A peer claims another source identity when writing a public-scope fact.
Expected result:
Exit success
The command exits successfully.
Focused gate passed
Output shows the focused pytest gate passed.
Rejection recorded
The rejected attempts are recorded as rejection / audit outcomes — not accepted replicated facts.
To save a machine-readable summary:
DEMO_ATTACK_TRANSCRIPT=/tmp/demo-attack.json make demo-attack
jq . /tmp/demo-attack.json
The transcript records the gate command, exit code, elapsed time, and expected scenario outcomes. It does not contain secrets or private keys.
The Helm chart has been moved to
experimental/deploy-helm/
per
ADR-002.
It remains buildable but is unsupported until the
ADR-008 reintroduction gates
pass. The supported v0.9.0a10 deployment surface is Docker Compose
(above).
Generate Ed25519 keypairs with python3 infra/soak/keys.py. Keys must
be base64url-encoded without padding (=).
Peer registration internals
The demo bootstraps local-only admin-federation keys inside each fresh container, registers each direction by running the CLI, then approves each pending peer with its public-key fingerprint:
stigmem federation register-peer \
--local-url http://node-a:8765 \
--remote-url http://node-b:8765 \
--scopes company,public \
--api-key "$NODE_B_ADMIN_KEY"
The receiving node fetches the sender's /.well-known/stigmem,
verifies the federation public key, and stores a pending peer record.
The script approves the peer after matching the SHA-256 fingerprint of
the advertised public key. HTTP 409 means a prior run already
registered that peer; the demo clears volumes before starting so the
handshake is deterministic.
Environment overrides
STIGMEM_NODE_A_HOST_PORTSTIGMEM_NODE_B_HOST_PORTPULL_WAIT_S35KEEP_UP01 to leave containers running after the demo.DEMO_BUILD01 to force docker compose up --build -d.Idempotency
Safe to re-run.
The script starts by running
docker compose down -v --remove-orphans so stale peer
registrations, test facts, and volumes do not affect the next
handshake.
Teardown
docker compose down -v # stop and delete all data volumes