Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c289c22
feat(common): add CredentialName and ExtensionName newtypes
ilblackdragon Apr 18, 2026
8bbb963
fix(common): address PR #2611 review feedback
ilblackdragon Apr 18, 2026
97dfc29
feat(common): apply ExtensionName newtype to fan-out sites (PR 2/2)
ilblackdragon Apr 18, 2026
cc7ea40
fix(web): return ExtensionName from pending_gate_extension_name
ilblackdragon Apr 18, 2026
7af6a7e
fix(router,web): address PR #2617 review feedback
ilblackdragon Apr 18, 2026
cd46201
Merge remote-tracking branch 'origin/staging' into feat/identity-newt…
ilblackdragon Apr 18, 2026
a3d489f
docs(identity): codify web-boundary rules + add static check
ilblackdragon Apr 18, 2026
c813caa
fix(auth): validate user-influenced names at the resolver boundary
ilblackdragon Apr 18, 2026
f8731de
Merge remote-tracking branch 'origin/staging' into feat/identity-newt…
ilblackdragon Apr 18, 2026
4841f58
fix(ci): adapt post-merge-from-staging sites to ExtensionName
ilblackdragon Apr 18, 2026
714f722
fix(auth): extract shared resolver; wrapper delegates instead of dupl…
ilblackdragon Apr 18, 2026
c9b1d04
Merge remote-tracking branch 'origin/staging' into feat/identity-newt…
ilblackdragon Apr 18, 2026
a2c35b7
Merge remote-tracking branch 'origin/staging' into feat/identity-newt…
ilblackdragon Apr 18, 2026
e49a586
fix(web): address PR #2617 round-3 review feedback
ilblackdragon Apr 19, 2026
a41b00d
Merge remote-tracking branch 'origin/staging' into feat/identity-newt…
ilblackdragon Apr 19, 2026
2a6b093
fix(ci): adapt replay_outcome to ExtensionName after staging merge
ilblackdragon Apr 19, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

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

26 changes: 17 additions & 9 deletions crates/ironclaw_common/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! frames, but other subsystems (agent loop, orchestrator, extensions)
//! produce and consume them too.

use crate::identity::ExtensionName;
use serde::{Deserialize, Serialize};

/// A single step in a plan progress update (SSE DTO).
Expand Down Expand Up @@ -64,15 +65,15 @@ impl OnboardingStateDto {
/// post-pairing) from silently disagreeing when new fields land on
/// `AppEvent::OnboardingState`.
pub fn pairing_required(
extension_name: impl Into<String>,
extension_name: ExtensionName,
request_id: Option<String>,
thread_id: Option<String>,
message: Option<String>,
instructions: Option<String>,
onboarding: Option<serde_json::Value>,
) -> AppEvent {
AppEvent::OnboardingState {
extension_name: extension_name.into(),
extension_name,
state: Self::PairingRequired,
request_id,
message,
Expand Down Expand Up @@ -161,7 +162,7 @@ pub enum AppEvent {
},
#[serde(rename = "onboarding_state")]
OnboardingState {
extension_name: String,
extension_name: ExtensionName,
state: OnboardingStateDto,
#[serde(skip_serializing_if = "Option::is_none")]
request_id: Option<String>,
Expand All @@ -186,7 +187,7 @@ pub enum AppEvent {
description: String,
parameters: String,
#[serde(skip_serializing_if = "Option::is_none")]
extension_name: Option<String>,
extension_name: Option<ExtensionName>,
resume_kind: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
Expand Down Expand Up @@ -281,7 +282,7 @@ pub enum AppEvent {
/// Extension activation status change (WASM channels).
#[serde(rename = "extension_status")]
ExtensionStatus {
extension_name: String,
extension_name: ExtensionName,
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
Expand Down Expand Up @@ -453,7 +454,7 @@ mod tests {
allow_always: false,
},
AppEvent::OnboardingState {
extension_name: String::new(),
extension_name: ExtensionName::from_trusted(String::new()),
state: OnboardingStateDto::AuthRequired,
request_id: None,
message: None,
Expand Down Expand Up @@ -524,7 +525,7 @@ mod tests {
thread_id: None,
},
AppEvent::ExtensionStatus {
extension_name: String::new(),
extension_name: ExtensionName::from_trusted(String::new()),
status: String::new(),
message: None,
},
Expand Down Expand Up @@ -579,7 +580,7 @@ mod tests {
#[test]
fn pairing_required_constructor_sets_invariant_fields() {
let event = OnboardingStateDto::pairing_required(
"telegram",
ExtensionName::new("telegram").unwrap(),
Some("req-1".to_string()),
Some("thread-1".to_string()),
Some("Paired!".to_string()),
Expand Down Expand Up @@ -618,7 +619,14 @@ mod tests {

#[test]
fn pairing_required_constructor_serializes_to_onboarding_state_event() {
let event = OnboardingStateDto::pairing_required("telegram", None, None, None, None, None);
let event = OnboardingStateDto::pairing_required(
ExtensionName::new("telegram").unwrap(),
None,
None,
None,
None,
None,
);
let json = serde_json::to_value(&event).unwrap();
assert_eq!(json["type"], "onboarding_state");
assert_eq!(json["state"], "pairing_required");
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ default = ["clipboard"]
clipboard = ["dep:arboard", "dep:image"]

[dependencies]
ironclaw_common = { path = "../ironclaw_common", version = "0.2.0" }
ratatui = { version = "0.29", features = ["crossterm"] }
tui-textarea = { version = "0.7", features = ["crossterm"] }
serde = { version = "1", features = ["derive"] }
Expand Down
5 changes: 3 additions & 2 deletions crates/ironclaw_tui/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use std::collections::VecDeque;

use ironclaw_common::ExtensionName;
use ratatui::crossterm::event::KeyEvent;

/// A single log entry displayed in the TUI Logs tab.
Expand Down Expand Up @@ -280,13 +281,13 @@ pub enum TuiEvent {

/// Extension needs user authentication.
AuthRequired {
extension_name: String,
extension_name: ExtensionName,
instructions: Option<String>,
},

/// Extension auth completed.
AuthCompleted {
extension_name: String,
extension_name: ExtensionName,
success: bool,
message: String,
},
Expand Down
33 changes: 33 additions & 0 deletions scripts/pre-commit-safety.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
# 5. Multi-step DB operations without transaction wrapping
# 6. .unwrap(), .expect(), assert!() in production code (panics)
# 7. Gateway/CLI handlers bypassing ToolDispatcher (must go through tools)
# 8. CredentialName referenced in web-layer code (wrong identity at boundary)
#
# Also runs check-i18n-parity.sh when crates/ironclaw_gateway/static/i18n/*.js
# files are staged, to ensure every language pack has the same key set.
#
# Suppress individual lines with an inline "// safety: <reason>" comment.
# For check #7, use "// dispatch-exempt: <reason>" instead.
# For check #8, use "// web-identity-exempt: <reason>" instead.

set -euo pipefail

Expand Down Expand Up @@ -333,10 +335,41 @@ if [ -n "$DISPATCH_DIFF" ]; then
fi
fi

# 8. CredentialName referenced in web-layer code.
# CredentialName is a backend/secrets-store identity. Web routes and
# web DTOs take ExtensionName; the dispatcher and auth_manager resolve
# credential identity from the extension name server-side. An explicit
# `CredentialName` reference in src/channels/web/** (except inside
# `#[cfg(test)] mod tests` blocks) means the wrong identity is reaching
# the web boundary. See src/channels/web/CLAUDE.md "Identity types at
# the web boundary" and .claude/rules/types.md.
#
# Suppress with "// web-identity-exempt: <reason>" when the reference
# is genuinely reading an already-typed value off a backend struct
# (e.g., destructuring `ResumeKind::Authentication` to log the name).
WEB_IDENTITY_DIFF=$(git diff --cached -U0 -- 'src/channels/web/*.rs' 'src/channels/web/**/*.rs' 2>/dev/null || true)
if [ -z "$WEB_IDENTITY_DIFF" ]; then
WEB_IDENTITY_DIFF=$(git diff "$(resolve_base_ref)" -U0 -- 'src/channels/web/*.rs' 'src/channels/web/**/*.rs' 2>/dev/null || true)
fi
if [ -n "$WEB_IDENTITY_DIFF" ]; then
# Strip lines inside `#[cfg(test)] mod tests` blocks using the same
# precomputed boundaries used for other prod-only checks.
WEB_IDENTITY_PROD=$(printf '%s\n' "$WEB_IDENTITY_DIFF" | strip_test_mod_lines)
WEB_IDENTITY_HITS=$(echo "$WEB_IDENTITY_PROD" | grep -nE '^\+' \
| grep -E '\bCredentialName\b' \
| grep -vE '// web-identity-exempt:|// safety:|^\+\+\+' \
| head -5 || true)
if [ -n "$WEB_IDENTITY_HITS" ]; then
warn "CREDNAME" "\`CredentialName\` referenced in src/channels/web/** — web code takes \`ExtensionName\`; credential identity stays backend-side. Push the mapping into bridge::auth_manager or annotate with '// web-identity-exempt: <reason>'."
echo "$WEB_IDENTITY_HITS" | sed 's/^/ /'
fi
fi

if [ "$WARNINGS" -gt 0 ]; then
echo ""
echo "Found $WARNINGS potential issue(s). Fix them or add '// safety: <reason>' to suppress."
echo "(For DISPATCH warnings, use '// dispatch-exempt: <reason>' instead.)"
echo "(For CREDNAME warnings, use '// web-identity-exempt: <reason>' instead.)"
echo ""
exit 1
fi
Loading
Loading