mobius_gateway/command/
init.rs1use super::*;
2
3pub(super) fn initialize(options: InitOptions) -> Result<()> {
4 let (store, config) = match options.cloudflare {
5 Some(CloudflareInit::Quick) => {
6 ConfigStore::initialize_quick_cloudflare(options.state_dir, options.listen)?
7 }
8 Some(CloudflareInit::Named { hostname, token }) => {
9 ConfigStore::initialize_named_cloudflare(
10 options.state_dir,
11 options.listen,
12 &hostname,
13 &token,
14 )?
15 }
16 None if options.tls.is_none() => {
17 ConfigStore::initialize_quick_cloudflare(options.state_dir, options.listen)?
18 }
19 None => ConfigStore::initialize(options.state_dir, options.listen, options.tls)?,
20 };
21 initialize_auth(&store, config.auth)?;
22 println!("initialized möbius gateway");
23 print_listener(&config, None);
24 println!("run `mobius-gateway connect` to pair a client");
25 Ok(())
26}
27
28pub(super) fn initialize_auth(store: &ConfigStore, policy: crate::auth::AuthConfig) -> Result<()> {
29 if let Err(error) = AuthStore::initialize(store.auth_path(), policy) {
30 return cleanup_failed_initialization(store, error);
31 }
32 Ok(())
33}
34
35pub(super) fn initialize_bootstrap(
36 state_dir: PathBuf,
37 save_local_client: fn(&Endpoint, String) -> Result<()>,
38) -> Result<()> {
39 let (store, config) = ConfigStore::initialize(state_dir, DEFAULT_LISTEN, None)?;
40 let initialized =
41 AuthStore::initialize(store.auth_path(), config.auth).and_then(|(auth, _)| {
42 let endpoint = direct_loopback_endpoint(&config)?;
43 let issued = auth.provision_local_client()?;
44 save_local_client(&endpoint, issued.token)
45 });
46 if let Err(error) = initialized {
47 return cleanup_failed_initialization(&store, error);
48 }
49 println!("initialized möbius gateway bootstrap");
50 print_listener(&config, None);
51 Ok(())
52}
53
54pub(super) fn reset_bot_defaults(state_dir: PathBuf) -> Result<()> {
55 #[cfg(unix)]
56 {
57 let (store, _) = ConfigStore::open(state_dir)?;
58 let _startup = StartupGuard::create(store.state_dir())?;
59 stop_gateway(store.state_dir(), None)?;
60 let (store, config) = ConfigStore::open(store.state_dir().to_path_buf())?;
61 let current = config.bot_defaults.as_ref().ok_or_else(|| {
62 Error::Config("configure a provider before resetting defaults".into())
63 })?;
64 let composition = crate::wire::AgentComposition {
65 provider: current.config.provider.clone(),
66 ..crate::wire::AgentComposition::default()
67 };
68 let config = config.replacing_bot_defaults(current.revision, composition)?;
69 store.save(&config)?;
70 println!("reset möbius gateway Bot defaults");
71 Ok(())
72 }
73 #[cfg(not(unix))]
74 {
75 let _ = state_dir;
76 Err(unsupported_lifecycle())
77 }
78}
79
80pub(super) fn set_desktop(state_dir: PathBuf, enabled: bool) -> Result<()> {
81 let (store, mut config) = ConfigStore::open(state_dir)?;
82 let _startup = StartupGuard::create(store.state_dir())?;
83 ensure_gateway_stopped(&store, &config)?;
84 config.desktop_enabled = enabled;
85 config.validate()?;
86 for bot in crate::bots::BotStore::open(store.state_dir())?.bots()? {
87 crate::config::validate_desktop_bot_policy(&config, &bot.config.config)?;
88 }
89 store.save(&config)?;
90 println!("gateway desktop enabled: {enabled}");
91 Ok(())
92}
93
94pub(super) fn direct_loopback_endpoint(config: &GatewayConfig) -> Result<Endpoint> {
95 if !config.listen.ip().is_loopback() || config.tls.is_some() || config.cloudflare.is_some() {
96 return Err(Error::Config(
97 "bootstrap commands require a direct plaintext loopback gateway".into(),
98 ));
99 }
100 loopback_endpoint(config)
101}
102
103fn cleanup_failed_initialization<T>(store: &ConfigStore, error: Error) -> Result<T> {
104 std::fs::remove_dir_all(store.state_dir()).map_err(|cleanup| {
105 Error::Config(format!(
106 "{error}; failed to remove incomplete gateway state at {}: {cleanup}",
107 store.state_dir().display()
108 ))
109 })?;
110 Err(error)
111}
112
113pub(super) fn provision_cloudflare_local_client(
114 auth: &AuthStore,
115 config: &GatewayConfig,
116) -> Result<Option<(Endpoint, String)>> {
117 if config.cloudflare.is_none() {
118 return Ok(None);
119 }
120 let endpoint = loopback_endpoint(config)?;
121 let issued = auth.provision_local_client()?;
122 Ok(Some((endpoint, issued.token)))
123}
124
125pub(super) fn loopback_endpoint(config: &GatewayConfig) -> Result<Endpoint> {
126 format!("tcp://{}", config.listen).parse()
127}
128
129pub fn initialize_quick_cloudflare(state_dir: PathBuf) -> Result<()> {
134 initialize(InitOptions {
135 state_dir,
136 listen: DEFAULT_LISTEN,
137 tls: None,
138 cloudflare: Some(CloudflareInit::Quick),
139 })
140}
141
142pub fn initialize_named_cloudflare(
147 state_dir: PathBuf,
148 hostname: String,
149 token: String,
150) -> Result<()> {
151 initialize(InitOptions {
152 state_dir,
153 listen: DEFAULT_LISTEN,
154 tls: None,
155 cloudflare: Some(CloudflareInit::Named { hostname, token }),
156 })
157}
158
159pub fn reset_gateway_state(state_dir: PathBuf) -> Result<()> {
166 #[cfg(unix)]
167 {
168 let had_config = validate_reset_target(&state_dir, false)?;
169 let state_dir = fs::canonicalize(state_dir)?;
170 let _startup = StartupGuard::create(&state_dir)?;
171 if validate_reset_target(&state_dir, true)? != had_config {
172 return Err(invalid_reset_target(&state_dir));
173 }
174 stop_gateway(&state_dir, None)?;
175 fs::remove_dir_all(state_dir)?;
176 Ok(())
177 }
178 #[cfg(not(unix))]
179 {
180 let _ = state_dir;
181 Err(unsupported_lifecycle())
182 }
183}
184
185#[cfg(unix)]
186pub(super) fn validate_reset_target(path: &Path, ignore_startup_lock: bool) -> Result<bool> {
187 let metadata = fs::symlink_metadata(path)?;
188 if metadata.file_type().is_symlink() || !metadata.is_dir() {
189 return Err(invalid_reset_target(path));
190 }
191 let mut empty = true;
192 for entry in fs::read_dir(path)? {
193 let entry = entry?;
194 if ignore_startup_lock && entry.file_name() == STARTUP_FILE {
195 continue;
196 }
197 empty = false;
198 }
199 if empty {
200 return Ok(false);
201 }
202 let marker = fs::symlink_metadata(path.join(STATE_MARKER_FILE))
203 .map_err(|_| invalid_reset_target(path))?;
204 if !marker.is_file() || marker.file_type().is_symlink() {
205 return Err(invalid_reset_target(path));
206 }
207 Ok(true)
208}
209
210#[cfg(unix)]
211pub(super) fn invalid_reset_target(path: &Path) -> Error {
212 Error::Config(format!(
213 "refusing to reset {}: expected an empty directory or möbius gateway state with a regular {STATE_MARKER_FILE}",
214 path.display()
215 ))
216}