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}
+