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}