refactor(claw): the forge kind is the integration's — config.rs carries no forge code
The last forge content in config.rs was vocabulary, not data: `ForgeKind` (name/parse/token_env/default_base) and the pure `resolve_token` helper. By the same rule that moved `Watch` to the watch table and left `CalendarWatch` in the calendar integration, the kind belongs to the crate that speaks the forges. - `rhema-forge-integration` gains `kind::ForgeKind` and `resolve_token`, with the parse and token-fallback tests moved alongside — the claw's config tests were testing the integration's type through a re-export. - The claw's surfaces import the type from the integration directly; the crate root re-exports it, so `rhema_claw::ForgeKind` keeps resolving for everything downstream. config.rs is now config: credentials, the surface, paths, cadence, and nothing that knows what a forge or a calendar is.
This commit is contained in:
1 parent
5047ff8803
commit
90212d1e0f
11 files changed
+160
-150
No files matched your search
@@ -35,8 +35,8 @@ use rhema_identity::{Account, AccountDraft, IdentityStore, Secret, SecretKind};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::config::ForgeKind;
|
||||
use crate::watch_store::ClawStore;
|
||||
use rhema_forge_integration::ForgeKind;
|
||||
|
||||
/// `identity_save`: store an account (label, service, url, username, and
|
||||
/// the secret — required) and optionally register its forge watch.
|
||||
|
||||
+19
-140
@@ -1,14 +1,17 @@
|
||||
//! The claw config file: what to watch, where state lives, and which
|
||||
//! model answers the dispatches.
|
||||
//! The claw config file: where state lives, which model answers the
|
||||
//! dispatches, and the credentials the surface answers to.
|
||||
//!
|
||||
//! YAML at `~/.config/rhema/claw.yaml` (`--config` overrides). Paths and
|
||||
//! credentials fail loudly at startup: a watch with no token, a gitea
|
||||
//! watch with no host, or an unresolvable state directory never starts a
|
||||
//! daemon that would poll half-configured. Forge tokens fall back to the
|
||||
//! per-forge environment variable; the model backend's keys resolve
|
||||
//! through `rhema-config` exactly as the TUI's do (env first, stored
|
||||
//! second) and are validated in the binary's startup, not here — the
|
||||
//! daemon library takes the built provider, so tests can inject a mock.
|
||||
//! YAML at `~/.config/rhema/claw.yaml` (`--config` overrides). The
|
||||
//! file carries only what boot needs; everything the daemon
|
||||
//! reconfigures at runtime — watches, calendars — lives in its tables
|
||||
//! behind the HTTP doors, validated where it enters. Unresolvable
|
||||
//! state directories and colliding credentials fail loudly at startup;
|
||||
//! a daemon that starts with nothing to guard still serves chat.
|
||||
//! Forge tokens fall back to the per-forge environment variable; the
|
||||
//! model backend's keys resolve through `rhema-config` exactly as the
|
||||
//! TUI's do (env first, stored second) and are validated in the
|
||||
//! binary's startup, not here — the daemon library takes the built
|
||||
//! provider, so tests can inject a mock.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -31,8 +34,8 @@ pub const DEFAULT_LISTEN: &str = "127.0.0.1:7799";
|
||||
/// Why a config operation failed.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
/// The config file does not exist. Claw has nothing to guess from: a
|
||||
/// daemon without its watch list would silently watch nothing.
|
||||
/// The config file does not exist. Claw has nothing to guess from:
|
||||
/// paths and credentials are the file's job.
|
||||
#[error("no claw config at {0}; pass --config or create one")]
|
||||
Missing(PathBuf),
|
||||
/// Reading or writing the file failed.
|
||||
@@ -76,85 +79,6 @@ pub enum ConfigError {
|
||||
},
|
||||
}
|
||||
|
||||
/// Which forge a watch polls. The wire spelling is the source name the
|
||||
/// dispatcher registers and the resource key carries (`github:acme/w#7`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ForgeKind {
|
||||
/// GitHub (api.github.com, or a GitHub Enterprise host).
|
||||
GitHub,
|
||||
/// Gitea — self-hosted; a watch must name its instance.
|
||||
Gitea,
|
||||
/// GitLab (gitlab.com, or a self-hosted instance).
|
||||
GitLab,
|
||||
}
|
||||
|
||||
impl ForgeKind {
|
||||
/// Every kind the claw ships a source for, in registration order.
|
||||
/// A kind missing here has no watch poller, whatever the table
|
||||
/// holds.
|
||||
pub const ALL: [ForgeKind; 3] = [ForgeKind::GitHub, ForgeKind::Gitea, ForgeKind::GitLab];
|
||||
|
||||
/// The source name this kind registers and dispatches under.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
ForgeKind::GitHub => "github",
|
||||
ForgeKind::Gitea => "gitea",
|
||||
ForgeKind::GitLab => "gitlab",
|
||||
}
|
||||
}
|
||||
|
||||
/// The environment variable the token falls back to.
|
||||
pub fn token_env(self) -> &'static str {
|
||||
match self {
|
||||
ForgeKind::GitHub => "GITHUB_TOKEN",
|
||||
ForgeKind::Gitea => "GITEA_TOKEN",
|
||||
ForgeKind::GitLab => "GITLAB_TOKEN",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a forge name as the config, the HTTP surface, and the
|
||||
/// watch tools spell it: the source name (`github`, `gitea`,
|
||||
/// `gitlab`). `None` for anything else.
|
||||
pub fn parse(name: &str) -> Option<Self> {
|
||||
match name.trim() {
|
||||
"github" => Some(ForgeKind::GitHub),
|
||||
"gitea" => Some(ForgeKind::Gitea),
|
||||
"gitlab" => Some(ForgeKind::GitLab),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The production base URL; `None` for the self-hosted-only forges.
|
||||
pub fn default_base(self) -> Option<&'static str> {
|
||||
match self {
|
||||
ForgeKind::GitHub => Some(rhema_forge_integration::github::DEFAULT_BASE_URL),
|
||||
ForgeKind::Gitea => None,
|
||||
ForgeKind::GitLab => Some(rhema_forge_integration::gitlab::DEFAULT_BASE_URL),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure token resolution: the config value first, then the forge's env
|
||||
/// variable; the first non-blank wins. Parameterized over the env lookup
|
||||
/// so tests can hold the environment still.
|
||||
pub fn resolve_token(
|
||||
get_env: impl Fn(&str) -> Result<String, std::env::VarError>,
|
||||
forge: ForgeKind,
|
||||
stored: Option<&str>,
|
||||
) -> Option<String> {
|
||||
stored
|
||||
.map(str::trim)
|
||||
.filter(|token| !token.is_empty())
|
||||
.map(str::to_owned)
|
||||
.or_else(|| {
|
||||
get_env(forge.token_env())
|
||||
.ok()
|
||||
.map(|token| token.trim().to_owned())
|
||||
.filter(|token| !token.is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a `host:port` listen address binds beyond loopback.
|
||||
/// `localhost` (any case) and any loopback IP literal — `127.x.x.x`,
|
||||
/// `::1` — are loopback; an empty host (`:7799`, the bind-everything
|
||||
@@ -388,11 +312,10 @@ impl ClawConfig {
|
||||
Ok(serde_yaml::from_str(&raw)?)
|
||||
}
|
||||
|
||||
/// The startup checks: every seeded watch resolvable (shape, host,
|
||||
/// token). Loud, before anything polls. An empty list is fine now —
|
||||
/// the watch table is seeded from it, and the agent manages its own
|
||||
/// watches from there; a daemon that starts with nothing to guard
|
||||
/// still serves chat.
|
||||
/// The startup checks: the credential classes do not collide, and
|
||||
/// the surface is open only where it is safe. Loud, before anything
|
||||
/// listens. Watch and calendar validity is not checked here — those
|
||||
/// are table rows, validated at their doors.
|
||||
pub fn validate(&self) -> Result<(), ConfigError> {
|
||||
// The agent token's safety is that it is scoped; making it the
|
||||
// human token silently unscopes everything the model's shell
|
||||
@@ -560,50 +483,6 @@ ingest_token: door-key
|
||||
assert_eq!(config.listen_addr(), DEFAULT_LISTEN);
|
||||
assert_eq!(config.token, None);
|
||||
}
|
||||
#[test]
|
||||
fn forge_names_parse_as_the_sources_spell_them() {
|
||||
assert_eq!(ForgeKind::parse("github"), Some(ForgeKind::GitHub));
|
||||
assert_eq!(ForgeKind::parse(" gitea "), Some(ForgeKind::Gitea));
|
||||
assert_eq!(ForgeKind::parse("gitlab"), Some(ForgeKind::GitLab));
|
||||
assert_eq!(ForgeKind::parse("sourcehut"), None);
|
||||
assert_eq!(ForgeKind::parse(""), None);
|
||||
for kind in [ForgeKind::GitHub, ForgeKind::Gitea, ForgeKind::GitLab] {
|
||||
assert_eq!(ForgeKind::parse(kind.name()), Some(kind), "{}", kind.name());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_config_token_wins_over_env_and_blank_counts_as_unset() {
|
||||
let env = |name: &str| match name {
|
||||
"GITHUB_TOKEN" => Ok("from-env".to_owned()),
|
||||
"GITEA_TOKEN" => Ok(" ".to_owned()),
|
||||
_ => Err(std::env::VarError::NotPresent),
|
||||
};
|
||||
assert_eq!(
|
||||
resolve_token(env, ForgeKind::GitHub, Some(" from-config ")),
|
||||
Some("from-config".to_owned()),
|
||||
"the config token wins"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_token(env, ForgeKind::GitHub, None),
|
||||
Some("from-env".to_owned()),
|
||||
"the env token serves when no config token"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_token(env, ForgeKind::Gitea, Some(" ")),
|
||||
None,
|
||||
"a blank env token counts as unset"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_token(
|
||||
|_| Err(std::env::VarError::NotPresent),
|
||||
ForgeKind::GitLab,
|
||||
None
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_non_loopback_surface_demands_a_token() {
|
||||
let config = |listen: Option<&str>, token: Option<&str>| ClawConfig {
|
||||
|
||||
@@ -36,13 +36,13 @@ use rhema_autonomous_agent::{Allowlist, Source, ToolRegistry};
|
||||
use rhema_core::ToolResolver;
|
||||
use rhema_rsd::{Dispatcher, ResourceKey, SystemClock};
|
||||
|
||||
use crate::config::ForgeKind;
|
||||
use crate::consents::WaitBridge;
|
||||
use crate::daemon::ClawError;
|
||||
use crate::forge::ForgeSource;
|
||||
use crate::hub::EventHub;
|
||||
use rhema_calendar_integration::CalendarSource;
|
||||
use rhema_calendar_integration::CalendarStore;
|
||||
use rhema_forge_integration::ForgeKind;
|
||||
|
||||
/// The store handles the standard set is assembled over. One struct,
|
||||
/// so a new extension is a field plus a constructor line, not another
|
||||
|
||||
@@ -30,8 +30,8 @@ use rhema_forge_integration::{
|
||||
use rhema_identity::{IdentityStore, SecretKind};
|
||||
use rhema_rsd::{Clock, Dispatcher, Event, KeyFn, SystemClock};
|
||||
|
||||
use crate::config::{ForgeKind, resolve_token};
|
||||
use crate::watch_store::{AccountWatchRecord, ClawStore, WatchRecord};
|
||||
use rhema_forge_integration::{ForgeKind, resolve_token};
|
||||
|
||||
/// The cache key of a forge client: everything that changes the client
|
||||
/// is the forge kind, its host, and the token it authenticates with.
|
||||
|
||||
@@ -23,13 +23,13 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
|
||||
use crate::config::ForgeKind;
|
||||
use crate::forge::{record_client, resolve_watch_token};
|
||||
use crate::watch_store::ClawStore;
|
||||
use rhema_agent_tools::AllowAll;
|
||||
use rhema_autonomous_agent::{Handler, SessionContext, async_trait};
|
||||
use rhema_coding_agent::{CodingAgent, CodingAgentConfig};
|
||||
use rhema_core::{Content, Event as ModelEvent, EventSink, Message, Session};
|
||||
use rhema_forge_integration::ForgeKind;
|
||||
use rhema_forge_integration::triage::Role;
|
||||
use rhema_forge_integration::{
|
||||
EventKind, Forge, ForgeError, ForgeEvent, Notification, RepoRef, Snapshot, ThreadState, decode,
|
||||
|
||||
@@ -738,7 +738,7 @@ pub struct WatchBody {
|
||||
}
|
||||
|
||||
async fn watches_add(State(state): State<ClawState>, Json(body): Json<WatchBody>) -> Response {
|
||||
let Some(kind) = crate::config::ForgeKind::parse(&body.forge) else {
|
||||
let Some(kind) = rhema_forge_integration::ForgeKind::parse(&body.forge) else {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "forge must be github, gitea, or gitlab"})),
|
||||
@@ -783,7 +783,7 @@ async fn watches_remove(
|
||||
State(state): State<ClawState>,
|
||||
Path((forge, repo)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
let removed = match crate::config::ForgeKind::parse(&forge) {
|
||||
let removed = match rhema_forge_integration::ForgeKind::parse(&forge) {
|
||||
Some(kind) => state
|
||||
.watches
|
||||
.remove(kind.name(), repo.trim())
|
||||
@@ -1242,7 +1242,7 @@ fn is_reserved_source(source: &str) -> bool {
|
||||
matches!(
|
||||
source,
|
||||
crate::interactive::SOURCE | crate::task_source::TASKS_SOURCE
|
||||
) || crate::config::ForgeKind::parse(source).is_some()
|
||||
) || rhema_forge_integration::ForgeKind::parse(source).is_some()
|
||||
}
|
||||
|
||||
/// The external event's dedup id: a hash of the request's coordinates
|
||||
|
||||
@@ -100,7 +100,7 @@ pub use bootstrap::{
|
||||
BOOTSTRAP_SAVE, Bootstrap, BootstrapError, BootstrapKind, BootstrapStatus, DEFAULT_SYSTEM_MD,
|
||||
render_system,
|
||||
};
|
||||
pub use config::{ClawConfig, ConfigError, ForgeKind};
|
||||
pub use config::{ClawConfig, ConfigError};
|
||||
pub use daemon::{ClawError, Daemon, TickReport, run_once};
|
||||
pub use environment::write_report;
|
||||
pub use extension::{ClawExtension, ExtensionDeps, Extensions, Lanes};
|
||||
@@ -112,6 +112,7 @@ pub use interactive::InteractiveHandler;
|
||||
#[cfg(feature = "mock")]
|
||||
pub use mock::MockLoop;
|
||||
pub use notifications::{NotificationRecord, NotificationStore, NotificationStoreError};
|
||||
pub use rhema_forge_integration::{ForgeKind, resolve_token};
|
||||
pub use settings::SettingsStore;
|
||||
pub use task_source::{TASKS_SOURCE, TaskScheduler};
|
||||
pub use task_store::{TaskKind, TaskRecord, TaskStore, TaskStoreError};
|
||||
|
||||
@@ -22,7 +22,7 @@ use rhema_forge_integration::RepoRef;
|
||||
use rusqlite::params;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::config::{ForgeKind, resolve_token};
|
||||
use rhema_forge_integration::{ForgeKind, resolve_token};
|
||||
|
||||
/// One watched repository — the row's vocabulary, and the type the
|
||||
/// routes parse into. It used to live in the config as the YAML seed's
|
||||
|
||||
@@ -18,8 +18,8 @@ use rhema_autonomous_agent::{ToolOp, ToolRegistry, async_trait};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::config::ForgeKind;
|
||||
use crate::watch_store::{ClawStore, validate_watch};
|
||||
use rhema_forge_integration::ForgeKind;
|
||||
|
||||
/// The compiled operations behind the manifests, by `run.builtin` id.
|
||||
fn builtins(store: Arc<ClawStore>) -> BTreeMap<String, Arc<dyn ToolOp>> {
|
||||
|
||||
Reference in new issue
Block a user