feat(cron): recurring cron-style scheduled jobs#141
Open
chatgptkrylor wants to merge 7 commits into
Open
Conversation
Adds a saved/named scheduling layer on top of Codeman's existing session primitives. Distinct from the legacy run-now ScheduledRun concept. - types/scheduler.ts: ScheduledJob + ScheduledJobRun - state-store: persist scheduledJobs/scheduledJobRuns in ~/.codeman/state.json - scheduler/scheduler-time.ts: pure once/interval/daily/weekly next-run math - scheduler/scheduler-service.ts: CRUD, Run Now, due-checker tick, run history; reuses SessionPort (create -> start -> writeViaMux) for launches - web/routes/scheduler-routes.ts: /api/scheduler/jobs CRUD + run + history - web/schemas.ts: zod validation with schedule-type-aware refinements - web/sse-events.ts: scheduler:* events - server.ts: wire service into route context + 30s background tick loop - test/scheduler-time.test.ts: 14 unit tests for next-run calculations Phase 1 discovery recorded in SCHEDULER_DISCOVERY.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rp7JhmQXcYJhmxFMdZuuah
- scheduler-ui.js: job list + create/edit form + Run Now/Enable/Disable/Delete, reacting to scheduler:* SSE events; same-agent Run Now warning - index.html: "⏰ Schedules" toolbar button + #schedulerModal + script include - constants.js / app.js: frontend SSE event constants + handler map entries - styles.css: scheduler row/badge/form styles Follows Codeman's vanilla-JS mixin + .modal/.form-row conventions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rp7JhmQXcYJhmxFMdZuuah
Rename the recurring-jobs feature scheduler->cron to disambiguate from the legacy ScheduledRun system (/api/scheduled), which is left untouched: - ScheduledJob->CronJob, SchedulerService->CronService - /api/scheduler/jobs -> /api/cron/jobs; SSE scheduler:* -> cron:* - state keys cronJobs/cronJobRuns - files moved to src/cron/, cron-routes.ts, cron-port.ts, types/cron.ts - frontend cron-ui.js, #cronModal, menu "Cron" - docs moved to docs/cron-discovery.md + docs/cron-build-brief.md, README guides - new tests: cron-service.test.ts, cron-time.test.ts Green: tsc, lint, frontend-syntax, format, 30 cron + 9 legacy scheduled-runs tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU
A cron job's promptFilePath is user-supplied via the API and was read with an unconfined readFile of any absolute path, so a hostile job config could exfil arbitrary host files (e.g. /etc/passwd, SSH keys) into a Claude session. Guard the read in resolvePrompt by mirroring the attachment-serving guard (resolveServableAttachmentPath in file-routes): realpath-resolve the path, then reject via the shared blocklist (/etc, /root, secret locations) plus the optional workspace-confinement toggle before reading. Regression tests in cron-service.test.ts: blocks /etc/passwd (the live repro) and /root/*, fails cleanly on a missing file, and still allows an ordinary prompt file outside the blocklist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU
1. once-rearm on edit: editing any field of a finished one-time job reset completedOnce, silently resurrecting it. Now only a SCHEDULE edit (scheduleType/runAt/interval/daily/weekly) re-arms a completed once job; cosmetic edits (rename/notes) leave completedOnce intact. 2. update-validation gap: CronJobUpdateSchema = .partial() drops the cross-field superRefine, so a PUT switching scheduleType without its dependent field produced a dead enabled job (nextRunAt:null). updateJob now re-validates the MERGED job against the full CronJobSchema and throws 400 on inconsistency, leaving the stored job untouched. 3. concurrency-skip silent starvation: skip_if_same_agent_running advanced the schedule but wrote no run record, so a perpetually-skipped job had empty history. Now records a 'skipped' run (new CronJobRunStatus) + lastStatus. Tests updated/added in cron-service.test.ts (37 pass): once non-schedule edit preserves completedOnce, schedule edit re-arms, inconsistent partial update is rejected with the stored job untouched, and the skip path records a skipped run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU
Two blind adversarial reviewers found real holes in the prior cron commits:
SECURITY (was CRITICAL): the prompt-file guard was blocklist-only by default,
so promptFilePath:/proc/self/environ leaked the SERVER PROCESS's entire
environment (every secret) into the agent session, and /dev/zero or a FIFO
caused an unbounded readFile → OOM/hang DoS. A denylist is the wrong posture
for an exfil-into-LLM sink. resolveSafePromptPath now:
- confines the realpath-resolved file to the job's working dir (ALLOWLIST) —
closes /proc, /dev, other homes, modern cloud-cred paths, and symlink escapes
- requires a regular file (rejects dirs/FIFOs/char devices)
- caps the read at MAX_PROMPT_FILE_BYTES (1 MiB)
- keeps the /etc,/root,secrets blocklist as defense-in-depth
LOGIC:
- once-rearm (was MED, defeated in prod): the edit UI round-trips the full
job, so the field-PRESENCE re-arm check always fired → a renamed fired
once-job could be resurrected via edit→re-enable. Now compares schedule
VALUES; an unchanged schedule never re-arms.
- skipped-run history (was HIGH): recording a skip every tick was unbounded
state.json growth. Now coalesces consecutive skips (one record per streak)
and prunes global run history to MAX_CRON_RUN_HISTORY (500), covering the
launch path too.
- skip bookkeeping (was MED): a skip no longer advances lastRunAt (nothing
ran); lastStatus still reflects 'skipped'.
Regression tests added/updated (43 pass): /proc/self/environ + outside-workspace
+ symlink-escape + non-regular + oversized all blocked, in-workspace file
passes; UI-path once resurrection blocked; consecutive skips coalesce to one
record; skip leaves lastRunAt null.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU
- docs/cron-guide.md: comprehensive user/operator guide for the Cron feature (fields, schedule types, prompt security, execution flow, API, SSE, limits, troubleshooting), sourced from the implementation. - SPEEDRUN.md: fast-execution protocol for Claude grounded in this repo's real commands and CLAUDE.md guardrails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SSVnYek4nq4Ztmbb3SrCA5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Cron feature: saved, named jobs that automatically launch an agent session (Claude / shell / OpenCode / Codex / Gemini) on a recurring schedule and deliver a prompt. It's a trigger + persistence + history layer on top of the existing session primitives — no tmux/PTY logic is reimplemented (fires via the same
create → addSession → setupSessionListeners → startInteractive/startShell → promptflow as quick-start).Deliberately distinct from the legacy
ScheduledRun(/api/scheduled, a run-now duration-bounded loop). The two never interact.What's included
src/cron/cron-service.ts(CRUD, Run Now, 30s due-tick, run history),src/cron/cron-time.ts(pure, unit-tested next-run math), types insrc/types/cron.ts, routes/api/cron/jobs*+/api/cron/runs,CronPort,CronJobSchemawith cross-field validation,cron:*SSE events,AppState.cronJobs/cronJobRunspersistence.cron-ui.js+#cronModal, header ⏰ Cron button.once/interval/daily/weekly(server-local TZ), enable/disable, next-run calc, per-job run history.warn_only/skip_if_same_agent_running(with skip coalescing so a perpetually-skipped job can't bloat state).docs/cron-guide.md(comprehensive user/operator guide), plusdocs/cron-discovery.md/docs/cron-build-brief.md, andSPEEDRUN.md(fast-execution protocol for Claude).Security hardening
Prompt-file reads (
promptMode: prompt_file_path) are an injectable exfil sink, soresolveSafePromptPath()uses an allowlist: realpath-resolve → confine to the job'sworkingDir→ require a regular file → 1 MiB cap, with a blocklist as defense-in-depth. Closes findings from an adversarial review round (/proc/self/environenv-leak,/dev/*unbounded-read DoS, once-job re-arm, partial-update validation gap, skipped-run history).Tests
test/cron-time.test.ts— schedule mathtest/cron-service.test.ts— CRUD, tick, concurrency, security43/43 passing locally;
tsc/lint/format/frontend-syntax green.🤖 Generated with Claude Code