Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c9a24ba
Add user-scoped skills settings UI
serrrfirat Jun 7, 2026
9f29097
Clarify scoped skill registry errors
serrrfirat Jun 7, 2026
40d62e1
Wire skills settings to scoped management
serrrfirat Jun 7, 2026
eac465b
Clean up skills settings structure
serrrfirat Jun 7, 2026
5b4108b
Merge branch 'main' into codex/settings-skills-user-scope
serrrfirat Jun 7, 2026
e8f23ca
Fix skills scoping review feedback
serrrfirat Jun 7, 2026
db7d66a
Merge remote-tracking branch 'origin/main' into codex/settings-skills…
serrrfirat Jun 7, 2026
2d11ce7
test: align skill removal e2e helper
serrrfirat Jun 7, 2026
3118350
fix: activate user-scoped skills at runtime
serrrfirat Jun 7, 2026
d528ac3
fix: allow caller-scoped reborn skill management
serrrfirat Jun 7, 2026
ae94fc8
fix: wire scoped skills through reborn webui
serrrfirat Jun 7, 2026
d758786
fix: align local-dev skill runtime roots
serrrfirat Jun 7, 2026
14492cb
fix: isolate scoped skill registries by tenant
serrrfirat Jun 7, 2026
1680fe6
fix: align agent skill activation scope
serrrfirat Jun 7, 2026
0eaf644
fix: scope chat skill management tools
serrrfirat Jun 7, 2026
56c3d7f
fix: validate webui skill mutations
serrrfirat Jun 8, 2026
8d4eecb
fix: align web skill mutation scopes
serrrfirat Jun 8, 2026
23ae721
fix: make legacy skill backfill one-time
serrrfirat Jun 8, 2026
3d81cbd
fix(web): address henrypark133 review - harden skill management (#4527)
serrrfirat Jun 8, 2026
4083be2
Merge remote-tracking branch 'origin/main' into codex/settings-skills…
serrrfirat Jun 8, 2026
cc5a1fb
Merge branch 'main' into codex/settings-skills-user-scope
serrrfirat Jun 8, 2026
11167ee
test: align reborn CLI skill smoke fixture
serrrfirat Jun 8, 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.

12 changes: 7 additions & 5 deletions crates/ironclaw_product_workflow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,13 @@ pub use reborn_services::{
RebornOutboundDeliveryTargetSummary, RebornOutboundPreferencesResponse,
RebornResolveGateResponse, RebornResumeGateResponse, RebornServices, RebornServicesApi,
RebornServicesError, RebornServicesErrorCode, RebornServicesErrorKind,
RebornSetOutboundPreferencesRequest, RebornSetupExtensionResponse, RebornStreamEventsRequest,
RebornStreamEventsResponse, RebornSubmitTurnResponse, RebornTimelineRequest,
RebornTimelineResponse, SetActiveLlmRequest, StaticConnectableChannelsProductFacade,
UnsupportedAutomationProductFacade, UnsupportedOutboundPreferencesProductFacade,
UpsertLlmProviderRequest,
RebornSetOutboundPreferencesRequest, RebornSetupExtensionResponse, RebornSkillActionResponse,
RebornSkillContentResponse, RebornSkillInfo, RebornSkillListResponse,
RebornSkillSearchResponse, RebornSkillSourceKind, RebornSkillTrustLevel,
RebornStreamEventsRequest, RebornStreamEventsResponse, RebornSubmitTurnResponse,
RebornTimelineRequest, RebornTimelineResponse, SetActiveLlmRequest, SkillsProductFacade,
StaticConnectableChannelsProductFacade, UnsupportedAutomationProductFacade,
UnsupportedOutboundPreferencesProductFacade, UpsertLlmProviderRequest,
};

pub use webui_inbound::{
Expand Down
176 changes: 173 additions & 3 deletions crates/ironclaw_product_workflow/src/reborn_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ pub use types::{
RebornOutboundDeliveryTargetId, RebornOutboundDeliveryTargetListResponse,
RebornOutboundDeliveryTargetOption, RebornOutboundDeliveryTargetSummary,
RebornOutboundPreferencesResponse, RebornResolveGateResponse, RebornResumeGateResponse,
RebornSetOutboundPreferencesRequest, RebornSetupExtensionResponse, RebornStreamEventsRequest,
RebornStreamEventsResponse, RebornSubmitTurnResponse, RebornTimelineRequest,
RebornTimelineResponse,
RebornSetOutboundPreferencesRequest, RebornSetupExtensionResponse, RebornSkillActionResponse,
RebornSkillContentResponse, RebornSkillInfo, RebornSkillListResponse,
RebornSkillSearchResponse, RebornSkillSourceKind, RebornSkillTrustLevel,
RebornStreamEventsRequest, RebornStreamEventsResponse, RebornSubmitTurnResponse,
RebornTimelineRequest, RebornTimelineResponse,
};

type SkillActivationRecorder =
Expand Down Expand Up @@ -127,6 +129,76 @@ impl ConnectableChannelsProductFacade for StaticConnectableChannelsProductFacade
}
}

#[async_trait]
pub trait SkillsProductFacade: Send + Sync {
async fn list_skills(
&self,
caller: WebUiAuthenticatedCaller,
) -> Result<RebornSkillListResponse, RebornServicesError> {
let _ = caller;
Err(RebornServicesError::service_unavailable(false))
}

async fn search_skills(
&self,
caller: WebUiAuthenticatedCaller,
query: String,
) -> Result<RebornSkillSearchResponse, RebornServicesError> {
let _ = (caller, query);
Err(RebornServicesError::service_unavailable(false))
}

async fn install_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
content: Option<String>,
) -> Result<RebornSkillActionResponse, RebornServicesError> {
let _ = (caller, name, content);
Err(RebornServicesError::service_unavailable(false))
}

async fn read_skill_content(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
) -> Result<RebornSkillContentResponse, RebornServicesError> {
let _ = (caller, name);
Err(RebornServicesError::service_unavailable(false))
}

async fn update_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
content: String,
) -> Result<RebornSkillActionResponse, RebornServicesError> {
let _ = (caller, name, content);
Err(RebornServicesError::service_unavailable(false))
}

async fn remove_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
) -> Result<RebornSkillActionResponse, RebornServicesError> {
let _ = (caller, name);
Err(RebornServicesError::service_unavailable(false))
}
}

#[derive(Debug, Default)]
pub struct UnsupportedSkillsProductFacade;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — UnsupportedSkillsProductFacade uses Default instead of new_static() like siblings.

Sibling UnsupportedAutomationProductFacade and UnsupportedLifecycleProductFacade expose pub fn new_static(). New UnsupportedSkillsProductFacade derives Default and uses ::default().

Fix: Add pub fn new_static() and update call site.


impl UnsupportedSkillsProductFacade {
pub fn new_static() -> Self {
Self
}
}

#[async_trait]
impl SkillsProductFacade for UnsupportedSkillsProductFacade {}

#[async_trait]
pub trait OutboundPreferencesProductFacade: Send + Sync {
/// Return the authenticated caller's scoped outbound preferences.
Expand Down Expand Up @@ -452,6 +524,43 @@ pub trait RebornServicesApi: Send + Sync {
caller: WebUiAuthenticatedCaller,
) -> Result<RebornExtensionListResponse, RebornServicesError>;

async fn list_skills(
&self,
caller: WebUiAuthenticatedCaller,
) -> Result<RebornSkillListResponse, RebornServicesError>;

async fn search_skills(
&self,
caller: WebUiAuthenticatedCaller,
query: String,
) -> Result<RebornSkillSearchResponse, RebornServicesError>;

async fn install_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
content: Option<String>,
) -> Result<RebornSkillActionResponse, RebornServicesError>;

async fn read_skill_content(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
) -> Result<RebornSkillContentResponse, RebornServicesError>;

async fn update_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
content: String,
) -> Result<RebornSkillActionResponse, RebornServicesError>;

async fn remove_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
) -> Result<RebornSkillActionResponse, RebornServicesError>;

async fn list_extension_registry(
&self,
caller: WebUiAuthenticatedCaller,
Expand Down Expand Up @@ -595,6 +704,7 @@ pub struct RebornServices {
event_stream: Option<Arc<dyn ProjectionStream>>,
lifecycle_facade: Arc<dyn LifecycleProductFacade>,
automation_facade: Arc<dyn AutomationProductFacade>,
skills_facade: Arc<dyn SkillsProductFacade>,
connectable_channels_facade: Arc<dyn ConnectableChannelsProductFacade>,
outbound_preferences_facade: Arc<dyn OutboundPreferencesProductFacade>,
approval_interactions: Arc<dyn ApprovalInteractionService>,
Expand All @@ -619,6 +729,7 @@ impl RebornServices {
"reborn_lifecycle_facade_unwired",
)),
automation_facade: Arc::new(UnsupportedAutomationProductFacade::new_static()),
skills_facade: Arc::new(UnsupportedSkillsProductFacade::new_static()),
connectable_channels_facade: Arc::new(StaticConnectableChannelsProductFacade::default()),
outbound_preferences_facade: Arc::new(
UnsupportedOutboundPreferencesProductFacade::new_static(),
Expand Down Expand Up @@ -659,6 +770,14 @@ impl RebornServices {
self
}

pub fn with_skills_product_facade(
mut self,
skills_facade: Arc<dyn SkillsProductFacade>,
) -> Self {
self.skills_facade = skills_facade;
self
}

pub fn with_connectable_channels_facade(
mut self,
connectable_channels_facade: Arc<dyn ConnectableChannelsProductFacade>,
Expand Down Expand Up @@ -1275,6 +1394,57 @@ impl RebornServicesApi for RebornServices {
extensions::list_extensions(self.lifecycle_facade.as_ref(), caller).await
}

async fn list_skills(
&self,
caller: WebUiAuthenticatedCaller,
) -> Result<RebornSkillListResponse, RebornServicesError> {
self.skills_facade.list_skills(caller).await
}

async fn search_skills(
&self,
caller: WebUiAuthenticatedCaller,
query: String,
) -> Result<RebornSkillSearchResponse, RebornServicesError> {
self.skills_facade.search_skills(caller, query).await
}

async fn install_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
content: Option<String>,
) -> Result<RebornSkillActionResponse, RebornServicesError> {
self.skills_facade
.install_skill(caller, name, content)
.await
}

async fn read_skill_content(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
) -> Result<RebornSkillContentResponse, RebornServicesError> {
self.skills_facade.read_skill_content(caller, name).await
}

async fn update_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
content: String,
) -> Result<RebornSkillActionResponse, RebornServicesError> {
self.skills_facade.update_skill(caller, name, content).await
}

async fn remove_skill(
&self,
caller: WebUiAuthenticatedCaller,
name: String,
) -> Result<RebornSkillActionResponse, RebornServicesError> {
self.skills_facade.remove_skill(caller, name).await
}

async fn list_extension_registry(
&self,
caller: WebUiAuthenticatedCaller,
Expand Down
74 changes: 74 additions & 0 deletions crates/ironclaw_product_workflow/src/reborn_services/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,80 @@ pub struct RebornExtensionListResponse {
pub extensions: Vec<RebornExtensionInfo>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebornSkillListResponse {
pub skills: Vec<RebornSkillInfo>,
pub count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebornSkillContentResponse {
pub name: String,
pub content: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebornSkillSearchResponse {
#[serde(default)]
pub catalog: Vec<serde_json::Value>,
#[serde(default)]
pub installed: Vec<RebornSkillInfo>,
#[serde(default)]
pub registry_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog_error: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebornSkillActionResponse {
pub success: bool,
pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — RebornSkillInfo.trust and RebornSkillInfo.source are stringly-typed wire fields for fixed enum sets.

RebornSkillInfo carries trust: String and source: String. Both carry values from typed enums (SkillTrust, SkillSource). types.md requires enums for fixed small sets.

Fix: Replace trust with RebornSkillTrustLevel enum and source with RebornSkillSourceKind or RebornSkillSourceDetail.

pub struct RebornSkillInfo {
pub name: String,
pub description: String,
pub version: String,
pub trust: RebornSkillTrustLevel,
pub source: RebornSkillSourceKind,
pub source_kind: RebornSkillSourceKind,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage_hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub setup_hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bundle_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub install_source_url: Option<String>,
#[serde(default)]
pub has_requirements: bool,
#[serde(default)]
pub has_scripts: bool,
#[serde(default)]
pub can_edit: bool,
#[serde(default)]
pub can_delete: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RebornSkillTrustLevel {
Trusted,
Installed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RebornSkillSourceKind {
User,
Installed,
Workspace,
System,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebornExtensionRegistryResponse {
pub entries: Vec<RebornExtensionRegistryEntry>,
Expand Down
8 changes: 6 additions & 2 deletions crates/ironclaw_reborn_cli/tests/smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ fn assert_empty_not_wired_surface(
}

fn write_reborn_skill(reborn_home: &std::path::Path, name: &str, description: &str) {
let skill_dir = reborn_home.join("local-dev/skills").join(name);
let skill_dir = reborn_cli_skill_root(reborn_home).join(name);
std::fs::create_dir_all(&skill_dir).expect("skill dir");
std::fs::write(
skill_dir.join("SKILL.md"),
Expand All @@ -769,7 +769,7 @@ fn write_reborn_skill(reborn_home: &std::path::Path, name: &str, description: &s
}

fn write_verbose_reborn_skill(reborn_home: &std::path::Path, name: &str, description: &str) {
let skill_dir = reborn_home.join("local-dev/skills").join(name);
let skill_dir = reborn_cli_skill_root(reborn_home).join(name);
std::fs::create_dir_all(&skill_dir).expect("skill dir");
std::fs::write(
skill_dir.join("SKILL.md"),
Expand All @@ -791,6 +791,10 @@ Use {name}.
.expect("skill file");
}

fn reborn_cli_skill_root(reborn_home: &std::path::Path) -> std::path::PathBuf {
reborn_home.join("local-dev/tenants/default/users/reborn-cli/skills")
}

fn assert_verbose_detail(args: &[&str], expected_detail: &str) {
let output = Command::new(reborn_bin())
.args(args)
Expand Down
Loading
Loading