feat(gateway): migrate chat transport to WebSocket and enforce single ironclaw profile

This commit introduces a new plan to transition the chat transport from CLI processes to Gateway WebSocket, while maintaining the existing SSE API contract. It locks the web to a single `ironclaw` profile, disables workspace/profile switching, and updates relevant tests. Key changes include the implementation of a WebSocket-backed adapter, API lockdown with 403 responses for profile mutations, and UI adjustments to remove profile switching controls.
This commit is contained in:
kumarabhirup 2026-03-03 15:39:27 -08:00
parent 72d5204e52
commit 477daad4ff
No known key found for this signature in database
GPG Key ID: DB7CA2289CAB0167
3 changed files with 146 additions and 17 deletions

View File

@ -0,0 +1,112 @@
---
name: gateway-ws ironclaw lock
overview: Migrate `apps/web` chat transport from CLI `--stream-json` processes to Gateway WebSocket while preserving the existing SSE API contract, then lock web to a single `ironclaw` profile and disable workspace/profile switching (403 for disabled APIs). Add targeted web and bootstrap tests for the new behavior.
todos:
- id: ws-transport-adapter
content: Implement Gateway WebSocket-backed AgentProcessHandle adapter in apps/web/lib/agent-runner.ts while keeping existing NDJSON event contract.
status: completed
- id: active-runs-ws-rpc
content: Swap abort and subagent follow-up CLI gateway calls to WebSocket RPC calls in active-runs/subagent-runs.
status: completed
- id: profile-default-lock
content: Default web runtime profile resolution to ironclaw in workspace.ts and ensure state/web-chat/workspace paths resolve under ~/.openclaw-ironclaw.
status: completed
- id: api-lockdown
content: Return 403 for profile/workspace mutation APIs and keep /api/profiles compatible with a single ironclaw profile payload.
status: completed
- id: ui-single-profile
content: Remove profile switch/create workspace controls from sidebars and empty state; clean workspace page wiring accordingly.
status: completed
- id: dench-path-update
content: Update skills/dench/SKILL.md workspace path references to ~/.openclaw-ironclaw/workspace.
status: completed
- id: web-tests
content: Update/add apps/web tests covering WS transport behavior, API lock responses, and ironclaw path resolution.
status: completed
- id: bootstrap-tests
content: Add src/cli tests for run-main bootstrap cutover logic and bootstrap-external diagnostics behavior.
status: completed
isProject: false
---
# Migrate Web Chat to Gateway WS + Lock Ironclaw Profile
## Final behavior
- Keep frontend transport unchanged (`/api/chat` + `/api/chat/stream` SSE contract remains intact).
- Replace backend CLI stream/process transport with Gateway WebSocket transport.
- Force single-profile behavior in web runtime (`ironclaw`), so workspace/chat/session paths resolve to `~/.openclaw-ironclaw/*`.
- Disable profile/workspace mutation endpoints with `403` (`/api/profiles/switch`, `/api/workspace/init`).
- Remove/disable UI controls for profile switching and workspace creation.
## Transport migration (backend only)
- Add a Gateway WS runtime client in `[apps/web/lib/agent-runner.ts](apps/web/lib/agent-runner.ts)` that:
- opens a WS connection to Gateway,
- performs `connect` handshake,
- starts parent runs via Gateway RPC,
- tails `agent` events and emits NDJSON lines compatible with existing `ActiveRun` parsing.
- Preserve `AgentProcessHandle` shape so `[apps/web/lib/active-runs.ts](apps/web/lib/active-runs.ts)` and `[apps/web/lib/subagent-runs.ts](apps/web/lib/subagent-runs.ts)` can keep their SSE event transformation logic unchanged.
- Replace CLI `gateway call` usage with WS RPC helper calls for abort/follow-up paths in:
- `[apps/web/lib/active-runs.ts](apps/web/lib/active-runs.ts)`
- `[apps/web/lib/subagent-runs.ts](apps/web/lib/subagent-runs.ts)`
## Profile/path locking
- Update profile resolution in `[apps/web/lib/workspace.ts](apps/web/lib/workspace.ts)` so web runtime defaults to `ironclaw` (without changing test-mode assumptions), ensuring state dir resolves to `~/.openclaw-ironclaw` unless explicitly overridden.
- Keep filesystem resolvers (`resolveOpenClawStateDir`, `resolveWebChatDir`, `resolveWorkspaceRoot`) as the single source of truth used by chat/session/tree APIs.
- Update watcher ignore path in `[apps/web/next.config.ts](apps/web/next.config.ts)` to include ironclaw state dir.
## Disable profile/workspace mutation surfaces
- Return `403` in:
- `[apps/web/app/api/profiles/switch/route.ts](apps/web/app/api/profiles/switch/route.ts)`
- `[apps/web/app/api/workspace/init/route.ts](apps/web/app/api/workspace/init/route.ts)`
- Make `[apps/web/app/api/profiles/route.ts](apps/web/app/api/profiles/route.ts)` return a single effective `ironclaw` profile payload for UI compatibility.
## UI updates (single-profile UX)
- Remove profile/workspace creation controls from:
- `[apps/web/app/components/workspace/workspace-sidebar.tsx](apps/web/app/components/workspace/workspace-sidebar.tsx)`
- `[apps/web/app/components/sidebar.tsx](apps/web/app/components/sidebar.tsx)`
- `[apps/web/app/components/workspace/empty-state.tsx](apps/web/app/components/workspace/empty-state.tsx)`
- Update workspace page wiring in `[apps/web/app/workspace/page.tsx](apps/web/app/workspace/page.tsx)` to drop `onProfileSwitch` / `onWorkspaceCreated` refresh flow no longer reachable in single-profile mode.
- Keep chat/subagent naming semantics intact (`agent:main:web:<sessionId>` and existing subagent keys).
## Dench skill path update
- Replace `~/.openclaw/workspace` references with `~/.openclaw-ironclaw/workspace` in `[skills/dench/SKILL.md](skills/dench/SKILL.md)`.
## Tests to add/update
- Transport and runtime tests:
- update/add in `[apps/web/lib/agent-runner.test.ts](apps/web/lib/agent-runner.test.ts)` for WS handshake/start/subscribe/abort behavior and session-key naming.
- update in `[apps/web/lib/active-runs.test.ts](apps/web/lib/active-runs.test.ts)` where transport assumptions changed.
- API lock tests:
- update `[apps/web/app/api/profiles/route.test.ts](apps/web/app/api/profiles/route.test.ts)` for single-profile payload and `403` switch behavior.
- update `[apps/web/app/api/workspace/init/route.test.ts](apps/web/app/api/workspace/init/route.test.ts)` for `403` lock behavior.
- Path behavior tests:
- add/adjust targeted assertions in workspace resolver tests for ironclaw state/web-chat/workspace directories.
- Bootstrap tests (new):
- add `src/cli` tests for rollout/cutover behavior in `[src/cli/run-main.ts](src/cli/run-main.ts)`.
- add diagnostics/rollout gate tests for `[src/cli/bootstrap-external.ts](src/cli/bootstrap-external.ts)` exported helpers.
## Runtime data flow (post-migration)
```mermaid
flowchart LR
chatPanel[ChatPanel useChat] --> apiChat[/api/chat]
apiChat --> activeRuns[active-runs startRun]
activeRuns --> gatewayProc[agent-runner WS process-handle adapter]
gatewayProc --> gatewayWs[Gateway WebSocket]
gatewayWs --> gatewayProc
gatewayProc --> activeRuns
activeRuns --> sse[/api/chat/stream SSE]
sse --> chatPanel
```
## Verification after implementation
- Run web tests for changed areas (`agent-runner`, `active-runs`, chat API, profiles/workspace-init API).
- Run bootstrap-focused tests for `src/cli/run-main.ts` and `src/cli/bootstrap-external.ts`.
- Smoke-check workspace tree and web sessions resolve under `~/.openclaw-ironclaw` with switching/creation controls disabled.

View File

@ -1451,27 +1451,28 @@ function ToolStep({
</pre>
)}
{/* Output toggle — skip for media files and diffs only */}
{/* Output toggle — show for completed tools, or partial output while running */}
{outputText &&
status === "done" &&
!isSingleMedia &&
!diffText && (
<div className="mt-1">
<button
type="button"
onClick={() =>
setShowOutput((v) => !v)
}
className="text-[11px] hover:underline cursor-pointer"
style={{
color: "var(--color-accent)",
}}
>
{showOutput
? "Hide output"
: "Show output"}
</button>
{showOutput && (
{status === "done" && (
<button
type="button"
onClick={() =>
setShowOutput((v) => !v)
}
className="text-[11px] hover:underline cursor-pointer"
style={{
color: "var(--color-accent)",
}}
>
{showOutput
? "Hide output"
: "Show output"}
</button>
)}
{(showOutput || status === "running") && (
<pre
className="mt-1 text-[11px] font-mono rounded-lg px-2.5 py-2 overflow-x-auto whitespace-pre-wrap break-all max-h-96 overflow-y-auto leading-relaxed"
style={{

View File

@ -548,6 +548,22 @@ export function createStreamParser() {
}
}
break;
case "tool-output-partial":
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
if (
p.type === "dynamic-tool" &&
p.toolCallId === event.toolCallId
) {
p.output =
(event.output as Record<
string,
unknown
>) ?? {};
break;
}
}
break;
case "tool-output-available":
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];