Decouple gateway protocol from Rust runtime types
Build Claw Telegram / build (push) Successful in 5m39s
Build Claw Telegram / cleanup (push) Successful in 8s

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) <noreply@anthropic.com>
This commit is contained in:
Wylabb
2026-04-07 20:12:16 +02:00
parent 951e48248b
commit 9e9d440770
11 changed files with 2136 additions and 52 deletions
@@ -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,
@@ -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<runtime::MailboxMessage>,
pub messages: Vec<MailboxMessage>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -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<runtime::TaskListStatus> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
pub status: TaskListStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocks: Vec<String>,
#[serde(rename = "blockedBy", default, skip_serializing_if = "Vec::is_empty")]
pub blocked_by: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<BTreeMap<String, Value>>,
#[serde(default)]
pub internal: bool,
#[serde(rename = "createdAt")]
pub created_at: u64,
#[serde(rename = "updatedAt")]
pub updated_at: u64,
}
impl From<runtime::TaskListRecord> 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<runtime::RuntimeTaskKind> 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<runtime::RuntimeTaskStatus> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code_file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub final_result: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(default)]
pub notified: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub team_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree_branch: Option<String>,
pub created_at: u64,
pub started_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at: Option<u64>,
}
impl From<runtime::RuntimeTaskRecord> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
pub joined_at: u64,
}
impl From<runtime::TeamMemberRecord> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
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<TeamMemberRecord>,
}
impl From<runtime::TeamRecord> 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<String>,
pub message: Value,
pub timestamp: u64,
#[serde(default)]
pub read: bool,
#[serde(default)]
pub notified: bool,
}
impl From<runtime::MessageEnvelope> 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<runtime::MailboxMessage> 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<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recent_messages: Vec<MailboxMessage>,
}
impl From<runtime::MailboxSummary> 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<runtime::BackgroundApprovalDecision> 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<String>,
pub created_at: u64,
#[serde(default)]
pub notified: bool,
#[serde(default)]
pub decision: Option<BackgroundApprovalDecision>,
}
impl From<runtime::BackgroundApprovalRecord> 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<runtime::FeedItemKind> 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<runtime::FeedItemSourceKind> 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<runtime::FeedItemChangeKind> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_file_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_files: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub change_kind: Option<FeedItemChangeKind>,
}
impl From<runtime::FeedItemSource> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stored_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preview_text: Option<String>,
#[serde(default)]
pub deleted: bool,
pub created_at: u64,
pub updated_at: u64,
}
impl From<runtime::FeedItemRecord> 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<runtime::AppKind> 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<runtime::AppVisibility> 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<runtime::AppCapability> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
}
impl From<runtime::AppRepoLink> 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<String>,
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<String>,
pub current_version_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub published_commit: Option<String>,
pub default_branch: String,
pub local_repo_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remote_repo: Option<AppRepoLink>,
pub kind: AppKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<AppCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_turn_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_feed_item_ids: Vec<String>,
}
impl From<runtime::LibraryAppManifestV2> 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<String>,
pub kind: AppKind,
pub entry: String,
pub files: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<AppCapability>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
impl From<runtime::AppBundleManifestV1> 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<String>,
#[serde(default = "default_branch_name")]
pub branch: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub published_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_turn_id: Option<String>,
#[serde(
alias = "source_artifact_ids",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub source_feed_item_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub changelog_summary: Option<String>,
}
impl From<runtime::LibraryAppVersionRecord> 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<runtime::LibraryAppRecord> 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl From<AppPackageRequest> 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<runtime::AppPackageResult> 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<runtime::AppWorkspaceRecord> 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<AppPublishRequest> 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<runtime::AppPublishResult> 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<runtime::AppHistoryEntry> 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<AppHistoryEntry>,
}
impl From<runtime::AppHistoryResponse> for AppHistoryResponse {
fn from(value: runtime::AppHistoryResponse) -> Self {
Self {
entries: value.entries.into_iter().map(Into::into).collect(),
}
}
}
+65 -34
View File
@@ -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<Json<WorkerTaskListResponse>, 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<Json<WorkerAppPublishResponse>, 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<WorkerTurnEvent> {
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<WorkerTurnEvent> {
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<WorkerTurnEvent> {
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<WorkerTurnEvent> {
@@ -1317,7 +1348,7 @@ fn derive_agent_spawned_event(value: &Value) -> Option<WorkerTurnEvent> {
.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<WorkerTurnEvent> {
@@ -1331,7 +1362,7 @@ fn derive_team_created_event(value: &Value) -> Option<WorkerTurnEvent> {
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
team,
team: team.into(),
},
})
}
+10 -9
View File
@@ -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
@@ -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;
+29
View File
@@ -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.
+953
View File
@@ -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
@@ -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
}
]
}
@@ -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
}
]
}
@@ -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
}