fix(webui): scope workspace and memory views - #7062
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
🚅 Deployed to the ironclaw-pr-7062 environment in ironclaw-ci-preview
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe change adds caller-scoped workspace policy across deployment configuration, runtime mounts, WebUI handlers, frontend browsing, and filesystem root handling. It adds coverage for profile defaults, authentication modes, workspace isolation, traversal rejection, and fresh workspace roots. ChangesWorkspace projection
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Serve
participant RuntimeComposition
participant WorkspaceFilesystem
participant WebUiV2
participant WorkspaceBrowser
Serve->>RuntimeComposition: enable caller-scoped workspace policy
RuntimeComposition->>WorkspaceFilesystem: resolve tenant/user workspace mount
Serve->>WebUiV2: propagate projection setting
WorkspaceBrowser->>WebUiV2: request workspace path
WebUiV2->>WorkspaceFilesystem: query caller-prefixed path
WorkspaceFilesystem-->>WebUiV2: return scoped result
WebUiV2-->>WorkspaceBrowser: return mount-relative path
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔎 Review · PR #7062
1 actionable findings →The change breaks the Workspace/Memory browser by adding tenant/user storage prefixes on top of an API that is already caller-scoped server-side. The new frontend tests pass because their mocks encode the incorrect raw-storage API contract. Automatic · PR opened + CI failed · attempt 1 of 3 · completed in 2m 28s Run details
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/frontend/src/pages/workspace/lib/workspace-api.test.ts`:
- Line 135: Update the affected hosted test calls to listWorkspace in the
workspace API tests to explicitly pass requireScopedWorkspace: true alongside
currentUser. Keep requireScopedWorkspace: false only for the local-fallback
test, ensuring the hosted workspace cases exercise the production required-scope
behavior rather than the module default.
- Around line 279-293: Update the affected workspace security tests around
readWorkspaceFile to record observed stat request paths in the fetch handler
instead of asserting there. Constrain assert.rejects to the expected error, then
assert after rejection that the unscoped shared path was never requested while
the scoped path behavior remains covered; apply the same pattern to both test
cases.
In `@crates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.ts`:
- Around line 196-206: The workspace path helpers re-resolve scoped roots and
memory directories for every file operation, causing redundant sequential
`/fs/list` requests. Update `resolveWorkspacePath` and `resolveMemoryPath` to
memoize resolved prefixes/collapsed directories keyed by `{currentUser,
requireScopedWorkspace}`, or propagate the listing result’s `actualPath` and
reuse it for stat and content URLs; ensure subsequent `readWorkspaceFile`
operations avoid repeating the hierarchy traversal.
- Around line 285-316: Change the default for requireScopedWorkspace from false
to true in resolveDirectory, resolveFilePath, listWorkspace, and
readWorkspaceFile so omitted options use scoped behavior. Update tests that rely
on permissive behavior to pass requireScopedWorkspace: false explicitly, while
preserving explicit true and false handling.
- Around line 145-206: Enforce tenant/user workspace containment at the server
boundary for the `/fs/list`, `/fs/stat`, `/fs/content`, and equivalent
project-file handlers, rather than relying on the frontend functions
`resolveWorkspaceRootWithOptions`, `resolveWorkspaceDirectory`, and
`resolveWorkspacePath`. Validate every client-supplied path against the
authenticated tenant/user prefix and return 403 for cross-user paths; add an
integration test covering that rejection.
🪄 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: 587fcacf-1111-47ee-8751-56b8edc8bc0c
📒 Files selected for processing (13)
crates/ironclaw_reborn_cli/src/commands/serve.rscrates/ironclaw_reborn_composition/tests/webui_v2_serve.rscrates/ironclaw_webui/frontend/src/app/app.tsxcrates/ironclaw_webui/frontend/src/app/auth.tscrates/ironclaw_webui/frontend/src/layout/gateway-layout.tsxcrates/ironclaw_webui/frontend/src/pages/workspace/hooks/useWorkspaceBrowser.tscrates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.test.tscrates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.tscrates/ironclaw_webui/frontend/src/pages/workspace/workspace-page.tsxcrates/ironclaw_webui/src/webui_serve.rscrates/ironclaw_webui/src/webui_v2/handlers.rscrates/ironclaw_webui/src/webui_v2/router.rscrates/ironclaw_webui/tests/webui_v2_handlers_contract.rs
There was a problem hiding this comment.
🔍 Review complete · PR #7062
The change breaks the Workspace/Memory browser by adding tenant/user storage prefixes on top of an API that is already caller-scoped server-side. The new frontend tests pass because their mocks encode the incorrect raw-storage API contract.
Findings
- 🔴 High · Do not prepend storage ownership paths to the already caller-scoped filesystem API —
crates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.ts:244-250
Details are attached to the relevant diff.
Validation and technical details
- Reviewed the complete
refs/ironloop/base...refs/ironloop/headcomparison across all 13 changed files and applicable crate guidance. - Traced
/api/webchat/v2/fs/{list,stat,content}through WebUI handlers, productauthorize_browse_scope,FilesystemBrowseReader, andMountScopedFilesystemReader. - Verified existing
webui_filesystem_memory_mount_is_scoped_to_authenticated_usercomposed E2E coverage establishes mount-relative, server-scoped behavior. git diff --check refs/ironloop/base...refs/ironloop/headcompleted cleanly.- Frontend Vitest suite passed: 124 files, 1,056 tests. TypeScript
tsc --noEmitcompleted successfully; Node 24.18.0 emitted an engine warning because the package requests Node 22.22.x. - Base:
main - Head:
codex/webui-workspace-scoped-projectionat411c751 - Run:
14148217-ffe0-4862-9ae0-e9d9ac245b7d
| let actualPath = prefix; | ||
| let response; | ||
| try { | ||
| response = await fetchFsList(MEMORY_MOUNT, actualPath); | ||
| } catch (error) { | ||
| if (!isNotFound(error)) throw error; | ||
| response = emptyDirectoryResponse(); |
There was a problem hiding this comment.
🔴 High · Do not prepend storage ownership paths to the already caller-scoped filesystem API
The /fs/* handlers already bind the authenticated caller, derive an authorized ResourceScope, and return mount-relative paths. Existing composed E2E coverage confirms that requesting mount=memory at the root returns the caller's MEMORY.md, while another caller cannot access the raw tenant/user path. This code instead starts every Memory lookup at tenants/{tenant}/users/{user} (and applies the same assumption to Workspace), so hosted users query that prefix inside their already-scoped mount. On the real backend this produces a 404/empty view; file previews similarly stat/download doubly-prefixed paths. Keep tenant/user prefix resolution authoritative on the server and consume only the mount-relative paths returned by /fs/*. The frontend tests should mock that existing contract rather than a raw shared tree.
There was a problem hiding this comment.
Valid finding — fixed in 03c5084.
The /fs/{list,stat,content} handlers bind the authenticated caller and return mount-relative, server-scoped paths (already proven by the composed test webui_filesystem_memory_mount_is_scoped_to_authenticated_user in crates/ironclaw_reborn_composition/tests/webui_v2_e2e.rs, which shows user B gets 404 for user A's MEMORY.md and for raw tenants/.../users/alice/... paths). The frontend was incorrectly prepending tenants/{tenant}/users/{user} on top of that server-scoped API.
The frontend now consumes the existing server-scoped contract directly:
- No tenant/user prefix is built for any
/fs/*request. Paths are mount-relative. currentUseris retained only as a fail-closed identity gate whenrequireScopedWorkspaceis true — missing identity renders empty listings and short-circuits file reads rather than requesting a raw mount root.requireScopedWorkspacedefaults totrueat the exported entry points so an omitted option cannot enable raw-root behavior.- Memory collapse-walk over the caller's own mount-relative tree is kept so internal
agents/<agent>/projects/_nonewrappers stay hidden; every request stays mount-relative.
No server logic was added — the server is already the authority for caller scope. Frontend tests now mock that mount-relative contract.
…orts Address review feedback on #7062 (reopened from #5831): - The /fs/{list,stat,content} handlers bind the authenticated caller and return mount-relative, server-scoped paths (covered by the existing webui_filesystem_memory_mount_is_scoped_to_authenticated_user composed test). The frontend now consumes that contract directly: it no longer builds tenants/{tenant}/users/{user} prefixes for /fs/* requests. currentUser is retained only as a fail-closed identity gate when requireScopedWorkspace is true; missing identity renders empty listings and short-circuits file reads instead of requesting a raw mount root. - Default requireScopedWorkspace to true at the exported entry points (listWorkspace, readWorkspaceFile) and internal resolvers so an omitted option cannot silently enable raw-root behavior. - Keep memory collapse-walk over the caller's own mount-relative tree so the internal agents/<agent>/projects/_none wrapper directories stay hidden; all requests remain mount-relative. - Gate the three test-support-only imports in composition runtime.rs so default-feature clippy no longer reports them unused. - Update workspace-api tests to mock the server-scoped, mount-relative contract: hosted calls expect mount-relative paths, requireScopedWorkspace is explicit, and assert.rejects records/constrains rejections instead of asserting inside the fetch handler.
3e24420 to
03c5084
Compare
Update — review feedback addressed (03c5084)Thanks @ironloopai and @coderabbitai. All actionable findings fixed; CI root cause identified and fixed. ironloopai blocking finding ( High — raw storage prefix on a caller-scoped API)Valid. The Fix: the frontend now consumes the existing server-scoped contract directly:
No server logic was added — the server is already the authority for caller scope. CodeRabbit findings
CI — Code Style / clippy failure root causeThe Root cause: these imports are used only inside Fix: gated the three imports with
Validation run locally
Branch rebased onto current |
|
Caution Command line has extra arguments The command line may contain only the IronLoop mention and command name. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- repository guidance ---'
fd -HI '^(CLAUDE|AGENTS)\.md$|^rules$' . -t f | head -40
for f in $(fd -HI '^(CLAUDE|AGENTS)\.md$' . -t f | head -10); do
echo "--- $f ---"
sed -n '1,220p' "$f"
done
echo '--- changed files ---'
git diff --name-only HEAD~2..HEAD
echo '--- workspace API outline ---'
ast-grep outline crates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.ts --items all
echo '--- workspace API relevant implementation ---'
rg -n -C 8 'tenant|user|requireScopedWorkspace|currentUser|/fs/|listWorkspace|readWorkspaceFile|memory' \
crates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.ts
echo '--- frontend tests for scoped versus fallback behavior ---'
rg -n -C 5 'requireScopedWorkspace|tenants/|users/|/fs/|assert\.rejects|forbidden' \
crates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.test.ts
echo '--- server e2e scope test ---'
rg -n -C 8 'webui_filesystem_memory_mount_is_scoped_to_authenticated_user|tenants/|MEMORY\.md' \
crates/ironclaw_reborn_composition/tests/webui_v2_e2e.rs
echo '--- test-only Rust imports and usages ---'
rg -n -C 5 'ChannelConnectionNoticePolicy|AdminUserService|ChannelConfigProductService|cfg\(any\(test, feature = "test-support"\)\)' \
crates/ironclaw_reborn_composition/src/runtime.rs
echo '--- patch summary ---'
git diff --check HEAD~2..HEAD
git diff --stat HEAD~2..HEADLength of output: 154067
The frontend now sends only mount-relative paths to This preserves the The composed E2E test confirms that cross-user and raw storage paths receive The three No new actionable finding from this update. 🐇 ✅ ✏️ Learnings added
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! You are interacting with an AI system. |
There was a problem hiding this comment.
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/frontend/src/pages/workspace/lib/workspace-api.ts`:
- Around line 218-237: Update resolveMemoryDirectory to initialize actualPath at
the memory mount root rather than relativePath, then let the existing
splitRelative(relativePath) loop descend through each segment exactly once. Add
a regression test for listWorkspace on a memory subdirectory with currentUser
and requireScopedWorkspace enabled, asserting the expected /fs/list request
paths.
- Around line 96-101: Update userScopedPrefix to return a boolean indicating
whether both tenant_id and user_id are present after normalization, removing the
tenants/.../users/... path construction. Keep scopedUserUnavailable using the
predicate’s truthiness and ensure no caller expects or re-appends a formatted
prefix.
🪄 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: f8d7ccd6-3f8d-4c11-9805-57616671acde
📒 Files selected for processing (14)
crates/ironclaw_reborn_cli/src/commands/serve.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/tests/webui_v2_serve.rscrates/ironclaw_webui/frontend/src/app/app.tsxcrates/ironclaw_webui/frontend/src/app/auth.tscrates/ironclaw_webui/frontend/src/layout/gateway-layout.tsxcrates/ironclaw_webui/frontend/src/pages/workspace/hooks/useWorkspaceBrowser.tscrates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.test.tscrates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.tscrates/ironclaw_webui/frontend/src/pages/workspace/workspace-page.tsxcrates/ironclaw_webui/src/webui_serve.rscrates/ironclaw_webui/src/webui_v2/handlers.rscrates/ironclaw_webui/src/webui_v2/router.rscrates/ironclaw_webui/tests/webui_v2_handlers_contract.rs
Update — workspace browser leak fixed (490f191)Root causeYou could see other users' documents in Home because the standalone FixConfine Workspace reads to the caller's own subtree (
Operator capability alone does not bypass a deployment that has set the scoped-projection flag — consistent with the existing session-feature logic. Operator bypass only applies when the deployment flag is off (local mode). Memory needs no change — it is already caller-scoped server-side (proven by Relationship to #5927Complementary, not overlapping:
Both are needed for end-to-end per-user workspace isolation: 5927 makes agent writes land per-user; 7062 makes the browser only show the caller's subtree. They merge independently. CodeRabbit follow-ups on 03c5084 (also in this commit)
Validation
New server-side contract tests
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/src/webui_v2/handlers.rs`:
- Around line 920-928: Update strip_workspace_prefix so it strips prefix only
when path exactly equals prefix or the prefix is immediately followed by a path
separator; otherwise return the existing trimmed path unchanged. Preserve the
current separator trimming for valid prefixed paths and avoid matching
identifier prefixes such as “alice” in “alice2”.
- Around line 894-918: Update workspace_served_path to validate the trimmed
requested path before prepending the scoped prefix, rejecting any path
containing a parent-directory component (“..”) and preventing
RebornFsStatRequest or RebornFsReadRequest dispatch for unsafe input. Preserve
the existing scoped-path construction and passthrough behavior for valid paths
and other mounts.
In `@crates/ironclaw_webui/tests/webui_v2_handlers_contract.rs`:
- Around line 8007-8041: Add a regression test alongside
stat_fs_path_prefixes_workspace_path_with_scoped_projection for the read_fs_file
route under scoped projection. Use caller user-alpha, enable workspace scoped
projection, request the workspace file through the download endpoint, and assert
the stub receives the caller-subtree-prefixed path while the returned file.path
is stripped to the mount-relative 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: 577053c9-4ccd-40a9-a329-008850208d56
📒 Files selected for processing (4)
crates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.test.tscrates/ironclaw_webui/frontend/src/pages/workspace/lib/workspace-api.tscrates/ironclaw_webui/src/webui_v2/handlers.rscrates/ironclaw_webui/tests/webui_v2_handlers_contract.rs
Scope WebUI Workspace and Memory presentation to the authenticated tenant/user instead of exposing shared storage paths. Fail closed for signed/non-operator sessions and for missing scoped roots, while preserving local operator fallback. Collapse internal memory wrapper directories and hide sidecar files from the user-facing view.
…orts Address review feedback on #7062 (reopened from #5831): - The /fs/{list,stat,content} handlers bind the authenticated caller and return mount-relative, server-scoped paths (covered by the existing webui_filesystem_memory_mount_is_scoped_to_authenticated_user composed test). The frontend now consumes that contract directly: it no longer builds tenants/{tenant}/users/{user} prefixes for /fs/* requests. currentUser is retained only as a fail-closed identity gate when requireScopedWorkspace is true; missing identity renders empty listings and short-circuits file reads instead of requesting a raw mount root. - Default requireScopedWorkspace to true at the exported entry points (listWorkspace, readWorkspaceFile) and internal resolvers so an omitted option cannot silently enable raw-root behavior. - Keep memory collapse-walk over the caller's own mount-relative tree so the internal agents/<agent>/projects/_none wrapper directories stay hidden; all requests remain mount-relative. - Gate the three test-support-only imports in composition runtime.rs so default-feature clippy no longer reports them unused. - Update workspace-api tests to mock the server-scoped, mount-relative contract: hosted calls expect mount-relative paths, requireScopedWorkspace is explicit, and assert.rejects records/constrains rejections instead of asserting inside the fetch handler.
The standalone /fs/* browser mount (scoped_browse_mount_view) maps the
Workspace alias to a shared /projects/workspace target, so a hosted user
could list other users' workspace artifacts through the WebUI. Memory is
already caller-scoped server-side; Workspace was not.
Confine Workspace reads to the caller's own subtree
(tenants/{tenant}/users/{user}) in the WebUI /fs/{list,stat,content}
handlers when the effective scoped-projection flag is true (deployment
flag OR non-operator caller). The flag reuses the same effective value
the session endpoint reports. Operator fallback with scoped projection
off keeps the raw shared workspace root so local/single-user workspaces
stay visible.
The handlers prepend the per-caller prefix before forwarding to the
product layer and strip it from any path the product layer echoes back,
so the browser keeps round-tripping mount-relative paths and never sees
the storage ownership prefix. A cross-user path from a scoped caller
becomes tenants/<caller>/users/<caller>/tenants/.../other and 404s
inside the caller's own subtree.
Complementary to #5927, which scopes agent tool/shell workspace writes
to the same per-user subtree. This PR scopes the browser read surface.
Also addresses CodeRabbit follow-up on 03c5084:
- resolveMemoryDirectory now seeds the walk at the mount root instead of
relativePath, which doubled any non-root memory subdirectory.
- userScopedPrefix is reduced to a boolean hasCallerIdentity predicate so
no future caller can re-prepend a tenant/user path string.
- Regression test for memory subdirectory navigation.
Server-side contract tests cover: scoped workspace list prefixes the
caller subtree; non-operator is scoped even when the state flag is off;
operator fallback with the flag off keeps the raw root; scoped stat
echoes the mount-relative path.
5ebee49 to
e7214a1
Compare
One conflict, in composition/src/runtime.rs: main independently added the same three `#[cfg(any(test, feature = "test-support"))]` gates #7117 added for #7119. Kept our explanatory comment; verified no import was duplicated (each of the three appears exactly once with exactly one gate). ALLOWLIST recounted off the compiler after #7155 touched the file ("ALLOWLIST grew to 123 entries" with the constant set to 0) — still **123**, so the baseline is unchanged and correct. WIP: the changed-lines coverage gate that dequeued this PR is NOT yet addressed.
⚠ WIP: pushed under session-time pressure. Conflicts are resolved and the reintroduced-name scan is clean, but this merge has NOT been compiled or tested locally. Treat CI as the first verification. Resolved 5 conflicts, all content-vs-rename, union taken in each: - extension_host_assembly.rs: main dropped `ScopedFilesystem` and switched the reader to `inbound_filesystem`; kept both plus our renames. - root/profile.rs: main added `workspace_scoped_per_caller`; kept it with our renamed `ironclaw_event_store` return type. - runtime/capability_host.rs: main added `WorkspaceMountPolicy`; kept it with our renamed `ironclaw_assistant` import. - capability_host/workspace_scoping_tests.rs: git flagged this as a file-location conflict — main ADDED it under the pre-rename directory. Taken at the renamed path. - CHECKLIST.md: append-only; main's amendments kept, our renames re-applied. The scan earned its keep again: this conflict-free merge reintroduced 20 old crate names with ZERO conflicts — 6 real `ironclaw_reborn_composition::` code references (serve.rs, profile_acceptance.rs, group_constructors.rs) that would not have compiled, 9 path-keyed rows in `tests/integration/changed-coverage-exemptions.toml` (which would have gone quiet, not loud), and 5 doc references. All repointed; code/config zone is back to 0.
`Check formatting` (step 6 of Fast deterministic checks) went red on cca5884: the merge was pushed under time pressure without running fmt. Only the two files whose crate references I rewrote by hand are affected — `ironclaw_reborn_composition` -> `ironclaw_composition` is 9 characters shorter, so call sites that were wrapped at the old width now fit on one line. No semantic change.
…44392) Merging main @ be33ae1 into this branch brought #7062's +371 production LOC of composition wiring, and the new absolute-mass gate correctly went red against its own merge context (44392 observed vs 44021+150 effective ceiling — the exact failure CI showed). Re-measured on the merged tree with the gate's own counter and re-seeded to current, not padded, per the manifest's ratchet convention. Gate + its 76-case self-test green locally; both new architecture gates (same-layer inventory, vendor census) pass on the merged tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…space mount
/projects/workspace/tenants/{tenant}/users/{user}, but the loop-host
attachment_read_port still read through the shared read-only fixed view
(services.workspace_filesystem), which resolves the workspace root. A
landed image therefore came back NotFound at model-gateway time and was
silently dropped, so vision-capable model payloads lost every inline
image (the duration-4 Playwright attachment failure).
Wire the read port over the same per-caller scoped handle the WebUI
lander uses (runtime_mounts::read_write_workspace_filesystem), mirroring
what #7062 already did for the channel-host assembly. Under the Shared
policy the handle is byte-identical to the old fixed view; under
PerCaller it now resolves the caller's subtree.
…space mount
/projects/workspace/tenants/{tenant}/users/{user}, but the loop-host
attachment_read_port still read through the shared read-only fixed view
(services.workspace_filesystem), which resolves the workspace root. A
landed image therefore came back NotFound at model-gateway time and was
silently dropped, so vision-capable model payloads lost every inline
image (the duration-4 Playwright attachment failure).
Wire the read port over the same per-caller scoped handle the WebUI
lander uses (runtime_mounts::read_write_workspace_filesystem), mirroring
what #7062 already did for the channel-host assembly. Under the Shared
policy the handle is byte-identical to the old fixed view; under
PerCaller it now resolves the caller's subtree.
* fix(composition): read landed attachments through the per-caller workspace mount
/projects/workspace/tenants/{tenant}/users/{user}, but the loop-host
attachment_read_port still read through the shared read-only fixed view
(services.workspace_filesystem), which resolves the workspace root. A
landed image therefore came back NotFound at model-gateway time and was
silently dropped, so vision-capable model payloads lost every inline
image (the duration-4 Playwright attachment failure).
Wire the read port over the same per-caller scoped handle the WebUI
lander uses (runtime_mounts::read_write_workspace_filesystem), mirroring
what nearai#7062 already did for the channel-host assembly. Under the Shared
policy the handle is byte-identical to the old fixed view; under
PerCaller it now resolves the caller's subtree.
* test(playwright): reconcile legacy WebUI v2 suites to the fetch-based SSE transport
event-source-plus (fetch/ReadableStream). The legacy suites still faked
window.EventSource, so the app never opened a stream and every
duration-1/duration-4 legacy test that emitted frames failed with "no
EventSource stream is open".
- Extract the smoke suite's proven fetch fake into
install_fake_v2_event_stream() in reborn_webui_harness, extended to
record request URLs and headers for reconnect assertions.
- Port all seven legacy scenario files onto it, updating cursor/token
assertions to the header contract (Authorization bearer,
Last-Event-ID) instead of the retired token/after_cursor query
params.
- legacy_skills delete: use the shared in-app confirmation dialog
instead of a native browser dialog.
- legacy_dom_resource_limits reconnect-timer: assert the pending
reconnect is cancelled when the tab hides (the fetch transport
schedules retries internally).
- legacy_rendering: assert no live onerror/iframe/img nodes instead of
substring-scanning escaped text.
- extensions_api: restore the nearai#6520 wire contract (retired
authenticated/active/needs_setup/has_auth/onboarding_state booleans
must be absent).
- tool_execution truncated-tool test: expect model_output_truncated
failure per nearai#6845's no-recovery contract instead of an assistant
recovery message.
- streaming_run_control_api: drop the stream=true assertion for the
OpenAI-compatible mock, which rides the buffered fallback since
nearai#7120 (rig-core cannot distinguish a complete stream from a truncated
one).
* fix(composition): read landed attachments through the per-caller workspace mount
/projects/workspace/tenants/{tenant}/users/{user}, but the loop-host
attachment_read_port still read through the shared read-only fixed view
(services.workspace_filesystem), which resolves the workspace root. A
landed image therefore came back NotFound at model-gateway time and was
silently dropped, so vision-capable model payloads lost every inline
image (the duration-4 Playwright attachment failure).
Wire the read port over the same per-caller scoped handle the WebUI
lander uses (runtime_mounts::read_write_workspace_filesystem), mirroring
what nearai#7062 already did for the channel-host assembly. Under the Shared
policy the handle is byte-identical to the old fixed view; under
PerCaller it now resolves the caller's subtree.
* test(playwright): reconcile legacy WebUI v2 suites to the fetch-based SSE transport
event-source-plus (fetch/ReadableStream). The legacy suites still faked
window.EventSource, so the app never opened a stream and every
duration-1/duration-4 legacy test that emitted frames failed with "no
EventSource stream is open".
- Extract the smoke suite's proven fetch fake into
install_fake_v2_event_stream() in reborn_webui_harness, extended to
record request URLs and headers for reconnect assertions.
- Port all seven legacy scenario files onto it, updating cursor/token
assertions to the header contract (Authorization bearer,
Last-Event-ID) instead of the retired token/after_cursor query
params.
- legacy_skills delete: use the shared in-app confirmation dialog
instead of a native browser dialog.
- legacy_dom_resource_limits reconnect-timer: assert the pending
reconnect is cancelled when the tab hides (the fetch transport
schedules retries internally).
- legacy_rendering: assert no live onerror/iframe/img nodes instead of
substring-scanning escaped text.
- extensions_api: restore the nearai#6520 wire contract (retired
authenticated/active/needs_setup/has_auth/onboarding_state booleans
must be absent).
- tool_execution truncated-tool test: expect model_output_truncated
failure per nearai#6845's no-recovery contract instead of an assistant
recovery message.
- streaming_run_control_api: drop the stream=true assertion for the
OpenAI-compatible mock, which rides the buffered fallback since
nearai#7120 (rig-core cannot distinguish a complete stream from a truncated
one).
* fix(composition): read landed attachments through the per-caller workspace mount
/projects/workspace/tenants/{tenant}/users/{user}, but the loop-host
attachment_read_port still read through the shared read-only fixed view
(services.workspace_filesystem), which resolves the workspace root. A
landed image therefore came back NotFound at model-gateway time and was
silently dropped, so vision-capable model payloads lost every inline
image (the duration-4 Playwright attachment failure).
Wire the read port over the same per-caller scoped handle the WebUI
lander uses (runtime_mounts::read_write_workspace_filesystem), mirroring
what nearai#7062 already did for the channel-host assembly. Under the Shared
policy the handle is byte-identical to the old fixed view; under
PerCaller it now resolves the caller's subtree.
* test(playwright): reconcile legacy WebUI v2 suites to the fetch-based SSE transport
event-source-plus (fetch/ReadableStream). The legacy suites still faked
window.EventSource, so the app never opened a stream and every
duration-1/duration-4 legacy test that emitted frames failed with "no
EventSource stream is open".
- Extract the smoke suite's proven fetch fake into
install_fake_v2_event_stream() in reborn_webui_harness, extended to
record request URLs and headers for reconnect assertions.
- Port all seven legacy scenario files onto it, updating cursor/token
assertions to the header contract (Authorization bearer,
Last-Event-ID) instead of the retired token/after_cursor query
params.
- legacy_skills delete: use the shared in-app confirmation dialog
instead of a native browser dialog.
- legacy_dom_resource_limits reconnect-timer: assert the pending
reconnect is cancelled when the tab hides (the fetch transport
schedules retries internally).
- legacy_rendering: assert no live onerror/iframe/img nodes instead of
substring-scanning escaped text.
- extensions_api: restore the nearai#6520 wire contract (retired
authenticated/active/needs_setup/has_auth/onboarding_state booleans
must be absent).
- tool_execution truncated-tool test: expect model_output_truncated
failure per nearai#6845's no-recovery contract instead of an assistant
recovery message.
- streaming_run_control_api: drop the stream=true assertion for the
OpenAI-compatible mock, which rides the buffered fallback since
nearai#7120 (rig-core cannot distinguish a complete stream from a truncated
one).
* fix(webui): scope workspace and memory views Scope WebUI Workspace and Memory presentation to the authenticated tenant/user instead of exposing shared storage paths. Fail closed for signed/non-operator sessions and for missing scoped roots, while preserving local operator fallback. Collapse internal memory wrapper directories and hide sidecar files from the user-facing view. * fix(webui): consume mount-relative /fs/* paths and gate test-only imports Address review feedback on nearai#7062 (reopened from nearai#5831): - The /fs/{list,stat,content} handlers bind the authenticated caller and return mount-relative, server-scoped paths (covered by the existing webui_filesystem_memory_mount_is_scoped_to_authenticated_user composed test). The frontend now consumes that contract directly: it no longer builds tenants/{tenant}/users/{user} prefixes for /fs/* requests. currentUser is retained only as a fail-closed identity gate when requireScopedWorkspace is true; missing identity renders empty listings and short-circuits file reads instead of requesting a raw mount root. - Default requireScopedWorkspace to true at the exported entry points (listWorkspace, readWorkspaceFile) and internal resolvers so an omitted option cannot silently enable raw-root behavior. - Keep memory collapse-walk over the caller's own mount-relative tree so the internal agents/<agent>/projects/_none wrapper directories stay hidden; all requests remain mount-relative. - Gate the three test-support-only imports in composition runtime.rs so default-feature clippy no longer reports them unused. - Update workspace-api tests to mock the server-scoped, mount-relative contract: hosted calls expect mount-relative paths, requireScopedWorkspace is explicit, and assert.rejects records/constrains rejections instead of asserting inside the fetch handler. * fix(webui): scope workspace browser to caller subtree server-side The standalone /fs/* browser mount (scoped_browse_mount_view) maps the Workspace alias to a shared /projects/workspace target, so a hosted user could list other users' workspace artifacts through the WebUI. Memory is already caller-scoped server-side; Workspace was not. Confine Workspace reads to the caller's own subtree (tenants/{tenant}/users/{user}) in the WebUI /fs/{list,stat,content} handlers when the effective scoped-projection flag is true (deployment flag OR non-operator caller). The flag reuses the same effective value the session endpoint reports. Operator fallback with scoped projection off keeps the raw shared workspace root so local/single-user workspaces stay visible. The handlers prepend the per-caller prefix before forwarding to the product layer and strip it from any path the product layer echoes back, so the browser keeps round-tripping mount-relative paths and never sees the storage ownership prefix. A cross-user path from a scoped caller becomes tenants/<caller>/users/<caller>/tenants/.../other and 404s inside the caller's own subtree. Complementary to nearai#5927, which scopes agent tool/shell workspace writes to the same per-user subtree. This PR scopes the browser read surface. Also addresses CodeRabbit follow-up on 03c5084: - resolveMemoryDirectory now seeds the walk at the mount root instead of relativePath, which doubled any non-root memory subdirectory. - userScopedPrefix is reduced to a boolean hasCallerIdentity predicate so no future caller can re-prepend a tenant/user path string. - Regression test for memory subdirectory navigation. Server-side contract tests cover: scoped workspace list prefixes the caller subtree; non-operator is scoped even when the state flag is off; operator fallback with the flag off keeps the raw root; scoped stat echoes the mount-relative path. * style: apply rustfmt to workspace scoping handlers and tests * fix(webui): reject parent traversal in scoped workspace projection Address CodeRabbit follow-up on the workspace-scoping handlers: - workspace_served_path now rejects any path containing a '..' segment before prepending the caller prefix, so a request like '../other-user/secret' can never become 'tenants/{tenant}/users/{user}/../other-user'. The product layer rejects traversal too, but this keeps the escape attempt from being dispatched at all. - Add a read_fs_file scoped-projection regression test mirroring the list/stat coverage: asserts the download command receives the caller-subtree-prefixed path. - Add a parent-traversal rejection test asserting a 400 and that the product layer is never reached. Rebased onto current main (resolved webui_v2_serve.rs conflict from nearai#6780/nearai#7050/nearai#7033). * fix(webui): wire per-caller workspace writes + address review 4847764083 Hosted non-admin users saw an empty workspace because the browser read the per-user subtree (tenants/{tenant}/users/{user}) while agent tool/attachment writes still landed in the shared /projects/workspace root. Wire the agent read-write workspace filesystem to a per-caller resolver so writes land in the caller's own subtree, mirroring memory scoping. The WebUI browser reads the same subtree, so a hosted user sees, reads, and writes only their own artifacts. Composition (runtime_mounts.rs): - Add scoped_workspace_mount_view(scope, permissions) mapping WORKSPACE_ALIAS to /projects/workspace/tenants/{tenant}/users/{user}. - read_write_workspace_filesystem now uses ScopedFilesystem::new with that resolver (per-call, per-caller) instead of the shared fixed view, so each authenticated caller keys its own subtree and the shared root is never exposed for writes. WebUI handlers (review 4847764083): - Extract workspace_scoped_projection_required(state, capabilities) and call it from both get_session and workspace_projection_for so /session and /fs/* never drift on the policy. - workspace_projection_for now fails closed (returns Err) when scoped projection is required but the caller identity cannot key a subtree, instead of returning None (which served the shared root). Validated newtypes make empty identity unreachable in practice; the guard is defense in depth. - strip_workspace_prefix strips only an exact prefix or prefix/ boundary, not sibling prefixes like tenants/.../users/alice2. - webui_serve.rs doc comment: remove presentation-layer-only wording; the flag controls /fs/* workspace path selection, not only UI display. Contract tests (webui_v2_handlers_contract.rs): - Stub FS_LIST_VIEW now echoes entry paths under the served (prefixed) root, so the handler's prefix stripping is observable. - Add browse_fs_dir_strips_prefixed_entry_paths_under_scoped_projection asserting response entries are mount-relative. - Existing scoped/traversal/raw-fallback/stat/read tests preserved. * fix(composition): scope agent workspace writes per caller on hosted profiles PR nearai#7062 scoped the WebUI workspace browser (and, in 0707e57, the WebUI attachment handle) to /projects/workspace/tenants/{tenant}/users/{user} on hosted profiles, but the agent still WROTE to the shared root: a hosted user saw an empty workspace because the browser read one subtree while every agent write landed in another. The read and write sides were deciding independently. Introduce ONE composition-owned decision --- DeploymentConfig::workspace_scoped_per_caller (true for every profile except Standalone/StandaloneUnrestricted) --- carried to the runtime as runtime_mounts::WorkspaceMountPolicy, and route every lane through it: - Grant minting: RefreshingLoopCapabilityPortFactory::create_capability_port now resolves the workspace MountView per run from the same ResourceScope the skill mounts already key off, so every `mounts = "workspace"` capability (write_file, read_file, apply_patch, list_dir, glob, grep, shell, http.save, attach_workspace_file_to_reply, ...) resolves paths inside the caller's own subtree. Previously it used the composition-wide ambient view. - Approval lease terms: PolicyApprovalLeaseTermsProvider mints workspace mounts from the gate's own ResourceScope, so approving one user's write no longer leases the shared root. Scoping failure denies rather than falling back. - Channel-inbound attachment lander: production channel_host_source built a fixed shared view while its test-support twin used the scoped resolver. Both now call one owner, runtime_mounts::read_write_workspace_filesystem, and the project_filesystem reader is built over the same handle so reader and lander address the same subtree. - WebUI attachment handle: read_write_workspace_filesystem scoped UNCONDITIONALLY after 0707e57, which relocated standalone uploads into tenants/local/users/... away from the raw root the standalone agent and browser use. It now follows the same policy bit. Standalone keeps the ambient shared view including the raw host-home aliases local coding profiles depend on; those are never scoped. The CLI's profile_requires_scoped_workspace_projection now delegates to RebornCompositionProfile::workspace_scoped_per_caller, so the browser projection and the write lanes cannot drift. Regression tests (each verified failing before the fix): - workspace_scoping_tests: a hosted-profile turn writing /workspace/note.txt through the production capability port lands under the caller's subtree, two callers writing the same relative path do not collide, and nothing lands in the shared root; the standalone counterpart still lands at the shared root. Fails pre-fix with the file absent from the caller subtree. - runtime::approval: per-caller lease terms key the gate's own subtree and two callers get different targets. Fails pre-fix against the shared view. - runtime_mounts: the shared read-write handle owner resolves different roots per policy branch, pinning the WebUI/channel/C-ATTACH lanes together. - serve.rs: the browser projection flag agrees with the composition write policy for every profile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(composition): close workspace read/write mismatch on multi-user standalone Follow-up to the per-caller workspace scoping commit, addressing two gaps the deeper lane trace surfaced. 1. The scoping decision had TWO derivations, so a host override was silently dropped. `production_build_assembly` re-derived it from the profile (`context.profile.workspace_scoped_per_caller()`) instead of reading the deployment the host assembled. The build context now carries the resolved boolean, so the deployment is the only source. Caught by the new e2e test below, which wrote to the shared root despite the override being set. 2. Profile alone was the wrong gate. The WebUI browser confines EVERY non-operator caller's Workspace reads to `tenants/{tenant}/users/{user}` (`workspace_scoped_projection_required` = state flag OR not-operator), so a standalone-composed deployment with a multi-user authenticator --- exactly what `serve` builds when SSO is on --- still read a per-user subtree while the agent wrote shared. That is the reported bug, alive outside the hosted profiles. `RebornHostBindings::with_workspace_scoped_per_caller` lets the assembling host raise the deployment's decision (raise-only; a hosted profile stays scoped). `serve` now computes the value ONCE as `profile default || SSO on` and feeds it to both `build_reborn_runtime` and `WebuiServeConfig::with_workspace_requires_scoped_projection`, so the browser and the write lanes can no longer disagree in any configuration the binary builds. Lanes verified against the trace findings, no change needed: - Grant view and execution mounts already come from ONE resolved value: `create_capability_port` resolves the per-caller view and passes it to both `visible_capability_request` (grant.constraints.mounts) and `with_execution_mounts` (context.mounts), so the authorization obligation subset check (`next.is_subset_of(&context.mounts)`) compares a view against itself. Proven end to end by the write_file dispatch test, which completes with verdict Success and lands bytes in the caller subtree --- a helper-only test would have missed the obligation wrapper. - Sandbox process lane: `resolve_grant` strips the source virtual root and joins the remainder as subdirectories, creating read-write targets on demand, so a nested `/projects/workspace/tenants/{t}/users/{u}` grant binds the caller's own directory inside the trusted source. Newly pinned by `per_caller_workspace_grant_binds_the_callers_subdirectory`. The default per-`RebornSandboxScopeKey` workdir bind (no `/workspace` in the view) is untouched. - `MountScopedRootFilesystem` for the ScopedVirtual backend is built from the same request mounts, so its containment re-check follows automatically. - WASM has no filesystem write host-call; nothing to change. Regression tests: - `agent_workspace_writes_are_visible_to_their_owner_and_hidden_from_other_users` (webui_v2_e2e) --- two-user WebUI over a standalone-composed runtime with scoping raised: user A's agent writes a workspace file, A lists it through `/fs/list`, and user B neither lists nor reads it. Fails before the fix (user A's listing 404s because the write landed in the shared root). - `per_caller_workspace_grant_binds_the_callers_subdirectory` (host runtime sandbox mounts). - Standalone integration suites still assert the FLAT workspace root and pass unchanged (group_journeys, group_multiuser, group_approvals, group_memory, integration_attach, integration_tool_call, webui_v2_product_api). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(composition): gate test-only workspace view accessor and single-source the mount policy The `Fast deterministic checks` gate failed on `crates/ironclaw_reborn_composition/src/factory/test_support.rs`: `workspace_mounts_for_test` carried an `.expect()` without an item-level `#[cfg(any(test, feature = "test-support"))]`, so `check_no_panics.py` classified it as production code. The parent module gate is not what the scanner reads. Gate the accessor like every sibling in the file, and drop the duplicated `#[cfg]` attribute that landed on `workspace_mount_policy_for_test`. `Code Style` was only a rollup of this same failure. Review follow-ups on the same scoping change: - `WorkspaceMountPolicy::resolve` is now the single constructor for the per-caller/shared decision. `host_access_assembly` and `production_backend_assembly` each re-implemented the branch, so a deployment could have drifted into a scoped view on one write lane and an ambient shared view on another. - Annotate the `.ok()?` in `read_write_workspace_filesystem`'s `Shared` branch per `.claude/rules/error-handling.md`; the mount view is built from compile-time constants with no caller input. - Use `tokio::fs::canonicalize` in the sandbox-mount test so the async test no longer blocks a Tokio worker. Behavior is unchanged; the existing scoping tests (`workspace_scoping_tests`, `webui_v2_e2e`) cover both policy branches through the production seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(composition): give a fresh caller an empty workspace instead of a hard error Production report (hosted volume container, profile local-dev + SSO, so workspace scoping is active): `builtin.list_dir` and `builtin.glob` on /workspace failed `operation_failed` for the caller `reborn-cli`. Grants were correct --- /workspace mapped to /projects/workspace/tenants/reborn-cli/users/reborn-cli --- but that directory had never been created, so the agent was unusable for every new user. Root cause: a shared deployment's /projects/workspace is created once at composition (`build_host_access`). Per-caller scoping introduced a root with no equivalent step. The local backend returns `NotFound`, and the coding tools map `NotFound` to `operation_error`, so the very first read of an untouched workspace was a hard failure rather than an empty listing. Fix: `runtime_mounts::ensure_caller_workspace_root` creates the caller's own workspace root, called once per run from `RefreshingLoopCapabilityPortFactory::create_capability_port` --- the single seam that already resolves the caller's scope for grant minting, so the path comes from their own mount view and never from tool input. Creating was preferred over softening `NotFound`: - it fixes reads and writes together, where mapping NotFound to empty would have had to be repeated in list_dir, glob, grep and the sensitive-path pre-check, each an opportunity to drift; - error mapping is left untouched everywhere else, so `NotFound` is not weakened for any other path (the scoping brief's memory-safety constraint); - it mirrors the sandbox lane, which already `create_dir_all`s its per-scope workspace in `prepare_workspace`. It is a no-op for shared deployments, whose root composition already created, and failure is logged and swallowed --- a run must not die because a directory the first write would create anyway could not be pre-created. Also fixes the `reborn_struct_test_support_ratchet` failure that c84c9c5 introduced: gating `workspace_mounts_for_test` for `check_no_panics.py` made it a `#[cfg]`-gated member on a production struct, which is what that ratchet counts. Converting it to a gated free function satisfies both gates. Verified the ratchet fails at c84c9c5 with this change stashed. Regression test `fresh_caller_reads_an_empty_workspace_then_writes_into_it`: a caller whose subtree was never created lists an empty workspace, globs and greps cleanly, then writes --- including into a nested path whose parents do not exist --- and reads the file back from their own subtree. Fails before the fix with exactly the production error (`RecoverableFailure { error_kind: OperationFailed }`) on the first list_dir. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: read a never-written workspace mount root as empty at every lane's own seam Relocates the fresh-caller fix (544cebb) from a per-run eager create_dir_all in create_capability_port to the read paths themselves, per review: the eager create covered only the agent lane (a fresh user opening the WebUI Workspace tab before any run still 404'd), was best-effort with a swallowed failure, and paid a per-run cost to fix a read problem. The rule, applied once per lane at the seam that knows the mount root: an authorized caller's mount ROOT exists by definition --- it is the namespace their grant names, not a path they chose --- so a backend that has nothing under it yet reads as an EMPTY directory. Anything deeper keeps reporting NotFound. - Coding tools (the agent lane, which resolves grant mounts against the raw HostWorkspace filesystem): `ResolvedPath::is_mount_root` + `list_dir_empty_if_missing_root`; list_dir's sensitive-stat guard, glob's walk, and grep's root stat tolerate exactly the missing root. The dead `deny_sensitive_existing_path` helper is deleted. - `ScopedFilesystem` (WebUI browse/attachment handles, channel lander): list/stat resolve root-aware and read a missing mount root as empty. - `MountScopedRootFilesystem` (ScopedVirtual backend): same rule. - WebUI browse handler: a scoped caller's own projection ROOT (their `tenants/{t}/users/{u}` subtree under the shared workspace mount) lists as empty on NotFound --- it is authorization-derived, not user input. Deeper paths keep their 404. Regression coverage: `fresh_caller_reads_an_empty_workspace_then_writes_into_it` (agent lane, drives the production capability port; fails before this change), and the two-user webui_v2_e2e test's fresh-user assertion is tightened from `OK || NOT_FOUND` to `OK` + zero entries --- the NOT_FOUND arm it used to tolerate was this bug. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cli): scope workspace writes whenever serve can mint a non-operator caller Review finding on 56dc411: gating the write-side raise on `sso_enabled` was provably too narrow. `serve` installs the admin-API token minter on every start, that minter stamps `operator = false` by design, and the no-SSO branch wires a `SessionAuthenticator` so those bearers validate. The WebUI browser confines every non-operator caller's Workspace reads to their own subtree, so on Standalone + SSO off, a `POST /admin/users` user got scoped reads over a shared write root: the exact empty-workspace mismatch this branch exists to close, still open through a different door. `serve` now raises `with_workspace_scoped_per_caller_services(true)` unconditionally, and the browser flag is READ BACK from the deployment the runtime actually received (`runtime_input.config()`) instead of being recomputed locally --- the raise is a request, composition owns the answer, and the two sides can no longer drift (this also closes the silent-no-op gap where a raise on a `services: None` input vanished while the local recomputation proceeded). The now-unused `profile_requires_scoped_workspace_projection` / `workspace_scoped_per_caller` helpers are deleted and `composition_profile` returns to private. Regression tests: `serve_scopes_workspace_writes_even_on_standalone_without_sso` pins the raise + read-back pair on the Standalone profile; `workspace_scoping_default_follows_deployment_profile` keeps pinning the per-profile defaults that the raise starts from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: address review comments on the fresh-workspace read path - A slash-only ?path= names the same projected workspace root as an empty path; the fresh-caller empty-listing fallback now keys on the trimmed path, and the two-user e2e pins path=/ returning OK + zero entries. - Mirror the parent-traversal rejection test across stat_fs_path and read_fs_file: all three /fs routes share workspace_served_path's `..` guard, so all three are pinned to 400 without reaching the product layer. - The workspace-scoping test helper now fails loudly on result-store errors instead of collapsing them into a missing output, and the fresh-caller test asserts glob and grep return empty file sets rather than bare success. Review comments 3711525396, 3707056909, 3711525388 on PR nearai#7062. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: integration-tier coverage for per-caller workspace scoping + gate residue exemptions The merge-queue run for this branch (actions/runs/30905070584) failed the changed-line coverage gate at 74.77%: the per-caller scoping and never-written-root behavior was pinned only by #[cfg(test)] unit modules, which the integration-tier lcov excludes. Close the gap with real coverage at the tiers the gate measures, and exempt the residue the lcov structurally cannot observe. New coverage: - tests/integration/group_multiuser/scenario_scoped_workspace_isolation.rs over the new RebornIntegrationGroup::multiuser_scoped_workspace(): a runtime built with the same scoping raise `serve` applies, capability grants confined to the caller's own tenants/{tenant}/users/{user} subtree via the production resolver (new test-support export scoped_workspace_mount_view_for_test). Drives through real turns: a fresh caller's list_dir/glob/grep read an EMPTY workspace instead of erroring; a GATED write is approved (per-caller lease) and lands on disk in the caller's own subtree with parents created, never at the shared root; the written subtree lists/greps back; a missing SUB-path stays a hard error. - HostRuntimeHarnessOptions::with_workspace_scoped_per_caller() — the harness seam applying RebornRuntimeInput::with_workspace_scoped_per_caller_services, so the integration tier can compose a scoped deployment at all. - filesystem_contract: never_written_mount_root_reads_as_empty_but_subpaths_stay_not_found pins ScopedFilesystem's list/list_bounded/stat mount-root rule at its owning seam (a tests/ binary, visible to the gate's lcov). - profile_acceptance: workspace_scoping_default_per_profile pins the per-profile default the serve raise starts from. Exemptions (tests/integration/changed-coverage-exemptions.toml, tracked in issue nearai#7142): the create_capability_port async-fn body (llvm-cov attributes it to the signature line — 114 hits — and emits no body DA records, the nearai#6963 class), the serve command body + webui_serve builder (unit-tier-only), the ScopedVirtual-only MountScopedRootFilesystem arm, defensive error arms, and a handful of match-arm construction counters whose driving tests assert the behavior. Verified against the queue run's own merged lcov unioned with a local llvm-cov run of the new binaries: changed-line coverage 98.74% (gate exit 0) without base-lcov subtraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: address review comments on the scoped-workspace coverage - filesystem_contract: pin the WRITE half of the never-written-root rule at the owning tier — the first write materializes the root and parents, the root then lists the entry, and the bytes read back (review 3712779771). - group_constructors: state the SINGLE-CALLER constraint on multiuser_scoped_workspace loudly — the grant view is resolved once for the canonical subject while dispatch resolves owners per run, so a second .with_actor_id thread would hold the wrong subtree grant; cross-actor isolation is pinned where grants are per-caller by construction (review 3712779794). - harness options: cite the symbol the harness actually calls (RebornRuntimeInput::with_workspace_scoped_per_caller_services), not the host-bindings raise (review 3712779803). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(coding): reject document paths in apply_patch --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(composition): read landed attachments through the per-caller workspace mount
/projects/workspace/tenants/{tenant}/users/{user}, but the loop-host
attachment_read_port still read through the shared read-only fixed view
(services.workspace_filesystem), which resolves the workspace root. A
landed image therefore came back NotFound at model-gateway time and was
silently dropped, so vision-capable model payloads lost every inline
image (the duration-4 Playwright attachment failure).
Wire the read port over the same per-caller scoped handle the WebUI
lander uses (runtime_mounts::read_write_workspace_filesystem), mirroring
what nearai#7062 already did for the channel-host assembly. Under the Shared
policy the handle is byte-identical to the old fixed view; under
PerCaller it now resolves the caller's subtree.
* test(playwright): reconcile legacy WebUI v2 suites to the fetch-based SSE transport
event-source-plus (fetch/ReadableStream). The legacy suites still faked
window.EventSource, so the app never opened a stream and every
duration-1/duration-4 legacy test that emitted frames failed with "no
EventSource stream is open".
- Extract the smoke suite's proven fetch fake into
install_fake_v2_event_stream() in reborn_webui_harness, extended to
record request URLs and headers for reconnect assertions.
- Port all seven legacy scenario files onto it, updating cursor/token
assertions to the header contract (Authorization bearer,
Last-Event-ID) instead of the retired token/after_cursor query
params.
- legacy_skills delete: use the shared in-app confirmation dialog
instead of a native browser dialog.
- legacy_dom_resource_limits reconnect-timer: assert the pending
reconnect is cancelled when the tab hides (the fetch transport
schedules retries internally).
- legacy_rendering: assert no live onerror/iframe/img nodes instead of
substring-scanning escaped text.
- extensions_api: restore the nearai#6520 wire contract (retired
authenticated/active/needs_setup/has_auth/onboarding_state booleans
must be absent).
- tool_execution truncated-tool test: expect model_output_truncated
failure per nearai#6845's no-recovery contract instead of an assistant
recovery message.
- streaming_run_control_api: drop the stream=true assertion for the
OpenAI-compatible mock, which rides the buffered fallback since
nearai#7120 (rig-core cannot distinguish a complete stream from a truncated
one).
Summary
Reopening note
Reopens #5831 on top of current
main. The prior branch had accumulated 5 merge commits keeping it in sync withmain'sironclaw_webui_v2→ironclaw_webuicrate rename. This PR replays the same net 13-file change as a single clean commit on top of the latestmain, with no merge commits.Change Type
Linked Issue
Supersedes #5831 (closed).
Validation
cargo fmt --all -- --checkcargo clippy -p ironclaw_webui --all-features --all-targets -- -D warnings— to be run by CI.cargo clippy -p ironclaw --all-features --all-targets -- -D warnings— to be run by CI.cargo build— Not applicable: the owning crate tests and all-target clippy compiled the affected targets.cargo test --features integration— Not applicable: no database-backed behavior changed.Test Strategy
User behavior: A signed hosted user sees only their scoped Workspace/Memory projection; missing scoped roots render empty, while local operator sessions retain raw workspace fallback.
Risk areas:
Tests added or updated:
What the tests prove: Tenant/user storage prefixes are hidden from presented paths; hosted and non-operator sessions cannot fall back to shared roots; local operators keep existing fallback; source-thread workspace routes remain isolated and functional after conflict resolution.
Commands run locally on the updated branch:
cargo fmt --all -- --check(clean)cargo test -p ironclaw_extension_support apply_patch_rejects_probe_clean_document_path_after_full_read -- --nocapture(1 passed)cargo test -p ironclaw_host_runtime --test first_party_coding_tools(41 passed)cargo clippy -p ironclaw_extension_support --all-features --all-targets -- -D warnings(clean)Security Impact
Changes file-view access presentation. Hosted/non-operator sessions now fail closed when caller identity or scoped roots are unavailable. No authentication bypass, secret handling, tool execution, or network policy is weakened.
Reborn Trust-Boundary Checklist
serde(default)fields fail closed: the new session feature defaults fail closed in the browser; no durable serde field added.Database Impact
None.
Blast Radius
WebUI session bootstrap, workspace/memory API projection, frontend workspace navigation, and CLI profile-to-WebUI configuration. Source-thread file browsing and artifact-export gating were explicitly preserved during the merge.
Rollback Plan
Revert this single commit. No schema or persisted-data rollback is required.
Review Follow-Through
CI must confirm changed-line coverage after the added CLI profile-matrix test. No known follow-up is required in this PR; tool/host execution scoping remains outside this UI-only change.
Review track: C (security boundary and hosted multi-user file visibility)