mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
feat(multiuser): phase 4, event fan-out + stream scoping
Scopes real-time streams and the init snapshot so a multi-user client only receives what it owns. No-op in single-user mode (identity-less clients). - WS terminal (ws-routes): owner gate after the session lookup. A non-admin may only attach to their own session (close 4003); the global auth hook already ran on the upgrade and decorated req.authUser, so an unauthenticated upgrade never reaches the handler. - SSE (sse-stream-manager): per-client identity stored at addClient; broadcast() and the terminal-batch flush both enforce a routing hint via canDeliver(). WebServer.broadcast auto-derives the hint (deriveSseHint): session-scoped event families resolve the owner from the payload's session id (fail closed when the owner can't be resolved), machine-level families (docker/tunnel/update/system/ cron) + host-plan telemetry are admin-only, everything else stays global. Raw terminal bytes resolve the owner once and are withheld from non-owners. - getLightState is filtered per connection AFTER the shared cache (sessions, respawnStatus, subagents, workflowRuns by owner; scheduledRuns + planUsage admin-only); applied to both the SSE init snapshot and GET /api/status. - file-routes: getKnownSessionWorkingDir + getSessionAttachmentHistory (the preview/thumbnail/history helpers that bypass findSessionOrFail) now owner-check the session, closing a cross-user file-read path. - GET /api/search: harvestSources is owner-scoped. Deferred to a follow-up (documented in docs/multi-user-plan.md): away-digest + subagent/workflow REST list scoping, push-subscription identity + routing, per-user screenshot subdirs. The live-event versions of these are already routed by the SSE hint; only the on-demand REST aggregates remain global for admins-only follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ import type { SessionPort } from '../ports/session-port.js';
|
||||
import { MAX_INPUT_LENGTH } from '../../config/terminal-limits.js';
|
||||
import { isAllowedRequestHost, isAllowedRequestOrigin, type HostPolicy } from '../network-auth-policy.js';
|
||||
import { WsConnectionRegistry } from '../ws-connection-registry.js';
|
||||
import { canAccessOwned, getAuthUser } from '../route-helpers.js';
|
||||
|
||||
/** Micro-batch interval for terminal output (ms). Short enough for low latency,
|
||||
* long enough to group Ink's rapid cursor-up redraw sequences into single frames. */
|
||||
@@ -93,6 +94,17 @@ export function registerWsRoutes(app: FastifyInstance, ctx: SessionPort, getHost
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-user owner gate: writing to this socket injects keystrokes into the
|
||||
// agent, so a non-admin may only attach to their OWN session. The global auth
|
||||
// hook already ran on the upgrade request and decorated req.authUser (an
|
||||
// unauthenticated upgrade never reaches here — the hook 401s the handshake).
|
||||
// findSessionOrFail throws an HTTP-shaped error, so the check is inlined here
|
||||
// as a 4003 close. No-op in single-user mode (canAccessOwned returns true).
|
||||
if (!canAccessOwned(getAuthUser(req), session.owner)) {
|
||||
socket.close(4003, 'Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Structured transport logging — surfaces WS open/close/timeout churn so the
|
||||
// tunnel-flap behavior (COD-134) is observable in the server logs. Fastify is
|
||||
// configured logger:false, so we log via console (→ journald under systemd).
|
||||
|
||||
Reference in New Issue
Block a user