Skip to main content

mlua_swarm/core/
config.rs

1//! EngineCfg + LongHoldConfig.
2
3use std::time::Duration;
4
5/// Built-in default for [`EngineCfg::worker_token_ttl_secs`] — 1800s
6/// (30 min). Deliberately short: a worker token is handed out of the
7/// process, so its TTL is the only bound on the capability. See the field
8/// doc for when to raise it.
9pub const DEFAULT_WORKER_TOKEN_TTL_SECS: u64 = 1800;
10
11fn random_token_secret() -> Vec<u8> {
12    let mut buf = vec![0u8; 32];
13    getrandom::fill(&mut buf).expect("OS RNG unavailable");
14    buf
15}
16
17/// Lock acquisition + max-hold guard configuration.
18#[derive(Debug, Clone)]
19pub struct EngineCfg {
20    /// When `true`, `Engine::with_state` never retries on a busy lock — it
21    /// fails fast with `EngineError::LockBusy` on the very first
22    /// `try_lock` miss instead of backing off and retrying.
23    pub try_only: bool,
24    /// Max number of `try_lock` retries before giving up with
25    /// `EngineError::LockBusyAfterRetry` (ignored when `try_only = true`).
26    pub max_retry: u32,
27    /// Linear backoff step (ms) between retries; the sleep duration for
28    /// attempt `n` is `backoff_ms_step * (n + 1)`.
29    pub backoff_ms_step: u64,
30    /// R4 guard threshold: if a single `with_state` closure holds the lock
31    /// longer than this (ms), `with_state` reports a long operation having
32    /// leaked inside the lock in violation of the R3 discipline — it emits
33    /// a `tracing::warn!` with `op` / `elapsed_ms` / `max_hold_ms` and
34    /// continues, unless [`Self::max_hold_panic`] escalates the overrun to
35    /// a panic.
36    pub max_hold_ms: u128,
37    /// When `true`, an R4 overrun panics (after the warn) instead of
38    /// continuing. Off by default in every build: the guard measures
39    /// wall-clock hold time, which on a loaded shared runner includes
40    /// scheduler preemption, so a threshold panic is structurally flaky in
41    /// CI — a trivial closure was observed held for 186ms > 50ms on a
42    /// GitHub macOS runner, and the panic killed the server under test.
43    /// Enable locally when hunting a suspected R3 violation (a long op /
44    /// blocking call leaked inside the lock).
45    pub max_hold_panic: bool,
46    /// HMAC secret used by `TokenSigner` to sign/verify `CapToken`s.
47    ///
48    /// `Default` generates this fresh (32 random bytes from the OS RNG) on
49    /// every call when the caller does not supply one — set it explicitly
50    /// for tokens to stay valid across restarts, or when multiple
51    /// independently-constructed engines/signers must accept each other's
52    /// tokens.
53    /// In the layered credential vocabulary (`mse://guides/auth-token-model`)
54    /// this is a **key, not a token**: it never travels on the wire and
55    /// guards no route — it only lets the server verify what it minted.
56    pub token_secret: Vec<u8>,
57    /// Long-hold session tuning (idle keepalive, heartbeat cadence).
58    pub long_hold: LongHoldConfig,
59    /// TTL (seconds) stamped into every worker capability token the engine
60    /// mints — both the `dispatch_attempt` path and the ordinary-spawn path
61    /// of `dispatch_run_ctx` read this one value.
62    ///
63    /// Unlike an Operator token, a worker token **leaves the process**: it is
64    /// handed to a SubAgent as `Authorization: Bearer <CapToken::encode()>`,
65    /// so nothing downstream can revoke it and this TTL is the only effective
66    /// bound on the capability. That is why it stays short by default
67    /// (`1800` = 30 min) rather than tracking the run TTL.
68    ///
69    /// The flip side: a Step whose SubAgent runs longer than this fails
70    /// authentication mid-flight. When raising the run TTL for long Steps
71    /// (`default_run_ttl` / `sync_timeout_secs`), raise this alongside it —
72    /// a run TTL on its own cannot lift the worker-side ceiling.
73    pub worker_token_ttl_secs: u64,
74    /// Worker recursive spawn depth ceiling (guards against unbounded spawn).
75    ///
76    /// When `Ctx.meta.runtime["spawn_depth"]` has already reached this value
77    /// and a Worker token tries to call `start_task`, the engine raises
78    /// `EngineError::SpawnDepthExceeded`. `0` = root (a task launched
79    /// directly by an Operator); `4` = default, which allows four levels of
80    /// nested sub-tasks.
81    pub max_spawn_depth: u32,
82    /// GH #31: threshold/mode/storage tuning for delivering an oversized
83    /// baked `system_prompt` by reference (`WorkerPayload.system_ref`)
84    /// instead of inline (`WorkerPayload.system`). See
85    /// [`SystemRefConfig`].
86    pub system_ref: SystemRefConfig,
87    /// Policy that governs how submit-time projection sinks react when a
88    /// fail-open condition is encountered (missing `work_dir`/
89    /// `project_root`, `OutputStore` write error, adapter materialize
90    /// error, state lookup error). Default [`CheckPolicy::Warn`]
91    /// preserves the pre-existing warn-and-continue behaviour of every
92    /// call site — see [`CheckPolicy`] for the semantics of the other
93    /// two modes and [`apply_check_policy`](crate::core::engine::apply_check_policy)
94    /// for the shared decision helper. Per-run override is threaded via
95    /// `POST /v1/tasks` (subtask-1c).
96    pub check_policy: CheckPolicy,
97}
98
99// `CheckPolicy` is the Swarm IF SoT type; it lives in the
100// `mlua-swarm-schema` crate (enum relocation) and is
101// re-exported here so every existing path
102// (`crate::core::config::CheckPolicy`, `EngineCfg.check_policy`,
103// `TaskSpec.check_policy`, `apply_check_policy`) keeps its type path
104// unchanged. There is exactly ONE definition — no duplicate enum / `From`
105// bridge — and the wire form (`"silent"` / `"warn"` / `"strict"`) is
106// byte-identical to the pre-relocation form. See the schema crate for the
107// full mode semantics.
108pub use mlua_swarm_schema::CheckPolicy;
109
110impl EngineCfg {
111    /// Strict variant: `try_only = true` (no retry/backoff) and a tight
112    /// `max_hold_ms = 10`. Useful for tests that want lock contention to
113    /// fail fast rather than silently wait.
114    pub fn strict() -> Self {
115        Self {
116            try_only: true,
117            max_retry: 0,
118            backoff_ms_step: 0,
119            max_hold_ms: 10,
120            ..Self::default()
121        }
122    }
123
124    /// Relaxed variant: higher `max_retry` / `backoff_ms_step` /
125    /// `max_hold_ms` than the default, for environments where lock
126    /// contention is expected to be more frequent or operations slower.
127    pub fn relaxed() -> Self {
128        Self {
129            try_only: false,
130            max_retry: 10,
131            backoff_ms_step: 50,
132            max_hold_ms: 200,
133            ..Self::default()
134        }
135    }
136}
137
138impl Default for EngineCfg {
139    /// Baseline configuration: bounded retry with backoff, a generous
140    /// (but non-zero) `max_hold_ms`, `max_spawn_depth = 4`, and
141    /// `worker_token_ttl_secs = 1800` (the pre-config hard-coded value).
142    /// `token_secret` is generated fresh per call — see the field doc.
143    fn default() -> Self {
144        Self {
145            try_only: false,
146            max_retry: 3,
147            backoff_ms_step: 10,
148            max_hold_ms: 50,
149            max_hold_panic: false,
150            token_secret: random_token_secret(),
151            long_hold: LongHoldConfig::default(),
152            worker_token_ttl_secs: DEFAULT_WORKER_TOKEN_TTL_SECS,
153            max_spawn_depth: 4,
154            system_ref: SystemRefConfig::default(),
155            check_policy: CheckPolicy::default(),
156        }
157    }
158}
159
160/// GH #31: server-side config for how a baked `system_prompt` too large
161/// to inline is delivered instead — see `Engine::fetch_worker_payload`'s
162/// threshold branch (`crate::core::engine`) for where this is consumed.
163/// Single server-side setting, not per-request: every fetch of a given
164/// `(task_id, attempt)` sees the same `mode` and `threshold_bytes`.
165#[derive(Debug, Clone)]
166pub struct SystemRefConfig {
167    /// Byte length of the baked `system` string above which
168    /// `Engine::fetch_worker_payload{,_trusted}` switches from inlining
169    /// the value (`WorkerPayload.system`) to a reference
170    /// (`WorkerPayload.system_ref`). Default (`25 * 1024`, 25 KiB)
171    /// matches `bp_doctor`'s existing WARN threshold
172    /// (`AGENT_MD_DEFAULT_WARN_BYTES` in
173    /// `crates/mlua-swarm-cli/src/mcp.rs`) — the same SubAgent
174    /// context-window headroom rationale that threshold documents
175    /// applies here to inline-vs-reference delivery.
176    pub threshold_bytes: usize,
177    /// Which [`crate::types::SystemRefMode`] an over-threshold response
178    /// uses to deliver its content.
179    pub mode: crate::types::SystemRefMode,
180    /// Directory `SystemRefMode::File` writes rendered `system` bodies
181    /// into (`<store_dir>/<task_id>-<attempt>.md`). No eviction policy —
182    /// files accumulate for the process lifetime (explicitly out of
183    /// scope; see the risk note on `Engine::fetch_worker_payload`).
184    pub store_dir: std::path::PathBuf,
185}
186
187impl Default for SystemRefConfig {
188    /// `threshold_bytes = 25 * 1024`, `mode = SystemRefMode::File`,
189    /// `store_dir = std::env::temp_dir().join("mse-system-ref")`.
190    fn default() -> Self {
191        Self {
192            threshold_bytes: 25 * 1024,
193            mode: crate::types::SystemRefMode::File,
194            store_dir: std::env::temp_dir().join("mse-system-ref"),
195        }
196    }
197}
198
199/// Tuning for long-running (suspend/resume-capable) sessions and tasks —
200/// how long a poll/suspend may hold, how often heartbeats are expected,
201/// and whether idle tasks are kept alive across a detach.
202#[derive(Debug, Clone)]
203pub struct LongHoldConfig {
204    /// Default wait duration used by long-poll style waits when the
205    /// caller does not specify one explicitly.
206    pub default_hold: Duration,
207    /// Upper bound on how long a single suspend/poll wait may block.
208    pub max_hold: Duration,
209    /// Expected cadence of `Engine::heartbeat` calls from an attached
210    /// session; consumed by `Engine::start_detach_loop`.
211    pub heartbeat_interval: Duration,
212    /// Number of missed heartbeat intervals tolerated before the detach
213    /// loop flips a session's `attached` flag to `false`.
214    pub heartbeat_miss_threshold: u32,
215    /// When `true`, a task survives (its state is retained) across a
216    /// session detach, so a later reattach can resume it in place.
217    pub keepalive_on_idle: bool,
218}
219
220impl Default for LongHoldConfig {
221    fn default() -> Self {
222        Self {
223            default_hold: Duration::from_secs(3600),
224            max_hold: Duration::from_secs(48 * 3600),
225            heartbeat_interval: Duration::from_secs(300),
226            heartbeat_miss_threshold: 3,
227            keepalive_on_idle: true,
228        }
229    }
230}