Skip to content

11.2 Scheduler Security

Nikolay Vyahhi edited this page Feb 19, 2026 · 2 revisions

Scheduler Security

Relevant source files

The following files were used as context for generating this wiki page:

This page documents how the cron job scheduler enforces security policies during job execution. The scheduler applies the same security controls as interactive agent sessions, including autonomy level checks, command allowlists, rate limiting, and path validation. For cron job configuration and scheduling behavior, see Cron Job Configuration. For the broader security architecture across all subsystems, see Security Model.


Security Architecture Overview

The scheduler's security model implements defense-in-depth by applying multiple independent checks before executing any shell command. Each scheduled job passes through a five-layer validation pipeline that mirrors the security controls used by the shell tool during interactive sessions.

flowchart TD
    Job["Scheduled Job<br/>(CronJob)"] --> Execute["execute_job_with_retry"]
    Execute --> CheckType{Job Type?}
    
    CheckType -->|Shell| RunCommand["run_job_command"]
    CheckType -->|Agent| RunAgent["run_agent_job"]
    
    RunCommand --> Layer1["Layer 1:<br/>can_act()"]
    Layer1 -->|blocked| BlockReadOnly["Return: autonomy is read-only"]
    Layer1 -->|pass| Layer2["Layer 2:<br/>is_rate_limited()"]
    
    Layer2 -->|blocked| BlockRate["Return: rate limit exceeded"]
    Layer2 -->|pass| Layer3["Layer 3:<br/>is_command_allowed()"]
    
    Layer3 -->|blocked| BlockCommand["Return: command not allowed"]
    Layer3 -->|pass| Layer4["Layer 4:<br/>forbidden_path_argument()"]
    
    Layer4 -->|blocked| BlockPath["Return: forbidden path argument"]
    Layer4 -->|pass| Layer5["Layer 5:<br/>record_action()"]
    
    Layer5 -->|budget exhausted| BlockBudget["Return: action budget exhausted"]
    Layer5 -->|pass| SpawnShell["spawn sh -lc command"]
    
    SpawnShell --> Timeout{Timeout?}
    Timeout -->|yes| ReturnTimeout["Return: job timed out"]
    Timeout -->|no| ReturnSuccess["Return: job output"]
    
    BlockReadOnly --> CheckRetry{Retryable?}
    BlockRate --> CheckRetry
    BlockCommand --> CheckRetry
    BlockPath --> CheckRetry
    BlockBudget --> CheckRetry
    
    CheckRetry -->|no| FinalFail["Final Failure"]
    CheckRetry -->|yes| Backoff["Exponential Backoff"]
    Backoff --> Execute
    
    ReturnSuccess --> RecordRun["record_run"]
    ReturnTimeout --> RecordRun
    FinalFail --> RecordRun
Loading

Sources: src/cron/scheduler.rs:52-85, src/cron/scheduler.rs:377-467


SecurityPolicy Integration

The scheduler instantiates a SecurityPolicy from the [autonomy] configuration section and shares it across all job executions. This ensures consistent enforcement of security rules regardless of whether actions originate from interactive sessions, scheduled jobs, or webhook handlers.

Policy Instantiation

Location Pattern Scope
Scheduler Main Loop Arc<SecurityPolicy> created once at startup Shared across all job executions in parallel
Manual Job Execution SecurityPolicy created per invocation Single-use for execute_job_now calls
graph TB
    Config["Config::autonomy"] --> CreatePolicy["SecurityPolicy::from_config"]
    CreatePolicy --> ArcPolicy["Arc<SecurityPolicy>"]
    ArcPolicy --> Loop["Scheduler Loop<br/>process_due_jobs"]
    
    Loop --> Job1["Job 1<br/>execute_job_with_retry"]
    Loop --> Job2["Job 2<br/>execute_job_with_retry"]
    Loop --> JobN["Job N<br/>execute_job_with_retry"]
    
    Job1 --> Check1["run_job_command<br/>security checks"]
    Job2 --> Check2["run_job_command<br/>security checks"]
    JobN --> CheckN["run_job_command<br/>security checks"]
    
    Check1 --> Policy1["security.can_act()"]
    Check1 --> Policy2["security.is_rate_limited()"]
    Check1 --> Policy3["security.is_command_allowed()"]
    Check1 --> Policy4["security.is_path_allowed()"]
    Check1 --> Policy5["security.record_action()"]
Loading

Sources: src/cron/scheduler.rs:21-45, src/cron/scheduler.rs:47-50, src/cron/scheduler.rs:87-101


Five-Layer Security Validation

Each layer enforces a different dimension of the security policy. Failures at any layer result in immediate rejection of the job without executing the command. Security policy violations are not retryable — if a job is blocked by policy, retries are skipped and the job is marked as failed.

Layer 1: Autonomy Level Check (can_act)

Verifies that the current autonomy level permits write operations. Read-only mode blocks all scheduled shell commands.

// src/cron/scheduler.rs:397-401
if !security.can_act() {
    return (
        false,
        "blocked by security policy: autonomy is read-only".to_string(),
    );
}
Autonomy Level Shell Jobs Allowed? Agent Jobs Allowed?
ReadOnly ❌ Blocked ✅ Allowed (read-only tools only)
Supervised ✅ Allowed ✅ Allowed
Full ✅ Allowed ✅ Allowed

Sources: src/cron/scheduler.rs:397-401

Layer 2: Rate Limiting Check (is_rate_limited)

Enforces the max_actions_per_hour budget. Once exhausted, all subsequent job executions are blocked until the rate limit window resets.

// src/cron/scheduler.rs:404-409
if security.is_rate_limited() {
    return (
        false,
        "blocked by security policy: rate limit exceeded".to_string(),
    );
}

The rate limit counter is shared across:

  • Interactive shell tool executions
  • Scheduled cron jobs
  • Tool executions triggered by agent jobs
  • Git operations and other write tools

Sources: src/cron/scheduler.rs:404-409

Layer 3: Command Allowlist Check (is_command_allowed)

Validates that the command's executable is present in the allowed_commands list. The check extracts the first token from the command string and matches it against the allowlist.

// src/cron/scheduler.rs:411-419
if !security.is_command_allowed(&job.command) {
    return (
        false,
        format!(
            "blocked by security policy: command not allowed: {}",
            job.command
        ),
    );
}

Configuration Example:

[autonomy]
allowed_commands = ["sh", "git", "npm", "python3"]

If allowed_commands is empty or not specified, all commands are permitted (deny-by-configuration approach requires explicit allowlist).

Sources: src/cron/scheduler.rs:411-419

Layer 4: Path Argument Validation (forbidden_path_argument)

Parses the shell command to extract path-like arguments and validates each against the security policy's forbidden path list and workspace scoping rules. This prevents scheduled jobs from accessing sensitive system directories even if the command itself is allowlisted.

Parsing Logic

The forbidden_path_argument function implements a multi-stage parser:

  1. Normalization: Replace shell operators (&&, ||, ;, |, newlines) with null bytes to segment commands
  2. Tokenization: Split each segment by whitespace
  3. Environment Variable Skipping: Skip leading KEY=value tokens
  4. Executable Skipping: Skip the first non-environment token (the command itself)
  5. Path Detection: Identify arguments that look like filesystem paths
  6. Validation: Check each path against security.is_path_allowed()
flowchart LR
    Input["Command String"] --> Normalize["Replace operators<br/>with \\0"]
    Normalize --> Split["Split by \\0"]
    Split --> Segment1["Segment 1"]
    Split --> Segment2["Segment 2"]
    
    Segment1 --> Tokenize1["Split whitespace"]
    Tokenize1 --> SkipEnv1["Skip ENV=val tokens"]
    SkipEnv1 --> SkipCmd1["Skip executable token"]
    SkipCmd1 --> Args1["Extract arguments"]
    
    Args1 --> Path1["Path-like?"]
    Path1 -->|yes| Check1["is_path_allowed?"]
    Path1 -->|no| Skip1["Skip"]
    
    Check1 -->|blocked| ReturnForbidden["Return forbidden path"]
    Check1 -->|allowed| Continue["Continue"]
Loading

Path Detection Heuristics:

A token is considered path-like if it:

  • Starts with / (absolute path)
  • Starts with ./ (relative to current directory)
  • Starts with ../ (parent directory)
  • Starts with ~/ (home directory)
  • Contains / anywhere (subdirectory reference)

And it is not:

  • Starting with - (command flag)
  • Containing :// (URL)
  • Wrapped in quotes (stripped before checking)

Sources: src/cron/scheduler.rs:319-375

Layer 5: Action Budget Recording (record_action)

Attempts to decrement the action budget counter. If the budget is already at zero, the command is blocked.

// src/cron/scheduler.rs:428-433
if !security.record_action() {
    return (
        false,
        "blocked by security policy: action budget exhausted".to_string(),
    );
}

This check occurs after all validation layers to ensure that only approved commands consume budget quota.

Sources: src/cron/scheduler.rs:428-433


Command Execution Flow

After passing all five security layers, the scheduler spawns the command as a subprocess with the following characteristics:

Property Value Rationale
Shell sh -lc Login shell with full environment
Working Directory config.workspace_dir Scoped to agent workspace
Stdin Stdio::null() No interactive input
Stdout/Stderr Stdio::piped() Captured for logging
Timeout 120 seconds (default) Prevents runaway jobs
Kill on Drop true Ensures cleanup if task is cancelled
sequenceDiagram
    participant Scheduler
    participant Security as SecurityPolicy
    participant Process as sh -lc
    participant Workspace as workspace_dir
    
    Scheduler->>Security: Pass 5 security layers
    Security-->>Scheduler: Approved
    
    Scheduler->>Process: spawn(sh -lc command)
    Note over Process: stdin: null<br/>stdout: piped<br/>stderr: piped<br/>cwd: workspace_dir
    
    Process->>Workspace: Execute in workspace
    
    alt Completes within timeout
        Process-->>Scheduler: stdout + stderr + exit code
        Scheduler->>Scheduler: Format as output
    else Timeout exceeded
        Scheduler->>Process: Kill process
        Scheduler->>Scheduler: Return timeout error
    end
Loading

Sources: src/cron/scheduler.rs:435-467


Non-Retryable Policy Violations

The scheduler's retry logic distinguishes between transient failures (e.g., network errors, temporary file locks) and deterministic policy violations. Security policy blocks are never retried because they represent configuration mismatches that cannot be resolved by retrying the same operation.

// src/cron/scheduler.rs:72-75
if last_output.starts_with("blocked by security policy:") {
    // Deterministic policy violations are not retryable.
    return (false, last_output);
}
Error Message Prefix Retryable? Reason
blocked by security policy: autonomy is read-only ❌ No Autonomy level is a stable configuration
blocked by security policy: rate limit exceeded ❌ No Rate limit resets on a fixed schedule, not by retrying
blocked by security policy: command not allowed ❌ No Allowlist is a fixed configuration
blocked by security policy: forbidden path argument ❌ No Path validation rules are deterministic
blocked by security policy: action budget exhausted ❌ No Budget exhaustion is a quota constraint, not a transient failure
spawn error: ... ✅ Yes May be transient (e.g., process limit, temporary lock)
job timed out after ... ✅ Yes Command may succeed on retry if system load decreases

Sources: src/cron/scheduler.rs:52-85


Agent Job Security

Agent-type jobs bypass shell command validation but still enforce security controls through the agent's tool execution layer. When a scheduled agent job invokes tools like shell, file_write, or git_operations, each tool independently applies the same SecurityPolicy checks.

graph TB
    AgentJob["Agent Job<br/>(CronJob::Agent)"] --> RunAgent["run_agent_job"]
    RunAgent --> AgentRun["crate::agent::run"]
    
    AgentRun --> ToolCall1["Tool: shell"]
    AgentRun --> ToolCall2["Tool: file_write"]
    AgentRun --> ToolCall3["Tool: git_operations"]
    
    ToolCall1 --> ShellCheck["ShellTool::execute"]
    ShellCheck --> SecurityCheck1["security.can_act()"]
    ShellCheck --> SecurityCheck2["security.is_command_allowed()"]
    ShellCheck --> SecurityCheck3["security.record_action()"]
    
    ToolCall3 --> GitCheck["GitOperationsTool::execute"]
    GitCheck --> GitSecurity1["security.can_act()"]
    GitCheck --> GitSecurity2["security.record_action()"]
    GitCheck --> GitSanitize["sanitize_git_args"]
Loading

Key Differences from Shell Jobs:

  • No Pre-Execution Blocking: Agent jobs are always executed (they don't call can_act() at the scheduler level)
  • Read-Only Mode Allowed: Agent jobs can run in read-only mode, but any write tools they attempt to use will be blocked
  • Tool-Level Enforcement: Each tool applies its own security checks (see src/tools/git_operations.rs:517-548 for an example)

Sources: src/cron/scheduler.rs:119-150, src/tools/git_operations.rs:517-548


Test Coverage

The scheduler security implementation includes comprehensive test coverage across all validation layers:

Test Case Validates Source
run_job_command_blocks_disallowed_command Command allowlist enforcement src/cron/scheduler.rs:554-565
run_job_command_blocks_forbidden_path_argument Path argument validation src/cron/scheduler.rs:568-580
run_job_command_blocks_readonly_mode Autonomy level check src/cron/scheduler.rs:583-594
run_job_command_blocks_rate_limited Rate limiting enforcement src/cron/scheduler.rs:597-608
execute_job_with_retry_recovers_after_first_failure Retry logic for transient failures src/cron/scheduler.rs:611-629
execute_job_with_retry_exhausts_attempts Retry exhaustion handling src/cron/scheduler.rs:632-644

Path Validation Test Examples:

The test suite validates that the command parser correctly identifies path arguments in various contexts:

# Blocked: absolute path argument
cat /etc/passwd

# Blocked: relative path with slashes
python3 ./scripts/dangerous.py

# Allowed: flags are not paths
ls -la --color=auto

# Blocked: path after environment variable
ENV_VAR=value cat /etc/shadow

# Blocked: path in multi-command pipeline
echo ok && cat /etc/hosts

Sources: src/cron/scheduler.rs:513-747


Configuration Reference

The scheduler's security behavior is controlled by the [autonomy] section of config.toml:

[autonomy]
level = "supervised"                    # ReadOnly | Supervised | Full
max_actions_per_hour = 100             # 0 = unlimited
allowed_commands = ["sh", "git", "npm"] # empty = deny all shell commands
workspace_only = true                   # restrict file operations to workspace_dir

Additionally, the [scheduler] section controls retry behavior:

[scheduler]
max_concurrent = 5  # maximum parallel job executions

[reliability]
scheduler_retries = 3          # retry attempts for transient failures
provider_backoff_ms = 1000     # initial backoff for retries

For complete configuration documentation, see Configuration File Reference.

Sources: src/cron/scheduler.rs:21-27, src/cron/scheduler.rs:52-60


Clone this wiki locally