Skip to main content

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}