Skip to content

v0.98.0

Choose a tag to compare

@github-actions github-actions released this 05 Jul 07:48
· 1046 commits to main since this release

Added

  • Hub pinning indicator — the hub now reports a config_epoch alongside its
    version, a short deterministic fingerprint of its configuration posture (the
    scalar limits and the armed/disarmed state of each optional subsystem: auth,
    ACL, per-message auth, metrics, multi-hub, federation). It appears in the hub's
    /health response, in the who snapshot, and in the dashboard's
    /snapshot.json (as hub_version and config_epoch), so a cockpit can badge
    which hub build and configuration it is watching and notice a deploy or a config
    drift. Honest scope: it fingerprints posture, not secrets — object-valued
    settings enter only as a presence marker, so rotating a key or editing an ACL
    rule within the same posture does not change it. A hub built without a grouped
    config (an ad-hoc SynapseHub()) reports an empty config_epoch.

  • /waits.json dashboard store feed (with --feeds-db) — the pending
    coordination gates reconstructed from the durable plan: each non-terminal task
    blocked on a dependency that has not reached a terminal status, with who is
    waiting (the task's suggested owner, or whoever declared it), on_what
    dependency ids block it, and since when it was declared, plus a wait_count.
    The "what is the fleet stuck behind" panel. Store-derived and deterministic
    (dependency satisfaction judged from the log's own recorded task statuses),
    available with the hub down; 404 without --feeds-db, 503 on an unreadable
    store. Transient socket waiters are not journalled and are omitted — this is the
    coordination gates the plan can prove, not who holds a socket open.

  • /sessions.json dashboard store feed (with --feeds-db) — the opt-in
    session_metric telemetry the fleet left in the durable log, in the same JSON
    synapse participants costs renders: per-session token counts, cost, latency,
    and error/abstention rates, plus totals aggregated across sessions. Every
    record carries the seq of the snapshot it was read from and the coordination
    task_id from the note body, so a cockpit joins a session's cost straight to
    its causal cone (via /causality.json) — "this session spent N tokens on task
    T42", not merely "on session S". Same posture as the other store feeds:
    store-derived and deterministic (available with the hub down), 404 without
    --feeds-db, 503 on an unreadable store; a log with no session notes reports
    empty sessions and zeroed totals, never a fabricated cost.

  • Operator write-path for the dashboard (opt-in) — synapse dashboard --operator
    arms three write routes so the cockpit can act on the fleet rather than only
    observe it: POST /message ({"to","text"}) relays a chat message, POST /task
    ({"id","title","depends_on"?}) declares a board task, and POST /task/update
    ({"id","status"?,"note"?}) changes a task's status and/or appends a progress
    note. Off by default: without the flag every route is a 404, indistinguishable
    from an unknown path, and the dashboard stays a read-only observer. When armed, a
    write still requires the dashboard bearer token, is rate-limited, and is sent
    under an explicit operator:<name> identity that never impersonates an agent.
    The relay reimplements neither authorisation nor auditing — the hub ACL-checks
    the relayed frame and records it in the durable log, so every operator action is
    authorised at the hub and appears in replay, /state-at, and the signal stream.
    Responds 200 when delivered, dead-lettered, or applied, 403 on ACL refusal,
    409 when the blackboard refuses a task on its own terms, and 503 when the hub
    is unreachable.

  • CycloneDX SBOM (synapse-channel-<tag>-sbom.cdx.json) is generated from the
    built wheel's dependency closure and attached to every GitHub Release alongside
    the distributions, so the published software bill of materials is a first-class
    release artifact. The generator (cyclonedx-bom) is hash-locked in a dedicated
    requirements-sbom.txt.

  • all convenience extra bundling every runtime feature library (cryptography,
    WASM, OTel, MCP) — pip install synapse-channel[all] for a full-feature install
    without naming each extra. A packaging drift guard keeps all exactly the union
    of the feature extras, keeps the runtime floor a single dependency, and imports
    every feature-consuming module to prove the base import surface never
    hard-requires an optional library.

Changed

  • Removed the hub's register / unregister / _authenticate_or_close methods —
    thin wrappers over the HubConnection collaborator that no live path or test
    reached once the socket lifecycle moved behind the handler entry point. The
    collaborator owns these steps and is tested on them directly; the hub keeps only
    the handler, _send_welcome, and _install_signal_handlers delegators, each
    with its own hub call site (serve, the withheld-welcome path, and shutdown
    wiring). No behaviour change — a redundant indirection is gone. core/hub.py
    drops from 1003 to 978 lines.
  • Extracted the hub's durable-state seeding into core/hub_state_seed.py
    (seed_hub_state): the decision to replay the event log — resuming live leases,
    chat history, the shared blackboard, and the ledger-guard seed (the message-id
    high-water mark, per-actor finding counts, and the idempotency cache) — or build an
    empty registry, together with the one-off compaction hint a hub emits when opened on
    an oversized log, now lives in one pure function returning a SeededHubState the
    constructor binds, instead of a ~50-line branch inlined in __init__. It holds no
    hub reference, so the resume-versus-fresh behaviour is testable without a live hub.
    No behaviour change — a restart resumes exactly as before and the compaction hint
    fires on the same threshold. Fourth slice of the resumed hub decomposition, taking
    core/hub.py from 1037 to 1003 lines (1294 at the start of the arc, with four
    single-responsibility collaborators peeled). 100% line+branch on the new module.
  • Extracted the hub's frame-authorisation gates into core/hub_frame_gates.py
    (HubFrameGates): verifying required per-message authentication (an HMAC frame
    signature or an Ed25519 signed-event signature), authorising a mutating frame
    against the ACL, and routing a claim by namespace ownership — granting locally,
    forwarding to the owning peer hub and relaying its verdict, or refusing fail-closed
    with the owner named — now live in one class the hub holds, with
    _verify_per_message_auth / _authorise_acl / _authorise_claim_ownership left as
    thin delegating wrappers (the handle_message pipeline keeps one entry point per
    gate) and the internal _observed_asserting_hubs / _forward_remote_claim folded
    into the collaborator. Routing itself stays on the hub, since a handler is invoked
    with the hub as its first argument; the gates take the hub's per-socket send and
    system-message factory as injected callbacks and capture their policy inputs at
    construction, so the collaborator carries no back-reference to the hub. No behaviour
    change — the auth verdicts, the ACL denials, and the claim grant/forward/refuse
    decisions are identical. Third slice of the resumed hub decomposition, taking
    core/hub.py from 1202 to 1037 lines. 100% line+branch on the new module.
  • Extracted the hub's socket-connection lifecycle into core/hub_connection.py
    (HubConnection): admitting a socket against the capacity, per-host, and
    unauthenticated-burst ceilings; welcoming it (on connect for an open hub, or only
    after the first frame authenticates on a secured one); reading the authenticated
    first frame under the auth deadline; pumping later frames into the routing
    pipeline; releasing the agent name and broadcasting the departure on disconnect;
    and the SIGTERM/SIGINT graceful-shutdown wiring — now live in one class the
    hub holds, with register / unregister / handler / _send_welcome /
    _authenticate_or_close / _install_signal_handlers left as thin delegating
    wrappers, so serve and every test keep one entry point. Frame routing itself
    stays on the hub and is handed in as an injected callback, so the collaborator
    carries no back-reference to the hub; _process_request also stays on the hub as
    the HTTP-endpoint renderer. No behaviour change — connection admission, the
    withheld-welcome timing on a secured hub, and disconnect cleanup are identical.
    Second slice of the resumed hub decomposition, taking core/hub.py from 1258 to
    1202 lines. 100% line+branch on the new module.
  • Extracted the hub's pre-route ingress guards into core/hub_ingress.py
    (HubIngress): authenticating a socket's first frame against the shared-secret
    token, binding the claimed sender name (with optional takeover), keying the remote
    host for per-host rate limiting, closing a socket, and refusing — or, when
    overridden, warning about — an exposed bind now live in one class the hub holds,
    with _authorise / _resolve_sender / _exposure_problems / _guard_exposure
    left as thin delegating wrappers and _close_socket / _remote_host kept as
    class-callable staticmethods (the handler call surface is unchanged). It reads the
    live socket registry and takes the hub's per-socket send and system-message factory
    as injected callbacks, so it carries no back-reference to the hub. No behaviour
    change; the token gate, name resolution, and exposure refusal are identical. First
    slice of the resumed hub decomposition, taking core/hub.py from 1294 to 1258
    lines. 100% line+branch on the new module.
  • synapse hub --paranoid is now the full production secure preset: besides the
    token, durable log, and per-message authentication it already required, it now
    also requires ACL enforcement (--require-acl with an --acl-policy) and native
    WSS (--tls-certfile/--tls-keyfile), and still disables metrics query tokens
    and the insecure off-loopback override. A paranoid start without ACL or TLS now
    fails closed with a specific message. The missing-hooks report drops ACL and
    signed events (now enforced) and names what genuinely remains — mutual-TLS
    client-certificate verification and cryptographic per-agent identity.

Fixed

  • A dashboard store feed (/state-at.json, /merkle-proof.json, /events.json,
    /causality.json) crashed with an unhandled OverflowError when a ?seq= or
    ?limit= query carried an integer beyond SQLite's signed 64-bit range — an
    arbitrarily large value parsed as an unbounded Python int, then overflowed
    inside the store query. The feeds now bound the parsed integer and answer 400
    instead of a 500. Found by the new query-feed fuzz test.

Tests

  • Fuzz coverage for the dashboard store-feed query parsers: hostile ?seq=,
    ?task=, ?direction=, and ?limit= values (non-numeric, negative, huge,
    duplicated, percent-encoded, multi-kilobyte) are thrown at every feed and the
    handler must answer a deliberate status (200/400/404/503), never an
    unhandled 500.
  • Property-based coverage (Hypothesis) for the coordination invariants a
    correctness bug would break silently: claim-scope overlap is symmetric and
    agrees with per-path overlap (no file collision slips a non-conflicting scope),
    a whole-worktree claim conflicts with any other, the idempotency cache replays
    the most recent response for a key and never exceeds its bound, and a task never
    takes a forbidden lifecycle transition — least of all out of a terminal state.

Documentation

  • SECURITY.md gains a deployment-profile matrix (local-dev, single-user
    workstation, team LAN, internet-exposed) mapping each profile to its required
    controls, plus a capability→extra map, and its paranoid-mode summary is updated
    for the now-required ACL and TLS. CONTRIBUTING.md gains a definition of done
    (changelog fragment, backward-compat statement, threat-model delta) and a
    core/* hot-path ownership note.

Security

  • Every dashboard and cockpit HTTP response now carries browser-hardening headers:
    X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer,
    X-Frame-Options: DENY, and a same-origin Content-Security-Policy
    (frame-ancestors 'none', base-uri 'none', object-src 'none'; inline
    script/style retained for the server-rendered pages). The dashboard is
    self-contained, so the policy blocks injected remote resources at no cost.
  • The metrics query-string token (--metrics-query-token-ok) is now loopback-only.
    Binding a non-loopback host with it set is refused with InsecureBindError (like
    the other exposure guards, downgradable with --insecure-off-loopback), because a
    ?token= value leaks into proxy access logs, browser history, and shell history.
    On a loopback bind it remains a legitimate local debug aid; off loopback the token
    belongs in the Authorization header.