Skip to main content

runtime/tools/exec/confinement/
placement.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Named command placement and the write-policy gate.
7
8use kernel::{AxCode, AxError};
9use serde_json::{Map, Value};
10
11/// Where a host command runs. The safe arm is what a call gets when it
12/// says nothing, because an agent that already knew a command was
13/// dangerous would not need to be told to sandbox it.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Placement {
16    /// In the confinement [`Confinement::detect`](super::Confinement::detect) reports: a copy of
17    /// the working tree, and the platform's own isolation where there is
18    /// a wrapper for it.
19    Sandbox,
20    /// Where it stands, on the room the run may write in. This is the
21    /// placement a person decides on; a command that needs it asks for
22    /// it by name.
23    Host,
24}
25
26/// Reads the placement out of the call. Absent is [`Placement::Sandbox`].
27pub fn parse_placement(args: &Map<String, Value>) -> Result<Placement, AxError> {
28    match args.get("where") {
29        None => Ok(Placement::Sandbox),
30        Some(Value::String(said)) if said == "sandbox" => Ok(Placement::Sandbox),
31        Some(Value::String(said)) if said == "host" => Ok(Placement::Host),
32        Some(other) => Err(AxError::failure(
33            AxCode::InvalidArgs,
34            "run exec",
35            format!("unrecognised placement: {other}"),
36        )
37        .with_recovery(
38            "leave `where` out to run in the sandbox, or name one of `sandbox` and \
39             `host`",
40        )),
41    }
42}
43
44impl Placement {
45    /// This placement, when the run's write limit opens it.
46    ///
47    /// A command on the host could change, remove or link any file it
48    /// reaches, and nothing on this machine makes the existing ones
49    /// read-only to it without administrator rights, so under `Create`
50    /// only the copy is open; what a command writes there stays there
51    /// (`crates/runtime/spec/Tools.lean` ยง8-55, runtime D11).
52    ///
53    /// # Errors
54    /// The refusal of `kernel::gate::replacing`, naming the run's
55    /// domain, before any process starts.
56    pub fn opened_by(self, setup: &crate::tools::ExecSetup) -> Result<Placement, AxError> {
57        match self {
58            Placement::Sandbox => Ok(self),
59            Placement::Host => {
60                match kernel::gate::replacing(setup.policy.now().write, &setup.domain) {
61                    kernel::GateOutcome::Allow => Ok(self),
62                    kernel::GateOutcome::Deny { refusal } => Err(*refusal),
63                    kernel::GateOutcome::Ask { question } => Err(*question),
64                }
65            }
66        }
67    }
68}