Skip to main content

Synnoesis

syn (together) + noēsis (thinking) — "minds thinking together."

A signed message mesh for AI agents. Give each agent an Ed25519 identity, and they can send each other messages — on one machine over the filesystem, or across machines over an MQTT broker — with every message verified locally by the receiver against its own keyring.

The broker moves bytes. It never vouches for identity.

Built and published by Groupe Kioptix Inc.


What works today

send / read Signed messages between agents on one machine. Zero dependencies — the file floor is standard library only.
listen Cross-machine delivery over an MQTT broker, re-verified against the receiving machine's keyring, with durable delivery across listener restarts.
agent A config-driven runner that attaches any OpenAI-compatible model to the mesh: verify → model turn → signed reply. No tools — text in, text out.
who Presence: retained, record-signed online / offline / last-will state, listed with age and signature status.
keygen / keyring / fingerprint Ed25519 identity: generate, exchange, pin, verify.
doctor Diagnostics — config, broker reachability, transport-security posture.
memory.save / memory.search (Python API, not a CLI verb) Persistent semantic memory: a single-file SQLite store with save / search over local embeddings. The embedder is optional — with none reachable it still saves, falls back to substring matching, and says so on every response. Pass trigger="when would someone need this?" and the entry becomes honestly testable later; omit it and the response tells you so, permanently.

Loop containment composes a per-process reply-rate budget with a signed hop-counter that bounds reply-chain depth across the mesh (SYN_MAX_HOPS).

Not yet: no authorization layer (a signature proves who sent a message, not that they were allowed to ask), no end-to-end encryption (signing authenticates, it does not conceal — the broker sees message bodies), and no process supervisor (running listen / agent under systemd / launchd is the operator's choice). See Roadmap.

The message contract — send by agent-id → inbox record; read the inbox tail — is the stable part. Transports underneath it may still change while the version is 0.x.

Install

pip install synnoesis                # file floor, no dependencies
pip install "synnoesis[signing]"     # + Ed25519 signing
pip install "synnoesis[mqtt]"        # + cross-machine transport (includes signing)

Python 3.10+. The bare install stays dependency-free on purpose: a single-machine mesh should never fail to build.

Upgrading a memory store from 0.7.0? Open it writable once — any save(), or memory.store.connect(path) — and the new trigger column is added in place. Existing rows are untouched and keep trigger = NULL.

A read-only open of an un-migrated store deliberately refuses, with a StoreNeedsMigration error rather than a raw SQLite one: a read must not quietly mutate your store, so it tells you what to do instead of doing it behind your back. If your first call after upgrading is search(readonly=True) or stats(), that is the error you will see, and one writable open clears it permanently.

⚠️ Check your Python first — a too-old one fails confusingly, not loudly. On Python < 3.10, pip install synnoesis may not error at all: it can quietly resolve to the ancient 0.0.1 name-reservation placeholder (the only release permitting 3.9), which installs "successfully" and does nothing. macOS in particular still ships python3 as 3.9.x on older systems, so build the venv with an explicit python3.12 -m venv .venv and confirm with python -V once it's activated.

60 seconds: two agents, one machine

Open two shells. Give each an identity:

export PA_AGENT_ID=alice        # shell 1   ($env:PA_AGENT_ID = "alice" on PowerShell)
export PA_AGENT_ID=bob          # shell 2

Alice sends:

synnoesis send --local --to bob "hello from alice"

Bob reads:

synnoesis read --agent-id bob

That's the whole contract. No broker, no daemon, no config file.

The message arrives marked [!UNSIGNED] — you haven't generated any keys yet, and Synnoesis says so rather than pretending. Run synnoesis keygen on each side and exchange public keys (synnoesis keyring --add) to get verify=ok; the quickstart walks through it.

Across machines

Point both machines at a broker and run the receiver:

export SYN_BROKER=broker.example.net:8883
export SYN_BROKER_TLS_CA=/path/to/ca.pem
synnoesis listen

send now publishes to the broker instead of the local floor, and announces which transport it used — an auto-selected network egress is never silent. --local forces the file path; --via {auto,file,mqtt} selects explicitly, and mqtt errors rather than quietly falling back.

Full walkthrough, including key exchange: docs/quickstart.md.

Security model

The design assumption is that the broker is hostile — it is infrastructure you may not control, sitting between agents that trust each other.

Property How
Authenticity Ed25519 signatures over _from, _to, _at, _urgency, body, _nonce.
Local verification The receiver verifies against its own keyring. A sender's self-reported verdict and the broker's word are both ignored.
Enforcement SYN_ENFORCE_SIGNING=1 drops anything that isn't ok — and fails loudly at startup if cryptography is missing, rather than silently rejecting all traffic.
Replay defense Bounded seen-nonce cache plus a signed-timestamp freshness window (SYN_MAX_AGE_SEC, default 300s). An attacker cannot forge a fresh _at — it is inside the signature.
Fail-closed transport A non-loopback broker without TLS is refused unless SYN_ALLOW_PLAINTEXT=1 is set explicitly — and even then it warns on every connect. There is deliberately no skip-verify option.
No secrets in argv The broker password is read from a file (SYN_BROKER_PASSFILE), never a flag.
Keys stay home Private keys are generated on the machine that uses them and are never transmitted. Only public keys are exchanged.
Loop containment A per-process reply budget bounds emission RATE; a signed _hops counter bounds chain DEPTH across the mesh (SYN_MAX_HOPS, default 25). A message past the ceiling is dropped, at both the ingest and emit boundaries.

What it deliberately does not do

  • Signing is not encryption. Messages are authenticated, not confidential. A broker operator can read every body. Use TLS for transport confidentiality, and don't put secrets in message bodies.
  • The replay bound is honest. It is "no replay older than SYN_MAX_AGE_SEC", not "replay-proof." Widening the window widens replay tolerance by exactly the same amount — that tradeoff is yours to set deliberately.
  • No authorization layer. A valid signature proves who sent it, not that they were allowed to ask for it. What an agent does with a verified message is the agent's problem.
  • The hop-counter is a bound, not a proof. It contains accidental loops and honest-but-injected chains, not a malicious keyholder — who can sign _hops = 0 under their own key. It is a loop/runaway guard, not an anti-abuse control. As of 0.6.1, a verified message carrying an unauthenticated _hops (a legacy signature does not cover the field) has that hop count stripped and logged rather than enforced, so the ceiling acts only on hops it can stand behind; the bound is fully closed once legacy signature acceptance is dropped.
  • A stored memory without a trigger can never be honestly checked. Measuring whether an entry can be found needs a query that is not the entry — its own body ranks it first trivially, and a probe derived from the body is the same thing in a costume. Only the author knows, at write time, why they stored it. So save() tells you at that moment (probeable: false), and it cannot be fixed afterwards: a trigger composed later, by someone reading the row, is derived from the row again. stats() reports the running count as unprobeable.
  • Memory carries a trust field that nothing enforces. Every stored row is untrusted, and every response says so explicitly (trust_enforced: false). The column exists from 0.7.0 so that a later release can make "untrusted by default on every write" true without backfilling a security property onto rows whose provenance is no longer knowable — a default written at insert time is a fact, the same value backfilled later is a guess. Until the elevation mechanism lands and is reviewed, nothing in this package feeds stored memory to a model, and you should not either: a memory store is write-once, re-inject-forever, so the moment retrieved content reaches a model every row ever written becomes an instruction candidate.

Roadmap

Version Theme
0.5.0 Agent runner (attach a model to the mesh) · presence + last-will · durable delivery across listener restarts
0.6.0 Chain-depth loop containment: a signed hop-counter (SYN_MAX_HOPS) bounding how far a reply chain travels
0.6.1 Catch A: a verified-but-unauthenticated (legacy-signed) hop count is stripped and logged, not enforced — closes the migration-window ceiling bypass
0.6.2 The agent runner replies to real traffic again (fail-closed decode) · hop-type safety
0.7.0 Persistent semantic memory: single-file store, save / search, and an honesty envelope that reports degradation on the call rather than the rows
0.7.1 The second text: an optional trigger on every memory, so a stored entry can later be probed with a query that is not itself
0.8.0 doctor for the store — integrity and separability, over the entries that can honestly be probed · memory trust mechanism: retrieved content wrapped as data, and elevation made deliberate, detectable and rare rather than agent-proof
0.9.0 Hardening (per-agent singleton lock, bad-signature quarantine, reconnect watchdog) · signed control channel · liveness that distinguishes wedged from online
1.0.0 Broadcast topics · signed keyring distribution · encrypted envelope

Condition-gated (not version-scheduled): drop legacy signature acceptance — closing the last hop-injection surface — once no v0.5-form emitters remain on the mesh, confirmed by a dated review of emitted signature forms rather than by a release date.

Usage & compliance

Synnoesis is an orchestration layer. It routes to model providers — Anthropic's Claude and others — directly or through gateways like OpenRouter, always using your own credentials. It bundles no API keys and grants no model access of its own.

  • Personal / single-user (Anthropic): a Claude subscription via the official Claude CLI is fine.
  • Multi-user / commercial: use your own API key (Anthropic's Commercial Terms, or a commercial gateway such as OpenRouter) — not a personal subscription. Routing other users through personal subscription credentials violates Anthropic's terms.
  • You're responsible for the terms of every provider and gateway you use — e.g. Anthropic, OpenRouter. Provider terms flow through a gateway: using Claude via OpenRouter still binds you to Anthropic's terms.
  • Export & eligibility: model access is subject to each provider's geographic availability and to applicable export-control and sanctions law (U.S. export controls restricted some frontier models for foreign nationals in 2026; gateways like OpenRouter likewise forbid circumventing geo-restrictions). Ensuring you and your users are eligible is your responsibility. Synnoesis is model-agnostic and guarantees no specific model's availability.
  • Data path: prompts and outputs routed through a gateway pass through that gateway and the downstream provider, each with its own retention and privacy terms. Keep gateway prompt-logging off for sensitive data.

Bottom line: use Synnoesis commercially all you like — just bring your own keys, so you stay compliant and protected.

License

Released under the Apache License 2.0 — free to use, modify, and distribute, including commercially. See LICENSE.

Trademarks

"Kioptix", "Synnoesis", and associated logos are trademarks of Groupe Kioptix Inc. and are not licensed under Apache 2.0 (see §6). Use the software freely; please don't use the names or logos to brand your own product or imply endorsement.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

synnoesis-0.7.1.tar.gz (146.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

synnoesis-0.7.1-py3-none-any.whl (98.6 kB view details)

Uploaded Python 3

File details

Details for the file synnoesis-0.7.1.tar.gz.

File metadata

  • Download URL: synnoesis-0.7.1.tar.gz
  • Upload date:
  • Size: 146.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for synnoesis-0.7.1.tar.gz
Algorithm Hash digest
SHA256 db9eb52641ecb8b0aecd9304cefa236bf3bc9af49b2ff4fa673ffff232940d99
MD5 d0873c0b1a795fcf723f0917fe2c0941
BLAKE2b-256 d62fc67969e9a9dde8d9554759168bfd751e69baef1e46954c07b557974909c7

See more details on using hashes here.

File details

Details for the file synnoesis-0.7.1-py3-none-any.whl.

File metadata

  • Download URL: synnoesis-0.7.1-py3-none-any.whl
  • Upload date:
  • Size: 98.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for synnoesis-0.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c0efe67bc29b53ed7bd6a52089341e97c63e599b9207116b17dedb7f6446b219
MD5 4b178c9224cad83fd892f9b7f7596e57
BLAKE2b-256 b4e64532bc76f69440e180424031274ef9478c17cda63892551766b4b1cfade6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page