Repository navigation
revert(upstream-packages): keep the webui branch's local changes inside packages/webui - #240
Merged
Merged
Conversation
…de packages/webui The webui branch is a downstream client of MiniMax-AI/minimax-code. Changes to the upstream packages do not flow back upstream and turn into unreviewed divergence at the next upstream sync, so they do not belong here. This removes the upstream-package half of the work merged today: - packages/local-runtime/src/turns/file-changes.ts (#232, #234) - packages/agent-tools/** (#238) - packages/local-runtime-v2/src/service/workspace/operations/workspace-path.ts (#238) - packages/local-runtime-v2/src/application/conversation/conversation-application.ts and .../stream/session-stream-service.ts (#237) Everything under packages/webui stays: the queue-drain client half, the regenerated visual baselines, the browser test budgets and the CJK font gate. The inventories follow the files: eight removed paths leave release/public-source.json and six removed suites leave test/vitest-suites.json.
This was referenced Oct 23, 2026
modacker
pushed a commit
that referenced
this pull request
Oct 23, 2026
What was broken: the upstream runtime's workspace write path (`resolveWorkspaceWritePath` in `packages/local-runtime-v2/src/service/workspace/operations/workspace-path.ts`) falls back to the lexical spelling when `realpath` fails. A dangling symlink fails `realpath` exactly as a file that is not there yet does, but a write through a dangling link creates the file it points at — so the write landed outside the workspace while the check compared a path inside it. The upstream fix for this was merged and then reverted (#240), because it touched packages this branch does not own. Why this fix: the same fence is reimplemented on the WebUI side, where the binding that forwards a workspace write is ours. `packages/webui/src/runtime/workspace-real-path.ts` resolves a path to the location the filesystem really uses for it — following symlinks, and following dangling ones by hand — and compares it against the real workspace root. A path that cannot be resolved at all (a symlink cycle, a permission error, a missing root) is refused rather than guessed at, and a symlink that stays inside the workspace keeps working. The fence is wired into the two WebUI bindings that write into a workspace: `applyCanvas`, whose operation names workspace files, and `extractWorkspaceArchive`, whose destination is created before anything is written into it. It validates only: the path reaches the runtime exactly as it was spelled, and the refusal is reported in the WebUI's own voice. How it was verified: with real temp directories and real symlinks. A dangling symlink pointing outside the workspace is refused and the file it points at is never created; a symlink pointing inside the workspace keeps working; a workspace root that is itself a symlink keeps working; an ordinary path is accepted; a symlink cycle is refused instead of followed. The harness half of the suite proves the runtime is never called for a path that leaves the workspace — both refusals fail without the fence. Known boundaries: the upstream `realpathOrFallback` is still live, so a write that does not pass through a WebUI binding is still exposed there. The fence cannot close the gap between its `realpath` and the write itself, so a symlink swapped in between the two is still a race. A shell run in the workspace is out of scope.
modacker
pushed a commit
that referenced
this pull request
Oct 23, 2026
What was broken: the upstream runtime's workspace write path (`resolveWorkspaceWritePath` in `packages/local-runtime-v2/src/service/workspace/operations/workspace-path.ts`) falls back to the lexical spelling when `realpath` fails. A dangling symlink fails `realpath` exactly as a file that is not there yet does, but a write through a dangling link creates the file it points at — so the write landed outside the workspace while the check compared a path inside it. The upstream fix for this was merged and then reverted (#240), because it touched packages this branch does not own. Why this fix: the same fence is reimplemented on the WebUI side, where the binding that forwards a workspace write is ours. `packages/webui/src/runtime/workspace-real-path.ts` resolves a path to the location the filesystem really uses for it — following symlinks, and following dangling ones by hand — and compares it against the real workspace root. A path that cannot be resolved at all (a symlink cycle, a permission error, a missing root) is refused rather than guessed at, and a symlink that stays inside the workspace keeps working. The fence is wired into the two WebUI bindings that write into a workspace: `applyCanvas`, whose operation names workspace files, and `extractWorkspaceArchive`, whose destination is created before anything is written into it. It validates only: the path reaches the runtime exactly as it was spelled, and the refusal is reported in the WebUI's own voice. How it was verified: with real temp directories and real symlinks. A dangling symlink pointing outside the workspace is refused and the file it points at is never created; a symlink pointing inside the workspace keeps working; a workspace root that is itself a symlink keeps working; an ordinary path is accepted; a symlink cycle is refused instead of followed. The harness half of the suite proves the runtime is never called for a path that leaves the workspace — both refusals fail without the fence. Known boundaries: the upstream `realpathOrFallback` is still live, so a write that does not pass through a WebUI binding is still exposed there. The fence cannot close the gap between its `realpath` and the write itself, so a symlink swapped in between the two is still a race. A shell run in the workspace is out of scope.
modacker
pushed a commit
that referenced
this pull request
Oct 23, 2026
A turn's diff can name a file outside the workspace. The runtime's capture layer records an out-of-workspace write as an absolute path, so the entry reaches the client with that file's content attached — and the client rendered it: the transcript's diff card, the workspace turn-review panel and the workspace review page each put the patch text on screen. The fix that stopped the collection at the source was made upstream in `packages/local-runtime/src/turns/file-changes.ts` and reverted in PR #240, because it changed a package this fork may not change. The boundary is therefore drawn on the client side, which is the last point before the content reaches a screen. A diff's file entries are now projected through that boundary before they reach a surface. An entry whose path resolves outside the workspace keeps its name, its status and its line counts, and loses its body: the diff text, the patch hunks, the file's content. The surface says what is withheld instead of showing it. The entry itself stays in the list — "which file was touched" is the part a person can still act on. The comparison is lexical, on posix-style strings, the same normalization the message file-reference reader uses. An absolute path is compared with the root segment by segment, so a sibling directory that shares the root's name is outside; a relative path is refused when it climbs out with `..`. Two limits are stated rather than papered over, and ADR 0024 and the tests say so out loud: * A symlink inside the workspace that points outside it stays contained here. The entry's path is a relative one and the browser has no filesystem to resolve it against; telling the two apart needs `realpath` on the runtime side. * An unknown workspace root is not a boundary. The entries are shown as they are, because a check that hid everything it could not place would hide ordinary content too. The upstream half of this is still open: nothing here stops the runtime from sending an out-of-workspace file's patch to the browser, only from the browser showing it. The client stops the content from being displayed; only the upstream change stops it from being collected.
modacker
pushed a commit
that referenced
this pull request
Oct 23, 2026
What was broken: the upstream runtime's workspace write path (`resolveWorkspaceWritePath` in `packages/local-runtime-v2/src/service/workspace/operations/workspace-path.ts`) falls back to the lexical spelling when `realpath` fails. A dangling symlink fails `realpath` exactly as a file that is not there yet does, but a write through a dangling link creates the file it points at — so the write landed outside the workspace while the check compared a path inside it. The upstream fix for this was merged and then reverted (#240), because it touched packages this branch does not own. Why this fix: the same fence is reimplemented on the WebUI side, where the binding that forwards a workspace write is ours. `packages/webui/src/runtime/workspace-real-path.ts` resolves a path to the location the filesystem really uses for it — following symlinks, and following dangling ones by hand — and compares it against the real workspace root. A path that cannot be resolved at all (a symlink cycle, a permission error, a missing root) is refused rather than guessed at, and a symlink that stays inside the workspace keeps working. The fence is wired into the two WebUI bindings that write into a workspace: `applyCanvas`, whose operation names workspace files, and `extractWorkspaceArchive`, whose destination is created before anything is written into it. It validates only: the path reaches the runtime exactly as it was spelled, and the refusal is reported in the WebUI's own voice. How it was verified: with real temp directories and real symlinks. A dangling symlink pointing outside the workspace is refused and the file it points at is never created; a symlink pointing inside the workspace keeps working; a workspace root that is itself a symlink keeps working; an ordinary path is accepted; a symlink cycle is refused instead of followed. The harness half of the suite proves the runtime is never called for a path that leaves the workspace — both refusals fail without the fence. Known boundaries: the upstream `realpathOrFallback` is still live, so a write that does not pass through a WebUI binding is still exposed there. The fence cannot close the gap between its `realpath` and the write itself, so a symlink swapped in between the two is still a race. A shell run in the workspace is out of scope.
modacker
added a commit
that referenced
this pull request
Oct 23, 2026
What was broken: the upstream runtime's workspace write path (`resolveWorkspaceWritePath` in `packages/local-runtime-v2/src/service/workspace/operations/workspace-path.ts`) falls back to the lexical spelling when `realpath` fails. A dangling symlink fails `realpath` exactly as a file that is not there yet does, but a write through a dangling link creates the file it points at — so the write landed outside the workspace while the check compared a path inside it. The upstream fix for this was merged and then reverted (#240), because it touched packages this branch does not own. Why this fix: the same fence is reimplemented on the WebUI side, where the binding that forwards a workspace write is ours. `packages/webui/src/runtime/workspace-real-path.ts` resolves a path to the location the filesystem really uses for it — following symlinks, and following dangling ones by hand — and compares it against the real workspace root. A path that cannot be resolved at all (a symlink cycle, a permission error, a missing root) is refused rather than guessed at, and a symlink that stays inside the workspace keeps working. The fence is wired into the two WebUI bindings that write into a workspace: `applyCanvas`, whose operation names workspace files, and `extractWorkspaceArchive`, whose destination is created before anything is written into it. It validates only: the path reaches the runtime exactly as it was spelled, and the refusal is reported in the WebUI's own voice. How it was verified: with real temp directories and real symlinks. A dangling symlink pointing outside the workspace is refused and the file it points at is never created; a symlink pointing inside the workspace keeps working; a workspace root that is itself a symlink keeps working; an ordinary path is accepted; a symlink cycle is refused instead of followed. The harness half of the suite proves the runtime is never called for a path that leaves the workspace — both refusals fail without the fence. Known boundaries: the upstream `realpathOrFallback` is still live, so a write that does not pass through a WebUI binding is still exposed there. The fence cannot close the gap between its `realpath` and the write itself, so a symlink swapped in between the two is still a race. A shell run in the workspace is out of scope. Co-authored-by: probe <probe@local>
modacker
pushed a commit
that referenced
this pull request
Oct 23, 2026
A turn's diff can name a file outside the workspace. The runtime's capture layer records an out-of-workspace write as an absolute path, so the entry reaches the client with that file's content attached — and the client rendered it: the transcript's diff card, the workspace turn-review panel and the workspace review page each put the patch text on screen. The fix that stopped the collection at the source was made upstream in `packages/local-runtime/src/turns/file-changes.ts` and reverted in PR #240, because it changed a package this fork may not change. The boundary is therefore drawn on the client side, which is the last point before the content reaches a screen. A diff's file entries are now projected through that boundary before they reach a surface. An entry whose path resolves outside the workspace keeps its name, its status and its line counts, and loses its body: the diff text, the patch hunks, the file's content. The surface says what is withheld instead of showing it. The entry itself stays in the list — "which file was touched" is the part a person can still act on. The comparison is lexical, on posix-style strings, the same normalization the message file-reference reader uses. An absolute path is compared with the root segment by segment, so a sibling directory that shares the root's name is outside; a relative path is refused when it climbs out with `..`. Two limits are stated rather than papered over, and ADR 0024 and the tests say so out loud: * A symlink inside the workspace that points outside it stays contained here. The entry's path is a relative one and the browser has no filesystem to resolve it against; telling the two apart needs `realpath` on the runtime side. * An unknown workspace root is not a boundary. The entries are shown as they are, because a check that hid everything it could not place would hide ordinary content too. The upstream half of this is still open: nothing here stops the runtime from sending an out-of-workspace file's patch to the browser, only from the browser showing it. The client stops the content from being displayed; only the upstream change stops it from being collected.
modacker
added a commit
that referenced
this pull request
Oct 23, 2026
…244) * fix(webui): withhold the body of a diff entry outside the workspace A turn's diff can name a file outside the workspace. The runtime's capture layer records an out-of-workspace write as an absolute path, so the entry reaches the client with that file's content attached — and the client rendered it: the transcript's diff card, the workspace turn-review panel and the workspace review page each put the patch text on screen. The fix that stopped the collection at the source was made upstream in `packages/local-runtime/src/turns/file-changes.ts` and reverted in PR #240, because it changed a package this fork may not change. The boundary is therefore drawn on the client side, which is the last point before the content reaches a screen. A diff's file entries are now projected through that boundary before they reach a surface. An entry whose path resolves outside the workspace keeps its name, its status and its line counts, and loses its body: the diff text, the patch hunks, the file's content. The surface says what is withheld instead of showing it. The entry itself stays in the list — "which file was touched" is the part a person can still act on. The comparison is lexical, on posix-style strings, the same normalization the message file-reference reader uses. An absolute path is compared with the root segment by segment, so a sibling directory that shares the root's name is outside; a relative path is refused when it climbs out with `..`. Two limits are stated rather than papered over, and ADR 0024 and the tests say so out loud: * A symlink inside the workspace that points outside it stays contained here. The entry's path is a relative one and the browser has no filesystem to resolve it against; telling the two apart needs `realpath` on the runtime side. * An unknown workspace root is not a boundary. The entries are shown as they are, because a check that hid everything it could not place would hide ordinary content too. The upstream half of this is still open: nothing here stops the runtime from sending an out-of-workspace file's patch to the browser, only from the browser showing it. The client stops the content from being displayed; only the upstream change stops it from being collected. * test(webui): stop the agent transcript case from racing three writes it reads The case reads three things that are written by someone else — the runtime's active turn, the client's captured request frames, and the runtime's event stream — and it read all three once, at a moment that is not the moment the write lands. The turn before the cancellation step is over when its answer is on screen, but the runtime retires the turn a moment later, and the case sent the next message into that gap. A session whose turn has not been retired does not start a turn: the message is queued instead, so it never reaches the model and the case waits for an event that will not come. The case now waits for the turn to be retired, the way it already does before its other sends. The plan-entry request was read out of the frame capture once, right after the answer that answers it had rendered. The capture is now polled, the way the persisted history next to it is already polled. The runtime's event watch was torn down the moment the turn went inactive — which is exactly when the runtime reports the failure — so an event still in flight when the watch was aborted was lost, and the comparison that follows had nothing on the runtime side to compare the client's event against. The watch now stays open until both sides carry the error. The turn retries the failing model call, so the runtime reports the failure more than once and the first report is not necessarily the one the client saw; the pairing now looks at every error each side reported, and the two views still have to name the same turn. How it was verified: the case was run repeatedly on the same machine. Before the change it failed on the cancellation gate in about two runs out of five, and the model server never received the message at all — the runtime never started a turn for it. The same case fails the same way on the trunk without this branch, so the flake is not this branch's; the change removes the races the case was reading through and leaves the assertions as they were. --------- Co-authored-by: probe <probe@local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The
webuibranch is the downstream client ofMiniMax-AI/minimax-code. Theupstream owns every package except
packages/webuiandpackages/browser-extension, and the sync direction is upstream → fork(
33ab349f chore(sync): merge upstream main into webui).A change to an upstream package here does not flow back upstream. It becomes
unreviewed divergence at the next upstream sync, and the defect stays open in
upstream. So local work on this branch belongs inside
packages/webui, plusthe documentation, tests and gates.
What this removes
The upstream-package half of the work merged today:
packages/local-runtime/src/turns/file-changes.tspackages/local-runtime/test/unit/diff-capture-symlink-containment.test.tspackages/local-runtime/test/unit/diff-mutation-symlink-containment.test.tspackages/agent-tools/src/shared/workspace-real-path.tspackages/agent-tools/src/desktop/workspace-fence.ts(+ its test)packages/agent-tools/src/desktop/path-guard.ts(+ its test)packages/agent-tools/src/desktop/local-pi-tools.tspackages/agent-tools/src/cloud/matrix-tools/path-guard.tspackages/local-runtime-v2/src/service/workspace/operations/workspace-path.ts(+ its test)packages/local-runtime-v2/src/application/conversation/conversation-application.tspackages/local-runtime-v2/src/service/session-system/stream/session-stream-service.ts(+ its test)Each file is restored to the state it had immediately before the change that
touched it — the parent of that change, not the start of the day, so the
upstream sync (
33ab349f) is preserved rather than undone.The inventories follow the files: eight paths leave
release/public-source.json(regenerated withnode scripts/source-inventory.mjs --write, not hand-edited) and six suitesleave
test/vitest-suites.json.What stays
Everything under
packages/webui:and the queue), from fix(turn): keep a resumed session stream live for a running turn #237
What this gives up
The upstream packages go back to the state upstream has, so the defects those
changes addressed are open again in this fork:
(upstream
local-runtime/src/turns/file-changes.ts)read/write/edittools resolve a path lexically, so asymlink inside the workspace reaches outside it
local-runtime-v2writes fall back to the lexical path when resolutionfails
The WebUI client cannot close those on its own. It does not sit on the write
path, on the capture path or on the tool path, and the runtime host options
expose no path or tool policy hook —
systemPromptOverridesis prompt text,which is advice, not a boundary.
This is stated here so the decision is on the record: the boundary and the
security fix cannot both be satisfied inside this branch. The options are to
carry the fix here, or to file it upstream and take it back through the sync.