fix(remote-control): harden self-hosted session flows (#278)

Co-authored-by: chengzifeng <chengzifeng@meituan.com>
This commit is contained in:
Cheng Zi Feng
2026-04-16 10:46:31 +08:00
committed by GitHub
parent 5a4c820e1d
commit fe08cacf8d
24 changed files with 1252 additions and 162 deletions

View File

@@ -8,7 +8,7 @@ import {
handleWebSocketClose,
ingestBridgeMessage,
} from "../../transport/ws-handler";
import { getSession } from "../../services/session";
import { getSession, resolveExistingSessionId } from "../../services/session";
const { upgradeWebSocket, websocket } = createBunWebSocket();
@@ -43,7 +43,8 @@ function authenticateRequest(c: any, label: string, expectedSessionId?: string):
/** POST /v2/session_ingress/session/:sessionId/events — HTTP POST (HybridTransport writes) */
app.post("/session/:sessionId/events", async (c) => {
const sessionId = c.req.param("sessionId")!;
const requestedSessionId = c.req.param("sessionId")!;
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
if (!authenticateRequest(c, `POST session/${sessionId}`, sessionId)) {
return c.json({ error: { type: "unauthorized", message: "Invalid auth" } }, 401);
@@ -71,7 +72,8 @@ app.post("/session/:sessionId/events", async (c) => {
app.get(
"/ws/:sessionId",
upgradeWebSocket(async (c) => {
const sessionId = c.req.param("sessionId")!;
const requestedSessionId = c.req.param("sessionId")!;
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
if (!authenticateRequest(c, `WS ${sessionId}`, sessionId)) {
return {

View File

@@ -4,6 +4,7 @@ import {
getSession,
updateSessionTitle,
archiveSession,
resolveExistingSessionId,
} from "../../services/session";
import { createWorkItem } from "../../services/work-dispatch";
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
@@ -38,7 +39,8 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
/** GET /v1/sessions/:id — Get session */
app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
const session = getSession(c.req.param("id")!);
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
@@ -47,27 +49,43 @@ app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
/** PATCH /v1/sessions/:id — Update session title */
app.patch("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
const existing = getSession(sessionId);
if (!existing) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json();
if (body.title) {
updateSessionTitle(c.req.param("id")!, body.title);
updateSessionTitle(sessionId, body.title);
}
const session = getSession(c.req.param("id")!);
const session = getSession(sessionId);
return c.json(session, 200);
});
/** POST /v1/sessions/:id/archive — Archive session */
app.post("/:id/archive", acceptCliHeaders, apiKeyAuth, async (c) => {
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
try {
archiveSession(c.req.param("id")!);
archiveSession(sessionId);
} catch {
return c.json({ status: "ok" }, 409);
}
return c.json({ status: "ok" }, 200);
});
/** POST /v1/sessions/:id/events — Send event to session */
app.post("/:id/events", acceptCliHeaders, apiKeyAuth, async (c) => {
const sessionId = c.req.param("id")!;
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json();
const events = body.events

View File

@@ -1,6 +1,6 @@
import { Hono } from "hono";
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
import { createSSEStream } from "../../transport/sse-writer";
import { createWorkerEventStream } from "../../transport/sse-writer";
import { getSession } from "../../services/session";
const app = new Hono();
@@ -18,7 +18,7 @@ app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async
const fromSeq = c.req.query("from_sequence_num");
const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
return createSSEStream(c, sessionId, fromSeqNum);
return createWorkerEventStream(c, sessionId, fromSeqNum);
});
export default app;

View File

@@ -1,32 +1,66 @@
import { Hono } from "hono";
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
import { publishSessionEvent } from "../../services/transport";
import { getSession, updateSessionStatus } from "../../services/session";
import { getSession, touchSession, updateSessionStatus } from "../../services/session";
const app = new Hono();
function extractWorkerEvents(body: unknown): Array<Record<string, unknown>> {
if (!body || typeof body !== "object") {
return [];
}
const payload = body as Record<string, unknown>;
const rawEvents = Array.isArray(payload.events)
? payload.events
: Array.isArray(body)
? body
: [body];
return rawEvents
.filter((evt): evt is Record<string, unknown> => !!evt && typeof evt === "object")
.map((evt) => {
const wrappedPayload = evt.payload;
if (wrappedPayload && typeof wrappedPayload === "object" && !Array.isArray(wrappedPayload)) {
return wrappedPayload as Record<string, unknown>;
}
return evt;
});
}
/** POST /v1/code/sessions/:id/worker/events — Write events */
app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json();
const events = Array.isArray(body) ? body : [body];
const events = extractWorkerEvents(body);
const published = [];
for (const evt of events) {
const result = publishSessionEvent(sessionId, evt.type || "message", evt, "inbound");
const eventType = typeof evt.type === "string" ? evt.type : "message";
const result = publishSessionEvent(sessionId, eventType, evt, "inbound");
published.push(result);
}
touchSession(sessionId);
return c.json({ status: "ok", count: published.length }, 200);
});
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */
app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json();
if (body.status) {
updateSessionStatus(sessionId, body.status);
} else {
touchSession(sessionId);
}
return c.json({ status: "ok" }, 200);
@@ -34,12 +68,29 @@ app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) =>
/** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
// TUI's CCRClient calls this for metadata reporting. Accept and discard.
return c.json({ status: "ok" }, 200);
});
/** POST /v1/code/sessions/:id/worker/events/delivery — Batch delivery tracking (no-op) */
app.post("/:id/worker/events/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
return c.json({ status: "ok" }, 200);
});
/** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */
app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
if (!getSession(sessionId)) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
// TUI's CCRClient reports event delivery status (received/processing/processed).
// Accept and discard — event bus doesn't track per-event delivery.
return c.json({ status: "ok" }, 200);

View File

@@ -1,9 +1,75 @@
import { Hono } from "hono";
import { getSession, incrementEpoch } from "../../services/session";
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
import { getSession, incrementEpoch, touchSession, updateSessionStatus } from "../../services/session";
import { apiKeyAuth, acceptCliHeaders, sessionIngressAuth } from "../../auth/middleware";
import { storeGetSessionWorker, storeUpsertSessionWorker } from "../../store";
const app = new Hono();
/** GET /v1/code/sessions/:id/worker — Read worker state */
app.get("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const worker = storeGetSessionWorker(sessionId);
return c.json({
worker: {
worker_status: worker?.workerStatus ?? session.status,
external_metadata: worker?.externalMetadata ?? null,
requires_action_details: worker?.requiresActionDetails ?? null,
last_heartbeat_at: worker?.lastHeartbeatAt?.toISOString() ?? null,
},
}, 200);
});
/** PUT /v1/code/sessions/:id/worker — Update worker state */
app.put("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const body = await c.req.json();
if (body.worker_status) {
updateSessionStatus(sessionId, body.worker_status);
} else {
touchSession(sessionId);
}
const worker = storeUpsertSessionWorker(sessionId, {
workerStatus: body.worker_status,
externalMetadata: body.external_metadata,
requiresActionDetails: body.requires_action_details,
});
return c.json({
status: "ok",
worker: {
worker_status: worker.workerStatus ?? session.status,
external_metadata: worker.externalMetadata,
requires_action_details: worker.requiresActionDetails,
last_heartbeat_at: worker.lastHeartbeatAt?.toISOString() ?? null,
},
}, 200);
});
/** POST /v1/code/sessions/:id/worker/heartbeat — Keep worker alive */
app.post("/:id/worker/heartbeat", acceptCliHeaders, sessionIngressAuth, async (c) => {
const sessionId = c.req.param("id")!;
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
const now = new Date();
storeUpsertSessionWorker(sessionId, { lastHeartbeatAt: now });
touchSession(sessionId);
return c.json({ status: "ok", last_heartbeat_at: now.toISOString() }, 200);
});
/** POST /v1/code/sessions/:id/worker/register — Register worker */
app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => {
const sessionId = c.req.param("id")!;

View File

@@ -1,5 +1,6 @@
import { Hono } from "hono";
import { storeGetSession, storeBindSession } from "../../store";
import { storeBindSession } from "../../store";
import { resolveExistingWebSessionId, toWebSessionId } from "../../services/session";
const app = new Hono();
@@ -14,13 +15,13 @@ app.post("/bind", async (c) => {
return c.json({ error: "sessionId and uuid are required" }, 400);
}
const session = storeGetSession(sessionId);
if (!session) {
const resolvedSessionId = resolveExistingWebSessionId(sessionId);
if (!resolvedSessionId) {
return c.json({ error: "Session not found" }, 404);
}
storeBindSession(sessionId, uuid);
return c.json({ ok: true, sessionId });
storeBindSession(resolvedSessionId, uuid);
return c.json({ ok: true, sessionId: toWebSessionId(resolvedSessionId) });
});
export default app;

View File

@@ -1,31 +1,46 @@
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, updateSessionStatus } from "../../services/session";
import { getSession, isSessionClosedStatus, resolveOwnedWebSessionId, updateSessionStatus } from "../../services/session";
import { publishSessionEvent } from "../../services/transport";
import { getEventBus } from "../../transport/event-bus";
import { storeIsSessionOwner } from "../../store";
const app = new Hono();
function checkOwnership(c: { get: (key: string) => string | undefined }, sessionId: string) {
type OwnershipCheckResult =
| { error: true }
| { error: true; reason: string }
| { error: false; session: NonNullable<ReturnType<typeof getSession>>; sessionId: string };
function checkOwnership(c: { get: (key: string) => string | undefined }, sessionId: string): OwnershipCheckResult {
const uuid = c.get("uuid")!;
if (!storeIsSessionOwner(sessionId, uuid)) {
return { error: true, session: null };
const resolvedSessionId = resolveOwnedWebSessionId(sessionId, uuid);
if (!resolvedSessionId) {
return { error: true };
}
const session = getSession(sessionId);
const session = getSession(resolvedSessionId);
if (!session) {
return { error: true, session: null };
return { error: true };
}
return { error: false, session };
if (isSessionClosedStatus(session.status)) {
return { error: true, reason: `Session is ${session.status}` };
}
return { error: false, session, sessionId: resolvedSessionId };
}
function closedSessionResponse(message: string) {
return { error: { type: "session_closed", message } };
}
/** POST /web/sessions/:id/events — Send user message to session */
app.post("/sessions/:id/events", uuidAuth, async (c) => {
const sessionId = c.req.param("id")!;
const { error } = checkOwnership(c, sessionId);
if (error) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
const requestedSessionId = c.req.param("id")!;
const ownership = checkOwnership(c, requestedSessionId);
if (ownership.error) {
const message = "reason" in ownership ? ownership.reason : "Not your session";
const status = "reason" in ownership ? 409 : 403;
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
}
const { sessionId } = ownership;
const body = await c.req.json();
const eventType = body.type || "user";
@@ -37,11 +52,14 @@ app.post("/sessions/:id/events", uuidAuth, async (c) => {
/** POST /web/sessions/:id/control — Send control request (permission approval etc) */
app.post("/sessions/:id/control", uuidAuth, async (c) => {
const sessionId = c.req.param("id")!;
const { error } = checkOwnership(c, sessionId);
if (error) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
const requestedSessionId = c.req.param("id")!;
const ownership = checkOwnership(c, requestedSessionId);
if (ownership.error) {
const message = "reason" in ownership ? ownership.reason : "Not your session";
const status = "reason" in ownership ? 409 : 403;
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
}
const { sessionId } = ownership;
const body = await c.req.json();
const event = publishSessionEvent(sessionId, body.type || "control_request", body, "outbound");
@@ -50,11 +68,14 @@ app.post("/sessions/:id/control", uuidAuth, async (c) => {
/** POST /web/sessions/:id/interrupt — Interrupt session */
app.post("/sessions/:id/interrupt", uuidAuth, async (c) => {
const sessionId = c.req.param("id")!;
const { error } = checkOwnership(c, sessionId);
if (error) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
const requestedSessionId = c.req.param("id")!;
const ownership = checkOwnership(c, requestedSessionId);
if (ownership.error) {
const message = "reason" in ownership ? ownership.reason : "Not your session";
const status = "reason" in ownership ? 409 : 403;
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
}
const { sessionId } = ownership;
publishSessionEvent(sessionId, "interrupt", { action: "interrupt" }, "outbound");
updateSessionStatus(sessionId, "idle");

View File

@@ -1,9 +1,16 @@
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, createSession } from "../../services/session";
import { storeListSessionsByOwnerUuid, storeIsSessionOwner, storeBindSession } from "../../store";
import {
createSession,
getSession,
isSessionClosedStatus,
listWebSessionSummariesByOwnerUuid,
listWebSessionsByOwnerUuid,
resolveOwnedWebSessionId,
toWebSessionResponse,
} from "../../services/session";
import { storeBindSession } from "../../store";
import { createWorkItem } from "../../services/work-dispatch";
import { listSessionSummariesByOwnerUuid } from "../../services/session";
import { createSSEStream } from "../../transport/sse-writer";
import { getEventBus } from "../../transport/event-bus";
@@ -38,36 +45,36 @@ app.post("/sessions", uuidAuth, async (c) => {
/** GET /web/sessions — List sessions owned by the requesting UUID */
app.get("/sessions", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessions = storeListSessionsByOwnerUuid(uuid);
const sessions = listWebSessionsByOwnerUuid(uuid);
return c.json(sessions, 200);
});
/** GET /web/sessions/all — List sessions owned by the requesting UUID (unowned sessions excluded) */
app.get("/sessions/all", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessions = listSessionSummariesByOwnerUuid(uuid);
const sessions = listWebSessionSummariesByOwnerUuid(uuid);
return c.json(sessions, 200);
});
/** GET /web/sessions/:id — Session detail */
app.get("/sessions/:id", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessionId = c.req.param("id")!;
if (!storeIsSessionOwner(sessionId, uuid)) {
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
if (!sessionId) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
return c.json(session, 200);
return c.json(toWebSessionResponse(session), 200);
});
/** GET /web/sessions/:id/history — Historical events for session */
app.get("/sessions/:id/history", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessionId = c.req.param("id")!;
if (!storeIsSessionOwner(sessionId, uuid)) {
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
if (!sessionId) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const session = getSession(sessionId);
@@ -83,14 +90,17 @@ app.get("/sessions/:id/history", uuidAuth, async (c) => {
/** SSE /web/sessions/:id/events — Real-time event stream */
app.get("/sessions/:id/events", uuidAuth, async (c) => {
const uuid = c.get("uuid")!;
const sessionId = c.req.param("id")!;
if (!storeIsSessionOwner(sessionId, uuid)) {
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
if (!sessionId) {
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
}
const session = getSession(sessionId);
if (!session) {
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
}
if (isSessionClosedStatus(session.status)) {
return c.json({ error: { type: "session_closed", message: `Session is ${session.status}` } }, 409);
}
const lastEventId = c.req.header("Last-Event-ID");
const fromSeqNum = lastEventId ? parseInt(lastEventId) : 0;