fix(hosts): share daemon links across isolated routed sessions

Multiplex logical sessions over the monitored daemon connection and wrap original binary bytes in routing headers. Admit channels under their authenticated parent and current grants; preserve scoped delivery and physical send completion.
This commit is contained in:
Mohamed Boudra
2026-10-20 07:59:01 +02:00
parent 4284209025
commit 5ab9b8dcd4
23 changed files with 1384 additions and 204 deletions
@@ -2,6 +2,9 @@ import { describe, expect, it } from "vitest";
import {
decodeBinaryFrame,
encodeHostRouteFrame,
decodeHostRouteFrame,
MAX_HOST_ROUTE_ID_BYTES,
encodeFileTransferFrame,
encodeTerminalStreamFrame,
FileTransferOpcode,
@@ -51,3 +54,38 @@ describe("binary frame demux", () => {
expect(decodeBinaryFrame(new Uint8Array([0xff, 0]))).toBeNull();
});
});
describe("host route binary framing", () => {
it("wraps terminal and file frames unchanged and leaves their direct interpretation intact", () => {
const frames = [
encodeTerminalStreamFrame({ opcode: TerminalStreamOpcode.Input, slot: 7, payload: "echo" }),
encodeFileTransferFrame({
opcode: FileTransferOpcode.FileChunk,
requestId: "upload",
payload: new Uint8Array([0, 255, 128, 1]),
}),
];
for (const payload of frames) {
const wire = encodeHostRouteFrame("channel", payload);
const decoded = decodeBinaryFrame(wire);
expect(decoded).toEqual({ kind: "host_route", frame: { routeId: "channel", payload } });
expect(wire.subarray(10)).toEqual(payload);
expect(decodeBinaryFrame(decodeHostRouteFrame(wire)!.payload)).toEqual(
decodeBinaryFrame(payload),
);
}
});
it("rejects truncated, oversized identifiers and invalid UTF-8 without throwing", () => {
for (const bytes of [
new Uint8Array([0x30]),
new Uint8Array([0x30, 0, 4, 65]),
new Uint8Array([0x30, 0, 0]),
new Uint8Array([0x30, 1, 1]),
new Uint8Array([0x30, 0, 1, 255]),
])
expect(decodeBinaryFrame(bytes)).toBeNull();
expect(() =>
encodeHostRouteFrame("a".repeat(MAX_HOST_ROUTE_ID_BYTES + 1), new Uint8Array()),
).toThrow("Invalid host route ID");
});
});
@@ -1,3 +1,4 @@
import { HOST_ROUTE_OPCODE, decodeHostRouteFrame, type HostRouteFrame } from "./host-route.js";
import {
decodeFileTransferFrame,
FileTransferOpcode,
@@ -10,11 +11,16 @@ import {
} from "./terminal.js";
export type BinaryFrame =
| { kind: "host_route"; frame: HostRouteFrame }
| { kind: "terminal"; frame: TerminalStreamFrame }
| { kind: "file_transfer"; frame: FileTransferFrame };
export function decodeBinaryFrame(bytes: Uint8Array): BinaryFrame | null {
switch (bytes[0]) {
case HOST_ROUTE_OPCODE: {
const frame = decodeHostRouteFrame(bytes);
return frame ? { kind: "host_route", frame } : null;
}
case TerminalStreamOpcode.Output:
case TerminalStreamOpcode.Input:
case TerminalStreamOpcode.Resize:
@@ -0,0 +1,33 @@
import { MAX_WEBSOCKET_MESSAGE_BYTES } from "../websocket-message-limit.js";
/** Routing adds a bounded header; the enclosed terminal/file frame is never re-encoded. */
export const HOST_ROUTE_OPCODE = 0x30;
export const MAX_HOST_ROUTE_ID_BYTES = 256;
export const MAX_HOST_ROUTE_HEADER_BYTES = 3 + MAX_HOST_ROUTE_ID_BYTES;
export const MAX_ROUTED_PAYLOAD_BYTES = MAX_WEBSOCKET_MESSAGE_BYTES - MAX_HOST_ROUTE_HEADER_BYTES;
export interface HostRouteFrame {
routeId: string;
payload: Uint8Array;
}
export function encodeHostRouteFrame(routeId: string, payload: Uint8Array): Uint8Array {
const id = new TextEncoder().encode(routeId);
if (!id.length || id.length > MAX_HOST_ROUTE_ID_BYTES) throw new Error("Invalid host route ID");
if (payload.byteLength > MAX_ROUTED_PAYLOAD_BYTES) throw new Error("Host frame is too large");
const frame = new Uint8Array(3 + id.length + payload.byteLength);
frame[0] = HOST_ROUTE_OPCODE;
new DataView(frame.buffer).setUint16(1, id.length);
frame.set(id, 3);
frame.set(payload, 3 + id.length);
return frame;
}
export function decodeHostRouteFrame(bytes: Uint8Array): HostRouteFrame | null {
if (bytes[0] !== HOST_ROUTE_OPCODE || bytes.length < 3) return null;
const length = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint16(1);
if (!length || length > MAX_HOST_ROUTE_ID_BYTES || bytes.length < 3 + length) return null;
if (bytes.length - 3 - length > MAX_ROUTED_PAYLOAD_BYTES) return null;
try {
const routeId = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(3, 3 + length));
return { routeId, payload: bytes.subarray(3 + length) };
} catch {
return null;
}
}
@@ -1,3 +1,4 @@
export * from "./demux.js";
export * from "./file-transfer.js";
export * from "./terminal.js";
export * from "./host-route.js";
+14 -3
View File
@@ -66,8 +66,8 @@ export const HostsRouteOpenRequestSchema = z.object({
export const HostsRouteFrameRequestSchema = z.object({
type: z.literal("hosts.route.frame.request"),
routeId: z.string(),
data: z.string().max(24 * 1024 * 1024),
binary: z.boolean(),
data: z.string().max(100 * 1024 * 1024),
binary: z.literal(false),
});
export const HostsRouteCloseRequestSchema = z.object({
type: z.literal("hosts.route.close.request"),
@@ -104,9 +104,20 @@ export const HostsRouteOpenResponseSchema = z.object({
});
export const HostsRouteFrameSchema = z.object({
type: z.literal("hosts.route.frame"),
payload: z.object({ subscriptionId: z.string(), data: z.string(), binary: z.boolean() }),
payload: z.object({ subscriptionId: z.string(), data: z.string(), binary: z.literal(false) }),
});
export const HostsRouteClosedSchema = z.object({
type: z.literal("hosts.route.closed"),
payload: z.object({ subscriptionId: z.string(), code: z.string(), message: z.string() }),
});
// Logical B sessions are admitted under the authenticated physical link, never hello auth.
export const HostsLinkOpenRequestSchema = z.object({
type: z.literal("hosts.link.open.request"),
requestId: z.string(),
channelId: z.string().min(1).max(256),
});
export const HostsLinkOpenResponseSchema = z.object({
type: z.literal("hosts.link.open.response"),
payload: z.object({ requestId: z.string(), routeId: z.string() }),
});
+5
View File
@@ -4,6 +4,7 @@ import {
HostsAddRequestSchema,
HostsUpdateRequestSchema,
HostsRemoveRequestSchema,
HostsLinkOpenRequestSchema,
HostsRouteOpenRequestSchema,
HostsRouteFrameRequestSchema,
HostsRouteCloseRequestSchema,
@@ -12,6 +13,7 @@ import {
HostsAddResponseSchema,
HostsUpdateResponseSchema,
HostsRemoveResponseSchema,
HostsLinkOpenResponseSchema,
HostsRouteOpenResponseSchema,
HostsRouteFrameSchema,
HostsRouteClosedSchema,
@@ -3223,6 +3225,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
HostsAddRequestSchema,
HostsUpdateRequestSchema,
HostsRemoveRequestSchema,
HostsLinkOpenRequestSchema,
HostsRouteOpenRequestSchema,
HostsRouteFrameRequestSchema,
HostsRouteCloseRequestSchema,
@@ -3620,6 +3623,7 @@ export const ServerInfoStatusPayloadSchema = z
backgroundWorkspaces: z.boolean().optional(),
providersSnapshot: z.boolean().optional(),
connectedHosts: z.boolean().optional(),
hostMultiplexing: z.boolean().optional(),
usageSources: z.boolean().optional(),
// COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10.
providersSnapshotCwd: z.boolean().optional(),
@@ -6904,6 +6908,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
HostsAddResponseSchema,
HostsUpdateResponseSchema,
HostsRemoveResponseSchema,
HostsLinkOpenResponseSchema,
HostsRouteOpenResponseSchema,
HostsRouteFrameSchema,
HostsRouteClosedSchema,