basis_tasks/approve.rs
1//! Approval policy: what a task's recorded spawn request says about
2//! consequential calls, and who answers `Prompt` while a process executes it.
3//!
4//! A task's `approve` mode is durable — recorded in `meta.json` at spawn and
5//! read back at every attach, exactly as `--provider` or `--model` are — so
6//! it lives beside them here rather than in the CLI that merely parses it off
7//! a flag.
8
9use basis::Approver;
10
11/// The shared host policy, under the durable task layer's existing public
12/// name.
13pub use basis_host::ApprovalPolicy as Approve;
14
15/// How a process answers `Approve::Prompt` while it executes a task's turns,
16/// and whether there is anyone there to ask at all.
17///
18/// The executor is whichever process holds a task's attach lock (ADR-0019),
19/// and only that process's own environment knows whether a person — or any
20/// other approving party — is behind it. A library has no terminal to ask at
21/// (ADR-0011, the same reason `basis-cli`'s `TerminalApprover` lives in the
22/// binary and not in `basis`), so `basis-tasks` does not decide this; a host
23/// that wants `Prompt` to work supplies one via
24/// [`Tasks::with_prompt_host`](crate::Tasks::with_prompt_host). A `Tasks`
25/// with none refuses `Prompt` the same way an unaskable process always did —
26/// safely, and by name.
27pub trait PromptHost: Send + Sync {
28 /// Whether this process can currently put a question to whoever answers
29 /// for it. Checked before a `Prompt`-mode task is allowed to spawn, and
30 /// again at every attach.
31 fn can_ask(&self) -> bool;
32
33 /// The approver used for one task's turns while this process drives it.
34 /// Called once per attach, only when [`can_ask`](Self::can_ask) said yes.
35 fn approver(&self) -> Box<dyn Approver>;
36}
37
38/// `Prompt` is answerable exactly when a process is driving the task *and*
39/// has somewhere to put the question — see [`PromptHost`]. `Always` and
40/// `Never` ask nobody, so they need no host at all.
41///
42/// Public because a host wants this checked as early as possible: refusing
43/// `Prompt` at spawn, before a task directory is even minted, is a cheaper
44/// and clearer failure than minting one that can never make progress.
45pub fn validate_approval(approve: Approve, interactive: bool) -> Result<(), crate::Error> {
46 match approve {
47 Approve::Always | Approve::Never => Ok(()),
48 Approve::Prompt if interactive => Ok(()),
49 Approve::Prompt => Err(crate::Error::new(
50 "`Approve::Prompt` needs a process driving the task with a prompt host that can \
51 currently ask; use `Always` or `Never` for work nobody is attached to",
52 )),
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn prompt_approval_needs_a_driver_that_can_ask() {
62 for approve in [Approve::Always, Approve::Never] {
63 assert!(
64 validate_approval(approve, false).is_ok(),
65 "{approve:?} asks nobody"
66 );
67 assert!(
68 validate_approval(approve, true).is_ok(),
69 "{approve:?} asks nobody"
70 );
71 }
72
73 assert!(
74 validate_approval(Approve::Prompt, true).is_ok(),
75 "a host that can ask is exactly what `Prompt` needs"
76 );
77
78 let refused = validate_approval(Approve::Prompt, false)
79 .expect_err("nobody able to ask means nobody to ask")
80 .to_string();
81 assert!(refused.contains("ask"), "{refused}");
82 }
83}