Skip to content

feat(reborn): expose user-scoped tool settings#5256

Merged
aiworkbot merged 7 commits into
mainfrom
codex/multi-tenant-tool-approvals-main
Jun 25, 2026
Merged

feat(reborn): expose user-scoped tool settings#5256
aiworkbot merged 7 commits into
mainfrom
codex/multi-tenant-tool-approvals-main

Conversation

@think-in-universe

Copy link
Copy Markdown
Collaborator

Fixes #5242
Replaces closed draft #5251 with a clean branch based directly on current main (6af6b9318).

Summary

  • Add authenticated non-operator WebUI v2 settings routes for tool approval settings:
    • GET /api/webchat/v2/settings/tools
    • POST /api/webchat/v2/settings/tools
    • POST /api/webchat/v2/settings/tools/{capability_id}
  • Route those handlers through the existing tenant/user-scoped approval config service without requiring operator_webui_config.
  • Update the Settings Tools tab and settings export/import helpers to use the new caller-scoped tools endpoints instead of /operator/config.
  • Lock the descriptor, handler, and operator-route predicate contracts for the new route behavior.

Security / tenancy

  • Operator config routes remain operator-gated.
  • The new settings tools routes still require bearer-authenticated callers, but they are not classified as operator routes.
  • The service continues to resolve settings at tenant/user scope and preserves the existing locked-tool and hard-floor policy behavior.

Tests

  • CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/home/robert/ironclaw-dev-1/target cargo +1.92.0 test -p ironclaw_webui_v2 --features webui-v2-beta every_descriptor_matches_the_locked_policy_surface
  • CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/home/robert/ironclaw-dev-1/target cargo +1.92.0 test -p ironclaw_webui_v2 --features webui-v2-beta settings_tool_routes_do_not_require_operator_capability
  • CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/home/robert/ironclaw-dev-1/target cargo +1.92.0 test -p ironclaw_webui_v2 --features webui-v2-beta operator_route_predicate_matches_operator_config_routes_only
  • git diff --check

Note: an initial parallel test attempt failed because the local filesystem was full while compiling. I removed generated target/debug/incremental cache artifacts and reran the focused checks above successfully with incremental compilation disabled.

@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 17:46 Destroyed
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added WebChat v2 “settings tools” endpoints to list tools, configure global auto-approve, and set per-tool approval permissions for authenticated caller-scoped sessions.
  • Bug Fixes
    • Tightened request/response validation (including state and capability_id checks) and improved fail-closed behavior for tool approval writes.
    • Updated settings UI to use the new endpoints and refresh behavior to invalidate auto-approve tool cache after save/import.
  • UI/Tests
    • Strengthened chat send gating to prevent blocked/duplicate submissions using ref-based checks; expanded relevant unit/contract tests.
  • Documentation
    • Updated feature parity and WebUI v2 route documentation for the caller-scoped tool approval behavior.

Walkthrough

WebUI v2 adds authenticated /api/webchat/v2/settings/tools routes for listing tools, toggling auto-approve, and updating per-tool permission state. Chat send handling now blocks submission when sending is disabled or a run is processing.

Changes

WebUI v2 settings/tools surface

Layer / File(s) Summary
Route contract and policy surface
FEATURE_PARITY.md, crates/ironclaw_webui_v2/CLAUDE.md, crates/ironclaw_webui_v2/src/descriptors.rs, crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs, crates/ironclaw_webui_v2/tests/webui_v2_operator_route_predicate_contract.rs
/api/webchat/v2/settings/tools gains route ids, URL patterns, ingress policy, docs, and locked route-table/predicate coverage for caller-scoped tool approval settings.
Handlers and router wiring
crates/ironclaw_webui_v2/src/handlers.rs, crates/ironclaw_webui_v2/src/router.rs, crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
New handlers list tools, update agent.auto_approve_tools, validate per-tool state, mount the new routes, and add contract coverage for the new request/response paths.
Settings client endpoint switch
crates/ironclaw_webui_v2_static/static/js/pages/settings/hooks/useSettings.js, crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js
The settings client reads and writes tool approval data through /api/webchat/v2/settings/tools, and invalidates ["settings-tools"] after auto-approve saves and imports.
Re-export formatting only
crates/ironclaw_webui_v2/src/lib.rs
The WebUI v2 re-export lists are reformatted without changing exported names.

Chat send blocking

Layer / File(s) Summary
Send blocking and Enter handling
crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js, crates/ironclaw_webui_v2_static/static/js/pages/chat/chat.js, crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat.test.mjs, crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/useChat-send.test.mjs
useChat and Chat use ref-backed send blocking during render and send dispatch, and the run-processing tests cover duplicate-submit suppression.
Composer Enter blocking
crates/ironclaw_webui_v2_static/static/js/pages/chat/components/chat-input.js, crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat-input.test.mjs
ChatInput adds synchronous send blocking for disabled state, and the input tests cover Enter handling and draft retention.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • nearai/ironclaw#5021: Both PRs touch the WebChat v2 frontend’s chat state/timing logic in useChat.js and related send/input gating.
  • nearai/ironclaw#5235: Both PRs modify crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js and its send flow behavior.

Suggested reviewers

  • copilot-pull-request-reviewer

Poem

A tools page found a quieter gate,
where scoped approvals now sit in state.
One Enter key waits,
one settings route updates,
and the chat box learns when not to send.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several chat send-path changes and tests are unrelated to the Settings tools issue and appear outside the linked scope. Split the chat send-path edits into a separate PR or add a linked issue justifying them, then re-review the settings-only change.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Conventional Commit-style title accurately summarizes the main change: exposing user-scoped tool settings.
Description check ✅ Passed The description covers summary, linked issue, tests, and security/tenancy, so it is mostly complete despite missing some template sections.
Linked Issues check ✅ Passed The PR adds caller-scoped settings/tools routes and updates the UI to stop using operator/config for regular users, matching #5242.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces new user-scoped tool approval settings endpoints (/api/webchat/v2/settings/tools) to allow regular multi-user sessions to read and update global auto-approve settings and per-tool overrides without requiring operator-level access. It updates the backend routes, descriptors, handlers, and tests, as well as the frontend API calls to leverage these new endpoints. The review feedback suggests adding validation to the state field in the set_settings_tool_permission handler to ensure only valid permission states are accepted.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1368 to +1387
/// `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> {
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))
}

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))
}

@github-actions github-actions Bot added scope: docs Documentation size: M 50-199 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jun 25, 2026
@think-in-universe think-in-universe changed the title [codex] expose user-scoped tool settings feat(reborn): expose user-scoped tool settings Jun 25, 2026
@think-in-universe think-in-universe marked this pull request as ready for review June 25, 2026 18:15
@railway-app

railway-app Bot commented Jun 25, 2026

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5256 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw 🕒 Building (View Logs) Web Jun 25, 2026 at 7:27 pm

@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 18:16 Destroyed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6185b855a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


export async function fetchSettingsExport() {
const data = await apiFetch(OPERATOR_CONFIG_BASE);
const data = await apiFetch(SETTINGS_TOOLS_BASE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rebuild the served WebUI bundle

This endpoint change is only in the source modules; the committed SPA bundle that index.html serves (/v2/dist/app.js) was not regenerated. I checked static/dist/app.js and it still contains /api/webchat/v2/operator/config with no /api/webchat/v2/settings/tools, so in multi-user deployments where operator routes are absent or gated the Settings Tools tab will keep calling the operator config API and fail instead of using the new caller-scoped route. Please rebuild and commit static/dist after editing static/js/**.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs`:
- Around line 716-766: The /settings/tools contract tests currently cover only
route metadata and do not verify that the new public surface is limited to
tool-specific keys. Add a caller-driven regression test around the existing
handlers in handlers.rs (especially list_operator_config and
set_operator_config_key) that exercises the real GET/POST call sites and asserts
GET returns only tool approval settings while POST can only modify
agent.auto_approve_tools and tool.* entries, preventing unrelated caller-scoped
config from leaking through RebornOperatorConfig* DTOs.

In `@crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs`:
- Around line 2975-3041: The settings tools route test is using StubServices in
a way that bypasses the production caller-scoped behavior, so it cannot detect
the real 503/service-unavailable path. Update
settings_tool_routes_do_not_require_operator_capability to exercise the same
service behavior as the production facade for set_operator_config_key and
list_operator_config, or add a separate test that uses the real reborn_services
implementation, so the assertions verify the route through the actual caller
path rather than a stubbed success path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e5617d9b-4c3d-4903-a871-b9a53ba4ab5d

📥 Commits

Reviewing files that changed from the base of the PR and between 6af6b93 and b53865e.

📒 Files selected for processing (11)
  • FEATURE_PARITY.md
  • crates/ironclaw_webui_v2/CLAUDE.md
  • crates/ironclaw_webui_v2/src/descriptors.rs
  • crates/ironclaw_webui_v2/src/handlers.rs
  • crates/ironclaw_webui_v2/src/lib.rs
  • crates/ironclaw_webui_v2/src/router.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_operator_route_predicate_contract.rs
  • crates/ironclaw_webui_v2_static/static/js/pages/settings/hooks/useSettings.js
  • crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js

Comment thread crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs
Comment thread crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 18:29 Destroyed
@github-actions github-actions Bot added size: L 200-499 changed lines and removed size: M 50-199 changed lines labels Jun 25, 2026
@think-in-universe think-in-universe force-pushed the codex/multi-tenant-tool-approvals-main branch from 9191719 to f6f8a88 Compare June 25, 2026 18:38
@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 18:38 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat-input.test.mjs`:
- Around line 230-256: The null-path test for ChatInput only verifies the draft
is preserved, but it should also cover the retry behavior in
ChatInput.handleSend. Update the test that uses onSend: async () => null to
press Enter a second time after the first failed send and assert that onSend is
invoked again, while still confirming the draft is not cleared. Use the existing
renderChatInput, templateProps, and textarea keydown flow so the regression
where sending gets stuck blocked is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cb891d9b-7852-4432-bb93-916e0dfaf26d

📥 Commits

Reviewing files that changed from the base of the PR and between 9191719 and f6f8a88.

📒 Files selected for processing (4)
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/chat.js
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/components/chat-input.js
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat-input.test.mjs
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat.test.mjs

Copy link
Copy Markdown
Collaborator Author

[BLOCK] Not ready for human final review yet.

Blocking status:

Pass list of checks:

  • Session/route integration is solid at a glance for global_auto_approve and settings-tools routing.
  • Core behavior in crates/ironclaw_webui_v2/src/handlers.rs includes validation on tool state and guarded timeout/fallback handling, so no new blocking runtime regressions were identified in this pass.

Action before human-ready:

  1. Resolve merge conflict and rebase onto current base.
  2. Confirm scope split/out-of-scope changes are intentional or split out.
  3. Re-verify preview/CI after conflict resolution before flipping readiness.

@think-in-universe think-in-universe force-pushed the codex/multi-tenant-tool-approvals-main branch from f6f8a88 to 8ae360e Compare June 25, 2026 19:16
@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 19:16 Destroyed
@github-actions github-actions Bot added size: XL 500+ changed lines and removed size: L 200-499 changed lines labels Jun 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js (1)

29-40: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the path key as capability_id, not name.

POST /api/webchat/v2/settings/tools/{capability_id} is keyed by capability id, but this client posts tool.name. toolFromConfigEntry() currently prefers value.name over the key suffix, so any display label returned by the server will turn permission writes into the wrong route.

Suggested diff
 export function toolFromConfigEntry(entry) {
   if (!entry?.key?.startsWith(TOOL_PREFIX)) return null;
   const value = entry.value || {};
-  const name = value.name || entry.key.slice(TOOL_PREFIX.length);
+  const capability_id = entry.key.slice(TOOL_PREFIX.length);
+  const name = value.name || capability_id;
   return {
+    capability_id,
     name,
     description: value.description || "",
     state: normalizeToolState(value.state),
@@
-export async function updateToolPermission(name, state) {
+export async function updateToolPermission(capabilityId, state) {
   const normalized = normalizeToolUpdateState(state);
-  const data = await apiFetch(`${SETTINGS_TOOLS_BASE}/${encodeURIComponent(name)}`, {
+  const data = await apiFetch(
+    `${SETTINGS_TOOLS_BASE}/${encodeURIComponent(capabilityId)}`,
+    {
       method: "POST",
       body: JSON.stringify({ state: normalized }),
-  });
+    }
+  );
   return { success: true, tool: toolFromConfigEntry(data.entry), entry: data.entry };
 }

Also applies to: 161-166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js`
around lines 29 - 40, The settings tool mapping is using the display name
instead of the capability key, which causes writes to target the wrong route.
Update toolFromConfigEntry() so it preserves the path-derived capability
identifier from entry.key as capability_id and does not prefer value.name for
that field; then adjust the settings API caller that posts tool updates to use
capability_id rather than tool.name. Keep name as the UI label if needed, but
make the identifier used for permission writes come from the TOOL_PREFIX key
suffix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@crates/ironclaw_webui_v2_static/static/js/pages/settings/hooks/useSettings.js`:
- Around line 48-50: The settings invalidation for agent.auto_approve_tools is
only handled in the direct save path, so mirror the same
queryClient.invalidateQueries call in the import flow as well. Update
importSettings()/importMutation.onSuccess in useSettings to also invalidate the
settings-tools query whenever the imported payload changes
agent.auto_approve_tools, alongside the existing settings-export invalidation,
so the Tools tab refreshes after import.

In `@crates/ironclaw_webui_v2/src/handlers.rs`:
- Around line 1329-1407: The settings handlers list_settings_tools,
set_settings_tools_auto_approve, and set_settings_tool_permission currently only
require WebUiAuthenticatedCaller, so they can execute even when
WebUiV2Capabilities was not injected by the host. Update these handler
signatures to also extract WebUiV2Capabilities as an axum::Extension, matching
the existing authenticated handler pattern, so the request fails closed with a
500 at the boundary when host composition is incomplete.
- Around line 1370-1403: `SettingsToolPermissionRequest` is still using a raw
`String` for the fixed `state` contract, which forces manual validation in
`validate_settings_tool_permission_state`. Replace that field with a serde enum
representing the four allowed values, using canonical wire names via
`#[serde(rename_all = "snake_case")]` or explicit renames. Then update
`set_settings_tool_permission` to rely on typed deserialization and serialize
the enum back into the config value instead of string matching.

In
`@crates/ironclaw_webui_v2/tests/webui_v2_operator_route_predicate_contract.rs`:
- Line 7: The operator predicate contract test is only excluding the two POST
settings tool routes, so it would miss a regression that incorrectly treats the
GET tools route as operator-only. Update the exclusion set in
webui_v2_operator_route_predicate_contract.rs for the predicate coverage around
WEBUI_V2_ROUTE_SET_SETTINGS_TOOL_PERMISSION and
WEBUI_V2_ROUTE_SET_SETTINGS_TOOLS_AUTO_APPROVE to also include
WEBUI_V2_ROUTE_LIST_SETTINGS_TOOLS, so the full /api/webchat/v2/settings/tools
surface stays caller-authenticated in the test.

---

Outside diff comments:
In
`@crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js`:
- Around line 29-40: The settings tool mapping is using the display name instead
of the capability key, which causes writes to target the wrong route. Update
toolFromConfigEntry() so it preserves the path-derived capability identifier
from entry.key as capability_id and does not prefer value.name for that field;
then adjust the settings API caller that posts tool updates to use capability_id
rather than tool.name. Keep name as the UI label if needed, but make the
identifier used for permission writes come from the TOOL_PREFIX key suffix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9a8fa76c-b5bc-4a9d-aadc-9dbfee2bebb1

📥 Commits

Reviewing files that changed from the base of the PR and between f6f8a88 and 8ae360e.

⛔ Files ignored due to path filters (1)
  • crates/ironclaw_webui_v2_static/static/dist/app.js is excluded by !**/dist/**
📒 Files selected for processing (15)
  • FEATURE_PARITY.md
  • crates/ironclaw_webui_v2/CLAUDE.md
  • crates/ironclaw_webui_v2/src/descriptors.rs
  • crates/ironclaw_webui_v2/src/handlers.rs
  • crates/ironclaw_webui_v2/src/lib.rs
  • crates/ironclaw_webui_v2/src/router.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_descriptors_contract.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_operator_route_predicate_contract.rs
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/chat.js
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/components/chat-input.js
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat-input.test.mjs
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/chat.test.mjs
  • crates/ironclaw_webui_v2_static/static/js/pages/settings/hooks/useSettings.js
  • crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js

Comment thread crates/ironclaw_webui_v2/src/handlers.rs
Comment thread crates/ironclaw_webui_v2/src/handlers.rs Outdated
Comment thread crates/ironclaw_webui_v2/tests/webui_v2_operator_route_predicate_contract.rs Outdated
@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 19:27 Destroyed
@think-in-universe

Copy link
Copy Markdown
Collaborator Author

@claude review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js (1)

249-265: 🎯 Functional Correctness | 🟡 Minor

Sync the send-blocking refs before paint. setIsProcessingState(false) / setPendingGateState(null) clear the thread-switch state during render, but isProcessingRef.current and pendingGateRef.current only update in React.useEffect. That leaves send() able to read the previous thread’s busy/gate state and reject the first submit on the new thread. Move those ref writes to React.useLayoutEffect, or keep the refs in lockstep with the reset path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js` around
lines 249 - 265, The send-blocking refs are updated too late, so `send()` can
still see stale busy/gate state after a thread switch. In `useChat`, keep
`isProcessingRef.current` and `pendingGateRef.current` in sync with the reset
path by moving their updates from `React.useEffect` to `React.useLayoutEffect`,
or otherwise update the refs immediately when `setIsProcessingState(false)` and
`setPendingGateState(null)` run. Ensure the ref values match the new thread
before paint so the first submit on the new thread is not rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js`:
- Around line 249-265: The send-blocking refs are updated too late, so `send()`
can still see stale busy/gate state after a thread switch. In `useChat`, keep
`isProcessingRef.current` and `pendingGateRef.current` in sync with the reset
path by moving their updates from `React.useEffect` to `React.useLayoutEffect`,
or otherwise update the refs immediately when `setIsProcessingState(false)` and
`setPendingGateState(null)` run. Ensure the ref values match the new thread
before paint so the first submit on the new thread is not rejected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 84a36cc9-484d-4a26-a939-6147b3fef2e9

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae360e and e06400f.

⛔ Files ignored due to path filters (1)
  • crates/ironclaw_webui_v2_static/static/dist/app.js is excluded by !**/dist/**
📒 Files selected for processing (3)
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/components/chat-input.js
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/useChat-send.test.mjs

@claude

claude Bot commented Jun 25, 2026

Copy link
Copy Markdown

Code Review Results — PR #5256

Found 3 issues:

1. [HIGH:75] Information Disclosure — Response data leakage from mutation endpoints

Location: crates/ironclaw_webui_v2/src/handlers.rs:1347, 1387

The set_settings_tools_auto_approve and set_settings_tool_permission handlers call the backend set_operator_config_key and return the response directly without filtering sensitive configuration entries. While list_settings_tools correctly filters responses to only tool-approval configs (lines 1334–1337), the mutation handlers do not perform this filtering.

The backend response RebornOperatorConfigGetResponse may contain operator-scoped config entries like provider.default, secret.api_key, etc., which should not be exposed through the user-facing settings API. This allows authenticated users to discover the presence of operator-level configuration.

Fix: Apply the same filtering pattern to both mutation handlers:

response.entries.retain(|e| {
    e.key == "agent.auto_approve_tools" || e.key.starts_with("tool.")
});

2. [MEDIUM:75] Type-driven design violation — stringly-typed permission state

Location: crates/ironclaw_webui_v2/src/handlers.rs:1371–1372, 1375–1383

The SettingsToolPermissionRequest::state field accepts a raw String validated against four literal strings. Per .claude/rules/types.md, domain values must use specialized types — enums for fixed small sets. The canonical CapabilityPermissionState enum already exists in ironclaw_approvals with the correct variants and wire format.

The PR also validates a "default" state (line 1377) that doesn't exist in the canonical enum — it appears only as a reason string in backend logic, not a valid wire value. This mismatch risks silent mishandling.

Fix: Replace String with CapabilityPermissionState/ToolPermissionState in the request struct and remove manual validation. Serde will validate at the boundary.


3. [MEDIUM:HIGH] Unbounded path parameter allocation

Location: crates/ironclaw_webui_v2/src/handlers.rs:1395

The capability_id path parameter is extracted as String and immediately used in format!("{SETTINGS_TOOL_CONFIG_PREFIX}{capability_id}") before validate_operator_config_key() validates length (128-byte limit). An attacker-controlled client could send an arbitrarily long path parameter, causing unbounded heap allocation before validation rejects it.

Fix: Validate capability_id length immediately after extraction:

const MAX_CAPABILITY_ID_LEN: usize = 128 - SETTINGS_TOOL_CONFIG_PREFIX.len();
if capability_id.len() > MAX_CAPABILITY_ID_LEN {
    return Err(/* validation error */);
}
let key = format!("{SETTINGS_TOOL_CONFIG_PREFIX}{capability_id}");

Positive Notes

✅ Route patterns, descriptors, and operator-capability boundary are correct
✅ Routes correctly omit operator-capability checks per spec (CLAUDE.md lines 109–113)
✅ Route table in CLAUDE.md accurate and matches implementation
✅ Handler tests have good coverage of the happy path and validation errors
✅ All error handling and HTTP status codes are correct
✅ No N+1 queries, blocking operations, or resource leaks

@think-in-universe

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jun 25, 2026

Copy link
Copy Markdown

Found 8 issues: 2 HIGH (inefficient filtering, JS response mismatch), 6 MEDIUM (type safety, validation, error handling, performance), 3 LOW (docs, tests, rate limits). See detailed analysis in https://github.com/anthropics/ironclaw/pull/5256

@claude

claude Bot commented Jun 25, 2026

Copy link
Copy Markdown

HIGH SEVERITY:

  1. Inefficient filtering in list_settings_tools (L1329-L1340)

    • Fetches ALL operator config then filters in-memory
    • Fix: Pass filter params to service layer for DB-level filtering
  2. JavaScript response type mismatch in AUTO_APPROVE_KEY path

    • UpdateSetting() may not handle response structure correctly
    • File: crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js

@claude

claude Bot commented Jun 25, 2026

Copy link
Copy Markdown

MEDIUM SEVERITY:

  1. Stringly-typed state and capability_id (L1343-L1375)

    • Should use enums for state, newtypes for capability_id per CLAUDE.md
    • Risk: Identity-confusion from raw String types
  2. capability_id validated after interpolation (L1378-L1383)

    • Validates format!('tool.{capability_id}') instead of component first
    • Result: Brittle, less specific error messages
  3. Incomplete error handling in JavaScript

    • fetchSetting() and updateSetting() have asymmetric error paths
    • File: crates/ironclaw_webui_v2_static/static/js/pages/settings/lib/settings-api.js
  4. Repeated string allocations in validate_settings_tool_permission_state (L1368-L1375)

    • Match comparisons on every request with no caching
    • Suggestion: Use lazy_static or const array
  5. Test code uses .expect('lock') on mutex

    • Violates CLAUDE.md no-.expect() rule
    • File: crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
  6. Frontend fetches full config to get one value

    • fetchSetting() for AUTO_APPROVE_KEY calls fetchSettingsExport()
    • Each tab load triggers unnecessary large response

@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 19:43 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@crates/ironclaw_webui_v2_static/static/js/pages/settings/hooks/useSettings.js`:
- Around line 63-66: The settings import handler in useSettings should
invalidate the tools query for per-tool permission imports as well, not just
agent.auto_approve_tools. Update the importedKeys check in the settings import
logic so that it also detects any tool.* entries before calling
queryClient.invalidateQueries for ["settings-tools"], ensuring the Tools tab
refreshes when imported payloads change individual tool permissions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 41106e22-97db-4b08-bfe1-83b1f713df5e

📥 Commits

Reviewing files that changed from the base of the PR and between e06400f and 4e4ddc3.

⛔ Files ignored due to path filters (1)
  • crates/ironclaw_webui_v2_static/static/dist/app.js is excluded by !**/dist/**
📒 Files selected for processing (4)
  • crates/ironclaw_webui_v2/src/handlers.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_handlers_contract.rs
  • crates/ironclaw_webui_v2/tests/webui_v2_operator_route_predicate_contract.rs
  • crates/ironclaw_webui_v2_static/static/js/pages/settings/hooks/useSettings.js

Comment on lines 63 to +66
const importedKeys = Object.keys(payload?.settings || {});
if (importedKeys.includes("agent.auto_approve_tools")) {
queryClient.invalidateQueries({ queryKey: ["settings-tools"] });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Invalidate ["settings-tools"] for imported tool.* keys too.

The new tools query covers both agent.auto_approve_tools and tool.* entries, so an import that only changes per-tool permissions leaves the Tools tab stale until refresh.

Suggested diff
     onSuccess: (_data, payload) => {
       queryClient.invalidateQueries({ queryKey: ["settings-export"] });
       const importedKeys = Object.keys(payload?.settings || {});
-      if (importedKeys.includes("agent.auto_approve_tools")) {
+      if (
+        importedKeys.some(
+          (key) =>
+            key === "agent.auto_approve_tools" || key.startsWith("tool.")
+        )
+      ) {
         queryClient.invalidateQueries({ queryKey: ["settings-tools"] });
       }
       if (importedKeys.some((key) => RESTART_REQUIRED_KEYS.has(key))) {
         setNeedsRestart(true);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const importedKeys = Object.keys(payload?.settings || {});
if (importedKeys.includes("agent.auto_approve_tools")) {
queryClient.invalidateQueries({ queryKey: ["settings-tools"] });
}
const importedKeys = Object.keys(payload?.settings || {});
if (
importedKeys.some(
(key) =>
key === "agent.auto_approve_tools" || key.startsWith("tool.")
)
) {
queryClient.invalidateQueries({ queryKey: ["settings-tools"] });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@crates/ironclaw_webui_v2_static/static/js/pages/settings/hooks/useSettings.js`
around lines 63 - 66, The settings import handler in useSettings should
invalidate the tools query for per-tool permission imports as well, not just
agent.auto_approve_tools. Update the importedKeys check in the settings import
logic so that it also detects any tool.* entries before calling
queryClient.invalidateQueries for ["settings-tools"], ensuring the Tools tab
refreshes when imported payloads change individual tool permissions.

@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5256 June 25, 2026 19:57 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js (2)

248-255: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the submit latch on thread changes.

Line 248 resets per-thread transient state, but submitBusyRef survives the switch. If the prior thread already returned a run_id, the only normal clear is onRunSettled; after useSSE moves to the new threadId, that old settlement may never be observed, leaving sends in the new thread returning null. Reset or thread-scope this ref without writing it in the render-time reset block.

Proposed direction
 React.useEffect(() => {
   resetToolActivityState(toolActivityStateRef);
   locallyResolvedGatesRef.current.clear();
+  submitBusyRef.current = false;
 }, [threadId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js` around
lines 248 - 255, Clear the submit latch when switching threads, since
submitBusyRef can stay latched from the previous thread and block sends in the
new one. Update useChat’s thread-change handling around the stateThreadId reset
so the latch is reset or scoped per thread, but do not write to the ref inside
the render-time reset block; instead tie the cleanup to the thread transition
and/or the existing onRunSettled flow so useSSE and submit handling stay in
sync.

417-471: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Set submitBusyRef before the first await.

Line 421 awaits resolveConnectAction() before Line 471 flips the local latch, so two same-tick sends can both pass Lines 409-413 and create duplicate threads/messages. Flip the latch immediately after the guard, and clear it for channel-connect early returns and preflight failures.

Proposed direction
       if (
         submitBusyRef.current ||
         isProcessingRef.current ||
         activeRunBlocksSend
       ) {
         return null;
       }
+      submitBusyRef.current = true;
 
       // Channel-connect slash commands...
+      try {
       if (stagedAttachments.length === 0) {
         const connectable = await resolveConnectAction(content);
         if (connectable) {
           setChannelConnectAction(connectable);
+          submitBusyRef.current = false;
           return { channel_connect_action: connectable };
         }
       }
       setChannelConnectAction(null);
 
       let sendThreadId = targetThreadId || threadId;
 
       if (!sendThreadId) {
         const created = await createThreadRequest();
         queryClient.invalidateQueries({ queryKey: ["threads"] });
         sendThreadId = created?.thread?.thread_id;
         if (!sendThreadId) {
           throw new Error("createThread returned no thread_id");
         }
       }
+      } catch (err) {
+        submitBusyRef.current = false;
+        throw err;
+      }
 
-      submitBusyRef.current = true;
       updateCurrentThread((prev) => [...prev, pendingRenderMessage]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js` around
lines 417 - 471, `useChat.js` has a race in the send flow: `submitBusyRef` is
set too late, after `resolveConnectAction()` and thread creation work have
already started. Move the latch assignment to immediately after the initial
guard in the send path so `submitBusyRef.current` blocks same-tick reentry
before any await in the send handler, and make sure it is reset on the
channel-connect early return and any preflight/error exit paths. Use the
existing `submitBusyRef`, `resolveConnectAction`, `createThreadRequest`, and
`setChannelConnectAction` flow to locate and update the send handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js`:
- Around line 248-255: Clear the submit latch when switching threads, since
submitBusyRef can stay latched from the previous thread and block sends in the
new one. Update useChat’s thread-change handling around the stateThreadId reset
so the latch is reset or scoped per thread, but do not write to the ref inside
the render-time reset block; instead tie the cleanup to the thread transition
and/or the existing onRunSettled flow so useSSE and submit handling stay in
sync.
- Around line 417-471: `useChat.js` has a race in the send flow: `submitBusyRef`
is set too late, after `resolveConnectAction()` and thread creation work have
already started. Move the latch assignment to immediately after the initial
guard in the send path so `submitBusyRef.current` blocks same-tick reentry
before any await in the send handler, and make sure it is reset on the
channel-connect early return and any preflight/error exit paths. Use the
existing `submitBusyRef`, `resolveConnectAction`, `createThreadRequest`, and
`setChannelConnectAction` flow to locate and update the send handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 361e4f41-4f76-485c-bb05-5cd27458c3a9

📥 Commits

Reviewing files that changed from the base of the PR and between 4e4ddc3 and 7cf4c40.

⛔ Files ignored due to path filters (1)
  • crates/ironclaw_webui_v2_static/static/dist/app.js is excluded by !**/dist/**
📒 Files selected for processing (2)
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/hooks/useChat.js
  • crates/ironclaw_webui_v2_static/static/js/pages/chat/lib/useChat-send.test.mjs

@aiworkbot aiworkbot added this pull request to the merge queue Jun 25, 2026
Merged via the queue into main with commit 9ce47c4 Jun 25, 2026
119 checks passed
@aiworkbot aiworkbot deleted the codex/multi-tenant-tool-approvals-main branch June 25, 2026 20:22
zetyquickly added a commit that referenced this pull request Jun 25, 2026
Rebase #4544 onto current main (106 commits of drift). Resolved 2 conflicts: product_workflow/src/lib.rs export list (kept both LifecycleSearchExtensionSummary from main and lifecycle_package_kind_label from #4544); FEATURE_PARITY.md (kept #4544's scoped-lifecycle clause on Hosted MCP, main's newer #5256 rows for NEAR AI MCP and Tool policies).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Reborn] Tools page under Settings shows operator-only tools error for WebUI users

2 participants