Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions .qwen/e2e-tests/2026-08-04-secondary-artifact-workspace-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Secondary artifact workspace ownership

Issue: https://github.com/QwenLM/qwen-code/issues/8494

## Baseline

- Global CLI: `qwen 0.21.3`
- `qwen serve --help` confirms repeated `--workspace` registration and Web
Shell serving are available.
- Current component behavior is captured failure-first: an artifact tab with
no owner uses root workspace actions, and scheduled-task detail omits the
secondary `workspaceId`.

## Local setup

1. Create isolated primary and secondary temporary workspaces.
2. Put the same relative sentinel filename in both workspaces with different
contents.
3. Create distinct durable scheduled-task fixtures for both runtimes.
4. Start the built `dist/cli.js serve` on loopback with both `--workspace`
flags and the production Web Shell bundle.

## Scenarios

### Secondary file action

Open a secondary-session artifact and download/preview its sentinel.

Expected:

- The request targets `/workspaces/<secondary>/file...`.
- The returned bytes are the secondary sentinel, never the primary sentinel.

### Secondary scheduled task

Open the secondary durable task, toggle it, edit it, and delete the test copy.

Expected:

- Every request targets `/workspaces/<secondary>/scheduled-tasks...`.
- The primary task and primary task file are unchanged.

### Ownership loss

Keep an artifact tab open, then remove or mark the secondary workspace
unavailable in the capability fixture before a delayed read resolves.

Expected:

- The panel displays the localized workspace-unavailable state.
- The delayed response is ignored.
- No request is retried against the primary route.

## Evidence

- Focused test output for resolver, turn outputs, artifact panel, nested
subagent, and app tab propagation.
- Production build/typecheck/lint output.
- Captured two-workspace HTTP route log and sentinel hashes.
- Browser screenshot of the secondary artifact panel and the fail-closed
ownership-loss state when the local browser harness can represent it.

## Results (2026-08-04)

Result: PASS

### Automated verification

- Failure-first run: 3 expected failures and 29 existing passes. The primary
sentinel leaked into an ownerless tab, the secondary qualified client was
unused, and scheduled-task list omitted `workspaceId`.
- Focused ownership suite: 413/413 tests passed across the six affected test
files.
- Full Web Shell suite: 167 files and 2,777 tests passed.
- Web Shell lint and TypeScript typecheck passed.
- Package Web Shell build and repository root production build passed.
- The changed files pass Prettier. The package-wide format check still reports
five unchanged baseline files:
`BranchPickerPopover.module.css`, `GitModePopover.module.css`,
`GitDialog.module.css`, `PlanExecutionView.module.css`, and `index.html`.

### Production two-workspace verification

The built CLI served the copied production bundle with two trusted workspaces.
`GET /capabilities` advertised distinct primary and secondary runtime IDs, and
the served JavaScript contained the new stale-owner guard.

The same relative file was read through both routes:

- `GET /file?path=artifact-owner.txt` returned
`PRIMARY_WORKSPACE_SENTINEL_8494`, SHA-256
`818d5f4f1fb9c7e3f9bdfd9a3ad39361c93a23ca3f3d0ba5e4a7c889b33b9127`.
- `GET /workspaces/<secondary-id>/file?path=artifact-owner.txt` returned
`SECONDARY_WORKSPACE_SENTINEL_8494`, SHA-256
`0e9fac7909b16ff8b17014ea103ed5018c8b4e9ad21d2f95e128fef3a3544a12`.
- The secondary bytes route returned only the secondary sentinel bytes.

Durable task CRUD was exercised against isolated primary and secondary task
fixtures:

- Secondary list, update, and delete requests all used
`/workspaces/<secondary-id>/scheduled-tasks...`.
- The secondary update changed only the secondary name/enabled state.
- The primary task remained present, enabled, and unchanged after that update.
- Deleting the secondary task emptied only the secondary list; the primary
task was still present. Both test tasks were then removed.

The daemon request log independently recorded the qualified file, bytes,
scheduled-task POST/GET/PATCH/DELETE routes and their successful statuses.

### Visual evidence

The in-app browser runtime reported no available browser instance, so a UI
screenshot could not be captured in this environment. No synthetic screenshot
was substituted. The production server returned the current Web Shell bundle
with HTTP 200, and the DOM behavior is covered by the focused and full suites
above.
101 changes: 101 additions & 0 deletions docs/design/web-shell-artifact-workspace-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Web Shell artifact workspace ownership

Status: implemented and verified

Issue: https://github.com/QwenLM/qwen-code/issues/8494

## Problem

The Web Shell has one app-level `DaemonWorkspaceProvider`. Its default
workspace actions target the primary workspace. Session providers can attach to
secondary workspaces, but artifact surfaces currently retain or fall back to
the app-level actions. A file preview, download, review, nested subagent
artifact, or scheduled-task mutation can therefore reach the primary runtime
even though the producing session belongs to a secondary workspace.

The unsafe fallback also survives ownership changes: action objects are stored
in right-panel tabs, so removing or distrusting a workspace does not invalidate
an already-open tab.

## Ownership contract

The producing session owns every turn output. Its `workspaceCwd`, resolved by
the session connection, is the source of the owner claim. The current daemon
capabilities are the authority that accepts or rejects that claim.

A target is usable only when one of these cases holds:

1. Exactly one advertised workspace has the same cwd and is trusted.
2. For a legacy single-workspace daemon with no workspace list, the cwd exactly
matches `capabilities.workspaceCwd`.

Unknown, duplicate, untrusted, removed, or identity-mismatched targets fail
closed. They never fall back to the primary workspace.

## Design

### Resolve at use time

Turn-output requests and right-panel tabs carry immutable owner identity
(`workspaceCwd` and the advertised `workspaceId`) instead of long-lived action
objects. `ArtifactPanel` resolves that identity against current capabilities on
every render. This makes workspace removal, trust loss, and runtime replacement
invalidate open tabs immediately.

The resolver returns a small artifact action surface only:

- `readWorkspaceFile`
- `readFileBytes`
- `stat`
- scheduled-task list/update/delete operations

Primary targets reuse the provider's primary actions. Trusted secondary
targets use `client.workspaceByCwd(cwd)` for file operations. Scheduled-task
operations still use the Web UI REST actions, but always receive the resolved
workspace id explicitly.

### Propagation

- `TurnOutputs` derives the owner from its session `workspaceCwd`, uses the
scoped file actions for direct downloads, and stamps owner identity onto
review, artifact, and scheduled-task open requests.
- `ChatPane` and `SubagentDetail` preserve the request identity and add only the
producing session id. They no longer stamp root workspace actions onto the
request or artifact snapshot.
- `App` stores owner identity on each right-panel tab. Pane artifact snapshots
contain artifacts only; they no longer retain workspace clients.
- `ArtifactPanel` renders the existing workspace-unavailable state when the
owner cannot be resolved or no longer matches. It never substitutes
`useWorkspaceActions()` for a missing owner.
- Durable scheduled-task snapshots carry `workspaceId`, and every list,
update, toggle, and delete request passes that id.

### Async invalidation

Scoped action wrappers check that their captured owner is still current before
starting an operation and again after each response. Effects and scheduled-task
mutations also ignore results after unmount or owner replacement. A response
started for one runtime cannot populate a tab after that runtime is removed or
replaced.

## Non-goals

- No daemon route or workspace-registry semantics change.
- No primary fallback for compatibility on multi-workspace daemons.
- No redesign of artifact storage, session attachment, or scheduled-task data.
- No attempt to make arbitrary app-level workspace actions session-scoped;
only the artifact surfaces in issue #8494 are changed.

## Verification

1. Failure-first component tests pin the current primary fallback and missing
scheduled-task workspace id.
2. Resolver tests cover primary, trusted secondary, unknown, duplicate,
untrusted, removed, and runtime-replaced targets.
3. Turn-output tests verify secondary downloads and open requests use the
secondary owner.
4. Artifact-panel tests verify missing/stale ownership fails closed and all
durable scheduled-task operations include `workspaceId`.
5. A production bundle is run against two registered local workspaces with
distinct sentinel files/tasks. Captured requests must use the secondary
workspace-qualified routes and leave the primary sentinels unchanged.
Loading
Loading