Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- *(web)* gateway onboarding/auth SSE now uses the unified `onboarding_state` event; external SSE clients should migrate from the older auth/pairing event names. Legacy WebSocket `auth_token` and `auth_cancel` client messages remain accepted during the temporary web v1-auth compatibility window.
- *(web)* extension setup/auth `ActionResponse` payloads no longer include the legacy `verification` field; clients should use `onboarding_state` / `onboarding` data instead of that deprecated wire field.

## [0.25.0](https://github.com/nearai/ironclaw/compare/ironclaw-v0.24.0...ironclaw-v0.25.0) - 2026-04-11

### Added
Expand Down
38 changes: 38 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,44 @@ E2E tests: see `tests/e2e/CLAUDE.md`.

Prefer generic/extensible architectures over hardcoding specific integrations. Ask clarifying questions about the desired abstraction level before implementing.

### Extension/Auth Invariants

Extension and channel onboarding has two distinct identities that must not be conflated:

- `credential_name`: backend secret identity used for storage, injection, and gate resume
- `extension_name`: user-facing installed extension/channel identity used for setup routing and UI

Examples:

- Telegram:
- `credential_name = telegram_bot_token`
- `extension_name = telegram`
- Gmail:
- `credential_name = google_oauth_token`
- `extension_name = gmail`

Rules:

- Never route web setup/configure UI directly from `credential_name`.
- Chat and Settings must use the same setup/configure path for installable extensions/channels.
- Generic auth-card UI is only for non-extension credential prompts or pure OAuth launch prompts.
- If an auth flow is for an installed extension/channel, resolve the `extension_name` once in shared backend logic and carry it through the wire contract rather than re-deriving it in multiple layers.
- New auth/onboarding code must reuse the shared resolver/controller path instead of adding channel-specific or frontend-only fallbacks.

Current ownership:

- `src/bridge/auth_manager.rs`: canonical auth-flow extension-name resolver
- `src/bridge/router.rs`: auth gate display + submit routing
- `src/channels/web/server.rs`: pending-gate/history rehydration
- `crates/ironclaw_gateway/static/app.js`: unified onboarding controller and configure-modal routing

Temporary compatibility boundary:

- Web auth prompts with a gate `request_id` are the v2 path and must resolve through `/api/chat/gate/resolve`.
- Web auth prompts without a `request_id` are legacy engine v1 `pending_auth` compatibility only.
- Keep that compatibility isolated; do not add new features to it.
- Once v1 auth mode is removed, delete the legacy `/api/chat/auth-token` and `/api/chat/auth-cancel` shim endpoints and the matching no-`request_id` UI branch.

Key traits for extensibility: `Database`, `Channel`, `Tool`, `LlmProvider`, `SuccessEvaluator`, `EmbeddingProvider`, `NetworkPolicyDecider`, `Hook`, `Observer`, `Tunnel`.

All I/O is async with tokio. Use `Arc<T>` for shared state, `RwLock` for concurrent access.
Expand Down
78 changes: 32 additions & 46 deletions crates/ironclaw_common/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ impl ToolDecisionDto {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OnboardingStateDto {
SetupRequired,
AuthRequired,
PairingRequired,
Ready,
Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AppEvent {
Expand Down Expand Up @@ -110,51 +120,34 @@ pub enum AppEvent {
/// Whether the "always" auto-approve option should be shown.
allow_always: bool,
},
#[serde(rename = "auth_required")]
AuthRequired {
#[serde(rename = "onboarding_state")]
OnboardingState {
extension_name: String,
state: OnboardingStateDto,
#[serde(skip_serializing_if = "Option::is_none")]
request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
auth_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
setup_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "auth_completed")]
AuthCompleted {
extension_name: String,
success: bool,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "pairing_required")]
PairingRequired {
channel: String,
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
onboarding: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "pairing_completed")]
PairingCompleted {
channel: String,
success: bool,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "gate_required")]
GateRequired {
request_id: String,
gate_name: String,
tool_name: String,
description: String,
parameters: String,
#[serde(skip_serializing_if = "Option::is_none")]
extension_name: Option<String>,
resume_kind: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
Expand Down Expand Up @@ -335,10 +328,7 @@ impl AppEvent {
Self::Status { .. } => "status",
Self::JobStarted { .. } => "job_started",
Self::ApprovalNeeded { .. } => "approval_needed",
Self::AuthRequired { .. } => "auth_required",
Self::AuthCompleted { .. } => "auth_completed",
Self::PairingRequired { .. } => "pairing_required",
Self::PairingCompleted { .. } => "pairing_completed",
Self::OnboardingState { .. } => "onboarding_state",
Self::GateRequired { .. } => "gate_required",
Self::GateResolved { .. } => "gate_resolved",
Self::Error { .. } => "error",
Expand Down Expand Up @@ -419,29 +409,25 @@ mod tests {
thread_id: None,
allow_always: false,
},
AppEvent::AuthRequired {
AppEvent::OnboardingState {
extension_name: String::new(),
state: OnboardingStateDto::AuthRequired,
request_id: None,
message: None,
instructions: None,
auth_url: None,
setup_url: None,
thread_id: None,
},
AppEvent::AuthCompleted {
extension_name: String::new(),
success: true,
message: String::new(),
thread_id: None,
},
AppEvent::PairingRequired {
channel: String::new(),
instructions: None,
onboarding: None,
thread_id: None,
},
AppEvent::PairingCompleted {
channel: String::new(),
success: true,
message: String::new(),
AppEvent::GateRequired {
request_id: String::new(),
gate_name: String::new(),
tool_name: String::new(),
description: String::new(),
parameters: String::new(),
extension_name: None,
resume_kind: serde_json::Value::Null,
thread_id: None,
},
AppEvent::Error {
Expand Down
2 changes: 1 addition & 1 deletion crates/ironclaw_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod event;
mod timezone;
mod util;

pub use event::{AppEvent, PlanStepDto, ToolDecisionDto};
pub use event::{AppEvent, OnboardingStateDto, PlanStepDto, ToolDecisionDto};
pub use timezone::{ValidTimezone, deserialize_option_lenient};
pub use util::truncate_preview;

Expand Down
Loading
Loading