recall_worker/lib.rs
1//! `recall-worker`: the process that merges, so the one facing the internet
2//! does not have to.
3//!
4//! It is an enrolled device with the `worker` scope, running beside
5//! `recall-server` (in the same compose file, as a second service) with no
6//! inbound port. It long-polls the server's merge queue, reconciles each
7//! job's two versions with the local `claude` CLI, and posts the result
8//! back. The `claude` login lives on the worker's own volume, so a
9//! compromise of the API process no longer reaches it. See
10//! `docs/design/part5-plan.md`, "The worker", and the "Jobs" section of
11//! `docs/reference/api.md`.
12//!
13//! No Anthropic API key appears anywhere here, as anywhere in Recall: the
14//! merge rides whatever account the CLI on this machine is logged in to.
15//!
16//! The crate has two unconditional parts, so `recall-server` and (later)
17//! `recall`'s own CLI can use them without the job loop: [`merge`] is the
18//! merge itself, the prompt and the flags that keep it cheap, and
19//! [`redact`] finds and masks secrets in memory text
20//! (`docs/history/memory-truth.md` decision 2). Everything else is behind
21//! the `client` feature (on by default): enrolling, signing requests, and
22//! the job loop, with the HTTP client they need. The server turns it off.
23
24#![deny(missing_docs)]
25
26pub mod merge;
27pub mod redact;
28
29#[cfg(feature = "client")]
30pub mod api;
31#[cfg(feature = "client")]
32pub mod config;
33#[cfg(feature = "client")]
34pub mod evaluate;
35#[cfg(feature = "client")]
36pub mod identity;
37#[cfg(feature = "client")]
38pub mod worker;
39
40pub use merge::Merger;
41pub use redact::Redactor;
42
43use time::OffsetDateTime;
44
45/// A timestamp in the format every Recall API answer uses: JavaScript's
46/// `Date.toISOString()`, millisecond precision with a `Z`, such as
47/// `2026-09-03T21:49:55.191Z`. The same function as `recall_server::now`,
48/// kept here so the merge does not depend on the server.
49pub fn now() -> String {
50 let fmt = time::macros::format_description!(
51 "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"
52 );
53 OffsetDateTime::now_utc()
54 .format(&fmt)
55 .expect("the timestamp format is a compile-time constant")
56}
57
58/// What `recall-worker` sends as its `User-Agent`, and so what `/health`
59/// shows as the worker's agent: `recall-worker/0.4.2 (linux-x86_64)`.
60pub fn user_agent() -> String {
61 format!(
62 "recall-worker/{} ({}-{})",
63 recall_wire::discovery::version(),
64 std::env::consts::OS,
65 std::env::consts::ARCH
66 )
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn timestamps_match_javascripts_toisostring() {
75 let t = now();
76 assert_eq!(t.len(), 24, "got {t}");
77 assert!(t.ends_with('Z') && &t[10..11] == "T" && &t[19..20] == ".");
78 }
79
80 #[test]
81 fn the_agent_names_the_worker() {
82 assert!(
83 user_agent().starts_with("recall-worker/"),
84 "{}",
85 user_agent()
86 );
87 }
88}