From 4e552967d913f7f533231b514f1c5873b9eab0d6 Mon Sep 17 00:00:00 2001 From: Gordo <123185582+gsknnft@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:29:20 -0400 Subject: [PATCH] [PRIORITY][FEAT] merge runtime profiles, office systems, doctor, and vera lane convergence (#109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: include kanbanImmersive in immersiveOverlayActive calculation When Kanban board is open, HUD elements (camera preset buttons, edit toolbar, overlays) should be suppressed. The kanbanImmersive flag was defined but not included in the immersiveOverlayActive condition, causing HUD elements to remain visible. This fix adds kanbanImmersive to the immersiveOverlayActive calculation so HUD elements are properly hidden when the Kanban board is open. Co-authored-by: Luke The Dev * Fix: Hide mini status bar when Kanban immersive overlay is open Wraps the bottom-left mini status bar (showing agent stats, vibe score, and control hints) with !immersiveOverlayActive check to match the behavior of other HUD elements like camera controls and toolbar. This ensures the status bar is properly hidden when the Kanban board or any other immersive overlay is active, maintaining a clean immersive experience. Co-authored-by: Luke The Dev * chore: drop unrelated package-lock line from branch Co-authored-by: Luke The Dev * universal-backend-plan * backend-neutral runtime seam * package.json update * feat: add Hermes gateway adapter as alternative to OpenClaw Adds a WebSocket adapter that lets Claw3D connect to a Hermes AI agent runtime without any changes to the frontend. The adapter implements the full Claw3D gateway protocol and bridges it to the Hermes HTTP API. Changes: - server/hermes-gateway-adapter.js: WebSocket bridge implementing the Claw3D gateway protocol against the Hermes HTTP API. Supports all core methods (agents, sessions, chat streaming, cron, config, files, approvals) and multi-agent orchestration via spawn_agent/delegate_task tools. Persists conversation history to ~/.hermes/clawd3d-history.json. - scripts/clawd3d-start.sh: All-in-one startup script that launches Hermes, the adapter, and the Next.js dev server with auto port conflict resolution. Alias as `claw3d` for convenience. - src/features/office/hooks/useCronAgents.ts: Hook that polls the gateway for cron-scheduled agents and surfaces them in the 3D office. - package.json: adds `hermes-adapter` npm script - .env.example: documents Hermes config vars - docs/hermes-gateway.md: setup guide and protocol reference Usage: npm run hermes-adapter # start adapter (connect to http://localhost:8642) npm run dev # start Claw3D, point browser at localhost:3000 # or: bash scripts/clawd3d-start.sh (starts everything automatically) Both OpenClaw and Hermes are supported simultaneously — the gateway URL in NEXT_PUBLIC_GATEWAY_URL determines which backend Claw3D connects to. Co-Authored-By: Claude Sonnet 4.6 * feat: add read_agent_context tool for cross-agent coordination Agents can now read each other's conversation history via the read_agent_context tool, enabling the orchestrator to check what a sub-agent has done before re-delegating work. Co-Authored-By: Claude Sonnet 4.6 * feat: wire Hermes office UX and role-aware runtime updates * feature update - demomode & hermes adapter * fix lint blockers * lintfix #2 * fix: stabilize retro office camera preset callbacks * Initial plan * fix: stabilize retro office overview preset hooks Agent-Logs-Url: https://github.com/gsknnft/Claw3D/sessions/9cc71555-591e-44cf-aec4-25affbdcb405 Co-authored-by: gsknnft <123185582+gsknnft@users.noreply.github.com> * feat: add truthful backend selection, Hermes adapter hardening, and demo gateway mode * fix: address bugbot review and finalize backend selection * fixed - onboarding and hermes calls * office systems roadmap * feat specs in docs * specs ready * feat: continue custom runtime seam and gateway alignment * custom lane wired * feat: add custom runtime provider path and office runtime alignment * office_sys prep * tighten multi-floor runtime spec * multi-floor v1 implementation * moved floor nav * runtime architecture specs * claw3doctor specs * feat: add first pass claw3doctor diagnostics * docs: align roadmap with runtime profiles and office systems priorities * feat: expand claw3doctor provider diagnostics and json output * feat: improve claw3doctor formatting and multi-runtime diagnostics * feat: formalize runtime profile resolution * feat: expand claw3doctor profile health diagnostics * feat: polish claw3doctor output and tunnel remediation * feat: add claw3doctor profile scoping and failure classification * docs: define claw3doctor v1 boundary and v2 backlog * test: fix stale claw3doctor branch expectations * test: fix stale expectations on claw3doctor branch * fix(claw3doctor): scope provider-specific checks to --profile / --all-profiles flags PR #101 finding: - Medium: --profile and --all-profiles only scoped the profile health probe loop; OpenClaw/Hermes/Demo/Custom check blocks still ran based on the selected adapter type in runtimeContext, making CLI output misleading. Fix: introduce adapterInScope(adapterType, defaultBehavior) helper in main(). - --profile -> only that adapter's checks run - --all-profiles -> all adapter checks run - no flag -> falls back to existing shouldRun* predicate (unchanged) Also: - Export parseDoctorArgs from claw3doctor-core.mjs (removed duplicate in script) - Add test suites: parseDoctorArgs flag parsing (6 cases) and adapterInScope scoping semantics (4 cases) — 10 new tests, all green * fix(office): persist per-floor selectedAgentId on focusLocalAgent + wire officeFloors runtime state PR #96 findings: - Medium #1: focusLocalAgent now writes selectedAgentId back to floorRosterCache so handleSelectFloor restores the agent the user last picked on each floor rather than snapping back to the hydration-time suggestion. - Medium #2: Add useEffect that calls settingsCoordinator.schedulePatch with officeFloors[activeFloorId] patch on every status/gatewayUrl change, writing status, gatewayUrl, lastKnownGoodAt, lastErrorCode, and lastErrorMessage so the persisted floor runtime state actually tracks live connection transitions. * fix unkept changes * fix audit findings - cross-floor misattribution & officefloors silent drops * pushed changes * multi-agentic runtime & chat bubble fix * partial parity with office-sys-next * claw3doctor parity * partial parity with v_lane * merged feat/office-systems-next -> merge_sys * parity across PR branches * rm *.orig postmerge * full parity across unmerged PRs & main * fix lukes findings * fix findings - bigger chatbox * real local upload path * fixed file upload, MIME integgration * minor fix * fix lukess findings * deleted *.orig * three bugs fixed - gatewayclient, coord, claw3doctor * address findings * fix lukes findings * fix findings #2 * fix: ignore temporary skill-agent names during identity recovery * fix: preserve stable identity names during temp-name recovery * fix(gateway): correct disconnect race and token-blanking on adapter switch - disconnect() now checks actual connection status rather than selectedAdapterType, which may already reflect the target adapter when the effect fires. Prevents stale WebSocket clients from persisting after switching to local/claw3d/custom backends. - setSelectedAdapterType() falls back to loadedGatewaySettings.current.profiles token when the in-memory adapterProfiles entry has an empty token (sanitized API form). Prevents saved tokens from being cleared when switching between backends. Closes Luke findings: High (stale gateway on floor switch), Medium (token blanking). Authored-By: GSKNNFT * feat(gateway): loopback bypass, control-ui remap, operator.read scope, 75ms connect Cherry-picked clean additions from pr/gsknnft-2 (fix/reduce-gateway-connect-delay): - proxy-url.ts: resolveStudioProxyGatewayUrl() now accepts optional upstreamGatewayUrl; loopback hosts (localhost/127.0.0.1/::1) bypass the Studio proxy and connect directly - gateway-proxy.js: remap unauthenticated openclaw-control-ui connections to webchat-ui client ID so OpenClaw doesn't reject them as unknown clients - GatewayBrowserClient.ts + nodeGatewayClient.ts: add operator.read scope to both browser and Node gateway clients for expanded access control - GatewayBrowserClient.ts: reduce socket open→connect delay from 750ms to 75ms GatewayClient.ts rewrites from that PR were intentionally excluded — they would regress our disconnect-race fix, token-blanking fix, broken-regex fix, local/claw3d adapter support, adapterProfiles type export, and private envelope path. Co-Authored-By: Claude Sonnet 4.6 * fix(floor-nav): show only available floors per active adapter; block hang on unconfigured runtime - floors.ts: add paperclip to FloorProvider; add listAvailableFloorsForAdapter() — lobby always visible, runtime floors only shown when their provider matches the active adapter, so demo-only users only see Lobby - OfficeFloorNav: accept activeAdapterType prop, filter floor list via listAvailableFloorsForAdapter(); fall back displayActiveFloorId to lobby if current floor is no longer in the available set - OfficeScreen: pass selectedAdapterType to OfficeFloorNav; add guard in handleSelectFloor — bail back to lobby immediately when a runtime floor has no gateway URL configured, preventing the connect-hang limbo state Authored-By: GSKNNFT * hardening: drop unsafe-eval in production CSP; add TRUSTED_PROXY IP resolution next.config.ts: - unsafe-eval removed from production script-src (Next.js dev/HMR needs it, but production build does not; React and Three.js make no use of eval) - connect-src intentionally kept broad with note: gateway URLs are user-configured at runtime, cannot be enumerated at build time server/access-gate.js: - Add resolveClientIp() helper: when TRUSTED_PROXY=1 env var is set, prefer the first value of X-Forwarded-For for rate-limiter keying (correct behavior behind nginx/Caddy/Vercel edge). Without the flag, remoteAddress is used (safe default for direct exposure — prevents X-Forwarded-For spoofing by untrusted clients). Authored-By: GSKNNFT * fix(security): remove upstream tokens from browser API; propagate abort to custom runtime HIGH — /api/studio: strip gatewayPrivate and localGatewayDefaultsPrivate from GET and PUT responses. Upstream tokens must not cross the browser API boundary. The Studio proxy (server/gateway-proxy.js) already injects the server-side token into connect frames when the browser sends an empty token, so the browser never needed raw tokens. GatewayClient.ts and OfficeScreen.tsx updated to work from sanitized public settings only. MEDIUM — /api/runtime/custom route: pass request.signal to the upstream fetch() call. Client abort (e.g. hitting Stop) now cancels the upstream runtime request instead of leaving it running after the browser fetch resolves. Authored By: GSKNNFT * fix(security): preserve stored token through empty-token UI state; handle non-JSON health responses GatewayClient.ts — autosave effects no longer overwrite persisted gateway tokens with empty strings. When the in-memory token is empty (proxy handles auth server-side), the patch omits the token field (undefined) so mergeGatewaySettings/mergeGatewayProfiles treats it as "leave unchanged". adapterProfiles updater also preserves the existing stored token when the new token is empty, preventing floor-switch from erasing tokens. runtime/custom/http.ts — requestCustomRuntime() checks the response Content-Type before calling response.json(). Non-JSON responses (e.g. plain-text /health "OK") are returned as-is instead of throwing a JSON parse error, making the custom/local/claw3d health probe path reliable for runtimes that return plain text. Authored By: GSKNNFT * fix(bug): avoid overwriting stored tokens with an empty UI value GatewayClient.ts:944 → token: "" || undefined = undefined → omitted from patch mergeGatewayConnectionState: patch.token === undefined → patchedToken = undefined → nextToken = undefined || current?.token ?? "" = "abc123" ✓ scenarionn- user explicitly clears token (empty string patch): mergeGatewayConnectionState: patchedToken = "" → nextToken = "" || current?.token ?? "" = falls back to existing stored token Authored By: GSKNNFT * fix ongoing findings issue * test: update gateway connection persistence expectations Co-authored-by: Luke The Dev * fix: tighten ts.net hostname matching in doctor Co-authored-by: Luke The Dev * chore(release): prepare v0.1.4 Pin in-repo app version to 0.1.4 to match the planned GitHub release tag, and document the convergence release in CHANGELOG.md with verified 0.1.3 and 0.1.4 entries covering runtime profiles, multi-floor offices, remote messaging/handoffs, file uploads, security hardening, and the claw3doctor diagnostics CLI. Made-with: Cursor --------- Co-authored-by: Cursor Agent Co-authored-by: Luke The Dev Co-authored-by: Elias Pfeffer Co-authored-by: Claude Sonnet 4.6 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Greg Clark Co-authored-by: iamlukethedev --- CHANGELOG.md | 83 ++ MULTI_AGENT_BETA.md | 250 +----- README.md | 24 +- ROADMAP.md | 58 +- docs/bulletin-board-spec.md | 393 ++++++++++ docs/desk-progression-spec.md | 410 ++++++++++ docs/hierarchy-and-teams-spec.md | 423 +++++++++++ docs/integrations/claw3doctor-spec.md | 293 +++++++ .../runtime-profile-architecture.md | 320 ++++++++ docs/meeting-room-workflow-spec.md | 390 ++++++++++ docs/multi-agent-beta.md | 265 +++++++ docs/office-systems-roadmap.md | 349 +++++++++ .../multi-floor-runtime-architecture.md | 641 ++++++++++++++++ docs/office_sys/office-systems-roadmap.md | 148 +++- docs/qa-department-spec.md | 464 ++++++++++++ docs/roadmap.md | 61 ++ docs/runtime-profiles.md | 145 ++++ docs/whiteboard-spec.md | 423 +++++++++++ next.config.ts | 13 +- package.json | 3 +- scripts/claw3doctor.mjs | 596 +++++++++++++++ scripts/lib/claw3doctor-core.mjs | 573 ++++++++++++++ server/access-gate.js | 20 +- server/demo-gateway-adapter.js | 21 +- server/gateway-proxy.js | 26 +- src/app/api/files/[file]/route.ts | 63 ++ src/app/api/files/upload/route.ts | 140 ++++ src/app/api/office/presence/route.ts | 91 ++- src/app/api/office/remote-handoff/route.ts | 101 +++ src/app/api/office/remote-message/route.ts | 57 +- src/app/api/runtime/custom/route.ts | 3 + src/app/api/studio/route.ts | 9 +- .../agents/components/AgentChatPanel.tsx | 225 +++++- .../agents/components/ConnectionPanel.tsx | 37 + .../components/GatewayConnectScreen.tsx | 54 +- .../components/inspect/AgentSettingsPanel.tsx | 70 +- .../agents/operations/agentFleetHydration.ts | 25 +- .../agents/operations/chatSendOperation.ts | 6 +- .../useChatInteractionController.ts | 29 +- .../agents/screens/AgentsPageScreen.tsx | 9 +- .../office/components/OfficeFloorNav.tsx | 165 ++++ .../components/RemoteAgentChatPanel.tsx | 106 ++- .../components/panels/SettingsPanel.tsx | 9 +- .../hooks/useOfficeFloorRuntimePersistence.ts | 71 ++ .../office/hooks/useRemoteOfficePresence.ts | 9 +- src/features/office/screens/OfficeScreen.tsx | 714 ++++++++++++++++-- src/features/retro-office/RetroOffice3D.tsx | 64 +- .../retro-office/core/furnitureDefaults.ts | 43 +- src/features/retro-office/objects/agents.tsx | 69 +- src/lib/gateway/GatewayClient.ts | 207 +++-- src/lib/gateway/nodeGatewayClient.ts | 2 +- .../gateway/openclaw/GatewayBrowserClient.ts | 7 +- src/lib/gateway/proxy-url.ts | 16 +- src/lib/office/floorRoster.ts | 121 +++ src/lib/office/floors.ts | 204 +++++ src/lib/openclaw/voiceTranscription.ts | 23 +- src/lib/runtime/agentMessaging.ts | 145 ++++ src/lib/runtime/createRuntimeProvider.ts | 24 +- src/lib/runtime/custom/http.ts | 8 + src/lib/runtime/custom/provider.ts | 113 ++- src/lib/runtime/demo/provider.ts | 17 +- src/lib/runtime/hermes/provider.ts | 17 +- src/lib/runtime/openclaw/provider.ts | 17 +- src/lib/runtime/types.ts | 17 +- src/lib/studio/coordinator.ts | 106 ++- src/lib/studio/settings-store.ts | 22 +- src/lib/studio/settings.ts | 298 +++++++- tests/unit/agentFleetHydration.test.ts | 96 ++- .../agentFleetHydrationDerivation.test.ts | 15 +- tests/unit/agentSettingsPanel.test.ts | 23 + tests/unit/agentStateRoute.test.ts | 7 +- tests/unit/claw3doctor.test.ts | 461 +++++++++++ tests/unit/floorRoster.test.ts | 115 +++ tests/unit/gatewayAgentFiles.test.ts | 6 +- tests/unit/gatewayEnvDefaults.test.ts | 56 +- tests/unit/officeFloors.test.ts | 87 +++ tests/unit/packagedSkills.test.ts | 3 +- tests/unit/skillsRemoveRoute.test.ts | 7 +- tests/unit/studioBootstrapOperation.test.ts | 18 +- tests/unit/studioBootstrapWorkflow.test.ts | 18 +- tests/unit/studioSettings.test.ts | 215 ++++++ tests/unit/studioSettingsCoordinator.test.ts | 111 +++ tests/unit/studioSetupPaths.test.ts | 4 +- tests/unit/taskBoardView.test.ts | 2 +- ...useAgentSettingsMutationController.test.ts | 15 +- tests/unit/useGatewayConnection.test.ts | 20 +- .../useOfficeFloorRuntimePersistence.test.ts | 222 ++++++ tests/unit/useRuntimeConnection.test.ts | 86 ++- 88 files changed, 10738 insertions(+), 774 deletions(-) create mode 100644 docs/bulletin-board-spec.md create mode 100644 docs/desk-progression-spec.md create mode 100644 docs/hierarchy-and-teams-spec.md create mode 100644 docs/integrations/claw3doctor-spec.md create mode 100644 docs/integrations/runtime-profile-architecture.md create mode 100644 docs/meeting-room-workflow-spec.md create mode 100644 docs/multi-agent-beta.md create mode 100644 docs/office-systems-roadmap.md create mode 100644 docs/office_sys/multi-floor-runtime-architecture.md create mode 100644 docs/qa-department-spec.md create mode 100644 docs/roadmap.md create mode 100644 docs/runtime-profiles.md create mode 100644 docs/whiteboard-spec.md create mode 100644 scripts/claw3doctor.mjs create mode 100644 scripts/lib/claw3doctor-core.mjs create mode 100644 src/app/api/files/[file]/route.ts create mode 100644 src/app/api/files/upload/route.ts create mode 100644 src/app/api/office/remote-handoff/route.ts create mode 100644 src/features/office/components/OfficeFloorNav.tsx create mode 100644 src/features/office/hooks/useOfficeFloorRuntimePersistence.ts create mode 100644 src/lib/office/floorRoster.ts create mode 100644 src/lib/office/floors.ts create mode 100644 src/lib/runtime/agentMessaging.ts create mode 100644 tests/unit/claw3doctor.test.ts create mode 100644 tests/unit/floorRoster.test.ts create mode 100644 tests/unit/officeFloors.test.ts create mode 100644 tests/unit/useOfficeFloorRuntimePersistence.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ee737d1..1e66263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,88 @@ # Changelog +## [0.1.4] - 2026-04-23 + +Runtime Profiles, Multi-Floor Offices, Remote Collaboration, and Diagnostics. + +This release converges the runtime-profiles, office-systems, claw3doctor, and selective `vera_lane` work into one branch. Backends become named profiles, the office becomes a multi-floor building, remote offices gain server-backed messaging and handoffs, and a new diagnostics CLI makes setup and troubleshooting first-class. + +### Added + +- Named runtime profiles for `openclaw`, `hermes`, `demo`, `local`, `claw3d`, and `custom` backends, each storing its own URL and token in Studio settings instead of a single global pair (`docs/runtime-profiles.md`, `src/lib/runtime/*`). +- Multi-floor office runtime model with one runtime binding per floor and persistent per-floor state, including `lobby`, `openclaw-ground`, `hermes-first`, `local-runtime`, `claw3d-runtime`, `custom-second`, `training`, `traders-floor`, and `campus` (`src/lib/office/floors.ts`, `docs/office_sys/multi-floor-runtime-architecture.md`). +- Floor roster persistence and a floor navigation HUD for moving between runtime-backed floors in a single session (`src/lib/office/floorRoster.ts`, `src/features/office/components/OfficeFloorNav.tsx`, `src/features/office/hooks/useOfficeFloorRuntimePersistence.ts`). +- Remote office messaging API for cross-office direct messages with structured assistant-history reply resolution (`src/app/api/office/remote-message/route.ts`). +- Remote office handoff API for sending task / context / deliverables / acceptance criteria to a remote agent through the runtime layer (`src/app/api/office/remote-handoff/route.ts`, `src/lib/runtime/agentMessaging.ts`). +- Local file upload route for chat attachments with allowlisted MIME types and a 10 MB upload cap (`src/app/api/files/upload/route.ts`). +- `claw3doctor` diagnostics CLI with profile-scoped and `--all-profiles` runs, OpenClaw / Hermes / demo / custom-runtime probes, gateway failure classification, JSON output, and a `npm run doctor` script (`scripts/claw3doctor.mjs`, `scripts/lib/claw3doctor-core.mjs`, `package.json`). +- New product and architecture docs covering runtime profiles, multi-floor architecture, the claw3doctor spec, runtime profile architecture, the refreshed roadmap, multi-agent beta, and the bulletin-board, desk-progression, hierarchy-and-teams, meeting-room-workflow, QA-department, and whiteboard specs (`docs/`). + +### Changed + +- Runtime provider selection is now profile-aware across `openclaw`, `hermes`, `demo`, `local`, `claw3d`, and `custom` instead of collapsing into one generic path (`src/lib/runtime/createRuntimeProvider.ts`, `src/lib/runtime/{openclaw,hermes,demo,custom}/provider.ts`). +- Studio settings now persist per-profile URL/token entries and an active profile selection, with coordinated bootstrap and hydration paths (`src/lib/studio/settings.ts`, `src/lib/studio/settings-store.ts`, `src/lib/studio/coordinator.ts`). +- Remote agent chat panel and remote office presence flows updated for the new server-backed delivery and reply behavior (`src/features/office/components/RemoteAgentChatPanel.tsx`, `src/features/office/hooks/useRemoteOfficePresence.ts`). +- Hardened production security headers: strict CSP with `'unsafe-eval'` only in dev, `Referrer-Policy`, `X-Content-Type-Options`, `X-Frame-Options: SAMEORIGIN`, restrictive `Permissions-Policy`, `Cross-Origin-Resource-Policy: same-origin`, and HSTS in production (`next.config.ts`). +- Access gate rewritten with constant-time token comparison, a per-IP rate limiter (10 attempts / 60s), and `TRUSTED_PROXY=1`-gated `X-Forwarded-For` handling to prevent IP spoofing (`server/access-gate.js`). +- Custom runtime provider and proxy URL handling tightened around runtime boundaries and allowlists (`src/lib/runtime/custom/provider.ts`, `src/lib/gateway/proxy-url.ts`). + +### Fixed + +- Repaired merge-corrupted files and removed tracked merge artifacts left over from the earlier overlapping branch stack. +- Resolved a UTF-8 / Turbopack parsing issue and cleaned up Turbopack root and optional `openclaw` resolution warnings (`next.config.ts`). +- Office navigation and pathfinding behavior tightened around floor-aware routing and runtime persistence (`src/features/office/screens/OfficeScreen.tsx`, `src/features/retro-office/RetroOffice3D.tsx`). + +### Tests + +- Added unit coverage for `claw3doctor`, office floors, floor roster, runtime connection, gateway connection, office floor runtime persistence, agent fleet hydration derivation, and studio settings coordinator behavior (`tests/unit/`). + +### Docs + +- Replaced top-level `MULTI_AGENT_BETA.md` and `ROADMAP.md` with stubs that point to canonical docs under `docs/`, and added a runtime profiles reference from `README.md`. +- Expanded `README.md` to describe `Local` and `Claw3D` runtime modes and persistent backend profile configuration, including the additional `local` and `claw3d` values for `CLAW3D_GATEWAY_ADAPTER_TYPE`. + +### Notes + +- This release bumps the in-repo app version to `0.1.4`. After merging, cut the GitHub release/tag as `v0.1.4`. +- This is still an early-stage release. Remote office workflows, runtime profiles, multi-floor offices, and the diagnostics CLI will continue to iterate quickly in upcoming versions. + +## [0.1.3] - 2026-03-28 + +Remote Offices, Skills Marketplace, and Company Builder. + +This release expands Claw3D from a single-office viewer into a more complete AI workplace, with guided setup, richer agent operations, remote office support, and stronger security hardening. + +### Added + +- New onboarding wizard for first-time setup, including gateway connection, prerequisites, company details, and initial agent configuration. +- New packaged skills marketplace with trigger-driven office routing, including starter skills like Todo Board and SOUNDCLAW. +- New office agent management wizard for creating and managing agents directly from the office experience. +- New multi-agent beta support for remote office layouts, presence sync, and remote messaging. +- New company builder wizard with AI-assisted organization generation and bootstrap planning. +- Runtime gateway URL fallback through `/api/studio` for more reliable environment-specific setup. + +### Changed + +- Improved UI polish, responsiveness, and accessibility across the main app and office surfaces. +- Hardened access control so gating applies across all routes, not only `/api`. +- Enforced voice upload size limits before buffering. + +### Fixed + +- Closed multiple path traversal and file-path validation gaps in local file operations. +- Resolved symlink handling issues in path suggestions. +- Improved office navigation and pathfinding by fixing diagonal corner-cutting, metadata-driven blockers, collision-aware routing, and A* failure behavior. +- Removed a TypeScript TS2367 build blocker in `skillGymDirective`. + +### Docs + +- Added an Agent Bus integration guide for visualizing AI sessions in Claw3D. +- Refreshed the public roadmap. + +### Notes + +- This is still an early-stage release. The platform is moving quickly, especially around remote office workflows, skills, and guided setup, so expect rapid iteration in upcoming versions. + ## [0.1.2] - 2026-03-20 ### Added diff --git a/MULTI_AGENT_BETA.md b/MULTI_AGENT_BETA.md index d3533c7..87198dc 100644 --- a/MULTI_AGENT_BETA.md +++ b/MULTI_AGENT_BETA.md @@ -1,251 +1,5 @@ # Multi-Agent Beta -This document explains the current multi-agent beta in Claw3D: what it does, how the two connection modes work, and how to connect a second office. +Moved to [docs/multi-agent-beta.md](docs/multi-agent-beta.md). -## What This Beta Does - -Claw3D can render a second office inside the same 3D scene so you can visualize agents from another machine. - -Today the beta supports: - -- showing a second office in the same world; -- displaying remote agents as read-only presence; -- optionally sending a plain-text message to a remote agent; -- keeping the remote side isolated from your local files and office controls. - -This is a beta feature. It is designed for visibility and lightweight cross-office messaging, not full shared-state collaboration. - -## Mental Model - -There are always two roles: - -- **Local office**: the Claw3D instance you are currently using; -- **Remote office**: another Claw3D instance or another OpenClaw gateway you want to visualize. - -The remote office can be connected in one of two ways: - -1. **Remote Claw3D presence endpoint**. -2. **Remote OpenClaw gateway**. - -## Connection Modes - -### 1. Remote Claw3D Presence Endpoint - -Use this when the other machine is also running Claw3D. - -How it works: - -- your local Claw3D server polls the remote Claw3D `presence` endpoint; -- it also tries to load the remote office `layout` snapshot; -- the local 3D scene renders the remote office as a read-only clone inside the same world. - -Typical URL: - -```text -https://other-office.example.com/api/office/presence -``` - -This mode is best when you want the remote side to feel like another full Claw3D office. - -### 2. Remote OpenClaw Gateway - -Use this when the other machine only runs OpenClaw and does not run Claw3D. - -How it works: - -- the browser connects directly to the remote gateway; -- Claw3D derives a read-only presence snapshot from gateway data such as `agents.list`, `status`, and `sessions.preview`; -- because there is no remote Claw3D layout endpoint, the second office uses a fallback office visualization. - -Typical URL: - -```text -ws://remote-host:18789 -``` - -or: - -```text -wss://remote-host.example.com -``` - -If you paste an `http://` or `https://` URL into gateway mode, Claw3D normalizes it to `ws://` or `wss://` before connecting. - -This mode is best when you want remote agent visibility without requiring a second Claw3D deployment. - -## What You Can See - -When the beta is enabled, you can: - -- see a second office in the same environment; -- see remote agents appear in that office; -- see remote agents move and change basic activity state; -- click a remote agent and open a text-only messaging panel. - -## What You Cannot See - -The remote office is intentionally limited. - -You cannot: - -- inspect the remote machine filesystem; -- browse the remote agent chat history in full; -- control the remote office furniture or builder state; -- take over the remote instance as if it were local. - -The goal is cross-office visualization, not remote workstation access. - -## Remote Messaging - -Remote messaging is currently a lightweight relay. - -What it does: - -- lets you send a plain-text note to a remote agent; -- is available from the remote agent chat panel; -- is designed to avoid exposing remote files or tool output in the Claw3D UI. - -Current limitations: - -- remote replies are not mirrored back into the panel yet; -- the panel currently shows your sent message plus delivery/system feedback; -- this is not a shared transcript viewer. - -## How To Connect - -### Prerequisites - -Before enabling the second office, make sure: - -- your local Claw3D is already working with your local OpenClaw gateway; -- you know which remote mode you want to use; -- the remote machine is reachable from your machine or browser; -- any required token, origin allowlist, or private-network access is already configured. - -### Setup Steps - -1. Start your local Claw3D instance. -2. Open the office UI. -3. Open the office settings panel. -4. Turn on `Show second office`. -5. Choose the correct `Source type`. -6. Fill the matching connection fields. - -### Setup For `Remote Claw3D presence endpoint` - -Use: - -- `Source type`: `Remote Claw3D presence endpoint`. -- `Presence URL`: the remote `/api/office/presence` URL. -- `Optional token`: only if that remote Claw3D endpoint is protected. - -Example: - -```text -https://other-office.example.com/api/office/presence -``` - -Expected behavior: - -- the second office appears inside the world; -- remote agents show up when the remote office has active presence; -- if the remote layout snapshot is unavailable, Claw3D falls back to a default/fallback office rendering for the remote side. - -### Setup For `Remote OpenClaw gateway` - -Use: - -- `Source type`: `Remote OpenClaw gateway`. -- `Gateway URL`: the remote gateway WebSocket URL. -- `Shared gateway token`: optional when the gateway already allows your Control UI origin and connection model. - -Examples: - -```text -ws://remote-host:18789 -``` - -```text -wss://remote-host.example.com -``` - -Expected behavior: - -- the second office appears inside the world; -- remote agents are derived from gateway presence data; -- the office shell is a fallback visualization, not a true remote layout clone from another Claw3D instance. - -## Recommended Network Patterns - -### Same private network - -Use a reachable private IP or local hostname for the remote Claw3D endpoint or OpenClaw gateway. - -### Tailscale - -Tailscale is a good fit for this beta because it lets both sides connect over a private network without exposing services publicly. - -Common patterns: - -- remote Claw3D endpoint over `https://.ts.net/api/office/presence`; -- remote OpenClaw gateway over `wss://.ts.net` if you are proxying the gateway through HTTPS/WSS; -- direct gateway over `ws://:18789` when both devices can reach the service privately. - -## Disable Behavior - -If you turn `Show second office` off: - -- the extra office should disappear from the 3D scene; -- the path/outdoor connection should disappear; -- remote office presence and layout hooks should stop driving the scene. - -This lets you return to a single-office view. - -## Troubleshooting - -### No remote agents appear - -Check: - -- the remote URL is correct; -- the remote machine is actually reachable; -- the remote service is running; -- the selected `Source type` matches the service you are pointing at. - -### Presence endpoint works but the remote layout does not - -That usually means the other machine has Claw3D presence available but not a layout snapshot yet. The beta should still render a fallback remote office. - -### Gateway mode connects but messaging fails - -In gateway mode, the browser connects directly to the remote gateway. That means the remote gateway may still reject the connection based on origin policy or other gateway-side security rules. - -If that happens, check: - -- the remote gateway URL; -- whether the remote gateway allows your Control UI origin; -- whether the remote gateway expects a token or device-auth flow you have not configured. - -### You can reach an HTTPS page but gateway mode still fails - -Opening a web page in the browser does not automatically mean the OpenClaw gateway WebSocket is reachable. - -Examples: - -- `https://host` may be reachable while `ws://host:18789` is not; -- a website reverse proxy may exist even though the raw gateway port is closed; -- the remote side may need a dedicated WSS proxy path for the gateway. - -## Current Beta Limitations - -- The second office is read-only. -- Remote replies are not mirrored into the local remote-chat panel yet. -- Gateway mode derives presence from gateway snapshots rather than a real remote Claw3D layout. -- Browser-based gateway mode depends on the remote gateway allowing the connection from your Control UI origin. -- This feature is still evolving and should be treated as beta, not final production-grade multi-tenant collaboration. - -## Summary - -Use `Remote Claw3D presence endpoint` when the other side runs Claw3D and you want the most complete office visualization. - -Use `Remote OpenClaw gateway` when the other side only runs OpenClaw and you mainly want remote agent presence plus lightweight text messaging. +This stub stays in place so older links and issue references do not break. diff --git a/README.md b/README.md index 6eb8193..a563d6c 100644 --- a/README.md +++ b/README.md @@ -100,26 +100,31 @@ npm run dev ``` Then open `http://localhost:3000` and configure the gateway URL and token in Studio. -Studio now also persists the selected backend mode (`OpenClaw`, `Hermes`, `Demo`, or `Custom`) and +Studio now also persists the selected backend mode (`OpenClaw`, `Hermes`, `Demo`, `Local`, `Claw3D`, or `Custom`) and shows the active backend reported by the connected gateway. -### Custom runtime mode +### Runtime profiles -If you are integrating an orchestrator-backed runtime through the `custom` -provider seam, start your runtime first, then start Claw3D: +If you are integrating an orchestrator-backed runtime through the direct +HTTP runtime seam, start your runtime first, then start Claw3D: ```bash npm run dev ``` -Then open `http://localhost:3000`, choose `Custom backend`, and point the -upstream URL at your runtime boundary, for example: +Then open `http://localhost:3000`, choose `Local runtime`, `Claw3D runtime`, +or `Custom backend`, and point the upstream URL at your runtime boundary. +Typical examples: ```text http://127.0.0.1:7770 ``` -Current `custom` runtime expectations: +```text +http://localhost:3000/api/runtime/custom +``` + +Current direct-runtime expectations: - `GET /health` - `GET /state` @@ -242,7 +247,7 @@ Common environment variables: - `CUSTOM_RUNTIME_ALLOWLIST` restricts which hosts `/api/runtime/custom` may fetch. If unset, it falls back to `UPSTREAM_ALLOWLIST`. - `NEXT_PUBLIC_GATEWAY_URL` provides the default upstream gateway URL when Studio settings are empty. **Note:** this is a build-time variable — changes require `npm run build` to take effect. - `CLAW3D_GATEWAY_URL` and `CLAW3D_GATEWAY_TOKEN` provide a runtime alternative to `NEXT_PUBLIC_GATEWAY_URL` that takes effect on server restart without a rebuild. -- `CLAW3D_GATEWAY_ADAPTER_TYPE` can pair with `CLAW3D_GATEWAY_URL` to mark those runtime defaults as `openclaw`, `hermes`, `demo`, or `custom`. +- `CLAW3D_GATEWAY_ADAPTER_TYPE` can pair with `CLAW3D_GATEWAY_URL` to mark those runtime defaults as `openclaw`, `hermes`, `demo`, `local`, `claw3d`, or `custom`. - If `CLAW3D_GATEWAY_URL` is not set, Studio can still surface local Hermes or demo adapter defaults from `HERMES_ADAPTER_PORT` / `DEMO_ADAPTER_PORT`. - OpenClaw file defaults still come from `~/.openclaw/openclaw.json` when present. - `OPENCLAW_STATE_DIR` and `OPENCLAW_CONFIG_PATH` override the default OpenClaw paths. @@ -270,7 +275,8 @@ See [`.env.example`](.env.example) for the full local development template. - [`VISION.md`](VISION.md): project direction and long-term guardrails. - [`ARCHITECTURE.md`](ARCHITECTURE.md): system boundaries, data flow, and major trade-offs. - [`TUTORIAL.md`](TUTORIAL.md): detailed step-by-step setup for OpenClaw + Tailscale + Claw3D. -- [`MULTI_AGENT_BETA.md`](MULTI_AGENT_BETA.md): remote office beta setup, connection modes, and limitations. +- [`docs/multi-agent-beta.md`](docs/multi-agent-beta.md): remote office beta setup, connection modes, and limitations. +- [`docs/runtime-profiles.md`](docs/runtime-profiles.md): saved backend/runtime profiles and the current HTTP runtime seam. - [`CODE_DOCUMENTATION.md`](CODE_DOCUMENTATION.md): practical code map, extension points, and contributor onboarding order. - [`CONTRIBUTING.md`](CONTRIBUTING.md): local workflow, testing, and PR expectations. - [`SUPPORT.md`](SUPPORT.md): where to ask for help and how to route reports. diff --git a/ROADMAP.md b/ROADMAP.md index 504fe32..bf9de8e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,59 +1,5 @@ # Roadmap -This file captures the near-term direction for Claw3D so outside contributors can find work that aligns with current priorities. +Moved to [docs/roadmap.md](docs/roadmap.md). -## Now - -- Open-source readiness: documentation, support routes, CI, disclosure files, and public-safe defaults. -- Runtime reliability: making gateway event handling, history reconciliation, and transport-specific recovery more predictable. -- Office architecture clarity: keeping the office intent layer centralized and reducing ad hoc room-specific behavior. - -## Next - -- Converge the immersive office and builder stack on a clearer shared model. -- Replace or fully clear unresolved bundled assets and dependency licensing risks. -- Improve security posture around Studio access bootstrap and runtime token handling. - -## Later - -- Broader office authoring workflows and richer world-building tools. -- Better contributor automation, release process, and publication tooling. -- More immersive agent/system surfaces that build on the existing office intent and runtime event model. - -## Product Ideas To Reduce OpenClaw Dependency - -- Expand the new agent wizard into reusable agent templates and presets, building on the existing playbook templates, onboarding flow, and agent creation steps. -- Turn the current onboarding and connection experience into a fuller workspace setup wizard that validates gateway access, permissions, local-vs-remote behavior, and common integrations in one place. -- Add a first-class heartbeat builder that unifies scheduled automations, `HEARTBEAT.md`, and related defaults into one guided UI instead of splitting that setup across multiple surfaces. -- Add a fleet-level tool access matrix with bulk controls so users can manage agent permissions and allowed tools across the whole office instead of one agent at a time. -- Add a shared user profile center that can manage and optionally sync `USER.md` defaults across multiple agents, rather than editing each agent independently. -- Add a real agent inbox and task queue that goes beyond the current results/inbox surfaces and lets users assign, retry, and route work between agents. -- Add a dedicated health dashboard that brings gateway status, failed runs, heartbeat issues, missing dependencies, and integration problems into one operational view. -- Add a broader prompt and playbook library on top of the current playbook template foundation so users can save, browse, and reuse recurring workflows more easily. -- Add visual office automation features that let users configure recurring behaviors and room-based actions directly from the office instead of relying on lower-level gateway concepts. -- Add an agent relationships and communication map so users can configure which agents collaborate, hand off work, or talk to each other without editing raw configuration. -- Add shared memory management for cross-agent context, since the current experience only exposes per-agent `MEMORY.md`. -- Add multi-agent orchestration and handoff workflows for common sequences such as PM -> Engineer -> QA, with explicit UI instead of relying on manual coordination. -- Add config diff and rollback tools so gateway-wide changes can be reviewed and safely reverted from Claw3D. -- Add conversation-to-agent bootstrap flows that can turn a successful chat or office interaction into a reusable new agent. -- Add a richer scenario simulator that extends the current mock phone/text scenarios into broader multi-agent rehearsal and testing flows. - -## Already In Progress Or Partially Covered - -- Skill installer compatibility checks already exist and should be expanded rather than reinvented. -- Playbook templates, scheduled automations, and onboarding flows already cover part of the templates/setup story. -- Per-agent capability controls and tool settings already exist, but not yet as a fleet-wide matrix. -- Analytics, connection status, and office activity surfaces already cover part of the future health dashboard story. -- The office builder, immersive office, and event-triggered behavior already cover part of the visual automation story. - -## Good First Contribution Areas - -- Documentation and developer-onboarding fixes. -- Focused unit-test additions around runtime workflows or office intent behavior. -- Small UI polish issues that stay inside one feature area. -- Replacing stale examples, placeholder text, or internal-only assumptions in public docs. - -## Before Starting Bigger Work - -- Read `README.md`, `CODE_DOCUMENTATION.md`, and `KNOWN_ISSUES.md`. -- Prefer opening or linking a GitHub issue before large architectural changes. +This stub stays in place so older links and issue references do not break. diff --git a/docs/bulletin-board-spec.md b/docs/bulletin-board-spec.md new file mode 100644 index 0000000..1a54a04 --- /dev/null +++ b/docs/bulletin-board-spec.md @@ -0,0 +1,393 @@ +# Bulletin Board Spec + +> First concrete office-system feature for Claw3D. + +## Goal + +Add a shared bulletin board inside the office that acts as the visible coordination surface for: + +- goals +- announcements +- blockers +- handoff notes +- standup outcomes +- lightweight task cards + +This should be the first step toward making Claw3D a real agent operations environment instead of only a gateway visualizer. + +## Product Position + +The bulletin board is not a replacement for the existing task board or Kanban views. + +It is the office-native layer above them. + +Think of it as: + +- the most important things the office should see right now +- the shared memory wall +- the in-world coordination surface + +## Why This Feature First + +This is the best first office-system feature because it is: + +- easy to understand +- visually natural in the office +- useful even before deeper simulation systems exist +- compatible with all backends +- able to reuse existing task and standup signals + +It also creates a clean landing zone for future systems: + +- whiteboards +- meeting summaries +- QA queues +- hierarchy / department routing +- shared office memory + +## Primary Use Cases + +### Shared Goals + +Examples: + +- "Ship Hermes adapter support" +- "Fix production bug in standup flow" +- "Prepare Friday release review" + +### Announcements + +Examples: + +- "Hermes provider smoke test passed" +- "Build is blocked on QA" +- "Meeting starts in 5 minutes" + +### Blockers + +Examples: + +- "Gateway auth broken on staging" +- "Agent Alice waiting on review" +- "No provider token configured" + +### Handoffs + +Examples: + +- "Backend done, hand off to QA" +- "Needs design signoff" +- "Waiting for owner approval" + +### Meeting Output + +Examples: + +- standup summary +- decisions made +- next actions +- active speaker queue + +## V1 Scope + +V1 should stay intentionally small. + +The board should support a few card types and simple interactions, not a full project-management suite. + +### Card Types + +Initial types: + +- `goal` +- `announcement` +- `blocker` +- `handoff` +- `meeting_note` + +### Card Fields + +Minimum shape: + +```ts +type BulletinBoardCardType = + | "goal" + | "announcement" + | "blocker" + | "handoff" + | "meeting_note"; + +type BulletinBoardCard = { + id: string; + type: BulletinBoardCardType; + title: string; + body: string; + createdAt: string; + updatedAt: string; + authorType: "human" | "agent" | "system"; + authorId?: string | null; + authorName?: string | null; + agentId?: string | null; + sessionKey?: string | null; + taskId?: string | null; + pinned: boolean; + archived: boolean; + priority?: "low" | "normal" | "high"; + tags?: string[]; +}; +``` + +### Basic Interactions + +V1 interactions: + +- create card +- edit card +- pin/unpin card +- archive/unarchive card +- filter by type +- filter by author +- open linked session or linked agent + +No drag-and-drop lane system is required for V1. + +## Visual Design + +The bulletin board should feel like a wall-mounted coordination surface inside the office. + +Possible visual forms: + +- cork board +- notice board +- sprint wall +- pinboard with index cards / sticky notes + +The in-world object should have: + +- a visible prop in the retro office +- a click target +- an immersive detail panel when opened + +It should feel distinct from the existing Kanban board. + +Suggested difference: + +- Kanban = detailed task workflow +- Bulletin board = office-wide signal surface + +## Information Hierarchy + +At a glance, the board should answer: + +1. What is the office trying to do? +2. What is blocked? +3. What changed recently? +4. What needs a human to notice? + +Recommended layout: + +- pinned cards first +- blockers prominently visible +- recent announcements grouped together +- meeting notes grouped separately + +## Integration Points + +The feature should hook into systems Claw3D already has. + +### Task Board / Kanban + +Use the bulletin board as a summary layer over the task board, not a duplicate. + +Examples: + +- show a pinned goal card that links into Kanban +- create blocker cards when a task enters a blocked state +- create handoff cards when work moves between agents or departments + +### Standup System + +The standup system already exists. + +Use it to populate: + +- current meeting announcement +- summary card after standup completes +- follow-up note cards for unresolved blockers + +### Agent Sessions + +Cards should be linkable to: + +- an agent +- a session key +- a run or task where applicable + +That lets the user jump from "office signal" to "underlying conversation or task". + +### Runtime-Neutral Backends + +The board must not depend on OpenClaw-specific methods. + +It should operate off: + +- Claw3D state +- local persisted office data +- optional provider metadata when available + +That keeps it usable with: + +- OpenClaw +- Hermes +- Vera +- Demo mode + +## Storage Model + +V1 storage should be local office data persisted through the same Studio settings path used by other office preferences. + +Suggested storage location: + +- studio office settings keyed by gateway URL + +Example shape: + +```ts +type OfficePreference = { + bulletinBoard?: { + cards: BulletinBoardCard[]; + updatedAt?: string; + }; +}; +``` + +Why: + +- matches existing office preference patterns +- backend-neutral +- fast to implement +- easy to migrate later + +## Authoring Rules + +Cards may be created by: + +- human user +- agent action +- system automation + +Recommended rules: + +- human cards should always be editable +- system cards can be archived but not freely mutated +- agent cards should show authorship clearly + +That keeps provenance visible without overcomplicating the model. + +## V1 Automation + +Useful automations to add early: + +- create a meeting note card after standup +- create a blocker card from explicit blocked-state flows +- create announcement cards for major office events + +Keep automation conservative. + +The board should not flood itself with noise. + +## UI Surfaces + +### In-World Object + +Add a dedicated bulletin board prop to the office layout. + +It should: + +- be visible from the main office floor +- support hover/click affordance +- open an immersive board panel + +### Sidebar / Panel Access + +Also add a panel entry for cases where the user wants quick access without camera movement. + +Possible placement: + +- HQ sidebar tab +- office control panel + +### Agent Interaction + +Optional for V1: + +- agents can approach the board during meetings or handoffs +- pinned cards can be reflected in ambient office behavior + +This is useful but not required for first delivery. + +## Out of Scope For V1 + +Do not include these initially: + +- full Kanban replacement +- freehand drawing +- multiplayer collaborative editing +- complicated permission lattice +- department-specific boards +- heavy simulation logic +- arbitrary external integrations + +Those belong in later systems. + +## Implementation Strategy + +Recommended order: + +1. Define board card types and storage schema. +2. Add persisted board data to office settings. +3. Add a simple board panel UI. +4. Add in-world bulletin board prop and open interaction. +5. Connect standup summary output. +6. Add simple blocker / announcement automation. + +## Existing Code Seams + +This feature should likely align with: + +- task board state and controller logic in `src/features/office/tasks` +- standup flows in `src/features/office/hooks/useOfficeStandupController.ts` +- office settings persistence +- retro office object interaction in `src/features/retro-office/RetroOffice3D.tsx` +- furniture/object definitions in `src/features/retro-office/objects` + +This is intentional. + +The bulletin board should reuse existing office mechanics where possible. + +## Success Criteria + +V1 is successful if: + +- the user can open the board from inside the office +- the board shows office-relevant cards, not just generic notes +- standup or blocker information can appear on the board +- cards can link back into agents/sessions/tasks +- the system works with Hermes, OpenClaw, and demo mode + +## Future Extensions + +Once V1 is stable, this can grow into: + +- department boards +- QA wall +- release wall +- meeting room whiteboard handoff +- agent-authored summaries +- office-wide historical archive +- team-specific bulletin surfaces + +## Summary + +The bulletin board should become the first shared memory surface inside Claw3D. + +It is the clearest next step toward making the office itself the product. diff --git a/docs/desk-progression-spec.md b/docs/desk-progression-spec.md new file mode 100644 index 0000000..f13ee16 --- /dev/null +++ b/docs/desk-progression-spec.md @@ -0,0 +1,410 @@ +# Desk Progression Spec + +> Fifth concrete office-system feature for Claw3D, connecting visible office presence to role maturity, permissions, and capability growth. + +## Goal + +Add a desk progression system so agents visibly grow from limited office members into more capable contributors. + +Desk progression should connect: + +- role maturity +- workspace/tool access +- permissions +- office identity +- visible progression in the environment + +The goal is not just cosmetics. + +The goal is to make office growth legible and meaningful. + +## Product Position + +Desk progression should be the physical expression of organizational state. + +It answers questions like: + +- is this agent an intern or a fully trusted contributor? +- what tools can they use? +- how much autonomy do they have? +- how much context or responsibility should they carry? + +In other words: + +- desk progression = visible capability ladder + +## Why This Feature Matters + +Without progression, all agents tend to feel flat. + +Desk progression creates: + +- visible hierarchy without requiring a complex org chart first +- a natural path for permissions and access +- stronger office storytelling +- motivation for role specialization and promotion systems later + +It also gives you a much cleaner bridge between: + +- abstract policy +- physical office layout +- agent identity + +## Core Principle + +Do not start with fake game stats. + +Start with operational capability tiers that can later gain more playful flavor. + +That means progression should first affect: + +- tool access +- workspace access +- review requirements +- ability to spawn/delegate +- context budget or workload tolerance + +The visual office layer should reflect those operational differences. + +## Example Role Ladder + +Recommended initial tiers: + +- `intern` +- `probation` +- `employee` +- `senior` +- `lead` +- `contractor` + +These are examples, not hard-coded lore. + +### Intern + +Characteristics: + +- limited tools +- limited workspace access +- small or shared desk +- requires close oversight + +### Probation + +Characteristics: + +- basic desk +- restricted autonomy +- still under review for sensitive actions + +### Employee + +Characteristics: + +- normal desk +- normal task ownership +- standard office access + +### Senior + +Characteristics: + +- stronger autonomy +- wider task scope +- can mentor or review others + +### Lead + +Characteristics: + +- can coordinate others +- can trigger certain meetings +- can manage or route work more broadly + +### Contractor + +Characteristics: + +- useful specialist +- limited long-term authority +- constrained workspace and access model + +## Suggested Capability Model + +V1 should describe progression in terms of clear capability flags. + +Example: + +```ts +type DeskTier = + | "intern" + | "probation" + | "employee" + | "senior" + | "lead" + | "contractor"; + +type DeskCapabilityProfile = { + tier: DeskTier; + canUseFileTools: boolean; + canUseWebTools: boolean; + canInstallSkills: boolean; + canRequestApprovalsDirectly: boolean; + canReviewOthers: boolean; + canTriggerMeetings: boolean; + canCreateTasks: boolean; + canDelegateTasks: boolean; + workspaceAccess: "none" | "limited" | "standard" | "extended"; + contextBudgetClass: "small" | "normal" | "large"; +}; +``` + +The exact values can evolve, but the idea should remain: + +- tier drives visible access differences + +## Visual Expression + +Each tier should map to a clear desk/environment feel. + +Examples: + +### Intern Desk + +- minimal desk +- no dedicated computer or weaker setup +- fewer personal objects +- close to a shared area or support station + +### Probation Desk + +- basic computer +- little customization +- modest footprint + +### Employee Desk + +- normal workstation +- standard office setup +- stable identity in the room + +### Senior Desk + +- expanded desk +- more equipment / screens / references +- visually established presence + +### Lead Desk + +- premium workstation +- visibility within the office +- closer proximity to planning or meeting surfaces + +### Contractor Desk + +- temporary station +- portable or isolated feel +- clearly functional but not deeply embedded + +## Relationship To Existing Systems + +Desk progression should integrate with real Claw3D systems rather than sit beside them. + +### Permissions + +Claw3D already has permission and approval surfaces. + +Desk progression should act as a higher-level office policy layer that influences: + +- what defaults an agent gets +- whether sensitive actions need review +- what tools or flows are emphasized + +Important: + +This does not need to replace existing permission logic. + +It should help explain and structure it. + +### Workspace Access + +Agents already have real workspaces. + +Desk progression should help determine: + +- how much workspace freedom an agent gets +- whether they operate in restricted or normal modes +- whether some installs or edits require higher tiers + +### QA Department + +More mature agents can naturally interact differently with QA. + +Examples: + +- interns more often route into review +- seniors can participate in review +- leads can mark certain work as ready for higher-level signoff + +### Meeting Room + +Meeting behavior can reflect progression. + +Examples: + +- leads can call planning meetings +- seniors can present or facilitate review +- interns may attend but not control outcomes + +### Bulletin Board / Whiteboard + +More mature tiers may: + +- author higher-priority office notes +- post official announcements +- create planning documents for others + +Again, this should be treated as office behavior, not roleplay for its own sake. + +## Promotion / Progression Logic + +V1 does not need automatic leveling. + +Start with: + +- manual assignment +- explicit promotion/demotion +- visible tier on the agent profile + +Later, progression can be influenced by: + +- successful task completion +- review outcomes +- reliability +- blockers created vs resolved +- trust level + +## Suggested Data Model + +Example V1 shape: + +```ts +type AgentDeskProfile = { + agentId: string; + tier: DeskTier; + assignedDeskUid?: string | null; + promotedAt?: string | null; + notes?: string | null; +}; +``` + +Office-level data: + +```ts +type OfficePreference = { + deskProgression?: { + byAgentId: Record; + updatedAt?: string; + }; +}; +``` + +## Human Interaction Model + +The human should be able to: + +- view an agent’s desk tier +- promote or demote an agent +- reassign desk placement +- understand what the tier changes operationally + +This should be clear and reversible. + +Do not hide progression behind mystery rules. + +## Agent Interaction Model + +Agents may later: + +- request promotion +- request better tools +- recommend another agent for a role upgrade +- be restricted from actions based on tier + +But V1 should not depend on autonomous progression requests. + +## V1 Scope + +Recommended V1 scope: + +- define desk tiers +- persist per-agent desk tier +- show desk tier in UI +- apply visual desk differentiation +- connect tier to a small number of capability differences + +Good first capability differences: + +- review / approval expectations +- delegation rights +- desk computer presence + +## Out of Scope For V1 + +Do not include these initially: + +- hidden progression XP systems +- complex morale simulation +- salary/economy systems +- automatic performance scoring +- punitive systems that make agents unusable + +Keep V1 understandable and operational. + +## Implementation Strategy + +Recommended order: + +1. Define desk tier model and profile storage. +2. Add UI for viewing and assigning tier. +3. Add retro-office visual differences by tier. +4. Connect tier to a small capability profile. +5. Surface tier in agent details and office presence. + +## Existing Code Seams + +This feature should likely align with: + +- office desk assignment systems +- agent settings / permissions UI +- approval and policy surfaces +- retro office desk rendering +- office preferences persistence + +This matters because progression should feel native to the office, not bolted on. + +## Success Criteria + +V1 is successful if: + +- desk tier is visible and understandable +- the office reflects agent maturity visually +- tier differences have real operational meaning +- the user can promote/demote intentionally +- the system reinforces office identity instead of distracting from it + +## Future Extensions + +Once V1 is stable, later systems can add: + +- promotion ceremonies or office events +- hierarchy-aware desk placement +- department-specific workstation styles +- probation rules +- contractor/offsite variants +- context / workload tuning by tier + +## Summary + +Desk progression should turn office growth into something visible and operational. + +It is the cleanest way to connect hierarchy, permissions, workspace access, and office identity without jumping straight into heavy simulation. diff --git a/docs/hierarchy-and-teams-spec.md b/docs/hierarchy-and-teams-spec.md new file mode 100644 index 0000000..9cf5d51 --- /dev/null +++ b/docs/hierarchy-and-teams-spec.md @@ -0,0 +1,423 @@ +# Hierarchy And Teams Spec + +> Sixth concrete office-system feature for Claw3D, turning visible office roles into an actual organizational model for delegation, permissions, and team coordination. + +## Goal + +Add a hierarchy and teams system so the office can express: + +- who leads +- who reports where +- who can delegate +- who can approve +- who belongs to which team + +The goal is to move from "a group of agents in one room" to "an actual organization with structure". + +## Product Position + +Hierarchy and teams should not exist only as labels. + +They should affect: + +- delegation +- review authority +- meeting participation +- bulletin/whiteboard authorship weight +- task routing +- desk progression meaning + +This is the organizational layer that sits above desks and departments. + +## Why This Feature Matters + +Without hierarchy, all agents are peers by default. + +That creates limits: + +- delegation feels flat +- responsibility is ambiguous +- review authority is unclear +- the office lacks believable structure + +Hierarchy and teams solve that by giving the office: + +- reporting lines +- ownership boundaries +- authority surfaces +- coordination lanes + +## Core Principle + +Keep hierarchy operational, not theatrical. + +Do not start with elaborate roleplay. + +Start with real organizational questions: + +- who can assign work? +- who can review work? +- who can call meetings? +- who can finalize outcomes? +- which agents belong together? + +## Suggested Role Model + +Recommended role classes: + +- `owner` +- `executive` +- `manager` +- `lead` +- `member` +- `contractor` +- `intern` + +These role classes are about authority and org structure. + +They are distinct from: + +- desk tier +- functional specialty +- department + +### Owner + +Characteristics: + +- human-controlled top authority +- final signoff for high-impact actions +- can override structure + +### Executive + +Characteristics: + +- broad office-level coordination +- can set direction across teams + +### Manager + +Characteristics: + +- owns a team or department lane +- routes work +- coordinates reviews and meetings + +### Lead + +Characteristics: + +- technical or functional authority inside a team +- can delegate and review + +### Member + +Characteristics: + +- standard contributor role +- executes work inside team scope + +### Contractor + +Characteristics: + +- scoped contributor +- limited authority outside assigned work + +### Intern + +Characteristics: + +- low-authority contributor +- learning / supervised mode + +## Team Model + +Teams should be explicit groups, not only emergent behavior. + +Suggested examples: + +- Platform +- Frontend +- QA +- Research +- Ops +- Design + +Suggested V1 shape: + +```ts +type TeamId = string; + +type OfficeTeam = { + id: TeamId; + name: string; + description?: string; + leadAgentId?: string | null; + managerAgentId?: string | null; + memberAgentIds: string[]; + departmentId?: string | null; +}; +``` + +## Hierarchy Model + +Suggested V1 shape: + +```ts +type OfficeHierarchyRole = + | "owner" + | "executive" + | "manager" + | "lead" + | "member" + | "contractor" + | "intern"; + +type AgentHierarchyProfile = { + agentId: string; + role: OfficeHierarchyRole; + reportsToAgentId?: string | null; + teamId?: string | null; + departmentId?: string | null; + canDelegate?: boolean; + canReview?: boolean; + canApprove?: boolean; + canCallMeetings?: boolean; +}; +``` + +The exact flags can be derived from role later. + +V1 can store them explicitly for clarity if needed. + +## Relationship To Desk Progression + +Desk progression expresses maturity and capability in a physical way. + +Hierarchy expresses authority and organizational position. + +These should be related, but not identical. + +Examples: + +- a senior desk does not automatically make an agent a manager +- a contractor may have a strong workstation but still limited authority +- a lead may have more coordination authority than a senior member + +That separation matters. + +## Relationship To Departments + +Departments are organizational domains. + +Examples: + +- Engineering +- QA +- Research +- Ops + +Teams live inside or alongside departments. + +Examples: + +- Engineering -> Frontend Team +- Engineering -> Platform Team +- QA -> Release Team + +Hierarchy determines authority. +Departments determine domain. +Teams determine working group. + +## Relationship To Meetings + +Hierarchy should affect meetings in practical ways. + +Examples: + +- managers or leads can call planning meetings +- review meetings may require a lead or manager present +- interns may attend but not finalize decisions +- executives may approve office-wide changes after summary + +This gives meetings more structure without overcomplicating V1. + +## Relationship To QA + +Hierarchy should influence QA responsibility. + +Examples: + +- leads can review work +- managers can route items into QA +- members can request review +- interns more often require review +- owners/executives can override final readiness decisions when needed + +QA should remain operationally distinct, but authority should not be flat. + +## Relationship To Bulletin Board / Whiteboard + +Hierarchy can shape information flow. + +Examples: + +- high-priority office announcements may come from leads/managers +- planning whiteboards may identify team ownership +- bulletin cards can show team and owner context + +Important: + +Do not hide information behind hierarchy. + +Use hierarchy to improve clarity, not to make the office opaque. + +## Delegation Model + +Hierarchy becomes most useful when it changes delegation behavior. + +Suggested operational rules: + +- owners, executives, managers, and leads can delegate +- members can hand off but not broadly route work across the org +- contractors delegate only within limited scope +- interns usually cannot delegate except in restricted workflows + +This should be represented both: + +- in UI +- in agent-facing behavior and constraints where appropriate + +## Visual Expression + +Hierarchy should have visible but restrained expression in the office. + +Examples: + +- title/subtitle on agent nameplate +- seat/desk placement +- room proximity to planning areas +- meeting table positioning +- desk quality in combination with progression + +The office should communicate structure without turning into a caricature. + +## Human Interaction Model + +The human should be able to: + +- assign hierarchy role +- assign team +- set reporting line +- move agents between teams +- understand what organizational changes actually affect + +This should be editable and transparent. + +## Agent Interaction Model + +Longer term, agents may: + +- recommend reassignments +- request escalation +- request specialist support from another team +- suggest promotions or org changes + +V1 does not need autonomous re-org behavior. + +V1 should focus on: + +- clear structure +- delegation paths +- UI visibility + +## V1 Scope + +Recommended V1 scope: + +- explicit hierarchy role per agent +- explicit team membership +- simple reporting line +- visible title/subtitle +- delegation and meeting authority rules at a lightweight level + +Keep V1 small enough that it improves office understanding immediately. + +## Storage Model + +Suggested shape: + +```ts +type OfficePreference = { + hierarchy?: { + byAgentId: Record; + teams: OfficeTeam[]; + updatedAt?: string; + }; +}; +``` + +This keeps the feature local, backend-neutral, and easy to evolve. + +## Out of Scope For V1 + +Do not include these initially: + +- automatic org chart optimization +- political simulation +- compensation/economy systems +- punitive management mechanics +- heavy workflow bureaucracy + +The system should clarify work, not create needless friction. + +## Implementation Strategy + +Recommended order: + +1. Define hierarchy profile and team schema. +2. Add office-level persistence. +3. Add UI for role/team assignment. +4. Show titles/subtitles and team membership in office/agent UI. +5. Apply lightweight authority rules to delegation and meeting actions. + +## Existing Code Seams + +This feature should likely align with: + +- role/title flow already added for agents +- desk progression data and UI +- meeting room workflows +- QA routing +- bulletin board ownership/priority metadata + +This is important because hierarchy should unify other office systems rather than stand apart from them. + +## Success Criteria + +V1 is successful if: + +- the office can represent who leads and who belongs where +- delegation and review paths are clearer +- titles/teams are visible in the office +- hierarchy affects at least a small set of real office behaviors +- the system remains understandable and editable + +## Future Extensions + +Once V1 is stable, follow-up work can add: + +- org chart views +- department dashboards +- automatic escalation paths +- team-specific meeting rituals +- richer approval chains +- promotion recommendations + +## Summary + +Hierarchy and teams should give Claw3D a real organizational model. + +That model should support delegation, ownership, and coordination without losing the clarity and playfulness of the office metaphor. diff --git a/docs/integrations/claw3doctor-spec.md b/docs/integrations/claw3doctor-spec.md new file mode 100644 index 0000000..7757fc2 --- /dev/null +++ b/docs/integrations/claw3doctor-spec.md @@ -0,0 +1,293 @@ +# Claw3Doctor Spec + +> First-pass diagnostics plan for Claw3D deployments so users stop chasing the same setup failures manually. + +## Goal + +Provide a single diagnostics surface for the common "Claw3D cannot connect" +or "runtime support looks broken" cases. + +The intent is similar to: + +- `openclaw doctor` +- `hermes doctor` + +but focused on Claw3D's integration points across providers. + +## Primary Outcomes + +`claw3doctor` should: + +- identify the selected runtime profile/provider +- verify the gateway is reachable +- identify common auth/config mistakes +- surface provider-specific hints without making the whole app provider-specific +- reduce issue-thread back-and-forth + +## First-Pass Scope + +### Claw3D Settings / Environment + +Checks: + +- current runtime profile selection +- gateway URL presence +- token presence when required +- adapter/provider selection +- obvious `.env` misconfiguration + +Outputs: + +- selected provider/profile +- missing env or token warnings +- suspicious profile precedence warnings + +### Gateway Reachability + +Checks: + +- can the configured gateway URL be reached? +- can Studio proxy the selected gateway? +- does the endpoint respond like a Claw3D-compatible gateway? + +Outputs: + +- reachable / unreachable +- timeout / refused / bad handshake +- wrong backend contract warning + +### OpenClaw Checks + +Checks: + +- OpenClaw version +- pairing/device-approval state hints +- common remote secure-context failures +- common `1008`, `1011`, `1012` patterns + +Outputs: + +- version found / not found +- device approval guidance +- remote/Tailscale/public tunnel guidance + +### Hermes Checks + +Checks: + +- Hermes adapter running +- Hermes API reachable +- Hermes model present +- auth key configured if required +- adapter env loaded correctly + +Outputs: + +- adapter found / missing +- API reachable / unreachable +- `401` / bad model / bad URL hints + +### Auth / Token Checks + +Checks: + +- missing Studio access token +- gateway token missing +- invalid API key patterns +- profile says tokened backend but token is absent + +Outputs: + +- precise missing-token messages +- auth mismatch guidance + +### WebSocket / Origin / Secure-Context Checks + +Checks: + +- localhost vs remote +- secure-context expectations +- browser/origin hints for public/tunneled deployments +- Cloudflare/ngrok/reverse-proxy warning patterns + +Outputs: + +- websocket handshake guidance +- origin/secure-context notes +- public tunnel caution notes + +## Recommended Output Shape + +`claw3doctor` should produce: + +- short headline result +- categorized checks +- pass / warn / fail per item +- copy-pasteable next actions + +Example: + +```text +Claw3Doctor: WARN + +[pass] Runtime profile: OpenClaw Default +[pass] Gateway URL reachable: ws://localhost:18789 +[warn] OpenClaw version: 2026.4.2 +[fail] Device approval required for remote browser +[warn] Secure-context mismatch for public remote setup + +Suggested next actions: +1. openclaw devices approve --latest +2. retry from an approved browser/device +3. if using a public tunnel, test local/LAN direct first +``` + +## Runtime-Profile Awareness + +`claw3doctor` should be designed against the runtime-profile model: + +- provider +- runtime profile +- floor binding + +That means the doctor should never assume: + +- one backend +- one port +- one global runtime mode + +Instead it should inspect the currently selected profile and run the +appropriate checks for that provider. + +## Provider-Specific Guidance Rules + +### OpenClaw + +Focus on: + +- pairing +- device identity +- remote websocket setup +- public/tunnel secure-context issues + +### Hermes + +Focus on: + +- adapter process +- Hermes API reachability +- model/config correctness +- auth key presence + +### Custom Runtime + +Focus on: + +- gateway contract compatibility +- reachability +- auth +- profile configuration + +## Suggested Implementation Order + +### PR 1: CLI / Script Scaffold + +Add: + +- doctor command entrypoint or script +- report formatter +- shared result types + +### PR 2: Runtime Profile Checks + +Add: + +- selected profile inspection +- settings/env parsing +- gateway URL/token checks + +### PR 3: Provider Checks + +Add: + +- OpenClaw checks +- Hermes checks +- custom runtime checks + +### PR 4: Common Failure Classifiers + +Add: + +- websocket close-code guidance +- secure-context/origin hints +- reverse-proxy/tunnel notes + +## Relationship To Office Systems + +`claw3doctor` should land before more runtime complexity because it will +make debugging: + +- multi-runtime support +- floor-to-profile binding +- public remote deployment + +much less painful. + +This is why it is sequenced ahead of deeper Office Systems feature work. + +## V1 Delivery Boundary + +`claw3doctor` v1 should be considered complete when it provides: + +- selected-profile diagnostics with optional per-profile probing +- grouped terminal output with clear pass / warn / fail results +- JSON output for automation and issue reporting +- provider-aware checks for OpenClaw, Hermes, demo, and custom runtimes +- common failure classification for transport and auth problems +- concrete remediation for local, remote, tunneled, and adapter-backed setups + +This keeps v1 reviewable as deployment diagnostics rather than letting it +turn into a full runtime orchestration project. + +## V2 Expansion Backlog + +After v1 lands, the next doctor-specific expansion should focus on better +diagnosis depth and better operator ergonomics rather than broader scope. + +### Higher-Signal Runtime Heuristics + +- deeper OpenClaw pairing and device-approval detection +- stronger close-code interpretation from real-world failures +- provider-specific contract validation for demo and custom runtimes +- better wrong-model / wrong-adapter mismatch detection + +### Tunnel / Proxy Guidance + +- Cloudflare-specific websocket and origin remediation +- Tailscale-specific remote deployment guidance +- reverse-proxy fingerprinting and likely-misconfiguration hints +- public-host checks when auth or secure-context expectations are missing + +### Output / Workflow Improvements + +- richer terminal presentation +- issue-template or bundle-friendly export +- in-app diagnostics panel later, reusing the same JSON report +- optional doctor autofix for safe configuration repairs + +### Runtime-Profile Follow-Through + +`claw3doctor` v2 should also benefit from the separate runtime-profile work: + +- simultaneous runtime profile visibility +- per-profile health history +- floor-to-profile diagnosis once Office Systems binding is live + +## Follow-Up Docs + +After this spec, the next planning doc should be: + +- floor schema and builder plan + +That doc should define the metadata model before any admin-side floor +builder is implemented. diff --git a/docs/integrations/runtime-profile-architecture.md b/docs/integrations/runtime-profile-architecture.md new file mode 100644 index 0000000..59dc8f3 --- /dev/null +++ b/docs/integrations/runtime-profile-architecture.md @@ -0,0 +1,320 @@ +# Runtime Profile Architecture + +> Forward-looking runtime model for Claw3D after the OpenClaw + Hermes adapter work landed on `main`. + +## Goal + +Claw3D should treat runtime connection targets as profiles, not as ad hoc +gateway URLs tied to one backend assumption. + +That means the app should model: + +- provider +- runtime profile +- floor binding + +instead of making the user think in terms of: + +- one hard-coded backend +- one port +- one global gateway selection + +## Recommendation + +Use one gateway contract in the UI, with different backend providers +behind it. + +Default path: + +- `OpenClaw` is the default runtime profile + +Optional paths: + +- `Hermes Adapter` +- `Custom Runtime(s)` + +The important rule is: + +- the UI keeps speaking one Claw3D gateway contract +- the backend behind that contract may be native OpenClaw, Hermes through + the adapter, or a custom runtime/provider + +## Core Terms + +### Provider + +A provider identifies the backend family behind a runtime profile. + +Initial provider set: + +- `openclaw` +- `hermes` +- `custom` +- `demo` + +Provider answers questions like: + +- what backend is this? +- what capabilities should the UI expect? +- which default labels or help copy apply? + +### Runtime Profile + +A runtime profile is a named connection target. + +Examples: + +- `OpenClaw Default` +- `Hermes Adapter` +- `Custom Staging Runtime` +- `Custom Prod Runtime` + +Suggested shape: + +```ts +export type RuntimeProfileId = string; + +export type RuntimeProfile = { + id: RuntimeProfileId; + label: string; + provider: "openclaw" | "hermes" | "custom" | "demo"; + gatewayUrl: string; + token?: string | null; + adapterType?: "openclaw" | "hermes" | "custom" | "demo" | null; + enabled: boolean; + defaultProfile: boolean; + notes?: string | null; +}; +``` + +Runtime profile answers: + +- where does Claw3D connect? +- what provider is behind this connection? +- which auth/token should be used? +- which profile should be the default? + +### Floor Binding + +A floor binding maps an office floor to a runtime profile. + +Examples: + +- `OpenClaw Floor -> openclaw-default` +- `Hermes Floor -> hermes-default` +- `Custom Floor -> custom-default` +- `Lobby -> null` +- `Campus -> null` + +Suggested shape: + +```ts +export type FloorRuntimeBinding = { + floorId: FloorId; + runtimeProfileId: RuntimeProfileId | null; +}; +``` + +Floor binding answers: + +- which runtime powers this floor? +- is this floor provider-backed, function-backed, or a destination? + +## Runtime Model + +The recommended mental model is: + +```text +Provider -> Runtime Profile -> Floor Binding +``` + +Examples: + +- provider: `openclaw` + - profile: `openclaw-default` + - bound floor: `openclaw-ground` + +- provider: `hermes` + - profile: `hermes-default` + - bound floor: `hermes-first` + +- provider: `custom` + - profiles: + - `custom-default` + - `custom-staging` + - `custom-prod` + - one or more custom floors may bind to them later + +## Default Behavior + +Initial default behavior should be: + +- if nothing else is configured, Claw3D prefers `OpenClaw` +- Hermes remains optional and adapter-backed +- custom runtimes remain optional and profile-driven + +That means: + +- OpenClaw is the safe baseline +- Hermes should not destabilize the default path +- custom runtimes should not require special-case UI logic + +## Hermes Adapter Position + +Right now Hermes works through the adapter path, and that is acceptable as +the near-term production path. + +Architecture implication: + +- Hermes should be represented as a provider/profile combination +- not as a special global mode + +So the app should think: + +- provider: `hermes` +- profile: `hermes-default` +- gateway URL: adapter endpoint + +This keeps the runtime selection model uniform even though Hermes is still +adapter-backed today. + +## Custom Runtime Position + +Custom runtimes should fit the same profile model: + +- provider: `custom` +- one or more named profiles +- floor binding selects which profile powers which floor + +This avoids making the "Custom Floor" logic one-off and lets Claw3D grow +to multiple custom environments without another architecture pass. + +## One Gateway Contract, Different Backends + +The UI should not branch everywhere on backend family. + +Instead: + +- the browser talks one Claw3D gateway contract +- Studio/settings select the runtime profile +- profile/provider metadata informs capability checks and defaults + +This keeps the frontend stable while backends differ behind the scenes. + +## Office Systems Implications + +This model supports the current Office Systems direction cleanly. + +### Floor Zones + +- `Building` +- `Outside` + +### Floor Types + +- provider-backed +- function-backed +- destination/outside + +### Examples + +- `Lobby` + - function-backed + - no runtime binding required + +- `OpenClaw Floor` + - provider-backed + - bound to `openclaw-default` + +- `Hermes Floor` + - provider-backed + - bound to `hermes-default` + +- `Custom Floor` + - provider-backed + - bound to `custom-default` + +- `Training Floor` + - function-backed + - may later bind to a chosen provider profile + +- `Trader's Floor` + - function-backed + - may later bind to a chosen provider profile + +- `Campus` / `Stadium` + - outside destinations + - not required to behave like numbered floors + +## Branch Sequence + +Recommended next branches: + +- `docs/runtime-profiles` +- `feat/claw3doctor` +- `refactor/office-shell` +- later: + - `docs/floor-builder-schema` + - `feat/floor-builder` + +## Next Sequence + +### 1. `docs: runtime profile architecture` + +Define: + +- provider vs profile vs floor binding +- `OpenClaw` default +- `Hermes Adapter` optional +- `Custom Runtime(s)` optional +- one gateway contract, different backends + +### 2. `feat: claw3doctor` + +First pass should check: + +- Claw3D settings/env +- gateway reachability +- OpenClaw version +- Hermes adapter/API availability +- auth/token issues +- common websocket/origin/secure-context failures + +### 3. `refactor: office shell modularization` + +Next extractions: + +- `OfficeShell` +- `OfficeFloorController` +- `OfficePanelsController` + +### 4. `docs: floor schema and builder plan` + +Define the schema before building the editor. + +### 5. `feat: admin floor builder` + +Only after floor metadata/schema stabilizes. + +## Why This Comes First + +The runtime-profile document should land before more implementation work +because it informs both: + +- multi-runtime support +- `claw3doctor` + +Without this, diagnostics and floor binding will keep being designed +against moving assumptions. + +## Summary + +Claw3D should move to a runtime profile model where: + +- providers describe the backend family +- profiles describe named connection targets +- floors bind to profiles + +OpenClaw remains the default. +Hermes adapter remains optional. +Custom runtimes become first-class without special-case UI debt. diff --git a/docs/meeting-room-workflow-spec.md b/docs/meeting-room-workflow-spec.md new file mode 100644 index 0000000..c1185df --- /dev/null +++ b/docs/meeting-room-workflow-spec.md @@ -0,0 +1,390 @@ +# Meeting Room Workflow Spec + +> Third concrete office-system feature for Claw3D, building on existing standup support and extending it into a generalized meeting workflow model. + +## Goal + +Turn the meeting room from a visual location into an operational workflow surface. + +The meeting room should become the place where agents: + +- gather +- present updates +- coordinate plans +- resolve blockers +- record decisions +- create follow-up actions + +## Product Position + +The meeting room is not just a room. + +It is a workflow type. + +That means the system should support: + +- visible in-world gathering +- structured meeting phases +- meeting outputs that affect the rest of the office + +It should connect naturally to: + +- standup +- whiteboard +- bulletin board +- task board +- QA/review systems later + +## Existing Foundation + +Claw3D already has meaningful meeting-related pieces: + +- a meeting room in the office layout +- standup meeting state and API routes +- participant arrival handling +- immersive standup board UI +- agent movement into the meeting area + +This spec should treat standup as the first implemented meeting type, not as a special one-off. + +## Core Principle + +Meetings should generate office state, not just temporary visuals. + +Every meeting should be able to produce: + +- summaries +- decisions +- blockers +- next actions +- linked whiteboard notes +- linked bulletin board items + +That is what makes the office feel alive and useful. + +## Meeting Types + +Recommended initial types: + +- `standup` +- `planning` +- `review` +- `incident` +- `sync` + +### Standup + +Purpose: + +- what each agent is working on +- blockers +- immediate next step visibility + +### Planning + +Purpose: + +- define approach +- compare options +- assign next actions + +### Review + +Purpose: + +- assess work completed +- gather feedback +- approve or reject next move + +### Incident + +Purpose: + +- coordinate under failure or urgency +- assign responsibilities +- capture current status and recovery path + +### Sync + +Purpose: + +- lightweight multi-agent coordination +- brief handoffs +- cross-team visibility + +## Workflow Model + +Each meeting should have explicit phases. + +Suggested phases: + +- `scheduled` +- `gathering` +- `in_progress` +- `decision` +- `complete` +- `archived` + +### Scheduled + +Meeting exists but has not started. + +### Gathering + +Agents are walking to the meeting room or otherwise being assembled. + +### In Progress + +Updates are being presented, questions asked, and information collected. + +### Decision + +The meeting is converging: + +- decisions recorded +- unresolved blockers identified +- next actions prepared + +### Complete + +The outputs are finalized and written back into office systems. + +### Archived + +Meeting is preserved in history but no longer active. + +## Suggested Data Model + +V1 generalized meeting shape: + +```ts +type MeetingType = + | "standup" + | "planning" + | "review" + | "incident" + | "sync"; + +type MeetingPhase = + | "scheduled" + | "gathering" + | "in_progress" + | "decision" + | "complete" + | "archived"; + +type MeetingActionItem = { + id: string; + text: string; + assignedAgentId?: string | null; + linkedTaskId?: string | null; + status: "open" | "done" | "dropped"; +}; + +type MeetingDecision = { + id: string; + text: string; + authorType: "human" | "agent" | "system"; + authorId?: string | null; +}; + +type OfficeMeeting = { + id: string; + type: MeetingType; + phase: MeetingPhase; + title: string; + startedAt?: string | null; + updatedAt: string; + participantAgentIds: string[]; + arrivedAgentIds: string[]; + currentSpeakerAgentId?: string | null; + summary?: string | null; + blockers: string[]; + decisions: MeetingDecision[]; + actionItems: MeetingActionItem[]; + whiteboardDocumentId?: string | null; + bulletinCardIds?: string[]; +}; +``` + +## Relationship To Standup + +The current standup system should become the first meeting implementation under this model. + +That means: + +- keep standup behavior working +- preserve arrival and speaker sequencing +- treat standup as a specialized meeting workflow +- reuse the immersive standup screen as the first meeting immersive view + +In practice: + +- standup = `MeetingType: standup` +- existing standup cards become structured meeting inputs +- standup completion should emit durable outputs into whiteboard and bulletin board systems + +## Whiteboard Integration + +Every meaningful meeting should have a whiteboard relationship. + +Possible behaviors: + +- auto-create whiteboard document when meeting starts +- write summary sections as the meeting progresses +- capture blockers, decisions, and next actions into whiteboard blocks + +Suggested mapping: + +- meeting discussion -> whiteboard notes +- decisions -> whiteboard decision blocks +- next actions -> whiteboard action blocks + +The whiteboard is the drafting surface during the meeting. + +## Bulletin Board Integration + +The bulletin board is the public output surface after the meeting. + +Suggested mapping: + +- important decision -> announcement card +- blocker -> blocker card +- action item with office-wide significance -> handoff card +- meeting completion -> meeting note card + +The meeting room should feed the bulletin board, not bypass it. + +## Task Board Integration + +Meetings should be able to seed or update tasks. + +Examples: + +- planning meeting creates task candidates +- review meeting marks work ready for QA +- incident meeting creates urgent recovery tasks + +The task board remains the detailed execution layer. + +The meeting room creates and updates intent. + +## Human Interaction Model + +The human should be able to: + +- start a meeting +- pick meeting type +- pick participants +- follow progress +- intervene during the meeting +- edit outcomes +- confirm or reject generated next steps + +The user should not lose control over the outputs just because the meeting is agent-driven. + +## Agent Interaction Model + +Agents should be able to: + +- gather into the meeting room +- take speaking turns +- surface blockers +- suggest next steps +- add whiteboard content +- create meeting-derived outputs when allowed + +Longer term, hierarchy may affect who can: + +- call meetings +- approve decisions +- assign action items + +## Visual / Spatial Behavior + +The meeting room should visibly change state during active meetings. + +Possible signals: + +- agents walk to seats +- current speaker highlighting +- board auto-opens or highlights +- room status banner +- meeting timer / phase indicator + +The office should make it obvious that something coordinated is happening. + +## V1 Scope + +V1 should focus on turning standup into the first generalized meeting flow. + +Recommended V1 scope: + +- meeting type abstraction for standup +- whiteboard output on meeting completion +- bulletin board output on meeting completion +- simple action-item capture +- immersive meeting screen improvements + +Do not try to build all meeting types at once. + +## Out of Scope For V1 + +- voice/video simulation +- arbitrary meeting transcripts +- real-time collaborative editing by many actors at once +- department-specific meeting policies +- advanced approval chains +- multiplayer human facilitation + +## Implementation Strategy + +Recommended order: + +1. Generalize standup data model into a broader meeting model. +2. Keep standup UI working on top of that generalized model. +3. Add whiteboard document creation/output for completed meetings. +4. Add bulletin board output for decisions and blockers. +5. Add action item seeding into task workflows. +6. Introduce second meeting type, likely `planning`. + +## Existing Code Seams + +This work should likely align with: + +- `src/features/office/hooks/useOfficeStandupController.ts` +- `src/features/office/screens/StandupImmersiveScreen.tsx` +- `src/app/api/office/standup/*` +- retro office meeting-room positioning and agent movement +- office state persistence + +This is important because Claw3D already has the skeleton of a meeting system. + +The right path is to extend it, not replace it. + +## Success Criteria + +V1 is successful if: + +- standup remains functional +- standup now behaves like the first generalized meeting workflow +- meeting completion can write useful results into whiteboard and bulletin board systems +- users can see meeting outcomes affect the rest of the office +- the system remains backend-neutral + +## Future Extensions + +Once the workflow model is stable, follow-up work can add: + +- planning meetings +- review meetings +- incident rooms +- hierarchy-aware meeting permissions +- department-specific meeting rituals +- richer meeting summaries and archives + +## Summary + +The meeting room should become the office’s coordination engine. + +Standup is the starting point, but the real goal is a general workflow where meetings create durable plans, blockers, decisions, and next actions that shape the whole office. diff --git a/docs/multi-agent-beta.md b/docs/multi-agent-beta.md new file mode 100644 index 0000000..0aa6a0b --- /dev/null +++ b/docs/multi-agent-beta.md @@ -0,0 +1,265 @@ +# Multi-Agent Beta + +This document explains the current multi-agent beta in Claw3D: what it does, how the two connection modes work, and how to connect a second office. + +## What This Beta Does + +Claw3D can render a second office inside the same 3D scene so you can visualize agents from another machine. + +Today the beta supports: + +- showing a second office in the same world; +- displaying remote agents as read-only presence; +- optionally sending a plain-text message to a remote agent; +- keeping the remote side isolated from your local files and office controls. + +This is a beta feature. It is designed for visibility and lightweight cross-office messaging, not full shared-state collaboration. + +## Mental Model + +There are always two roles: + +- **Local office**: the Claw3D instance you are currently using; +- **Remote office**: another Claw3D instance or another OpenClaw gateway you want to visualize. + +The remote office can be connected in one of two ways: + +1. **Remote Claw3D presence endpoint**. +2. **Remote OpenClaw gateway**. + +## Connection Modes + +### 1. Remote Claw3D Presence Endpoint + +Use this when the other machine is also running Claw3D. + +How it works: + +- your local Claw3D server polls the remote Claw3D `presence` endpoint; +- it also tries to load the remote office `layout` snapshot; +- the local 3D scene renders the remote office as a read-only clone inside the same world. + +Typical URL: + +```text +https://other-office.example.com/api/office/presence +``` + +This mode is best when you want the remote side to feel like another full Claw3D office. + +### 2. Remote OpenClaw Gateway + +Use this when the other machine only runs OpenClaw and does not run Claw3D. + +How it works: + +- the browser connects directly to the remote gateway; +- Claw3D derives a read-only presence snapshot from gateway data such as `agents.list`, `status`, and `sessions.preview`; +- because there is no remote Claw3D layout endpoint, the second office uses a fallback office visualization. + +Typical URL: + +```text +ws://remote-host:18789 +``` + +or: + +```text +wss://remote-host.example.com +``` + +If you paste an `http://` or `https://` URL into gateway mode, Claw3D normalizes it to `ws://` or `wss://` before connecting. + +This mode is best when you want remote agent visibility without requiring a second Claw3D deployment. + +## What You Can See + +When the beta is enabled, you can: + +- see a second office in the same environment; +- see remote agents appear in that office; +- see remote agents move and change basic activity state; +- click a remote agent and open a text-only messaging panel. + +## What You Cannot See + +The remote office is intentionally limited. + +You cannot: + +- inspect the remote machine filesystem; +- browse the remote agent chat history in full; +- control the remote office furniture or builder state; +- take over the remote instance as if it were local. + +The goal is cross-office visualization, not remote workstation access. + +## Remote Messaging + +Remote messaging is currently a lightweight relay with two send modes. + +What it does: + +- lets you send a plain-text note to a remote agent; +- lets you choose `direct` or `interval` delivery in the remote chat panel; +- is available from the remote agent chat panel; +- is designed to avoid exposing remote files or tool output in the Claw3D UI. + +`direct` is for one-off pings. + +`interval` is for an ongoing coordination thread where you expect short periodic updates or checkpoints. + +Current limitations: + +- remote replies are not mirrored back into the panel yet; +- the panel currently shows your sent message plus delivery/system feedback; +- this is not a shared transcript viewer. + +## Runtime Message And Handoff Layer + +Under the hood, Claw3D now uses a shared runtime contract for: + +- `agents.message` +- `agents.handoff` + +OpenClaw, Hermes, Demo, and direct custom/local/claw3d runtime profiles can all target the same message/handoff seam. Provider-native adapters such as Anthropic or Claude Code are still a follow-up slice. + +## How To Connect + +### Prerequisites + +Before enabling the second office, make sure: + +- your local Claw3D is already working with your local OpenClaw gateway; +- you know which remote mode you want to use; +- the remote machine is reachable from your machine or browser; +- any required token, origin allowlist, or private-network access is already configured. + +### Setup Steps + +1. Start your local Claw3D instance. +2. Open the office UI. +3. Open the office settings panel. +4. Turn on `Show second office`. +5. Choose the correct `Source type`. +6. Fill the matching connection fields. + +### Setup For `Remote Claw3D presence endpoint` + +Use: + +- `Source type`: `Remote Claw3D presence endpoint`. +- `Presence URL`: the remote `/api/office/presence` URL. +- `Optional token`: only if that remote Claw3D endpoint is protected. + +Example: + +```text +https://other-office.example.com/api/office/presence +``` + +Expected behavior: + +- the second office appears inside the world; +- remote agents show up when the remote office has active presence; +- if the remote layout snapshot is unavailable, Claw3D falls back to a default/fallback office rendering for the remote side. + +### Setup For `Remote OpenClaw gateway` + +Use: + +- `Source type`: `Remote OpenClaw gateway`. +- `Gateway URL`: the remote gateway WebSocket URL. +- `Shared gateway token`: optional when the gateway already allows your Control UI origin and connection model. + +Examples: + +```text +ws://remote-host:18789 +``` + +```text +wss://remote-host.example.com +``` + +Expected behavior: + +- the second office appears inside the world; +- remote agents are derived from gateway presence data; +- the office shell is a fallback visualization, not a true remote layout clone from another Claw3D instance. + +## Recommended Network Patterns + +### Same private network + +Use a reachable private IP or local hostname for the remote Claw3D endpoint or OpenClaw gateway. + +### Tailscale + +Tailscale is a good fit for this beta because it lets both sides connect over a private network without exposing services publicly. + +Common patterns: + +- remote Claw3D endpoint over `https://.ts.net/api/office/presence`; +- remote OpenClaw gateway over `wss://.ts.net` if you are proxying the gateway through HTTPS/WSS; +- direct gateway over `ws://:18789` when both devices can reach the service privately. + +## Disable Behavior + +If you turn `Show second office` off: + +- the extra office should disappear from the 3D scene; +- the path/outdoor connection should disappear; +- remote office presence and layout hooks should stop driving the scene. + +This lets you return to a single-office view. + +## Troubleshooting + +### No remote agents appear + +Check: + +- the remote URL is correct; +- the remote machine is actually reachable; +- the remote service is running; +- the selected `Source type` matches the service you are pointing at. + +### Presence endpoint works but the remote layout does not + +That usually means the other machine has Claw3D presence available but not a layout snapshot yet. The beta should still render a fallback remote office. + +### Gateway mode connects but messaging fails + +In gateway mode, the browser connects directly to the remote gateway. That means the remote gateway may still reject the connection based on origin policy or other gateway-side security rules. + +If that happens, check: + +- the remote gateway URL; +- whether the remote gateway allows your Control UI origin; +- whether the remote gateway expects a token or device-auth flow you have not configured. + +### You can reach an HTTPS page but gateway mode still fails + +Opening a web page in the browser does not automatically mean the OpenClaw gateway WebSocket is reachable. + +Examples: + +- `https://host` may be reachable while `ws://host:18789` is not; +- a website reverse proxy may exist even though the raw gateway port is closed; +- the remote side may need a dedicated WSS proxy path for the gateway. + +## Current Beta Limitations + +- The second office is read-only. +- Remote replies are not mirrored into the local remote-chat panel yet. +- Gateway mode derives presence from gateway snapshots rather than a real remote Claw3D layout. +- Browser-based gateway mode depends on the remote gateway allowing the connection from your Control UI origin. +- This feature is still evolving and should be treated as beta, not final production-grade multi-tenant collaboration. + +## Summary + +Use `Remote Claw3D presence endpoint` when the other side runs Claw3D and you want the most complete office visualization. + +Use `Remote OpenClaw gateway` when the other side only runs OpenClaw and you mainly want remote agent presence plus lightweight text messaging. diff --git a/docs/office-systems-roadmap.md b/docs/office-systems-roadmap.md new file mode 100644 index 0000000..edfac90 --- /dev/null +++ b/docs/office-systems-roadmap.md @@ -0,0 +1,349 @@ +# Office Systems Roadmap + +> Product roadmap for turning Claw3D from a gateway visualizer into a living agent operations environment. + +## Core Direction + +Claw3D should keep users inside the office. + +That means companion tools should be brought into the space as rooms, surfaces, devices, and shared systems instead of pulling users out into separate interfaces. + +The guiding principle is: + +- do not spawn Claw3D inside another tool +- bring the other tool into Claw3D + +This is especially relevant for ideas like Moltbook. The better version is not "leave Claw3D to use Moltbook". The better version is: + +- a bulletin board in the office +- a whiteboard in meeting rooms +- a desk computer app +- a shared intranet terminal +- a wall display in common spaces + +## Product Goal + +Claw3D should evolve into an agent operations environment with: + +- visual presence +- planning and task coordination +- meetings and handoffs +- review and QA +- hierarchy and permissions +- workplace state +- progression and identity + +The office should feel like a real place where work happens, not only a dashboard for remote agent calls. + +## Design Principles + +- Keep primary workflows in-world when possible. +- Prefer physical metaphors that make the office easier to understand. +- Separate real operational systems from cosmetic flavor. +- Build useful features first, then layer on simulation and style. +- Preserve backend neutrality so these systems work across OpenClaw, Hermes, Vera, and future providers. + +## V1: Useful Office Systems + +These should be the first systems because they add product value immediately and fit the existing office concept naturally. + +### Bulletin Board + +Purpose: + +- shared goals +- current sprint priorities +- blockers +- announcements +- handoff notes + +Possible behaviors: + +- sticky notes or task cards pinned by agents or humans +- cards linked to sessions, agents, or tasks +- quick visibility into what the office is trying to accomplish + +Why it matters: + +- low ambiguity +- high utility +- strong visual fit for the office + +### Whiteboard + +Purpose: + +- brainstorming +- architecture notes +- meeting notes +- rough plans +- idea capture + +Possible behaviors: + +- text notes +- grouped cards +- simple sketches or structured plan areas +- human and agent authored content + +Why it matters: + +- good bridge between conversation and execution +- natural place for planning artifacts + +### Meeting Room Workflows + +Purpose: + +- standups +- planning +- coordination +- decision making +- status reviews + +Possible behaviors: + +- gather selected agents into a meeting +- produce summary, decisions, and next actions +- write results to bulletin board or whiteboard +- trigger structured follow-up tasks + +Why it matters: + +- gives multi-agent coordination a visible home +- makes the office feel operational instead of decorative + +### QA Department + +Purpose: + +- review +- testing +- bug triage +- release readiness + +Possible behaviors: + +- route tasks or runs to QA agents +- visualize test queues +- track failures and review outcomes +- require QA signoff before release-style actions + +Why it matters: + +- this is real product value, not only flavor +- it matches how users already think about software teams + +### Desk / CPU Progression + +Purpose: + +- make role maturity visible +- tie capability to office presence + +Possible behaviors: + +- interns start with minimal desk access +- probationary agents have limited tools or workspace +- promoted agents unlock desk computers, tools, or context budget +- contractors get restricted environments + +Why it matters: + +- strong visual progression +- easy to understand +- creates room for permissions and capability systems later + +## V2: Management Systems + +These systems add organizational structure once the basic office workflows are useful. + +### Hierarchy + +Possible levels: + +- human owner +- CEO / lead orchestrator +- managers / bosses +- employees +- contractors +- interns + +Possible effects: + +- delegation rights +- approval authority +- visibility across teams +- access to spaces and tools + +### Departments + +Examples: + +- Engineering +- QA +- Research +- Ops +- Design +- Support + +Possible effects: + +- room ownership +- task routing +- dashboards by department +- workload balancing + +### Permission Lanes + +Possible controls: + +- context budget +- tool access +- file access +- approval requirements +- concurrency +- agent spawning / dismissal rights + +Why it matters: + +- lets the office represent real operational constraints +- reduces "all agents are identical" flatness + +### Office Rituals + +Examples: + +- daily standup +- sprint planning +- review/demo +- retrospective +- incident response + +Why it matters: + +- converts routine coordination into visible office behavior + +## V3: Simulation Systems + +These are the fun layers, but they should sit on top of useful product systems rather than replace them. + +### Agent State Model + +Avoid fake emotions first. Start with operational states: + +- focused +- idle +- blocked +- overloaded +- waiting +- cooling down +- degraded + +Possible effects: + +- response speed +- delegation tendency +- context budget +- summarization pressure +- task throughput + +This can later evolve into a more playful "wellbeing" or "comfort" layer without losing technical meaning. + +### Workplace Culture + +Examples: + +- recognition +- probation periods +- promotions +- competitions +- events + +Use carefully: + +- good for flavor and identity +- should not obscure the operational state of the system + +### Shared Office Memory + +Examples: + +- bulletin archives +- meeting minutes +- org notes +- playbooks +- team history + +Why it matters: + +- gives the office continuity across sessions +- helps explain why teams get better over time + +## Moltbook Integration Direction + +Moltbook should be integrated into Claw3D, not the other way around. + +Best forms: + +- office bulletin board +- intranet terminal +- desk CPU app +- wall monitor +- break-room or lobby information surface + +Bad form: + +- forcing users to leave Claw3D for core team coordination workflows + +The office should remain the primary interaction layer. + +## Candidate Feature Order + +Recommended sequence: + +1. Bulletin board +2. Whiteboard +3. Meeting room workflows +4. QA department +5. Desk / CPU progression +6. Hierarchy and departments +7. Agent operational state model +8. Culture / sim systems +9. Theme skins + +## Theme / Skin Strategy + +Skins should come after the office has enough systems worth skinning. + +Mechanics should stay consistent while art, labels, props, and room names vary. + +Possible theme packs: + +- Office Space +- The Office +- Parks & Rec +- The I.T. Crowd + +Examples: + +- conference room becomes town hall, bullpen, annex, or ops room +- bulletin board becomes notice board, incident wall, municipal board, or sprint wall +- QA area becomes testing lab, audit desk, or review bullpen + +## Immediate Next Deliverables + +If this roadmap is used for implementation planning, the best next concrete docs/tasks are: + +1. Bulletin board system spec +2. Whiteboard interaction spec +3. Meeting room workflow spec +4. QA department workflow spec + +Those four would create the strongest foundation for future hierarchy, progression, and simulation layers. + +## Summary + +Claw3D gets stronger when the office becomes the place where work actually happens. + +The best next step is not expanding external tooling around the office. It is bringing planning, meetings, reviews, and shared memory into the office itself. diff --git a/docs/office_sys/multi-floor-runtime-architecture.md b/docs/office_sys/multi-floor-runtime-architecture.md new file mode 100644 index 0000000..97db5af --- /dev/null +++ b/docs/office_sys/multi-floor-runtime-architecture.md @@ -0,0 +1,641 @@ +# Multi-Floor Runtime Architecture + +> Architecture note for evolving Claw3D from single-runtime switching into one persistent building with multiple runtime-backed floors. + +## Goal + +Claw3D should move from: + +- one selected runtime at a time + +to: + +- one building shell +- multiple floors +- one runtime binding per floor +- one or more floors active in the same session +- persistent roster/state per floor +- controlled cross-floor interaction + +This is the bridge from the merged runtime seam work into Office Systems. + +## Product Model + +The user should think in places, not provider toggles. + +Examples: + +- `Lobby` + - onboarding, demo, reception, visitor flow +- `OpenClaw Floor` + - default upstream team +- `Hermes Floor` + - supervisor / orchestration team +- `Custom Floor` + - downstream/orchestrator/runtime experiments +- `Training Floor` + - classrooms, auditorium, distillation labs, evals, coaching, simulations +- `Trader's Floor` + - event streams, signals, analyst desks, execution pits +- `Outside / Campus` + - stadium, events, unlockables, public scenes + +Additional future departments: + +- `War Room` + - incident response, debugging, approvals, ops escalation +- `R&D Lab` + - prompt experiments, model comparisons, benchmarks +- `Legal / Compliance` + - permissions, policies, audit trails +- `Studio / Broadcast Room` + - demos, presentations, voice/video outputs +- `Watercooler / Commons` + - intentional cross-agent cross-talk space + +## Core Principles + +- One runtime per floor. +- One shared building shell above all floors. +- Floor state is persistent and local to that floor. +- Building systems are shared and runtime-neutral. +- Cross-floor coordination is explicit, not accidental. +- The gateway/runtime remains the source of truth for runtime-owned data. +- Floor switching owns the connection lifecycle for that floor. + +## Why Floors + +Floors solve several problems at once: + +- they preserve backend neutrality +- they prevent multi-runtime support from flattening into one undifferentiated roster +- they make agent origin legible to the user +- they let Office Systems map naturally onto place +- they create a clean future path for cross-runtime coordination + +Instead of "choose one provider", the user can think: + +- OpenClaw is downstairs +- Hermes is on the first floor +- Custom is upstairs +- Demo starts in the lobby + +## Building Layers + +### 1. Building Shell + +Persistent across the whole app: + +- top-level navigation +- player identity +- building map / floor switcher +- building-wide settings +- shared event feed +- shared progression/unlocks +- common Office Systems surfaces + +This layer should not depend on one runtime being selected. + +### 2. Floor Runtime Surface + +Owned per floor: + +- provider binding +- runtime profile and connection settings +- connection status and error state +- hydrated roster for that floor +- floor-local room state +- floor signage / presentation metadata + +### 3. Shared Building Systems + +Runtime-neutral systems that can reference one or many floors: + +- bulletin board +- whiteboard +- meeting rooms +- QA systems +- approvals +- shared announcements +- watercooler / commons + +### 4. Cross-Floor Coordination + +Later-phase systems: + +- cross-floor messaging +- supervisor handoff chains +- dispatch boards +- agent encounter rules +- multi-floor meetings + +## Runtime Rules + +Each floor has exactly one runtime binding at a time. + +Examples: + +- `openclaw-ground` + - provider: `openclaw` +- `hermes-first` + - provider: `hermes` +- `custom-second` + - provider: `custom` +- `demo-lobby` + - provider: `demo` + +A floor can be: + +- configured but disconnected +- connecting +- connected +- errored + +Multiple floors may be loaded in the same session, but they should not share runtime connection state. + +When the user switches to another runtime-backed floor: + +- the shell should keep the building mounted +- the current runtime should disconnect if the target floor uses a different transport +- the next floor should connect using that floor's saved runtime profile +- the floor label should not get ahead of the actual runtime handoff +- reconnect churn should collapse into one transition state instead of flashing through multiple disconnected/connecting states + +## State Ownership + +### Runtime-owned + +Still owned by the runtime/gateway: + +- agent records +- sessions +- approvals +- runtime files +- runtime event streams + +### Studio-owned + +Local Claw3D state should own: + +- floor registry +- active floor +- saved runtime profile per floor +- last-known-good profile per floor +- floor-local presentation preferences +- building-level Office Systems state + +This follows the existing architecture boundary in [ARCHITECTURE.md](/c:/Users/G/Desktop/Builds/sigilnet/isolation/Claw3D/ARCHITECTURE.md): Claw3D should not become the system of record for runtime agent state. + +## Floor Registry + +The first concrete implementation step should be a floor registry. + +Required fields: + +- floor id +- label +- provider +- zone / level kind +- connection profile key +- whether the floor is enabled + +Suggested shape: + +```ts +type FloorProvider = "openclaw" | "hermes" | "custom" | "demo"; + +type FloorId = + | "lobby" + | "openclaw-ground" + | "hermes-first" + | "custom-second" + | "training" + | "traders-floor" + | "campus"; + +type FloorDefinition = { + id: FloorId; + label: string; + provider: FloorProvider; + kind: "core" | "support" | "simulation" | "outside"; + enabled: boolean; + runtimeProfileId: string | null; +}; +``` + +## Persistent Per-Floor Runtime State + +This should be the first real implementation slice after the doc. + +Each floor needs persistent local state for: + +- selected runtime profile +- last-known-good connection profile +- connection status +- recent connect error +- last successful roster snapshot metadata + +Suggested shape: + +```ts +type FloorRuntimeState = { + floorId: FloorId; + provider: FloorProvider; + runtimeProfileId: string | null; + gatewayUrl: string | null; + status: "disconnected" | "connecting" | "connected" | "error"; + lastKnownGoodAt: number | null; + lastErrorCode: string | null; + lastErrorMessage: string | null; +}; +``` + +Important rule: + +- floor-local runtime state should not be overwritten by switching to another floor +- switching floors should not leave the previous runtime active under the next floor's label + +## PR Breakdown + +Office Systems should ship as a sequence of narrow PRs, not one long-running mega branch. + +Recommended slices: + +1. `office: add floor registry and canonical floor definitions` + - floor ids + - provider/kind definitions + - registry helpers + +2. `office: persist per-floor runtime state` + - floor-local runtime profile binding + - connection status + - recent error + - last-known-good metadata + +3. `office: add per-floor roster hydration` + - one roster cache per floor + - runtime-neutral hydration entry points + +4. `office: add building shell floor switcher` + - active floor selection + - shell navigation + - floor-local presentation handoff + +5. `office: add cross-floor messaging primitives` + - explicit inter-floor message model + - supervisor handoff + - shared commons channels + +6. `office: add higher-level Office Systems features` + - training + - trader's floor + - war room + - bulletin/meeting systems + +7. `office: integrate campus and specialized environments` + - stadium / outside campus + - specialized booths and labs + +## Current Implementation Status + +Implemented in the current Office Systems foundation slice: + +- `1. floor registry and canonical floor definitions` + - canonical floor ids + - provider/kind definitions + - enabled-floor helpers +- `2. persistent per-floor runtime state` + - persisted floor-local runtime profile binding + - connection status + - recent error state + - last-known-good metadata +- `3. per-floor roster hydration` + - one roster cache per floor + - runtime-neutral hydration/state builders + - preserved runtime/identity/session display-name provenance +- `4. building shell floor switcher` + - persisted `activeFloorId` + - enabled-floor switching helpers + - shell-level floor picker in OfficeScreen + - floor-local roster status surfaced in the shell + +Explicitly deferred from this slice: + +- cross-floor messaging +- supervisor handoff chains +- shared commons/watercooler traffic +- specialized floor systems like Training, Trader's Floor, and Campus gameplay + +Reason for deferral: + +- cross-agent messaging primitives should be tightened first +- then cross-floor messaging can build on a cleaner interaction model + +## Multi-Provider Roster Loading + +Today Claw3D mostly thinks in one active roster. + +The next model should be: + +- one roster per floor +- one hydration pipeline per floor +- one selected active floor in the UI + +Suggested shape: + +```ts +type FloorRosterEntry = { + id: string; + displayName: string; + runtimeName: string | null; + identityName: string | null; + sessionDisplayName: string | null; + role?: string | null; + status: "idle" | "running" | "error"; +}; + +type FloorRosterState = { + floorId: FloorId; + loadedAt: number | null; + entries: FloorRosterEntry[]; +}; +``` + +This matches recent runtime work: + +- preserve useful runtime and identity metadata +- do not throw away `runtimeName`, `identityName`, or `sessionDisplayName` + +## Building Shell vs Floor Scene + +The office should split into: + +### Building shell + +- navigation +- floor switcher +- global overlays +- building systems surfaces + +### Floor scene + +- runtime-backed roster +- room layout for that floor +- floor-local devices and props +- floor-local agent simulation + +That prevents reconnecting or swapping floors from feeling like the whole app is remounting. + +## Cross-Floor Messaging Model + +Cross-floor coordination should be explicit. + +Do not infer it from raw runtime adjacency. + +Recommended primitives: + +- handoff board +- floor inbox +- supervisor dispatch +- meeting invite +- commons encounter + +Minimal event shape: + +```ts +type CrossFloorMessage = { + id: string; + fromFloorId: FloorId; + fromAgentId: string; + toFloorId: FloorId; + toAgentId: string | null; + kind: "handoff" | "request" | "broadcast" | "meeting-invite"; + subject: string; + body: string; + createdAt: number; +}; +``` + +Important rule: + +- cross-floor messaging is a building system +- it should not require editing runtime config files directly + +## Office Systems Fit + +This architecture is meant to support the Office Systems roadmap, not compete with it. + +Good examples: + +- `Lobby` + - onboarding, demo, reception +- `Training Floor` + - classrooms, evals, replay, distillation +- `Trader's Floor` + - feeds, signals, alerts, analyst desks +- `Outside / Campus` + - stadium and event spaces + +The pending stadium PR [#88](https://github.com/iamlukethedev/Claw3D/pull/88) should be treated as a future `Outside / Campus` scene, not as a blocker for the core floor/runtime model. + +## Progression / Unlocks + +Possible progression model: + +- first login + - lobby only +- after first runtime setup + - OpenClaw floor +- after multi-runtime setup + - Hermes floor +- after usage thresholds + - Training floor +- later milestones + - Trader's floor + - Campus / stadium + +Possible unlock outputs: + +- floor access +- room access +- signage themes +- team/floor colors +- props and trophies + +## Recommended Implementation Order + +1. Finalize multi-floor architecture doc +2. Add floor registry model +3. Add persistent per-floor runtime state +4. Add multi-provider roster loading +5. Add building shell + floor switcher +6. Add cross-floor messaging primitives +7. Build Office Systems on top + +This keeps floors foundational, and avoids building bulletin boards / meetings / QA on top of a single-runtime assumption that will just need to be broken later. + +## Concrete Delivery Plan + +### Phase 1: Floor Registry + +Deliverables: + +- define canonical `FloorId` and `FloorProvider` types +- add a floor definition registry in Studio-owned state +- mark which floors are enabled, core, support, simulation, or outside +- add runtime profile linkage per floor + +Acceptance criteria: + +- Claw3D can enumerate all known floors without connecting to any runtime +- floor definitions are runtime-neutral and local-state only +- the building shell can reference floor labels and kinds without depending on roster data + +### Phase 2: Persistent Per-Floor Runtime State + +Deliverables: + +- store connection/runtime profile state per floor +- persist `lastKnownGood` per floor +- persist per-floor gateway URL/token profile linkage +- preserve connection errors per floor instead of one global connection slot + +Acceptance criteria: + +- switching floors does not wipe another floor’s runtime state +- reconnecting one floor does not reset another floor +- Claw3D can show disconnected/configured/connected/errored state per floor +- moving from one runtime floor to another reconnects against the target runtime before the floor is treated as live + +### Phase 3: Per-Floor Roster Hydration + +Deliverables: + +- hydrate one roster per floor +- preserve `runtimeName`, `identityName`, and `sessionDisplayName` in roster entries +- cache roster load metadata per floor +- add floor-local selected agent state + +Acceptance criteria: + +- multiple floors can have rosters loaded in the same session +- roster entries remain associated with their owning floor +- the UI can distinguish local-floor vs other-floor agent origin cleanly + +### Phase 4: Building Shell + Floor Switcher + +Deliverables: + +- add building map / floor switcher UI +- keep shell mounted while changing floors +- render active floor scene without remounting global app state +- make lobby and campus valid destinations even before all rooms are implemented + +Acceptance criteria: + +- floor switching is UI-stateful, not route-destructive +- the shell remains stable while floor scenes swap +- disconnected floors remain visible as places, not absent data +- runtime-backed floors enter through a transition/arrival flow, not by silently reusing the previous floor's live runtime + +### Phase 5: Cross-Floor Coordination Primitives + +Deliverables: + +- define handoff board / floor inbox / supervisor dispatch primitives +- add message/event records with source floor and target floor +- support explicit cross-floor meeting invites or requests + +Acceptance criteria: + +- cross-floor actions are visible building events +- routing is explicit, not inferred from hidden runtime config +- Hermes supervising OpenClaw can be modeled as a building behavior + +### Phase 6: Office Systems on Top + +Deliverables: + +- lobby onboarding +- training rooms +- trader floor / specialized rooms +- QA / meetings / bulletin systems +- outside campus and stadium integration + +Acceptance criteria: + +- Office Systems are built against the building/floor model +- room features do not assume single-runtime global state +- specialized rooms remain optional extensions, not core architecture blockers + +## Immediate Implementation Checklist + +### Floor Registry Slice + +- add `FloorId`, `FloorProvider`, and `FloorDefinition` types +- create a canonical floor registry module +- include at least: + - `lobby` + - `openclaw-ground` + - `hermes-first` + - `custom-second` + - `training` + - `traders-floor` + - `campus` +- decide where floor registry state lives inside Studio settings/local state + +### Per-Floor Runtime State Slice + +- define `FloorRuntimeState` +- store runtime profile key per floor +- store connection status per floor +- store last-known-good timestamp per floor +- store last error code/message per floor + +### Roster Slice + +- define `FloorRosterEntry` +- define `FloorRosterState` +- preserve runtime/identity/session naming metadata +- keep floor-local selected agent state + +### UI Shell Slice + +- add a floor switcher stub in the building shell +- keep current office scene as one floor implementation first +- do not attempt full cross-floor scene rendering in the first pass + +## Reference Branches + +Use these as references, not merge targets for the foundational slice: + +- `upstream/soccer-stadium-outside-office` + - reference for `Outside / Campus` + - useful for environment/scene ideas +- `upstream/feature/crypto-booth` + - reference for specialized room/department patterns + - useful later for `Trader's Floor` or a market/crypto room + +The foundational multi-floor work should still be built from current `upstream/main`, not from either feature branch. + +## Immediate Non-Goals + +Not for the first slice: + +- full cross-floor conversation simulation +- automatic agent movement across floors +- deep unlock/economy system +- multi-user tenancy +- replacing the runtime as system of record + +## Summary + +Claw3D should evolve into: + +- one building shell +- multiple runtime-backed floors +- one roster per floor +- persistent floor-local state +- shared building-native Office Systems + +That gives the project a clean path from merged runtime support into real Office Systems without collapsing everything back into one flat provider toggle. diff --git a/docs/office_sys/office-systems-roadmap.md b/docs/office_sys/office-systems-roadmap.md index edfac90..439c9fd 100644 --- a/docs/office_sys/office-systems-roadmap.md +++ b/docs/office_sys/office-systems-roadmap.md @@ -43,6 +43,102 @@ The office should feel like a real place where work happens, not only a dashboar - Build useful features first, then layer on simulation and style. - Preserve backend neutrality so these systems work across OpenClaw, Hermes, Vera, and future providers. +## External Validation + +Recent production-user feedback strongly reinforces the current direction. + +What that feedback confirms: + +- multi-agent visibility is the core differentiator +- agent state to animation mapping should become config/schema driven +- mobile-first support matters for real demos and daily use +- subtle sound design and event cues add operational value, not just polish +- enterprise auth and reverse-proxy compatibility matter for adoption +- external event webhooks fit naturally into the office as world reactions + +This means the roadmap should not treat those as side quests. They are +adoption-critical support for the office-as-operations-center model. + +## Current Post-Merge Sequence + +After the recent hardening and runtime work, the next sequence should be: + +1. runtime profile architecture +2. `claw3doctor` +3. `OfficeScreen` modularization +4. floor schema and builder plan +5. admin floor builder + +That sequence keeps the base stable before adding more office complexity. + +## Supporting Product Lanes + +### Mobile-First Office + +The office should work well on phones and tablets, not just scale down. + +Implications: + +- floor navigation and shell chrome must collapse cleanly +- touch navigation should be intentional +- control surfaces should avoid desktop-only assumptions + +### State-To-Animation Mapping + +Agent operational state should not be locked in source code. + +Target direction: + +- state -> animation mapping should be data driven +- operator-facing config should define mappings like: + - idle + - writing + - executing + - syncing + - error + +This will matter for adoption across teams with different runtime semantics. + +### Sound And Event Cues + +Office audio should be treated as operational feedback, not decoration. + +Good first examples: + +- subtle office ambience +- start/finish cues +- warning/alarm cues for failures +- event-based celebratory cues + +### Auth And Enterprise Adoption + +Enterprise deployments will often sit behind: + +- `oauth2-proxy` +- Entra / OIDC +- reverse proxies +- HTTPS termination + +That means Claw3D should continue improving: + +- documented auth integration patterns +- public-host hardening +- secure-context guidance +- proxy compatibility + +### Event Ingress / Webhooks + +External systems should be able to push state into the office. + +Examples: + +- CI failure -> alarm in the server room +- new customer onboarded -> front-desk cue +- CRM event -> bulletin board or celebration event + +This fits naturally with the office model and should become a first-class +integration surface later. + ## V1: Useful Office Systems These should be the first systems because they add product value immediately and fit the existing office concept naturally. @@ -312,6 +408,18 @@ Recommended sequence: 8. Culture / sim systems 9. Theme skins +## Platform Prerequisites + +These platform lanes should keep moving in parallel with the office feature +roadmap because they directly affect adoption and maintainability: + +1. runtime profile architecture +2. `claw3doctor` +3. `OfficeScreen` modularization +4. mobile shell work +5. auth/proxy deployment guidance +6. event ingress/webhook planning + ## Theme / Skin Strategy Skins should come after the office has enough systems worth skinning. @@ -335,12 +443,42 @@ Examples: If this roadmap is used for implementation planning, the best next concrete docs/tasks are: -1. Bulletin board system spec -2. Whiteboard interaction spec -3. Meeting room workflow spec -4. QA department workflow spec +1. Runtime profile architecture doc +2. `claw3doctor` implementation +3. `OfficeScreen` modularization plan and extraction work +4. Floor schema and builder plan +5. Bulletin board system spec +6. Whiteboard interaction spec +7. Meeting room workflow spec +8. QA department workflow spec -Those four would create the strongest foundation for future hierarchy, progression, and simulation layers. +Those create the strongest foundation for future hierarchy, progression, +simulation layers, and enterprise-ready adoption. + +## Diagnostics Roadmap Split + +To keep diagnostics aligned with the platform work without turning them into +an unbounded tooling branch, treat `claw3doctor` in two phases: + +### `claw3doctor` v1 + +- first-pass deployment diagnostics +- runtime profile awareness +- grouped terminal output +- JSON output +- OpenClaw / Hermes / demo / custom provider checks +- tunnel, auth, and close-code remediation + +### `claw3doctor` v2 + +- deeper OpenClaw pairing/device heuristics +- stronger Cloudflare / Tailscale / reverse-proxy fingerprints +- richer export and issue-bundle workflows +- in-app diagnostics surface later +- floor-to-profile and multi-runtime diagnostics once runtime binding matures + +This keeps the current branch reviewable while preserving the next layer of +operator-focused work. ## Summary diff --git a/docs/qa-department-spec.md b/docs/qa-department-spec.md new file mode 100644 index 0000000..ca5c9b0 --- /dev/null +++ b/docs/qa-department-spec.md @@ -0,0 +1,464 @@ +# QA Department Spec + +> Fourth concrete office-system feature for Claw3D, completing the first real office loop: plan, coordinate, execute, review. + +## Goal + +Add a QA department workflow to Claw3D so the office can visibly review, test, triage, and sign off on work before it is treated as complete. + +The QA department should make review state legible in-world. + +It is where the office asks: + +- does this actually work? +- what failed? +- what is blocked? +- what is safe to ship? + +## Product Position + +QA should not be just flavor. + +It should be an operational system that connects: + +- tasks +- agent work output +- reviews +- approvals +- regressions +- release-readiness + +The QA department is the office’s verification layer. + +## Why This Feature Matters + +Without a QA layer, the office can generate and coordinate work but not convincingly validate it. + +QA adds: + +- visible review state +- feedback loops +- bug triage +- approval pressure where needed +- a clearer path from "done writing" to "done safely" + +It also pairs naturally with: + +- bulletin board blockers +- meeting room review workflows +- task board status +- approval systems + +## Core Responsibilities + +The QA department should handle: + +- review intake +- test/result tracking +- bug triage +- regression visibility +- release gate / readiness signal + +## Primary Use Cases + +### Review Queue + +Examples: + +- a task is ready for QA +- an agent requests review +- a release candidate needs signoff + +### Bug Triage + +Examples: + +- classify failures +- route issues to the right owner +- mark severity +- surface blockers to the office + +### Regression Detection + +Examples: + +- recent change broke existing behavior +- previously passing workflow now fails +- approval flow or adapter integration regressed + +### Approval-Aware Review + +Examples: + +- code/run needs human approval before release-like action +- QA can recommend approval but not finalize it +- owners or leads can override or sign off + +### Release Readiness + +Examples: + +- green / yellow / red office-level signal +- unresolved blockers prevent completion +- review summary appears on bulletin board + +## V1 Scope + +V1 should focus on clear office-level QA workflows, not a full CI system. + +Recommended V1 scope: + +- QA queue +- QA status per task or work item +- bug / blocker recording +- review outcome states +- office-visible readiness signal + +## Suggested Workflow Model + +Recommended QA states: + +- `queued` +- `in_review` +- `changes_requested` +- `blocked` +- `approved` +- `failed` +- `verified` + +### Queued + +Work has entered QA but has not been actively reviewed yet. + +### In Review + +A QA agent or human reviewer is assessing the work. + +### Changes Requested + +Work is not acceptable yet and must be revised. + +### Blocked + +QA cannot proceed because a dependency, approval, or missing artifact prevents review. + +### Approved + +Review is positive, but final release/ship behavior may still depend on a higher-level approval model. + +### Failed + +Verification found concrete failure. + +### Verified + +The work passed the required QA checks and is complete from the department’s perspective. + +## Suggested Data Model + +V1 shape: + +```ts +type QaStatus = + | "queued" + | "in_review" + | "changes_requested" + | "blocked" + | "approved" + | "failed" + | "verified"; + +type QaSeverity = "low" | "medium" | "high" | "critical"; + +type QaIssue = { + id: string; + title: string; + body: string; + severity: QaSeverity; + createdAt: string; + updatedAt: string; + authorType: "human" | "agent" | "system"; + authorId?: string | null; + linkedTaskId?: string | null; + linkedAgentId?: string | null; + linkedSessionKey?: string | null; + resolved: boolean; +}; + +type QaReviewItem = { + id: string; + title: string; + status: QaStatus; + createdAt: string; + updatedAt: string; + assignedReviewerAgentId?: string | null; + linkedTaskId?: string | null; + linkedAgentId?: string | null; + linkedSessionKey?: string | null; + summary?: string | null; + issues: QaIssue[]; +}; + +type QaDepartmentState = { + items: QaReviewItem[]; + readiness: "green" | "yellow" | "red"; + updatedAt?: string; +}; +``` + +## Relationship To Existing Systems + +The QA department should plug into systems Claw3D already has. + +### Task Board / Kanban + +The QA department should consume work from the task board. + +Examples: + +- task moves into a review-ready state +- QA item is created or updated +- blocked QA creates blocker visibility back on the bulletin board + +Suggested relationship: + +- task board = execution status +- QA department = verification status + +### Bulletin Board + +The bulletin board should show the important QA outcomes. + +Examples: + +- "Build blocked on QA" +- "Regression found in Hermes adapter flow" +- "Release candidate verified" + +Suggested card mapping: + +- critical QA issue -> blocker card +- release-ready signal -> announcement card +- changes requested -> handoff card + +### Meeting Room + +Review meetings should naturally feed into QA. + +Examples: + +- planning meeting creates work +- execution completes +- review meeting sends selected items into QA +- QA findings can be discussed in a follow-up review meeting + +This makes the meeting room and QA department part of one loop instead of separate ideas. + +### Approvals + +Claw3D already has approval-related surfaces. + +The QA department should integrate with them conceptually, even if V1 is mostly local office state. + +Important distinction: + +- QA approval = "this looks good from verification" +- release approval = "a human or higher authority allows the next action" + +Those are related but not identical. + +### GitHub / Review Surfaces + +Claw3D already has review-adjacent UI, including GitHub-oriented immersive screens. + +The QA department should be able to: + +- reflect review outcomes +- ingest review summaries +- show whether work is waiting for review or returned with changes requested + +## In-World UX + +The QA department should feel like a place in the office. + +Possible visual forms: + +- QA lab +- testing bullpen +- release desk +- audit wall + +Behavior: + +- queue visible in-world +- blocked items stand out clearly +- verified items visibly clear from the queue +- readiness state visible at a glance + +The room should communicate office health, not just hold another panel. + +## Secondary UI + +Also provide a non-spatial UI surface. + +Good options: + +- HQ sidebar panel +- immersive QA screen +- release/readiness panel + +Users should be able to inspect: + +- queued reviews +- open issues +- who owns each item +- overall readiness state + +## V1 Automation + +Useful automations: + +- create a QA item when a task enters review-ready state +- create blocker cards for high-severity QA issues +- update readiness color based on unresolved critical/high issues +- generate a short QA summary when an item leaves review + +Keep automation conservative. + +Avoid flooding the system with low-value noise. + +## Storage Model + +V1 can be stored in office preferences, similar to bulletin board and whiteboard systems. + +Suggested shape: + +```ts +type OfficePreference = { + qaDepartment?: QaDepartmentState; +}; +``` + +This keeps the feature: + +- backend-neutral +- easy to persist +- easy to evolve later + +## Human Interaction Model + +The human should be able to: + +- open the QA queue +- inspect a review item +- mark status changes +- add issues +- resolve issues +- promote or reject readiness + +Humans should remain the final arbiter when needed, especially for ship/release-style outcomes. + +## Agent Interaction Model + +QA agents should be able to: + +- review work items +- generate findings +- summarize likely regressions +- mark items as changes requested or verified +- surface blockers + +Longer term: + +- specialized QA agents may exist by area +- adapter QA +- UI QA +- release QA +- regression QA + +## Readiness Signal + +The department should publish an office-level readiness state: + +- `green` +- `yellow` +- `red` + +Suggested meaning: + +- green = no blocking QA issues +- yellow = warnings / pending review / moderate unresolved issues +- red = blocking failures or critical unresolved issues + +This signal should be visible outside the QA room as well. + +For example: + +- bulletin board card +- office status banner +- release desk indicator + +## Out of Scope For V1 + +Do not include these initially: + +- full CI orchestration +- external test runner infrastructure +- rich flake analytics +- cross-repo release orchestration +- advanced approval hierarchies +- fully automated release pipelines + +V1 should be office workflow first. + +## Implementation Strategy + +Recommended order: + +1. Define QA review item and issue schema. +2. Add local persisted QA department state. +3. Build a simple QA queue panel. +4. Add readiness signal. +5. Connect task board / review-ready states to QA queue creation. +6. Emit bulletin board blockers or announcements from QA outcomes. + +## Existing Code Seams + +This work should likely align with: + +- task board state and transitions +- approval/review UI surfaces +- GitHub immersive review screens +- office performance / approvals analytics +- bulletin board and meeting room outputs from the new docs + +The key is to avoid building QA as an isolated toy feature. + +It should be another operational loop in the same office system. + +## Success Criteria + +V1 is successful if: + +- the office can visibly route work into QA +- QA findings can block or clear work in a legible way +- users can inspect review items and issues +- readiness state is visible at the office level +- QA outcomes can feed the bulletin board + +## Future Extensions + +Once V1 is stable, follow-up work can add: + +- QA meeting rituals +- release room / release wall +- specialized QA subteams +- automated regression summaries +- richer review analytics +- policy-aware signoff chains + +## Summary + +The QA department should make verification a first-class part of office life. + +It closes the loop between planning, execution, and trustworthy completion. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..5026b11 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,61 @@ +# Roadmap + +This file captures the near-term direction for Claw3D so outside contributors can find work that aligns with current priorities. + +## Now + +- Open-source readiness: documentation, support routes, CI, disclosure files, and public-safe defaults. +- Runtime reliability: making gateway event handling, history reconciliation, and transport-specific recovery more predictable. +- Office architecture clarity: keeping the office intent layer centralized and reducing ad hoc room-specific behavior. + +## Next + +- Converge the immersive office and builder stack on a clearer shared model. +- Replace or fully clear unresolved bundled assets and dependency licensing risks. +- Improve security posture around Studio access bootstrap and runtime token handling. +- Expand runtime profiles from the new shared `agents.message` / `agents.handoff` contract into fuller provider adapters and richer multi-agent handoff flows. + +## Later + +- Broader office authoring workflows and richer world-building tools. +- Better contributor automation, release process, and publication tooling. +- More immersive agent/system surfaces that build on the existing office intent and runtime event model. + +## Product Ideas To Reduce OpenClaw Dependency + +- Expand the new agent wizard into reusable agent templates and presets, building on the existing playbook templates, onboarding flow, and agent creation steps. +- Turn the current onboarding and connection experience into a fuller workspace setup wizard that validates gateway access, permissions, local-vs-remote behavior, and common integrations in one place. +- Add a first-class heartbeat builder that unifies scheduled automations, `HEARTBEAT.md`, and related defaults into one guided UI instead of splitting that setup across multiple surfaces. +- Add a fleet-level tool access matrix with bulk controls so users can manage agent permissions and allowed tools across the whole office instead of one agent at a time. +- Add a shared user profile center that can manage and optionally sync `USER.md` defaults across multiple agents, rather than editing each agent independently. +- Add a real agent inbox and task queue that goes beyond the current results/inbox surfaces and lets users assign, retry, and route work between agents. +- Add a dedicated health dashboard that brings gateway status, failed runs, heartbeat issues, missing dependencies, and integration problems into one operational view. +- Add a broader prompt and playbook library on top of the current playbook template foundation so users can save, browse, and reuse recurring workflows more easily. +- Add visual office automation features that let users configure recurring behaviors and room-based actions directly from the office instead of relying on lower-level gateway concepts. +- Add an agent relationships and communication map so users can configure which agents collaborate, hand off work, or talk to each other without editing raw configuration. +- Add shared memory management for cross-agent context, since the current experience only exposes per-agent `MEMORY.md`. +- Add multi-agent orchestration and handoff workflows for common sequences such as PM -> Engineer -> QA, with explicit UI instead of relying on manual coordination. +- Add config diff and rollback tools so gateway-wide changes can be reviewed and safely reverted from Claw3D. +- Add conversation-to-agent bootstrap flows that can turn a successful chat or office interaction into a reusable new agent. +- Add a richer scenario simulator that extends the current mock phone/text scenarios into broader multi-agent rehearsal and testing flows. + +## Already In Progress Or Partially Covered + +- Skill installer compatibility checks already exist and should be expanded rather than reinvented. +- Playbook templates, scheduled automations, and onboarding flows already cover part of the templates/setup story. +- Per-agent capability controls and tool settings already exist, but not yet as a fleet-wide matrix. +- Analytics, connection status, and office activity surfaces already cover part of the future health dashboard story. +- The office builder, immersive office, and event-triggered behavior already cover part of the visual automation story. +- Runtime profiles now preserve separate per-backend URLs and tokens for gateway-style and direct-runtime slices. + +## Good First Contribution Areas + +- Documentation and developer-onboarding fixes. +- Focused unit-test additions around runtime workflows or office intent behavior. +- Small UI polish issues that stay inside one feature area. +- Replacing stale examples, placeholder text, or internal-only assumptions in public docs. + +## Before Starting Bigger Work + +- Read `README.md`, `CODE_DOCUMENTATION.md`, and `KNOWN_ISSUES.md`. +- Prefer opening or linking a GitHub issue before large architectural changes. diff --git a/docs/runtime-profiles.md b/docs/runtime-profiles.md new file mode 100644 index 0000000..6645136 --- /dev/null +++ b/docs/runtime-profiles.md @@ -0,0 +1,145 @@ +# Runtime Profiles + +Claw3D now treats runtime backends as named saved profiles instead of one global URL/token pair. + +## Current Profiles + +- `openclaw` +- `hermes` +- `demo` +- `local` +- `claw3d` +- `custom` + +Each profile keeps its own saved URL and token in Studio settings. + +## What Each Profile Means + +### `openclaw` + +The normal OpenClaw gateway flow over Studio's WebSocket bridge. + +This is the provider-rich path. OpenClaw already knows how to sit in front of many upstream model providers, so Claw3D should treat it as a first-class gateway adapter rather than flattening it into `custom`. + +Typical URL: + +```text +ws://localhost:18789 +``` + +### `hermes` + +The bundled Hermes adapter over the same gateway-shaped WebSocket flow. + +This is also a provider-aware runtime path. Hermes can own its own provider/account setup behind the gateway boundary. + +Typical URL: + +```text +ws://localhost:18789 +``` + +### `demo` + +The built-in demo gateway for a no-framework office. + +If that gateway is not available, the office can still fall back to a seeded local `main` agent so the scene is explorable instead of dead-ending on the connect overlay. + +Typical URL: + +```text +ws://localhost:18789 +``` + +### `local` + +A direct HTTP runtime boundary for local orchestrators or local model routers. + +Typical URL: + +```text +http://localhost:7770 +``` + +### `claw3d` + +A Claw3D-shaped HTTP runtime profile for stacks that want to keep Claw3D transcript and chat conventions while still using the direct runtime seam. + +Typical URL: + +```text +http://localhost:3000/api/runtime/custom +``` + +### `custom` + +The generic HTTP runtime seam when you want to point Claw3D at any compatible orchestrator boundary. + +Typical URL: + +```text +http://localhost:7770 +``` + +## Current Runtime Contract + +The direct runtime seam currently probes for: + +- `GET /health` +- `GET /state` +- `GET /registry` +- `POST /v1/chat/completions` + +That means `local`, `claw3d`, and `custom` are first-class saved profiles today. + +On top of the normal chat/session calls, runtime providers now expose a shared multi-agent message seam: + +- `agents.message` +- `agents.handoff` + +These methods currently route through the existing gateway/runtime session model rather than inventing a second transcript transport. + +## Multi-Agent Message Contract + +`agents.message` supports: + +- `targetAgentId` +- `message` +- `mode: "direct" | "interval"` +- optional `sourceAgentId` +- optional `sourceLabel` +- optional `cadenceHint` + +`agents.handoff` supports: + +- `targetAgentId` +- `task` +- optional `context` +- optional `deliverables` +- optional `acceptanceCriteria` +- optional `sourceAgentId` +- optional `sourceLabel` + +The intent is to keep one stable message/handoff contract while different runtime adapters decide how to deliver it. + +## What Is Not Wired Yet + +These are not first-class connection profiles yet in this branch: + +- Anthropic +- Claude Code +- OpenRouter +- other provider-native transports + +Those should land as real adapters, not as buttons that pretend the HTTP runtime seam already understands provider-specific auth and event semantics. + +The current provider review path should borrow from existing Hermes/OpenClaw wizard flows where possible, but land as Claw3D-native adapters instead of hard-coupling Claw3D UI state to another project's connector code. + +## Why This Matters For Multi-Agent Work + +The profile split is the first step toward: + +- separate per-runtime saved connection state +- agent-to-agent chat and handoff across backends +- shared-floor and coworking flows without flattening every runtime into one transport +- future provider adapters that do not require rewriting Studio UI state diff --git a/docs/whiteboard-spec.md b/docs/whiteboard-spec.md new file mode 100644 index 0000000..2a4c6f4 --- /dev/null +++ b/docs/whiteboard-spec.md @@ -0,0 +1,423 @@ +# Whiteboard Spec + +> Second concrete office-system feature for Claw3D, designed to work alongside the bulletin board. + +## Goal + +Add a whiteboard system inside the office for collaborative planning, meeting notes, and draft idea shaping. + +The whiteboard is where the office thinks. + +The bulletin board is where the office posts what matters. + +## Product Position + +The whiteboard should not duplicate the bulletin board. + +Use the distinction: + +- bulletin board = visible office signals +- whiteboard = active drafting and planning surface + +The whiteboard is best for: + +- brainstorming +- architecture outlines +- meeting notes +- draft task breakdowns +- org planning +- decision framing + +It is not a Kanban replacement and not a polished document editor. + +## Why This Feature Matters + +The office already has: + +- standup logic +- meeting room space +- whiteboard props in the retro office +- task board and planning-adjacent systems + +What is missing is a shared in-world planning surface. + +The whiteboard creates that surface. + +## Primary Use Cases + +### Meeting Notes + +Examples: + +- standup talking points +- decisions made during a meeting +- action items +- unresolved questions + +### Brainstorming + +Examples: + +- possible approaches to a feature +- tradeoff comparisons +- rough implementation ideas +- product concept sketches in text form + +### Architecture Planning + +Examples: + +- component breakdown +- adapter/provider mapping +- system boundaries +- workflow diagrams in structured text + +### Org Planning + +Examples: + +- team structure drafts +- role definitions +- department responsibilities +- handoff chains + +### Session-to-Plan Bridge + +Examples: + +- summarize an agent conversation into a board section +- turn standup outputs into grouped notes +- capture a working draft before turning it into bulletin board items or tasks + +## V1 Scope + +V1 should be structured, not freehand. + +That means: + +- text blocks +- sections +- cards / note clusters +- ordering +- lightweight templates + +Do not start with arbitrary drawing tools. + +## Whiteboard Model + +Suggested V1 shape: + +```ts +type WhiteboardBlockType = + | "heading" + | "note" + | "decision" + | "question" + | "action" + | "group"; + +type WhiteboardBlock = { + id: string; + type: WhiteboardBlockType; + title?: string; + body?: string; + createdAt: string; + updatedAt: string; + authorType: "human" | "agent" | "system"; + authorId?: string | null; + authorName?: string | null; + linkedAgentId?: string | null; + linkedSessionKey?: string | null; + linkedTaskId?: string | null; + color?: string | null; + collapsed?: boolean; +}; + +type WhiteboardDocument = { + id: string; + title: string; + mode: "planning" | "meeting" | "architecture" | "org" | "freeform"; + createdAt: string; + updatedAt: string; + archived: boolean; + blocks: WhiteboardBlock[]; +}; +``` + +## V1 Interaction Model + +V1 interactions: + +- create whiteboard +- rename whiteboard +- add/edit/delete blocks +- reorder blocks +- collapse/expand groups +- link a block to an agent, task, or session +- archive whiteboard +- duplicate whiteboard + +Optional but useful: + +- convert a block into a bulletin-board card +- convert a block into a task seed + +## Templates + +Templates are important because they make the feature useful immediately. + +Recommended V1 templates: + +- `Meeting Notes` +- `Standup Review` +- `Planning Session` +- `Architecture Draft` +- `Org Planning` + +### Example: Meeting Notes Template + +Sections: + +- attendees +- current topic +- decisions +- blockers +- next actions + +### Example: Planning Session Template + +Sections: + +- problem +- options +- risks +- chosen direction +- tasks + +## Relationship To Existing Systems + +The whiteboard should integrate with what Claw3D already has. + +### Standup + +The standup controller already exists. + +The whiteboard should support: + +- auto-creating a meeting notes board for an active standup +- writing participant summaries to blocks +- collecting blockers and next actions into dedicated sections + +### Bulletin Board + +The whiteboard should feed the bulletin board, not replace it. + +Examples: + +- convert a decision block into an announcement card +- convert a blocker block into a blocker card +- convert a next-action block into a handoff card + +### Task Board / Kanban + +The whiteboard is where a plan is shaped before it becomes a tracked workflow. + +Examples: + +- rough task breakdown on whiteboard +- selected action blocks converted into actual task records +- blocked tasks reflected back to the bulletin board + +### Company Builder / Org Planning + +The whiteboard is a natural fit for: + +- team structure drafts +- department planning +- role relationship mapping + +This is especially useful before company-builder output becomes actual agents. + +## In-World UX + +The whiteboard should exist as a real office surface. + +Recommended forms: + +- meeting-room whiteboard +- wall-mounted planning board +- design room / architecture board in future themes + +Behavior: + +- clicking the board opens an immersive planning surface +- active meetings can auto-focus or highlight the whiteboard +- whiteboard state should feel like part of the room, not a random modal + +## Sidebar / Secondary Access + +The user should also be able to open the whiteboard from a panel or shortcut. + +Good options: + +- HQ sidebar tab +- meeting controls +- standup panel + +This is especially important when users want direct access without camera movement. + +## Storage Model + +Like the bulletin board, V1 should be persisted locally in office preferences. + +Suggested shape: + +```ts +type OfficePreference = { + whiteboards?: { + documents: WhiteboardDocument[]; + activeDocumentId?: string | null; + updatedAt?: string; + }; +}; +``` + +Storage should be keyed by gateway URL / office context so each connected office can keep its own working state. + +## JSON Canvas Compatibility + +JSON Canvas is a good interoperability target for the whiteboard, but it should not define the product by itself. + +Recommended stance: + +- use Claw3D's own whiteboard model as the primary domain model +- support export/import to JSON Canvas as a compatibility layer +- avoid turning the whiteboard into a generic infinite-canvas editor before the office workflow is proven + +Why: + +- Claw3D needs stronger links to meetings, bulletin board items, tasks, agents, and sessions +- the whiteboard is a workflow surface, not only a canvas +- structured planning is more important than unconstrained canvas freedom in V1 + +Good use of JSON Canvas: + +- export planning boards +- import external draft canvases +- map blocks/groups into JSON Canvas nodes +- preserve links where practical + +Bad use of JSON Canvas: + +- letting a generic canvas model dictate the first product UX +- replacing office-native planning behavior with a broad but shallow editor + +## Authoring Rules + +Allowed authors: + +- human +- agent +- system + +Recommended behavior: + +- human edits are fully editable +- system-generated sections should remain editable but visibly marked +- agent-authored blocks should show provenance + +That balance keeps the board useful without feeling rigid. + +## V1 Automation + +Useful automations: + +- create a whiteboard automatically when a standup meeting starts +- seed a whiteboard from a planning command or meeting ritual +- let an agent summarize a session into selected board blocks + +Important: + +- automation should create structure, not spam content +- the user should remain able to edit the board freely + +## Visual Structure + +V1 should look like a structured planning board, not a blank canvas. + +Possible presentation: + +- left column for sections +- center canvas for block editing +- right rail for linked agents/sessions/tasks + +Or: + +- grouped lanes by section with text cards inside them + +The design should prioritize clarity over novelty. + +## Out of Scope For V1 + +Do not include these initially: + +- freehand drawing tools +- multiplayer cursor presence +- arbitrary shapes/connectors +- full diagramming toolkit +- external document sync +- rich media embedding +- advanced permissions by department + +Those can come later if the structured board proves valuable. + +## Implementation Strategy + +Recommended order: + +1. Define whiteboard document and block schema. +2. Add office preference persistence. +3. Build a simple whiteboard panel UI with templates. +4. Connect the in-world whiteboard object to open the panel. +5. Add standup seeding / meeting integration. +6. Add conversions into bulletin-board cards and task seeds. + +## Existing Code Seams + +This feature should align with: + +- standup systems in `src/features/office/hooks/useOfficeStandupController.ts` +- standup API routes under `src/app/api/office/standup` +- retro office whiteboard objects and room interactions +- office settings persistence +- task board seeding concepts already present in office task flows + +This reduces implementation risk and keeps the feature tied to real office mechanics. + +## Success Criteria + +V1 is successful if: + +- the user can open a whiteboard from inside the office +- a meeting or planning session can write structured notes to it +- the board can link to agents, sessions, and tasks +- users can turn whiteboard outputs into bulletin board items or task seeds +- the system works independently of OpenClaw-specific behavior + +## Future Extensions + +Once V1 is working, the whiteboard can evolve into: + +- diagram mode +- relationship mapping +- architecture views +- agent collaboration sessions +- department-specific whiteboards +- persistent planning archives +- richer visual theming by office skin + +## Summary + +The whiteboard should become Claw3D’s active planning surface. + +It is where meetings, drafts, and rough plans take shape before they become tasks, bulletin items, or office decisions. diff --git a/next.config.ts b/next.config.ts index c57dbda..15b59ba 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,5 @@ import type { NextConfig } from "next"; +import path from "node:path"; const securityHeaders = [ { @@ -11,7 +12,14 @@ const securityHeaders = [ "img-src 'self' data: blob: http: https:", "font-src 'self' data: https:", "style-src 'self' 'unsafe-inline' https:", - "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:", + // 'unsafe-eval' is required by Next.js dev mode (source maps, HMR). + // In production it is dropped — React and Three.js do not need eval. + ...(process.env.NODE_ENV !== "production" + ? ["script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:"] + : ["script-src 'self' 'unsafe-inline' blob:"]), + // connect-src is intentionally broad: gateway URLs are user-configured + // at runtime and cannot be enumerated at build time. + // Restrict further when a fixed deployment target is known. "connect-src 'self' ws: wss: http: https:", "media-src 'self' blob: data: http: https:", "worker-src 'self' blob:", @@ -49,6 +57,9 @@ if (process.env.NODE_ENV === "production") { } const nextConfig: NextConfig = { + turbopack: { + root: path.resolve(__dirname), + }, async headers() { return [ { diff --git a/package.json b/package.json index c16d970..6feff73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claw3d", - "version": "0.1.5", + "version": "0.1.4", "private": true, "license": "MIT", "scripts": { @@ -14,6 +14,7 @@ "cleanup:ux-artifacts": "node scripts/cleanup-ux-artifacts.mjs", "sync:gateway-client": "node scripts/sync-openclaw-gateway-client.ts", "studio:setup": "node scripts/studio-setup.js", + "doctor": "node scripts/claw3doctor.mjs", "smoke:dev-server": "node scripts/smoke-dev-server.mjs", "typecheck": "tsc --noEmit", "test": "vitest", diff --git a/scripts/claw3doctor.mjs b/scripts/claw3doctor.mjs new file mode 100644 index 0000000..90d246a --- /dev/null +++ b/scripts/claw3doctor.mjs @@ -0,0 +1,596 @@ +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; + +import { WebSocket } from "ws"; +import { + buildCustomRuntimeWarnings, + buildDoctorJsonReport, + buildGatewayFailureActions, + buildGatewayWarnings, + buildOpenClawWarnings, + buildProfileWarnings, + classifyGatewayFailure, + DOCTOR_STATUSES, + formatDoctorReport, + parseDoctorArgs, + resolveRuntimeContext, + isCustomRuntimeAdapter, + shouldRunCustomChecks, + shouldRunDemoChecks, + shouldRunHermesChecks, + shouldRunOpenClawChecks, + summarizeChecks, +} from "./lib/claw3doctor-core.mjs"; + +const require = createRequire(import.meta.url); +const { + loadUpstreamGatewaySettings, + resolveStateDir, + resolveStudioSettingsPath, +} = require("../server/studio-settings.js"); + +function loadDotenvFile(filePath) { + if (!fs.existsSync(filePath)) return; + const content = fs.readFileSync(filePath, "utf8"); + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/); + if (!match) continue; + const [, key, rawValue] = match; + if (process.env[key] !== undefined) continue; + let value = rawValue.trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + process.env[key] = value; + } +} + +function loadRuntimeEnv() { + const cwd = process.cwd(); + loadDotenvFile(path.join(cwd, ".env.local")); + loadDotenvFile(path.join(cwd, ".env")); +} + +const readJsonFile = (filePath) => { + try { + if (!fs.existsSync(filePath)) return null; + return JSON.parse(fs.readFileSync(filePath, "utf8")); + } catch { + return null; + } +}; + +const formatErrorMessage = (error, fallback) => { + if (!(error instanceof Error)) return fallback; + if ( + error.name === "AggregateError" && + Array.isArray(error.errors) && + error.errors.length > 0 + ) { + const details = error.errors + .map((entry) => + entry instanceof Error + ? entry.message || entry.name + : String(entry ?? "").trim(), + ) + .filter(Boolean); + if (details.length > 0) { + return details.join("; "); + } + } + return error.message || error.name || fallback; +}; + +const checkPass = (category, label, message, actions) => ({ + status: DOCTOR_STATUSES.pass, + category, + label, + message, + ...(actions?.length ? { actions } : {}), +}); + +const checkWarn = (category, label, message, actions) => ({ + status: DOCTOR_STATUSES.warn, + category, + label, + message, + ...(actions?.length ? { actions } : {}), +}); + +const checkFail = (category, label, message, actions) => ({ + status: DOCTOR_STATUSES.fail, + category, + label, + message, + ...(actions?.length ? { actions } : {}), +}); + +const trim = (value) => (typeof value === "string" ? value.trim() : ""); + +const probeWebSocket = async (url, timeoutMs = 3500) => + await new Promise((resolve) => { + let settled = false; + const finish = (result) => { + if (settled) return; + settled = true; + clearTimeout(timer); + try { + socket.close(); + } catch {} + resolve(result); + }; + const socket = new WebSocket(url, { handshakeTimeout: timeoutMs }); + const timer = setTimeout( + () => finish({ ok: false, message: `Timed out after ${timeoutMs}ms.` }), + timeoutMs + 250, + ); + socket.once("open", () => + finish({ ok: true, message: "WebSocket handshake succeeded." }), + ); + socket.once("error", (error) => + finish({ + ok: false, + message: formatErrorMessage(error, "WebSocket handshake failed."), + }), + ); + socket.once("unexpected-response", (_req, res) => + finish({ + ok: false, + message: `Unexpected HTTP ${res.statusCode ?? "response"} during WebSocket upgrade.`, + }), + ); + }); + +const probeHttpJson = async ({ url, headers = {}, timeoutMs = 3500 }) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(url, { headers, signal: controller.signal }); + const text = await response.text(); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch {} + return { ok: response.ok, status: response.status, text, json }; + } catch (error) { + return { + ok: false, + status: 0, + text: error instanceof Error ? error.message : "HTTP probe failed.", + json: null, + }; + } finally { + clearTimeout(timeout); + } +}; + +const detectOpenClawVersion = () => { + try { + return execFileSync("openclaw", ["--version"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 4000, + }).trim(); + } catch (error) { + return error instanceof Error + ? error.message + : "Unable to run openclaw --version"; + } +}; + +const detectWorkspaceState = () => { + try { + const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 3000, + }).trim(); + const dirty = + execFileSync("git", ["status", "--porcelain"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 3000, + }).trim().length > 0; + return { branch, dirty, available: true }; + } catch { + return { branch: "", dirty: false, available: false }; + } +}; + +const detectHermesModelHealth = async () => { + const apiUrl = ( + trim(process.env.HERMES_API_URL) || "http://localhost:8642" + ).replace(/\/$/, ""); + const apiKey = trim(process.env.HERMES_API_KEY); + const model = trim(process.env.HERMES_MODEL) || "hermes"; + const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {}; + const result = await probeHttpJson({ url: `${apiUrl}/v1/models`, headers }); + return { + apiUrl, + model, + apiKeyConfigured: Boolean(apiKey), + probe: result, + }; +}; + +const probeCustomRuntimeHealth = async (runtimeUrl) => { + const baseUrl = runtimeUrl.replace(/\/$/, ""); + const health = await probeHttpJson({ url: `${baseUrl}/health` }); + if (health.ok) { + return { + ok: true, + message: "Custom runtime /health responded successfully.", + }; + } + + const registry = await probeHttpJson({ url: `${baseUrl}/registry` }); + if (registry.ok) { + return { + ok: true, + message: "Custom runtime /registry responded successfully.", + }; + } + + return { + ok: false, + message: + health.text || + registry.text || + "Custom runtime did not respond on /health or /registry.", + }; +}; + +const probeProfileHealth = async ({ adapterType, url }) => { + if (isCustomRuntimeAdapter(adapterType)) { + const result = await probeCustomRuntimeHealth(url); + return { + ok: result.ok, + message: result.message, + }; + } + + const result = await probeWebSocket(url); + return { + ok: result.ok, + message: result.message, + }; +}; + +async function main() { + loadRuntimeEnv(); + const args = parseDoctorArgs(process.argv.slice(2)); + + const env = process.env; + const stateDir = resolveStateDir(env); + const settingsPath = resolveStudioSettingsPath(env); + const upstreamGateway = loadUpstreamGatewaySettings(env); + const studioSettings = readJsonFile(settingsPath); + const runtimeContext = resolveRuntimeContext({ + settings: studioSettings, + upstreamGateway, + env, + }); + const workspace = detectWorkspaceState(); + + /** + * Returns whether provider-specific checks for `adapterType` should run. + * When --profile is set, only that adapter is in scope. + * When --all-profiles is set, all adapters are in scope. + * Otherwise falls back to `defaultBehavior` (the existing shouldRun* predicate). + */ + const adapterInScope = (adapterType, defaultBehavior) => { + if (args.allProfiles) return true; + if (args.profile) return args.profile === adapterType; + return defaultBehavior; + }; + + const checks = []; + + checks.push( + workspace.available + ? workspace.dirty + ? checkWarn( + "Workspace", + "Git branch", + `${workspace.branch} (working tree has local modifications)`, + ) + : checkPass( + "Workspace", + "Git branch", + `${workspace.branch} (clean working tree)`, + ) + : checkWarn( + "Workspace", + "Git branch", + "Git branch could not be detected from this working directory.", + ), + ); + + checks.push( + runtimeContext.gatewayUrl + ? checkPass( + "Runtime profiles", + "Runtime profile", + `${runtimeContext.adapterType} selected at ${runtimeContext.gatewayUrl}`, + ) + : checkFail( + "Runtime profiles", + "Runtime profile", + "No runtime profile / gateway URL is configured.", + ["Set the gateway URL in Claw3D connect/settings before retrying."], + ), + ); + + checks.push( + runtimeContext.tokenConfigured + ? checkPass( + "Runtime profiles", + "Gateway token", + "A gateway token is configured for the selected profile.", + ) + : checkWarn( + "Runtime profiles", + "Gateway token", + "No gateway token is configured for the selected profile.", + [ + "If this backend requires token auth, set the upstream token in Claw3D settings or openclaw.json.", + ], + ), + ); + + for (const warning of buildProfileWarnings({ runtimeContext })) { + checks.push( + checkWarn("Runtime profiles", "Profile collision", warning, [ + "Assign distinct local ports or URLs if you want OpenClaw, Hermes, and demo running simultaneously instead of swapping one backend onto the same endpoint.", + ]), + ); + } + + if (args.profile) { + const requestedProfile = runtimeContext.profiles?.[args.profile]; + checks.push( + requestedProfile + ? checkPass( + "Runtime profiles", + "Profile selection", + `Scoped diagnostics to the ${args.profile} profile.`, + ) + : checkFail( + "Runtime profiles", + "Profile selection", + `Requested profile "${args.profile}" is not configured in current Studio settings.`, + [ + "Run `node scripts/claw3doctor.mjs --all-profiles` to see the configured profile list.", + ], + ), + ); + } else if (args.allProfiles) { + checks.push( + checkPass( + "Runtime profiles", + "Profile selection", + "Running diagnostics across all configured runtime profiles.", + ), + ); + } else { + checks.push( + checkPass( + "Runtime profiles", + "Profile selection", + `Running diagnostics for the selected ${runtimeContext.adapterType} profile only.`, + ), + ); + } + + for (const warning of buildGatewayWarnings({ + gatewayUrl: runtimeContext.gatewayUrl, + studioAccessToken: trim(env.STUDIO_ACCESS_TOKEN), + host: trim(env.HOST), + })) { + checks.push(checkWarn("Gateway access", "Gateway hints", warning)); + } + + if (adapterInScope("openclaw", runtimeContext.adapterType === "openclaw")) { + for (const warning of buildOpenClawWarnings({ + gatewayUrl: runtimeContext.gatewayUrl, + tokenConfigured: runtimeContext.tokenConfigured, + })) { + checks.push( + checkWarn("OpenClaw", "OpenClaw hints", warning, [ + "If the browser/device is not yet approved, check `openclaw devices list` and approve the pending device before retrying the remote connection.", + ]), + ); + } + } + + const customRuntimeInScope = args.allProfiles + ? true + : args.profile + ? isCustomRuntimeAdapter(args.profile) + : shouldRunCustomChecks({ runtimeContext }); + if (customRuntimeInScope) { + for (const warning of buildCustomRuntimeWarnings({ + gatewayUrl: runtimeContext.gatewayUrl, + allowlist: + trim(env.CUSTOM_RUNTIME_ALLOWLIST) || trim(env.UPSTREAM_ALLOWLIST), + nodeEnv: trim(env.NODE_ENV), + })) { + checks.push(checkWarn("Custom runtime", "Custom runtime hints", warning)); + } + } + + const profileEntries = Object.entries(runtimeContext.profiles ?? {}).filter( + ([adapterType]) => { + if (args.allProfiles) return true; + if (args.profile) return adapterType === args.profile; + return adapterType === runtimeContext.adapterType; + }, + ); + for (const [adapterTypeRaw, profile] of profileEntries) { + const adapterType = adapterTypeRaw; + const url = trim(profile?.url); + if (!url) continue; + const isSelected = adapterType === runtimeContext.adapterType; + const label = `${adapterType} profile${isSelected ? " (selected)" : ""}`; + const health = await probeProfileHealth({ adapterType, url }); + checks.push( + health.ok + ? checkPass("Profile health", label, `${url} -> ${health.message}`) + : checkFail( + "Profile health", + label, + `${url} -> ${health.message}`, + buildGatewayFailureActions({ + adapterType, + message: health.message, + gatewayUrl: url, + }).concat( + isCustomRuntimeAdapter(adapterType) + ? [ + "If this runtime sits behind the Studio custom proxy, verify CUSTOM_RUNTIME_ALLOWLIST / UPSTREAM_ALLOWLIST for the target host.", + ] + : [ + "Verify the configured gateway URL is correct and the backend is listening.", + ], + ), + ), + ); + const classification = classifyGatewayFailure({ message: health.message }); + if (classification) { + checks.push( + checkWarn( + "Failure analysis", + `${label} failure class`, + `${classification.code} ${classification.label}: ${classification.message}`, + ), + ); + } + } + + const openclawConfigPath = path.join(stateDir, "openclaw.json"); + const openclawConfigExists = fs.existsSync(openclawConfigPath); + if (shouldRunOpenClawChecks({ runtimeContext, openclawConfigExists })) { + checks.push( + openclawConfigExists + ? checkPass( + "OpenClaw", + "OpenClaw config", + `Found ${openclawConfigPath}.`, + ) + : checkWarn( + "OpenClaw", + "OpenClaw config", + `No openclaw.json found at ${openclawConfigPath}.`, + [ + "If you expect a local OpenClaw default, verify OPENCLAW_STATE_DIR or create openclaw.json.", + ], + ), + ); + + const version = detectOpenClawVersion(); + const versionLooksValid = /^OpenClaw\s+/i.test(version); + checks.push( + versionLooksValid + ? checkPass("OpenClaw", "OpenClaw version", version) + : checkWarn("OpenClaw", "OpenClaw version", version, [ + "Install OpenClaw or ensure it is available on PATH if this machine should run it directly.", + ]), + ); + } + + if (adapterInScope("demo", shouldRunDemoChecks({ runtimeContext, env }))) { + const configuredPort = trim(env.DEMO_ADAPTER_PORT) || "18789"; + checks.push( + checkPass( + "Demo gateway", + "Demo gateway config", + `Demo mode expects the mock gateway on ws://localhost:${configuredPort}.`, + [ + "Run `npm run demo-gateway` if you want a no-runtime office smoke test.", + ], + ), + ); + } + + if ( + adapterInScope("hermes", shouldRunHermesChecks({ runtimeContext, env })) + ) { + const hermes = await detectHermesModelHealth(); + checks.push( + checkPass( + "Hermes", + "Hermes adapter config", + `Hermes API target ${hermes.apiUrl} | model ${hermes.model} | key ${ + hermes.apiKeyConfigured ? "configured" : "missing" + }`, + ), + ); + + if (hermes.probe.ok) { + const models = Array.isArray(hermes.probe.json?.data) + ? hermes.probe.json.data.map((entry) => trim(entry?.id)).filter(Boolean) + : []; + checks.push( + checkPass( + "Hermes", + "Hermes API", + models.length > 0 + ? `Hermes API reachable. Reported models: ${models.join(", ")}` + : "Hermes API reachable.", + ), + ); + if (models.length > 0 && !models.includes(hermes.model)) { + checks.push( + checkWarn( + "Hermes", + "Hermes model", + `Configured model "${hermes.model}" was not returned by /v1/models.`, + [ + "Set HERMES_MODEL to one of the reported model ids or update the Hermes API configuration.", + ], + ), + ); + } + } else if (hermes.probe.status === 401) { + checks.push( + checkFail("Hermes", "Hermes API", "Hermes API returned HTTP 401.", [ + "Verify HERMES_API_KEY and confirm the adapter is loading the same .env values you expect.", + ]), + ); + } else { + checks.push( + checkFail( + "Hermes", + "Hermes API", + hermes.probe.text || "Hermes API probe failed.", + [ + "Start the Hermes API server and verify /v1/models responds before starting the adapter.", + ], + ), + ); + } + } + + const summary = summarizeChecks(checks); + const reportInput = { + summary, + runtimeContext, + paths: { stateDir, settingsPath }, + checks, + }; + if (args.json) { + console.log(JSON.stringify(buildDoctorJsonReport(reportInput), null, 2)); + } else { + console.log(formatDoctorReport(reportInput)); + } + process.exit(summary === DOCTOR_STATUSES.fail ? 1 : 0); +} + +await main(); diff --git a/scripts/lib/claw3doctor-core.mjs b/scripts/lib/claw3doctor-core.mjs new file mode 100644 index 0000000..68c2132 --- /dev/null +++ b/scripts/lib/claw3doctor-core.mjs @@ -0,0 +1,573 @@ +export const DOCTOR_STATUSES = { + pass: "PASS", + warn: "WARN", + fail: "FAIL", +}; + +const VALID_ADAPTER_TYPES = new Set([ + "openclaw", + "hermes", + "demo", + "local", + "claw3d", + "custom", +]); +const TUNNEL_HOST_PATTERN = + /(cloudflare|trycloudflare|ngrok|tailscale|tunnel)/i; +const DEFAULT_GATEWAY_URL_BY_ADAPTER = { + openclaw: "ws://localhost:18789", + hermes: "ws://localhost:18789", + demo: "ws://localhost:18789", + local: "http://localhost:7770", + claw3d: "http://localhost:3000/api/runtime/custom", + custom: "http://localhost:7770", +}; + +const isRecord = (value) => + Boolean(value && typeof value === "object" && !Array.isArray(value)); + +const trimString = (value) => (typeof value === "string" ? value.trim() : ""); +const hasHostnameSuffix = (hostname, suffix) => + hostname === suffix || hostname.endsWith(`.${suffix}`); +const isTunnelBackedHostname = (hostname) => + Boolean( + hostname && + (TUNNEL_HOST_PATTERN.test(hostname) || hasHostnameSuffix(hostname, "ts.net")), + ); +const supportsAnsi = () => + Boolean(process.stdout?.isTTY && process.env.NO_COLOR !== "1"); +const colorize = (text, code) => + supportsAnsi() ? `\u001b[${code}m${text}\u001b[0m` : text; +const formatStatusBadge = (status) => { + switch (status) { + case DOCTOR_STATUSES.pass: + return colorize("[PASS]", "32"); + case DOCTOR_STATUSES.warn: + return colorize("[WARN]", "33"); + case DOCTOR_STATUSES.fail: + return colorize("[FAIL]", "31"); + default: + return `[${status}]`; + } +}; + +export const normalizeAdapterType = (value, fallback = "openclaw") => { + const normalized = trimString(value).toLowerCase(); + return VALID_ADAPTER_TYPES.has(normalized) ? normalized : fallback; +}; + +export const isCustomRuntimeAdapter = (adapterType) => { + const normalized = normalizeAdapterType(adapterType, ""); + return ( + normalized === "custom" || + normalized === "local" || + normalized === "claw3d" + ); +}; + +export const resolveRuntimeContext = ({ + settings, + upstreamGateway, + env = process.env, +}) => { + const gateway = isRecord(settings?.gateway) ? settings.gateway : null; + const adapterType = normalizeAdapterType( + gateway?.adapterType ?? + upstreamGateway?.adapterType ?? + env.CLAW3D_GATEWAY_ADAPTER_TYPE, + "openclaw", + ); + const rawProfiles = isRecord(gateway?.profiles) ? gateway.profiles : null; + const profiles = {}; + for (const key of VALID_ADAPTER_TYPES) { + const profile = isRecord(rawProfiles?.[key]) ? rawProfiles[key] : null; + const url = trimString(profile?.url); + const token = trimString(profile?.token); + if (!url) continue; + profiles[key] = { url, token }; + } + + const upstreamUrl = trimString(upstreamGateway?.url); + const selectedProfile = profiles[adapterType] + ? profiles[adapterType] + : upstreamUrl + ? { + url: upstreamUrl, + token: trimString(upstreamGateway?.token), + } + : { + url: DEFAULT_GATEWAY_URL_BY_ADAPTER[adapterType], + token: "", + }; + if (selectedProfile?.url && !profiles[adapterType]) { + profiles[adapterType] = { + url: selectedProfile.url, + token: selectedProfile.token ?? "", + }; + } + + return { + adapterType, + gatewayUrl: selectedProfile?.url ?? "", + token: selectedProfile?.token ?? "", + tokenConfigured: Boolean(selectedProfile?.token), + profiles, + }; +}; + +export const buildGatewayWarnings = ({ + gatewayUrl, + studioAccessToken = "", + host = "", +}) => { + const warnings = []; + const url = trimString(gatewayUrl); + if (!url) { + warnings.push("No gateway URL configured."); + return warnings; + } + + let parsed = null; + try { + parsed = new URL(url); + } catch { + warnings.push("Gateway URL is not a valid URL."); + return warnings; + } + + const protocol = parsed.protocol.toLowerCase(); + const hostname = parsed.hostname.toLowerCase(); + const isLocalHost = + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" || + hostname.endsWith(".local"); + const isRemote = !isLocalHost; + + if (isRemote && protocol === "ws:") { + warnings.push( + "Remote gateway uses ws://. Public or cross-device browser connections usually need wss:// or an HTTPS-backed Studio proxy.", + ); + } + + if (isRemote && isTunnelBackedHostname(hostname)) { + warnings.push( + "Gateway host looks tunnel-backed. If connect fails, compare direct local/LAN behavior before debugging the runtime itself.", + ); + } + + const normalizedHost = trimString(host).toLowerCase(); + const publicStudioHost = + normalizedHost && + normalizedHost !== "localhost" && + normalizedHost !== "127.0.0.1" && + normalizedHost !== "::1" && + normalizedHost !== "0.0.0.0"; + if (publicStudioHost && !trimString(studioAccessToken)) { + warnings.push( + "Studio appears to be configured for a public host without STUDIO_ACCESS_TOKEN. Remote admin access should not be exposed that way.", + ); + } + + return warnings; +}; + +export const buildProfileWarnings = ({ runtimeContext }) => { + const warnings = []; + const urlToAdapters = new Map(); + for (const [adapterType, profile] of Object.entries( + runtimeContext?.profiles ?? {}, + )) { + const url = trimString(profile?.url); + if (!url) continue; + const key = url.toLowerCase(); + const adapters = urlToAdapters.get(key) ?? []; + adapters.push(adapterType); + urlToAdapters.set(key, adapters); + } + + for (const [url, adapters] of urlToAdapters.entries()) { + if (adapters.length < 2) continue; + warnings.push( + `Multiple runtime profiles share the same endpoint (${url}): ${adapters.join(", ")}. That is fine for one-runtime-at-a-time local use, but simultaneous runtimes need distinct URLs or ports.`, + ); + } + + return warnings; +}; + +export const buildOpenClawWarnings = ({ + gatewayUrl, + tokenConfigured = false, +}) => { + const warnings = []; + const url = trimString(gatewayUrl); + if (!url) return warnings; + + let parsed = null; + try { + parsed = new URL(url); + } catch { + return warnings; + } + + const hostname = parsed.hostname.toLowerCase(); + const isLocalHost = + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" || + hostname.endsWith(".local"); + if (isLocalHost) { + return warnings; + } + + if (!tokenConfigured) { + warnings.push( + "Remote OpenClaw profile has no gateway token configured. Remote/browser clients often fail with pairing or approval-style errors until the device or token path is approved.", + ); + } + + if (isTunnelBackedHostname(hostname)) { + warnings.push( + "Remote OpenClaw host looks tunnel-backed. If you hit 1008/1011/1012-style failures, verify direct local or LAN access first, then check pairing/device approval and reverse-proxy websocket handling.", + ); + } + + return warnings; +}; + +export const buildCustomRuntimeWarnings = ({ + gatewayUrl, + allowlist = "", + nodeEnv = "", +}) => { + const warnings = []; + const url = trimString(gatewayUrl); + if (!url) return warnings; + + let parsed = null; + try { + parsed = new URL(url); + } catch { + warnings.push("Custom runtime URL is not a valid URL."); + return warnings; + } + + if (parsed.protocol === "ws:" || parsed.protocol === "wss:") { + warnings.push( + "Custom runtime profile uses a websocket URL. The custom provider boundary is expected to expose an HTTP API (for example /health and /v1/chat/completions).", + ); + } + + const isProduction = trimString(nodeEnv).toLowerCase() === "production"; + const hostname = parsed.hostname.toLowerCase(); + const isLocalHost = + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" || + hostname.endsWith(".local"); + if (isProduction && !isLocalHost && !trimString(allowlist)) { + warnings.push( + "Production custom runtime is configured without CUSTOM_RUNTIME_ALLOWLIST or UPSTREAM_ALLOWLIST. The runtime proxy should not rely on open-host defaults there.", + ); + } + + return warnings; +}; + +export const buildGatewayFailureActions = ({ + adapterType, + message = "", + gatewayUrl = "", +}) => { + const actions = []; + const normalized = trimString(message).toLowerCase(); + const url = trimString(gatewayUrl); + let parsedUrl = null; + try { + parsedUrl = url ? new URL(url) : null; + } catch {} + const hostname = parsedUrl?.hostname?.toLowerCase() ?? ""; + const isTunnelBacked = isTunnelBackedHostname(hostname); + const isCloudflare = hostname.includes("cloudflare"); + const isTailscale = + hostname.includes("tailscale") || hasHostnameSuffix(hostname, "ts.net"); + + if (normalized.includes("econnrefused") || normalized.includes("timed out")) { + actions.push( + "Verify the backend is actually listening on the configured host and port before retrying from Claw3D.", + ); + } + + if (normalized.includes("1011")) { + actions.push( + "If this is OpenClaw behind a reverse proxy or tunnel, verify websocket upgrade handling and compare direct local/LAN behavior before assuming the runtime itself is broken.", + ); + } + + if (normalized.includes("1012")) { + actions.push( + "A 1012-style close usually means the upstream is restarting or unavailable temporarily. Retry after checking the backend service logs.", + ); + } + + if ( + normalized.includes("1008") || + normalized.includes("pairing required") || + normalized.includes("approve") + ) { + actions.push( + "For OpenClaw, check pending device/browser approval with `openclaw devices list` and approve the request before retrying the remote browser session.", + ); + } + + if ( + normalized.includes("401") || + normalized.includes("403") || + normalized.includes("unexpected http 401") + ) { + actions.push( + "Recheck the configured token/auth path. The gateway or proxy is rejecting the connection before the office can load.", + ); + } + + if (isCloudflare) { + actions.push( + "For Cloudflare or similar HTTPS tunnels, verify websocket upgrade forwarding and prefer an HTTPS-backed Studio path rather than a bare ws:// remote endpoint.", + ); + } + + if (isTailscale) { + actions.push( + "For Tailnet-hosted OpenClaw, test the same gateway directly on local/LAN first, then compare against the Tailnet URL so pairing/proxy issues do not get conflated.", + ); + } + + if (isTunnelBacked) { + actions.push( + "Because this endpoint looks tunnel-backed, reproduce once via direct local or LAN access to separate runtime problems from tunnel/proxy problems.", + ); + } + + if (isCustomRuntimeAdapter(adapterType)) { + actions.push( + "Custom runtimes should answer over HTTP on /health or /registry, not just a raw websocket endpoint.", + ); + } + + return [...new Set(actions)]; +}; + +export const classifyGatewayFailure = ({ message = "" }) => { + const normalized = trimString(message).toLowerCase(); + if (!normalized) return null; + + if (normalized.includes("1008") || normalized.includes("pairing required")) { + return { + code: "1008", + label: "Policy or pairing gate", + message: + "The upstream is rejecting this session for policy/pairing reasons. Check device approval, browser identity, and token flow.", + }; + } + + if (normalized.includes("1011")) { + return { + code: "1011", + label: "Upstream runtime or proxy failure", + message: + "The websocket upgraded but the upstream failed mid-connect or during runtime handling. Check runtime logs and reverse-proxy websocket support.", + }; + } + + if (normalized.includes("1012")) { + return { + code: "1012", + label: "Service restart or temporary unavailability", + message: + "The upstream likely restarted or was briefly unavailable. Recheck service health and retry once the backend settles.", + }; + } + + if ( + normalized.includes("401") || + normalized.includes("403") || + normalized.includes("unexpected http 401") || + normalized.includes("unexpected http 403") + ) { + return { + code: normalized.includes("403") ? "403" : "401", + label: "Auth rejection", + message: + "The upstream or proxy rejected auth before the office connected. Recheck the selected profile token, studio access path, and adapter env alignment.", + }; + } + + if (normalized.includes("econnrefused")) { + return { + code: "ECONNREFUSED", + label: "Listener missing", + message: + "Nothing is listening on the configured host/port. Start the backend or fix the profile URL before retrying.", + }; + } + + if (normalized.includes("timed out")) { + return { + code: "TIMEOUT", + label: "Connection timeout", + message: + "The endpoint did not complete the handshake in time. Check proxy path, host reachability, and whether the backend is overloaded or hanging.", + }; + } + + return null; +}; + +export const summarizeChecks = (checks) => { + let hasFail = false; + let hasWarn = false; + for (const check of checks) { + if (check.status === DOCTOR_STATUSES.fail) hasFail = true; + if (check.status === DOCTOR_STATUSES.warn) hasWarn = true; + } + if (hasFail) return DOCTOR_STATUSES.fail; + if (hasWarn) return DOCTOR_STATUSES.warn; + return DOCTOR_STATUSES.pass; +}; + +export const shouldRunHermesChecks = ({ runtimeContext, env = process.env }) => + runtimeContext.adapterType === "hermes" || + Boolean( + trimString(env.HERMES_API_URL) || trimString(env.HERMES_ADAPTER_PORT), + ); + +export const shouldRunOpenClawChecks = ({ + runtimeContext, + openclawConfigExists = false, +}) => runtimeContext.adapterType === "openclaw" || openclawConfigExists; + +export const shouldRunDemoChecks = ({ runtimeContext, env = process.env }) => + runtimeContext.adapterType === "demo" || + Boolean(trimString(env.DEMO_ADAPTER_PORT)); + +export const shouldRunCustomChecks = ({ runtimeContext }) => + isCustomRuntimeAdapter(runtimeContext.adapterType); + +export const formatDoctorReport = ({ + summary, + runtimeContext, + paths, + checks, +}) => { + const summaryCounts = { + pass: checks.filter((check) => check.status === DOCTOR_STATUSES.pass) + .length, + warn: checks.filter((check) => check.status === DOCTOR_STATUSES.warn) + .length, + fail: checks.filter((check) => check.status === DOCTOR_STATUSES.fail) + .length, + }; + const groupedChecks = new Map(); + for (const check of checks) { + const category = check.category || "General"; + const entries = groupedChecks.get(category) ?? []; + entries.push(check); + groupedChecks.set(category, entries); + } + const lines = []; + lines.push("=================================================="); + lines.push(`Claw3Doctor ${formatStatusBadge(summary)}`); + lines.push("=================================================="); + lines.push(""); + lines.push(`Runtime provider: ${runtimeContext.adapterType}`); + lines.push( + `Selected profile: ${runtimeContext.gatewayUrl || "(not configured)"}`, + ); + lines.push( + `Gateway token: ${runtimeContext.tokenConfigured ? "configured" : "missing"}`, + ); + lines.push(`State dir: ${paths.stateDir}`); + lines.push(`Studio settings: ${paths.settingsPath}`); + const configuredProfiles = Object.entries(runtimeContext.profiles ?? {}); + if (configuredProfiles.length > 0) { + lines.push("Configured profiles:"); + for (const [adapterType, profile] of configuredProfiles) { + lines.push(` - ${adapterType}: ${profile.url}`); + } + } + lines.push( + `Check counts: ${summaryCounts.pass} pass, ${summaryCounts.warn} warn, ${summaryCounts.fail} fail`, + ); + lines.push(""); + for (const [category, categoryChecks] of groupedChecks.entries()) { + lines.push(`${category}`); + lines.push("-".repeat(category.length)); + for (const check of categoryChecks) { + lines.push( + ` ${formatStatusBadge(check.status)} ${check.label}: ${check.message}`, + ); + } + lines.push(""); + } + const actions = checks.flatMap((check) => check.actions ?? []); + if (actions.length > 0) { + lines.push("Suggested next actions:"); + actions.forEach((action, index) => { + lines.push(`${index + 1}. ${action}`); + }); + } + return lines.join("\n"); +}; + +export const buildDoctorJsonReport = ({ + summary, + runtimeContext, + paths, + checks, +}) => ({ + doctor: "claw3doctor", + summary, + runtimeContext, + paths, + checks, + counts: { + pass: checks.filter((check) => check.status === DOCTOR_STATUSES.pass) + .length, + warn: checks.filter((check) => check.status === DOCTOR_STATUSES.warn) + .length, + fail: checks.filter((check) => check.status === DOCTOR_STATUSES.fail) + .length, + }, +}); + +/** + * Parse CLI argv into structured doctor args. + * Exported so the flag behaviour can be unit tested without spawning a process. + */ +export const parseDoctorArgs = (argv) => { + const args = { + json: false, + allProfiles: false, + profile: null, + }; + for (let index = 0; index < argv.length; index += 1) { + const entry = argv[index]; + if (entry === "--json") { + args.json = true; + continue; + } + if (entry === "--all-profiles") { + args.allProfiles = true; + continue; + } + if (entry === "--profile") { + const next = trimString(argv[index + 1] ?? "").toLowerCase(); + if (next) { + args.profile = next; + index += 1; + } + } + } + return args; +}; diff --git a/server/access-gate.js b/server/access-gate.js index 1f5e72e..3c75d2c 100644 --- a/server/access-gate.js +++ b/server/access-gate.js @@ -61,6 +61,24 @@ const createRateLimiter = (maxAttempts = 10, windowMs = 60_000) => { }; }; +/** + * Resolve client IP for rate limiting. + * When TRUSTED_PROXY=1 is set, the first value of X-Forwarded-For is used. + * Only set TRUSTED_PROXY=1 when this server sits behind a reverse proxy that + * you control (nginx, Caddy, Vercel edge). Without it, X-Forwarded-For is + * ignored to prevent spoofing by direct clients. + */ +const resolveClientIp = (req) => { + if (process.env.TRUSTED_PROXY === "1") { + const forwarded = req.headers?.["x-forwarded-for"]; + if (typeof forwarded === "string") { + const first = forwarded.split(",")[0]?.trim(); + if (first) return first; + } + } + return req.socket?.remoteAddress || "unknown"; +}; + function createAccessGate(options) { const token = String(options?.token ?? "").trim(); const cookieName = String(options?.cookieName ?? "studio_access").trim() || "studio_access"; @@ -70,7 +88,7 @@ function createAccessGate(options) { const getAuthState = (req) => { if (!enabled) return { authorized: true, limited: false }; - const ip = req.socket?.remoteAddress || "unknown"; + const ip = resolveClientIp(req); const cookieHeader = req.headers?.cookie; const cookies = parseCookies(cookieHeader); const authorized = safeCompare(cookies[cookieName] || "", token); diff --git a/server/demo-gateway-adapter.js b/server/demo-gateway-adapter.js index b83f5b3..44ac45d 100644 --- a/server/demo-gateway-adapter.js +++ b/server/demo-gateway-adapter.js @@ -91,17 +91,28 @@ function agentListPayload() { function buildDemoReply(agent, message) { const normalized = message.trim(); + const compactMessage = normalized.replace(/\s+/g, " ").trim(); + const greetingOnly = /^(hi|hello|hey|yo|sup|what'?s up|how are you)[!.? ]*$/i.test(compactMessage); const opening = agent.role === "Orchestrator" ? `${agent.name} here. Demo office is live and the team is synced.` - : `${agent.name} reporting in from the ${agent.role.toLowerCase()} desk.`; + : `${agent.name} checking in from the ${agent.role.toLowerCase()} desk.`; + if (greetingOnly) { + return agent.role === "Orchestrator" + ? `${opening} I can coordinate the room, sketch a plan, or hand work to Research and Builder.` + : `${opening} Give me a concrete task and I will respond in-character with a focused next step.`; + } + const focusLine = + compactMessage.length > 160 + ? `${compactMessage.slice(0, 160).trimEnd()}...` + : compactMessage; const action = agent.role === "Research" - ? "I would break this down into sources, constraints, and next questions." + ? "I would turn this into source checks, constraints, and follow-up questions." : agent.role === "Builder" - ? "I would turn that into concrete implementation steps and validation." - : "I can coordinate the team, route work, and summarize progress."; - return `${opening} You said: "${normalized}". ${action}`; + ? "I would translate this into implementation steps, edge cases, and validation." + : "I would route the work, keep the team aligned, and summarize the next move."; + return `${opening} Focus: ${focusLine}. ${action}`; } async function handleMethod(method, params, id, sendEvent) { diff --git a/server/gateway-proxy.js b/server/gateway-proxy.js index 71e542e..cb6167d 100644 --- a/server/gateway-proxy.js +++ b/server/gateway-proxy.js @@ -221,12 +221,36 @@ function createGatewayProxy(options) { return; } - const connectFrame = browserHasAuth + const baseConnectFrame = browserHasAuth ? frame : { ...frame, params: injectAuthToken(frame.params, upstreamToken), }; + + const connectParams = isObject(baseConnectFrame.params) + ? { ...baseConnectFrame.params } + : {}; + const hasDeviceAuth = hasCompleteDeviceAuth(connectParams); + const client = isObject(connectParams.client) ? { ...connectParams.client } : {}; + const clientId = typeof client.id === "string" ? client.id.trim() : ""; + + if ( + upstreamAdapterType === "openclaw" && + clientId === "openclaw-control-ui" && + !hasDeviceAuth + ) { + client.id = "webchat-ui"; + connectParams.client = client; + if (isObject(connectParams.device) && !hasCompleteDeviceAuth(connectParams)) { + delete connectParams.device; + } + } + + const connectFrame = { + ...baseConnectFrame, + params: connectParams, + }; upstreamWs.send(JSON.stringify(connectFrame)); }; diff --git a/src/app/api/files/[file]/route.ts b/src/app/api/files/[file]/route.ts new file mode 100644 index 0000000..380df5d --- /dev/null +++ b/src/app/api/files/[file]/route.ts @@ -0,0 +1,63 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { NextResponse } from "next/server"; + +import { resolveStateDir } from "@/lib/clawdbot/paths"; + +export const runtime = "nodejs"; + +const uploadsDir = () => path.join(resolveStateDir(), "claw3d", "uploads"); + +const contentTypeFromName = (fileName: string): string => { + const ext = path.extname(fileName).toLowerCase(); + switch (ext) { + case ".png": + return "image/png"; + case ".jpg": + case ".jpeg": + return "image/jpeg"; + case ".gif": + return "image/gif"; + case ".webp": + return "image/webp"; + case ".pdf": + return "application/pdf"; + case ".md": + case ".markdown": + return "text/markdown; charset=utf-8"; + case ".json": + return "application/json; charset=utf-8"; + case ".csv": + return "text/csv; charset=utf-8"; + default: + return "text/plain; charset=utf-8"; + } +}; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ file: string }> } +) { + try { + const { file } = await params; + const safeFile = path.basename(file); + const targetPath = path.join(uploadsDir(), safeFile); + const resolvedUploads = path.resolve(uploadsDir()); + const resolvedTarget = path.resolve(targetPath); + if (!resolvedTarget.startsWith(`${resolvedUploads}${path.sep}`) && resolvedTarget !== resolvedUploads) { + return NextResponse.json({ error: "Invalid file path." }, { status: 400 }); + } + + const bytes = await fs.readFile(resolvedTarget); + return new Response(new Blob([Uint8Array.from(bytes)], { type: contentTypeFromName(safeFile) }), { + headers: { + "Content-Type": contentTypeFromName(safeFile), + "Cache-Control": "no-store", + }, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "File not found."; + return NextResponse.json({ error: message }, { status: 404 }); + } +} diff --git a/src/app/api/files/upload/route.ts b/src/app/api/files/upload/route.ts new file mode 100644 index 0000000..2aa682d --- /dev/null +++ b/src/app/api/files/upload/route.ts @@ -0,0 +1,140 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { NextResponse } from "next/server"; + +import { resolveStateDir } from "@/lib/clawdbot/paths"; + +export const runtime = "nodejs"; + +const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; +const CONTENT_TYPE_BY_EXT: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".txt": "text/plain", + ".md": "text/markdown", + ".markdown": "text/markdown", + ".csv": "text/csv", + ".json": "application/json", + ".xml": "application/xml", + ".pdf": "application/pdf", + ".js": "text/plain", + ".jsx": "text/plain", + ".ts": "text/plain", + ".tsx": "text/plain", + ".py": "text/plain", + ".rb": "text/plain", + ".go": "text/plain", + ".rs": "text/plain", + ".java": "text/plain", + ".kt": "text/plain", + ".sql": "text/plain", + ".html": "text/plain", + ".css": "text/plain", + ".yaml": "text/plain", + ".yml": "text/plain", + ".log": "text/plain", +}; +const ALLOWED_CONTENT_TYPES = new Set([ + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + "text/plain", + "text/markdown", + "text/csv", + "application/json", + "application/xml", + "text/xml", + "application/pdf", +]); +const ALLOWED_EXTENSIONS = new Set(Object.keys(CONTENT_TYPE_BY_EXT)); + +const TEXT_CONTENT_TYPES = [ + "text/", + "application/json", + "application/xml", +]; + +const sanitizeFilename = (input: string): string => { + const cleaned = input.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); + return cleaned || "upload"; +}; + +const uploadsDir = () => path.join(resolveStateDir(), "claw3d", "uploads"); + +const isTextContentType = (contentType: string): boolean => + TEXT_CONTENT_TYPES.some((prefix) => contentType.startsWith(prefix)); + +const resolveUploadContentType = (file: File): string | null => { + const explicit = file.type.trim().toLowerCase(); + if (explicit && (ALLOWED_CONTENT_TYPES.has(explicit) || explicit.startsWith("text/"))) { + return explicit; + } + const ext = path.extname(file.name || "").trim().toLowerCase(); + if (!ext || !ALLOWED_EXTENSIONS.has(ext)) { + return null; + } + return CONTENT_TYPE_BY_EXT[ext] ?? null; +}; + +export async function POST(request: Request) { + try { + const formData = await request.formData(); + const file = formData.get("file"); + if (!(file instanceof File)) { + return NextResponse.json({ error: "No file uploaded." }, { status: 400 }); + } + if (file.size <= 0) { + return NextResponse.json({ error: "Uploaded file is empty." }, { status: 400 }); + } + if (file.size > MAX_UPLOAD_BYTES) { + return NextResponse.json({ error: "File exceeds 10 MB limit." }, { status: 400 }); + } + + const contentType = resolveUploadContentType(file); + if (!contentType) { + const ext = path.extname(file.name || "").trim().toLowerCase(); + return NextResponse.json( + { error: `Unsupported file type: ${ext || file.type.trim().toLowerCase() || "(unknown)"}` }, + { status: 400 } + ); + } + + const fileId = crypto.randomBytes(8).toString("hex"); + const safeName = sanitizeFilename(file.name || "upload"); + const storedName = `${fileId}-${safeName}`; + const targetDir = uploadsDir(); + const targetPath = path.join(targetDir, storedName); + + await fs.mkdir(targetDir, { recursive: true }); + const bytes = Buffer.from(await file.arrayBuffer()); + await fs.writeFile(targetPath, bytes); + + let extractedText: string | undefined; + if (isTextContentType(contentType)) { + const normalizedText = bytes.toString("utf8").trim(); + if (normalizedText) { + extractedText = + normalizedText.length > 12_000 + ? `${normalizedText.slice(0, 12_000).trimEnd()}\n[Truncated]` + : normalizedText; + } + } + + return NextResponse.json({ + id: fileId, + name: file.name || safeName, + url: `/api/files/${storedName}`, + contentType, + extractedText, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Upload failed."; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/office/presence/route.ts b/src/app/api/office/presence/route.ts index acd3ccb..21bbdb3 100644 --- a/src/app/api/office/presence/route.ts +++ b/src/app/api/office/presence/route.ts @@ -1,9 +1,15 @@ import { NextResponse } from "next/server"; +import type { + SummaryPreviewSnapshot, + SummaryStatusSnapshot, +} from "@/features/agents/state/runtimeEventBridge"; import { fetchRemoteOfficePresenceSnapshot, loadOfficePresenceSnapshot, } from "@/lib/office/presence"; +import { buildOfficePresenceSnapshotFromGateway } from "@/lib/office/gatewayPresence"; +import { NodeGatewayClient, buildAgentMainSessionKey } from "@/lib/gateway/nodeGatewayClient"; import { loadStudioSettings } from "@/lib/studio/settings-store"; import { resolveOfficePreference } from "@/lib/studio/settings"; @@ -14,13 +20,15 @@ export async function GET(request: Request) { const url = new URL(request.url); const source = url.searchParams.get("source")?.trim() || "local"; const workspaceId = url.searchParams.get("workspaceId")?.trim() || "default"; - if (source === "remote") { + if (source === "remote" || source === "remote_gateway") { const settings = loadStudioSettings(); const gatewayUrl = settings.gateway?.url?.trim() || ""; const officePreference = resolveOfficePreference(settings, gatewayUrl); if ( !officePreference.remoteOfficeEnabled || - !officePreference.remoteOfficePresenceUrl.trim() + (source === "remote" + ? !officePreference.remoteOfficePresenceUrl.trim() + : !officePreference.remoteOfficeGatewayUrl.trim()) ) { return NextResponse.json( { @@ -31,22 +39,71 @@ export async function GET(request: Request) { { headers: { "Cache-Control": "no-store" } } ); } + if (source === "remote") { + const startedAt = Date.now(); + console.info("[office-presence] Fetching remote office presence.", { + presenceUrl: officePreference.remoteOfficePresenceUrl, + tokenConfigured: Boolean(officePreference.remoteOfficeToken?.trim()), + }); + const snapshot = await fetchRemoteOfficePresenceSnapshot({ + presenceUrl: officePreference.remoteOfficePresenceUrl, + token: officePreference.remoteOfficeToken, + timeoutMs: 15_000, + }); + console.info("[office-presence] Remote office presence loaded.", { + presenceUrl: officePreference.remoteOfficePresenceUrl, + elapsedMs: Date.now() - startedAt, + agentCount: snapshot.agents.length, + }); + return NextResponse.json(snapshot, { headers: { "Cache-Control": "no-store" } }); + } + const startedAt = Date.now(); - console.info("[office-presence] Fetching remote office presence.", { - presenceUrl: officePreference.remoteOfficePresenceUrl, - tokenConfigured: Boolean(officePreference.remoteOfficeToken?.trim()), - }); - const snapshot = await fetchRemoteOfficePresenceSnapshot({ - presenceUrl: officePreference.remoteOfficePresenceUrl, - token: officePreference.remoteOfficeToken, - timeoutMs: 15_000, - }); - console.info("[office-presence] Remote office presence loaded.", { - presenceUrl: officePreference.remoteOfficePresenceUrl, - elapsedMs: Date.now() - startedAt, - agentCount: snapshot.agents.length, - }); - return NextResponse.json(snapshot, { headers: { "Cache-Control": "no-store" } }); + const gatewayClient = new NodeGatewayClient(); + try { + await gatewayClient.connect({ + gatewayUrl: officePreference.remoteOfficeGatewayUrl, + token: officePreference.remoteOfficeToken, + }); + const agentsResult = (await gatewayClient.request("agents.list", {})) as { + mainKey?: string; + agents?: Array<{ id?: string; name?: string; identity?: { name?: string } }>; + }; + const statusSummary = (await gatewayClient.request( + "status", + {}, + )) as SummaryStatusSnapshot; + const remoteAgentIds = Array.isArray(agentsResult.agents) + ? agentsResult.agents + .map((agent) => (typeof agent.id === "string" ? agent.id.trim() : "")) + .filter((agentId) => agentId.length > 0) + : []; + const sessionKeys = remoteAgentIds.map((agentId) => + buildAgentMainSessionKey(agentId, agentsResult.mainKey?.trim() || "main"), + ); + const previewSnapshot: SummaryPreviewSnapshot | null = + sessionKeys.length > 0 + ? ((await gatewayClient.request("sessions.preview", { + keys: sessionKeys, + limit: 8, + maxChars: 240, + })) as SummaryPreviewSnapshot) + : null; + const snapshot = buildOfficePresenceSnapshotFromGateway({ + agentsResult, + statusSummary, + previewSnapshot, + workspaceId: "remote-gateway", + }); + console.info("[office-presence] Remote gateway presence loaded.", { + gatewayUrl: officePreference.remoteOfficeGatewayUrl, + elapsedMs: Date.now() - startedAt, + agentCount: snapshot.agents.length, + }); + return NextResponse.json(snapshot, { headers: { "Cache-Control": "no-store" } }); + } finally { + gatewayClient.close(); + } } const snapshot = loadOfficePresenceSnapshot(workspaceId); return NextResponse.json(snapshot, { headers: { "Cache-Control": "no-store" } }); diff --git a/src/app/api/office/remote-handoff/route.ts b/src/app/api/office/remote-handoff/route.ts new file mode 100644 index 0000000..9a5b3a7 --- /dev/null +++ b/src/app/api/office/remote-handoff/route.ts @@ -0,0 +1,101 @@ +import { randomUUID } from "node:crypto"; +import { NextResponse } from "next/server"; +import { NodeGatewayClient } from "@/lib/gateway/nodeGatewayClient"; +import { sendAgentHandoffViaRuntime } from "@/lib/runtime/agentMessaging"; +import { loadStudioSettings } from "@/lib/studio/settings-store"; +import { resolveOfficePreference } from "@/lib/studio/settings"; + +export const runtime = "nodejs"; +const MAX_REMOTE_MESSAGE_CHARS = 2_000; + +const stripRemoteAgentPrefix = (agentId: string) => + agentId.startsWith("remote:") ? agentId.slice("remote:".length) : agentId; + +export async function POST(request: Request) { + const gatewayClient = new NodeGatewayClient(); + try { + const body = (await request.json()) as { + agentId?: unknown; + task?: unknown; + context?: unknown; + deliverables?: unknown; + acceptanceCriteria?: unknown; + }; + const requestedAgentId = + typeof body.agentId === "string" ? stripRemoteAgentPrefix(body.agentId.trim()) : ""; + const task = typeof body.task === "string" ? body.task.trim() : ""; + const context = typeof body.context === "string" ? body.context.trim() : ""; + const acceptanceCriteria = + typeof body.acceptanceCriteria === "string" ? body.acceptanceCriteria.trim() : ""; + const deliverables = Array.isArray(body.deliverables) + ? body.deliverables.filter((entry): entry is string => typeof entry === "string") + : []; + + if (!requestedAgentId) { + return NextResponse.json({ error: "Remote agent ID is required." }, { status: 400 }); + } + if (!task) { + return NextResponse.json({ error: "Remote handoff task is required." }, { status: 400 }); + } + if (task.length > MAX_REMOTE_MESSAGE_CHARS) { + return NextResponse.json( + { error: `Remote handoff must be ${MAX_REMOTE_MESSAGE_CHARS} characters or fewer.` }, + { status: 400 }, + ); + } + + const settings = loadStudioSettings(); + const gatewayUrl = settings.gateway?.url?.trim() || ""; + const officePreference = resolveOfficePreference(settings, gatewayUrl); + if (!officePreference.remoteOfficeEnabled) { + return NextResponse.json({ error: "Remote office is disabled." }, { status: 400 }); + } + if (officePreference.remoteOfficeSourceKind !== "openclaw_gateway") { + return NextResponse.json( + { error: "Remote handoffs currently work only with the remote gateway source." }, + { status: 400 }, + ); + } + const remoteGatewayUrl = officePreference.remoteOfficeGatewayUrl.trim(); + if (!remoteGatewayUrl) { + return NextResponse.json( + { error: "Remote office gateway URL is not configured." }, + { status: 400 }, + ); + } + + await gatewayClient.connect({ + gatewayUrl: remoteGatewayUrl, + token: officePreference.remoteOfficeToken, + }); + + const handoffResult = (await sendAgentHandoffViaRuntime( + { call: gatewayClient.request.bind(gatewayClient) }, + { + targetAgentId: requestedAgentId, + task, + sourceLabel: "another office user", + context: context || undefined, + acceptanceCriteria: acceptanceCriteria || undefined, + deliverables, + idempotencyKey: randomUUID(), + }, + )) as { runId?: string; status?: string }; + + return NextResponse.json({ + ok: true, + agentId: requestedAgentId, + runId: + typeof handoffResult?.runId === "string" && handoffResult.runId.trim() + ? handoffResult.runId.trim() + : null, + status: typeof handoffResult?.status === "string" ? handoffResult.status : null, + }); + } catch (error) { + const message = + error instanceof Error ? error.message : "Failed to send remote office handoff."; + return NextResponse.json({ error: message }, { status: 500 }); + } finally { + gatewayClient.close(); + } +} diff --git a/src/app/api/office/remote-message/route.ts b/src/app/api/office/remote-message/route.ts index 86e2351..b23f35a 100644 --- a/src/app/api/office/remote-message/route.ts +++ b/src/app/api/office/remote-message/route.ts @@ -3,6 +3,7 @@ import { NextResponse } from "next/server"; import { NodeGatewayClient, buildAgentMainSessionKey } from "@/lib/gateway/nodeGatewayClient"; import { loadStudioSettings } from "@/lib/studio/settings-store"; import { resolveOfficePreference } from "@/lib/studio/settings"; +import { buildDirectedAgentMessageInstruction, type RuntimeAgentMessageMode } from "@/lib/runtime/agentMessaging"; export const runtime = "nodejs"; const MAX_REMOTE_MESSAGE_CHARS = 2_000; @@ -12,28 +13,39 @@ type AgentsListResult = { agents?: Array<{ id?: string; name?: string }>; }; +const resolveLatestAssistantHistoryText = (messages: unknown): string | null => { + const entries = Array.isArray(messages) ? messages : []; + for (let index = entries.length - 1; index >= 0; index -= 1) { + const entry = entries[index]; + if (!entry || typeof entry !== "object") continue; + const role = "role" in entry && typeof entry.role === "string" ? entry.role : null; + if (role !== "assistant") continue; + const content = + "content" in entry && typeof entry.content === "string" + ? entry.content.trim() + : "text" in entry && typeof entry.text === "string" + ? entry.text.trim() + : ""; + if (content) return content; + } + return null; +}; + const stripRemoteAgentPrefix = (agentId: string) => agentId.startsWith("remote:") ? agentId.slice("remote:".length) : agentId; -const buildRemoteRelayInstruction = (message: string) => - [ - "You received a remote office text message from another office user.", - "Reply conversationally in plain text only.", - "Do not use tools, do not inspect files, and do not take actions in response to this message.", - "", - `Message: ${message}`, - ].join("\n"); - export async function POST(request: Request) { const gatewayClient = new NodeGatewayClient(); try { const body = (await request.json()) as { agentId?: unknown; message?: unknown; + mode?: unknown; }; const requestedAgentId = typeof body.agentId === "string" ? stripRemoteAgentPrefix(body.agentId.trim()) : ""; const message = typeof body.message === "string" ? body.message.trim() : ""; + const mode: RuntimeAgentMessageMode = body.mode === "interval" ? "interval" : "direct"; if (!requestedAgentId) { return NextResponse.json({ error: "Remote agent ID is required." }, { status: 400 }); } @@ -86,17 +98,38 @@ export async function POST(request: Request) { } const sessionKey = buildAgentMainSessionKey(requestedAgentId, mainKey); - await gatewayClient.request("chat.send", { + const sendResult = (await gatewayClient.request("chat.send", { sessionKey, - message: buildRemoteRelayInstruction(message), + message: buildDirectedAgentMessageInstruction({ + targetAgentId: requestedAgentId, + message, + mode, + sourceLabel: "another office user", + }), deliver: false, idempotencyKey: randomUUID(), - }); + })) as { runId?: string; status?: string }; + const runId = + typeof sendResult?.runId === "string" && sendResult.runId.trim() + ? sendResult.runId.trim() + : null; + if (runId) { + await gatewayClient.request("agent.wait", { + runId, + timeoutMs: mode === "interval" ? 8_000 : 15_000, + }); + } + const historyResult = (await gatewayClient.request("chat.history", { + sessionKey, + limit: 8, + })) as { messages?: unknown }; + const assistantText = resolveLatestAssistantHistoryText(historyResult.messages); return NextResponse.json({ ok: true, agentId: requestedAgentId, sessionKey, + assistantText, }); } catch (error) { const message = diff --git a/src/app/api/runtime/custom/route.ts b/src/app/api/runtime/custom/route.ts index 9f85d5f..0377238 100644 --- a/src/app/api/runtime/custom/route.ts +++ b/src/app/api/runtime/custom/route.ts @@ -84,6 +84,8 @@ export async function POST(request: Request) { const runtimeUrl = normalizeRuntimeUrl(payload.runtimeUrl ?? ""); const pathname = normalizePathname(payload.pathname); const method = normalizeMethod(payload.method); + // Propagate the browser abort signal so that cancelling the client-side fetch + // (e.g. hitting Stop) also cancels the upstream runtime request. const response = await fetch(`${runtimeUrl}${pathname}`, { method, headers: { @@ -92,6 +94,7 @@ export async function POST(request: Request) { }, body: method === "POST" ? JSON.stringify(payload.body ?? {}) : undefined, cache: "no-store", + signal: request.signal, }); const text = await response.text(); return new NextResponse(text, { diff --git a/src/app/api/studio/route.ts b/src/app/api/studio/route.ts index cd9653f..888800f 100644 --- a/src/app/api/studio/route.ts +++ b/src/app/api/studio/route.ts @@ -24,6 +24,9 @@ export async function GET() { { settings: sanitizeStudioSettings(settings), localGatewayDefaults: sanitizeStudioGatewaySettings(localGatewayDefaults), + // gatewayPrivate and localGatewayDefaultsPrivate are intentionally omitted. + // Upstream tokens must not cross the browser API boundary — the Studio proxy + // (server/gateway-proxy.js) injects the server-side token into connect frames. }, { headers: { "Cache-Control": "no-store" } } ); @@ -46,7 +49,11 @@ export async function PUT(request: Request) { } const settings = applyStudioSettingsPatch(body); return NextResponse.json( - { settings: sanitizeStudioSettings(settings) }, + { + settings: sanitizeStudioSettings(settings), + localGatewayDefaults: sanitizeStudioGatewaySettings(loadLocalGatewayDefaults()), + // gatewayPrivate intentionally omitted — see GET handler comment. + }, { headers: { "Cache-Control": "no-store" } } ); } catch (err) { diff --git a/src/features/agents/components/AgentChatPanel.tsx b/src/features/agents/components/AgentChatPanel.tsx index 9ec3b94..4a2f34f 100644 --- a/src/features/agents/components/AgentChatPanel.tsx +++ b/src/features/agents/components/AgentChatPanel.tsx @@ -13,7 +13,7 @@ import { import type { AgentState as AgentRecord } from "@/features/agents/state/store"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -import { Check, ChevronRight, Clock, Mic, Pencil, Square, Trash2, X } from "lucide-react"; +import { Check, ChevronRight, Clock, Mic, Paperclip, Pencil, Square, Trash2, X } from "lucide-react"; import type { GatewayModelChoice } from "@/lib/gateway/models"; import type { AgentAvatarProfile } from "@/lib/avatars/profile"; import { rewriteMediaLinesToMarkdown } from "@/lib/text/media-markdown"; @@ -25,6 +25,7 @@ import type { ExecApprovalDecision, PendingExecApproval, } from "@/features/agents/approvals/types"; +import type { RuntimeAttachment } from "@/lib/runtime/types"; import { buildAgentChatRenderBlocks, buildFinalAgentChatItems, @@ -64,6 +65,76 @@ const EMPTY_CHAT_INTRO_MESSAGES = [ "What are we working on today?", "I'm here and ready. What's the plan?", ]; +const TEXT_ATTACHMENT_EXTENSIONS = new Set([ + "txt", + "md", + "markdown", + "json", + "js", + "jsx", + "ts", + "tsx", + "py", + "rb", + "go", + "rs", + "java", + "kt", + "sql", + "html", + "css", + "xml", + "yaml", + "yml", + "csv", + "log", +]); +const MAX_ATTACHMENT_TEXT_CHARS = 12_000; +const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; + +type UploadAttachment = { + id: string; + name: string; + url: string; + contentType: string; + extractedText?: string; +}; + +const isTextAttachmentFile = (file: File): boolean => { + const mime = file.type.trim().toLowerCase(); + if (mime.startsWith("text/")) return true; + if ( + mime.includes("json") || + mime.includes("javascript") || + mime.includes("typescript") || + mime.includes("xml") || + mime.includes("yaml") + ) { + return true; + } + const extension = file.name.split(".").pop()?.trim().toLowerCase() ?? ""; + return extension.length > 0 && TEXT_ATTACHMENT_EXTENSIONS.has(extension); +}; + +const buildAttachmentPromptBlock = (fileName: string, content: string): string => + [ + `[Attached reference: ${fileName}]`, + content, + `[End attached reference: ${fileName}]`, + ].join("\n"); + +const buildUploadedAttachmentPromptBlock = (attachment: UploadAttachment): string => { + const lines = [ + `[Attached file: ${attachment.name}]`, + `URL: ${attachment.url}`, + `Content-Type: ${attachment.contentType}`, + ]; + if (attachment.extractedText) { + lines.push("", attachment.extractedText); + } + lines.push(`[End attached file: ${attachment.name}]`); + return lines.join("\n"); +}; const stableStringHash = (value: string): number => { let hash = 0; @@ -133,7 +204,7 @@ type AgentChatPanelProps = { onToolCallingToggle?: (enabled: boolean) => void; onThinkingTracesToggle?: (enabled: boolean) => void; onDraftChange: (value: string) => void; - onSend: (message: string) => void; + onSend: (message: string, attachments: RuntimeAttachment[]) => void; onRemoveQueuedMessage?: (index: number) => void; onStopRun: () => void; onAvatarShuffle: () => void; @@ -882,6 +953,9 @@ const AgentChatComposer = memo(function AgentChatComposer({ onChange, onKeyDown, onSend, + onAttachmentFiles, + attachments, + onRemoveAttachment, onVoiceToggle, onStop, canSend, @@ -893,6 +967,8 @@ const AgentChatComposer = memo(function AgentChatComposer({ voiceSupported, voiceState, voiceError, + attachmentStatus, + attachmentInputRef, queuedMessages, onRemoveQueuedMessage, inputRef, @@ -911,6 +987,9 @@ const AgentChatComposer = memo(function AgentChatComposer({ onChange: (event: ChangeEvent) => void; onKeyDown: (event: KeyboardEvent) => void; onSend: () => void; + onAttachmentFiles: (event: ChangeEvent) => void; + attachments: UploadAttachment[]; + onRemoveAttachment: (id: string) => void; onVoiceToggle?: () => void; onStop: () => void; canSend: boolean; @@ -922,6 +1001,8 @@ const AgentChatComposer = memo(function AgentChatComposer({ voiceSupported: boolean; voiceState: VoiceRecorderState; voiceError?: string | null; + attachmentStatus?: string | null; + attachmentInputRef: MutableRefObject; queuedMessages: string[]; onRemoveQueuedMessage?: (index: number) => void; inputRef: (el: HTMLTextAreaElement | HTMLInputElement | null) => void; @@ -1125,7 +1206,43 @@ const AgentChatComposer = memo(function AgentChatComposer({ ) : null} - {voiceStatusText || voiceError ? ( + {attachments.length > 0 ? ( +
+ {attachments.map((attachment) => { + const isImage = attachment.contentType.startsWith("image/"); + return ( +
+ {isImage ? ( + {attachment.name} + ) : ( +
+ File +
+ )} + +
+ {attachment.name} +
+
+ ); + })} +
+ ) : null} + {voiceStatusText || voiceError || attachmentStatus ? (
- {voiceError ?? voiceStatusText} + {voiceError ?? voiceStatusText ?? attachmentStatus}
) : null}
+