Skip to content

v0.99.31

Choose a tag to compare

@github-actions github-actions released this 29 Sep 11:27

Upgrade notes

  • Hubs off loopback, or declared multi-seat, now need provisioned identity.
    • They refuse to start without --identity-trust and
      --require-identity-binding.
    • Enrol each seat with synapse identity machine-key --sender NAME --trust FILE.
      --insecure-unbound-identity keeps the old behaviour with a warning.
    • The production docker-compose.yml needs SYNAPSE_IDENTITY_TRUST_FILE and
      SYNAPSE_HEALTH_KEY_FILE; see docs/deployment.md.
    • Single-owner loopback hubs are unaffected.
  • A --db hub creates DB.rowmac.key beside its log on the first start.
    Back it up with the log: a log whose rows carry MACs refuses to start without
    it.
  • Downgrade.
    • An older version can still open the log. The rows it writes carry no MAC, so
      a later 0.99.31 start quarantines them and serves read-only.
    • To accept them after review, clear every row MAC
      (UPDATE events SET mac = NULL) and move the key file aside, so a new key
      starts at the current tip.
  • The durable-ingress chat quota is on by default
    (--no-durable-ingress-quota opts out).
  • Pathless MCP claims now claim the whole Git worktree. Outside Git they
    need task_only=true.

Added

  • Add two synapse doctor checks against MCP server impersonation. An MCP
    client cannot tell the genuine synapse mcp from a server that reports the
    same name and tools. Such a server can answer claim granted while the hub
    holds nothing (reproduced).

    • mcp-host-sources lists every server whose name contains synapse in the
      documented host sources and warns about any that does not launch this
      installation's synapse mcp. The sources are Claude Code's user and local
      scope, the project .mcp.json, the Synapse Claude plugin and the Codex
      profile.
    • mcp-claim-guard warns when a host has such a server but no claim guard.
      The guard asks the hub itself before an edit, so it stops a false grant.
    • Both checks are read-only and warn rather than fail.
    • synapse adapters mutation-status now also counts .claude/settings.local.json and
      the Synapse plugin's hooks as a configured Claude guard.
  • Let synapse health sign its registration. --identity-key-file and
    --identity-key-id name an Ed25519 key enrolled in the hub's
    --identity-trust bundle for the probe's --name (default HEALTH), so a
    container healthcheck can probe a hub started with
    --require-identity-binding. On a token-guarded hub a refused registration
    gets no welcome, so an unenrolled probe reports the hub unhealthy. Giving only
    one of the two flags, or a key file that cannot be read, exits 2 as a
    configuration error.

  • Add synapse identity machine-key. Every client signs its registration with
    this machine's auto-provisioned key; the command prints that key as a
    trust-bundle entry for the --sender names given, or enrols it with --trust.
    A seat can then join a hub started with --require-identity-binding or
    --team-secure without per-command key flags. A key is enrolled once per
    bundle; a second enrolment exits 2.

  • Ship the cryptography runtime in the container image. The hash-locked image
    closure now includes the encryption extra (cryptography 50.0.1 with cffi
    and pycparser, the versions the other locks pin). Before, the image could
    not verify an Ed25519 identity-bound registration, so --require-identity-binding
    and --team-secure could not work inside the container. The compose smoke
    now signs a registration inside the running image and checks that a foreign
    sender is refused.

Changed

  • Record durable message-auth sequence floors under --secure and --team-secure.
    When per-message auth and a durable replay ledger are present, an unset
    --message-auth-sequence-floor-mode now becomes compat, which records each
    key's high-water sequence without refusing anything. strict is not the profile
    default. Tested through the public claim route: the shipped client numbers
    frames from 1 in every process, so a strict floor refused a restarted agent's
    first frame with sequence_mismatch. It stays an explicit operator choice.

  • Accept a retried chat once. Once a copy with a client_msg_id has reached a
    live recipient (or a peer hub's forward outbox), a same-content retry from the
    same sender within 24 hours in the same hub process is no longer stored,
    journalled or delivered again. The sender gets a system frame with
    duplicate: true naming the first copy's msg_id, and the same id with
    different content is refused. A retry of a chat that reached nobody is still
    routed again, so redelivery flows such as review-feedback route keep working.
    Before, every retry was delivered and journalled again (reproduced: two sends
    reached the recipient twice). Chats without client_msg_id are unchanged, and
    peer-forwarded chats keep their forward_id dedupe. New metrics:
    synapse_chat_duplicates_suppressed_total and
    synapse_chat_client_id_conflicts_total. This revises INV-DG-1 in
    docs/coordination-spec.md.

  • Anchor the anti-rollback checkpoint while the hub serves. Before, the hub
    anchored its log only at start and never closed the checkpoint store, so
    cutting events written while it ran passed the next start (reproduced on the
    previous code). Now it anchors every --checkpoint-interval seconds (default
    60), folding only new events, and a clean stop anchors at once and closes the
    store. A hub served again re-verifies before serving. The declared window is
    the interval: after a crash, writes newer than the last anchor can still be cut
    undetected. SynapseHub gains checkpoint_interval, and EventStore.iter_events
    gains after_seq.

  • Require provisioned identity on exposed and multi-seat hubs. A hub that binds
    off loopback, or that declares a multi-seat profile on loopback
    (--expect-multi-seat, --bridge-exposed, --private-directed-messages,
    --require-role-claim, --role-grants or --identity-trust), now refuses to
    start with exit 2 unless it runs with --identity-trust and
    --require-identity-binding. The refusal happens before any durable store is
    opened. Before, a shared token was the only guard, so any token holder could
    register as any seat. Migration: enrol each seat's key
    (synapse identity machine-key --sender NAME --trust FILE on each client
    machine, or synapse identity keygen --trust FILE) and add both flags, or pass
    --insecure-unbound-identity to keep the old behaviour with a warning.
    --paranoid and --secure refuse that flag. The production
    docker-compose.yml now mounts SYNAPSE_IDENTITY_TRUST_FILE and a signed
    health-probe key SYNAPSE_HEALTH_KEY_FILE (see docs/deployment.md). The
    local-development compose file carries the explicit downgrade. Single-owner
    loopback hubs are unaffected, and library users constructing SynapseHub
    directly are unaffected. A peer hub granted by --multihub-serving-policy
    proves its id with its pinned mutual-TLS client certificate. So cross-hub
    forwarding, watch and claim routing keep working into an identity-bound hub,
    while a socket that uses the peer's id without that certificate is still
    refused.

  • State the open loopback default explicitly. A hub on loopback without a
    connect token or identity binding now prints a one-line start-up notice, and
    SECURITY.md has a new "Open loopback default" section on who can join such a
    hub and which profile to use instead.

  • Bound how long a disconnected holder keeps its claims. A claim survives its
    holder's disconnect for the hub's lease window (--lease-offline-ttl, default
    3600 s). During that window synapse state marks it holder-offline=<s>, and
    the state snapshot carries holder_online and holder_offline_seconds. After
    the window the next claim attempt releases the claim, journals the release and
    announces it with released_by: "hub:holder-offline". Before, such a claim
    stayed locked until its lease TTL even when its holder never came back. The new
    metric is synapse_claims_released_abandoned_total.

  • Turn the durable-ingress chat quota on by default for synapse hub. The limit
    is 100 chats and 1 MiB of serialized chat frames per quota principal per 60
    seconds; zero values for the size flags keep these defaults.
    --no-durable-ingress-quota turns it off and is refused under --secure.
    Before, a default hub accepted unbounded durable chat growth. Measured on a
    100-day production log, the peak was 49 chats and 72,560 bytes per window, so
    no recorded traffic would have been refused. Library users constructing
    SynapseHub directly are unaffected.

  • Make the scope of a claim without paths explicit. MCP synapse_claim without
    paths now claims the MCP process's whole Git worktree, and the reply names it.
    Outside Git it is refused. The new task_only=true form locks the task id alone
    with no file scope, like a keyless synapse lock. Previously such a claim was
    sent with an empty worktree, which made it a lock over the hub's shared default
    namespace while the reply said "the whole worktree". Remote (HTTP) MCP claims
    must use task_only=true. The JavaScript SDK's claim gains a
    { taskOnly: true } option and throws on a claim with neither paths nor a
    worktree identity.

Security

  • Authenticate every durable event row (K4-REPLAY).

    • A --db hub MACs each row it writes (HMAC-SHA256 over seq, ts, kind and
      payload) with a key kept outside the log, in DB.rowmac.key (owner-only,
      created on first start) or at --row-mac-key-file.
    • At start, rows written since the key existed are checked. A missing or wrong
      MAC quarantines the row through the journal-recovery gate: replay skips it
      and the hub refuses mutations.
    • Before, anyone who could write the SQLite file could append a claim or
      release that replay applied as hub state. The anti-rollback checkpoint does
      not catch appended rows.
    • Existing rows are the unchecked legacy prefix.
    • A log with MACs refuses to start without its key.
    • The SQLCipher migration carries the MACs.
    • EventStore gains enable_row_mac, has_row_macs, row_quarantine, and an
      apply_row_quarantine switch on iter_events that the Merkle commitment
      uses to fold raw rows.
    • synapse sandbox run --attest DB MACs its row with the hub's key when
      DB.rowmac.key exists, so an attestation into a hub log is not quarantined.
  • Apply the WebSocket handshake's Host and Origin guard to the /metrics and
    /health probes before they answer. An open loopback hub with --metrics and
    no metrics token answered any Host, so a DNS-rebinding page could read its
    metrics and health (reproduced: a foreign Host got 200). Scrapers that reach
    the hub by a name other than the loopback names must now pass it with
    --advertised-host.

Artifact checksums (SHA-256)

8858c6a030a74a0b2f468af93ef07e997d8f86031032697c36e3704bd8a1ec6b  synapse_channel-0.99.31-py3-none-any.whl
f9f041860c229ff6cfecc4635289ff4935b0b0e043ea2eed40a51782c37bf25c  synapse_channel-0.99.31.tar.gz
5769d2e8b70eca90dfb6883e977100d4969fdb6ccb62a9e1422e56d13f70d2da  synapse-channel-v0.99.31-sbom.cdx.json