Skip to main content

pitboard_core/
lib.rs

1//! The engine behind Pitboard: parking and restoring a person's own Claude Code, Codex and
2//! Claude Desktop logins, and reading what each Claude Code and Codex account has left. It
3//! serves Pitboard's own front ends, the command line and the native apps, which reach it
4//! through [`service::Pitboard`] with an explicit [`context::Context`].
5//!
6//! # What is supported
7//!
8//! This crate is published because the `pitboard` binary depends on it, not because it was
9//! designed for other programs to build on. The supported interface is [`service::Pitboard`],
10//! [`context::Context`], and the types those two return. Everything else public is what the
11//! front ends in this repository use, and may change in any release.
12//!
13//! What a version promises, for the part that is supported: a code is a name, and names are
14//! kept. Adding an error, warning or check code is not a breaking change, which is why every
15//! enum a caller reads codes out of is `#[non_exhaustive]` and every such caller needs a
16//! fallback arm. Renaming or removing a code is a breaking change: a new minor version of
17//! this crate, whose version is its own, apart from Pitboard's.
18//! A report a caller reads, such as what `uninstall` returns, may likewise say more in a
19//! later release, so those structs are `#[non_exhaustive]` too.
20//!
21//! What is deliberately not reachable: nothing outside this crate may write Pitboard's index.
22//! Every change goes through [`switch`], which records what it is about to do first.
23
24// Pitboard for Windows is being built, and reaches people only once the command line, the
25// app, their installers and their docs are done. Until then a Windows build of a release
26// compiles and refuses everything at run time (`release`). It compiles only for the two
27// targets its builds are made and tested on: an x64 or ARM64 PC with the MSVC toolchain.
28// A 32-bit, GNU, UWP or Arm64EC build is another program nobody has run.
29#[cfg(all(
30    windows,
31    not(all(
32        target_vendor = "pc",
33        target_env = "msvc",
34        any(target_arch = "x86_64", target_arch = "aarch64")
35    ))
36))]
37compile_error!(
38    "Pitboard for Windows is built for x86_64-pc-windows-msvc and aarch64-pc-windows-msvc \
39     alone, the targets it is built and tested on."
40);
41
42#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
43compile_error!(
44    "Pitboard runs on macOS, Linux and Windows. Another system needs a host of its own in \
45     `host/`, saying where its stores, processes and scheduler are."
46);
47
48pub(crate) mod api;
49pub mod app;
50pub mod assumptions;
51pub mod audit;
52pub mod context;
53// The Claude Desktop the tests play, reached as `testing::desktop`.
54#[cfg(any(test, feature = "test-support"))]
55#[doc(hidden)]
56#[allow(
57    clippy::disallowed_methods,
58    reason = "it leaves Claude Desktop's files as Claude does"
59)]
60pub mod desktop_testing;
61pub mod doctor;
62pub mod duty;
63pub mod error;
64pub(crate) mod in_use;
65pub mod label;
66pub mod provider;
67pub mod redact;
68pub mod release;
69pub mod schedule;
70pub mod service;
71pub(crate) mod settings;
72pub mod state;
73pub mod status;
74pub mod statusline;
75pub mod switch;
76pub mod time;
77pub mod usage;
78pub mod words;
79
80pub(crate) mod atomic;
81pub(crate) mod base64;
82pub(crate) mod fault;
83#[cfg(test)]
84mod helper;
85pub mod holder;
86pub(crate) mod home;
87pub mod host;
88pub(crate) mod lock;
89pub(crate) mod park;
90pub(crate) mod pending;
91pub(crate) mod proxy;
92pub(crate) mod sessions;
93/// The program the tests start in place of a tool, reached as `testing::stand_in`. Its own
94/// tests run with the core's.
95#[cfg(any(test, feature = "test-support"))]
96#[doc(hidden)]
97pub mod stand_in;
98pub(crate) mod store;
99
100/// What the integration tests reach into: they plant and inspect parked logins in the real
101/// store, and must never touch the credential slot this machine's Claude Code reads.
102#[cfg(feature = "test-support")]
103#[doc(hidden)]
104pub mod testing {
105    pub use crate::api::scripted::{Answer, Asked, ScriptedApi, Trouble};
106    pub use crate::desktop_testing as desktop;
107    pub use crate::duty::warm::run::{Started, WarmScript};
108    pub use crate::host::Ran;
109    /// What a test or a fixture does to a file's access, or to make a link, which only this
110    /// system's face says how to do: the integration tests and the apps' fixtures do it here,
111    /// as the core's own tests do.
112    pub use crate::host::fs::testing as fs;
113    pub use crate::host::memory::MemoryHost;
114    pub use crate::provider::claude::api::Renewed;
115    pub use crate::provider::claude::paths::live_service;
116    pub use crate::provider::claude::slot::{LIVE_SERVICE, dir_hash, service_for_dir};
117    /// The program the integration tests, the model's tests and the fixtures' tests start in
118    /// place of `claude`, `codex` and every other program they run, on every system.
119    pub use crate::stand_in;
120    pub use crate::store::memory::{Fault, MemoryStore};
121    pub use crate::store::vault_read;
122    pub use crate::switch::enroll::{ScriptedSignIn, SignInScript};
123    pub use crate::time::{Clock, FixedClock};
124
125    /// Every variable Pitboard reads from its environment, by name, which a test withholds
126    /// from every command it runs unless it means to pass one on.
127    pub fn variables() -> impl Iterator<Item = &'static str> {
128        crate::context::variables()
129    }
130
131    // The one guard every test's folder is refused by.
132    pub fn lies_in(path: &std::path::Path, place: &std::path::Path) -> bool {
133        crate::host::lies_in(path, place)
134    }
135
136    pub fn real_login_places(
137        started_in: &crate::context::Context,
138    ) -> Result<Vec<std::path::PathBuf>, String> {
139        crate::host::real_login_places(Some(started_in))
140    }
141
142    /// Parks `contents` under `service` in this context's vault, as a change does, once the
143    /// gate every change passes has let it: a test that runs as root is refused here too.
144    pub fn vault_write(
145        ctx: &crate::context::Context,
146        service: &str,
147        contents: &str,
148    ) -> crate::error::Result<()> {
149        let permit = crate::service::gate(ctx)?;
150        Ok(crate::store::vault_write(ctx, permit, service, contents)?)
151    }
152
153    pub fn may_be_running(pid: u32) -> bool {
154        crate::host::proc::may_be_running(pid)
155    }
156
157    // Every process's id, its parent's and its program's name, from one snapshot.
158    #[cfg(windows)]
159    pub fn processes() -> std::io::Result<Vec<(u32, u32, String)>> {
160        crate::host::proc::snapshot()
161    }
162
163    // A program's version resource, read as Claude Code's is (W17).
164    #[cfg(windows)]
165    pub mod version_resource {
166        use std::path::Path;
167
168        pub fn file_version(program: &Path) -> Option<[u16; 4]> {
169            crate::host::proc::file_version(program)
170        }
171
172        pub fn product_version(program: &Path) -> Option<[u16; 4]> {
173            crate::host::proc::product_version(program)
174        }
175
176        pub fn string(program: &Path, name: &str) -> Option<String> {
177            crate::host::proc::version_string(program, name)
178        }
179    }
180
181    /// Claude Code's login in Credential Manager as Pitboard writes, reads and deletes it, under
182    /// a test's own service, and each item as Windows keeps it, its blob by length alone.
183    #[cfg(windows)]
184    pub mod credman {
185        use crate::context::Context;
186        use crate::store::RawStore;
187
188        pub struct Item {
189            pub kind: u32,
190            pub persist: u32,
191            pub user: String,
192            pub comment: Option<String>,
193            pub attributes: u32,
194            pub bytes: usize,
195        }
196
197        fn refused(target: &str) -> std::io::Result<()> {
198            match crate::provider::names::refusal(target, &[]) {
199                Some(why) => Err(std::io::Error::other(why)),
200                None => Ok(()),
201            }
202        }
203
204        pub fn item(target: &str) -> std::io::Result<Option<Item>> {
205            refused(target)?;
206            Ok(
207                crate::host::windows_credman::item(target)?.map(|held| Item {
208                    kind: held.kind,
209                    persist: held.persist,
210                    user: held.user,
211                    comment: held.comment,
212                    attributes: held.attributes,
213                    bytes: held.blob.len(),
214                }),
215            )
216        }
217
218        // As `Bun.secrets.set` writes one (RF-G3).
219        pub fn write_as_bun(target: &str, user: &str, value: &str) -> std::io::Result<()> {
220            refused(target)?;
221            crate::host::windows_credman::write(target, user, value.as_bytes())
222        }
223
224        fn store(ctx: &Context) -> crate::provider::claude::credman::Store {
225            crate::provider::claude::credman::Store::of(ctx)
226        }
227
228        pub fn write(ctx: &Context, service: &str, contents: &str) -> crate::error::Result<()> {
229            let permit = crate::service::gate(ctx)?;
230            Ok(store(ctx).write(permit, service, contents)?)
231        }
232
233        pub fn read(ctx: &Context, service: &str) -> crate::error::Result<Option<String>> {
234            Ok(store(ctx).read(service)?)
235        }
236
237        // Every piece too, as Claude Code deletes its login.
238        pub fn discard(ctx: &Context, service: &str) -> crate::error::Result<()> {
239            let permit = crate::service::gate(ctx)?;
240            Ok(store(ctx).delete(permit, service)?)
241        }
242
243        // A test's cleanup: every item under `service`, each by name.
244        pub fn sweep(ctx: &Context, service: &str) -> Result<(), String> {
245            crate::provider::claude::credman::swept(ctx, service)
246        }
247    }
248
249    /// Deletes what is parked under `service`, once the gate has let it.
250    pub fn vault_delete(ctx: &crate::context::Context, service: &str) -> crate::error::Result<()> {
251        let permit = crate::service::gate(ctx)?;
252        Ok(crate::store::vault_delete(ctx, permit, service)?)
253    }
254
255    /// A sign-in to `which`'s tool that finished with `document`, the login the tool would
256    /// have stored, as JSON, and never ran the tool: what enrolling a second account by
257    /// signing in privately needs, in a test that may run no tool. Made once the gate has let
258    /// it, as every sign-in is.
259    pub fn signed_in(
260        ctx: &crate::context::Context,
261        which: crate::provider::ProviderId,
262        document: &str,
263    ) -> crate::error::Result<crate::switch::SignIn> {
264        use crate::provider::LoginKind;
265        let document = serde_json::from_str(document).expect("a login a test wrote as JSON");
266        match crate::provider::of(which) {
267            LoginKind::Document(tool) => {
268                crate::switch::planted(ctx, crate::service::gate(ctx)?, tool, document)
269            }
270            LoginKind::Folder(_) => Err(crate::error::Error::Usage(
271                "a Claude Desktop sign-in is played by testing::desktop".into(),
272            )),
273        }
274    }
275}