From 22e1aae44b3632bbce6c08277fed17e7dee87c14 Mon Sep 17 00:00:00 2001 From: charlie0228 <24387287+charlie0228@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:49:07 +0000 Subject: [PATCH 1/5] docs(cron): add security considerations for cron trust bypass --- docs/cronjob.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/cronjob.md b/docs/cronjob.md index 4aa0231ad..409f9147f 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -321,6 +321,26 @@ usercron_enabled = true usercron_path = "cronjob.toml" ``` +## Security Considerations + +Cron jobs are system-initiated. When a job fires, the scheduler hands the prompt directly to the message router instead of going through a platform adapter, so a cron prompt is **not** subject to the inbound checks that apply to chat messages: + +- the platform trust gate (`allowed_users` / `allowed_channels`) +- @mention requirements +- bot-message filtering + +This is by design — scheduled jobs may target channels outside the human-facing allowlist (see the [Identity Trust-None ADR](adr/identity-trust-none.md)). The prompt runs with the same tool permissions as any other session (e.g. `--trust-all-tools`), and the message text is forwarded verbatim, so agent CLI commands such as `/clear` or `/model ` are executed as well. + +The practical consequence: **whoever can write the cron configuration can run unattended prompts as the agent.** For baseline `[[cron.jobs]]`, that is whoever controls `config.toml`. For usercron, it is whoever can write `cronjob.toml` — which, with [Agent-Managed Schedules](#agent-managed-schedules), includes the agent itself. + +If your agent reads untrusted content (web pages, RSS feeds, transcripts, messages from other users), a prompt injection that persuades the agent to append a `[[jobs]]` entry turns a one-off injection into a recurring one that looks like a legitimate schedule. + +Mitigations: + +- Leave `usercron_enabled = false` unless you need agent-managed schedules. +- Review changes to `cronjob.toml` — for example, track it in git and diff it periodically, or watch it with a file monitor such as `inotifywait`. +- Put long-lived schedules in baseline `[[cron.jobs]]`, stored where the agent cannot write (e.g. a read-only mount or a Kubernetes ConfigMap). + ## Behaviors - **Minute-aligned**: The scheduler aligns to minute boundaries (`:00`), so `0 9 * * *` fires at exactly 09:00:00, not at whatever second the process started. From d34039a1173d93c6f31288b25085d902a2b2e6ab Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Mon, 5 Oct 2026 03:17:27 +0000 Subject: [PATCH 2/5] docs(cron): frame cron trust bypass as autonomy scope, cover disable_on_success Reframe the section as "Autonomy and Permission Scope": cron skipping inbound chat checks is intentional and is how agents act autonomously. Document that usercron write access also grants scheduled shell execution via disable_on_success (run by OpenAB, outside agent tool approval), and replace the mitigation list with three explicit scope choices. Drop the unverified /model example. Builds on #1560. Co-authored-by: charlie0228 <24387287+charlie0228@users.noreply.github.com> Co-authored-by: thoththeagent <287470606+thoththeagent@users.noreply.github.com> --- docs/cronjob.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/cronjob.md b/docs/cronjob.md index 409f9147f..4fb1cefe1 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -321,7 +321,11 @@ usercron_enabled = true usercron_path = "cronjob.toml" ``` -## Security Considerations +## Autonomy and Permission Scope + +Cron is how OpenAB gives an agent a degree of autonomy: it can act on a schedule without a human in the loop, and with [Agent-Managed Schedules](#agent-managed-schedules) it can decide its own schedule. This section describes exactly what that capability covers, so you can decide how much of it to grant. + +### Cron prompts skip inbound chat checks (by design) Cron jobs are system-initiated. When a job fires, the scheduler hands the prompt directly to the message router instead of going through a platform adapter, so a cron prompt is **not** subject to the inbound checks that apply to chat messages: @@ -329,17 +333,28 @@ Cron jobs are system-initiated. When a job fires, the scheduler hands the prompt - @mention requirements - bot-message filtering -This is by design — scheduled jobs may target channels outside the human-facing allowlist (see the [Identity Trust-None ADR](adr/identity-trust-none.md)). The prompt runs with the same tool permissions as any other session (e.g. `--trust-all-tools`), and the message text is forwarded verbatim, so agent CLI commands such as `/clear` or `/model ` are executed as well. +This is intentional: scheduled jobs have no external sender and may target channels outside the human-facing allowlist (see the [Identity Trust-None ADR](adr/identity-trust-none.md)). The prompt runs with the same tool permissions as any other session (e.g. `--trust-all-tools`), and the message text is forwarded verbatim, so agent CLI commands such as `/clear` are executed as well. + +### What write access to the cron configuration grants + +Writing the cron configuration is equivalent to scheduling unattended work: + +| Config | Who can write it | What a job can do | +|--------|------------------|-------------------| +| Baseline `[[cron.jobs]]` in `config.toml` | Whoever controls `config.toml` | Send a recurring prompt to the agent | +| Usercron `[[jobs]]` in `cronjob.toml` | Whoever can write `cronjob.toml`, including the agent itself with Agent-Managed Schedules | Send a recurring prompt to the agent, **and** run a shell command via [`disable_on_success`](#goal-driven-auto-disable) | + +`disable_on_success` is executed by OpenAB itself (`sh -c` on Unix, `cmd /C` on Windows) on every schedule match, before the prompt is sent. It does not go through the agent, so the agent CLI's tool-approval settings do not apply to it. An agent that can write `cronjob.toml` can therefore run shell commands on a schedule, even if its own shell tool is restricted. -The practical consequence: **whoever can write the cron configuration can run unattended prompts as the agent.** For baseline `[[cron.jobs]]`, that is whoever controls `config.toml`. For usercron, it is whoever can write `cronjob.toml` — which, with [Agent-Managed Schedules](#agent-managed-schedules), includes the agent itself. +If your agent reads untrusted content (web pages, RSS feeds, transcripts, messages from other users), keep in mind that a prompt injection which persuades the agent to add a `[[jobs]]` entry becomes recurring and looks like a legitimate schedule. -If your agent reads untrusted content (web pages, RSS feeds, transcripts, messages from other users), a prompt injection that persuades the agent to append a `[[jobs]]` entry turns a one-off injection into a recurring one that looks like a legitimate schedule. +### Choosing the right scope -Mitigations: +- **Full autonomy**: enable usercron and let the agent manage `cronjob.toml`. Best for a personal agent whose inputs you trust. +- **Agent-managed prompts, operator-managed commands**: keep usercron enabled, but restrict the agent's file-write tools from `$HOME/.openab/` (or require approval for that path) where your agent CLI supports path-scoped permissions. Add `disable_on_success` entries yourself. +- **Operator-managed only**: leave `usercron_enabled = false` and put schedules in baseline `[[cron.jobs]]`, stored where the agent cannot write (e.g. a read-only mount or a Kubernetes ConfigMap). -- Leave `usercron_enabled = false` unless you need agent-managed schedules. -- Review changes to `cronjob.toml` — for example, track it in git and diff it periodically, or watch it with a file monitor such as `inotifywait`. -- Put long-lived schedules in baseline `[[cron.jobs]]`, stored where the agent cannot write (e.g. a read-only mount or a Kubernetes ConfigMap). +Whichever scope you choose, tracking `cronjob.toml` in git or watching it with a file monitor such as `inotifywait` gives you an audit trail. This is detective, not preventive: changes are hot-reloaded and take effect within about a minute. ## Behaviors From d2ef9d24c675151874631eb496df986d1b9168e5 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Mon, 5 Oct 2026 16:46:03 +0000 Subject: [PATCH 3/5] docs(cron): address review round-1 findings on autonomy scope - Drop the unenforceable "agent-managed prompts, operator-managed commands" scope; explain prompts and commands share one file and note absolute usercron_path and shell bypass of path rules (F1) - State that disable_on_success runs as the OpenAB process user, outside tool approval and any agent sandbox; spell out the full injection chain in a CAUTION callout (F2, F6) - Distinguish persistence (trust-all-tools) from new privilege (restricted or sandboxed agent) (F3) - Clarify only the inbound adapter path is bypassed and that slash command handling is backend-specific (F4) - Make audit advice durable: commit or persist events outside the agent's writable scope (F5) - Replace the table with prose and add a CAUTION pointer in Goal-Driven Auto-Disable (F7) --- docs/cronjob.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/cronjob.md b/docs/cronjob.md index 4fb1cefe1..94d233ee5 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -298,6 +298,9 @@ Execution flow: `disable_on_success` is supported only in usercron `[[jobs]]`, not baseline `[[cron.jobs]]`. This keeps scheduler writeback limited to the user-managed cron file. +> [!CAUTION] +> `disable_on_success` is run by OpenAB as its own process user, outside the agent's tool approval. Anyone who can write `cronjob.toml`, including the agent, can schedule shell commands this way. See [Autonomy and Permission Scope](#autonomy-and-permission-scope). + ### Re-enabling a Disabled Job Once a goal is achieved and the job is disabled, re-enable it by editing `$HOME/.openab/cronjob.toml`: @@ -323,38 +326,40 @@ usercron_path = "cronjob.toml" ## Autonomy and Permission Scope -Cron is how OpenAB gives an agent a degree of autonomy: it can act on a schedule without a human in the loop, and with [Agent-Managed Schedules](#agent-managed-schedules) it can decide its own schedule. This section describes exactly what that capability covers, so you can decide how much of it to grant. +Cron is how OpenAB gives an agent a degree of autonomy: it can act on a schedule without a human in the loop, and with [Agent-Managed Schedules](#agent-managed-schedules) it can decide its own schedule. This section describes what that capability covers, so you can decide how much of it to grant. ### Cron prompts skip inbound chat checks (by design) -Cron jobs are system-initiated. When a job fires, the scheduler hands the prompt directly to the message router instead of going through a platform adapter, so a cron prompt is **not** subject to the inbound checks that apply to chat messages: +Cron jobs are system-initiated. When a job fires, the scheduler hands the prompt directly to the message router, bypassing the platform adapter's inbound event path (replies are still sent through the adapter). A cron prompt is therefore **not** subject to the inbound checks that apply to chat messages: - the platform trust gate (`allowed_users` / `allowed_channels`) - @mention requirements - bot-message filtering -This is intentional: scheduled jobs have no external sender and may target channels outside the human-facing allowlist (see the [Identity Trust-None ADR](adr/identity-trust-none.md)). The prompt runs with the same tool permissions as any other session (e.g. `--trust-all-tools`), and the message text is forwarded verbatim, so agent CLI commands such as `/clear` are executed as well. +This is intentional: scheduled jobs have no external sender and may target channels outside the human-facing allowlist (see the [Identity Trust-None ADR](adr/identity-trust-none.md)). The prompt runs with the same tool permissions as any other session. OpenAB forwards the message text verbatim and does not interpret it; depending on the agent CLI, text such as `/clear` may be handled as a command (kiro-cli does). -### What write access to the cron configuration grants +### What write access to `cronjob.toml` grants -Writing the cron configuration is equivalent to scheduling unattended work: +Baseline `[[cron.jobs]]` in `config.toml` can only send prompts. Usercron `[[jobs]]` in `cronjob.toml` can also run a shell command through [`disable_on_success`](#goal-driven-auto-disable), and with Agent-Managed Schedules the agent itself can write `cronjob.toml`. -| Config | Who can write it | What a job can do | -|--------|------------------|-------------------| -| Baseline `[[cron.jobs]]` in `config.toml` | Whoever controls `config.toml` | Send a recurring prompt to the agent | -| Usercron `[[jobs]]` in `cronjob.toml` | Whoever can write `cronjob.toml`, including the agent itself with Agent-Managed Schedules | Send a recurring prompt to the agent, **and** run a shell command via [`disable_on_success`](#goal-driven-auto-disable) | +`disable_on_success` is executed by OpenAB itself (`sh -c` on Unix, `cmd /C` on Windows) on every schedule match (when `disable_on_success_match` is set), before the prompt is sent. It runs as the OpenAB process user, outside the agent's tool approval and any agent sandbox, and inherits OpenAB's filesystem, network access, credentials, and mounts. Run OpenAB with least privilege. -`disable_on_success` is executed by OpenAB itself (`sh -c` on Unix, `cmd /C` on Windows) on every schedule match, before the prompt is sent. It does not go through the agent, so the agent CLI's tool-approval settings do not apply to it. An agent that can write `cronjob.toml` can therefore run shell commands on a schedule, even if its own shell tool is restricted. +How much this adds depends on your setup: -If your agent reads untrusted content (web pages, RSS feeds, transcripts, messages from other users), keep in mind that a prompt injection which persuades the agent to add a `[[jobs]]` entry becomes recurring and looks like a legitimate schedule. +- If the agent already has unrestricted shell access (for example `--trust-all-tools`, as in the [example above](#minimal-configtoml-example)), `disable_on_success` adds unattended persistence, not new privilege. +- If the agent's shell tool is restricted, or the agent runs sandboxed with less privilege than the OpenAB process, but it can still write `cronjob.toml`, then `disable_on_success` grants shell execution the agent would not otherwise have. + +> [!CAUTION] +> If your agent reads untrusted content (web pages, RSS feeds, transcripts, messages from other users), a prompt injection that persuades it to add a `[[jobs]]` entry with `disable_on_success` gains recurring shell execution as the OpenAB process user, without agent tool approval, and the entry looks like a legitimate schedule. ### Choosing the right scope +Prompts and `disable_on_success` live in the same file, so write access cannot be split by field. Choose one of: + - **Full autonomy**: enable usercron and let the agent manage `cronjob.toml`. Best for a personal agent whose inputs you trust. -- **Agent-managed prompts, operator-managed commands**: keep usercron enabled, but restrict the agent's file-write tools from `$HOME/.openab/` (or require approval for that path) where your agent CLI supports path-scoped permissions. Add `disable_on_success` entries yourself. -- **Operator-managed only**: leave `usercron_enabled = false` and put schedules in baseline `[[cron.jobs]]`, stored where the agent cannot write (e.g. a read-only mount or a Kubernetes ConfigMap). +- **Operator-managed only**: leave `usercron_enabled = false` and put schedules in baseline `[[cron.jobs]]`, or keep usercron but make the file unwritable by the agent at the filesystem level (a read-only mount, a different file owner, or a Kubernetes ConfigMap). `usercron_path` may be absolute, so protect the file it actually points to. Agent CLI path rules alone are not enough, because an agent with a shell tool can bypass them. -Whichever scope you choose, tracking `cronjob.toml` in git or watching it with a file monitor such as `inotifywait` gives you an audit trail. This is detective, not preventive: changes are hot-reloaded and take effect within about a minute. +For an audit trail, commit changes to version control or persist file-monitor events (e.g. from `inotifywait`) to a location outside the agent's writable scope. This is detective, not preventive: changes are hot-reloaded and take effect within about a minute. ## Behaviors From 49517de4dd42d5f2119eda7cedc4ea807f0c3e7a Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Mon, 5 Oct 2026 16:51:00 +0000 Subject: [PATCH 4/5] docs(cron): protect containing directory and note writeback limits An agent that can write the directory can replace a file it does not own, and a read-only usercron file blocks enabled=false / thread_id writeback, so achieved goal jobs keep firing. --- docs/cronjob.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cronjob.md b/docs/cronjob.md index 94d233ee5..8f3f11e32 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -357,7 +357,7 @@ How much this adds depends on your setup: Prompts and `disable_on_success` live in the same file, so write access cannot be split by field. Choose one of: - **Full autonomy**: enable usercron and let the agent manage `cronjob.toml`. Best for a personal agent whose inputs you trust. -- **Operator-managed only**: leave `usercron_enabled = false` and put schedules in baseline `[[cron.jobs]]`, or keep usercron but make the file unwritable by the agent at the filesystem level (a read-only mount, a different file owner, or a Kubernetes ConfigMap). `usercron_path` may be absolute, so protect the file it actually points to. Agent CLI path rules alone are not enough, because an agent with a shell tool can bypass them. +- **Operator-managed only**: leave `usercron_enabled = false` and put schedules in baseline `[[cron.jobs]]`, or keep usercron but make the file unwritable by the agent at the filesystem level (a read-only mount, a Kubernetes ConfigMap, or a file and containing directory owned by a different user than the agent runs as). Write access to the directory alone is enough to replace the file, and `usercron_path` may be absolute, so protect the file and directory it actually points to. Agent CLI path rules alone are not enough, because an agent with a shell tool can bypass them. A protected file also blocks scheduler writeback, so avoid `disable_on_success` (and `thread_id` persistence) there: an achieved goal cannot be written back as `enabled = false` and the job keeps firing. For an audit trail, commit changes to version control or persist file-monitor events (e.g. from `inotifywait`) to a location outside the agent's writable scope. This is detective, not preventive: changes are hot-reloaded and take effect within about a minute. From 7accd5f45d7b2bade39f12a9b90b3689c6fa7211 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Mon, 5 Oct 2026 20:32:41 +0000 Subject: [PATCH 5/5] docs(cron): address round-2 supplemental review findings - disable_on_success inherits OpenAB's full environment (bot tokens), unlike the env-cleared agent process; fix the trust-all-tools bullet and the injection CAUTION (S1) - describe the load-time drop when id or disable_on_success_match is missing (F6) - writeback fails only when OpenAB cannot write the directory; state the shared-uid default, sticky bit, and thread_id/id guidance (S3) - describe bypassed inbound filters by category, including allow_dm (S2) - mark kiro-cli /clear handling as an example (F2) - require audit records outside agent reach, not local git (F5) - refer to the resolved usercron_path for writeback and re-enable --- docs/cronjob.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/cronjob.md b/docs/cronjob.md index 8f3f11e32..653d52e76 100644 --- a/docs/cronjob.md +++ b/docs/cronjob.md @@ -293,7 +293,7 @@ Execution flow: 1. The schedule matches. 2. The scheduler runs `disable_on_success`. -3. If the command exits `0` and output contains `disable_on_success_match`, OpenAB posts `✅ Goal achieved`, writes `enabled = false` back to `$HOME/.openab/cronjob.toml`, and skips the regular prompt. +3. If the command exits `0` and output contains `disable_on_success_match`, OpenAB posts `✅ Goal achieved`, writes `enabled = false` back to the usercron file (`usercron_path`, `$HOME/.openab/cronjob.toml` by default), and skips the regular prompt. 4. Otherwise, OpenAB sends the regular `message` and the agent continues working. `disable_on_success` is supported only in usercron `[[jobs]]`, not baseline `[[cron.jobs]]`. This keeps scheduler writeback limited to the user-managed cron file. @@ -303,7 +303,7 @@ Execution flow: ### Re-enabling a Disabled Job -Once a goal is achieved and the job is disabled, re-enable it by editing `$HOME/.openab/cronjob.toml`: +Once a goal is achieved and the job is disabled, re-enable it by editing the usercron file (`$HOME/.openab/cronjob.toml` by default, or the resolved `usercron_path`): ```toml # Flip back to true to restart the job @@ -330,36 +330,45 @@ Cron is how OpenAB gives an agent a degree of autonomy: it can act on a schedule ### Cron prompts skip inbound chat checks (by design) -Cron jobs are system-initiated. When a job fires, the scheduler hands the prompt directly to the message router, bypassing the platform adapter's inbound event path (replies are still sent through the adapter). A cron prompt is therefore **not** subject to the inbound checks that apply to chat messages: +Cron jobs are system-initiated. When a job fires, the scheduler hands the prompt directly to the message router, bypassing the platform adapter's inbound event path (replies are still sent through the adapter). A cron prompt therefore never enters the adapter's event handler, and none of the adapter's inbound filters apply to it. For example: -- the platform trust gate (`allowed_users` / `allowed_channels`) +- user, channel, and role allowlists (`allowed_users`, `allowed_channels`, and similar) +- DM gating (`allow_dm`, where supported) - @mention requirements - bot-message filtering -This is intentional: scheduled jobs have no external sender and may target channels outside the human-facing allowlist (see the [Identity Trust-None ADR](adr/identity-trust-none.md)). The prompt runs with the same tool permissions as any other session. OpenAB forwards the message text verbatim and does not interpret it; depending on the agent CLI, text such as `/clear` may be handled as a command (kiro-cli does). +This is intentional: scheduled jobs have no external sender and may target channels outside the human-facing allowlist (see the [Identity Trust-None ADR](adr/identity-trust-none.md)). The prompt runs with the same tool permissions as any other session. OpenAB forwards the message text verbatim and does not interpret it; depending on the agent CLI, text such as `/clear` may be handled as a command (for example, kiro-cli does). ### What write access to `cronjob.toml` grants Baseline `[[cron.jobs]]` in `config.toml` can only send prompts. Usercron `[[jobs]]` in `cronjob.toml` can also run a shell command through [`disable_on_success`](#goal-driven-auto-disable), and with Agent-Managed Schedules the agent itself can write `cronjob.toml`. -`disable_on_success` is executed by OpenAB itself (`sh -c` on Unix, `cmd /C` on Windows) on every schedule match (when `disable_on_success_match` is set), before the prompt is sent. It runs as the OpenAB process user, outside the agent's tool approval and any agent sandbox, and inherits OpenAB's filesystem, network access, credentials, and mounts. Run OpenAB with least privilege. +`disable_on_success` is executed by OpenAB itself (`sh -c` on Unix, `cmd /C` on Windows) on every schedule match, before the prompt is sent. A usercron job that sets `disable_on_success` must also set a non-empty `id` and `disable_on_success_match`. If either is missing, the whole entry is dropped at load time with only a warning in the logs, and the job never fires. Jobs that do not set `disable_on_success` do not need `id` for this. + +The command runs as the OpenAB process user, outside the agent's tool approval and any agent sandbox. It inherits OpenAB's filesystem, network access, mounts, and full environment, which often includes platform bot tokens (for example `bot_token = "${DISCORD_BOT_TOKEN}"`) and other credentials. The agent's own process does not get these: OpenAB clears the environment when it starts the agent and passes through only a few baseline variables plus `[agent].env` and `inherit_env`. Run OpenAB under a dedicated account, and give its environment and mounts only the credentials OpenAB itself needs. How much this adds depends on your setup: -- If the agent already has unrestricted shell access (for example `--trust-all-tools`, as in the [example above](#minimal-configtoml-example)), `disable_on_success` adds unattended persistence, not new privilege. +- If the agent already has unrestricted shell access (for example `--trust-all-tools`, as in the [example above](#minimal-configtoml-example)), `disable_on_success` adds unattended persistence **and** access to OpenAB's environment, which the agent's shell does not have. For example, `disable_on_success = "env > /tmp/openab-env"` can expose a bot token to the agent. - If the agent's shell tool is restricted, or the agent runs sandboxed with less privilege than the OpenAB process, but it can still write `cronjob.toml`, then `disable_on_success` grants shell execution the agent would not otherwise have. > [!CAUTION] -> If your agent reads untrusted content (web pages, RSS feeds, transcripts, messages from other users), a prompt injection that persuades it to add a `[[jobs]]` entry with `disable_on_success` gains recurring shell execution as the OpenAB process user, without agent tool approval, and the entry looks like a legitimate schedule. +> If your agent reads untrusted content (web pages, RSS feeds, transcripts, messages from other users), a prompt injection that persuades it to add a `[[jobs]]` entry with `disable_on_success` gains recurring shell execution as the OpenAB process user, with OpenAB's credentials and without agent tool approval, and the entry looks like a legitimate schedule. With a bot token from that environment, the command can act as the bot in any channel the bot can reach, including channels that `allowed_users` / `allowed_channels` exclude. The inbound allowlist does not cover this path. ### Choosing the right scope Prompts and `disable_on_success` live in the same file, so write access cannot be split by field. Choose one of: - **Full autonomy**: enable usercron and let the agent manage `cronjob.toml`. Best for a personal agent whose inputs you trust. -- **Operator-managed only**: leave `usercron_enabled = false` and put schedules in baseline `[[cron.jobs]]`, or keep usercron but make the file unwritable by the agent at the filesystem level (a read-only mount, a Kubernetes ConfigMap, or a file and containing directory owned by a different user than the agent runs as). Write access to the directory alone is enough to replace the file, and `usercron_path` may be absolute, so protect the file and directory it actually points to. Agent CLI path rules alone are not enough, because an agent with a shell tool can bypass them. A protected file also blocks scheduler writeback, so avoid `disable_on_success` (and `thread_id` persistence) there: an achieved goal cannot be written back as `enabled = false` and the job keeps firing. +- **Operator-managed only**: leave `usercron_enabled = false` and put schedules in baseline `[[cron.jobs]]`, or keep usercron but make the file unwritable by the agent at the filesystem level. By default the agent runs as the same OS user as OpenAB, so anything the agent cannot write, OpenAB cannot write either, unless you run them as different users. Options: + - A read-only mount or a Kubernetes ConfigMap. OpenAB cannot write the file either. + - A file **and** containing directory owned by a different user than the agent runs as. Write access to the directory alone is enough to replace the file, so making only the file read-only protects nothing. If the directory is shared-writable (such as `/tmp`), also set the sticky bit. + + `usercron_path` may be absolute, so protect the file and directory it actually points to. Agent CLI path rules alone are not enough, because an agent with a shell tool can bypass them. + + OpenAB rewrites the file through a temporary file in the same directory, so scheduler writeback fails whenever OpenAB itself cannot write the file's directory. In that case, do not use `disable_on_success`: an achieved goal cannot be written back as `enabled = false`, so the command keeps running and `✅ Goal achieved` is posted on every schedule match. Also avoid jobs that set `id` but leave `thread_id` unset: the new thread's ID cannot be saved, so a new thread is created on every run. Instead, set a real `thread_id`, or omit `id` if you want a new thread per run. -For an audit trail, commit changes to version control or persist file-monitor events (e.g. from `inotifywait`) to a location outside the agent's writable scope. This is detective, not preventive: changes are hot-reloaded and take effect within about a minute. +For an audit trail, record changes somewhere the agent cannot write: push commits to a protected remote, or persist file-monitor events (e.g. from `inotifywait`) to an external or append-only store. A local git repository is not enough, because an agent with a shell tool can rewrite its history. This is detective, not preventive: changes are hot-reloaded and take effect within about a minute. ## Behaviors