# Virtual workspace manifest for `runner-manager`. # # `crates/` sits at the repository root; there is no nested product directory # (`.taskflow/2026-08-21-local-runner-manager/02-target-architecture.md`, # "Workspace shape"). # # ---------------------------------------------------------------------------- # ADDING A NEW EXTERNAL CRATE IS AN A-GROUP CHANGE. # ---------------------------------------------------------------------------- # Task `a1` owns every `Cargo.toml` in this workspace and the whole # `[workspace.dependencies]` table below. Every member manifest consumes an # entry from it with `workspace = true` and never states a version of its own. # # The reason is mechanical, not stylistic: 22 later tasks run in parallel # against this tree. A task that edits a manifest also rewrites `Cargo.lock`, # and `Cargo.lock` is a single file that every one of them would then collide # on. So the member manifests below already list dependencies their crate does # not use yet, precisely so that no later task has to add one. # # If a task genuinely needs an external crate that is not listed here, that is # an A-group change: it is made in this file, by an A-group task, and not in a # member manifest. # # Versions are exact `major.minor.patch` selections; `Cargo.lock` is committed # and is what actually pins the resolved graph # (`02-target-architecture.md`: "Exact dependency versions are selected in # implementation, pinned with `Cargo.lock` and `rust-toolchain.toml`"). [workspace] resolver = "3" members = [ "crates/agent", "crates/app", "crates/coordinator", "crates/domain", "crates/github", "crates/platform", "crates/testkit", "tests", ] [workspace.package] version = "0.4.60" edition = "2024" rust-version = "1.94.0" license = "MIT" authors = ["Ivan Murzak"] repository = "https://github.com/IvanMurzak/GitHub-Runner-Scaler-UI" homepage = "https://github.com/IvanMurzak/GitHub-Runner-Scaler-UI" [workspace.dependencies] # --- Workspace members ------------------------------------------------------ # Each carries a `version` as well as a `path` so the published `runner-manager` # crate (D11, `cargo install`) can resolve them from the registry. runner-manager-agent = { path = "crates/agent", version = "0.4.60" } runner-manager-domain = { path = "crates/domain", version = "0.4.60" } runner-manager-github = { path = "crates/github", version = "0.4.60" } runner-manager-platform = { path = "crates/platform", version = "0.4.60" } # The coordinator server is deployed as a container image, never published; it # is a dev-dependency of the app's link tests and so carries no `version`. runner-manager-coordinator = { path = "crates/coordinator" } # `testkit` is never published; it is a dev-dependency everywhere and therefore # carries no `version`, which is what lets `cargo publish` strip it. runner-manager-testkit = { path = "crates/testkit" } # --- Async runtime and HTTP ------------------------------------------------- tokio = { version = "1.53.1", features = [ "fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time", ] } futures = "0.3.34" async-trait = "0.1.92" # `rustls` rather than the platform TLS stack: a released Linux binary that # links the system OpenSSL fails on hosts carrying a different major version, # and every documented install path here is "download the binary and run it" # (`09-release-distribution.md`). # # `query` gates `RequestBuilder::query()`, which every paginated GitHub call # needs: `c3` pages the runner inventory and filters in-progress runs, `c4` # pages queued runs and their jobs. Both are group C, so without this feature # both tasks would have to edit `crates/github/Cargo.toml`. # # `stream` gates `Response::bytes_stream()`. `e2` downloads the GitHub runner # package — 150-300 MB — and must verify its SHA-256 *before* extraction and # delete the partial file on mismatch. Without a stream the only option is # buffering the whole package in RAM on a home host, and there is no partial # file left on disk to remove. reqwest = { version = "0.13.5", default-features = false, features = [ "charset", "gzip", "http2", "json", "query", "rustls", "stream", "system-proxy", ] } url = "2.5.8" # --- Serialization and configuration ---------------------------------------- serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" # Non-secret local configuration is TOML (`05-infrastructure.md`). toml = "1.1.6" base64 = "0.23.1" hex = "0.4.3" # --- Persistence ------------------------------------------------------------ # `bundled` compiles SQLite from source, so no CI leg and no user machine needs # a system SQLite of a particular version. rusqlite = { version = "0.40.2", features = [ "bundled", "chrono", "serde_json", "uuid", ] } # --- Domain value types ----------------------------------------------------- uuid = { version = "1.26.1", features = ["serde", "v4"] } chrono = { version = "0.4.45", default-features = false, features = [ "clock", "serde", "std", ] } # --- Errors and diagnostics ------------------------------------------------- thiserror = "2.0.20" anyhow = "1.0.104" tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = [ "env-filter", "json", "registry", ] } # Rotating local agent diagnostics (`05-infrastructure.md`, `logs/`). tracing-appender = "0.2.5" # Keeps the user access token and the encoded JIT configuration out of `Debug` # output by construction rather than by review (`07-security.md`). secrecy = "0.10.3" # --- Terminal UI ------------------------------------------------------------ # Ratatui 0.30 keeps Crossterm as its default backend and re-exports it # (`02-target-architecture.md`). Crossterm is *also* declared directly, at the # version Ratatui uses, so the `bracketed-paste` and `event-stream` features # that `g1` requires are unified into Ratatui's own copy. `ratatui::init()` # alone enables neither (`03-control-flows.md`, flow 6.1). ratatui = "0.30.2" crossterm = { version = "0.29.0", features = ["bracketed-paste", "event-stream"] } # The TUI grid measures every cell in terminal COLUMNS, not bytes or chars: a # repository or runner name may be wide or combining, and a column that is one # cell too wide is exactly the ragged table this dependency exists to prevent. # `unicode-truncate` does the cutting and padding at that same measure, and is # grapheme-aware where hand-written character arithmetic is not. Ratatui already # builds both, so neither is a new crate in the tree. unicode-width = "0.2.2" unicode-truncate = "3.0.0" # --- Command line ----------------------------------------------------------- clap = { version = "4.6.6", features = ["derive", "env", "wrap_help"] } # --- Filesystem, packaging, and hashing ------------------------------------- # The runner package is a `.zip` on Windows and a `.tar.gz` elsewhere, and its # published SHA-256 is verified before extraction (`05-infrastructure.md`). sha2 = "0.11.0" flate2 = "1.1.10" tar = "0.4.46" zip = "8.6.0" tempfile = "3.27.0" directories = "6.0.0" rand = "0.10.2" remove_dir_all = "1.0.0" # --- Windows-only platform adapters ----------------------------------------- # DPAPI machine scope for the secret store (`d2`) and the service control # handler the Windows service host needs (`d3`). # # The `windows` crate gates every binding behind a feature module, so a missing # module is a compile error in the task that needs the call, not a runtime # surprise. Each one below is here because a named task's Definition of Done # cannot be met without it: # # Win32_Foundation `LocalFree`, `HANDLE`, `CloseHandle` — the # types every other module below returns. # Win32_Security parent of the two security modules; also # `SECURITY_ATTRIBUTES` for the restrictive # handles `d1` and `e3` create. # Win32_Security_Cryptography `CryptProtectData`/`CryptUnprotectData`, the # DPAPI machine scope behind `d2`. # Win32_Security_Authorization `SetNamedSecurityInfoW`/`GetNamedSecurityInfoW` # — `d2` must ACL the stored token to the service # account so an ordinary local user cannot read # it, and its DoD asserts that per OS, which # means reading the ACL back. # Win32_System_Threading `OpenProcess` + `GetProcessTimes`. `d1` needs a # process identity that survives a restart and # does *not* match a recycled PID; process start # time is the discriminator. `e3`'s # restart-recovery DoD rests on the same # primitive. # Win32_Storage_FileSystem `CreateFileW` taking `SECURITY_ATTRIBUTES`, for # `d1`'s restrictive handoff and `e3`'s # restrictive temporary file. Also # `GetVolumePathNameW` + `GetDriveTypeW`, which # `b1-runner-path-platform` uses to prove a # configured runner root is on a local volume # rather than a network share, and the # directory-handle open that answers "may this # account create entries here?" without writing # anything. # Win32_System_SystemInformation # `GetSystemDirectoryW`. # `b1-runner-path-platform`'s Definition of Done # is that the Windows default runner root is # `\rman` derived "without reading # a mutable `%SystemDrive%` value as authority", # and this is the platform API that answers it. # Win32_System_WindowsProgramming # the `DRIVE_*` return values of `GetDriveTypeW`. # They live in a different feature module from # the function itself; spelling the constants by # hand instead would be three magic numbers in # the one place that decides whether a runner # root is remote. # Win32_System_Registry `RegGetValueW`, `RegCreateKeyExW`, # `RegSetValueExW`, `RegDeleteKeyValueW` and # `RegEnumValueW`. `host doctor` reads # `LongPathsEnabled`, Developer Mode, the # PowerShell execution policy and Defender's # exclusion list without starting a process; # `host prepare` writes the first three back. # Defender's own keys are tamper-protected and go # through `Add-MpPreference` instead. Also a # prerequisite of `SHELLEXECUTEINFOW` below. # Win32_System_Diagnostics_ToolHelp # `CreateToolhelp32Snapshot` and # `Process32FirstW`/`Process32NextW`: the # parent links of every process, from which the # coordinator's job statistics find a native # runner's process tree (it runs in no Job # Object, so there is no job accounting). # Win32_System_ProcessStatus `GetProcessMemoryInfo`, the working set of # each process of that tree. # Win32_UI_Shell `ShellExecuteExW` with the `runas` verb, the # documented way to ask the user for elevation. # `host prepare` relaunches only its own fix step # that way, so a batch of fixes costs one UAC # prompt. # # None of these modules adds a package to `Cargo.lock`: the `windows` crate's # features are cfg gates over its own generated bindings and pull in no # optional dependency. windows = { version = "0.62.2", features = [ "Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", "Win32_System_ProcessStatus", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell", ] } windows-service = "0.8.1" # --- macOS-only platform adapters ------------------------------------------- # System Keychain for the machine-scoped secret store (`d2`). security-framework = "3.7.0" # `security-framework` wraps every Security.framework object in a Core # Foundation type and exposes the raw reference only through # `core_foundation::base::TCFType`. `d2` needs that reference for the one call # the wrapper does not make -- setting a stored item's access so that it is # granted to every application rather than to the single binary that wrote it, # without which a macOS daemon loses its own credential on every upgrade. This # adds no package to `Cargo.lock`: `security-framework` already resolves the # same 0.10 line. core-foundation = "0.10.1" # --- Unix-only platform adapters -------------------------------------------- # The Unix half of the same process-identity primitive the Windows block above # describes: `d1` needs a process identity that a recycled PID cannot forge, so # it pairs the PID with the process start time. Windows reads that through # `GetProcessTimes`; macOS reads it through `sysctl(KERN_PROC)` and Linux # through field 22 of `/proc//stat`, neither of which `std` exposes. # # `libc` rather than `sysinfo`, on three grounds. (1) `sysinfo` would only be # doing the Unix half anyway — Windows already goes through the `windows` crate # for `GetProcessTimes` — so it buys one platform path and leaves two sources # of truth for the same fact. (2) `sysinfo` reports process start time as whole # seconds since the epoch, which is coarser than a PID-recycling discriminator # wants, and it refreshes a whole process table to answer a question about one # PID. (3) `libc` is already in the resolved graph transitively, so declaring # it adds no package to `Cargo.lock` and nothing new to audit. The cost is the # two `unsafe` platform paths in `d1`, both short and both testable against the # calling process itself. libc = "0.2.189" # --- Optional coordinator --------------------------------------------------- # The coordinator server (`crates/coordinator`) and the daemon's outbound link # to it (`.taskflow/2026-11-07-coordinator-server`). `axum` serves the webhook, # enrollment and WebSocket endpoints behind a TLS-terminating proxy; # `tokio-tungstenite` is the host's client, over the same `rustls` the GitHub # client already uses, with the platform verifier `reqwest` already builds. # Hosts authenticate with Ed25519 challenge signatures; webhooks with # HMAC-SHA256 over the workspace's `sha2` 0.11. # RS256 for the coordinator's GitHub App JWT. Already in the resolved graph as # `rustls`'s crypto provider, so declaring it adds no package to `Cargo.lock`. aws-lc-rs = { version = "1.18.0", default-features = false, features = ["aws-lc-sys"] } axum = { version = "0.8.9", default-features = false, features = [ "http1", "json", "query", "tokio", "ws", ] } tokio-tungstenite = { version = "0.29.0", default-features = false, features = [ "connect", "handshake", "__rustls-tls", ] } rustls = { version = "0.23.44", default-features = false, features = ["aws_lc_rs", "std", "tls12"] } rustls-platform-verifier = "0.7.0" ed25519-dalek = { version = "3.0.0", features = ["zeroize"] } hmac = "0.13.0" # --- Test-only -------------------------------------------------------------- insta = "1.48.0" wiremock = "0.6.5" assert_cmd = "2.2.2" predicates = "3.1.4" serial_test = "4.0.1"