Rascal has three runtime parts and a small set of internal abstractions that keep control-plane orchestration, execution, and persistence separate.
The easiest way to read Rascal is to separate control-plane responsibilities from execution-plane responsibilities.
- Control plane:
rascalandrascald - Execution plane: detached runner containers launched via the Docker launcher
- Runtimes:
goose-codex,codex,claude, andgoose-claude - Harnesses (derived from runtime):
gooseanddirect - Model providers (derived from runtime):
codexandanthropic - Packaging: separate runner images per runtime (Goose-Codex, Codex, Claude, Goose-Claude)
In simple terms:
- A Task is the long-lived unit of work.
- A Run is one attempt to advance that task.
- A Task tracks the runtime selected for its latest run.
- A Task may have one current task-scoped session record.
- A Run uses the runtime recorded on that run and may resume the task's session when the runtime still matches.
- A detached container is
RunExecutionstate for a run, not the run itself.
This split is important during deploys and restarts:
- Blue/green is control-plane topology for
rascald. - Active work keeps running in detached containers in the execution plane.
- After cutover or restart, the active slot recovers and adopts detached run supervision.
rascal (CLI) or GitHub webhook
|
v
rascald control plane
- create/update task
- create run
- persist state
- schedule/supervise
|
v
Docker launcher / execution plane
- start detached runner container
- inspect / stop / remove container
|
v
rascal-runner in container
- clone repo
- run goose or codex
- write artifacts and meta.json
- push branch / update PR
|
v
rascald finalization
- read artifacts
- update run/task state
- post GitHub status/comments
rascal(CLI)
- Local operator interface.
- Handles init, deploy, config, run creation, logs, and control commands.
- Lives in
cmd/rascal.
rascald(orchestrator server)
- Receives API requests and GitHub webhooks.
- Persists task, run, lease, cancellation, and detached execution state.
- Schedules runs serially per task and concurrently across different tasks.
- Starts detached runner containers and supervises them via persisted execution handles.
- Supports blue/green slot handoff by letting the previous slot drain for one generation and reclaiming the oldest drainer on the next deploy when needed.
- Lives in
cmd/rascald.
- Runner container (
rascal-runner)
- Clones the repository and checks out the target branches.
- Executes the selected runtime (
goose-codex,codex,claude, orgoose-claude). - Commits changes, pushes the head branch, and creates or reuses a PR.
- Writes canonical artifacts into mounted
/rascal-meta. - Runtime logic lives in Go in
cmd/rascal-runner. runner/entrypoint.shis a thin shim that only executes/usr/local/bin/rascal-runner.
These are the main layers in the Go codebase.
- Entry points
cmd/rascal: operator-facing CLI.cmd/rascald: HTTP API, webhook handling, scheduling, supervision, recovery.cmd/rascal-runner: in-container task executor.
- Agent abstraction
internal/runtimedefinesRuntime,Harness,ModelProvider, andSessionMode.Runtimeis the user-facing selection;HarnessandModelProviderare derived from it viaRuntime.Harness()andRuntime.Provider()methods.
- Execution abstraction
internal/runnerdefines theRunnerlauncher interface andSpec/ExecutionHandlecontract.- Current production implementation is Docker;
noopexists for non-runtime/test scenarios. - Session mounting is harness-aware: Goose uses
GOOSE_PATH_ROOT, Codex usesCODEX_HOME, Claude usesCLAUDE_CONFIG_DIR.
- Control-plane and client boundaries
internal/githubis the single deep GitHub boundary for API calls, webhook payload interpretation helpers, and comment rendering helpers.internal/apiclientowns the CLI transport layer for HTTP and SSH-backed requests torascald.internal/clientconfigowns client config load/save/effective-resolution behavior.internal/remoteowns shared SSH/SCP/shell-quoting primitives reused by CLI and deploy flows.
- Persistence abstraction
internal/stateowns SQLite-backed persistence and state transitions.- It stores runs, tasks, run leases, detached run executions, cancel requests, webhook deliveries, task agent session records, and encrypted stored credentials plus credential leases.
- SQL schema lives in embedded migrations and typed queries are generated under
internal/state/sqlitegen.
- Supporting integrations
internal/runsummary: PR body and completion comment formatting.internal/logs: tailing run log files.
- Rascal builds and deploys one orchestrator binary:
rascald. - Rascal also builds one runner binary:
rascal-runner. - That runner binary is packaged into separate Docker images for Goose and Codex.
rascaldselects the runner image based on the task/run runtime.- Blue/green deploy replaces the control plane, while runner containers remain detached in the execution plane.
- User triggers a run from the CLI or via GitHub webhook.
rascaldcreates or updates task context, writes run artifacts, and queues the run.- Scheduler claims a queued run, enforces per-task serialization, and records a run lease.
rascaldresolves runtime/session settings and persists a deterministic detached execution handle.internal/runnerstarts a detached Docker container forrascal-runner.- Active slot supervises the detached execution by inspect/stop/remove operations and lease heartbeats.
- On slot rotation or process restart, a new slot can recover the persisted handle and adopt supervision.
rascal-runnerfinalizesmeta.json;rascaldreads that artifact, updates run/task state, posts GitHub reactions/comments, and removes the container.- User monitors via
ps,logs, andopen.
Task and run lifecycle:
Task created or reused
|
+--> Run queued --> Run running --> review | succeeded | failed | canceled
|
+--> detached RunExecution created and supervised
Deploy and recovery lifecycle:
active slot A running
|
+--> if slot B is still draining from an earlier deploy, reclaim B
+--> deploy prepares slot B
+--> slot B passes readiness
+--> traffic flips to B
+--> slot A enters deploy-drain and may keep supervising active runs
+--> later deploy or restart may reclaim/adopt remaining executions
- A run belongs to exactly one task.
- A run uses the runtime recorded on that run.
- A task may have at most one current task-scoped session record.
- Changing a task runtime must discard incompatible task-scoped session resume state before the next run starts.
- At most one orchestrator instance should own a run lease at a time.
run_executionsstore detached execution metadata, not user-visible business progress.- Only the active slot should process webhook traffic during blue/green overlap.
Persistent state is stored on the server in a SQLite database under the Rascal data directory.
By default, task-scoped session state is also stored on disk under
${RASCAL_DATA_DIR}/agent-sessions/<task-key>/.
Runs stay short-lived. Each run mounts its run directory plus, when session resume is enabled, a task-scoped session directory. There is no always-on background worker.
Key persisted entities:
runs: user-visible execution records and final outcome.tasks: long-lived task identity across retries and follow-up feedback; API responses include a derivedpending_inputflag (computed from queued runs, not stored as a task column).run_leases: supervision ownership and heartbeat expiry.run_executions: detached execution handle metadata for adoption and cleanup.run_cancels: persisted cancel intent.task sessions: stable harness session identifiers and mounted session roots. In the current SQLite schema this data lives in thetask_sessionstable.credentials: encrypted stored credential payloads and allocation metadata.credential_leases: per-run credential assignments and lease expiry state.deliveries: webhook dedupe/claim bookkeeping.
| Location | What lives there | Notes |
|---|---|---|
| SQLite state DB | tasks, runs, leases, execution handles, sessions, credentials | Primary control-plane source of truth |
| Run directory | per-run artifacts, logs, meta.json, transient auth material |
Short-lived execution artifacts |
| Task session directory | resumable harness session state | Optional and task-scoped |
| Docker runtime | detached runner container process state | Execution-plane state, not the system of record |
| Caddy and systemd config on host | active slot routing and service activation | Deployment/control-plane topology |
| Object | Source of truth | Why |
|---|---|---|
| Task | tasks table |
Durable unit of work across iterations |
| Run | runs table |
User-visible attempt and final outcome |
| Active supervision owner | run_leases table |
Coordinates which rascald instance supervises |
| Detached container identity | run_executions table |
Enables adoption and cleanup across restarts |
| Session resume state | task session records plus mounted session directory | Tracks harness session identity and storage root |
| Run artifacts | run directory on disk | Execution outputs consumed during finalization |
| Live container process | Docker runtime | Actual execution process while the run is active |
Each run directory stores metadata and artifacts such as:
context.jsoninstructions.mdrunner.logagent.ndjson(canonical agent stream log path for all runtimes)agent_output.txt(structured/fallback agent output, especially for Codex)commit_message.txtpr_body.mdmeta.json- SQLite-backed run response targets and completion-comment state, with file fallbacks only for legacy runs flows are used
- Session policy is configured at the orchestrator via
off,pr-only, orall. pr-onlycurrently resumes forpr_comment,pr_synchronize,pr_review,pr_review_comment,pr_review_thread,retry, andissue_edited.- Goose resumes by named Goose session plus mounted session storage.
- Codex resumes by reusing a task-scoped
CODEX_HOMEand the discovered harness session id. - If a task switches runtime between runs, Rascal starts a fresh session for the new runtime and replaces the stored task session record.
- If a Goose resume attempt fails because the stored session is missing or invalid, the runner falls back to a fresh session.
Common failure and recovery cases:
rascaldrestart: persisted run execution handles let the restarted process recover and re-adopt detached runs.- Blue/green deploy during active work: detached containers keep running while the new active slot adopts supervision.
- Missing detached container during adoption: Rascal marks the run failed because execution disappeared before finalization.
- Lease ownership loss: the local instance stops supervision so another instance can take over safely.
- Credential lease renewal failure: Rascal requests cancellation and attempts to stop the detached run.
- Cancel during slot rotation: cancel intent is persisted, and the active slot after cutover should still stop/finalize the run.
Rascal uses stored credentials tagged by provider and managed by rascald.
- Stored credentials are encrypted before being persisted in SQLite.
- Each credential has a
providertag (codexoranthropic) that determines which runtimes can use it:codexcredentials (default, including legacy credentials with empty provider): used bycodexandgoose-codexruntimes viaauth.json.anthropiccredentials: used byclaudeandgoose-clauderuntimes via OAuth token.
- Each credential is either
personal(owned by a user) orshared. - When a run starts,
rascaldasks the credential broker to lease a credential matching the run's runtime and records the selected credential id in state. - The broker chooses from eligible credentials using the configured allocation strategy, provider compatibility filter, and tracks lease assignment per run.
- The leased auth blob is written into a per-run secrets directory outside the
broad
/rascal-metamount and then mounted read-only into the container at/run/rascal-secrets(codex_auth.jsonfor codex/goose runs,claude_oauth_tokenfor claude/goose-claude runs). Legacy run-local auth paths remain as fallback for older runs. - While a run is active,
rascaldrenews the credential lease. If renewal is lost, the run is canceled. - Bootstrap and deploy can seed an initial shared stored credential from a local Codex auth file.
- Operators can manage credentials with
rascal auth credentials ...and use--provider codex|anthropicto tag credentials for specific providers.
Required:
RASCAL_RUN_IDRASCAL_TASK_IDRASCAL_REPOGH_TOKENorGH_TOKEN_FILE
Common optional:
RASCAL_INSTRUCTIONRASCAL_AGENT_RUNTIME(goose-codex,codex,claude, orgoose-claude; defaults togoose-codexwhen unset;gooseis accepted as an alias)RASCAL_BASE_BRANCH(default:main)RASCAL_HEAD_BRANCH(runner fallback default:rascal/<run_id>when unset;rascaldnormally sets a task-derived branch and may reuse the previous head branch for PR comment/review follow-ups)RASCAL_ISSUE_NUMBER(default:0)RASCAL_PR_NUMBER(default:0)RASCAL_TRIGGER(default:cli)RASCAL_GOOSE_DEBUG(default:true)RASCAL_CONTEXTRASCAL_META_DIR(default:/rascal-meta)RASCAL_WORK_ROOT(default:/work)RASCAL_REPO_DIR(default:${RASCAL_WORK_ROOT}/repo)RASCAL_TASK_SESSION_MODE(off,pr-only,all; orchestrator default:all)RASCAL_TASK_SESSION_RESUME(set by orchestrator per run)RASCAL_TASK_SESSION_KEY(stable task-scoped key when resume is enabled)RASCAL_TASK_SESSION_ID(runtime session id when known)CODEX_HOME(run-scoped/rascal-meta/codexin stateless mode, or task-scoped mount in resume mode for Codex)CODEX_AUTH_FILE(default secure mode path:/run/rascal-secrets/codex_auth.json)CLAUDE_CODE_OAUTH_TOKEN_FILE(default secure mode path:/run/rascal-secrets/claude_oauth_token)GOOSE_PATH_ROOT(run-scoped/rascal-meta/goosein stateless mode, or task-scoped mount in resume mode for Goose)