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
17//! while Pitboard is at 0.x, as Cargo reads one, and a new major version after 1.0.
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)]
56pub mod desktop_testing;
57pub mod doctor;
58pub mod duty;
59pub mod error;
60pub(crate) mod in_use;
61pub mod label;
62pub mod provider;
63pub mod redact;
64pub mod release;
65pub mod schedule;
66pub mod service;
67pub(crate) mod settings;
68pub mod state;
69pub mod status;
70pub mod statusline;
71pub mod switch;
72pub mod time;
73pub mod usage;
74pub mod words;
75
76pub(crate) mod atomic;
77pub(crate) mod fault;
78pub mod holder;
79pub(crate) mod home;
80pub mod host;
81pub(crate) mod lock;
82pub(crate) mod park;
83pub(crate) mod pending;
84pub(crate) mod proxy;
85pub(crate) mod sessions;
86/// The program the tests start in place of a tool, reached as `testing::stand_in`. Its own
87/// tests run with the core's.
88#[cfg(any(test, feature = "test-support"))]
89#[doc(hidden)]
90pub mod stand_in;
91pub(crate) mod store;
92
93/// What the integration tests reach into: they plant and inspect parked logins in the real
94/// store, and must never touch the credential slot this machine's Claude Code reads.
95#[cfg(feature = "test-support")]
96#[doc(hidden)]
97pub mod testing {
98    pub use crate::api::scripted::{Answer, Asked, ScriptedApi, Trouble};
99    pub use crate::desktop_testing as desktop;
100    pub use crate::duty::warm::run::{Started, WarmScript};
101    pub use crate::host::Ran;
102    /// What a test or a fixture does to a file's access, or to make a link, which only this
103    /// system's face says how to do: the integration tests and the apps' fixtures do it here,
104    /// as the core's own tests do.
105    pub use crate::host::fs::testing as fs;
106    pub use crate::host::memory::MemoryHost;
107    pub use crate::provider::claude::api::Renewed;
108    pub use crate::provider::claude::paths::live_service;
109    pub use crate::provider::claude::slot::{LIVE_SERVICE, dir_hash, service_for_dir};
110    /// The program the integration tests, the model's tests and the fixtures' tests start in
111    /// place of `claude`, `codex` and every other program they run, on every system.
112    pub use crate::stand_in;
113    pub use crate::store::memory::{Fault, MemoryStore};
114    pub use crate::store::vault_read;
115    pub use crate::switch::enroll::{ScriptedSignIn, SignInScript};
116    pub use crate::time::{Clock, FixedClock};
117
118    /// Every variable Pitboard reads from its environment, by name, which a test withholds
119    /// from every command it runs unless it means to pass one on.
120    pub fn variables() -> impl Iterator<Item = &'static str> {
121        crate::context::variables()
122    }
123
124    // The one guard every test's folder is refused by.
125    pub fn lies_in(path: &std::path::Path, place: &std::path::Path) -> bool {
126        crate::host::lies_in(path, place)
127    }
128
129    pub fn real_login_places(
130        started_in: &crate::context::Context,
131    ) -> Result<Vec<std::path::PathBuf>, String> {
132        use crate::host::user;
133        let home = user::accounts_own_home().ok_or("this system names no home for this account")?;
134        let pitboard = user::accounts_own_pitboard_home()
135            .ok_or("this system names no Pitboard folder for this account")?;
136        let own = crate::context::Context::new(home).with_pitboard_home(pitboard);
137        Ok([&own, started_in]
138            .into_iter()
139            .flat_map(|ctx| {
140                [
141                    crate::provider::claude::paths::config_dir(ctx),
142                    crate::provider::claude::paths::config_file(ctx),
143                    crate::provider::codex::paths::home(ctx),
144                    ctx.pitboard_home.clone(),
145                    ctx.home.join("Library/Application Support/Claude"),
146                    ctx.home.join("Library/Application Support/Claude-3p"),
147                    std::path::PathBuf::from("/Applications/Claude.app"),
148                ]
149            })
150            .filter(|place| place.is_absolute())
151            .collect())
152    }
153
154    /// Parks `contents` under `service` in this context's vault, as a change does, once the
155    /// gate every change passes has let it: a test that runs as root is refused here too.
156    pub fn vault_write(
157        ctx: &crate::context::Context,
158        service: &str,
159        contents: &str,
160    ) -> crate::error::Result<()> {
161        let permit = crate::service::gate(ctx)?;
162        Ok(crate::store::vault_write(ctx, permit, service, contents)?)
163    }
164
165    /// Deletes what is parked under `service`, once the gate has let it.
166    pub fn vault_delete(ctx: &crate::context::Context, service: &str) -> crate::error::Result<()> {
167        let permit = crate::service::gate(ctx)?;
168        Ok(crate::store::vault_delete(ctx, permit, service)?)
169    }
170
171    /// A sign-in to `which`'s tool that finished with `document`, the login the tool would
172    /// have stored, as JSON, and never ran the tool: what enrolling a second account by
173    /// signing in privately needs, in a test that may run no tool. Made once the gate has let
174    /// it, as every sign-in is.
175    pub fn signed_in(
176        ctx: &crate::context::Context,
177        which: crate::provider::ProviderId,
178        document: &str,
179    ) -> crate::error::Result<crate::switch::SignIn> {
180        use crate::provider::LoginKind;
181        let document = serde_json::from_str(document).expect("a login a test wrote as JSON");
182        match crate::provider::of(which) {
183            LoginKind::Document(tool) => {
184                crate::switch::planted(ctx, crate::service::gate(ctx)?, tool, document)
185            }
186            LoginKind::Folder(_) => Err(crate::error::Error::Usage(
187                "a Claude Desktop sign-in is played by testing::desktop".into(),
188            )),
189        }
190    }
191}