Skip to content

CanXPAI/maplenode

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

maplenode

A sovereign edge cognition appliance for MapleOS. Local-first semantic memory, document indexing, and an MCP-compatible tool server, running on a Radxa ROCK 4C+ under Armbian.

status arch license

RockPi 4C+ Rockchip is officially supported. Dual-Core Arm Cortex-A72 and Quad-Core Cortex-A53 are officially supported.

What this is

maplenode is a tiny, always-on appliance built around three ideas:

  1. Local-first memory. Vector-searchable semantic store, on-device, with no cloud round-trip on the hot path.
  2. Pluggable embeddings. sentence-transformers/all-MiniLM-L6-v2 (384-d) by default, with a deterministic hash fallback so the API stays up even if heavy ML deps fail to install.
  3. Discoverable. mDNS publishes maplenode.local, and the node broadcasts its capabilities on UDP :8766 every 30s so MapleOS can find it without manual config.

It is not a GPU server. It is an edge memory + orchestration node.

At a glance

Target board Radxa ROCK 4C+ (RK3399, 4GB)
OS Armbian Bookworm (Debian 12), kernel 6.7.x
Stack FastAPI · SQLite · LanceDB · sentence-transformers (CPU)
HTTP :8765
UDP discovery :8766
mDNS maplenode.local, service _maplenode._tcp
Process systemd unit, runs as user maplenode
Auth one setup token, sent via X-MapleSeed-Token

Quick start

On a freshly-flashed ROCK 4C+ with Armbian (headless / multi-user.target):

sudo hostnamectl set-hostname maplenode
git clone https://github.com/CanXPAI/maplenode.git ~/maplenode-src
cd ~/maplenode-src
bash scripts/install.sh

Then, from any machine on the same LAN:

curl http://maplenode.local:8765/health
# {"status":"ok","device":"maplenode","hostname":"maplenode","version":"0.1.0"}

The installer prints the setup token at the end. Grab it:

sudo cat /opt/maplenode/app/data/.setup_token

API tour

TOKEN=<paste the setup token>
BASE=http://maplenode.local:8765

# Open endpoints — no token required
curl -s $BASE/health
curl -s $BASE/capabilities
curl -s $BASE/mcp/tools

# Write endpoints — require X-MapleSeed-Token
curl -s -X POST $BASE/memory/store \
  -H "X-MapleSeed-Token: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"text":"User prefers local-first agents.","metadata":{"source":"test"}}'

curl -s -X POST $BASE/memory/search \
  -H "X-MapleSeed-Token: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"query":"agent preferences","top_k":5}'

# Upload a document and index it
curl -s -X POST $BASE/documents/upload \
  -H "X-MapleSeed-Token: $TOKEN" \
  -F file=@notes.txt -F namespace=default
# returns {"id":"<doc-id>", ...}

curl -s -X POST $BASE/documents/index \
  -H "X-MapleSeed-Token: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"document_id":"<doc-id>"}'

# MCP-style tools
curl -s $BASE/mcp/device_status -H "X-MapleSeed-Token: $TOKEN"
curl -s -X POST $BASE/mcp/run_command_safe \
  -H "X-MapleSeed-Token: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"command":"uptime"}'

Allowlisted diagnostic commands are: uptime, df, free, cpuinfo, service_status. There is no unrestricted shell endpoint by design.

TypeScript client

A typed client lives in client/MapleSeedClient.ts:

import { MapleSeedClient } from "./MapleSeedClient";

const node = new MapleSeedClient("http://maplenode.local:8765", { token: TOKEN });

await node.storeMemory("User prefers local-first agents.", { source: "test" });
const hits = await node.searchMemory("agent preferences", 5);
const tools = await node.listTools();

Drop it into MapleOS as the integration surface.

Discovery

maplenode is reachable in two complementary ways:

  1. mDNS / Bonjour. maplenode.local resolves to its LAN IP. An avahi service file at /etc/avahi/services/maplenode.service publishes _maplenode._tcp with TXT records: version, capabilities, api.
  2. UDP capability broadcast. Every 30s the node sends a JSON packet on UDP :8766:
    {
      "type": "maplenode.advertise",
      "name": "maplenode",
      "hostname": "maplenode.local",
      "api": "http://maplenode.local:8765",
      "version": "0.1.0",
      "capabilities": ["memory", "documents", "mcp"]
    }
    MapleOS can listen on :8766 to auto-detect nodes joining the LAN.

Pairing flow (MapleOS side)

  1. Discover the node via mDNS or the UDP broadcast.
  2. User opens an SSH/console on the device and runs:
    sudo cat /opt/maplenode/app/data/.setup_token
  3. User pastes the token into MapleOS.
  4. MapleOS stores the token and uses it in X-MapleSeed-Token for all write calls.

The token is generated on first boot (32 bytes of secrets.token_urlsafe) and persists to a 0600 file under /opt/maplenode/app/data/.

Updating and backups

# Update from a new checkout
bash /opt/maplenode/scripts/update.sh ~/maplenode-src

# Backup SQLite + LanceDB + uploaded docs + setup token
bash /opt/maplenode/scripts/backup.sh /home/vince/maplenode-backups

The backup is a single timestamped .tar.gz. Restore by extracting it back over /opt/maplenode/app/data/.

Layout

maplenode/
├─ app/
│  ├─ main.py                 FastAPI app + token middleware + lifespan
│  ├─ config.py               paths, ports, capability flags, setup-token mgmt
│  ├─ api/                    HTTP routers: health, memory, documents, mcp, sync
│  └─ core/
│     ├─ database.py          SQLite schema + connection helper
│     ├─ embeddings.py        pluggable backend (local-minilm | hash)
│     ├─ vector_store.py      LanceDB wrapper
│     ├─ document_indexer.py  chunker + extractor
│     ├─ discovery.py         UDP capability broadcaster
│     └─ agent_runtime.py     stub for tiny local inference (Phase 9)
├─ scripts/
│  ├─ install.sh              one-shot installer (apt + venv + systemd + ufw + avahi)
│  ├─ update.sh               rsync + pip refresh + restart
│  └─ backup.sh               consistent tar.gz of data dir
├─ systemd/
│  ├─ maplenode.service       systemd unit
│  └─ maplenode-avahi.service avahi service definition
├─ client/
│  └─ MapleSeedClient.ts      TypeScript client for MapleOS
└─ README.md

Runtime data (not in repo, created by the installer):

/opt/maplenode/app/data/
├─ maplenode.db          SQLite (memories, documents, chunks)
├─ vectors/              LanceDB
├─ documents/            uploaded files
├─ models/               sentence-transformers cache
├─ logs/
└─ .setup_token          chmod 0600

Configuration

All knobs are environment variables, read by app/config.py:

Variable Default Meaning
MAPLENODE_DEVICE_NAME maplenode name returned in /health and the broadcast
MAPLENODE_PORT 8765 HTTP port
MAPLENODE_UDP_PORT 8766 capability broadcast port
MAPLENODE_BROADCAST_INTERVAL 30 seconds between broadcasts
MAPLENODE_EMBED_BACKEND local-minilm local-minilm or hash
MAPLENODE_LOCAL_INFERENCE false toggles a local_inference capability flag
MAPLENODE_CLOUD_SYNC false toggles a cloud_sync capability flag
MAPLENODE_LOG_LEVEL INFO python logging level

To set them under systemd, drop a file at /etc/systemd/system/maplenode.service.d/override.conf:

[Service]
Environment=MAPLENODE_EMBED_BACKEND=hash

Then sudo systemctl daemon-reload && sudo systemctl restart maplenode.

Security posture

  • LAN-only by design. UFW opens 22/tcp, 8765/tcp, 5353/udp (mDNS), 8766/udp (broadcast). Do not port-forward 8765 to the public internet.
  • Write-gated. All non-GET endpoints (and POST /mcp/run_command_safe) require the setup token. GET /health, /capabilities, and /mcp/tools are open so a discovering MapleOS can probe before pairing.
  • No arbitrary shell. Diagnostic commands are a hard-coded allowlist.
  • Process isolation. The systemd unit runs as the maplenode system user with NoNewPrivileges, ProtectSystem=full, ProtectHome=true, and ReadWritePaths=/opt/maplenode/app/data.

Notes for installers

  • The default PyPI torch wheel on aarch64 pulls ~2GB of CUDA libraries that are useless on RK3399. scripts/install.sh installs torch from the CPU-only index (https://download.pytorch.org/whl/cpu) and points pip's TMPDIR at the SD card to avoid filling the 1.9GB /tmp tmpfs.
  • First call to /memory/store or /memory/search will load the 90MB MiniLM model into memory. The installer warms the cache at install time so the first user request isn't slow.

Troubleshooting

Symptom Try
maplenode.local doesn't resolve sudo systemctl status avahi-daemon; make sure the client host has libnss-mdns or systemd-resolved with mDNS enabled; check both are on the same subnet
Service won't start journalctl -u maplenode -n 80 --no-pager
pip install ran out of disk The fix is already in the installer (CPU torch index + TMPDIR=/opt/maplenode/.tmp). If you skipped it, replicate those flags manually.
401 on write endpoints Header must be X-MapleSeed-Token; token at /opt/maplenode/app/data/.setup_token
/memory/search returns nothing First write a memory; first call may pause while the embedding model downloads
Want to swap to hash fallback MAPLENODE_EMBED_BACKEND=hash via the systemd override above

Roadmap

  • Phase 9 — tiny local inference (llama.cpp + small GGUF) for intent classification and summarization
  • Phase 10 — CanXP AI cloud sync
  • USB-gadget pairing mode for direct one-device pairing without LAN
  • Per-namespace ACLs and multi-token auth
  • PDF / HTML / Markdown extractors for documents.index

License

MIT — see LICENSE.

About

Sovereign edge cognition appliance for MapleOS.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors