agentguard-local 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml ADDED
@@ -0,0 +1,22 @@
1
+ [workspace]
2
+ members = ["crates/agentguard-core", "crates/agentguard"]
3
+ resolver = "3"
4
+
5
+ [workspace.package]
6
+ version = "0.1.0"
7
+ edition = "2024"
8
+ license = "MIT OR Apache-2.0"
9
+ authors = ["AgentGuard contributors"]
10
+
11
+ [workspace.dependencies]
12
+ agentguard-core = { path = "crates/agentguard-core" }
13
+ anyhow = "1.0.100"
14
+ chrono = { version = "0.4.42", features = ["serde"] }
15
+ clap = { version = "4.5.53", features = ["derive", "env"] }
16
+ libc = "0.2.178"
17
+ rusqlite = { version = "0.37.0", features = ["bundled", "chrono", "serde_json"] }
18
+ serde = { version = "1.0.228", features = ["derive"] }
19
+ serde_json = "1.0.145"
20
+ tempfile = "3.23.0"
21
+ thiserror = "2.0.17"
22
+ uuid = { version = "1.19.0", features = ["v4", "serde"] }
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # AgentGuard
2
+
3
+ AgentGuard is a macOS-first local admission controller for AI coding CLI processes. It starts guarded jobs only when local policy and memory pressure allow it, queues new guarded work when the machine is under stress, pauses guarded process groups on critical pressure, and explains those decisions through a small CLI.
4
+
5
+ AgentGuard is not an agent orchestrator, worktree manager, sandbox, cloud IDE, RAM cleaner, or process killer for arbitrary apps. It only manages jobs launched through `agentguard run` or an installed AgentGuard shim.
6
+
7
+ ## What ships in this MVP
8
+
9
+ - `agentguardd`: launchd-friendly user daemon with a user-only Unix domain socket.
10
+ - `agentguard`: CLI for `status`, `run`, `policy`, `pause`, `resume`, `cancel`, `doctor`, and `install-shim`.
11
+ - SQLite state for jobs, process samples, events, policy, and tool path mappings.
12
+ - Pure scheduler and policy modules with deterministic tests.
13
+ - Process-group spawning with `SIGSTOP` and `SIGCONT` for guarded jobs only.
14
+ - Codex adapter that injects `-c agents.max_threads=N` unless the user already supplied an explicit `agents.max_threads` config.
15
+ - Deterministic fake pressure support for tests through `AGENTGUARD_FAKE_PRESSURE_FILE`.
16
+
17
+ ## Install
18
+
19
+ The npm package currently ships prebuilt binaries for macOS on Apple Silicon:
20
+
21
+ ```sh
22
+ npm install -g agentguard-local
23
+ ```
24
+
25
+ The npm package is named `agentguard-local` to avoid collisions with existing packages. It installs the `agentguard` and `agentguardd` commands.
26
+
27
+ Then start the daemon:
28
+
29
+ ```sh
30
+ agentguardd \
31
+ --socket "$HOME/.agentguard/agentguard.sock" \
32
+ --db "$HOME/.agentguard/state.db" \
33
+ --foreground
34
+ ```
35
+
36
+ ## Build From Source
37
+
38
+ ```sh
39
+ cargo build --workspace --all-features
40
+ ```
41
+
42
+ If Cargo is not on `PATH`, install Rust first. On macOS with Homebrew:
43
+
44
+ ```sh
45
+ brew install rust
46
+ ```
47
+
48
+ ## Run The Daemon
49
+
50
+ Foreground mode is easiest while testing from a source checkout:
51
+
52
+ ```sh
53
+ target/debug/agentguardd \
54
+ --socket "$HOME/.agentguard/agentguard.sock" \
55
+ --db "$HOME/.agentguard/state.db" \
56
+ --foreground
57
+ ```
58
+
59
+ The daemon listens only on a local Unix domain socket and stores state in SQLite. It does not open a TCP listener.
60
+
61
+ ## Use The CLI
62
+
63
+ Check status:
64
+
65
+ ```sh
66
+ target/debug/agentguard status
67
+ target/debug/agentguard status --json
68
+ ```
69
+
70
+ Run a guarded command:
71
+
72
+ ```sh
73
+ target/debug/agentguard run -- /bin/echo hello
74
+ target/debug/agentguard run --priority low -- /bin/sleep 30
75
+ ```
76
+
77
+ Manage policy:
78
+
79
+ ```sh
80
+ target/debug/agentguard policy get
81
+ target/debug/agentguard policy set max_active_jobs 1
82
+ target/debug/agentguard policy set pause_enabled true
83
+ ```
84
+
85
+ Pause, resume, or cancel a guarded job:
86
+
87
+ ```sh
88
+ target/debug/agentguard pause <job-id>
89
+ target/debug/agentguard resume <job-id>
90
+ target/debug/agentguard cancel <job-id>
91
+ ```
92
+
93
+ Run diagnostics:
94
+
95
+ ```sh
96
+ target/debug/agentguard doctor
97
+ ```
98
+
99
+ ## Install A Temporary Shim
100
+
101
+ Install a shim for a real tool path into a directory you control:
102
+
103
+ ```sh
104
+ target/debug/agentguard install-shim codex \
105
+ --real-path /path/to/real/codex \
106
+ --shim-dir "$HOME/.agentguard/bin"
107
+ ```
108
+
109
+ Then put the shim directory before the real tool in `PATH`:
110
+
111
+ ```sh
112
+ export PATH="$HOME/.agentguard/bin:$PATH"
113
+ ```
114
+
115
+ The shim dispatches through `agentguard run`, preserves cwd/env/stdin/stdout/stderr/args/exit code, and avoids PATH recursion by storing the real binary path.
116
+
117
+ ## Uninstall
118
+
119
+ Stop any foreground `agentguardd` process, remove the shim directory from `PATH`, and delete the local data directory if you no longer want state:
120
+
121
+ ```sh
122
+ rm -rf "$HOME/.agentguard"
123
+ ```
124
+
125
+ If you later add a launchd plist, unload and remove it before deleting state.
126
+
127
+ ## Safety Model
128
+
129
+ - AgentGuard never pauses, resumes, cancels, or kills unguarded user processes.
130
+ - Automatic pause/resume uses process-group signals only for jobs that AgentGuard admitted and registered.
131
+ - Cancel sends `SIGTERM` only to the guarded process group for the selected job.
132
+ - Job commands and events are stored locally in SQLite. Prompts are not separately captured.
133
+ - The v1 control plane is a user-only Unix socket with `0600` permissions.
134
+
135
+ ## Limitations
136
+
137
+ - v1 does not queue individual Codex subagents after Codex is already running. It caps fan-out at launch.
138
+ - The current system pressure sampler uses stable macOS command output as a documented fallback while preserving the sampler boundary for native API work.
139
+ - There is no menu bar UI, cloud offload, container runtime, microVM dependency, or team dashboard.
140
+ - Pause does not immediately free all memory; it stops execution so pressure can recover.
@@ -0,0 +1,23 @@
1
+ [package]
2
+ name = "agentguard"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+ authors.workspace = true
7
+
8
+ [dependencies]
9
+ agentguard-core.workspace = true
10
+ anyhow.workspace = true
11
+ clap.workspace = true
12
+ serde_json.workspace = true
13
+
14
+ [[bin]]
15
+ name = "agentguard"
16
+ path = "src/bin/agentguard.rs"
17
+
18
+ [[bin]]
19
+ name = "agentguardd"
20
+ path = "src/bin/agentguardd.rs"
21
+
22
+ [dev-dependencies]
23
+ tempfile.workspace = true
@@ -0,0 +1,460 @@
1
+ use agentguard_core::adapter::codex;
2
+ use agentguard_core::process::{exit_code, spawn_in_process_group};
3
+ use agentguard_core::protocol::{DoctorReport, PROTOCOL_VERSION, Request, Response};
4
+ use agentguard_core::server::{default_db_path, default_socket_path};
5
+ use agentguard_core::store::Store;
6
+ use agentguard_core::types::{Priority, StatusSnapshot};
7
+ use clap::{Parser, Subcommand};
8
+ use std::io::{BufRead, BufReader, Write};
9
+ use std::os::unix::net::UnixStream;
10
+ use std::path::{Path, PathBuf};
11
+ use std::time::Duration;
12
+
13
+ #[derive(Debug, Parser)]
14
+ #[command(
15
+ name = "agentguard",
16
+ about = "RAM-aware admission control for local AI coding agents"
17
+ )]
18
+ struct Cli {
19
+ #[arg(long, env = "AGENTGUARD_SOCKET", global = true)]
20
+ socket: Option<PathBuf>,
21
+ #[arg(long, env = "AGENTGUARD_DB", global = true)]
22
+ db: Option<PathBuf>,
23
+ #[command(subcommand)]
24
+ command: Commands,
25
+ }
26
+
27
+ #[derive(Debug, Subcommand)]
28
+ enum Commands {
29
+ Status {
30
+ #[arg(long)]
31
+ json: bool,
32
+ },
33
+ Run {
34
+ #[arg(long, default_value = "normal")]
35
+ priority: String,
36
+ #[arg(long, hide = true)]
37
+ adapter: Option<String>,
38
+ #[arg(long, hide = true)]
39
+ real_path: Option<PathBuf>,
40
+ #[arg(trailing_var_arg = true, required = true, num_args = 1..)]
41
+ command: Vec<String>,
42
+ },
43
+ Policy {
44
+ #[command(subcommand)]
45
+ command: PolicyCommand,
46
+ },
47
+ Pause {
48
+ job_id: String,
49
+ },
50
+ Resume {
51
+ job_id: String,
52
+ },
53
+ Cancel {
54
+ job_id: String,
55
+ },
56
+ Doctor,
57
+ InstallShim {
58
+ tool_name: String,
59
+ #[arg(long)]
60
+ real_path: PathBuf,
61
+ #[arg(long)]
62
+ shim_dir: PathBuf,
63
+ },
64
+ }
65
+
66
+ #[derive(Debug, Subcommand)]
67
+ enum PolicyCommand {
68
+ Get,
69
+ Set { key: String, value: String },
70
+ }
71
+
72
+ fn main() -> anyhow::Result<()> {
73
+ let cli = Cli::parse();
74
+ let socket = cli.socket.unwrap_or_else(default_socket_path);
75
+ let db = cli.db.unwrap_or_else(default_db_path);
76
+ match cli.command {
77
+ Commands::Status { json } => status(&socket, json),
78
+ Commands::Run {
79
+ priority,
80
+ adapter,
81
+ real_path,
82
+ command,
83
+ } => run_command(&socket, priority, adapter, real_path, command),
84
+ Commands::Policy { command } => policy(&socket, command),
85
+ Commands::Pause { job_id } => ack(
86
+ &socket,
87
+ Request::Pause {
88
+ protocol_version: PROTOCOL_VERSION,
89
+ job_id,
90
+ },
91
+ ),
92
+ Commands::Resume { job_id } => ack(
93
+ &socket,
94
+ Request::Resume {
95
+ protocol_version: PROTOCOL_VERSION,
96
+ job_id,
97
+ },
98
+ ),
99
+ Commands::Cancel { job_id } => ack(
100
+ &socket,
101
+ Request::Cancel {
102
+ protocol_version: PROTOCOL_VERSION,
103
+ job_id,
104
+ },
105
+ ),
106
+ Commands::Doctor => doctor(&socket, &db),
107
+ Commands::InstallShim {
108
+ tool_name,
109
+ real_path,
110
+ shim_dir,
111
+ } => install_shim(&socket, &db, &tool_name, &real_path, &shim_dir),
112
+ }
113
+ }
114
+
115
+ fn status(socket: &Path, json: bool) -> anyhow::Result<()> {
116
+ match send(
117
+ socket,
118
+ Request::Status {
119
+ protocol_version: PROTOCOL_VERSION,
120
+ include_processes: true,
121
+ },
122
+ )? {
123
+ Response::Status { snapshot, .. } => {
124
+ if json {
125
+ println!("{}", serde_json::to_string_pretty(&snapshot)?);
126
+ } else {
127
+ print_status(&snapshot);
128
+ }
129
+ }
130
+ response => handle_unexpected(response)?,
131
+ }
132
+ Ok(())
133
+ }
134
+
135
+ fn run_command(
136
+ socket: &Path,
137
+ priority: String,
138
+ adapter: Option<String>,
139
+ real_path: Option<PathBuf>,
140
+ command: Vec<String>,
141
+ ) -> anyhow::Result<()> {
142
+ let priority: Priority = priority.parse().map_err(anyhow::Error::msg)?;
143
+ let display_command = command
144
+ .first()
145
+ .ok_or_else(|| anyhow::anyhow!("missing command after --"))?
146
+ .clone();
147
+ let display_args = command.iter().skip(1).cloned().collect::<Vec<_>>();
148
+ let adapter = adapter.unwrap_or_else(|| {
149
+ Path::new(&display_command)
150
+ .file_name()
151
+ .and_then(|name| name.to_str())
152
+ .unwrap_or(&display_command)
153
+ .to_string()
154
+ });
155
+ let cwd = std::env::current_dir()?.display().to_string();
156
+
157
+ let admission = send(
158
+ socket,
159
+ Request::Admission {
160
+ protocol_version: PROTOCOL_VERSION,
161
+ adapter: adapter.clone(),
162
+ command: display_command.clone(),
163
+ args: display_args.clone(),
164
+ cwd,
165
+ priority,
166
+ tty: true,
167
+ },
168
+ )?;
169
+
170
+ let (job_id, mut codex_max_threads) = match admission {
171
+ Response::Admission {
172
+ job_id,
173
+ admitted,
174
+ reason,
175
+ codex_max_threads,
176
+ ..
177
+ } => {
178
+ if !admitted {
179
+ eprintln!(
180
+ "agentguard: queued {job_id}: {}",
181
+ reason.as_deref().unwrap_or("waiting for admission")
182
+ );
183
+ wait_for_lease(socket, &job_id)?;
184
+ }
185
+ (job_id, codex_max_threads)
186
+ }
187
+ response => handle_unexpected(response)?,
188
+ };
189
+
190
+ if codex_max_threads == 0 {
191
+ codex_max_threads = 1;
192
+ }
193
+ let spawn_command = real_path
194
+ .as_ref()
195
+ .map(|path| path.display().to_string())
196
+ .unwrap_or(display_command);
197
+ let mut spawn_args = display_args;
198
+ if adapter == "codex" {
199
+ spawn_args = codex::apply_thread_cap(&spawn_args, codex_max_threads);
200
+ }
201
+
202
+ let mut child = spawn_in_process_group(&spawn_command, &spawn_args)?;
203
+ let root_pid = child.id() as i64;
204
+ let pgid = root_pid;
205
+ let _ = send(
206
+ socket,
207
+ Request::Register {
208
+ protocol_version: PROTOCOL_VERSION,
209
+ job_id: job_id.clone(),
210
+ root_pid,
211
+ pgid,
212
+ },
213
+ );
214
+ let status = child.wait()?;
215
+ let code = exit_code(status);
216
+ let _ = send(
217
+ socket,
218
+ Request::Exit {
219
+ protocol_version: PROTOCOL_VERSION,
220
+ job_id,
221
+ exit_code: code,
222
+ },
223
+ );
224
+ std::process::exit(code);
225
+ }
226
+
227
+ fn wait_for_lease(socket: &Path, job_id: &str) -> anyhow::Result<()> {
228
+ let mut last_reason = String::new();
229
+ loop {
230
+ std::thread::sleep(Duration::from_millis(250));
231
+ match send(
232
+ socket,
233
+ Request::LeaseCheck {
234
+ protocol_version: PROTOCOL_VERSION,
235
+ job_id: job_id.to_string(),
236
+ },
237
+ )? {
238
+ Response::Lease {
239
+ admitted,
240
+ state,
241
+ reason,
242
+ ..
243
+ } => {
244
+ if admitted {
245
+ return Ok(());
246
+ }
247
+ if matches!(state.as_str(), "cancelled" | "failed" | "lost" | "exited") {
248
+ anyhow::bail!("queued job ended before admission with state {state}");
249
+ }
250
+ let reason = reason.unwrap_or_else(|| "waiting for admission".to_string());
251
+ if reason != last_reason {
252
+ eprintln!("agentguard: still queued {job_id}: {reason}");
253
+ last_reason = reason;
254
+ }
255
+ }
256
+ response => handle_unexpected(response)?,
257
+ }
258
+ }
259
+ }
260
+
261
+ fn policy(socket: &Path, command: PolicyCommand) -> anyhow::Result<()> {
262
+ let request = match command {
263
+ PolicyCommand::Get => Request::PolicyGet {
264
+ protocol_version: PROTOCOL_VERSION,
265
+ },
266
+ PolicyCommand::Set { key, value } => Request::PolicySet {
267
+ protocol_version: PROTOCOL_VERSION,
268
+ key,
269
+ value,
270
+ },
271
+ };
272
+ match send(socket, request)? {
273
+ Response::Policy { policy, .. } => {
274
+ println!("{}", serde_json::to_string_pretty(&policy)?);
275
+ }
276
+ response => handle_unexpected(response)?,
277
+ }
278
+ Ok(())
279
+ }
280
+
281
+ fn ack(socket: &Path, request: Request) -> anyhow::Result<()> {
282
+ match send(socket, request)? {
283
+ Response::Ack { message, .. } => println!("{message}"),
284
+ response => handle_unexpected(response)?,
285
+ }
286
+ Ok(())
287
+ }
288
+
289
+ fn doctor(socket: &Path, db: &Path) -> anyhow::Result<()> {
290
+ match send(
291
+ socket,
292
+ Request::Doctor {
293
+ protocol_version: PROTOCOL_VERSION,
294
+ },
295
+ ) {
296
+ Ok(Response::Doctor { report, .. }) => print_doctor(&report),
297
+ Ok(response) => handle_unexpected(response)?,
298
+ Err(_) => {
299
+ let report = local_doctor(socket, db)?;
300
+ print_doctor(&report);
301
+ }
302
+ }
303
+ Ok(())
304
+ }
305
+
306
+ fn install_shim(
307
+ socket: &Path,
308
+ db: &Path,
309
+ tool_name: &str,
310
+ real_path: &Path,
311
+ shim_dir: &Path,
312
+ ) -> anyhow::Result<()> {
313
+ anyhow::ensure!(
314
+ real_path.exists(),
315
+ "real path does not exist: {}",
316
+ real_path.display()
317
+ );
318
+ std::fs::create_dir_all(shim_dir)?;
319
+ let agentguard = std::env::current_exe()?;
320
+ let shim_path = shim_dir.join(tool_name);
321
+ let script = format!(
322
+ "#!/bin/sh\nexec {} --socket {} run --adapter {} --real-path {} -- {} \"$@\"\n",
323
+ shell_quote(&agentguard.display().to_string()),
324
+ shell_quote(&socket.display().to_string()),
325
+ shell_quote(tool_name),
326
+ shell_quote(&real_path.display().to_string()),
327
+ shell_quote(tool_name),
328
+ );
329
+ std::fs::write(&shim_path, script)?;
330
+ let mut perms = std::fs::metadata(&shim_path)?.permissions();
331
+ #[cfg(unix)]
332
+ {
333
+ use std::os::unix::fs::PermissionsExt;
334
+ perms.set_mode(0o755);
335
+ }
336
+ std::fs::set_permissions(&shim_path, perms)?;
337
+
338
+ let request = Request::ToolPathSet {
339
+ protocol_version: PROTOCOL_VERSION,
340
+ tool: tool_name.to_string(),
341
+ real_path: real_path.display().to_string(),
342
+ };
343
+ if send(socket, request).is_err() {
344
+ let store = Store::open(db)?;
345
+ store.set_tool_path(tool_name, &real_path.display().to_string())?;
346
+ }
347
+ println!("{}", shim_path.display());
348
+ Ok(())
349
+ }
350
+
351
+ fn send(socket: &Path, request: Request) -> anyhow::Result<Response> {
352
+ let mut stream = UnixStream::connect(socket).map_err(|err| {
353
+ anyhow::anyhow!(
354
+ "could not connect to daemon at {}: {err}. Start agentguardd first.",
355
+ socket.display()
356
+ )
357
+ })?;
358
+ serde_json::to_writer(&mut stream, &request)?;
359
+ stream.write_all(b"\n")?;
360
+ stream.flush()?;
361
+
362
+ let mut reader = BufReader::new(stream);
363
+ let mut line = String::new();
364
+ reader.read_line(&mut line)?;
365
+ let response: Response = serde_json::from_str(&line)?;
366
+ if let Response::Error { message, .. } = &response {
367
+ anyhow::bail!("{message}");
368
+ }
369
+ Ok(response)
370
+ }
371
+
372
+ fn print_status(snapshot: &StatusSnapshot) {
373
+ println!("Pressure: {}", snapshot.pressure);
374
+ println!(
375
+ "Policy: max_active_jobs={}, codex_threads={}, pause_enabled={}",
376
+ snapshot.policy.max_active_jobs,
377
+ snapshot.policy.default_codex_max_threads,
378
+ snapshot.policy.pause_enabled
379
+ );
380
+ println!("Jobs:");
381
+ if snapshot.jobs.is_empty() {
382
+ println!(" none");
383
+ }
384
+ for job in &snapshot.jobs {
385
+ let reason = job
386
+ .queued_reason
387
+ .as_ref()
388
+ .map(|reason| format!(" - {reason}"))
389
+ .unwrap_or_default();
390
+ println!(
391
+ " {} {} {} {}{}",
392
+ job.id, job.status, job.priority, job.command, reason
393
+ );
394
+ }
395
+ }
396
+
397
+ fn print_doctor(report: &DoctorReport) {
398
+ println!("OS supported: {}", report.os_supported);
399
+ println!("OS: {}", report.os);
400
+ println!(
401
+ "Socket: {} ({})",
402
+ report.socket_path,
403
+ if report.socket_exists {
404
+ "present"
405
+ } else {
406
+ "missing"
407
+ }
408
+ );
409
+ println!(
410
+ "Database: {} ({})",
411
+ report.db_path,
412
+ if report.db_exists {
413
+ "present"
414
+ } else {
415
+ "missing"
416
+ }
417
+ );
418
+ println!("launchd plist: {}", report.launchd_plist_exists);
419
+ println!("Tool paths:");
420
+ if report.discovered_tools.is_empty() {
421
+ println!(" none");
422
+ }
423
+ for (tool, path) in &report.discovered_tools {
424
+ println!(" {tool}: {path}");
425
+ }
426
+ }
427
+
428
+ fn local_doctor(socket: &Path, db: &Path) -> anyhow::Result<DoctorReport> {
429
+ let discovered_tools = if db.exists() {
430
+ Store::open(db)?
431
+ .list_tool_paths()?
432
+ .into_iter()
433
+ .map(|tool| (tool.tool, tool.real_path))
434
+ .collect()
435
+ } else {
436
+ vec![]
437
+ };
438
+ let plist = std::env::var_os("HOME")
439
+ .map(PathBuf::from)
440
+ .unwrap_or_default()
441
+ .join("Library/LaunchAgents/dev.agentguard.agentguardd.plist");
442
+ Ok(DoctorReport {
443
+ os_supported: std::env::consts::OS == "macos",
444
+ os: format!("{} {}", std::env::consts::OS, std::env::consts::ARCH),
445
+ socket_path: socket.display().to_string(),
446
+ socket_exists: socket.exists(),
447
+ db_path: db.display().to_string(),
448
+ db_exists: db.exists(),
449
+ launchd_plist_exists: plist.exists(),
450
+ discovered_tools,
451
+ })
452
+ }
453
+
454
+ fn handle_unexpected<T>(response: Response) -> anyhow::Result<T> {
455
+ anyhow::bail!("unexpected daemon response: {response:?}")
456
+ }
457
+
458
+ fn shell_quote(value: &str) -> String {
459
+ format!("'{}'", value.replace('\'', "'\\''"))
460
+ }
@@ -0,0 +1,23 @@
1
+ use agentguard_core::server::{DaemonConfig, default_db_path, default_socket_path, run_daemon};
2
+ use clap::Parser;
3
+ use std::path::PathBuf;
4
+
5
+ #[derive(Debug, Parser)]
6
+ #[command(name = "agentguardd", about = "AgentGuard user daemon")]
7
+ struct Args {
8
+ #[arg(long, env = "AGENTGUARD_SOCKET")]
9
+ socket: Option<PathBuf>,
10
+ #[arg(long, env = "AGENTGUARD_DB")]
11
+ db: Option<PathBuf>,
12
+ #[arg(long)]
13
+ foreground: bool,
14
+ }
15
+
16
+ fn main() -> anyhow::Result<()> {
17
+ let args = Args::parse();
18
+ run_daemon(DaemonConfig {
19
+ socket_path: args.socket.unwrap_or_else(default_socket_path),
20
+ db_path: args.db.unwrap_or_else(default_db_path),
21
+ foreground: args.foreground,
22
+ })
23
+ }