mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 23:19:43 +02:00
feat(multiuser): phase 3, ownership threading + scoping
Threads per-user ownership through sessions, cases, cron, and the permission policy. All scoping is a no-op in single-user mode (isMultiUserMode() guards). Sessions - Session.owner stamped at every create path from req.authUser / job.owner: POST /api/sessions, /api/run, /api/quick-start, ralph start, cron launch, plan generation. Round-trips through recovery (MuxSession.owner mirror, read muxSession.owner ?? savedState?.owner) and the mux layer. - findSessionOrFail(ctx, id, req) now does a NOT_FOUND owner check (never 403, so other users' session existence is not leaked); wired at ~50 call sites. - List endpoints filtered by owner: GET /api/sessions, /api/sessions/unified (live+persisted+lifecycle scoped, host-wide transcripts admin-only), cron jobs. Permission policy (section 6.3) - resolveClaudeModeForUsername wraps getClaudeModeConfig at every spawn site so a non-granted user is forced to --permission-mode auto (bypass -> auto), including recovery (or a reboot would un-downgrade). buildPromptArgs now respects the session's claudeMode, closing the one-shot (runPrompt) bypass hole. - Shell mode and cron launchCommand require canBypassPermissions: 403 at POST /api/sessions, /api/quick-start create, cron job create, AND cron fire time (re-checked against the owner's current grant). Cases - resolveCasesDir(user): per-user ~/codeman-users/<name>/cases in multi-user, the shared ~/codeman-cases otherwise. All case CRUD + ralph + plan + quick-start resolve through it. resolveCasePath is owner-aware. - GET /api/cases scoped per user (own folders; legacy linked cases admin-only; remote/docker cases owner-filtered). RemoteCase/DockerCase gain owner, stamped at link/quickcreate/import. - Remote + Docker host CRUD is admin-only. - Non-admin workingDir confinement (the linchpin): realpath must resolve inside the user's space, enforced at POST /api/sessions and /api/run BEFORE any disk write. Limits - sessionCapacityState / sessionCapacityMessage centralize the global + per-user cap (CODEMAN_MAX_SESSIONS_PER_USER, default global/2), replacing the 6 copy-pasted MAX_CONCURRENT_SESSIONS checks. Tests: test/ownership-scoping.test.ts (case isolation, host-CRUD gate, workingDir + shell gates, and the scoping helpers). Deferred to phase 4: WS owner gate, SSE fan-out filtering, file-route preview/thumbnail helper scoping, push routing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+12
-2
@@ -136,7 +136,7 @@ import { getLatestPlanUsage } from './plan-usage-latest.js';
|
||||
import type { ScheduledRun } from './ports/index.js';
|
||||
import { registerAuthMiddleware, registerSecurityHeaders, registerHostGuard } from './middleware/auth.js';
|
||||
import { isMultiUserMode } from '../config/multiuser.js';
|
||||
import { bootstrapInitialAdmin, hasUsers } from '../user-store.js';
|
||||
import { bootstrapInitialAdmin, hasUsers, resolveClaudeModeForUsername } from '../user-store.js';
|
||||
import { installRouteErrorHandler } from './route-error-handler.js';
|
||||
import { isExplicitlyEnabled, isLoopbackBindHost, buildHostPolicy, type HostPolicy } from './network-auth-policy.js';
|
||||
import {
|
||||
@@ -2238,7 +2238,16 @@ export class WebServer extends EventEmitter {
|
||||
const sessionName = savedState?.name || muxSession.name || muxSession.muxName;
|
||||
|
||||
// Create a session object for this mux session
|
||||
const recoveryClaudeMode = await this.getClaudeModeConfig();
|
||||
// Owner round-trips like remote/docker: mux-sessions.json carries
|
||||
// MuxSession.owner, state.json carries SessionState.owner. Recovery must
|
||||
// re-resolve the permission mode with the RECOVERED owner or a reboot
|
||||
// would silently un-downgrade a non-granted user's restored session.
|
||||
const recoveredOwner = muxSession.owner ?? savedState?.owner;
|
||||
const recoveryClaudeModeConfig = await this.getClaudeModeConfig();
|
||||
const recoveryClaudeMode = {
|
||||
claudeMode: await resolveClaudeModeForUsername(recoveryClaudeModeConfig.claudeMode, recoveredOwner),
|
||||
allowedTools: recoveryClaudeModeConfig.allowedTools,
|
||||
};
|
||||
// Recover envOverrides from the internal __envOverrides field written by
|
||||
// session-manager (see updateSessionState). Cast to read the non-public field.
|
||||
// Note: a legacy CLAUDE_CODE_EFFORT_LEVEL entry is auto-migrated to `effort`
|
||||
@@ -2275,6 +2284,7 @@ export class WebServer extends EventEmitter {
|
||||
// MuxSession.docker; state.json carries SessionState.docker), so recovery
|
||||
// rebuilds the `docker exec` launch instead of a broken local command.
|
||||
docker: muxSession.docker ?? savedState?.docker,
|
||||
owner: recoveredOwner,
|
||||
});
|
||||
|
||||
// Update session name if it was a "Restored:" placeholder or doesn't match saved name
|
||||
|
||||
Reference in New Issue
Block a user