Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ parking_lot = "0.12"
cc-keybindings = { path = "crates/cc-keybindings" }
cc-observability = { path = "crates/cc-observability" }
cc-types = { path = "crates/cc-types" }
cc-bootstrap = { path = "crates/cc-bootstrap" }
cc-auth = { path = "crates/cc-auth" }
cc-skills = { path = "crates/cc-skills" }

# Daemon HTTP server
axum = { version = "0.8", features = ["ws"] }
Expand Down
24 changes: 24 additions & 0 deletions crates/cc-auth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "cc-auth"
version = "0.1.0"
edition = "2021"
description = "Authentication (API key, OAuth, Keychain) for cc-rust"

[dependencies]
anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
reqwest = { workspace = true }
keyring = { workspace = true }
base64 = { workspace = true }
rand = { workspace = true }
sha2 = { workspace = true }
dirs = { workspace = true }
parking_lot = { workspace = true }
urlencoding = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! Supports three active auth methods:
//! - API Key: via `ANTHROPIC_API_KEY` env var or system keychain
//! - External Auth Token: via `ANTHROPIC_AUTH_TOKEN` env var
//! - OAuth Token: from `~/.cc-rust/credentials.json` (Claude.ai / Console / OpenAI Codex)
//! - OAuth Token: from the registered credentials path (Claude.ai / Console / OpenAI Codex)

pub mod api_key;
pub mod codex_cli;
Expand All @@ -12,6 +12,54 @@ pub mod token;

const OPENAI_CODEX_AUTH_TOKEN_ENV: &str = "OPENAI_CODEX_AUTH_TOKEN";

// ---------------------------------------------------------------------------
// Host-provided credentials path
// ---------------------------------------------------------------------------
//
// cc-auth used to call `crate::config::paths::credentials_path()` directly
// from `token.rs`. That's a cycle the moment `auth` moves out of the root
// crate, so the host now registers the path once at startup and cc-auth reads
// it back through this module.

use parking_lot::RwLock;
use std::path::PathBuf;
use std::sync::LazyLock;

static CREDENTIALS_PATH: LazyLock<RwLock<Option<PathBuf>>> =
LazyLock::new(|| RwLock::new(None));

/// Register the OAuth credentials file path. The host calls this once during
/// process startup; if a caller reaches token I/O without it having run
/// (e.g. a unit test that exercises `resolve_auth` directly), the fallback
/// in [`credentials_path`] mirrors the root crate's
/// `config::paths::credentials_path()` layout.
pub fn set_credentials_path(path: PathBuf) {
*CREDENTIALS_PATH.write() = Some(path);
}

/// Return the registered credentials path, falling back to
/// `{CC_RUST_HOME | ~/.cc-rust | $TMP/cc-rust}/credentials.json` when the host
/// hasn't registered one. Kept in sync with `config::paths::data_root` in the
/// root crate — a small duplication that decouples cc-auth from it.
pub(crate) fn credentials_path() -> PathBuf {
if let Some(p) = CREDENTIALS_PATH.read().clone() {
return p;
}
data_root_fallback().join("credentials.json")
}

fn data_root_fallback() -> PathBuf {
if let Ok(override_dir) = std::env::var("CC_RUST_HOME") {
if !override_dir.trim().is_empty() {
return PathBuf::from(override_dir);
}
}
if let Some(home) = dirs::home_dir() {
return home.join(".cc-rust");
}
std::env::temp_dir().join("cc-rust")
}

// ---------------------------------------------------------------------------
// Auth method enum
// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
//! OAuth token persistence.
//!
//! Stores OAuth tokens at `~/.cc-rust/credentials.json`.
//! Stores OAuth tokens at the path registered via
//! [`crate::set_credentials_path`]. The path is injected from the root crate at
//! startup (see `main.rs`) so cc-auth stays decoupled from `config::paths`.

use anyhow::Result;

/// Token storage file path: `{data_root}/credentials.json`
/// Token storage file path (set once by the host at startup).
///
/// Panics if [`crate::set_credentials_path`] has not been called yet. Any code
/// path that reaches token I/O runs after the early bootstrap has registered
/// the path, so this is a programmer-error guard rather than a runtime check.
pub fn token_file_path() -> std::path::PathBuf {
crate::config::paths::credentials_path()
crate::credentials_path()
}

/// Stored token data (OAuth).
Expand Down
14 changes: 14 additions & 0 deletions crates/cc-bootstrap/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "cc-bootstrap"
version = "0.1.0"
edition = "2021"
description = "Process-level singleton layer (session IDs, model strings, timing, diagnostics) for cc-rust"

[dependencies]
serde = { workspace = true }
parking_lot = { workspace = true }
tokio = { workspace = true }
uuid = { workspace = true }

[dev-dependencies]
serde_json = { workspace = true }
9 changes: 9 additions & 0 deletions crates/cc-skills/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "cc-skills"
version = "0.1.0"
edition = "2021"
description = "Skill discovery, loading, and registry for cc-rust (bundled + user + project + plugin)"

[dependencies]
serde = { workspace = true }
parking_lot = { workspace = true }
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,14 @@ pub fn register_bundled_skills() {
#[cfg(test)]
mod tests {
use super::*;
use crate::skills;

#[test]
fn test_register_bundled_skills() {
// Clear any previous state
skills::clear_skills();
crate::clear_skills();

register_bundled_skills();
let all = skills::get_all_skills();
let all = crate::get_all_skills();

// We should have at least 5 bundled skills
assert!(
Expand All @@ -196,7 +195,7 @@ mod tests {
// Ensure bundled skills are registered (may already be from other tests)
register_bundled_skills();

let all = skills::get_all_skills();
let all = crate::get_all_skills();
// These may or may not be present due to concurrent clear_skills() from other tests.
// We verify the properties of SkillDefinition directly instead.
let simplify = SkillDefinition {
Expand Down Expand Up @@ -236,10 +235,10 @@ mod tests {

#[test]
fn test_bundled_skill_prompts_not_empty() {
skills::clear_skills();
crate::clear_skills();
register_bundled_skills();

let all = skills::get_all_skills();
let all = crate::get_all_skills();
for skill in &all {
assert!(
!skill.prompt_body.is_empty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,24 +161,37 @@ impl SkillDefinition {
// ---------------------------------------------------------------------------
// Subsystem event emission
// ---------------------------------------------------------------------------
//
// cc-skills used to hold a `broadcast::Sender<crate::ipc::subsystem_events::SubsystemEvent>`
// directly. Once `skills` moved into its own crate (issue #71), referencing
// the root crate's `ipc` module would have been a cycle. The host now
// registers a simple callback that receives cc-skills's own minimal event
// enum and is responsible for adapting it into `SubsystemEvent`.

/// Minimal event set emitted by the skill subsystem. The host adapts these
/// into its own subsystem-event wrapper.
#[derive(Debug, Clone)]
pub enum SkillSubsystemEvent {
/// Skills were loaded / reloaded.
SkillsLoaded { count: usize },
}

/// Event sender for subsystem events.
static EVENT_TX: LazyLock<
Mutex<Option<tokio::sync::broadcast::Sender<crate::ipc::subsystem_events::SubsystemEvent>>>,
> = LazyLock::new(|| Mutex::new(None));
type EventCallback = Box<dyn Fn(SkillSubsystemEvent) + Send + Sync>;

/// Inject the event sender from the headless event loop.
#[allow(dead_code)] // Called by headless event loop wiring (Task 12).
pub fn set_event_sender(
tx: tokio::sync::broadcast::Sender<crate::ipc::subsystem_events::SubsystemEvent>,
) {
*EVENT_TX.lock() = Some(tx);
static EVENT_CALLBACK: LazyLock<Mutex<Option<EventCallback>>> = LazyLock::new(|| Mutex::new(None));

/// Register the host's event adapter. Replaces any previous callback.
pub fn set_event_callback<F>(cb: F)
where
F: Fn(SkillSubsystemEvent) + Send + Sync + 'static,
{
*EVENT_CALLBACK.lock() = Some(Box::new(cb));
}

/// Emit a subsystem event.
fn emit_event(event: crate::ipc::subsystem_events::SubsystemEvent) {
if let Some(tx) = EVENT_TX.lock().as_ref() {
let _ = tx.send(event);
/// Emit an event through the registered callback (no-op if unset).
fn emit_event(event: SkillSubsystemEvent) {
if let Some(cb) = EVENT_CALLBACK.lock().as_ref() {
cb(event);
}
}

Expand Down Expand Up @@ -226,14 +239,20 @@ pub fn clear_skills() {
}

/// Initialize the skill system — loads bundled + directory skills.
pub fn init_skills(project_dir: Option<&std::path::Path>) {
///
/// `user_skills_dir` is the path that used to be resolved internally via
/// `crate::config::paths::skills_dir_global()`. The host passes it in so
/// cc-skills stays decoupled from the root crate's path layer.
pub fn init_skills(
user_skills_dir: &std::path::Path,
project_dir: Option<&std::path::Path>,
) {
// 1. Register bundled skills
bundled::register_bundled_skills();

// 2. Load user skills from {data_root}/skills/
let user_skills_dir = crate::config::paths::skills_dir_global();
// 2. Load user skills from the host-provided directory
if user_skills_dir.is_dir() {
let skills = loader::load_skills_from_dir(&user_skills_dir, SkillSource::User);
let skills = loader::load_skills_from_dir(user_skills_dir, SkillSource::User);
for skill in skills {
register_skill(skill);
}
Expand All @@ -250,11 +269,9 @@ pub fn init_skills(project_dir: Option<&std::path::Path>) {
}
}

// 4. Emit skills-loaded event
// 4. Emit skills-loaded event through the host-registered callback
let count = get_all_skills().len();
emit_event(crate::ipc::subsystem_events::SubsystemEvent::Skill(
crate::ipc::subsystem_events::SkillEvent::SkillsLoaded { count },
));
emit_event(SkillSubsystemEvent::SkillsLoaded { count });
}

// ---------------------------------------------------------------------------
Expand Down
5 changes: 4 additions & 1 deletion crates/claude-code-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,13 @@ dotenvy = { workspace = true }
# 同步原语
parking_lot = { workspace = true }

# 内部 workspace crates (P1)
# 内部 workspace crates (P1/P2)
cc-keybindings = { workspace = true }
cc-observability = { workspace = true }
cc-types = { workspace = true }
cc-bootstrap = { workspace = true }
cc-auth = { workspace = true }
cc-skills = { workspace = true }

# Daemon
axum = { workspace = true }
Expand Down
14 changes: 13 additions & 1 deletion crates/claude-code-rs/src/ipc/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,19 @@ impl HeadlessRuntime {
crate::lsp_service::set_event_sender(event_bus.sender());
crate::mcp::set_event_sender(event_bus.sender());
crate::plugins::set_event_sender(event_bus.sender());
crate::skills::set_event_sender(event_bus.sender());
// cc-skills lives in its own crate and no longer knows about
// `SubsystemEvent`. Adapt its minimal event enum into ours here.
let skills_tx = event_bus.sender();
crate::skills::set_event_callback(move |e| {
let adapted = match e {
crate::skills::SkillSubsystemEvent::SkillsLoaded { count } => {
super::subsystem_events::SubsystemEvent::Skill(
super::subsystem_events::SkillEvent::SkillsLoaded { count },
)
}
};
let _ = skills_tx.send(adapted);
});

// ── 2. Send Ready ────────────────────────────────────────────
let app_state = self.engine.app_state();
Expand Down
5 changes: 4 additions & 1 deletion crates/claude-code-rs/src/ipc/subsystem_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@ pub fn handle_skill_command(cmd: super::subsystem_events::SkillCommand) -> Vec<B
SkillCommand::Reload => {
let cwd = std::env::current_dir().ok();
crate::skills::clear_skills();
crate::skills::init_skills(cwd.as_deref());
crate::skills::init_skills(
&crate::config::paths::skills_dir_global(),
cwd.as_deref(),
);
let count = crate::skills::get_all_skills().len();
tracing::info!(count, "Skills reloaded via IPC");
vec![BackendMessage::SkillEvent {
Expand Down
Loading
Loading