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
2 changes: 1 addition & 1 deletion FEATURE_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ Trace Commons issuer/TenantCtx note: the server-side `zmanian/tracedao-server` s
| GSuite WASM tools | ✅ | 🚧 | Reborn bundles operation-level Google Drive/Docs/Sheets/Slides WASM packages with host-mediated HTTP egress, product-auth scoped bearer injection, and manifest-declared Google OAuth setup metadata; full live-recorded parity remains follow-up |
| Hosted MCP extensions | ✅ | 🚧 | Reborn composes host-mediated MCP runtime, bundles the current Notion MCP supported tool set, wires Notion ProductAuth OAuth exchange/refresh, can use Reborn ProductAuth DCR OAuth setup through the host callback origin, and can activate hosted MCP packages with live `tools/list` schema discovery through host-staged product-auth credentials |
| NEAR AI MCP extension | ✅ | 🚧 | Host-bundled Reborn MCP extension exposes `nearai.web_search` via host-mediated HTTP and `llm_nearai_api_key`; local-dev startup now auto-seeds product-auth and activates the bundled MCP extension when `NEARAI_BASE_URL` plus `NEARAI_API_KEY` are configured, and WebChat v2 no longer projects that host-managed credential as extension setup work while NEAR remains a static supported-tool adapter |
| Tool policies (allow/deny) | ✅ | ✅ | Reborn now stores scoped persistent `AlwaysAllow` approval policies for manifest-allow capabilities and replays them at the current sandbox scope; product-facing revoke paths remain follow-up while the policy-store revoke interface is available |
| Tool policies (allow/deny) | ✅ | ✅ | Reborn now stores scoped persistent `AlwaysAllow` approval policies for manifest-allow capabilities and replays them at the current sandbox scope; WebChat v2 exposes authenticated caller-scoped tool approval settings at `/api/webchat/v2/settings/tools` so regular multi-user sessions do not need operator config access; product-facing revoke paths remain follow-up while the policy-store revoke interface is available |
| Exec approvals (`/approve`) | ✅ | ✅ | TUI approval overlay |
| Tool inventory cache | ✅ | ❌ | Coalesced effective-tool inventory cache with channel-registry invalidation |
| Pending exec approval `errorMessage` cleanup | ✅ | ❌ | Failed restart-interrupted approval-pending sessions instead of replaying stale ids |
Expand Down
9 changes: 9 additions & 0 deletions crates/ironclaw_webui_v2/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ browser-reachable.
| `webui.v2.set_active_llm` | POST | `/api/webchat/v2/llm/active` | None | `ProductWorkflow` |
| `webui.v2.test_llm_connection` | POST | `/api/webchat/v2/llm/test-connection` | None | `ProductWorkflow` |
| `webui.v2.list_llm_models` | POST | `/api/webchat/v2/llm/list-models` | None | `ProductWorkflow` |
| `webui.v2.settings.list_tools` | GET | `/api/webchat/v2/settings/tools` | None | `ProjectionOnly` |
| `webui.v2.settings.set_tools_auto_approve` | POST | `/api/webchat/v2/settings/tools` | None | `ProductWorkflow` |
| `webui.v2.settings.set_tool_permission` | POST | `/api/webchat/v2/settings/tools/{capability_id}` | None | `ProductWorkflow` |
| `webui.v2.operator.get_setup` | GET | `/api/webchat/v2/operator/setup` | None | `ProjectionOnly` |
| `webui.v2.operator.run_setup` | POST | `/api/webchat/v2/operator/setup` | None | `ProductWorkflow` |
| `webui.v2.operator.list_config` | GET | `/api/webchat/v2/operator/config` | None | `ProjectionOnly` |
Expand All @@ -103,6 +106,12 @@ constructing the `WebUiAuthenticatedCaller`, carrying the matched
token's `WebUiV2Capabilities`, and injecting both as axum
`Extension`s before the handler runs.

The `/api/webchat/v2/settings/tools` routes are authenticated caller routes,
not operator routes. They expose the caller's tenant/user-scoped tool approval
settings so regular multi-user sessions can read and update global
auto-approve plus per-tool overrides without access to the operator command
plane.

The LLM configuration and operator setup/config/service-control routes are
operator-wide. Host composition mounts them only when the authenticator says
the deployment has an operator configuration surface, and must still authorize
Expand Down
53 changes: 53 additions & 0 deletions crates/ironclaw_webui_v2/src/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ pub const WEBUI_V2_ROUTE_UPDATE_SKILL: &str = "webui.v2.update_skill";
pub const WEBUI_V2_ROUTE_REMOVE_SKILL: &str = "webui.v2.remove_skill";
pub const WEBUI_V2_ROUTE_SET_SKILL_AUTO_ACTIVATE: &str = "webui.v2.set_skill_auto_activate";
pub const WEBUI_V2_ROUTE_SET_AUTO_ACTIVATE_LEARNED: &str = "webui.v2.set_auto_activate_learned";
pub const WEBUI_V2_ROUTE_LIST_SETTINGS_TOOLS: &str = "webui.v2.settings.list_tools";
pub const WEBUI_V2_ROUTE_SET_SETTINGS_TOOLS_AUTO_APPROVE: &str =
"webui.v2.settings.set_tools_auto_approve";
pub const WEBUI_V2_ROUTE_SET_SETTINGS_TOOL_PERMISSION: &str =
"webui.v2.settings.set_tool_permission";
pub const WEBUI_V2_ROUTE_GET_LLM_CONFIG: &str = "webui.v2.get_llm_config";
pub const WEBUI_V2_ROUTE_UPSERT_LLM_PROVIDER: &str = "webui.v2.upsert_llm_provider";
pub const WEBUI_V2_ROUTE_DELETE_LLM_PROVIDER: &str = "webui.v2.delete_llm_provider";
Expand Down Expand Up @@ -131,6 +136,9 @@ pub const WEBUI_V2_PATTERN_SET_SKILL_AUTO_ACTIVATE: &str =
"/api/webchat/v2/skills/{name}/auto-activate";
pub const WEBUI_V2_PATTERN_SET_AUTO_ACTIVATE_LEARNED: &str =
"/api/webchat/v2/skills/auto-activate-learned";
pub const WEBUI_V2_PATTERN_SETTINGS_TOOLS: &str = "/api/webchat/v2/settings/tools";
pub const WEBUI_V2_PATTERN_SETTINGS_TOOL_PERMISSION: &str =
"/api/webchat/v2/settings/tools/{capability_id}";
pub const WEBUI_V2_PATTERN_GET_LLM_CONFIG: &str = "/api/webchat/v2/llm/providers";
pub const WEBUI_V2_PATTERN_UPSERT_LLM_PROVIDER: &str = "/api/webchat/v2/llm/providers";
pub const WEBUI_V2_PATTERN_DELETE_LLM_PROVIDER: &str =
Expand Down Expand Up @@ -211,6 +219,9 @@ pub fn webui_v2_routes() -> Vec<IngressRouteDescriptor> {
remove_skill_descriptor(),
set_skill_auto_activate_descriptor(),
set_auto_activate_learned_descriptor(),
list_settings_tools_descriptor(),
set_settings_tools_auto_approve_descriptor(),
set_settings_tool_permission_descriptor(),
get_llm_config_descriptor(),
upsert_llm_provider_descriptor(),
delete_llm_provider_descriptor(),
Expand Down Expand Up @@ -1025,6 +1036,48 @@ fn set_auto_activate_learned_descriptor() -> IngressRouteDescriptor {
)
}

fn list_settings_tools_descriptor() -> IngressRouteDescriptor {
descriptor(
WEBUI_V2_ROUTE_LIST_SETTINGS_TOOLS,
NetworkMethod::Get,
WEBUI_V2_PATTERN_SETTINGS_TOOLS,
read_policy(
read_rate_limit(),
AuditTraceClass::UserAction,
AllowedEffectPath::ProjectionOnly,
StreamingMode::None,
),
)
}

fn set_settings_tools_auto_approve_descriptor() -> IngressRouteDescriptor {
descriptor(
WEBUI_V2_ROUTE_SET_SETTINGS_TOOLS_AUTO_APPROVE,
NetworkMethod::Post,
WEBUI_V2_PATTERN_SETTINGS_TOOLS,
mutation_policy(
body_limit_kib(4),
mutation_rate_limit(),
AuditTraceClass::UserAction,
AllowedEffectPath::ProductWorkflow,
),
)
}

fn set_settings_tool_permission_descriptor() -> IngressRouteDescriptor {
descriptor(
WEBUI_V2_ROUTE_SET_SETTINGS_TOOL_PERMISSION,
NetworkMethod::Post,
WEBUI_V2_PATTERN_SETTINGS_TOOL_PERMISSION,
mutation_policy(
body_limit_kib(4),
mutation_rate_limit(),
AuditTraceClass::UserAction,
AllowedEffectPath::ProductWorkflow,
),
)
}

fn get_llm_config_descriptor() -> IngressRouteDescriptor {
descriptor(
WEBUI_V2_ROUTE_GET_LLM_CONFIG,
Expand Down
116 changes: 116 additions & 0 deletions crates/ironclaw_webui_v2/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ use crate::router::{WebUiV2Capabilities, WebUiV2State};
use crate::schema::WebChatV2EventFrame;
use crate::sse_capacity::{SSE_MAX_LIFETIME, SseSlot};

const SETTINGS_TOOLS_AUTO_APPROVE_KEY: &str = "agent.auto_approve_tools";
const SETTINGS_TOOL_CONFIG_PREFIX: &str = "tool.";
const SETTINGS_TOOL_CAPABILITY_ID_MAX_BYTES: usize =
OPERATOR_CONFIG_KEY_MAX_BYTES - SETTINGS_TOOL_CONFIG_PREFIX.len();

#[derive(Debug, Clone, Serialize)]
pub struct WebUiV2SessionResponse {
pub tenant_id: String,
Expand Down Expand Up @@ -1322,6 +1327,117 @@ pub async fn run_operator_setup(
Ok(Json(response))
}

/// `GET /api/webchat/v2/settings/tools`
pub async fn list_settings_tools(
State(state): State<WebUiV2State>,
Extension(caller): Extension<WebUiAuthenticatedCaller>,
Extension(_capabilities): Extension<WebUiV2Capabilities>,
) -> Result<Json<RebornOperatorConfigListResponse>, WebUiV2HttpError> {
let mut response = state.services().list_operator_config(caller).await?;
response.entries.retain(|entry| {
entry.key == SETTINGS_TOOLS_AUTO_APPROVE_KEY
|| entry.key.starts_with(SETTINGS_TOOL_CONFIG_PREFIX)
});
Ok(Json(response))
}

#[derive(Debug, Deserialize)]
pub struct SettingsToolsAutoApproveRequest {
pub enabled: bool,
}

/// `POST /api/webchat/v2/settings/tools`
pub async fn set_settings_tools_auto_approve(
State(state): State<WebUiV2State>,
Extension(caller): Extension<WebUiAuthenticatedCaller>,
Extension(_capabilities): Extension<WebUiV2Capabilities>,
Json(body): Json<SettingsToolsAutoApproveRequest>,
) -> Result<Json<RebornOperatorConfigGetResponse>, WebUiV2HttpError> {
let response = state
.services()
.set_operator_config_key(
caller,
SETTINGS_TOOLS_AUTO_APPROVE_KEY.to_string(),
RebornOperatorConfigSetRequest {
value: serde_json::json!(body.enabled),
},
)
.await?;
validate_settings_tool_config_response(&response)?;
Ok(Json(response))
}

#[derive(Debug, Deserialize)]
pub struct SettingsToolPermissionPath {
pub capability_id: String,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SettingsToolPermissionState {
Default,
AlwaysAllow,
AskEachTime,
Disabled,
}

#[derive(Debug, Deserialize)]
pub struct SettingsToolPermissionRequest {
pub state: SettingsToolPermissionState,
}

/// `POST /api/webchat/v2/settings/tools/{capability_id}`
pub async fn set_settings_tool_permission(
State(state): State<WebUiV2State>,
Extension(caller): Extension<WebUiAuthenticatedCaller>,
Extension(_capabilities): Extension<WebUiV2Capabilities>,
Path(SettingsToolPermissionPath { capability_id }): Path<SettingsToolPermissionPath>,
Json(body): Json<SettingsToolPermissionRequest>,
) -> Result<Json<RebornOperatorConfigGetResponse>, WebUiV2HttpError> {
validate_settings_tool_capability_id(&capability_id)?;
let key =
validate_operator_config_key(format!("{SETTINGS_TOOL_CONFIG_PREFIX}{capability_id}"))?;
let response = state
.services()
.set_operator_config_key(
caller,
key,
RebornOperatorConfigSetRequest {
value: serde_json::json!({ "state": body.state }),
},
)
.await?;
validate_settings_tool_config_response(&response)?;
Ok(Json(response))
}
Comment on lines +1389 to +1412

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The state field in SettingsToolPermissionRequest is passed directly to the configuration service without validation. To prevent malformed or invalid state values from being persisted, validate that body.state is one of the allowed permission states ("default", "always_allow", "ask_each_time", or "disabled") before processing the request.

/// `POST /api/webchat/v2/settings/tools/{capability_id}`
pub async fn set_settings_tool_permission(
    State(state): State<WebUiV2State>,
    Extension(caller): Extension<WebUiAuthenticatedCaller>,
    Path(SettingsToolPermissionPath { capability_id }): Path<SettingsToolPermissionPath>,
    Json(body): Json<SettingsToolPermissionRequest>,
) -> Result<Json<RebornOperatorConfigGetResponse>, WebUiV2HttpError> {
    match body.state.as_str() {
        "default" | "always_allow" | "ask_each_time" | "disabled" => {}
        _ => {
            return Err(RebornServicesError::from(WebUiInboundValidationError::new(
                "state",
                WebUiInboundValidationCode::InvalidValue,
            ))
            .into());
        }
    }
    let key = validate_operator_config_key(format!("tool.{capability_id}"))?;
    let response = state
        .services()
        .set_operator_config_key(
            caller,
            key,
            RebornOperatorConfigSetRequest {
                value: serde_json::json!({ "state": body.state }),
            },
        )
        .await?;
    Ok(Json(response))
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.

fn validate_settings_tool_capability_id(capability_id: &str) -> Result<(), WebUiV2HttpError> {
if capability_id.len() > SETTINGS_TOOL_CAPABILITY_ID_MAX_BYTES {
return Err(RebornServicesError::from(WebUiInboundValidationError::new(
"capability_id",
WebUiInboundValidationCode::TooLong,
))
.into());
}
Ok(())
}

fn validate_settings_tool_config_response(
response: &RebornOperatorConfigGetResponse,
) -> Result<(), WebUiV2HttpError> {
if response.entry.key == SETTINGS_TOOLS_AUTO_APPROVE_KEY
|| response.entry.key.starts_with(SETTINGS_TOOL_CONFIG_PREFIX)
{
return Ok(());
}

Err(RebornServicesError::from(WebUiInboundValidationError::new(
"key",
WebUiInboundValidationCode::InvalidValue,
))
.into())
}

/// `GET /api/webchat/v2/operator/config`
pub async fn list_operator_config(
State(state): State<WebUiV2State>,
Expand Down
23 changes: 13 additions & 10 deletions crates/ironclaw_webui_v2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ pub use descriptors::{
WEBUI_V2_ROUTE_LIST_EXTENSION_REGISTRY, WEBUI_V2_ROUTE_LIST_EXTENSIONS,
WEBUI_V2_ROUTE_LIST_FS_MOUNTS, WEBUI_V2_ROUTE_LIST_LLM_MODELS,
WEBUI_V2_ROUTE_LIST_OUTBOUND_DELIVERY_TARGETS, WEBUI_V2_ROUTE_LIST_PROJECT_FILES,
WEBUI_V2_ROUTE_LIST_PROJECT_MEMBERS, WEBUI_V2_ROUTE_LIST_PROJECTS, WEBUI_V2_ROUTE_LIST_SKILLS,
WEBUI_V2_ROUTE_LIST_THREADS, WEBUI_V2_ROUTE_LOGS, WEBUI_V2_ROUTE_OPERATOR_DIAGNOSTICS,
WEBUI_V2_ROUTE_LIST_PROJECT_MEMBERS, WEBUI_V2_ROUTE_LIST_PROJECTS,
WEBUI_V2_ROUTE_LIST_SETTINGS_TOOLS, WEBUI_V2_ROUTE_LIST_SKILLS, WEBUI_V2_ROUTE_LIST_THREADS,
WEBUI_V2_ROUTE_LOGS, WEBUI_V2_ROUTE_OPERATOR_DIAGNOSTICS,
WEBUI_V2_ROUTE_OPERATOR_GET_CONFIG_KEY, WEBUI_V2_ROUTE_OPERATOR_GET_SETUP,
WEBUI_V2_ROUTE_OPERATOR_LIST_CONFIG, WEBUI_V2_ROUTE_OPERATOR_LOGS,
WEBUI_V2_ROUTE_OPERATOR_RUN_SETUP, WEBUI_V2_ROUTE_OPERATOR_SERVICE_LIFECYCLE,
Expand All @@ -83,7 +84,8 @@ pub use descriptors::{
WEBUI_V2_ROUTE_REMOVE_PROJECT_MEMBER, WEBUI_V2_ROUTE_REMOVE_SKILL, WEBUI_V2_ROUTE_RESOLVE_GATE,
WEBUI_V2_ROUTE_RESUME_AUTOMATION, WEBUI_V2_ROUTE_SEARCH_SKILLS, WEBUI_V2_ROUTE_SEND_MESSAGE,
WEBUI_V2_ROUTE_SET_ACTIVE_LLM, WEBUI_V2_ROUTE_SET_AUTO_ACTIVATE_LEARNED,
WEBUI_V2_ROUTE_SET_OUTBOUND_PREFERENCES, WEBUI_V2_ROUTE_SET_SKILL_AUTO_ACTIVATE,
WEBUI_V2_ROUTE_SET_OUTBOUND_PREFERENCES, WEBUI_V2_ROUTE_SET_SETTINGS_TOOL_PERMISSION,
WEBUI_V2_ROUTE_SET_SETTINGS_TOOLS_AUTO_APPROVE, WEBUI_V2_ROUTE_SET_SKILL_AUTO_ACTIVATE,
WEBUI_V2_ROUTE_SETUP_EXTENSION, WEBUI_V2_ROUTE_START_CODEX_LOGIN,
WEBUI_V2_ROUTE_START_NEARAI_LOGIN, WEBUI_V2_ROUTE_STAT_FS_PATH,
WEBUI_V2_ROUTE_STAT_PROJECT_FILE, WEBUI_V2_ROUTE_STREAM_EVENTS,
Expand All @@ -103,13 +105,14 @@ pub use handlers::{
get_operator_status, get_outbound_preferences, get_session, get_skill_content, get_timeline,
install_extension, install_skill, list_automations, list_connectable_channels,
list_extension_registry, list_extensions, list_fs_mounts, list_llm_models,
list_operator_config, list_outbound_delivery_targets, list_skills, list_threads,
pause_automation, query_logs, query_operator_logs, read_fs_file, remove_extension,
remove_skill, resolve_gate, resume_automation, run_operator_service_lifecycle,
run_operator_setup, search_skills, send_message, set_active_llm, set_auto_activate_learned,
set_operator_config_key, set_outbound_preferences, set_skill_auto_activate, setup_extension,
start_codex_login, start_nearai_login, stat_fs_path, stream_events, stream_events_ws,
test_llm_connection, trace_credits, update_skill, upsert_llm_provider,
list_operator_config, list_outbound_delivery_targets, list_settings_tools, list_skills,
list_threads, pause_automation, query_logs, query_operator_logs, read_fs_file,
remove_extension, remove_skill, resolve_gate, resume_automation,
run_operator_service_lifecycle, run_operator_setup, search_skills, send_message,
set_active_llm, set_auto_activate_learned, set_operator_config_key, set_outbound_preferences,
set_settings_tool_permission, set_settings_tools_auto_approve, set_skill_auto_activate,
setup_extension, start_codex_login, start_nearai_login, stat_fs_path, stream_events,
stream_events_ws, test_llm_connection, trace_credits, update_skill, upsert_llm_provider,
};
#[cfg(feature = "webui-v2-beta")]
pub use router::{
Expand Down
9 changes: 9 additions & 0 deletions crates/ironclaw_webui_v2/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use crate::descriptors::{
WEBUI_V2_PATTERN_RESUME_AUTOMATION, WEBUI_V2_PATTERN_SEARCH_SKILLS,
WEBUI_V2_PATTERN_SEND_MESSAGE, WEBUI_V2_PATTERN_SET_ACTIVE_LLM,
WEBUI_V2_PATTERN_SET_AUTO_ACTIVATE_LEARNED, WEBUI_V2_PATTERN_SET_SKILL_AUTO_ACTIVATE,
WEBUI_V2_PATTERN_SETTINGS_TOOL_PERMISSION, WEBUI_V2_PATTERN_SETTINGS_TOOLS,
WEBUI_V2_PATTERN_SETUP_EXTENSION, WEBUI_V2_PATTERN_SKILL_DETAIL,
WEBUI_V2_PATTERN_START_CODEX_LOGIN, WEBUI_V2_PATTERN_START_NEARAI_LOGIN,
WEBUI_V2_PATTERN_STAT_FS_PATH, WEBUI_V2_PATTERN_STAT_PROJECT_FILE,
Expand Down Expand Up @@ -268,6 +269,14 @@ pub fn webui_v2_router_with_options(state: WebUiV2State, options: WebUiV2RouteOp
WEBUI_V2_PATTERN_SET_AUTO_ACTIVATE_LEARNED,
post(handlers::set_auto_activate_learned),
)
.route(
WEBUI_V2_PATTERN_SETTINGS_TOOLS,
get(handlers::list_settings_tools).post(handlers::set_settings_tools_auto_approve),
)
.route(
WEBUI_V2_PATTERN_SETTINGS_TOOL_PERMISSION,
post(handlers::set_settings_tool_permission),
)
.route(
WEBUI_V2_PATTERN_LIST_EXTENSION_REGISTRY,
get(handlers::list_extension_registry),
Expand Down
Loading
Loading