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}