From 3b04b8299881248cd0f888ffed496dd15008e7ca Mon Sep 17 00:00:00 2001
From: Daniel Lundin <dln@arity.se>
Date: Sun, 12 Jan 2025 14:27:46 +0100
Subject: [PATCH 1/3] chore(clippy): cleanup

---
 instrumentation/src/lib.rs | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/instrumentation/src/lib.rs b/instrumentation/src/lib.rs
index 31df7f6..fd69950 100644
--- a/instrumentation/src/lib.rs
+++ b/instrumentation/src/lib.rs
@@ -96,20 +96,18 @@ pub fn init_tracing(otel_endpoint: Option<&String>, log_stderr: bool) -> Result<
         None => None,
     };
 
-    let metrics_layer = match meter_provider {
-        Some(ref p) => Some(MetricsLayer::new(p.to_owned())),
-        None => None,
-    };
+    let metrics_layer = meter_provider
+        .as_ref()
+        .map(|p| MetricsLayer::new(p.to_owned()));
 
     let tracer_provider = match otel_endpoint {
         Some(endpoint) => Some(init_tracer_provider(endpoint, resource)?),
         None => None,
     };
 
-    let trace_layer = match tracer_provider {
-        Some(ref p) => Some(OpenTelemetryLayer::new(p.tracer("tracing-otel-subscriber"))),
-        None => None,
-    };
+    let trace_layer = tracer_provider
+        .as_ref()
+        .map(|p| OpenTelemetryLayer::new(p.tracer("tracing-otel-subscriber")));
 
     opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
 

From 9b7e1fb226e99f6a1f3e3c968960a62102147183 Mon Sep 17 00:00:00 2001
From: Daniel Lundin <dln@arity.se>
Date: Wed, 8 Jan 2025 11:58:35 +0100
Subject: [PATCH 2/3] feat: Add user resource w/database as storage

---
 .cargo/audit.toml                             |   8 +
 Cargo.lock                                    | 687 +++++++++++++++++-
 Cargo.toml                                    |   1 +
 agent/Cargo.toml                              |   2 +
 api.json                                      | 135 +++-
 ...9d5910bfdb2a6c9b82ddb296854973369594c.json |  47 ++
 ...dc8fdfc05f489328e8376513124dfb42996e3.json |  46 ++
 controller/Cargo.toml                         |   4 +
 controller/build.rs                           |   5 +
 .../migrations/20250108132540_users.sql       |   7 +
 controller/src/api.rs                         |   2 +
 controller/src/bin/patagia-controller.rs      |  23 +-
 controller/src/context.rs                     |  16 +-
 controller/src/lib.rs                         |   1 +
 controller/src/user/api.rs                    | 110 +++
 controller/src/user/mod.rs                    |  14 +
 flake.nix                                     |   5 +
 justfile                                      |  34 +-
 18 files changed, 1117 insertions(+), 30 deletions(-)
 create mode 100644 .cargo/audit.toml
 create mode 100644 controller/.sqlx/query-40dee0d539971f95bb3dc2ba4c49d5910bfdb2a6c9b82ddb296854973369594c.json
 create mode 100644 controller/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json
 create mode 100644 controller/build.rs
 create mode 100644 controller/migrations/20250108132540_users.sql
 create mode 100644 controller/src/user/api.rs
 create mode 100644 controller/src/user/mod.rs

diff --git a/.cargo/audit.toml b/.cargo/audit.toml
new file mode 100644
index 0000000..352d558
--- /dev/null
+++ b/.cargo/audit.toml
@@ -0,0 +1,8 @@
+[advisories]
+ignore = [
+  # Advisory about a vulnerability in rsa, which we don't use, but comes via sqlx due
+  # to a bug in cargo. For context, see:
+  #   https://github.com/launchbadge/sqlx/issues/2911
+  #   and https://github.com/rust-lang/cargo/issues/10801
+  "RUSTSEC-2023-0071"
+]
diff --git a/Cargo.lock b/Cargo.lock
index 0c8d346..67b0d67 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -147,6 +147,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "atomic-waker"
 version = "1.1.2"
@@ -218,7 +227,7 @@ dependencies = [
  "miniz_oxide",
  "object",
  "rustc-demangle",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -227,11 +236,20 @@ version = "0.22.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
 [[package]]
 name = "bitflags"
 version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "block-buffer"
@@ -302,7 +320,7 @@ dependencies = [
  "iana-time-zone",
  "num-traits",
  "serde",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -352,6 +370,21 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
 [[package]]
 name = "core-foundation"
 version = "0.9.4"
@@ -387,6 +420,21 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "crc"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
 [[package]]
 name = "crc32fast"
 version = "1.4.2"
@@ -405,6 +453,15 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.21"
@@ -427,6 +484,17 @@ version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21"
 
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.11"
@@ -443,7 +511,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer",
+ "const-oid",
  "crypto-common",
+ "subtle",
 ]
 
 [[package]]
@@ -478,6 +548,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
 [[package]]
 name = "dropshot"
 version = "0.15.1"
@@ -554,6 +630,9 @@ name = "either"
 version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "encoding_rs"
@@ -580,6 +659,28 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "fastrand"
 version = "2.3.0"
@@ -596,12 +697,29 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foldhash"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+
 [[package]]
 name = "foreign-types"
 version = "0.3.2"
@@ -668,6 +786,17 @@ dependencies = [
  "futures-util",
 ]
 
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
 [[package]]
 name = "futures-io"
 version = "0.3.31"
@@ -790,6 +919,20 @@ name = "hashbrown"
 version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.2",
+]
 
 [[package]]
 name = "heck"
@@ -803,6 +946,39 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "hostname"
 version = "0.3.1"
@@ -1221,6 +1397,9 @@ name = "lazy_static"
 version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
 
 [[package]]
 name = "libc"
@@ -1228,6 +1407,12 @@ version = "0.2.169"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
 
+[[package]]
+name = "libm"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
+
 [[package]]
 name = "libredox"
 version = "0.1.3"
@@ -1238,6 +1423,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
@@ -1287,6 +1482,16 @@ version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
 
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.4"
@@ -1363,12 +1568,49 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
 [[package]]
 name = "num-conv"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
 
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -1376,6 +1618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -1560,6 +1803,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
 [[package]]
 name = "parking_lot"
 version = "0.12.3"
@@ -1580,7 +1829,7 @@ dependencies = [
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -1595,6 +1844,7 @@ version = "0.2.0"
 dependencies = [
  "anyhow",
  "clap",
+ "futures",
  "instrumentation",
  "progenitor",
  "reqwest",
@@ -1602,6 +1852,7 @@ dependencies = [
  "serde",
  "tokio",
  "tracing",
+ "uuid",
 ]
 
 [[package]]
@@ -1617,10 +1868,21 @@ dependencies = [
  "serde",
  "slog",
  "slog-async",
+ "sqlx",
  "tokio",
  "trace-request",
  "tracing",
  "tracing-slog",
+ "uuid",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
 ]
 
 [[package]]
@@ -1661,6 +1923,27 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.31"
@@ -2011,6 +2294,26 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "rsa"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.24"
@@ -2314,6 +2617,17 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
 [[package]]
 name = "sharded-slab"
 version = "0.1.7"
@@ -2338,6 +2652,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -2407,6 +2731,9 @@ name = "smallvec"
 version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "socket2"
@@ -2423,6 +2750,217 @@ name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
+dependencies = [
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.2",
+ "hashlink",
+ "indexmap 2.7.0",
+ "log",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rustls 0.23.20",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.9",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "uuid",
+ "webpki-roots",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.9",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.9",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "time",
+ "tracing",
+ "url",
+ "uuid",
+]
 
 [[package]]
 name = "stable_deref_trait"
@@ -2430,6 +2968,17 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
 [[package]]
 name = "strsim"
 version = "0.11.1"
@@ -2861,6 +3410,7 @@ version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
 dependencies = [
+ "log",
  "pin-project-lite",
  "tracing-attributes",
  "tracing-core",
@@ -3000,12 +3550,33 @@ dependencies = [
  "typify-impl",
 ]
 
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
 
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
+
 [[package]]
 name = "unsafe-libyaml"
 version = "0.2.11"
@@ -3099,6 +3670,12 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.99"
@@ -3208,6 +3785,16 @@ dependencies = [
  "rustls-pki-types",
 ]
 
+[[package]]
+name = "whoami"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
+dependencies = [
+ "redox_syscall",
+ "wasite",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -3237,7 +3824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
 dependencies = [
  "windows-core",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3246,7 +3833,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3257,7 +3844,7 @@ checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
 dependencies = [
  "windows-result",
  "windows-strings",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3266,7 +3853,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3276,7 +3863,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
 dependencies = [
  "windows-result",
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
@@ -3285,7 +3881,7 @@ version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -3294,7 +3890,22 @@ version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
 ]
 
 [[package]]
@@ -3303,28 +3914,46 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
  "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.52.6"
@@ -3337,24 +3966,48 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.52.6"
diff --git a/Cargo.toml b/Cargo.toml
index 6cf8849..ca5b5c1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,6 +32,7 @@ clap = { version = "4.5.23", features = [
   "string",
 ] }
 dropshot = "0.15.1"
+futures = "0.3"
 http = "1.2.0"
 once_cell = "1.20.2"
 progenitor = "0.8.0"
diff --git a/agent/Cargo.toml b/agent/Cargo.toml
index 4edcf82..2305f9f 100644
--- a/agent/Cargo.toml
+++ b/agent/Cargo.toml
@@ -7,6 +7,7 @@ version.workspace = true
 [dependencies]
 anyhow.workspace = true
 clap.workspace = true
+futures.workspace = true
 instrumentation = { path = "../instrumentation" }
 progenitor.workspace = true
 reqwest.workspace = true
@@ -14,6 +15,7 @@ schemars.workspace = true
 serde.workspace = true
 tokio.workspace = true
 tracing.workspace = true
+uuid.workspace = true
 
 [package.metadata.cargo-machete]
 ignored = ["reqwest", "serde"]
diff --git a/api.json b/api.json
index 9c88ea6..4ea6803 100644
--- a/api.json
+++ b/api.json
@@ -5,6 +5,96 @@
     "version": "1.0.0"
   },
   "paths": {
+    "/users": {
+      "get": {
+        "tags": [
+          "user"
+        ],
+        "summary": "List users",
+        "operationId": "list_users",
+        "parameters": [
+          {
+            "in": "query",
+            "name": "limit",
+            "description": "Maximum number of items returned by a single call",
+            "schema": {
+              "nullable": true,
+              "type": "integer",
+              "format": "uint32",
+              "minimum": 1
+            }
+          },
+          {
+            "in": "query",
+            "name": "page_token",
+            "description": "Token returned by previous call to retrieve the subsequent page",
+            "schema": {
+              "nullable": true,
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UserResultsPage"
+                }
+              }
+            }
+          },
+          "4XX": {
+            "$ref": "#/components/responses/Error"
+          },
+          "5XX": {
+            "$ref": "#/components/responses/Error"
+          }
+        },
+        "x-dropshot-pagination": {
+          "required": []
+        }
+      }
+    },
+    "/users/{userId}": {
+      "get": {
+        "tags": [
+          "user"
+        ],
+        "summary": "Fetch user info.",
+        "operationId": "get_user_by_id",
+        "parameters": [
+          {
+            "in": "path",
+            "name": "userId",
+            "required": true,
+            "schema": {
+              "type": "string",
+              "format": "uuid"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "successful operation",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/User"
+                }
+              }
+            }
+          },
+          "4XX": {
+            "$ref": "#/components/responses/Error"
+          },
+          "5XX": {
+            "$ref": "#/components/responses/Error"
+          }
+        }
+      }
+    },
     "/version": {
       "get": {
         "summary": "Fetch version info.",
@@ -51,6 +141,44 @@
           "request_id"
         ]
       },
+      "User": {
+        "description": "User",
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "name": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "id",
+          "name"
+        ]
+      },
+      "UserResultsPage": {
+        "description": "A single page of results",
+        "type": "object",
+        "properties": {
+          "items": {
+            "description": "list of items on this page of results",
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/User"
+            }
+          },
+          "next_page": {
+            "nullable": true,
+            "description": "token used to fetch the next page of results (if any)",
+            "type": "string"
+          }
+        },
+        "required": [
+          "items"
+        ]
+      },
       "VersionInfo": {
         "description": "Version and build information",
         "type": "object",
@@ -80,5 +208,10 @@
         }
       }
     }
-  }
+  },
+  "tags": [
+    {
+      "name": "user"
+    }
+  ]
 }
diff --git a/controller/.sqlx/query-40dee0d539971f95bb3dc2ba4c49d5910bfdb2a6c9b82ddb296854973369594c.json b/controller/.sqlx/query-40dee0d539971f95bb3dc2ba4c49d5910bfdb2a6c9b82ddb296854973369594c.json
new file mode 100644
index 0000000..2c440e7
--- /dev/null
+++ b/controller/.sqlx/query-40dee0d539971f95bb3dc2ba4c49d5910bfdb2a6c9b82ddb296854973369594c.json
@@ -0,0 +1,47 @@
+{
+  "db_name": "PostgreSQL",
+  "query": "SELECT * FROM users WHERE id > coalesce($1, '00000000-0000-0000-0000-000000000000'::UUID) ORDER BY id LIMIT $2",
+  "describe": {
+    "columns": [
+      {
+        "ordinal": 0,
+        "name": "id",
+        "type_info": "Uuid"
+      },
+      {
+        "ordinal": 1,
+        "name": "name",
+        "type_info": "Varchar"
+      },
+      {
+        "ordinal": 2,
+        "name": "time_deleted",
+        "type_info": "Timestamptz"
+      },
+      {
+        "ordinal": 3,
+        "name": "time_created",
+        "type_info": "Timestamptz"
+      },
+      {
+        "ordinal": 4,
+        "name": "time_modified",
+        "type_info": "Timestamptz"
+      }
+    ],
+    "parameters": {
+      "Left": [
+        "Uuid",
+        "Int8"
+      ]
+    },
+    "nullable": [
+      false,
+      false,
+      true,
+      false,
+      false
+    ]
+  },
+  "hash": "40dee0d539971f95bb3dc2ba4c49d5910bfdb2a6c9b82ddb296854973369594c"
+}
diff --git a/controller/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json b/controller/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json
new file mode 100644
index 0000000..043a176
--- /dev/null
+++ b/controller/.sqlx/query-843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3.json
@@ -0,0 +1,46 @@
+{
+  "db_name": "PostgreSQL",
+  "query": "SELECT * FROM users WHERE id = $1",
+  "describe": {
+    "columns": [
+      {
+        "ordinal": 0,
+        "name": "id",
+        "type_info": "Uuid"
+      },
+      {
+        "ordinal": 1,
+        "name": "name",
+        "type_info": "Varchar"
+      },
+      {
+        "ordinal": 2,
+        "name": "time_deleted",
+        "type_info": "Timestamptz"
+      },
+      {
+        "ordinal": 3,
+        "name": "time_created",
+        "type_info": "Timestamptz"
+      },
+      {
+        "ordinal": 4,
+        "name": "time_modified",
+        "type_info": "Timestamptz"
+      }
+    ],
+    "parameters": {
+      "Left": [
+        "Uuid"
+      ]
+    },
+    "nullable": [
+      false,
+      false,
+      true,
+      false,
+      false
+    ]
+  },
+  "hash": "843923b9a0257cf80f1dff554e7dc8fdfc05f489328e8376513124dfb42996e3"
+}
diff --git a/controller/Cargo.toml b/controller/Cargo.toml
index baa4054..7157501 100644
--- a/controller/Cargo.toml
+++ b/controller/Cargo.toml
@@ -15,10 +15,14 @@ schemars.workspace = true
 serde.workspace = true
 slog-async.workspace = true
 slog.workspace = true
+sqlx = { version = "0.8.3", default-features = false, features = [
+    "macros", "migrate", "postgres", "runtime-tokio", "tls-rustls", "time", "uuid"
+  ] }
 tokio.workspace = true
 trace-request = { path = "../trace-request" }
 tracing-slog.workspace = true
 tracing.workspace = true
+uuid.workspace = true
 
 [package.metadata.cargo-machete]
 ignored = ["http"]
diff --git a/controller/build.rs b/controller/build.rs
new file mode 100644
index 0000000..d506869
--- /dev/null
+++ b/controller/build.rs
@@ -0,0 +1,5 @@
+// generated by `sqlx migrate build-script`
+fn main() {
+    // trigger recompilation when a new migration is added
+    println!("cargo:rerun-if-changed=migrations");
+}
diff --git a/controller/migrations/20250108132540_users.sql b/controller/migrations/20250108132540_users.sql
new file mode 100644
index 0000000..f6e7e8d
--- /dev/null
+++ b/controller/migrations/20250108132540_users.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS patagia.public.Users(
+    id            UUID PRIMARY KEY,
+    name          VARCHAR(63) NOT NULL,
+    time_deleted  TIMESTAMP WITH TIME ZONE, -- non-NULL if deleted
+    time_created  TIMESTAMP WITH TIME ZONE NOT NULL,
+    time_modified TIMESTAMP WITH TIME ZONE NOT NULL
+);
diff --git a/controller/src/api.rs b/controller/src/api.rs
index 1906567..5be86ce 100644
--- a/controller/src/api.rs
+++ b/controller/src/api.rs
@@ -4,12 +4,14 @@ use dropshot::ApiDescription;
 use std::sync::Arc;
 
 use crate::context::ControllerContext;
+use crate::user;
 use crate::version;
 
 type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>;
 
 pub fn api() -> Result<ControllerApiDescription> {
     let mut api = ControllerApiDescription::new();
+    user::register_api(&mut api)?;
     api.register(version::version)?;
     Ok(api)
 }
diff --git a/controller/src/bin/patagia-controller.rs b/controller/src/bin/patagia-controller.rs
index 845cfd6..a73f4a7 100644
--- a/controller/src/bin/patagia-controller.rs
+++ b/controller/src/bin/patagia-controller.rs
@@ -1,8 +1,8 @@
 use anyhow::{anyhow, Result};
 use clap::Parser;
 use dropshot::{ConfigDropshot, ServerBuilder};
-
 use slog::Drain;
+use sqlx::postgres::PgPool;
 use tracing_slog::TracingSlogDrain;
 
 use std::net::SocketAddr;
@@ -36,6 +36,13 @@ struct Cli {
         env = "LISTEN_ADDRESS"
     )]
     listen_address: String,
+
+    #[arg(
+        long = "database-url",
+        default_value = "postgresql://localhost/patagia",
+        env = "DATABASE_URL"
+    )]
+    database_url: Option<String>,
 }
 
 #[tokio::main]
@@ -57,7 +64,19 @@ async fn main() -> Result<()> {
         slog::Logger::root(async_drain, slog::o!())
     };
 
-    let ctx = ControllerContext::new();
+    let database_url = args.database_url.unwrap();
+
+    tracing::info!(
+        database_url,
+        listen_address = args.listen_address,
+        "Starting server"
+    );
+
+    let pg = PgPool::connect(&database_url).await?;
+
+    sqlx::migrate!().run(&pg).await?;
+
+    let ctx = ControllerContext::new(pg);
     let api = api::api()?;
     ServerBuilder::new(api, Arc::new(ctx), logger)
         .config(config)
diff --git a/controller/src/context.rs b/controller/src/context.rs
index d994d44..b99d559 100644
--- a/controller/src/context.rs
+++ b/controller/src/context.rs
@@ -1,13 +1,11 @@
-pub struct ControllerContext {}
+use sqlx::postgres::PgPool;
+
+pub struct ControllerContext {
+    pub pg_pool: PgPool,
+}
 
 impl ControllerContext {
-    pub fn new() -> ControllerContext {
-        ControllerContext {}
-    }
-}
-
-impl Default for ControllerContext {
-    fn default() -> Self {
-        Self::new()
+    pub fn new(pg_pool: PgPool) -> ControllerContext {
+        ControllerContext { pg_pool }
     }
 }
diff --git a/controller/src/lib.rs b/controller/src/lib.rs
index 0caaf72..2d12df1 100644
--- a/controller/src/lib.rs
+++ b/controller/src/lib.rs
@@ -1,4 +1,5 @@
 pub mod api;
 pub mod context;
 
+mod user;
 mod version;
diff --git a/controller/src/user/api.rs b/controller/src/user/api.rs
new file mode 100644
index 0000000..c9b82e1
--- /dev/null
+++ b/controller/src/user/api.rs
@@ -0,0 +1,110 @@
+use dropshot::{
+    endpoint, EmptyScanParams, HttpError, HttpResponseOk, PaginationParams, Path, Query,
+    RequestContext, ResultsPage, WhichPage,
+};
+use dropshot::{ApiDescription, ApiDescriptionRegisterError};
+use schemars::JsonSchema;
+use serde::Deserialize;
+use serde::Serialize;
+use trace_request::trace_request;
+use uuid::Uuid;
+
+use std::sync::Arc;
+
+use super::User;
+use crate::context::ControllerContext;
+
+#[derive(Deserialize, JsonSchema, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct UsersPathParams {
+    user_id: Uuid,
+}
+
+#[derive(Deserialize, JsonSchema, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct UserPage {
+    user_id: Uuid,
+}
+
+pub fn register_api(
+    api: &mut ApiDescription<Arc<ControllerContext>>,
+) -> Result<(), ApiDescriptionRegisterError> {
+    api.register(get_user_by_id)?;
+    api.register(list_users)
+}
+
+/// Fetch user info.
+#[endpoint {
+    method = GET,
+    path = "/users/{userId}",
+    tags = [ "user" ],
+}]
+#[trace_request]
+async fn get_user_by_id(
+    rqctx: RequestContext<Arc<ControllerContext>>,
+    params: Path<UsersPathParams>,
+) -> Result<HttpResponseOk<User>, HttpError> {
+    let id = params.into_inner().user_id;
+    tracing::debug!(id = id.to_string(), "Getting user by id");
+
+    let pg = rqctx.context().pg_pool.to_owned();
+
+    let rec = sqlx::query!(r#"SELECT * FROM users WHERE id = $1"#, id)
+        .fetch_one(&pg)
+        .await
+        .map_err(|e| match e {
+            sqlx::Error::RowNotFound => {
+                HttpError::for_not_found(None, format!("User not found by id: {:?}", id))
+            }
+            err => HttpError::for_internal_error(format!("Error: {}", err)),
+        })?;
+
+    let user = User {
+        id: rec.id,
+        name: rec.name,
+    };
+
+    Ok(HttpResponseOk(user))
+}
+
+/// List users
+#[endpoint {
+    method = GET,
+    path = "/users",
+    tags = [ "user" ],
+}]
+#[trace_request]
+async fn list_users(
+    rqctx: RequestContext<Arc<ControllerContext>>,
+    query: Query<PaginationParams<EmptyScanParams, UserPage>>,
+) -> Result<HttpResponseOk<ResultsPage<User>>, HttpError> {
+    let pag_params = query.into_inner();
+    let limit = rqctx.page_limit(&pag_params)?.get() as i64;
+    let pg = rqctx.context().pg_pool.to_owned();
+
+    let last_seen = match &pag_params.page {
+        WhichPage::Next(UserPage { user_id: id }) => Some(id),
+        _ => None,
+    };
+
+    let users = sqlx::query!(
+        r#"SELECT * FROM users WHERE id > coalesce($1, '00000000-0000-0000-0000-000000000000'::UUID) ORDER BY id LIMIT $2"#,
+        last_seen,
+        limit
+    )
+    .fetch_all(&pg)
+    .await
+    .map_err(|e| HttpError::for_internal_error(format!("Error: {}", e)))?
+    .into_iter()
+    .map(|rec| User {
+        id: rec.id,
+        name: rec.name,
+    })
+    .collect();
+
+    Ok(HttpResponseOk(ResultsPage::new(
+        users,
+        &EmptyScanParams {},
+        |u: &User, _| UserPage { user_id: u.id },
+    )?))
+}
diff --git a/controller/src/user/mod.rs b/controller/src/user/mod.rs
new file mode 100644
index 0000000..46397e9
--- /dev/null
+++ b/controller/src/user/mod.rs
@@ -0,0 +1,14 @@
+use schemars::JsonSchema;
+use serde::Serialize;
+use uuid::Uuid;
+
+mod api;
+
+pub use self::api::register_api;
+
+/// User
+#[derive(Serialize, JsonSchema)]
+struct User {
+    id: Uuid,
+    name: String,
+}
diff --git a/flake.nix b/flake.nix
index 4b22c31..ac97f6e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -49,6 +49,8 @@
           root = ./.;
           fileset = pkgs.lib.fileset.unions [
             ./api.json
+            ./controller/.sqlx
+            ./controller/migrations
             (craneLib.fileset.commonCargoSources ./.)
           ];
         };
@@ -116,6 +118,7 @@
         formatter =
           (treefmt-nix.lib.evalModule pkgs {
             projectRootFile = "flake.nix";
+
             programs = {
               nixfmt.enable = true;
               nixfmt.package = pkgs.nixfmt-rfc-style;
@@ -141,6 +144,8 @@
               just
               nixfmt-rfc-style
               rust-dev-toolchain
+              sqls
+              sqlx-cli
               watchexec
             ]
             ++ commonArgs.buildInputs;
diff --git a/justfile b/justfile
index f7b69d7..a953f53 100644
--- a/justfile
+++ b/justfile
@@ -5,11 +5,13 @@ default:
 	@just --choose
 
 # Run controller
+[group('controller')]
 run-controller $RUST_LOG="debug,h2=info,hyper_util=info,tower=info":
   cargo run --package patagia-controller -- --log-stderr
 
 # Run controller local development
-dev-controller:
+[group('controller')]
+dev-controller: dev-controller-db-migrate
   watchexec --clear --restart --stop-signal INT --debounce 300ms -- just run-controller
 
 # Run agent
@@ -48,6 +50,10 @@ machete:
 open-api:
   cargo xtask open-api
 
+# Update OpenAPI spec
+gen-open-api:
+  cargo xtask open-api > api.json
+
 # Run all tests
 check: check-nix
 
@@ -56,7 +62,12 @@ check-nix:
   nix flake check
 
 # Run PostgreSQL for development and testing
+[group('controller')]
 dev-postgres:
+  #!/usr/bin/env sh
+  if podman ps --filter "name=patagia-postgres" --filter "status=running" -q | grep -q .; then
+    exit 0
+  fi
   mkdir -p "${XDG_RUNTIME_DIR}/patagia-postgres"
   podman volume exists patagia-postgres || podman volume create patagia-postgres
   podman run \
@@ -69,12 +80,33 @@ dev-postgres:
   --volume patagia-postgres:/var/lib/postgresql/data \
   --volume "${XDG_RUNTIME_DIR}/patagia-postgres:/var/run/postgresql" \
   docker.io/postgres:17
+  sleep 0.3
 
 # Clean up PostgreSQL data
+[group('controller')]
 dev-postgres-clean:
   podman rm -f patagia-postgres || true
   podman volume rm patagia-postgres || true
 
 # Connect to PostgreSQL with psql
+[group('controller')]
 dev-postgres-psql:
   podman exec -it patagia-postgres psql -U patagia
+
+[group('controller')]
+[working-directory: 'controller']
+dev-controller-db-migrate: dev-postgres
+  cargo sqlx migrate run
+
+[group('controller')]
+[working-directory: 'controller']
+dev-controller-db-reset:
+  cargo sqlx db reset -y
+
+[group('controller')]
+[working-directory: 'controller']
+gen-controller-sqlx-prepare:
+  cargo sqlx prepare
+
+gen: gen-open-api gen-controller-sqlx-prepare fmt
+

From d7f4d877b65ea9cac1d032aad960abd48a2d888f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lars=20Sj=C3=B6strom?= <lars@radicore.se>
Date: Wed, 8 Jan 2025 11:09:34 +0100
Subject: [PATCH 3/3] feat(hostd): varlink interfaced host controller to manage
 machine configuration and boot mgmt

---
 Cargo.lock                            | 255 +++++++++++++++++-----
 Cargo.toml                            |   1 +
 hostd/.gitignore                      |   1 +
 hostd/Cargo.toml                      |  12 ++
 hostd/build.rs                        |   6 +
 hostd/src/io.patagia.Hostd.varlink    |  27 +++
 hostd/src/io.systemd.Hostname.varlink |  32 +++
 hostd/src/io_patagia_Hostd.rs         | 264 +++++++++++++++++++++++
 hostd/src/io_systemd_Hostname.rs      | 295 ++++++++++++++++++++++++++
 hostd/src/main.rs                     |   3 +
 10 files changed, 846 insertions(+), 50 deletions(-)
 create mode 100644 hostd/.gitignore
 create mode 100644 hostd/Cargo.toml
 create mode 100644 hostd/build.rs
 create mode 100644 hostd/src/io.patagia.Hostd.varlink
 create mode 100644 hostd/src/io.systemd.Hostname.varlink
 create mode 100644 hostd/src/io_patagia_Hostd.rs
 create mode 100644 hostd/src/io_systemd_Hostname.rs
 create mode 100644 hostd/src/main.rs

diff --git a/Cargo.lock b/Cargo.lock
index 67b0d67..b9cefe0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,7 +23,7 @@ version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "once_cell",
  "version_check",
  "zerocopy",
@@ -59,6 +59,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "anstream"
 version = "0.6.18"
@@ -133,7 +142,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -144,7 +153,7 @@ checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -222,7 +231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
 dependencies = [
  "addr2line",
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
  "miniz_oxide",
  "object",
@@ -298,6 +307,12 @@ dependencies = [
  "shlex",
 ]
 
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -310,6 +325,12 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
+[[package]]
+name = "chainerror"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce1bb7fb0c258a6600d699950da347a7a9dad66c3ce815769b5f11cf8fce78e"
+
 [[package]]
 name = "chrono"
 version = "0.4.39"
@@ -355,7 +376,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -441,7 +462,7 @@ version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
@@ -522,7 +543,7 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "dirs-sys-next",
 ]
 
@@ -545,7 +566,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -616,7 +637,7 @@ dependencies = [
  "semver",
  "serde",
  "serde_tokenstream",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -640,7 +661,7 @@ version = "0.8.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
@@ -665,7 +686,7 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "home",
  "windows-sys 0.48.0",
 ]
@@ -811,7 +832,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -854,13 +875,22 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "js-sys",
  "libc",
  "wasi",
@@ -979,6 +1009,16 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "hostd"
+version = "0.2.0"
+dependencies = [
+ "anyhow",
+ "tokio",
+ "varlink",
+ "varlink_generator",
+]
+
 [[package]]
 name = "hostname"
 version = "0.3.1"
@@ -996,7 +1036,7 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
  "windows",
 ]
@@ -1272,7 +1312,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1488,7 +1528,7 @@ version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "digest",
 ]
 
@@ -1498,6 +1538,15 @@ version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "mime"
 version = "0.3.17"
@@ -1663,7 +1712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
 dependencies = [
  "bitflags",
- "cfg-if",
+ "cfg-if 1.0.0",
  "foreign-types",
  "libc",
  "once_cell",
@@ -1679,7 +1728,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -1825,7 +1874,7 @@ version = "0.9.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
  "redox_syscall",
  "smallvec",
@@ -1876,6 +1925,33 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "peg"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f76678828272f177ac33b7e2ac2e3e73cc6c1cd1e3e387928aa69562fa51367"
+dependencies = [
+ "peg-macros",
+ "peg-runtime",
+]
+
+[[package]]
+name = "peg-macros"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "636d60acf97633e48d266d7415a9355d4389cea327a193f87df395d88cd2b14d"
+dependencies = [
+ "peg-runtime",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "peg-runtime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555b1514d2d99d78150d3c799d4c357a3e2c2a8062cd108e93a06d9057629c5"
+
 [[package]]
 name = "pem-rfc7468"
 version = "0.7.0"
@@ -1908,7 +1984,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2016,7 +2092,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
- "syn",
+ "syn 2.0.95",
  "thiserror 1.0.69",
  "typify",
  "unicode-ident",
@@ -2037,7 +2113,7 @@ dependencies = [
  "serde_json",
  "serde_tokenstream",
  "serde_yaml",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2060,7 +2136,7 @@ dependencies = [
  "itertools",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2286,7 +2362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
  "cc",
- "cfg-if",
+ "cfg-if 1.0.0",
  "getrandom",
  "libc",
  "spin",
@@ -2453,7 +2529,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde_derive_internals",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2524,7 +2600,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2535,7 +2611,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2578,7 +2654,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2612,7 +2688,7 @@ version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "cpufeatures",
  "digest",
 ]
@@ -2623,7 +2699,7 @@ version = "0.10.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "cpufeatures",
  "digest",
 ]
@@ -2825,7 +2901,7 @@ dependencies = [
  "quote",
  "sqlx-core",
  "sqlx-macros-core",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -2848,7 +2924,7 @@ dependencies = [
  "sqlx-mysql",
  "sqlx-postgres",
  "sqlx-sqlite",
- "syn",
+ "syn 2.0.95",
  "tempfile",
  "tokio",
  "url",
@@ -2991,6 +3067,17 @@ version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
 [[package]]
 name = "syn"
 version = "2.0.95"
@@ -3019,7 +3106,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3055,7 +3142,7 @@ version = "3.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "fastrand",
  "getrandom",
  "once_cell",
@@ -3110,7 +3197,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3121,7 +3208,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3130,7 +3217,7 @@ version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "once_cell",
 ]
 
@@ -3218,7 +3305,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3400,7 +3487,7 @@ dependencies = [
  "http",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
  "tracing",
 ]
 
@@ -3424,7 +3511,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -3528,7 +3615,7 @@ dependencies = [
  "semver",
  "serde",
  "serde_json",
- "syn",
+ "syn 2.0.95",
  "thiserror 1.0.69",
  "unicode-ident",
 ]
@@ -3546,10 +3633,21 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_tokenstream",
- "syn",
+ "syn 2.0.95",
  "typify-impl",
 ]
 
+[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "winapi",
+]
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.18"
@@ -3577,6 +3675,22 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
 
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "unix_socket"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+]
+
 [[package]]
 name = "unsafe-libyaml"
 version = "0.2.11"
@@ -3634,6 +3748,47 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
+[[package]]
+name = "varlink"
+version = "11.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "409e275987d74665c23610c0959c133360cafd761c1a6ddb1ca6d0685c8cef5d"
+dependencies = [
+ "libc",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tempfile",
+ "uds_windows",
+ "unix_socket",
+ "winapi",
+]
+
+[[package]]
+name = "varlink_generator"
+version = "10.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d8ff746c5b65d4bfb3a50f630b85cfb6a9d59f18720126e3ebd6bc98527fa51"
+dependencies = [
+ "chainerror",
+ "getopts",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "varlink_parser",
+]
+
+[[package]]
+name = "varlink_parser"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb9f3c1e8ccb33cdb6c84a4477ef3f3884ce6f4b70514ef1fbf7686eae921e"
+dependencies = [
+ "ansi_term",
+ "chainerror",
+ "peg",
+]
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
@@ -3682,7 +3837,7 @@ version = "0.2.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "once_cell",
  "wasm-bindgen-macro",
 ]
@@ -3697,7 +3852,7 @@ dependencies = [
  "log",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
  "wasm-bindgen-shared",
 ]
 
@@ -3707,7 +3862,7 @@ version = "0.4.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "js-sys",
  "once_cell",
  "wasm-bindgen",
@@ -3732,7 +3887,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -4065,7 +4220,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
  "synstructure",
 ]
 
@@ -4087,7 +4242,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
@@ -4107,7 +4262,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
  "synstructure",
 ]
 
@@ -4136,7 +4291,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.95",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index ca5b5c1..653c291 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ members = [
   "agent",
   "controller",
   "instrumentation",
+  "hostd",
   "trace-request",
   "xtask",
 ]
diff --git a/hostd/.gitignore b/hostd/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/hostd/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/hostd/Cargo.toml b/hostd/Cargo.toml
new file mode 100644
index 0000000..f6d79cc
--- /dev/null
+++ b/hostd/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "hostd"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+tokio.workspace = true
+varlink = "11.0.1"
+
+[build-dependencies]
+varlink_generator = "10.1.0"
diff --git a/hostd/build.rs b/hostd/build.rs
new file mode 100644
index 0000000..c12279a
--- /dev/null
+++ b/hostd/build.rs
@@ -0,0 +1,6 @@
+extern crate varlink_generator;
+
+fn main() {
+    varlink_generator::cargo_build_tosource("src/io.systemd.Hostname.varlink", true);
+    varlink_generator::cargo_build_tosource("src/io.patagia.Hostd.varlink", true);
+}
diff --git a/hostd/src/io.patagia.Hostd.varlink b/hostd/src/io.patagia.Hostd.varlink
new file mode 100644
index 0000000..46123a8
--- /dev/null
+++ b/hostd/src/io.patagia.Hostd.varlink
@@ -0,0 +1,27 @@
+interface io.patagia.Hostd
+
+type Label (
+  key: string,
+  value: string
+)
+
+type PatagiaAgentConfig (
+  url: string,
+  extraMounts: [string]string
+)
+
+type Machine(
+  machineId: string,
+  nodeLabels: ?[]Label,
+  patagiaAgent: ?PatagiaAgentConfig
+)
+
+method Describe() -> (
+  machine: Machine
+)
+
+method Apply(
+  machine: Machine
+) -> ()
+
+error InvalidMachineConfig()
diff --git a/hostd/src/io.systemd.Hostname.varlink b/hostd/src/io.systemd.Hostname.varlink
new file mode 100644
index 0000000..be4cf01
--- /dev/null
+++ b/hostd/src/io.systemd.Hostname.varlink
@@ -0,0 +1,32 @@
+interface io.systemd.Hostname
+
+method Describe() -> (
+	Hostname: string,
+	StaticHostname: ?string,
+	PrettyHostname: ?string,
+	DefaultHostname: ?string,
+	HostnameSource: string,
+	IconName: ?string,
+	Chassis: ?string,
+	Deployment: ?string,
+	Location: ?string,
+	KernelName: string,
+	KernelRelease: string,
+	KernelVersion: string,
+	OperatingSystemPrettyName: ?string,
+	OperatingSystemCPEName: ?string,
+	OperatingSystemHomeURL: ?string,
+	OperatingSystemSupportEnd: ?int,
+	OperatingSystemReleaseData: ?[]string,
+	MachineInformationData: ?[]string,
+	HardwareVendor: ?string,
+	HardwareModel: ?string,
+	HardwareSerial: ?string,
+	FirmwareVersion: ?string,
+	FirmwareVendor: ?string,
+	FirmwareDate: ?int,
+	MachineID: string,
+	BootID: string,
+	ProductUUID: ?string,
+	VSockCID: ?int
+)
diff --git a/hostd/src/io_patagia_Hostd.rs b/hostd/src/io_patagia_Hostd.rs
new file mode 100644
index 0000000..a2de13d
--- /dev/null
+++ b/hostd/src/io_patagia_Hostd.rs
@@ -0,0 +1,264 @@
+#![doc = "This file was automatically generated by the varlink rust generator"]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+use serde_derive::{Deserialize, Serialize};
+use std::io::BufRead;
+use std::sync::{Arc, RwLock};
+use varlink::{self, CallTrait};
+#[allow(dead_code)]
+#[derive(Clone, PartialEq, Debug)]
+#[allow(clippy::enum_variant_names)]
+pub enum ErrorKind {
+    Varlink_Error,
+    VarlinkReply_Error,
+    InvalidMachineConfig(Option<InvalidMachineConfig_Args>),
+}
+impl ::std::fmt::Display for ErrorKind {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        match self {
+            ErrorKind::Varlink_Error => write!(f, "Varlink Error"),
+            ErrorKind::VarlinkReply_Error => write!(f, "Varlink error reply"),
+            ErrorKind::InvalidMachineConfig(v) => {
+                write!(f, "io.patagia.Hostd.InvalidMachineConfig: {:#?}", v)
+            }
+        }
+    }
+}
+pub struct Error(
+    pub ErrorKind,
+    pub Option<Box<dyn std::error::Error + 'static + Send + Sync>>,
+    pub Option<&'static str>,
+);
+impl Error {
+    #[allow(dead_code)]
+    pub fn kind(&self) -> &ErrorKind {
+        &self.0
+    }
+}
+impl From<ErrorKind> for Error {
+    fn from(e: ErrorKind) -> Self {
+        Error(e, None, None)
+    }
+}
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        self.1
+            .as_ref()
+            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
+    }
+}
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        std::fmt::Display::fmt(&self.0, f)
+    }
+}
+impl std::fmt::Debug for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        use std::error::Error as StdError;
+        if let Some(ref o) = self.2 {
+            std::fmt::Display::fmt(o, f)?;
+        }
+        std::fmt::Debug::fmt(&self.0, f)?;
+        if let Some(e) = self.source() {
+            std::fmt::Display::fmt("\nCaused by:\n", f)?;
+            std::fmt::Debug::fmt(&e, f)?;
+        }
+        Ok(())
+    }
+}
+#[allow(dead_code)]
+pub type Result<T> = std::result::Result<T, Error>;
+impl From<varlink::Error> for Error {
+    fn from(e: varlink::Error) -> Self {
+        match e.kind() {
+            varlink::ErrorKind::VarlinkErrorReply(r) => Error(
+                ErrorKind::from(r),
+                Some(Box::from(e)),
+                Some(concat!(file!(), ":", line!(), ": ")),
+            ),
+            _ => Error(
+                ErrorKind::Varlink_Error,
+                Some(Box::from(e)),
+                Some(concat!(file!(), ":", line!(), ": ")),
+            ),
+        }
+    }
+}
+#[allow(dead_code)]
+impl Error {
+    pub fn source_varlink_kind(&self) -> Option<&varlink::ErrorKind> {
+        use std::error::Error as StdError;
+        let mut s: &dyn StdError = self;
+        while let Some(c) = s.source() {
+            let k = self
+                .source()
+                .and_then(|e| e.downcast_ref::<varlink::Error>())
+                .map(|e| e.kind());
+            if k.is_some() {
+                return k;
+            }
+            s = c;
+        }
+        None
+    }
+}
+impl From<&varlink::Reply> for ErrorKind {
+    #[allow(unused_variables)]
+    fn from(e: &varlink::Reply) -> Self {
+        match e {
+            varlink::Reply {
+                error: Some(ref t), ..
+            } if t == "io.patagia.Hostd.InvalidMachineConfig" => match e {
+                varlink::Reply {
+                    parameters: Some(p),
+                    ..
+                } => match serde_json::from_value(p.clone()) {
+                    Ok(v) => ErrorKind::InvalidMachineConfig(v),
+                    Err(_) => ErrorKind::InvalidMachineConfig(None),
+                },
+                _ => ErrorKind::InvalidMachineConfig(None),
+            },
+            _ => ErrorKind::VarlinkReply_Error,
+        }
+    }
+}
+pub trait VarlinkCallError: varlink::CallTrait {
+    fn reply_invalid_machine_config(&mut self) -> varlink::Result<()> {
+        self.reply_struct(varlink::Reply::error(
+            "io.patagia.Hostd.InvalidMachineConfig",
+            None,
+        ))
+    }
+}
+impl<'a> VarlinkCallError for varlink::Call<'a> {}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct r#Label {
+    pub r#key: String,
+    pub r#value: String,
+}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct r#Machine {
+    pub r#machineId: String,
+    pub r#nodeLabels: Option<Vec<Label>>,
+    pub r#patagiaAgent: Option<PatagiaAgentConfig>,
+}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct r#PatagiaAgentConfig {
+    pub r#url: String,
+    pub r#extraMounts: varlink::StringHashMap<String>,
+}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct InvalidMachineConfig_Args {}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct Apply_Reply {}
+impl varlink::VarlinkReply for Apply_Reply {}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct Apply_Args {
+    pub r#machine: Machine,
+}
+pub trait Call_Apply: VarlinkCallError {
+    fn reply(&mut self) -> varlink::Result<()> {
+        self.reply_struct(varlink::Reply::parameters(None))
+    }
+}
+impl<'a> Call_Apply for varlink::Call<'a> {}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct Describe_Reply {
+    pub r#machine: Machine,
+}
+impl varlink::VarlinkReply for Describe_Reply {}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct Describe_Args {}
+pub trait Call_Describe: VarlinkCallError {
+    fn reply(&mut self, r#machine: Machine) -> varlink::Result<()> {
+        self.reply_struct(Describe_Reply { r#machine }.into())
+    }
+}
+impl<'a> Call_Describe for varlink::Call<'a> {}
+pub trait VarlinkInterface {
+    fn apply(&self, call: &mut dyn Call_Apply, r#machine: Machine) -> varlink::Result<()>;
+    fn describe(&self, call: &mut dyn Call_Describe) -> varlink::Result<()>;
+    fn call_upgraded(
+        &self,
+        _call: &mut varlink::Call,
+        _bufreader: &mut dyn BufRead,
+    ) -> varlink::Result<Vec<u8>> {
+        Ok(Vec::new())
+    }
+}
+pub trait VarlinkClientInterface {
+    fn apply(&mut self, r#machine: Machine) -> varlink::MethodCall<Apply_Args, Apply_Reply, Error>;
+    fn describe(&mut self) -> varlink::MethodCall<Describe_Args, Describe_Reply, Error>;
+}
+#[allow(dead_code)]
+pub struct VarlinkClient {
+    connection: Arc<RwLock<varlink::Connection>>,
+}
+impl VarlinkClient {
+    #[allow(dead_code)]
+    pub fn new(connection: Arc<RwLock<varlink::Connection>>) -> Self {
+        VarlinkClient { connection }
+    }
+}
+impl VarlinkClientInterface for VarlinkClient {
+    fn apply(&mut self, r#machine: Machine) -> varlink::MethodCall<Apply_Args, Apply_Reply, Error> {
+        varlink::MethodCall::<Apply_Args, Apply_Reply, Error>::new(
+            self.connection.clone(),
+            "io.patagia.Hostd.Apply",
+            Apply_Args { r#machine },
+        )
+    }
+    fn describe(&mut self) -> varlink::MethodCall<Describe_Args, Describe_Reply, Error> {
+        varlink::MethodCall::<Describe_Args, Describe_Reply, Error>::new(
+            self.connection.clone(),
+            "io.patagia.Hostd.Describe",
+            Describe_Args {},
+        )
+    }
+}
+#[allow(dead_code)]
+pub struct VarlinkInterfaceProxy {
+    inner: Box<dyn VarlinkInterface + Send + Sync>,
+}
+#[allow(dead_code)]
+pub fn new(inner: Box<dyn VarlinkInterface + Send + Sync>) -> VarlinkInterfaceProxy {
+    VarlinkInterfaceProxy { inner }
+}
+impl varlink::Interface for VarlinkInterfaceProxy {
+    fn get_description(&self) -> &'static str {
+        "interface io.patagia.Hostd\n\ntype Label (\n  key: string,\n  value: string\n)\n\ntype PatagiaAgentConfig (\n  url: string,\n  extraMounts: [string]string\n)\n\ntype Machine(\n  machineId: string,\n  nodeLabels: ?[]Label,\n  patagiaAgent: ?PatagiaAgentConfig\n)\n\nmethod Describe() -> (\n  machine: Machine\n)\n\nmethod Apply(\n  machine: Machine\n) -> ()\n\nerror InvalidMachineConfig()\n"
+    }
+    fn get_name(&self) -> &'static str {
+        "io.patagia.Hostd"
+    }
+    fn call_upgraded(
+        &self,
+        call: &mut varlink::Call,
+        bufreader: &mut dyn BufRead,
+    ) -> varlink::Result<Vec<u8>> {
+        self.inner.call_upgraded(call, bufreader)
+    }
+    fn call(&self, call: &mut varlink::Call) -> varlink::Result<()> {
+        let req = call.request.unwrap();
+        match req.method.as_ref() {
+            "io.patagia.Hostd.Apply" => {
+                if let Some(args) = req.parameters.clone() {
+                    let args: Apply_Args = match serde_json::from_value(args) {
+                        Ok(v) => v,
+                        Err(e) => {
+                            let es = format!("{}", e);
+                            let _ = call.reply_invalid_parameter(es.clone());
+                            return Err(varlink::context!(varlink::ErrorKind::SerdeJsonDe(es)));
+                        }
+                    };
+                    self.inner
+                        .apply(call as &mut dyn Call_Apply, args.r#machine)
+                } else {
+                    call.reply_invalid_parameter("parameters".into())
+                }
+            }
+            "io.patagia.Hostd.Describe" => self.inner.describe(call as &mut dyn Call_Describe),
+            m => call.reply_method_not_found(String::from(m)),
+        }
+    }
+}
diff --git a/hostd/src/io_systemd_Hostname.rs b/hostd/src/io_systemd_Hostname.rs
new file mode 100644
index 0000000..e9fa185
--- /dev/null
+++ b/hostd/src/io_systemd_Hostname.rs
@@ -0,0 +1,295 @@
+#![doc = "This file was automatically generated by the varlink rust generator"]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+use serde_derive::{Deserialize, Serialize};
+use std::io::BufRead;
+use std::sync::{Arc, RwLock};
+use varlink::{self, CallTrait};
+#[allow(dead_code)]
+#[derive(Clone, PartialEq, Debug)]
+#[allow(clippy::enum_variant_names)]
+pub enum ErrorKind {
+    Varlink_Error,
+    VarlinkReply_Error,
+}
+impl ::std::fmt::Display for ErrorKind {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        match self {
+            ErrorKind::Varlink_Error => write!(f, "Varlink Error"),
+            ErrorKind::VarlinkReply_Error => write!(f, "Varlink error reply"),
+        }
+    }
+}
+pub struct Error(
+    pub ErrorKind,
+    pub Option<Box<dyn std::error::Error + 'static + Send + Sync>>,
+    pub Option<&'static str>,
+);
+impl Error {
+    #[allow(dead_code)]
+    pub fn kind(&self) -> &ErrorKind {
+        &self.0
+    }
+}
+impl From<ErrorKind> for Error {
+    fn from(e: ErrorKind) -> Self {
+        Error(e, None, None)
+    }
+}
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        self.1
+            .as_ref()
+            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
+    }
+}
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        std::fmt::Display::fmt(&self.0, f)
+    }
+}
+impl std::fmt::Debug for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        use std::error::Error as StdError;
+        if let Some(ref o) = self.2 {
+            std::fmt::Display::fmt(o, f)?;
+        }
+        std::fmt::Debug::fmt(&self.0, f)?;
+        if let Some(e) = self.source() {
+            std::fmt::Display::fmt("\nCaused by:\n", f)?;
+            std::fmt::Debug::fmt(&e, f)?;
+        }
+        Ok(())
+    }
+}
+#[allow(dead_code)]
+pub type Result<T> = std::result::Result<T, Error>;
+impl From<varlink::Error> for Error {
+    fn from(e: varlink::Error) -> Self {
+        match e.kind() {
+            varlink::ErrorKind::VarlinkErrorReply(r) => Error(
+                ErrorKind::from(r),
+                Some(Box::from(e)),
+                Some(concat!(file!(), ":", line!(), ": ")),
+            ),
+            _ => Error(
+                ErrorKind::Varlink_Error,
+                Some(Box::from(e)),
+                Some(concat!(file!(), ":", line!(), ": ")),
+            ),
+        }
+    }
+}
+#[allow(dead_code)]
+impl Error {
+    pub fn source_varlink_kind(&self) -> Option<&varlink::ErrorKind> {
+        use std::error::Error as StdError;
+        let mut s: &dyn StdError = self;
+        while let Some(c) = s.source() {
+            let k = self
+                .source()
+                .and_then(|e| e.downcast_ref::<varlink::Error>())
+                .map(|e| e.kind());
+            if k.is_some() {
+                return k;
+            }
+            s = c;
+        }
+        None
+    }
+}
+impl From<&varlink::Reply> for ErrorKind {
+    #[allow(unused_variables)]
+    fn from(e: &varlink::Reply) -> Self {
+        match e {
+            _ => ErrorKind::VarlinkReply_Error,
+        }
+    }
+}
+pub trait VarlinkCallError: varlink::CallTrait {}
+impl<'a> VarlinkCallError for varlink::Call<'a> {}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct Describe_Reply {
+    pub r#Hostname: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#StaticHostname: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#PrettyHostname: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#DefaultHostname: Option<String>,
+    pub r#HostnameSource: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#IconName: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#Chassis: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#Deployment: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#Location: Option<String>,
+    pub r#KernelName: String,
+    pub r#KernelRelease: String,
+    pub r#KernelVersion: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#OperatingSystemPrettyName: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#OperatingSystemCPEName: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#OperatingSystemHomeURL: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#OperatingSystemSupportEnd: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#OperatingSystemReleaseData: Option<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#MachineInformationData: Option<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#HardwareVendor: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#HardwareModel: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#HardwareSerial: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#FirmwareVersion: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#FirmwareVendor: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#FirmwareDate: Option<i64>,
+    pub r#MachineID: String,
+    pub r#BootID: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#ProductUUID: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub r#VSockCID: Option<i64>,
+}
+impl varlink::VarlinkReply for Describe_Reply {}
+#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+pub struct Describe_Args {}
+pub trait Call_Describe: VarlinkCallError {
+    fn reply(
+        &mut self,
+        r#Hostname: String,
+        r#StaticHostname: Option<String>,
+        r#PrettyHostname: Option<String>,
+        r#DefaultHostname: Option<String>,
+        r#HostnameSource: String,
+        r#IconName: Option<String>,
+        r#Chassis: Option<String>,
+        r#Deployment: Option<String>,
+        r#Location: Option<String>,
+        r#KernelName: String,
+        r#KernelRelease: String,
+        r#KernelVersion: String,
+        r#OperatingSystemPrettyName: Option<String>,
+        r#OperatingSystemCPEName: Option<String>,
+        r#OperatingSystemHomeURL: Option<String>,
+        r#OperatingSystemSupportEnd: Option<i64>,
+        r#OperatingSystemReleaseData: Option<Vec<String>>,
+        r#MachineInformationData: Option<Vec<String>>,
+        r#HardwareVendor: Option<String>,
+        r#HardwareModel: Option<String>,
+        r#HardwareSerial: Option<String>,
+        r#FirmwareVersion: Option<String>,
+        r#FirmwareVendor: Option<String>,
+        r#FirmwareDate: Option<i64>,
+        r#MachineID: String,
+        r#BootID: String,
+        r#ProductUUID: Option<String>,
+        r#VSockCID: Option<i64>,
+    ) -> varlink::Result<()> {
+        self.reply_struct(
+            Describe_Reply {
+                r#Hostname,
+                r#StaticHostname,
+                r#PrettyHostname,
+                r#DefaultHostname,
+                r#HostnameSource,
+                r#IconName,
+                r#Chassis,
+                r#Deployment,
+                r#Location,
+                r#KernelName,
+                r#KernelRelease,
+                r#KernelVersion,
+                r#OperatingSystemPrettyName,
+                r#OperatingSystemCPEName,
+                r#OperatingSystemHomeURL,
+                r#OperatingSystemSupportEnd,
+                r#OperatingSystemReleaseData,
+                r#MachineInformationData,
+                r#HardwareVendor,
+                r#HardwareModel,
+                r#HardwareSerial,
+                r#FirmwareVersion,
+                r#FirmwareVendor,
+                r#FirmwareDate,
+                r#MachineID,
+                r#BootID,
+                r#ProductUUID,
+                r#VSockCID,
+            }
+            .into(),
+        )
+    }
+}
+impl<'a> Call_Describe for varlink::Call<'a> {}
+pub trait VarlinkInterface {
+    fn describe(&self, call: &mut dyn Call_Describe) -> varlink::Result<()>;
+    fn call_upgraded(
+        &self,
+        _call: &mut varlink::Call,
+        _bufreader: &mut dyn BufRead,
+    ) -> varlink::Result<Vec<u8>> {
+        Ok(Vec::new())
+    }
+}
+pub trait VarlinkClientInterface {
+    fn describe(&mut self) -> varlink::MethodCall<Describe_Args, Describe_Reply, Error>;
+}
+#[allow(dead_code)]
+pub struct VarlinkClient {
+    connection: Arc<RwLock<varlink::Connection>>,
+}
+impl VarlinkClient {
+    #[allow(dead_code)]
+    pub fn new(connection: Arc<RwLock<varlink::Connection>>) -> Self {
+        VarlinkClient { connection }
+    }
+}
+impl VarlinkClientInterface for VarlinkClient {
+    fn describe(&mut self) -> varlink::MethodCall<Describe_Args, Describe_Reply, Error> {
+        varlink::MethodCall::<Describe_Args, Describe_Reply, Error>::new(
+            self.connection.clone(),
+            "io.systemd.Hostname.Describe",
+            Describe_Args {},
+        )
+    }
+}
+#[allow(dead_code)]
+pub struct VarlinkInterfaceProxy {
+    inner: Box<dyn VarlinkInterface + Send + Sync>,
+}
+#[allow(dead_code)]
+pub fn new(inner: Box<dyn VarlinkInterface + Send + Sync>) -> VarlinkInterfaceProxy {
+    VarlinkInterfaceProxy { inner }
+}
+impl varlink::Interface for VarlinkInterfaceProxy {
+    fn get_description(&self) -> &'static str {
+        "interface io.systemd.Hostname\n\nmethod Describe() -> (\n\tHostname: string,\n\tStaticHostname: ?string,\n\tPrettyHostname: ?string,\n\tDefaultHostname: ?string,\n\tHostnameSource: string,\n\tIconName: ?string,\n\tChassis: ?string,\n\tDeployment: ?string,\n\tLocation: ?string,\n\tKernelName: string,\n\tKernelRelease: string,\n\tKernelVersion: string,\n\tOperatingSystemPrettyName: ?string,\n\tOperatingSystemCPEName: ?string,\n\tOperatingSystemHomeURL: ?string,\n\tOperatingSystemSupportEnd: ?int,\n\tOperatingSystemReleaseData: ?[]string,\n\tMachineInformationData: ?[]string,\n\tHardwareVendor: ?string,\n\tHardwareModel: ?string,\n\tHardwareSerial: ?string,\n\tFirmwareVersion: ?string,\n\tFirmwareVendor: ?string,\n\tFirmwareDate: ?int,\n\tMachineID: string,\n\tBootID: string,\n\tProductUUID: ?string,\n\tVSockCID: ?int\n)\n"
+    }
+    fn get_name(&self) -> &'static str {
+        "io.systemd.Hostname"
+    }
+    fn call_upgraded(
+        &self,
+        call: &mut varlink::Call,
+        bufreader: &mut dyn BufRead,
+    ) -> varlink::Result<Vec<u8>> {
+        self.inner.call_upgraded(call, bufreader)
+    }
+    fn call(&self, call: &mut varlink::Call) -> varlink::Result<()> {
+        let req = call.request.unwrap();
+        match req.method.as_ref() {
+            "io.systemd.Hostname.Describe" => self.inner.describe(call as &mut dyn Call_Describe),
+            m => call.reply_method_not_found(String::from(m)),
+        }
+    }
+}
diff --git a/hostd/src/main.rs b/hostd/src/main.rs
new file mode 100644
index 0000000..47ad8c6
--- /dev/null
+++ b/hostd/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+    println!("Hello World!");
+}