Status: MLP/3 implementation
Malink is an ACP coding-agent client built on Matrix. Matrix provides durable encrypted store-and-forward, multi-device sync, room/thread history, and media; it is not execution authority and is not used as an application RPC queue.
- A Gateway runs beside the coding agents on a workstation or server.
- The same online-updatable PWA is the UI in desktop browsers and inside the first-party Android shell.
- The Android foreground connection service owns durable background Matrix sync, command reconciliation, local projection, and task notifications.
- A workspace is organized as project rooms; one encrypted room is exactly one
project and each Agent session is an
m.threadin that room. - Future desktop shells may reuse the PWA and native-service boundary without moving the web application into a separately updated offline bundle.
gatewayId is the stable Workspace authorization identifier retained on the
MLP/3 wire for compatibility. gatewayNodeId identifies one execution node.
Every Gateway node in a Workspace holds the same Workspace signing identity,
while its Matrix transport binding and working directories remain node-local.
Consequently a client is paired once with the Workspace, receives one portable
device grant, and manages every project route in the signed Gateway Directory
at the same time without pairing again.
Each node also has a durable, user-editable gatewayName. New installations
default that name to the machine hostname, while the UI appends a stable short
form of gatewayNodeId so duplicate hostnames remain distinguishable. The
signed Gateway Directory is the authority for the mapping from every project
ID to its owning node. Clients derive project labels from that mapping instead
of treating the Gateway used for initial pairing as a globally active node.
Android does not contain or run a Gateway. “Multiple Gateways on Android” means that one native Matrix account session subscribes to every authorized project room and routes each command to the Gateway node that owns that project. Browser PWA and Android parse and persist the same authorization and project-routing documents; their difference is only lifecycle ownership and durable native storage. There is no active-Gateway switch in the product model.
Adding a trusted Gateway is normally an in-product enrollment:
- An authorized client sends
gateway.enrollment.invitation.createto an existing Gateway. The returned short-lived setup link contains the Matrix rendezvous, a one-time login token, the public Workspace key, and a random challenge; it does not contain the Workspace private identity. - The new node opens the setup link, creates a temporary application key and
a stable
gatewayNodeId, logs the new Matrix device into the Workspace-owned Gateway account, and publishes a signed enrollment request into the rendezvous project room. - Every authorized client receives the pending request inside the encrypted
Workspace snapshot. The user compares the six-digit verification code shown
by the client and the new node, then sends
gateway.enrollment.approveto the Gateway node that issued the setup link. - Approval seals the high-authority
malink://gateway-joinmaterial directly to the temporary key from that request and publishes the sealed response through Matrix. Matrix never receives the Workspace private identity in plaintext, and possession of a setup link alone cannot approve a node. - The new node decrypts the response, commits its Workspace identity, then reuses its new Matrix device session when the normal Gateway service starts. Signed directory, device-grant, and revocation state converges from the rendezvous room instead of inflating the approval event beyond Matrix's event-size limit.
- The new node publishes its descriptor into the signed Gateway Directory on its stable bootstrap control route. Root-signed directory, portable grant, and revocation documents are not copied into every conversation room. Authorized clients are invited to new rooms, verify the directory signature, join automatically, and add the route to their existing Matrix session. No Gateway switch is exposed to the user.
- To retire a node, run
malink gateway remove-gateway NODE_ID --gateway-data-dir PATHon another active node. The signed tombstone removes its project routes from every client; the retired process stops when it observes the directory update.
The private Malink Synapse deployment enables login_via_existing_session
without an additional UIAA round trip for this owner-authorized operation. On
a compatible homeserver that does require UIAA, the existing Gateway can read
its Matrix account password through MALINK_MATRIX_GATEWAY_PASSWORD or
MALINK_MATRIX_GATEWAY_PASSWORD_FILE; that password is used locally for the
token request and is never placed in the setup link.
The original invite-gateway bearer command remains an offline recovery tool.
Its output contains the Workspace private identity and must never be posted to
Matrix, a public URL, logs, or chat.
Gateway nodes currently use the same Workspace-owned Matrix user account with
distinct Matrix device IDs. This lets a newly joined node publish the signed
directory into existing private project rooms immediately; client Matrix users
remain separate and are invited from their portable Workspace grants.
On first startup or invite-gateway, active certificates created before
portable grants existed are migrated in place with identical operations and
expiry, so existing clients do not need to pair again.
Gateway compromise and hostile Gateway nodes are outside this deployment's threat model. Matrix remains untrusted transport: it cannot forge grants, directories, commands, or snapshots, and a homeserver compromise does not expose a direct control endpoint on a Gateway.
PWA or Android native service
-> durable signed/encrypted MLP/3 command event
-> Matrix project-room timeline
-> MatrixMlp3GatewayRunner
-> durable command journal / authorization
-> TopicSession -> SemanticSessionRuntime -> AgentProvider
-> ConversationEvent
-> signed/encrypted MLP/3 Gateway event
-> durable Matrix outbox
-> Matrix project thread
-> client raw inbox -> verified local projection -> UI / notification
Edits, redactions, membership changes, Matrix state power, and ordinary Matrix text never directly mutate local execution state. Only a Malink command signed by a currently certified device and accepted by the Gateway journal can do so.
Gateway workspace
└─ project room <-> projectId <-> fixed Gateway working directory
└─ session root <-> m.thread <-> TopicSession
Project display names need not be unique, but project IDs and room bindings are. A session stores its provider binding, model/reasoning configuration, lifecycle, thread root, and immutable project ID. It cannot move between rooms.
The configured room is a bootstrap route, not a fixed project catalog. An
authorized PWA or Android client creates another project by sending
project.create through any existing route owned by the selected Gateway node.
The target node validates (and optionally creates) the absolute working
directory, provisions an encrypted Matrix room with a deterministic alias and
scope-bound ownership marker, commits the room to its durable project catalog,
then republishes its routes in the signed Workspace Gateway Directory. Clients
join and project the new room through the same multi-Gateway reconciliation
path; no local Gateway UI or shell access is required. Newly provisioned
project IDs include gatewayNodeId in their identity scope so identical paths
on different Gateway nodes cannot collide. Legacy bootstrap project IDs retain
their existing cwd-only identity.
Every active session owns its own TopicSession, SemanticSessionRuntime, and
provider instance. Sessions may execute concurrently. Selecting a conversation
is client-local view state and never suspends another session or mutates a
Gateway-wide “current session”. Archive releases runtime resources while
retaining metadata; restore recreates them; delete writes an authenticated
tombstone but does not claim to erase Matrix or provider-retained history.
Sensitive fields—including paths, prompts, Agent output, tool arguments, provider session IDs, credentials, and execution grants—remain inside Malink application encryption. Matrix-visible room names and message bodies are non-sensitive placeholders.
- Pairing pins the Gateway application key, Matrix transport binding, device identity, and command certificate. Pairing and Gateway transport rotation form an independently versioned pre-trust control plane.
- The Gateway directly grants each trusted device the current project key ring
through addressed
io.malink.project.key_grant.v3Room State. - Commands and Gateway outputs use the same MLP/3 application envelope in
ordinary
m.room.messagetimeline events. - A device signature authorizes an exact command. A Gateway signature proves an exact lifecycle transition, Agent/tool event, snapshot, rejection, or terminal result. MLP/3 has no separate command-acknowledgement lane.
- Application encryption binds workspace, project, room, key epoch, logical ID, nonce, and ciphertext. A homeserver cannot relocate or rewrite a signed relation without rejection.
A pairing certificate that includes device.invite represents a full
Workspace member: because that device can add another full member, it already
dominates every ordinary MLP/3 command capability. Gateways therefore map it to
the current protocol's ordinary operation set so existing clients inherit new
product operations without re-pairing or rotating every project key grant.
Explicitly restricted certificates remain restricted. Root privilege approval
is never inherited and still requires the separate privilege.approve grant.
Once a command signature and immutable bindings have been verified, a policy
denial is journaled and returned as a signed command.rejected terminal event;
unverified commands produce no application event.
6. Logical event identity is independent of the physical Matrix event ID.
causationCommandId is a relationship, never message identity.
7. The client saves exact outbound content before send and reuses a stable
Matrix transaction ID. A returned Matrix event ID records homeserver
persistence and stops retransmission; terminal convergence comes from the
signed Gateway chain.
8. The Gateway journals a command before execution. Redelivery of the same
command ID returns its recorded state and cannot execute twice.
9. Current project state is an ordinary signed snapshot referenced by
io.malink.project.current.v3. It is a recovery accelerator, not a separate
mutable authority or a manual checkpoint.
10. /sync is the sole normal source of recent events. Clients commit its
cursor only after durable inbox/projection handling; a limited timeline
creates a durable background gap-recovery job. Thread relations are used
only to establish a cache-cold selected window and to page older history.
11. Clients persist a raw event before projection. Poison is quarantined per
event and dependency-deferred records converge in multiple passes.
12. MLP/1 and MLP/2 application events are neither emitted nor parsed by
production Gateway, PWA, or APK entry points. There is no negotiated data
downgrade.
The normative wire and recovery rules are in
malink-protocol.md.
The native foreground service remains connected while the Activity/WebView is backgrounded. It owns:
- Matrix login,
/sync, thread pagination, and media transfer; - encrypted identity, trust, project keys, raw inbox, projection, and outbox;
- exactly-once command reconciliation across process death;
- notification emission when an Agent task reaches a user-relevant result;
- versioned store migrations before connection starts.
Native application releases are account state owned by the Gateway. Deployment
stores an immutable APK first, then submits its metadata to the owner-only
Gateway admin socket. The Gateway persists the latest release and includes it
in the ordinary signed, encrypted workspace.snapshot; offline devices receive
only that latest state when Matrix synchronization resumes. Android verifies
the APK hash, package identity, monotonic version, architecture, and application
signing certificate before PackageInstaller can replace it. Downloads are
resumable and rebuildable; Matrix tokens, trust, commands, and history never
enter update storage. There is no public update manifest or second update key.
The WebView subscribes to a versioned native bridge and renders service-owned
state. Its malink.events.ack method advances only the local Native-to-WebView
event cursor; it is not a Matrix or MLP/3 command acknowledgement. Detaching,
reloading, or online-updating the PWA cannot cancel a running Agent or create a
second Matrix client. Browser-only use implements the same MLP/3
projection in IndexedDB. It cannot keep Matrix /sync executing after the
browser suspends it, but an opted-in standards-based Web Push subscription lets
the Service Worker wake for a generic task-terminal system notification.
durable turn.completed / turn.failed
-> Gateway Web Push outbox (dedupe + retry)
-> browser push service (VAPID + encrypted payload)
-> PWA Service Worker
-> system notification -> #session=<id>
The PWA registers or removes only its own subscription through encrypted,
signed MLP/3 commands. The Gateway persists one subscription per Malink device
and a stable VAPID key pair beside the replay ledger. Push is an attention
signal, not another conversation transport: the payload contains only bounded
workspace/project/session routing IDs and terminal status, and the UI still
loads and verifies the result from Matrix. Visible clients receive a lightweight
Service Worker message and suppress the duplicate system popup. Expired 404/410
subscriptions are removed; transient delivery remains in the outbox. A bounded
Service Worker eventId cache absorbs the residual duplicate case where a push
provider accepted delivery immediately before the Gateway process stopped.
Matrix media is storage only. A sender encrypts every attachment with a fresh
AES-256-GCM key before upload and signs the mxc:// locator, key, IV, hashes,
name, MIME type, and bounded size inside the application event. The Gateway
downloads only signed descriptors, enforces limits, authenticates/decrypts the
bytes, and converts supported media to ACP rich content. Explicit send_file
is the only Agent-to-client local-file delivery authority. Its MCP entry is
available from the first turn and routes by the fixed Malink session ID through
the owner-only Gateway socket into the owning SemanticSessionRuntime.
- Gateway command journal: deduplication and execution outcome.
- Gateway raw Matrix inbox: cursor-commit barrier before authorization and execution.
- Gateway Matrix outbox: exact content, ordering, retry-after, transaction ID.
- Client durable command outbox: intent and Matrix-send reconciliation.
- Client raw inbox: crash-safe receipt before verification/projection.
- Client projection: rebuildable sessions, messages, lifecycle, and snapshot.
- Matrix timeline/threads: durable cross-device history and audit.
The UI reads only the local projection. A session updatedAt change, browser
focus, visibility change, or network recovery never synchronously reloads its
recent thread relations. Live /sync events update the projection directly;
explicit gap workers and user-requested older pagination are the only recovery
paths allowed to read remote history.
Warm startup is likewise cursor-driven. A browser may skip thread-directory
recovery only when its application projection checkpoint exactly matches the
Matrix SDK's durably saved sync token. Android may do so only when both its
encrypted projection and application-control cursor are present. A missing or
mismatched checkpoint triggers the complete thread-directory rebuild, while
additional Workspace project rooms converge independently in the background
and cannot hold an already-authoritative primary project in Connecting.
No layer substitutes for another. In particular, increasing an in-memory event window, publishing a manual checkpoint, or resending an already Matrix-acked command is not a recovery strategy.
The Gateway process does not update itself. An independently launched,
owner-only supervisor constructs a complete signed release, reusing verified
files from the active release and downloading only changed files. It verifies
every staged file against a locally pinned release key, switches the stable
current symlink, and asks launchd to restart the Gateway. The old Gateway
first enters a drain state: it stops starting commands, waits for active turns
in the default update mode, and keeps durably staging new Matrix events. The
replacement process resumes that inbox before the Matrix sync cursor can skip
an accepted event.
There is no release-channel poller. Each node publishes its current build ID and supervised-update capability in the root-signed Gateway Directory. A manually deployed PWA may embed one exact signed Gateway release and, after it connects, send one stage/apply request through a project owned by every older capable node. The public website stores immutable files but does not become execution or release-signing authority.
Activation requires the expected build ID, a ready and recent Matrix sync, and
a readable durable inbox throughout probation. Failure restores the previous
symlink. Protected-state schema changes are refused by automatic activation
because binary rollback would be unsafe. The full release and recovery
procedure is in gateway-online-updates.md.
A release that changes this vertical slice is not accepted on unit tests alone.
The real Synapse Alpha journey must pair two browsers and an installed isolated
APK, create and run concurrent sessions, receive background completion and a
notification, survive Android process restart, restore history, quarantine a
malformed event without blocking later data, converge across devices, and
delete sessions concurrently. See real-matrix-testing.md.