Skip to main content

kernel/gate/
attach.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//! The one door that asks a person.
7//! The part `crates/kernel/spec/Gate.lean` specifies this module with the
8//! rest of `kernel::gate`.
9//!
10//! Every other door answers from the rules it was handed. This one
11//! cannot: what it would grant is not an action but the whole of the
12//! login state inside a browser the person is already using - mail,
13//! banking, the company console - and the per-building isolation that
14//! `browser::Profile` keeps is exactly what stops applying the moment a
15//! run attaches to the browser a person drives. The person's own
16//! action is the only answer the platform offers: they turn remote
17//! debugging on, in the running browser, themselves.
18//!
19//! So the verdict has three arms here and exactly one door in the
20//! roster returns the third. The recovery sentence is the question: it
21//! names the step only the person can take.
22
23use crate::error::{AxCode, AxError, GateRefusal};
24
25use super::{EgressTarget, GateOutcome};
26
27/// May a run attach to the person's browser?
28///
29/// `endpoint` is the address the person declared for their browser, as
30/// the egress classifier read it. `None` is a building that enabled the
31/// tool and has not said where the browser answers, which is the state
32/// the person's next action resolves.
33///
34/// # The rule
35/// A declared loopback address is the person's own machine reached from
36/// the person's own machine: attach. No declaration is a question for
37/// the person, not a refusal, because they can answer it. Any other
38/// address would read that login state over a network, and the answer
39/// there is no.
40#[must_use]
41pub fn attach(endpoint: Option<&EgressTarget>) -> GateOutcome {
42    match endpoint {
43        None => GateOutcome::Ask {
44            question: Box::new(
45                AxError::refusal(
46                    AxCode::ApprovalPending,
47                    "attach to the User's browser",
48                    "no browser address is declared",
49                    GateRefusal::new(
50                        "a run drives the User's browser only where the User has told \
51                         it to listen",
52                        "this building enabled `usersbrowser` and declared no address",
53                        "ask the User for the address their browser is listening on, and \
54                         have them put it on a `usersbrowser` key in RULES.toml",
55                    ),
56                )
57                .with_recovery(
58                    "start the browser with remote debugging, copy the ws://127.0.0.1:<port>/session \
59                     address it prints, and add `usersbrowser = \"<that address>\"` to the building's \
60                     RULES.toml; the next dispatch picks it up. Only the User can do this, \
61                     because it is their logins the attachment reaches",
62                ),
63            ),
64        },
65        Some(EgressTarget::Loopback) => GateOutcome::Allow,
66        Some(other) => GateOutcome::Deny {
67            refusal: Box::new(
68                AxError::refusal(
69                    AxCode::GateDenied,
70                    "attach to the User's browser",
71                    subject_of(other),
72                    GateRefusal::new(
73                        "a browser attachment stays on this machine",
74                        "the declared address is not loopback",
75                        "declare the ws://127.0.0.1:<port>/session address the browser \
76                         itself prints",
77                    ),
78                )
79                .with_recovery(
80                    "attach only reaches a browser on this machine; declare a loopback \
81                     address, or keep using the `browser` tool, which this city starts \
82                     itself",
83                ),
84            ),
85        },
86    }
87}
88
89/// What to name as the subject of the refusal: the far end, in the
90/// terms the target was classified with. The raw declared URL never
91/// appears, because a URL can carry a credential in its query.
92fn subject_of(target: &EgressTarget) -> String {
93    match target {
94        EgressTarget::Loopback => "loopback".to_owned(),
95        EgressTarget::Private => "a host on the same private network".to_owned(),
96        EgressTarget::Public { host } => host.clone(),
97        EgressTarget::Connector { label } => format!("connector {}", label.as_str()),
98    }
99}