core.cron
Manage system cron jobs via /etc/cron.d/: one rule per file.
core.cron keeps the job file /etc/cron.d/<name> in the required state — a single line <schedule> <user> <command>. present is idempotent by a byte-for-byte comparison of the file's contents; cron picks up changes on its own, no daemon reload is needed. It covers only system-level jobs (user crontab via crontab -u is out of scope); requires root — the write goes to /etc/cron.d/.
Requirements
- Rootrequired
- Side
soul-side - Collection
soulstack.system - Category
system
run_as_rootfs_write_rootStates
core.cron.present — The job file /etc/cron.d/<name> exists with the given schedule and command.
The file did not exist or its contents differ from the target line <schedule> <user> <command> (byte-for-byte comparison).
The file already contains the target line byte-for-byte.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
name | string | required | Job name (= file name in /etc/cron.d, [A-Za-z0-9_-]). |
schedule | string | required | Cron schedule (5 fields), e.g. "0 3 * * *". |
command | string | required | The command run on schedule. |
user | string | optional | User to run as (default root). |
Example — Nightly log rotation
- name: Schedule nightly log rotation module: core.cron.present params: name: app-log-rotate schedule: "0 3 * * *" command: "/usr/local/bin/rotate-logs.sh" user: rootExample — Backup under a dedicated user (privilege minimization)
- name: Schedule nightly backup module: core.cron.present params: name: nightly-backup schedule: "0 3 * * *" command: "/usr/local/bin/backup.sh" user: backupOutput
| Field | Type | Description |
|---|---|---|
name | string | |
path | string | |
installed | true |
core.cron.absent — The job file /etc/cron.d/<name> is removed.
The file existed and was removed.
The file does not exist.
Parameters
| Param | Type | Required / default | Description |
|---|---|---|---|
name | string | required | Job name (= file name in /etc/cron.d). |
Output
| Field | Type | Description |
|---|---|---|
name | string | |
path | string | |
installed | false |
Notes
- The file is written in-process (os.WriteFile / os.Remove), without a shell and without subprocesses; the /etc/cron.d directory is created if needed (minimal containers may not have it). Cron picks up changes in /etc/cron.d/ automatically — no daemon reload is required.
- The final contents are a single line <schedule> <user> <command>\n. present compares it with the file byte-for-byte: changed=true if the file did not exist or the contents differ.
- The file is written with mode 0644 (cron strictly requires that a file in /etc/cron.d/ is not group/world-writable); the module does not change the owner — the assumption is that a production Soul runs as root.
- MVP — only system-level jobs (one rule per file /etc/cron.d/<name>); user crontab (crontab -u) is deferred. The platform is Linux distributions whose cron reads /etc/cron.d/; on systems without this directory (FreeBSD, for example) it cannot be applied — this is controlled by the where: predicate in the scenario, not by the module itself.
Security & defaults
- command is executed by the cron daemon on schedule as user (root by default) — the module's main invariant. schedule, user, and command are substituted into the file as-is, without syntax validation and without escaping: untrusted interpolation into command = deferred execution of someone else's code as user.
- An additional vector: a value with \n in command or schedule will write extra cron lines into the file (the module does not parse the syntax). The source of command / schedule / user must be the Destiny/scenario author, not external input.
- Only the job name is validated: name is limited to [A-Za-z0-9_-] (run-parts compatibility + a guard against path-injection — a dot/slash/.. are rejected before the write). There is no such check on schedule / command / user — the module trusts the task author.
- required_capabilities [run_as_root, fs_write_root] — the write to /etc/cron.d/ goes outside /var/lib/soul-stack and requires UID 0; exec_subprocess is deliberately not declared (the module does not run external binaries). This is a declaration for soul-lint's static check, not a runtime privilege escalation.