The invariants in CLAUDE.md were tested functions nobody called. There was no config type at all: --config was accepted and ignored by all three binaries, figment was an unused dependency, and the 124-line config.toml.tmpl was aspirational. assert_not_anthropic could not run at startup because nothing read the provider it checks. Add tireless_core::config, validated on construction so a Config in hand is a checked one, and tireless_agent::preflight, called by the worker at startup and by `tireless preflight` on demand — the same function, so the two cannot disagree. Move the Anthropic guard to tireless_core::policy where validate() can reach it; tireless_agent::opencode re-exports it so the documented path resolves. Also make /v1/ready honest. It returned "ok" unconditionally while its own doc comment promised dependency checks, so the deploy probe greened on a process that could do nothing. It now reports per-dependency state, with unwired ones saying not_implemented rather than ok. Tests parse the shipped template rather than a fixture, so template and code cannot drift apart silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
139 lines
5.7 KiB
Rust
139 lines
5.7 KiB
Rust
//! Everything checked before a service is allowed to start.
|
|
//!
|
|
//! One function, called by `tireless-worker` at startup and by `tireless
|
|
//! preflight` on demand, so that what an operator can verify by hand is exactly
|
|
//! what the service verifies for itself. A check that only one of them runs is a
|
|
//! check that will disagree with the other at the worst moment.
|
|
//!
|
|
//! This module is why the constraints in `CLAUDE.md` are true rather than
|
|
//! merely written down. Each one below names the invariant it enforces.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use tireless_core::{Config, PromptSet};
|
|
use tireless_entities::{BillingMode, Error};
|
|
|
|
/// What preflight found. Everything here is either fatal — in which case
|
|
/// [`run`] returned `Err` and this does not exist — or something the operator
|
|
/// should be able to read off the journal's first lines.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Report {
|
|
/// Which billing mode a Claude Code run will use, derived from the
|
|
/// environment tireless is about to hand over.
|
|
pub billing: BillingMode,
|
|
/// Service account home, where Claude Code keeps its credentials.
|
|
pub home: PathBuf,
|
|
/// Whether a credential store exists. Not fatal for the poller, which spends
|
|
/// nothing; fatal for the runner, which cannot work without it.
|
|
pub claude_credentials: bool,
|
|
/// OpenCode lane target, already asserted non-Anthropic by `Config::validate`.
|
|
pub oc_provider: String,
|
|
pub oc_model: String,
|
|
/// True when the prompt set came from files on disk rather than the binary.
|
|
pub prompts_overridden: bool,
|
|
}
|
|
|
|
impl Report {
|
|
/// Write the report to the journal. Called once, at startup, so that "what
|
|
/// was this service actually configured to do" is answerable from logs
|
|
/// alone rather than by reading a config file on a host.
|
|
pub fn log(&self) {
|
|
match self.billing {
|
|
// Deliberately a warning: it means the subscription this whole
|
|
// architecture exists to use is sitting unused, which is far more
|
|
// likely to be an accident than a decision.
|
|
BillingMode::ApiKey => tracing::warn!(
|
|
"ANTHROPIC_API_KEY is set: Claude Code runs will bill \
|
|
pay-as-you-go, not the subscription"
|
|
),
|
|
mode => tracing::info!(?mode, home = %self.home.display(), "Claude Code billing mode"),
|
|
}
|
|
tracing::info!(
|
|
provider = %self.oc_provider,
|
|
model = %self.oc_model,
|
|
"OpenCode lane target (asserted non-Anthropic)"
|
|
);
|
|
if self.prompts_overridden {
|
|
tracing::warn!(
|
|
"system prompts are overridden from disk; the running behaviour is \
|
|
not the behaviour of this build"
|
|
);
|
|
}
|
|
if !self.claude_credentials {
|
|
tracing::warn!(
|
|
home = %self.home.display(),
|
|
"no Claude Code credential store; the runner will refuse to start"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load and check everything, or refuse.
|
|
///
|
|
/// Enforces, in order:
|
|
///
|
|
/// 1. **Invariant 4** — the OpenCode lane is not pointed at Anthropic.
|
|
/// [`Config::validate`] calls
|
|
/// [`tireless_core::policy::assert_not_anthropic`], so this cannot be
|
|
/// bypassed by any caller holding a `Config`.
|
|
/// 2. **Invariant 8** — the prompt set is internally consistent, including any
|
|
/// file overrides.
|
|
/// 3. **Invariants 2 and 3** — the credential store is *stat'd, never read*, and
|
|
/// `ANTHROPIC_API_KEY` is only ever observed, never set.
|
|
pub fn run(config_path: &str) -> Result<Report, Error> {
|
|
let config = Config::load(config_path)?;
|
|
|
|
let prompts = PromptSet::resolve(&config.prompt)?;
|
|
// Touch the loaded set so a future refactor cannot quietly stop using it.
|
|
debug_assert!(!prompts.plan_cc.is_empty());
|
|
|
|
// Invariant 3: tireless *observes* this variable and never sets it. Its
|
|
// presence selects pay-as-you-go; its absence selects the subscription. That
|
|
// choice belongs to whoever wrote the unit environment file.
|
|
let has_key = std::env::var_os("ANTHROPIC_API_KEY").is_some();
|
|
|
|
let home = std::env::var_os("HOME")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| config.work.root.clone());
|
|
|
|
Ok(Report {
|
|
billing: crate::claude::expected_billing(has_key),
|
|
// Invariant 2: this stats the file. Its contents are Claude Code's
|
|
// business, and tireless has no reason ever to open it.
|
|
claude_credentials: crate::claude::has_credentials(&home) || has_key,
|
|
home,
|
|
oc_provider: config.lane.oc.provider.clone(),
|
|
oc_model: config.lane.oc.model.clone(),
|
|
prompts_overridden: config.prompt.any(),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_missing_config_is_a_refusal_not_a_default() {
|
|
// Starting on built-in defaults when the operator's config is absent
|
|
// would mean a service running against settings nobody chose — including
|
|
// a database and a forge it was never pointed at.
|
|
assert!(run("/nonexistent/tireless/config.toml").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn an_api_key_counts_as_credentials_without_a_login() {
|
|
// Pay-as-you-go is a supported mode (design.md §3.2), so a runner with a
|
|
// key but no interactive login must be allowed to start. The reverse of
|
|
// this test is what the runner's own bail covers.
|
|
let report = Report {
|
|
billing: BillingMode::ApiKey,
|
|
home: PathBuf::from("/nonexistent"),
|
|
claude_credentials: true,
|
|
oc_provider: "lair-helexa".into(),
|
|
oc_model: "Qwen/Qwen3.6-27B".into(),
|
|
prompts_overridden: false,
|
|
};
|
|
assert!(report.claude_credentials);
|
|
}
|
|
}
|