mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09: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:
@@ -17,7 +17,15 @@ import {
|
||||
PlanTaskUpdateSchema,
|
||||
PlanTaskAddSchema,
|
||||
} from '../schemas.js';
|
||||
import { findSessionOrFail, parseBody, CASES_DIR, validatePathWithinBase } from '../route-helpers.js';
|
||||
import {
|
||||
findSessionOrFail,
|
||||
getAuthUser,
|
||||
ownerFor,
|
||||
parseBody,
|
||||
resolveCasesDir,
|
||||
validatePathWithinBase,
|
||||
} from '../route-helpers.js';
|
||||
import { resolveClaudeModeForUsername } from '../../user-store.js';
|
||||
import { SseEvent } from '../sse-events.js';
|
||||
import type { SessionPort, EventPort, ConfigPort, InfraPort } from '../ports/index.js';
|
||||
|
||||
@@ -124,12 +132,19 @@ Return ONLY a JSON array. Each item MUST have:
|
||||
|
||||
NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
// Create temporary session for the AI call using Opus 4.5 for deep reasoning
|
||||
// Create temporary session for the AI call using Opus 4.5 for deep reasoning.
|
||||
// Section 6.3: downgrade a non-granted user's one-shot to a classifier-guarded mode.
|
||||
const planOwner = ownerFor(req);
|
||||
const planClaudeModeConfig = await ctx.getClaudeModeConfig();
|
||||
const planClaudeMode = await resolveClaudeModeForUsername(planClaudeModeConfig.claudeMode, planOwner);
|
||||
const session = new Session({
|
||||
workingDir: process.cwd(),
|
||||
mux: ctx.mux,
|
||||
useMux: false, // No mux needed for one-shot
|
||||
mode: 'claude',
|
||||
claudeMode: planClaudeMode,
|
||||
allowedTools: planClaudeModeConfig.allowedTools,
|
||||
owner: planOwner,
|
||||
});
|
||||
|
||||
// Use configured model for plan generation, falling back to opus
|
||||
@@ -228,7 +243,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
// Determine output directory for saving wizard results
|
||||
let outputDir: string | undefined;
|
||||
if (caseName) {
|
||||
const casePath = validatePathWithinBase(caseName, CASES_DIR);
|
||||
const casePath = validatePathWithinBase(caseName, resolveCasesDir(getAuthUser(req)));
|
||||
if (casePath && existsSync(casePath)) {
|
||||
outputDir = join(casePath, 'ralph-wizard');
|
||||
|
||||
@@ -359,7 +374,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
app.patch('/api/sessions/:id/plan/task/:taskId', async (req) => {
|
||||
const { id, taskId } = req.params as { id: string; taskId: string };
|
||||
const session = findSessionOrFail(ctx, id);
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
|
||||
const tracker = session.ralphTracker;
|
||||
if (!tracker) {
|
||||
@@ -385,7 +400,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
app.post('/api/sessions/:id/plan/checkpoint', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = findSessionOrFail(ctx, id);
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
|
||||
const tracker = session.ralphTracker;
|
||||
if (!tracker) {
|
||||
@@ -401,7 +416,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
app.get('/api/sessions/:id/plan/history', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = findSessionOrFail(ctx, id);
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
|
||||
const tracker = session.ralphTracker;
|
||||
if (!tracker) {
|
||||
@@ -415,7 +430,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
app.post('/api/sessions/:id/plan/rollback/:version', async (req) => {
|
||||
const { id, version } = req.params as { id: string; version: string };
|
||||
const session = findSessionOrFail(ctx, id);
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
|
||||
const tracker = session.ralphTracker;
|
||||
if (!tracker) {
|
||||
@@ -435,7 +450,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
app.post('/api/sessions/:id/plan/task', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const session = findSessionOrFail(ctx, id);
|
||||
const session = findSessionOrFail(ctx, id, req);
|
||||
|
||||
const tracker = session.ralphTracker;
|
||||
if (!tracker) {
|
||||
|
||||
Reference in New Issue
Block a user