Files
Codeman/src/types/cron.ts
T
Codeman maintainer 453605a58f 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>
2026-07-20 04:02:46 +02:00

104 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @fileoverview Cron Jobs type definitions.
*
* NOTE: This is intentionally distinct from the existing `ScheduledRun` concept
* (see src/web/ports/infra-port.ts), which is a run-now, duration-bounded
* autonomous loop. A `CronJob` is a SAVED, NAMED job with a recurring
* schedule (once/interval/daily/weekly), enable/disable, next-run calculation,
* and a history of `CronJobRun` records. The two do not interact.
*
* Persisted to `~/.codeman/state.json` via StateStore (see AppState).
*/
import type { SessionMode } from './session.js';
/** How a job's fire times are computed. */
export type ScheduleType = 'once' | 'interval' | 'daily' | 'weekly';
/** Where the prompt text comes from. */
export type PromptMode = 'inline_text' | 'prompt_file_path';
/** How the prompt is delivered into the session. */
export type InputMode = 'paste' | 'typed';
/** Lifecycle status of a single job execution. */
export type CronJobRunStatus = 'created' | 'session_started' | 'prompt_sent' | 'failed' | 'skipped';
/** What triggered a run. */
export type TriggerType = 'scheduled' | 'manual_run_now';
/** What to do for an AUTOMATIC run when sessions of the same agent already exist. */
export type ConcurrencyPolicy = 'warn_only' | 'skip_if_same_agent_running';
/**
* A saved, named cron job.
*/
export interface CronJob {
id: string;
name: string;
/** Owning username in multi-user mode; the job launches as this user. Undefined in single-user. */
owner?: string;
/** Reuses Codeman's existing session modes; 'shell' covers Terminal/custom. */
agentType: SessionMode;
workingDir: string;
/** Optional custom launch command (only meaningful for 'shell' mode). */
launchCommand?: string;
promptMode: PromptMode;
promptText?: string;
promptFilePath?: string;
inputMode: InputMode;
scheduleType: ScheduleType;
/** once: absolute epoch-ms fire time. */
runAt?: number;
/** interval: minutes between fires. */
intervalMinutes?: number;
/** daily: 'HH:MM' (24h, server-local time). */
dailyTime?: string;
/** weekly: weekdays 0–6 (0=Sunday). */
weeklyDays?: number[];
/** weekly: 'HH:MM' (24h, server-local time). */
weeklyTime?: string;
enabled: boolean;
notes?: string;
/** Applies to automatic (scheduled) runs only. Manual Run Now always warns client-side. */
concurrencyPolicy: ConcurrencyPolicy;
/**
* Close the still-open session created by this job's previous run before the
* next run launches (via the normal session-cleanup path), so unattended
* recurring jobs don't accumulate tabs until the global session cap.
* Default true. Ignored for 'once' schedules.
*/
autoClosePreviousSession?: boolean;
// ── Bookkeeping (server-maintained) ─────────────────────────────────────
createdAt: number;
updatedAt: number;
lastRunAt: number | null;
nextRunAt: number | null;
lastStatus: CronJobRunStatus | null;
/** Duplicate-launch guard: identifies the most recent due-time consumed. */
lastDueKey: string | null;
/** True once a 'once' job has fired (it is also disabled). */
completedOnce?: boolean;
}
/**
* A single execution of a cron job (history record).
*/
export interface CronJobRun {
id: string;
cronJobId: string;
sessionId: string | null;
sessionName: string | null;
startedAt: number;
finishedAt: number | null;
status: CronJobRunStatus;
errorMessage?: string;
triggerType: TriggerType;
/** Best-effort deep link to the created session in the web UI. */
createdSessionUrl: string | null;
}