Expand description
@yah:ticket(R040-F9, “Lift KeysStore into shared crate; cloud reads vault then env”)
@yah:at(2026-05-05T00:33:17Z)
@yah:status(review)
@yah:assignee(agent:claude)
@yah:parent(R040)
@yah:handoff(“DRY-up landed: app/yah/cli/src/keys.rs lifted to crates/yah/keys (own Cargo.toml, ProjectDirs::data_dir() unchanged so the existing on-disk vault keeps working). app/yah/cli now keys = { path = ... } deps the new crate; aes-gcm and rand drops out of the CLI’s direct deps (transitive now). main.rs/agent.rs/agentd.rs swapped mod keys;/crate::keys:: for the external crate path. cloud crate gained keys dep + HetznerDriver::from_default_sources() that tries KeysStore::open().get(slot) per-key then falls back to env: hetzner-api-token↔HETZNER_API_TOKEN, hetzner-s3-access-key↔HETZNER_S3_ACCESS_KEY, hetzner-s3-secret-key↔HETZNER_S3_SECRET_KEY. Vault open errors are swallowed (no vault → fall back to env). yah cloud callsites (app/yah/cli/src/cloud.rs:257 + :423) flipped to from_default_sources(). Tests: 6/6 green for keys (moved from CLI), 26/26 green for cloud (no test changes — all existing). cargo check -p keys + -p cloud + -p yah –bin yah-agentd clean.”)
@yah:next(“Follow-up scoped as R043 (relay) with phases F1 bridge / F2 naming / F3 cleanup — unify desktop api_keys with this vault, KeysStore as canonical, drop keyring dep once soaked.”)
@yah:next(“Optional: yah keys CLI could grow --from-keychain <provider> flag to one-shot import a desktop-vault token without typing it. Not urgent; the user can already pipe via security find-generic-password ... | yah keys set --from-stdin <slot>.”)
@yah:verify(“cargo test -p keys”)
@yah:verify(“cargo test -p cloud”)
@yah:verify(“cargo check -p yah –bin yah-agentd”)
@yah:gotcha(“cargo check –workspace currently fails on app/yah/cli/src/cloud.rs:210 because handle_agent is referenced but not yet defined — that’s parallel R040-F7 WIP (yah cloud agent ping/services/logs against yah-yubaba), not this refactor. The keys = ... and HetznerDriver::from_default_sources additions compile clean on their own.”)
@yah:ticket(R040-F10, “Pass ssh_keys through to Hetzner create_server (pre-mesh SSH access)”)
@yah:at(2026-05-05T00:33:17Z)
@yah:status(review)
@yah:assignee(agent:claude)
@yah:parent(R040)
@yah:handoff(“Threaded ssh_keys end-to-end. MachineConfig.ssh_keys: Vec\"ssh_keys\": [...] to the POST body when non-empty (Hetzner ignores empty arrays anyway). provision.rs threads MachineConfig.ssh_keys → ProvisionRequest → ServerSpec. 6 test sites updated to add the field; 29/29 cloud tests green.”)
@yah:handoff(“Live-verified: provisioned yah-cloud-1 (cpx11, hil) via direct API (cloud-crate provision flow blew the 32KiB user_data cap embedding yubaba — separate ticket). With ssh_keys=[111513970, 111525493] the box accepted SSH on first boot via ~/.ssh/yah, no out-of-band root password recovery needed. The agentd round-trip succeeded over the socket: agent.list_sessions returned {sessions:[]}, bogus method returned -32601, stop-unknown returned {stopped:false}.”)
@yah:next(“Follow-up R040-Tx: 32KiB user_data cap blocks the canonical yah cloud machine provision path because the yubaba binary embeds at ~3MB after base64. Refactor cloud-init to fetch yubaba from a URL (GitHub release artifact during cloud-init runcmd) instead of inlining the bytes. Until then, provision flows that need yubaba need a different transport (post-boot scp + register-hostkey).”)
@yah:verify(“cargo test -p cloud”)
@yah:verify(“cargo run -p yah –bin yah – cloud machine status (sees ssh_keys field via the new yah-cloud-1.toml in .yah/cloud/machines/)”)
@yah:ticket(R040-F14, “Extract shared crates/yah/hetzner: lift transport+DTOs out of desktop and cloud parallel impls”) @yah:at(2026-05-05T00:33:17Z) @yah:assignee(agent:claude) @yah:status(review) @yah:parent(R040) @yah:handoff(“app/yah/desktop/src/hetzner.rs (~641 LOC) and crates/yah/cloud/src/provider/hetzner.rs (~560 LOC) are parallel HTTP clients. Both ship their own auth_client/check_status, RawServer/HetznerServer DTOs, error types. Lift the shared core into crates/yah/hetzner; both call sites become thin layers over it. Surfaced in R040-F12 + F13 (destroy-button work) where the obvious DRY move would have ballooned the change set.”) @yah:next(“New crate crates/yah/hetzner: HetznerClient (reqwest+bearer + check_status), wire DTOs (RawServer/RawSshKey/RawLocation/RawImage), HetznerError”) @yah:next(“Desktop hetzner.rs becomes UI-flow operations (list_server_types/_locations/_images stay desktop-only — catalog browse for the form) over the shared client”) @yah:next(“Cloud hetzner.rs becomes reconcile-flow operations (find_server_by_name, server_status, destroy_server, bucket APIs) over the shared client”) @yah:next(“Token-source convergence is a separate ticket: shared client takes &str token, desktop reads keychain blob, cloud reads KeysStore vault — unifying those vaults is its own refactor”) @yah:next(“Estimate: 2-4 hours; tests on both sides pass without functional change”)
@yah:ticket(R040-T17, “Decision-recorded: do NOT build floating-IP plumbing (superseded by Cloudflare Tunnels + Headscale mesh)”)
@yah:at(2026-05-05T00:33:17Z)
@yah:assignee(agent:claude)
@yah:status(review)
@yah:parent(R040)
@yah:kind(task)
@yah:handoff(“Question raised this session: ‘should yah cloud have one floating IP per region as an ingress target?’ Answer recorded so future-self doesn’t redo the analysis. NO — combination of R040-F15 (Cloudflare Tunnel for public ingress) + R040-F16 (Headscale mesh for inter-node TCP) means the entire stable-IP question evaporates. Public DNS points at yah cloud ip {create,list,destroy} subcommand — but DON’T pre-build any of that until reopened.”)
Structs§
- Hetzner
Driver - Hetzner Cloud + Object Storage driver for Phase-1 mirror bootstrap.