Cloud project collaboration architecture
The current V4 UI source of truth is
/Users/hongyu9/Downloads/wework-delivery-v4-TODO.pen. Implement the interaction from that design instead of deriving page layout from this document.
Goalβ
A cloud project is the shared collaboration and storage boundary for a team. Members may select the same cloud project as the default destination of their own local projects, execute work in Wework, and submit selected conversations, files, and Markdown as immutable delivery snapshots.
A cloud project is not the existing Project model:
Projectis a user-owned local execution workspace containing device, path, Git, and runtime configuration.CloudProjectis a shared aggregate containing membership, TODOs, shared files, and a MinIO namespace.- Local projects owned by different members may independently select the same cloud project; the cloud project stores no reverse link.
- One TODO may link to many Wework Tasks, while one Task may process at most one active TODO at a time.
Client reuse boundaryβ
Execution configuration and waiting statesβ
Project settings keep collaboration organization separate from runtime resources:
- Collaboration members presents project participants in the order Agents β Project members β Collaboration groups. A collaboration group is a reusable organization whose members and leader may be humans or Agents. Workflow stages belong to the collaboration group.
- Automatic processing defines trigger rules only. Issue creation, Tag changes, external events, or schedules route work to a project member, Agent, or collaboration group. A rule never binds a device.
- Execution environments manages the project's authorized device pool. Agent creation does not select a device. Manual assignments and automatic processing resolve a device from this pool when a Run is claimed.
A Mention never changes the assignee; explicitly mentioning an available agent can trigger comment execution. Assignment, Mention, Subscription, and Run have independent semantics. Explicit assignment changes ownership and creates a Run when the target is an Agent or collaboration group.
Model identity and provider options remain opaque dictionaries and are not subject to API field case conversion. Device presence comes from connection heartbeats. If model or workspace configuration is missing, the execution remains in waiting_runtime and uses the unified runtime-configuration entrypoint; the device itself is bound when the Run is claimed.
Successful planning and assignment by the coordinator does not mean the Issue is complete. Parent steps and child details display the child's execution state. Missing model or workspace configuration keeps an execution in waiting_runtime; Configure and continue completes that existing execution's profile. If no device currently satisfies authorization and capacity constraints, the Run remains queued for device claim instead of requiring a device binding on the Agent. Completing runtime configuration does not change project or Agent defaults and preserves manual approval requirements. Workflow progress counts steps only after acceptance.
Wegent Web replaces the former Inbox entry with Collaboration and directly reuses the Backend APIs for cloud projects, board Issues, comments, attachments, shared files, members, and execution records. Web and Wework do not maintain a second domain model or API surface.
Cross-client types, API clients, copy, test contracts, and host-independent React components live in packages/collaboration. CollaborationApp is the sole primary interface for cloud projects in both Web and Wework. It owns the project home, board, Issue details, comments, attachments, files, members, runs, and project settings; the clients must not maintain parallel cloud-collaboration pages.
Web supplies Next.js routing, notifications, and external-link behavior through a host adapter. Wework uses the same adapter to inject local project storage and a Desktop tools entry. Only local projects, terminals, device execution, AI orchestration, and other behavior that depends on Electron, the local filesystem, or the local executor may enter the desktop-specific workspace. New portable behavior must land in the shared package first instead of being copied into both hosts and synchronized later.
The shared package owns business state, field structure, and interaction contracts, but it must not duplicate a host's existing design system. Dialogs, tabs, selects, inputs, and primary actions should be injected through an explicit host adapter; the shared package keeps only a neutral default for host-independent use. Hosts pass brand colors through semantic CSS variables instead of hard-coding Web or Wework colors in shared components. The shared board sizing chain must preserve min-width: 0, min-height: 0, and vertical flex constraints so horizontal overflow remains inside the board scroll container instead of placing a page-level scrollbar above the remaining content.
The Collaboration members settings page uses a wide content container because Agents, project members, and collaboration-group forms need multi-column space; ordinary settings pages retain the default narrow container. Member and responsibility columns in collaboration-group details must use shrinkable minmax(0, ...) tracks, with min-width: 0 and truncation on text nodes. Fixed minimum track widths must not push form controls outside the panel, and heading actions remain on one line.
When chat messages enter a collaboration space, the Backend creates immutable message snapshots from a source Task the current user is authorized to access. The target may be a new Issue or a comment on an existing Issue. Clients must not write chat text directly as if it were a trusted snapshot.
Domain relationshipsβ
CloudProject
βββ ResourceMember(resource_type=CloudProject)
βββ ShareLink(resource_type=CloudProject)
βββ LoopItem
βββ LoopItemTaskBinding
β βββ TaskResource
β βββ Project (local execution workspace)
βββ Delivery
βββ DeliveryAsset
Data ownershipβ
| Data | Source of truth |
|---|---|
| Cloud projects, members, TODOs, task links, delivery metadata | Backend MySQL |
| Local paths, devices, Git, execution configuration, and default project-space reference | Device-local Codex project state |
| Shared files, Markdown, conversations, and delivery snapshots | MinIO/S3 |
| AI access to cloud data | MCP authorized by the Backend |
Objects are isolated by the cloud project's public ID:
projects/{cloud-project-public-id}/
shared/
loop-items/{loop-item-id}/
deliveries/{delivery-id}/
markdown.md
chat.json
manifest.json
files/
Finalized delivery prefixes are immutable. Later tasks may only read or copy them.
Data modelβ
CloudProjectβ
cloud_projects stores the shared project and never stores local runtime configuration.
id, public_id, project_key, name, description
created_by_user_id, storage_prefix, next_item_number
status, version, created_at, updated_at
Local-project default spaceβ
A local Codex project may store one { projectStore, projectId } default project-space reference. The reference belongs to device-local project state, never enters the Backend, and creates no reverse index on the project space. A new conversation may override or clear the default before its first message is sent.
LoopItemβ
The existing loop_items table stores cloud TODOs. cloud_project_id references cloud_projects, and sequence_number produces display identifiers such as WEG-18.
The initial fixed workflow is:
inbox β pending β in_progress β in_review β completed
Completed TODOs may be reopened into in_progress. Updates carry a version value and use optimistic locking.
Board execution by Bots and Agentsβ
A board assignee is either a project member or a project Bot (ProjectChatAgent). A Wegent Agent (Kind(kind=Team)) is runtime configuration for that Bot, not an assignee: the user creates a Bot in the board, selects Wegent as its execution environment, and binds one runnable Team. The binding lives in the Bot's existing metadata_json; no table is created.
Automation execution connection graphβ
Every edge has one owner:
| Edge | Sole responsibility | Current code owner |
|---|---|---|
| Entry β assignment | Validate member/Bot and persist assignee | loop_items/service.py, external_provider.py |
| Assignment β execution truth | Cancel the old attempt and create a new one | loop_item_executions/service.py |
| Automation β runtime activation | Activate the new execution after assignment commit | project_automation_execution.py |
| Project archive β automation cleanup | Disable and soft-delete every rule and clear its next trigger in the project-archive transaction | cloud_projects/service.py, project_automations.py |
| Wework activation | Local device pull or cloud consumer claim | robot_queue_tasks.py, Wework local puller |
| Settings β device total concurrency | Persist and immediately apply each scheduler limit through authenticated Runtime RPC; slot_used/slot_max are capacity projections only | devices.py, runtime_rpc_service.py, Rust runtime.settings.* |
| Wegent activation | Create Task/Subtask by execution ID and enter Team pipeline | board_team_execution.py, project_automation_tasks.py |
| Wegent board MCP injection | Backend detects board execution from native Task labels and injects the Backend MCP URL plus task-scoped authentication into the same ExecutionRequest used by ChatShell and Executor; it must not depend on a caller-owned temporary boolean | execution/request_builder.py, mcp_server/server.py |
| Backend board MCP β domain services | Expose the canonical local-Space-MCP tool names and operate through existing Backend CloudProject, LoopItem, file, attachment, delivery, and assignment services; never invoke the Wework local stdio MCP | mcp_server/tools/wework_space.py and the corresponding domain services |
| Wework local MCP | Started only by the Wework Runtime for local project-space and local-path capabilities; it must not replace the remote board MCP injected for a Wegent Runtime | executor/src/task_runtime/mcp.rs |
| Board execution β all runtime inputs | Local, cloud, and Wegent share one visible user input containing canonical IDs, the task URI, and the Bot execution prompt; the runtime reads task content through MCP | loop_item_executions/profile.py, board_team_execution.py |
| Wegent terminal β execution truth | Project terminal state after strict identity checks | board_team_completion.py |
| Wegent comment β native continuation | Resolve the exact backend_task_id from the reply target and create Subtasks in the same Task; project the result only to that reply without rewriting the terminal execution | board_team_continuation.py, project_automation_tasks.py |
| Wegent user stop β cancellation intent | Persist Task CANCELLING and execution cancel_requested, then send the Runtime cancellation command | chat_namespace.py, board_team_completion.py |
| Runtime cancellation ACK β terminal truth | After process-stop confirmation, persist Task/Subtask CANCELLED and project board cancelled through the unified terminal projector | Rust executor, status_updating.py, board_team_completion.py |
The Backend board MCP exposes the complete Backend cloud-board domain surface: get_current_context; space list/create/update; board-item list/search/create/get/update/reorder; assignment candidates and assign; provider comments; space-file list/read; item-attachment list/upload/read/delete; and delivery list/read. The remote MCP transfers file contents as inline text or Base64 and never accepts a Runtime-local file path. DingTalk AI Table dynamic field/record tools remain a Wework-local provider route and must not be faked when no Backend provider service exists.
The 2026-08-15 queue defect was a missing edge: HTTP assignment invoked Wegent activation, while an automation manager's internal assignment only created a queued execution. Consequently claimed_at and backend_task_id stayed empty, and device consumers correctly ignored records whose execution_environment=wegent. The fix must add the automation-to-runtime-activation edge. It must not send Wegent rows to a Wework device consumer or infer execution from the queue UI.
Automation assignment and execution sequenceβ
Review the sequence against these invariants, in order:
LoopItem.assignee_agent_idis always the board Bot; the Wegent Team exists only in Bot configuration and executionteam_id.- Runtime activation occurs only after assignee and execution commit, so consumers can always read the execution.
- Wegent dispatch locks an exact
execution_idand idempotently checksbackend_task_id; native Task labels and the execution binding commit together while the lock is held, so it never guesses the latest task or releases the lock before binding. queuedonly means execution intent is durable. The UI cannot show running before abackend_task_idor Runtime acceptance event exists.- Automation run, Bot execution, and native Wegent Task keep separate state boundaries. Only the unified projector may write board terminal truth after verifying every identity label; Runtime events and user stops both invoke it.
- Manual, API, scheduled, and AI-manager assignment converge on one runtime activator. New entry points must not copy dispatch logic.
- Failure to enqueue activation, or activation failure in the worker, must persist an explicit
failedterminal state; an execution with no remaining consumer must never stayqueued. - A Wegent UI/API stop first writes only
CANCELLING/cancel_requested. Both sides becomeCANCELLED/cancelledonly after a Runtime ACK or trustworthyCANCELLEDcallback. Delivery failure cannot invent terminal truth, and the frontend must await and display the server ACK. - All three runtimes use the same visible user input: canonical
project_id,task_id, andexecution_id, the taskcloud://URI, and the user-configured Bot execution prompt. The execution prompt never enters a Team/Ghost/Bot system prompt or hidden application context; MCP reads the latest task title, description, and state. - A Wegent comment continuation resolves the native Task from the reply target's exact
execution_idandbackend_task_id, then revalidates the thread-bound project Bot and original execution Team. It never infers a session from the latest execution, a device runtime list, or frontend memory. Each turn creates Subtasks in the same Task, preserves the execution's terminal state, and uses the reply comment only as that turn's display projection. A native Task may have at most onependingorstreamingcontinuation at a time so concurrent requests cannot overwrite the active Subtask label or cross-write projections. - Whenever native Wegent Task labels identify a board execution or board automation, Backend injects the board MCP on every request build. ChatShell and Executor consume the same injection result, and continuations never depend on MCP state left in a previous container.
- The Backend board MCP and Wework's native local Space MCP are separate runtime boundaries. Backend owns the former with Task Token authentication; Wework Runtime starts the latter locally. They share canonical tool names and domain semantics but never fall back to or overwrite each other.
- The Task Token's
task_id/subtask_idand native Task labels jointly scope the current board space. The model may operate on other items inside that space, but the current item, automation run, and execution identities are resolved by the server and are never guessed, and a Task Token cannot cross the current space boundary. - A project cannot be archived while it has an active automation run; Settings provides the cross-project stop control. Project archival and rule cleanup then commit in one transaction. Every rule is disabled, soft-deleted, and stripped of its next trigger; schedule scans only select rules whose parent project remains
active. Historical terminal runs remain as audit records and can never create new executions. - Device-wide concurrency belongs to each Runtime scheduler and is separate from Bot concurrency. Settings updates each device through authenticated Runtime RPC; an offline device cannot report a fabricated saved result. The sum of device
slot_maxvalues is capacity display only and never becomes execution-state truth.
Wegent board comment continuation sequenceβ
loop_item_executions is the sole source of truth for board execution state, while native tasks/subtasks own Team-internal execution. backend_task_id and labels containing the execution, Subtask, and Team identities fence the two records together. Messages and activity rows are presentation projections and never override execution truth.
A board-originated reassignment or stop first moves board truth to cancel_requested when a process may exist, or cancelled when execution provably has not started, then routes cancellation to the device Runtime or native Team Task. A native Wegent stop atomically persists Task CANCELLING and board execution cancel_requested. Only after the Runtime actually stops and calls back may Task/Subtask become CANCELLED; the unified terminal event then advances the board execution to cancelled. A UI click or delivered HTTP request is not a substitute for Runtime ACK. A delayed worker must recheck board execution truth after claiming and cannot start a cancelled run.
Execution scope remains owned by the board Bot. agent_id determines queue columns, assignment history, and concurrency identity; team_id records only the actual Wegent runtime target. Different board tasks may still enter the native Team pipeline concurrently, where Team collaboration configuration controls internal parallelism.
LoopItemTaskBindingβ
loop_item_task_bindings stores the historical many-to-many relationship between a TODO and concrete Wework Tasks. A runtime Task is identified by task_user_id + device_id + task_id, because a locally executed Task may not exist in the Backend tasks table; backend_task_id is only an optional index. Unlinking sets unlinked_at so execution provenance remains auditable.
The Wework local runtime classifies bindings as system or user. Every runtime task must retain one system binding to default-work-items. The current UI maintains at most one additional user binding, while the storage model can be extended to multiple user bindings later. Task-to-issue lookup prefers a user binding and falls back to the system binding. Runtime status, title, and archive synchronization update only the system binding; unlinking a user-selected board can soft-delete only the user binding and must not remove the system binding. The My tasks board reads only system issues for currently unarchived runtime tasks. It neither aggregates issues from other project spaces nor shows historical system issues whose runtime tasks have left the Task inventory.
Deliveryβ
deliveries and delivery_assets store immutable snapshot metadata. The nullable Delivery.source_task_binding_id points to a verified TODO/Task binding for local delivery and is null when a TODO is completed directly in the cloud UI.
Authorizationβ
Reuse resource_members and share_links with a new CloudProject resource type.
| Role | Read | Edit TODOs/files | Manage members | Archive project |
|---|---|---|---|---|
| Reporter | Yes | No | No | No |
| Developer | Yes | Yes | No | No |
| Maintainer | Yes | Yes | Yes | No |
| Owner | Yes | Yes | Yes | Yes |
Every TODO, delivery, file, and MCP request resolves the caller's cloud-project role first. Inaccessible resources return 404 to avoid disclosing their existence.
Service boundariesβ
cloud_projects/ projects and members
loop_items/ TODOs, state transitions, and Task bindings
delivery/ immutable delivery snapshots
cloud_files/ mutable shared files
mcp_server/tools/delivery.py authorized AI access to cloud references
Delivery services do not own TODO CRUD. LoopItem services do not access MinIO directly. MCP never holds or returns S3 credentials.
Delivery transactionβ
- Create a draft Delivery and write its Markdown and optional conversation object.
- Upload assets in bounded chunks and record size and SHA-256 metadata.
finalizelocks the Delivery and LoopItem and validates that the source Task is still linked to the TODO.- Write
manifest.json. - In one database transaction, mark the Delivery delivered, complete the TODO, and update
current_delivery_id. - If the database commit fails, remove the new manifest while keeping the draft retryable.
APIβ
/v1/cloud-projects
/v1/cloud-projects/{id}/members
/v1/cloud-projects/{id}/members/{user_id}
/v1/cloud-projects/{id}/files
/v1/cloud-projects/{id}/folders
/v1/cloud-projects/files/{file_id}
/v1/cloud-projects/{id}/loop-items
/v1/loop-items/{id}
/v1/loop-items/{id}/tasks
/v1/loop-items/{id}/start-task
/v1/loop-items/{id}/deliveries
/v1/deliveries/{id}
/v1/cloud-work-items/my-work
/v1/runtime-tasks/loop-item
Create boards and tasks with a personal API keyβ
Users can call the two creation endpoints with a personal API key while preserving the existing authorization and board-state rules. Both X-API-Key: wg-... and Authorization: Bearer wg-... are supported, and browser JWT authentication remains valid. Service keys cannot create boards or tasks as a user.
Create a board:
curl -X POST 'https://<host>/api/v1/cloud-projects' \
-H 'Content-Type: application/json' \
-H 'X-API-Key: wg-<personal-api-key>' \
-d '{
"project_key": "OPS",
"name": "Operations board",
"description": "Created through the API"
}'
Create a task with the board id returned by the previous request:
curl -X POST 'https://<host>/api/v1/cloud-projects/<project-id>/loop-items' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer wg-<personal-api-key>' \
-d '{
"title": "Check cloud execution state",
"description": "Keep the board as the source of truth",
"priority": "high",
"tags": ["api"]
}'
Task creation still passes through board membership authorization, status-definition validation, provider routing, and automation rules. If status is omitted, the task enters the board's inbox state. An unknown status returns 422, while an inaccessible private board returns 404 under the resource-hiding policy. These are create operations, not PUT upserts; callers should determine the outcome of an earlier POST before retrying to avoid duplicates.
Creation and updates use separate endpoints rather than PUT upsert. Shared files support folder creation, upload, rename/move, short-lived access, and recursive deletion. A move copies MinIO objects first, commits metadata, and only then removes the old objects; failed moves clean up newly copied objects.
When Wework adds a new runtime task to a cloud project space, it composes the existing primitives: create a LoopItem, then bind the runtime task; when execution status changes, read the task context and update the linked TODO. The Backend intentionally has no aggregate tracking endpoint dedicated to that orchestration. This allows the desktop app and Backend to be released independently while the stable TODO-creation, task-binding, and optimistic-locking APIs preserve the same behavior. The desktop app deduplicates concurrent association requests for the same runtime task and reuses a created TODO after a temporary binding failure to avoid duplicate cards.
The Wework Composer encodes cloud projects, directories, files, TODOs, and deliveries as atomic cloud:// references. Tasks carrying cloud-project context receive the Delivery MCP, and resolve_cloud_reference authorizes and resolves every reference in Backend so neither clients nor AI receive S3 credentials. The TODO board refreshes periodically while visible, while writes continue to use version optimistic locking for concurrent collaborators.
Delivery sequenceβ
- Add CloudProject, membership authorization, and local-project bindings.
- Move LoopItem ownership to CloudProject and add the state machine and optimistic locking.
- Add Task bindings and start-a-task-from-TODO.
- Migrate delivery authorization, source Task references, and MinIO paths.
- Add shared files and the cloud workspace MCP.
Project members and comment executionβ
Agent configuration visibility controls whether members can select an agent. Collaboration on an already authorized Issue uses project permissions and the comment thread's execution binding. A Developer can reply without gaining access to the executor's personal devices, models, or credentials.
- A reply to an AI thread continues its original execution identity and session, even after the Issue is reassigned.
- A new top-level comment uses an explicitly mentioned available agent, or the Issue's assigned agent. The existing execution queue starts an independent session and preserves approval and configuration-waiting states.
- Without an assigned or mentioned agent, the comment is saved without execution. Retrying that request after reassignment does not start AI unexpectedly.
- Mentioning another agent still requires picker visibility. Comments do not change the Issue assignee.
Clients send project, Issue, saved comment, and attachment IDs through wework:project_chat:comment:execute. Execution identity and device configuration are resolved on the server. Comment/thread locks and existing response records prevent duplicate dispatch. Execution failures are surfaced without resending saved comments. A follow-up creates its own activity and does not inherit or reopen a completed automation run.
Focused checks cover hidden admin agents, independent root sessions, reassignment, duplicate requests, device rejection, comment-only behavior, and cross-project, read-only, and other-author rejection. Desktop regression coverage belongs to the existing collaboration-shared-core scenario; E2E runs require an explicit request.
Reading project execution sessionsβ
Execution details and Issue conversations declare projectSession: { projectId, issueId } as their read context. HTTP and Socket.IO transcript requests share one authorization path: project Reporter access or higher, an existing Issue, an exact device/task match in an execution record or active TaskBinding, and a single original execution owner. Client workspace paths and Runtime Handles cannot expand this authority. Personal transcripts retain device ownership checks; project reads do not grant access to the owner's device catalog, model credentials, or other sessions.
Historical execution outcomes and transcript availability are independent. A read failure must not appear as empty history or confirmed executor idleness. Initial loading uses a skeleton. An offline executor remains an explicit error with retry; clients must not switch to a member's similarly named device. The existing automation regression includes member reads of admin-owned history. Focused unit tests run by default; E2E requires an explicit request.
HTTP transcript responses preserve the Runtime's turns, running, origin, historyUnavailable, and turnNavigation, matching the Socket.IO contract. Missing turns is a protocol error, not an empty-history fallback.