From 9e9d440770d7a046ba651af73a1ca5e6b704eb09 Mon Sep 17 00:00:00 2001 From: Wylabb <77673282+Wylabb@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:12:16 +0200 Subject: [PATCH] Decouple gateway protocol from Rust runtime types Move all worker-facing record types (TaskListRecord, RuntimeTaskRecord, TeamRecord, MailboxSummary, FeedItemRecord, LibraryAppRecord, etc.) into protocol-owned mirrors in records.rs. The gateway and worker_client now use these protocol types instead of importing from the runtime crate. Add worker-protocol/ with OpenAPI spec and JSON schemas as the language-neutral contract authority for the gateway-worker boundary. This is the migration surface for the TS worker replacement. Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/crates/channel-gateway-core/src/lib.rs | 10 + .../channel-gateway-core/src/protocol.rs | 8 +- .../channel-gateway-core/src/records.rs | 877 ++++++++++++++++ rust/crates/claw-profile-worker/src/server.rs | 99 +- rust/crates/claw-telegram/src/gateway.rs | 19 +- .../crates/claw-telegram/src/worker_client.rs | 9 +- worker-protocol/README.md | 29 + worker-protocol/openapi.yaml | 953 ++++++++++++++++++ .../worker-approval-decision.schema.json | 37 + .../schemas/worker-turn-event.schema.json | 109 ++ .../schemas/worker-turn-request.schema.json | 38 + 11 files changed, 2136 insertions(+), 52 deletions(-) create mode 100644 rust/crates/channel-gateway-core/src/records.rs create mode 100644 worker-protocol/README.md create mode 100644 worker-protocol/openapi.yaml create mode 100644 worker-protocol/schemas/worker-approval-decision.schema.json create mode 100644 worker-protocol/schemas/worker-turn-event.schema.json create mode 100644 worker-protocol/schemas/worker-turn-request.schema.json diff --git a/rust/crates/channel-gateway-core/src/lib.rs b/rust/crates/channel-gateway-core/src/lib.rs index 04cfb68..be8dadc 100644 --- a/rust/crates/channel-gateway-core/src/lib.rs +++ b/rust/crates/channel-gateway-core/src/lib.rs @@ -1,5 +1,6 @@ pub mod manifest; pub mod protocol; +pub mod records; pub mod runtime_host; pub mod unraid_template; @@ -9,6 +10,15 @@ pub use manifest::{ ChannelIdentity, DmKind, GatewayManifest, GatewaySettings, ManifestError, ProfileId, ProfileRecord, WorkerDefaults, WorkerSpec, }; +pub use records::{ + AppBundleManifestV1, AppCapability, AppHistoryEntry, AppHistoryResponse, AppKind, + AppPackageRequest, AppPackageResult, AppPublishRequest, AppPublishResult, AppRepoLink, + AppVisibility, AppWorkspaceRecord, BackgroundApprovalDecision, + BackgroundApprovalRecord, FeedItemChangeKind, FeedItemKind, FeedItemRecord, FeedItemSource, + FeedItemSourceKind, LibraryAppManifestV2, LibraryAppRecord, LibraryAppVersionRecord, + MailboxMessage, MailboxSummary, MessageEnvelope, RuntimeTaskKind, RuntimeTaskRecord, + RuntimeTaskStatus, TaskListRecord, TaskListStatus, TeamMemberRecord, TeamRecord, +}; pub use protocol::{ GeneratedFileDescriptor, InboundAttachment, TurnSource, WorkerAgentListResponse, WorkerAppHistoryResponse, WorkerAppListResponse, WorkerAppPackageResponse, diff --git a/rust/crates/channel-gateway-core/src/protocol.rs b/rust/crates/channel-gateway-core/src/protocol.rs index 4c35d64..7a5c716 100644 --- a/rust/crates/channel-gateway-core/src/protocol.rs +++ b/rust/crates/channel-gateway-core/src/protocol.rs @@ -1,10 +1,10 @@ use serde::{Deserialize, Serialize}; -use runtime::{ + +use crate::records::{ AppHistoryResponse, AppPackageResult, AppPublishResult, AppWorkspaceRecord, BackgroundApprovalRecord, FeedItemRecord, LibraryAppRecord, LibraryAppVersionRecord, - MailboxSummary, RuntimeTaskRecord, TaskListRecord, TeamRecord, + MailboxMessage, MailboxSummary, RuntimeTaskRecord, TaskListRecord, TeamRecord, }; - use crate::runtime_host::{ApprovalRequestPayload, AttachmentKind}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -182,7 +182,7 @@ pub struct WorkerMailboxSummaryResponse { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WorkerMailboxPendingResponse { - pub messages: Vec, + pub messages: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/rust/crates/channel-gateway-core/src/records.rs b/rust/crates/channel-gateway-core/src/records.rs new file mode 100644 index 0000000..db77340 --- /dev/null +++ b/rust/crates/channel-gateway-core/src/records.rs @@ -0,0 +1,877 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +fn default_branch_name() -> String { + "main".to_string() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TaskListStatus { + Pending, + InProgress, + Completed, +} + +impl std::fmt::Display for TaskListStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pending => write!(f, "pending"), + Self::InProgress => write!(f, "in_progress"), + Self::Completed => write!(f, "completed"), + } + } +} + +impl From for TaskListStatus { + fn from(value: runtime::TaskListStatus) -> Self { + match value { + runtime::TaskListStatus::Pending => Self::Pending, + runtime::TaskListStatus::InProgress => Self::InProgress, + runtime::TaskListStatus::Completed => Self::Completed, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TaskListRecord { + pub id: String, + pub subject: String, + pub description: String, + #[serde(rename = "activeForm", default, skip_serializing_if = "Option::is_none")] + pub active_form: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub owner: Option, + pub status: TaskListStatus, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub blocks: Vec, + #[serde(rename = "blockedBy", default, skip_serializing_if = "Vec::is_empty")] + pub blocked_by: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub metadata: Option>, + #[serde(default)] + pub internal: bool, + #[serde(rename = "createdAt")] + pub created_at: u64, + #[serde(rename = "updatedAt")] + pub updated_at: u64, +} + +impl From for TaskListRecord { + fn from(value: runtime::TaskListRecord) -> Self { + Self { + id: value.id, + subject: value.subject, + description: value.description, + active_form: value.active_form, + owner: value.owner, + status: value.status.into(), + blocks: value.blocks, + blocked_by: value.blocked_by, + metadata: value.metadata, + internal: value.internal, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeTaskKind { + Agent, + Shell, +} + +impl From for RuntimeTaskKind { + fn from(value: runtime::RuntimeTaskKind) -> Self { + match value { + runtime::RuntimeTaskKind::Agent => Self::Agent, + runtime::RuntimeTaskKind::Shell => Self::Shell, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RuntimeTaskStatus { + Running, + Completed, + Failed, + Stopped, +} + +impl RuntimeTaskStatus { + #[must_use] + pub fn is_terminal(self) -> bool { + matches!(self, Self::Completed | Self::Failed | Self::Stopped) + } +} + +impl std::fmt::Display for RuntimeTaskStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Running => write!(f, "running"), + Self::Completed => write!(f, "completed"), + Self::Failed => write!(f, "failed"), + Self::Stopped => write!(f, "stopped"), + } + } +} + +impl From for RuntimeTaskStatus { + fn from(value: runtime::RuntimeTaskStatus) -> Self { + match value { + runtime::RuntimeTaskStatus::Running => Self::Running, + runtime::RuntimeTaskStatus::Completed => Self::Completed, + runtime::RuntimeTaskStatus::Failed => Self::Failed, + runtime::RuntimeTaskStatus::Stopped => Self::Stopped, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeTaskRecord { + pub task_id: String, + pub kind: RuntimeTaskKind, + pub status: RuntimeTaskStatus, + pub description: String, + pub prompt: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub output_file: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exit_code_file: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub final_result: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exit_code: Option, + #[serde(default)] + pub notified: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub team_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worktree_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worktree_branch: Option, + pub created_at: u64, + pub started_at: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub completed_at: Option, +} + +impl From for RuntimeTaskRecord { + fn from(value: runtime::RuntimeTaskRecord) -> Self { + Self { + task_id: value.task_id, + kind: value.kind.into(), + status: value.status.into(), + description: value.description, + prompt: value.prompt, + output_file: value.output_file, + exit_code_file: value.exit_code_file, + final_result: value.final_result, + error: value.error, + exit_code: value.exit_code, + notified: value.notified, + pid: value.pid, + agent_id: value.agent_id, + agent_name: value.agent_name, + team_name: value.team_name, + cwd: value.cwd, + worktree_path: value.worktree_path, + worktree_branch: value.worktree_branch, + created_at: value.created_at, + started_at: value.started_at, + completed_at: value.completed_at, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TeamMemberRecord { + pub agent_id: String, + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_type: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub model: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, + pub joined_at: u64, +} + +impl From for TeamMemberRecord { + fn from(value: runtime::TeamMemberRecord) -> Self { + Self { + agent_id: value.agent_id, + name: value.name, + agent_type: value.agent_type, + model: value.model, + status: value.status, + joined_at: value.joined_at, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TeamRecord { + pub team_name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent_type: Option, + pub lead_agent_id: String, + pub created_at: u64, + pub updated_at: u64, + #[serde(default)] + pub deleted: bool, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub members: Vec, +} + +impl From for TeamRecord { + fn from(value: runtime::TeamRecord) -> Self { + Self { + team_name: value.team_name, + description: value.description, + agent_type: value.agent_type, + lead_agent_id: value.lead_agent_id, + created_at: value.created_at, + updated_at: value.updated_at, + deleted: value.deleted, + members: value.members.into_iter().map(Into::into).collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MessageEnvelope { + pub id: String, + pub from: String, + pub to: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub summary: Option, + pub message: Value, + pub timestamp: u64, + #[serde(default)] + pub read: bool, + #[serde(default)] + pub notified: bool, +} + +impl From for MessageEnvelope { + fn from(value: runtime::MessageEnvelope) -> Self { + Self { + id: value.id, + from: value.from, + to: value.to, + summary: value.summary, + message: value.message, + timestamp: value.timestamp, + read: value.read, + notified: value.notified, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MailboxMessage { + pub recipient: String, + pub envelope: MessageEnvelope, +} + +impl From for MailboxMessage { + fn from(value: runtime::MailboxMessage) -> Self { + Self { + recipient: value.recipient, + envelope: value.envelope.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MailboxSummary { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub team_name: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub recent_messages: Vec, +} + +impl From for MailboxSummary { + fn from(value: runtime::MailboxSummary) -> Self { + Self { + team_name: value.team_name, + recent_messages: value.recent_messages.into_iter().map(Into::into).collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "decision", rename_all = "snake_case")] +pub enum BackgroundApprovalDecision { + ApproveOnce, + ApproveToolForSession, + ApproveAllForSession, + Deny { reason: String }, + CancelTurn, +} + +impl From for BackgroundApprovalDecision { + fn from(value: runtime::BackgroundApprovalDecision) -> Self { + match value { + runtime::BackgroundApprovalDecision::ApproveOnce => Self::ApproveOnce, + runtime::BackgroundApprovalDecision::ApproveToolForSession => { + Self::ApproveToolForSession + } + runtime::BackgroundApprovalDecision::ApproveAllForSession => { + Self::ApproveAllForSession + } + runtime::BackgroundApprovalDecision::Deny { reason } => Self::Deny { reason }, + runtime::BackgroundApprovalDecision::CancelTurn => Self::CancelTurn, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BackgroundApprovalRecord { + pub approval_id: String, + pub task_id: String, + pub tool_name: String, + pub input: String, + pub current_mode: String, + pub required_mode: String, + #[serde(default)] + pub reason: Option, + pub created_at: u64, + #[serde(default)] + pub notified: bool, + #[serde(default)] + pub decision: Option, +} + +impl From for BackgroundApprovalRecord { + fn from(value: runtime::BackgroundApprovalRecord) -> Self { + Self { + approval_id: value.approval_id, + task_id: value.task_id, + tool_name: value.tool_name, + input: value.input, + current_mode: value.current_mode, + required_mode: value.required_mode, + reason: value.reason, + created_at: value.created_at, + notified: value.notified, + decision: value.decision.map(Into::into), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FeedItemKind { + Markdown, + Code, + Json, + Diff, + Image, + HtmlPreview, + Table, + Binary, + DeletedFile, + AppPublished, +} + +impl From for FeedItemKind { + fn from(value: runtime::FeedItemKind) -> Self { + match value { + runtime::FeedItemKind::Markdown => Self::Markdown, + runtime::FeedItemKind::Code => Self::Code, + runtime::FeedItemKind::Json => Self::Json, + runtime::FeedItemKind::Diff => Self::Diff, + runtime::FeedItemKind::Image => Self::Image, + runtime::FeedItemKind::HtmlPreview => Self::HtmlPreview, + runtime::FeedItemKind::Table => Self::Table, + runtime::FeedItemKind::Binary => Self::Binary, + runtime::FeedItemKind::DeletedFile => Self::DeletedFile, + runtime::FeedItemKind::AppPublished => Self::AppPublished, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FeedItemSourceKind { + Generated, + Workspace, + AppEvent, +} + +impl From for FeedItemSourceKind { + fn from(value: runtime::FeedItemSourceKind) -> Self { + match value { + runtime::FeedItemSourceKind::Generated => Self::Generated, + runtime::FeedItemSourceKind::Workspace => Self::Workspace, + runtime::FeedItemSourceKind::AppEvent => Self::AppEvent, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FeedItemChangeKind { + Created, + Modified, + Deleted, +} + +impl From for FeedItemChangeKind { + fn from(value: runtime::FeedItemChangeKind) -> Self { + match value { + runtime::FeedItemChangeKind::Created => Self::Created, + runtime::FeedItemChangeKind::Modified => Self::Modified, + runtime::FeedItemChangeKind::Deleted => Self::Deleted, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FeedItemSource { + pub source_kind: FeedItemSourceKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub turn_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_file_id: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub source_files: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub change_kind: Option, +} + +impl From for FeedItemSource { + fn from(value: runtime::FeedItemSource) -> Self { + Self { + source_kind: value.source_kind.into(), + turn_id: value.turn_id, + source_file_id: value.source_file_id, + source_files: value.source_files, + change_kind: value.change_kind.map(Into::into), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FeedItemRecord { + pub feed_item_id: String, + pub title: String, + pub kind: FeedItemKind, + pub source: FeedItemSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub linked_app_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub media_type: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub file_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stored_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub preview_text: Option, + #[serde(default)] + pub deleted: bool, + pub created_at: u64, + pub updated_at: u64, +} + +impl From for FeedItemRecord { + fn from(value: runtime::FeedItemRecord) -> Self { + Self { + feed_item_id: value.feed_item_id, + title: value.title, + kind: value.kind.into(), + source: value.source.into(), + linked_app_id: value.linked_app_id, + media_type: value.media_type, + file_name: value.file_name, + stored_path: value.stored_path, + preview_text: value.preview_text, + deleted: value.deleted, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AppKind { + HtmlPage, + ReactApp, + Dashboard, + DocumentExperience, + ToolApp, +} + +impl From for AppKind { + fn from(value: runtime::AppKind) -> Self { + match value { + runtime::AppKind::HtmlPage => Self::HtmlPage, + runtime::AppKind::ReactApp => Self::ReactApp, + runtime::AppKind::Dashboard => Self::Dashboard, + runtime::AppKind::DocumentExperience => Self::DocumentExperience, + runtime::AppKind::ToolApp => Self::ToolApp, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AppVisibility { + Family, + Admin, + Archived, +} + +impl From for AppVisibility { + fn from(value: runtime::AppVisibility) -> Self { + match value { + runtime::AppVisibility::Family => Self::Family, + runtime::AppVisibility::Admin => Self::Admin, + runtime::AppVisibility::Archived => Self::Archived, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AppCapability { + GetContext, + ListFiles, + ReadText, + ReadJson, + OpenFeedItem, + OpenApp, + PublishPatch, + RequestAction, + EmitEvent, +} + +impl From for AppCapability { + fn from(value: runtime::AppCapability) -> Self { + match value { + runtime::AppCapability::GetContext => Self::GetContext, + runtime::AppCapability::ListFiles => Self::ListFiles, + runtime::AppCapability::ReadText => Self::ReadText, + runtime::AppCapability::ReadJson => Self::ReadJson, + runtime::AppCapability::OpenFeedItem => Self::OpenFeedItem, + runtime::AppCapability::OpenApp => Self::OpenApp, + runtime::AppCapability::PublishPatch => Self::PublishPatch, + runtime::AppCapability::RequestAction => Self::RequestAction, + runtime::AppCapability::EmitEvent => Self::EmitEvent, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppRepoLink { + pub url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub branch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub commit: Option, +} + +impl From for AppRepoLink { + fn from(value: runtime::AppRepoLink) -> Self { + Self { + url: value.url, + branch: value.branch, + commit: value.commit, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct LibraryAppManifestV2 { + pub schema_version: String, + pub app_id: String, + pub title: String, + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub visibility: AppVisibility, + pub created_at: u64, + pub updated_at: u64, + pub last_launched_at: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub created_from_feed_item_id: Option, + pub current_version_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub published_commit: Option, + pub default_branch: String, + pub local_repo_path: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub remote_repo: Option, + pub kind: AppKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub icon: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub capabilities: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_turn_id: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub source_feed_item_ids: Vec, +} + +impl From for LibraryAppManifestV2 { + fn from(value: runtime::LibraryAppManifestV2) -> Self { + Self { + schema_version: value.schema_version, + app_id: value.app_id, + title: value.title, + name: value.name, + description: value.description, + visibility: value.visibility.into(), + created_at: value.created_at, + updated_at: value.updated_at, + last_launched_at: value.last_launched_at, + created_from_feed_item_id: value.created_from_feed_item_id, + current_version_id: value.current_version_id, + published_commit: value.published_commit, + default_branch: value.default_branch, + local_repo_path: value.local_repo_path, + remote_repo: value.remote_repo.map(Into::into), + kind: value.kind.into(), + icon: value.icon, + tags: value.tags, + capabilities: value.capabilities.into_iter().map(Into::into).collect(), + source_turn_id: value.source_turn_id, + source_feed_item_ids: value.source_feed_item_ids, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppBundleManifestV1 { + pub schema_version: String, + pub app_id: String, + pub name: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub kind: AppKind, + pub entry: String, + pub files: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub icon: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub capabilities: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, +} + +impl From for AppBundleManifestV1 { + fn from(value: runtime::AppBundleManifestV1) -> Self { + Self { + schema_version: value.schema_version, + app_id: value.app_id, + name: value.name, + title: value.title, + description: value.description, + kind: value.kind.into(), + entry: value.entry, + files: value.files, + icon: value.icon, + capabilities: value.capabilities.into_iter().map(Into::into).collect(), + tags: value.tags, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct LibraryAppVersionRecord { + pub version_id: String, + pub created_at: u64, + pub entry: String, + pub files: Vec, + #[serde(default = "default_branch_name")] + pub branch: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub commit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub published_at: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source_turn_id: Option, + #[serde( + alias = "source_artifact_ids", + default, + skip_serializing_if = "Vec::is_empty" + )] + pub source_feed_item_ids: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub changelog_summary: Option, +} + +impl From for LibraryAppVersionRecord { + fn from(value: runtime::LibraryAppVersionRecord) -> Self { + Self { + version_id: value.version_id, + created_at: value.created_at, + entry: value.entry, + files: value.files, + branch: value.branch, + commit: value.commit, + published_at: value.published_at, + source_turn_id: value.source_turn_id, + source_feed_item_ids: value.source_feed_item_ids, + changelog_summary: value.changelog_summary, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct LibraryAppRecord { + pub manifest: LibraryAppManifestV2, + pub bundle_manifest: AppBundleManifestV1, + pub current_version: LibraryAppVersionRecord, +} + +impl From for LibraryAppRecord { + fn from(value: runtime::LibraryAppRecord) -> Self { + Self { + manifest: value.manifest.into(), + bundle_manifest: value.bundle_manifest.into(), + current_version: value.current_version.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppPackageRequest { + pub feed_item_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub requested_app_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +impl From for runtime::AppPackageRequest { + fn from(value: AppPackageRequest) -> Self { + Self { + feed_item_id: value.feed_item_id, + requested_app_id: value.requested_app_id, + title: value.title, + description: value.description, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppPackageResult { + pub app: LibraryAppRecord, + pub created: bool, +} + +impl From for AppPackageResult { + fn from(value: runtime::AppPackageResult) -> Self { + Self { + app: value.app.into(), + created: value.created, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppWorkspaceRecord { + pub app_id: String, + pub cwd: String, + pub branch: String, + pub head_commit: String, + pub dirty: bool, + pub bundle_manifest: AppBundleManifestV1, +} + +impl From for AppWorkspaceRecord { + fn from(value: runtime::AppWorkspaceRecord) -> Self { + Self { + app_id: value.app_id, + cwd: value.cwd, + branch: value.branch, + head_commit: value.head_commit, + dirty: value.dirty, + bundle_manifest: value.bundle_manifest.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppPublishRequest { + pub message: String, +} + +impl From for runtime::AppPublishRequest { + fn from(value: AppPublishRequest) -> Self { + Self { + message: value.message, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppPublishResult { + pub app: LibraryAppRecord, + pub workspace: AppWorkspaceRecord, +} + +impl From for AppPublishResult { + fn from(value: runtime::AppPublishResult) -> Self { + Self { + app: value.app.into(), + workspace: value.workspace.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppHistoryEntry { + pub commit: String, + pub message: String, + pub authored_at: u64, +} + +impl From for AppHistoryEntry { + fn from(value: runtime::AppHistoryEntry) -> Self { + Self { + commit: value.commit, + message: value.message, + authored_at: value.authored_at, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AppHistoryResponse { + pub entries: Vec, +} + +impl From for AppHistoryResponse { + fn from(value: runtime::AppHistoryResponse) -> Self { + Self { + entries: value.entries.into_iter().map(Into::into).collect(), + } + } +} diff --git a/rust/crates/claw-profile-worker/src/server.rs b/rust/crates/claw-profile-worker/src/server.rs index 3b30e2b..7cd611c 100644 --- a/rust/crates/claw-profile-worker/src/server.rs +++ b/rust/crates/claw-profile-worker/src/server.rs @@ -14,11 +14,13 @@ use axum::routing::{get, post}; use axum::{Json, Router}; use base64::Engine as _; use channel_gateway_core::{ - ApprovalDecision, ApprovalResponder, AttachmentRef, GeneratedFileDescriptor, HostError, - RuntimeEvent, RuntimeHost, RuntimeHostConfig, SessionApprovalState, WorkerApprovalDecision, + AppPackageRequest, AppPublishRequest, ApprovalDecision, ApprovalResponder, AttachmentRef, + BackgroundApprovalRecord, GeneratedFileDescriptor, HostError, LibraryAppVersionRecord, + MailboxMessage, MailboxSummary, RuntimeEvent, RuntimeHost, RuntimeHostConfig, + RuntimeTaskRecord, SessionApprovalState, WorkerAgentListResponse, WorkerAppHistoryResponse, WorkerAppListResponse, WorkerAppPackageResponse, WorkerAppPublishResponse, WorkerAppSnapshotResponse, - WorkerAppVersionResponse, WorkerAppWorkspaceResponse, + WorkerAppVersionResponse, WorkerAppWorkspaceResponse, WorkerApprovalDecision, WorkerBackgroundApprovalListResponse, WorkerFeedItemResponse, WorkerFeedListResponse, WorkerMailboxMessageEvent, WorkerMailboxPendingResponse, WorkerMailboxSummaryResponse, WorkerStatusResponse, WorkerTaskListResponse, WorkerTaskSnapshotResponse, @@ -26,10 +28,10 @@ use channel_gateway_core::{ WorkerTurnRequest, }; use runtime::{ - clear_app_workspace_context, current_task_list_id, AppPackageRequest, AppPublishRequest, - ArtifactLibraryStore, BackgroundApprovalDecision, BackgroundApprovalStore, FeedFileInput, - FeedItemChangeKind, FeedItemSourceKind, RuntimeTaskKind, RuntimeTaskRecord, - RuntimeTaskStore, TaskListStore, TeamStore, WorkspaceSnapshot, + clear_app_workspace_context, current_task_list_id, ArtifactLibraryStore, + BackgroundApprovalDecision, BackgroundApprovalStore, FeedFileInput, FeedItemChangeKind, + FeedItemSourceKind, RuntimeTaskKind, RuntimeTaskStore, TaskListStore, TeamStore, + WorkspaceSnapshot, }; use serde::Deserialize; use serde_json::Value; @@ -311,7 +313,10 @@ async fn list_feed( let store = ArtifactLibraryStore::new(); let items = store .list_feed(query.turn_id.as_deref()) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .into_iter() + .map(Into::into) + .collect(); Ok(Json(WorkerFeedListResponse { items, state_root: store.state_root_display().ok(), @@ -328,7 +333,8 @@ async fn get_feed_item( let store = ArtifactLibraryStore::new(); let item = store .get_feed_item(&feed_item_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); Ok(Json(WorkerFeedItemResponse { item, state_root: store.state_root_display().ok(), @@ -382,9 +388,11 @@ async fn package_feed_item( authorize(&headers, &state.config.auth_token)?; request.feed_item_id = feed_item_id; let result = ArtifactLibraryStore::new() - .package_feed_item(request) + .package_feed_item(request.into()) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(WorkerAppPackageResponse { result })) + Ok(Json(WorkerAppPackageResponse { + result: result.map(Into::into), + })) } async fn list_tasks( @@ -393,7 +401,12 @@ async fn list_tasks( ) -> Result, StatusCode> { authorize(&headers, &state.config.auth_token)?; let store = TaskListStore::current().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let tasks = store.list(false).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let tasks = store + .list(false) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .into_iter() + .map(Into::into) + .collect(); Ok(Json(WorkerTaskListResponse { task_list_id: store.task_list_id().to_string(), tasks, @@ -409,10 +422,12 @@ async fn get_task( let task = TaskListStore::current() .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .get(&task_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); let runtime_task = RuntimeTaskStore::new() .get(&task_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); if task.is_none() && runtime_task.is_none() { return Err(StatusCode::NOT_FOUND); } @@ -440,7 +455,8 @@ async fn get_team( let task_list_id = current_task_list_id().unwrap_or_else(|_| state.config.profile_id.clone()); let team = TeamStore::new() .current_team() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); Ok(Json(WorkerTeamSnapshotResponse { team, task_list_id })) } @@ -454,6 +470,7 @@ async fn list_agents( .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .into_iter() .filter(|task| task.kind == RuntimeTaskKind::Agent) + .map(Into::into) .collect(); Ok(Json(WorkerAgentListResponse { agents })) } @@ -468,7 +485,7 @@ async fn list_apps( .list_apps_with_warnings() .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(WorkerAppListResponse { - apps, + apps: apps.into_iter().map(Into::into).collect(), state_root: store.state_root_display().ok(), warnings, })) @@ -483,7 +500,8 @@ async fn get_app( let store = ArtifactLibraryStore::new(); let app = store .get_app(&app_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); Ok(Json(WorkerAppSnapshotResponse { app, state_root: store.state_root_display().ok(), @@ -499,7 +517,7 @@ async fn get_app_version( let version = ArtifactLibraryStore::new() .get_app(&app_id) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .map(|app| app.current_version); + .map(|app| LibraryAppVersionRecord::from(app.current_version)); Ok(Json(WorkerAppVersionResponse { version })) } @@ -511,7 +529,8 @@ async fn get_app_history( authorize(&headers, &state.config.auth_token)?; let history = ArtifactLibraryStore::new() .app_history(&app_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); Ok(Json(WorkerAppHistoryResponse { history })) } @@ -524,7 +543,8 @@ async fn launch_app( let store = ArtifactLibraryStore::new(); let app = store .mark_app_launched(&app_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); Ok(Json(WorkerAppSnapshotResponse { app, state_root: store.state_root_display().ok(), @@ -539,7 +559,8 @@ async fn open_app_workspace( authorize(&headers, &state.config.auth_token)?; let workspace = ArtifactLibraryStore::new() .open_workspace(&app_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); Ok(Json(WorkerAppWorkspaceResponse { workspace })) } @@ -551,9 +572,11 @@ async fn publish_app_workspace( ) -> Result, StatusCode> { authorize(&headers, &state.config.auth_token)?; let result = ArtifactLibraryStore::new() - .publish_workspace(&app_id, request) + .publish_workspace(&app_id, request.into()) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - Ok(Json(WorkerAppPublishResponse { result })) + Ok(Json(WorkerAppPublishResponse { + result: result.map(Into::into), + })) } async fn archive_app( @@ -580,7 +603,8 @@ async fn get_app_workspace_status( authorize(&headers, &state.config.auth_token)?; let workspace = ArtifactLibraryStore::new() .workspace_status(&app_id) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(Into::into); Ok(Json(WorkerAppWorkspaceResponse { workspace })) } @@ -628,7 +652,7 @@ async fn get_agent( .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .filter(|task| task.kind == RuntimeTaskKind::Agent) .ok_or(StatusCode::NOT_FOUND)?; - Ok(Json(task)) + Ok(Json(task.into())) } async fn mark_agent_notified( @@ -652,7 +676,10 @@ async fn list_background_approvals( authorize(&headers, &state.config.auth_token)?; let approvals = BackgroundApprovalStore::new() .list_pending() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .into_iter() + .map(BackgroundApprovalRecord::from) + .collect(); Ok(Json(WorkerBackgroundApprovalListResponse { approvals })) } @@ -710,8 +737,9 @@ async fn get_mailbox( { Some(team) => TeamStore::new() .mailbox_summary(&team.team_name, 20) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, - None => runtime::MailboxSummary { + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .into(), + None => MailboxSummary { team_name: None, recent_messages: Vec::new(), }, @@ -731,7 +759,10 @@ async fn get_pending_mailbox_messages( { Some(team) => TeamStore::new() .pending_messages(&team.team_name, &recipient, 20) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .into_iter() + .map(MailboxMessage::from) + .collect(), None => Vec::new(), }; Ok(Json(WorkerMailboxPendingResponse { messages })) @@ -1291,7 +1322,7 @@ fn derive_task_created_event(value: &Value) -> Option { let task = store.get(task_id).ok()??; Some(WorkerTurnEvent::TaskCreated { task_list_id: store.task_list_id().to_string(), - task, + task: task.into(), }) } @@ -1301,14 +1332,14 @@ fn derive_task_updated_event(value: &Value) -> Option { let task = store.get(task_id).ok()??; Some(WorkerTurnEvent::TaskUpdated { task_list_id: store.task_list_id().to_string(), - task, + task: task.into(), }) } fn derive_task_stopped_event(value: &Value) -> Option { let task_id = value.get("task_id")?.as_str()?; let task = RuntimeTaskStore::new().get(task_id).ok()??; - Some(WorkerTurnEvent::TaskStopped { task }) + Some(WorkerTurnEvent::TaskStopped { task: task.into() }) } fn derive_agent_spawned_event(value: &Value) -> Option { @@ -1317,7 +1348,7 @@ fn derive_agent_spawned_event(value: &Value) -> Option { .and_then(Value::as_str) .or_else(|| value.get("agentId").and_then(Value::as_str))?; let task = RuntimeTaskStore::new().get(task_id).ok()??; - Some(WorkerTurnEvent::AgentSpawned { agent: task }) + Some(WorkerTurnEvent::AgentSpawned { agent: task.into() }) } fn derive_team_created_event(value: &Value) -> Option { @@ -1331,7 +1362,7 @@ fn derive_team_created_event(value: &Value) -> Option { .and_then(Value::as_str) .unwrap_or_default() .to_string(), - team, + team: team.into(), }, }) } diff --git a/rust/crates/claw-telegram/src/gateway.rs b/rust/crates/claw-telegram/src/gateway.rs index 6c37cb3..c897957 100644 --- a/rust/crates/claw-telegram/src/gateway.rs +++ b/rust/crates/claw-telegram/src/gateway.rs @@ -5,13 +5,14 @@ use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use channel_gateway_core::{ - AttachmentKind, AttachmentRef, GatewayManifest, GatewaySettings, ManifestError, ProfileId, - ProfileRecord, TurnSource, WorkerAgentListResponse, WorkerApprovalDecision, + AttachmentKind, AttachmentRef, BackgroundApprovalRecord, GatewayManifest, GatewaySettings, + MailboxMessage, ManifestError, ProfileId, ProfileRecord, RuntimeTaskRecord, + RuntimeTaskStatus, TaskListRecord, TurnSource, WorkerAgentListResponse, + WorkerApprovalDecision, WorkerBackgroundApprovalListResponse, WorkerDefaults, WorkerMailboxMessageEvent, WorkerMailboxSummaryResponse, WorkerTaskListResponse, WorkerTaskSnapshotResponse, WorkerTeamCreatedEvent, WorkerTeamSnapshotResponse, WorkerTurnEvent, }; -use runtime::{BackgroundApprovalRecord, MailboxMessage, RuntimeTaskRecord}; use tokio::sync::Mutex; use crate::config::GatewayConfig; @@ -1625,10 +1626,10 @@ fn render_agent_snapshot(agent: &RuntimeTaskRecord) -> String { fn render_background_agent_terminal_notice(agent: &RuntimeTaskRecord) -> String { let title = match agent.status { - runtime::RuntimeTaskStatus::Completed => "Background agent completed", - runtime::RuntimeTaskStatus::Failed => "Background agent failed", - runtime::RuntimeTaskStatus::Stopped => "Background agent stopped", - runtime::RuntimeTaskStatus::Running => "Background agent update", + RuntimeTaskStatus::Completed => "Background agent completed", + RuntimeTaskStatus::Failed => "Background agent failed", + RuntimeTaskStatus::Stopped => "Background agent stopped", + RuntimeTaskStatus::Running => "Background agent update", }; let mut lines = vec![ title.to_string(), @@ -1716,14 +1717,14 @@ fn render_runtime_task(label: &str, task: &RuntimeTaskRecord) -> String { ) } -fn render_task_created_notice(task_list_id: &str, task: &runtime::TaskListRecord) -> String { +fn render_task_created_notice(task_list_id: &str, task: &TaskListRecord) -> String { format!( "Task created\nTask list: {}\n{} [{}] {}", task_list_id, task.id, task.status, task.subject ) } -fn render_task_updated_notice(task_list_id: &str, task: &runtime::TaskListRecord) -> String { +fn render_task_updated_notice(task_list_id: &str, task: &TaskListRecord) -> String { format!( "Task updated\nTask list: {}\n{} [{}] {}", task_list_id, task.id, task.status, task.subject diff --git a/rust/crates/claw-telegram/src/worker_client.rs b/rust/crates/claw-telegram/src/worker_client.rs index cff0919..da3b72d 100644 --- a/rust/crates/claw-telegram/src/worker_client.rs +++ b/rust/crates/claw-telegram/src/worker_client.rs @@ -3,16 +3,15 @@ use std::path::{Path, PathBuf}; use base64::Engine as _; use channel_gateway_core::{ - AttachmentRef, GeneratedFileDescriptor, TurnSource, WorkerAgentListResponse, - WorkerAppHistoryResponse, WorkerAppListResponse, WorkerAppPackageResponse, - WorkerAppPublishResponse, WorkerAppSnapshotResponse, WorkerAppVersionResponse, - WorkerAppWorkspaceResponse, + AppPackageRequest, AppPublishRequest, AttachmentRef, GeneratedFileDescriptor, + RuntimeTaskRecord, TurnSource, WorkerAgentListResponse, WorkerAppHistoryResponse, + WorkerAppListResponse, WorkerAppPackageResponse, WorkerAppPublishResponse, + WorkerAppSnapshotResponse, WorkerAppVersionResponse, WorkerAppWorkspaceResponse, WorkerApprovalDecision, WorkerBackgroundApprovalListResponse, WorkerMailboxSummaryResponse, WorkerFeedItemResponse, WorkerFeedListResponse, WorkerMailboxPendingResponse, WorkerStatusResponse, WorkerTaskListResponse, WorkerTaskSnapshotResponse, WorkerTeamSnapshotResponse, WorkerTurnAccepted, WorkerTurnEvent, WorkerTurnRequest, }; -use runtime::{AppPackageRequest, AppPublishRequest, RuntimeTaskRecord}; use futures_util::StreamExt; use serde::Serialize; use tokio::sync::mpsc; diff --git a/worker-protocol/README.md b/worker-protocol/README.md new file mode 100644 index 0000000..95412a5 --- /dev/null +++ b/worker-protocol/README.md @@ -0,0 +1,29 @@ +# Worker Protocol + +This directory is the contract boundary between the Rust gateway in +[`/Users/makarnovozhilov/clawdcode/claw-code-parity`](/Users/makarnovozhilov/clawdcode/claw-code-parity) +and the future TypeScript worker forked from +[`/Users/makarnovozhilov/clawdcode/claude-code-source`](/Users/makarnovozhilov/clawdcode/claude-code-source). + +The migration goal is: + +- keep the current gateway HTTP/SSE client stable +- move worker ownership out of Rust runtime internals +- make the worker contract language-neutral + +Current state: + +- `openapi.yaml` is the contract authority for the worker surface the gateway + already calls +- `schemas/` contains standalone JSON Schemas for the most important request + and event payloads used by the migration slice +- Rust protocol code is being migrated to use protocol-owned record mirrors + instead of importing runtime structs directly + +Notes: + +- The contract is intentionally endpoint-compatible with the current gateway. +- Some domain records are still broad `object` shapes in the OpenAPI file while + the TS worker port is in progress. +- The next tightening step is to generate TS/Rust bindings from this directory + instead of hand-maintaining parallel definitions. diff --git a/worker-protocol/openapi.yaml b/worker-protocol/openapi.yaml new file mode 100644 index 0000000..5a6edf2 --- /dev/null +++ b/worker-protocol/openapi.yaml @@ -0,0 +1,953 @@ +openapi: 3.1.0 +info: + title: Claw Worker Protocol + version: 0.1.0 + description: > + Language-neutral HTTP/SSE contract between the Rust gateway and the worker + process. This is the migration boundary for the Rust gateway plus + TypeScript worker architecture. +servers: + - url: http://worker +paths: + /healthz: + get: + operationId: healthz + responses: + "200": + description: Worker is healthy + /v1/status: + get: + operationId: getStatus + responses: + "200": + description: Session status + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerStatusResponse" + /v1/session/reset: + post: + operationId: resetSession + responses: + "204": + description: Session reset + /v1/turns: + post: + operationId: submitTurn + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerTurnRequest" + responses: + "200": + description: Turn accepted + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerTurnAccepted" + /v1/turns/{turn_id}/events: + get: + operationId: streamTurnEvents + parameters: + - $ref: "#/components/parameters/TurnId" + responses: + "200": + description: SSE stream of worker turn events + content: + text/event-stream: + schema: + type: string + /v1/turns/{turn_id}/approval: + post: + operationId: resolveTurnApproval + parameters: + - $ref: "#/components/parameters/TurnId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TurnApprovalRequest" + responses: + "204": + description: Approval applied + /v1/turns/{turn_id}/cancel: + post: + operationId: cancelTurn + parameters: + - $ref: "#/components/parameters/TurnId" + responses: + "204": + description: Turn cancelled + /v1/turns/{turn_id}/files/{file_id}: + get: + operationId: fetchGeneratedFile + parameters: + - $ref: "#/components/parameters/TurnId" + - $ref: "#/components/parameters/FileId" + responses: + "200": + description: Generated file bytes + content: + application/octet-stream: + schema: + type: string + format: binary + /v1/tasks: + get: + operationId: listTasks + responses: + "200": + description: Task list snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerTaskListResponse" + /v1/tasks/{task_id}: + get: + operationId: getTask + parameters: + - $ref: "#/components/parameters/TaskId" + responses: + "200": + description: Task snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerTaskSnapshotResponse" + /v1/tasks/{task_id}/stop: + post: + operationId: stopTask + parameters: + - $ref: "#/components/parameters/TaskId" + responses: + "204": + description: Task stop requested + /v1/team: + get: + operationId: getTeam + responses: + "200": + description: Team snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerTeamSnapshotResponse" + /v1/agents: + get: + operationId: listAgents + responses: + "200": + description: Agent runtime list + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAgentListResponse" + /v1/agents/{agent_id}: + get: + operationId: getAgent + parameters: + - $ref: "#/components/parameters/AgentId" + responses: + "200": + description: Agent runtime snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/RuntimeTaskRecord" + /v1/background-approvals: + get: + operationId: listBackgroundApprovals + responses: + "200": + description: Background approvals + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerBackgroundApprovalListResponse" + /v1/background-approvals/{approval_id}: + post: + operationId: resolveBackgroundApproval + parameters: + - $ref: "#/components/parameters/ApprovalId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TurnApprovalRequest" + responses: + "204": + description: Background approval applied + /v1/background-approvals/{approval_id}/notified: + post: + operationId: markBackgroundApprovalNotified + parameters: + - $ref: "#/components/parameters/ApprovalId" + responses: + "204": + description: Background approval notification acknowledged + /v1/mailbox: + get: + operationId: getMailbox + responses: + "200": + description: Mailbox summary + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerMailboxSummaryResponse" + /v1/mailbox/pending/{recipient}: + get: + operationId: getPendingMailboxMessages + parameters: + - $ref: "#/components/parameters/Recipient" + responses: + "200": + description: Pending mailbox messages + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerMailboxPendingResponse" + /v1/feed: + get: + operationId: listFeed + parameters: + - in: query + name: turn_id + required: false + schema: + type: string + responses: + "200": + description: Feed items + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerFeedListResponse" + /v1/feed/{feed_item_id}: + get: + operationId: getFeedItem + parameters: + - $ref: "#/components/parameters/FeedItemId" + responses: + "200": + description: Feed item snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerFeedItemResponse" + /v1/feed/{feed_item_id}/file: + get: + operationId: fetchFeedFile + parameters: + - $ref: "#/components/parameters/FeedItemId" + responses: + "200": + description: Feed payload bytes + content: + application/octet-stream: + schema: + type: string + format: binary + /v1/feed/{feed_item_id}/make-app: + post: + operationId: packageFeedItem + parameters: + - $ref: "#/components/parameters/FeedItemId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppPackageRequest" + responses: + "200": + description: App create/update result + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppPackageResponse" + /v1/apps: + get: + operationId: listApps + responses: + "200": + description: Library apps + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppListResponse" + /v1/apps/{app_id}: + get: + operationId: getApp + parameters: + - $ref: "#/components/parameters/AppId" + responses: + "200": + description: App snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppSnapshotResponse" + /v1/apps/{app_id}/version: + get: + operationId: getAppVersion + parameters: + - $ref: "#/components/parameters/AppId" + responses: + "200": + description: App current version + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppVersionResponse" + /v1/apps/{app_id}/history: + get: + operationId: getAppHistory + parameters: + - $ref: "#/components/parameters/AppId" + responses: + "200": + description: App publish history + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppHistoryResponse" + /v1/apps/{app_id}/launch: + post: + operationId: launchApp + parameters: + - $ref: "#/components/parameters/AppId" + responses: + "200": + description: App launch side effect + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppSnapshotResponse" + /v1/apps/{app_id}/open-workspace: + post: + operationId: openAppWorkspace + parameters: + - $ref: "#/components/parameters/AppId" + responses: + "200": + description: App workspace snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppWorkspaceResponse" + /v1/apps/{app_id}/publish: + post: + operationId: publishAppWorkspace + parameters: + - $ref: "#/components/parameters/AppId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppPublishRequest" + responses: + "200": + description: App publish result + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppPublishResponse" + /v1/apps/{app_id}/archive: + post: + operationId: archiveApp + parameters: + - $ref: "#/components/parameters/AppId" + responses: + "204": + description: App archived + /v1/apps/{app_id}/worktree-status: + get: + operationId: getAppWorktreeStatus + parameters: + - $ref: "#/components/parameters/AppId" + responses: + "200": + description: App worktree snapshot + content: + application/json: + schema: + $ref: "#/components/schemas/WorkerAppWorkspaceResponse" + /v1/apps/{app_id}/files/{path}: + get: + operationId: fetchAppFile + parameters: + - $ref: "#/components/parameters/AppId" + - in: path + name: path + required: true + schema: + type: string + responses: + "200": + description: App bundle file bytes + content: + application/octet-stream: + schema: + type: string + format: binary +components: + parameters: + TurnId: + in: path + name: turn_id + required: true + schema: + type: string + TaskId: + in: path + name: task_id + required: true + schema: + type: string + AgentId: + in: path + name: agent_id + required: true + schema: + type: string + ApprovalId: + in: path + name: approval_id + required: true + schema: + type: string + Recipient: + in: path + name: recipient + required: true + schema: + type: string + FeedItemId: + in: path + name: feed_item_id + required: true + schema: + type: string + AppId: + in: path + name: app_id + required: true + schema: + type: string + FileId: + in: path + name: file_id + required: true + schema: + type: string + schemas: + TurnSource: + type: object + required: + - channel + - sender_id + properties: + channel: + type: string + sender_id: + type: string + chat_id: + type: string + display_name: + type: string + InboundAttachment: + type: object + required: + - file_name + - kind + - data_base64 + properties: + file_name: + type: string + kind: + type: string + enum: [photo, document] + media_type: + type: string + data_base64: + type: string + WorkerTurnRequest: + type: object + required: + - prompt + - source + properties: + prompt: + type: string + source: + $ref: "#/components/schemas/TurnSource" + attachments: + type: array + items: + $ref: "#/components/schemas/InboundAttachment" + WorkerTurnAccepted: + type: object + required: [turn_id] + properties: + turn_id: + type: string + ApprovalRequestPayload: + type: object + required: + - approval_id + - tool_name + - input + - current_mode + - required_mode + properties: + approval_id: + type: string + tool_name: + type: string + input: + type: string + current_mode: + type: string + required_mode: + type: string + reason: + type: string + WorkerApprovalDecision: + oneOf: + - type: object + required: [decision] + properties: + decision: + type: string + enum: + - approve_once + - approve_tool_for_session + - approve_all_for_session + - cancel_turn + - type: object + required: [decision, reason] + properties: + decision: + type: string + enum: [deny] + reason: + type: string + TurnApprovalRequest: + type: object + required: + - approval_id + - decision + properties: + approval_id: + type: string + decision: + $ref: "#/components/schemas/WorkerApprovalDecision" + GeneratedFileDescriptor: + type: object + required: + - file_id + - file_name + - size_bytes + - is_image + properties: + file_id: + type: string + file_name: + type: string + media_type: + type: string + size_bytes: + type: integer + minimum: 0 + is_image: + type: boolean + TaskListRecord: + type: object + additionalProperties: true + RuntimeTaskRecord: + type: object + additionalProperties: true + TeamRecord: + type: object + additionalProperties: true + MailboxMessage: + type: object + additionalProperties: true + MailboxSummary: + type: object + additionalProperties: true + BackgroundApprovalRecord: + type: object + additionalProperties: true + FeedItemRecord: + type: object + additionalProperties: true + LibraryAppRecord: + type: object + additionalProperties: true + LibraryAppVersionRecord: + type: object + additionalProperties: true + AppPackageRequest: + type: object + required: [feed_item_id] + properties: + feed_item_id: + type: string + requested_app_id: + type: string + title: + type: string + description: + type: string + AppPublishRequest: + type: object + required: [message] + properties: + message: + type: string + AppPackageResult: + type: object + required: [app, created] + properties: + app: + $ref: "#/components/schemas/LibraryAppRecord" + created: + type: boolean + AppWorkspaceRecord: + type: object + additionalProperties: true + AppPublishResult: + type: object + required: [app, workspace] + properties: + app: + $ref: "#/components/schemas/LibraryAppRecord" + workspace: + $ref: "#/components/schemas/AppWorkspaceRecord" + AppHistoryResponse: + type: object + additionalProperties: true + WorkerStatusResponse: + type: object + required: + - profile_id + - message_count + - model + - permission_mode + - default_cwd + - busy + - task_list_id + properties: + profile_id: + type: string + message_count: + type: integer + minimum: 0 + model: + type: string + permission_mode: + type: string + default_cwd: + type: string + busy: + type: boolean + task_list_id: + type: string + active_team: + type: string + WorkerTaskListResponse: + type: object + required: [task_list_id, tasks] + properties: + task_list_id: + type: string + tasks: + type: array + items: + $ref: "#/components/schemas/TaskListRecord" + WorkerTaskSnapshotResponse: + type: object + properties: + task: + $ref: "#/components/schemas/TaskListRecord" + runtime_task: + $ref: "#/components/schemas/RuntimeTaskRecord" + WorkerTeamSnapshotResponse: + type: object + required: [task_list_id] + properties: + team: + $ref: "#/components/schemas/TeamRecord" + task_list_id: + type: string + WorkerAgentListResponse: + type: object + required: [agents] + properties: + agents: + type: array + items: + $ref: "#/components/schemas/RuntimeTaskRecord" + WorkerMailboxSummaryResponse: + type: object + required: [mailbox] + properties: + mailbox: + $ref: "#/components/schemas/MailboxSummary" + WorkerMailboxPendingResponse: + type: object + required: [messages] + properties: + messages: + type: array + items: + $ref: "#/components/schemas/MailboxMessage" + WorkerBackgroundApprovalListResponse: + type: object + required: [approvals] + properties: + approvals: + type: array + items: + $ref: "#/components/schemas/BackgroundApprovalRecord" + WorkerFeedListResponse: + type: object + required: [items] + properties: + items: + type: array + items: + $ref: "#/components/schemas/FeedItemRecord" + state_root: + type: string + warnings: + type: array + items: + type: string + WorkerFeedItemResponse: + type: object + properties: + item: + $ref: "#/components/schemas/FeedItemRecord" + state_root: + type: string + WorkerAppListResponse: + type: object + required: [apps] + properties: + apps: + type: array + items: + $ref: "#/components/schemas/LibraryAppRecord" + state_root: + type: string + warnings: + type: array + items: + type: string + WorkerAppSnapshotResponse: + type: object + properties: + app: + $ref: "#/components/schemas/LibraryAppRecord" + state_root: + type: string + WorkerAppVersionResponse: + type: object + properties: + version: + $ref: "#/components/schemas/LibraryAppVersionRecord" + WorkerAppPackageResponse: + type: object + properties: + result: + $ref: "#/components/schemas/AppPackageResult" + WorkerAppWorkspaceResponse: + type: object + properties: + workspace: + $ref: "#/components/schemas/AppWorkspaceRecord" + WorkerAppPublishResponse: + type: object + properties: + result: + $ref: "#/components/schemas/AppPublishResult" + WorkerAppHistoryResponse: + type: object + properties: + history: + $ref: "#/components/schemas/AppHistoryResponse" + WorkerTeamCreatedEvent: + type: object + required: + - team + - task_list_id + - team_file_path + properties: + team: + $ref: "#/components/schemas/TeamRecord" + task_list_id: + type: string + team_file_path: + type: string + WorkerMailboxMessageEvent: + type: object + required: + - team_name + - sender + - count + properties: + team_name: + type: string + sender: + type: string + count: + type: integer + minimum: 0 + recipients: + type: array + items: + type: string + summary: + type: string + WorkerTurnEvent: + oneOf: + - type: object + required: [type, delta] + properties: + type: + const: assistant_text_delta + delta: + type: string + - type: object + required: [type, id, name, input] + properties: + type: + const: tool_use + id: + type: string + name: + type: string + input: + type: string + - type: object + required: [type, tool_use_id, tool_name, output, is_error] + properties: + type: + const: tool_result + tool_use_id: + type: string + tool_name: + type: string + output: + type: string + is_error: + type: boolean + - type: object + required: [type, request] + properties: + type: + const: approval_requested + request: + $ref: "#/components/schemas/ApprovalRequestPayload" + - type: object + required: [type, removed_message_count] + properties: + type: + const: auto_compaction + removed_message_count: + type: integer + minimum: 0 + - type: object + required: [type, task_list_id, task] + properties: + type: + const: task_created + task_list_id: + type: string + task: + $ref: "#/components/schemas/TaskListRecord" + - type: object + required: [type, task_list_id, task] + properties: + type: + const: task_updated + task_list_id: + type: string + task: + $ref: "#/components/schemas/TaskListRecord" + - type: object + required: [type, task] + properties: + type: + const: task_stopped + task: + $ref: "#/components/schemas/RuntimeTaskRecord" + - type: object + required: [type, agent] + properties: + type: + const: agent_spawned + agent: + $ref: "#/components/schemas/RuntimeTaskRecord" + - type: object + required: [type, team] + properties: + type: + const: team_created + team: + $ref: "#/components/schemas/WorkerTeamCreatedEvent" + - type: object + required: [type, team_name] + properties: + type: + const: team_deleted + team_name: + type: string + - type: object + required: [type, message] + properties: + type: + const: mailbox_message + message: + $ref: "#/components/schemas/WorkerMailboxMessageEvent" + - type: object + required: + - type + - final_text + - iterations + - input_tokens + - output_tokens + - generated_files + properties: + type: + const: completed + final_text: + type: string + iterations: + type: integer + minimum: 0 + input_tokens: + type: integer + minimum: 0 + output_tokens: + type: integer + minimum: 0 + generated_files: + type: array + items: + $ref: "#/components/schemas/GeneratedFileDescriptor" + - type: object + required: [type, message] + properties: + type: + const: failed + message: + type: string diff --git a/worker-protocol/schemas/worker-approval-decision.schema.json b/worker-protocol/schemas/worker-approval-decision.schema.json new file mode 100644 index 0000000..b6089f8 --- /dev/null +++ b/worker-protocol/schemas/worker-approval-decision.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://claw.local/worker-protocol/schemas/worker-approval-decision.schema.json", + "title": "WorkerApprovalDecision", + "oneOf": [ + { + "type": "object", + "required": ["decision"], + "properties": { + "decision": { + "type": "string", + "enum": [ + "approve_once", + "approve_tool_for_session", + "approve_all_for_session", + "cancel_turn" + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["decision", "reason"], + "properties": { + "decision": { + "type": "string", + "enum": ["deny"] + }, + "reason": { + "type": "string" + } + }, + "additionalProperties": false + } + ] +} diff --git a/worker-protocol/schemas/worker-turn-event.schema.json b/worker-protocol/schemas/worker-turn-event.schema.json new file mode 100644 index 0000000..d5c42d4 --- /dev/null +++ b/worker-protocol/schemas/worker-turn-event.schema.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://claw.local/worker-protocol/schemas/worker-turn-event.schema.json", + "title": "WorkerTurnEvent", + "oneOf": [ + { + "type": "object", + "required": ["type", "delta"], + "properties": { + "type": { "const": "assistant_text_delta" }, + "delta": { "type": "string" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["type", "id", "name", "input"], + "properties": { + "type": { "const": "tool_use" }, + "id": { "type": "string" }, + "name": { "type": "string" }, + "input": { "type": "string" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["type", "tool_use_id", "tool_name", "output", "is_error"], + "properties": { + "type": { "const": "tool_result" }, + "tool_use_id": { "type": "string" }, + "tool_name": { "type": "string" }, + "output": { "type": "string" }, + "is_error": { "type": "boolean" } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["type", "request"], + "properties": { + "type": { "const": "approval_requested" }, + "request": { + "type": "object", + "required": [ + "approval_id", + "tool_name", + "input", + "current_mode", + "required_mode" + ], + "properties": { + "approval_id": { "type": "string" }, + "tool_name": { "type": "string" }, + "input": { "type": "string" }, + "current_mode": { "type": "string" }, + "required_mode": { "type": "string" }, + "reason": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type", + "final_text", + "iterations", + "input_tokens", + "output_tokens", + "generated_files" + ], + "properties": { + "type": { "const": "completed" }, + "final_text": { "type": "string" }, + "iterations": { "type": "integer", "minimum": 0 }, + "input_tokens": { "type": "integer", "minimum": 0 }, + "output_tokens": { "type": "integer", "minimum": 0 }, + "generated_files": { + "type": "array", + "items": { + "type": "object", + "required": ["file_id", "file_name", "size_bytes", "is_image"], + "properties": { + "file_id": { "type": "string" }, + "file_name": { "type": "string" }, + "media_type": { "type": "string" }, + "size_bytes": { "type": "integer", "minimum": 0 }, + "is_image": { "type": "boolean" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["type", "message"], + "properties": { + "type": { "const": "failed" }, + "message": { "type": "string" } + }, + "additionalProperties": false + } + ] +} diff --git a/worker-protocol/schemas/worker-turn-request.schema.json b/worker-protocol/schemas/worker-turn-request.schema.json new file mode 100644 index 0000000..bee6007 --- /dev/null +++ b/worker-protocol/schemas/worker-turn-request.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://claw.local/worker-protocol/schemas/worker-turn-request.schema.json", + "title": "WorkerTurnRequest", + "type": "object", + "required": ["prompt", "source"], + "properties": { + "prompt": { + "type": "string" + }, + "source": { + "type": "object", + "required": ["channel", "sender_id"], + "properties": { + "channel": { "type": "string" }, + "sender_id": { "type": "string" }, + "chat_id": { "type": "string" }, + "display_name": { "type": "string" } + }, + "additionalProperties": false + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "required": ["file_name", "kind", "data_base64"], + "properties": { + "file_name": { "type": "string" }, + "kind": { "type": "string", "enum": ["photo", "document"] }, + "media_type": { "type": "string" }, + "data_base64": { "type": "string" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +}