ai-server --enroll-link: mint one more device's link while the server runs

Prints the enrollment URI, one line on stdout, and exits; the running
server adopts the token the first time that device presents it, via the
spool wg-app-link's enroll module now provides (submodule bumped to
d35c880). This is the server half of enrolling through Dev Updater: its
coming per-component Enroll button runs this command and opens whatever
it prints on the phone, which is what a reinstall -- a signing change, a
new phone -- needs when nobody is at the terminal the QR is printed on.

Verified against the sandbox server: minted while it ran, first request
with the token served and the token moved into config.ron, spool empty,
second request served as an ordinary token. 108 tests, clippy clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-02 05:44:17 -04:00
1 parent 68e77c7b37
commit 1656b058bf
7 files changed
+138 -13

No files matched your search

+30
View File
@@ -81,6 +81,14 @@ struct Args {
#[arg(long)]
rotate_token: bool,
/// Enroll one more device without touching the running server: mint a
/// token, print its enrollment link (one line, stdout, nothing else)
/// and exit. The server adopts the token the first time that device
/// uses it. For a tool -- Dev Updater -- that opens the link on the
/// phone, where a QR printed here cannot be scanned.
#[arg(long)]
enroll_link: bool,
/// Hold every response back by this many milliseconds.
///
/// A development aid, and a specific one: over the tunnel a phone's
@@ -148,6 +156,28 @@ async fn main() -> Result<()> {
let config_path = args
.config
.unwrap_or_else(|| config_home("ai-app").join("config.ron"));
// Before the manager exists, on purpose: constructing it and seeding
// setups touches sessions and subprocesses this invocation has no
// business with while another instance is serving. Only the hash
// reaches disk, in the spool `auth.rs` reads; the link itself goes to
// stdout alone, because the caller opens whatever this prints.
if args.enroll_link {
let bind_ip = match args.bind {
Some(ip) => ip,
None => netif::wg_address("ai-server")?,
};
let token = enroll::generate_token();
enroll::spool_pending(
&config::pending_enrollments_dir(&config_path),
"phone",
&token,
)?;
println!(
"{}",
enroll::enrollment_uri("aiapp", bind_ip, args.port, &token)
);
return Ok(());
}
let data_dir = args
.data_dir
.unwrap_or_else(|| data_home("ai-app").join("sessions"));