1use super::errors::ProtocolError;
16use super::snapshot::{
17 DeviceSnapshot, LogLine, RemoteAgentProvider, RemoteAgentUsage, RemoteBundle, RemoteIncident,
18 RemotePullRequest, RemoteService, RemoteTimelineEntry, RemoteWorkflowJob, RemoteWorkflowRun,
19};
20use super::version::CapabilitySet;
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(tag = "type", content = "payload")]
26pub enum PlatformBound {
27 #[serde(rename = "session.hello")]
30 SessionHello(SessionHello),
31 #[serde(rename = "session.heartbeat")]
35 SessionHeartbeat(super::device_bound::Empty),
36
37 #[serde(rename = "device.snapshot.response")]
38 DeviceSnapshot(DeviceSnapshotResponse),
39 #[serde(rename = "service.list.response")]
40 ServiceList(ServiceListResponse),
41 #[serde(rename = "service.action.response")]
42 ServiceAction(ServiceActionResponse),
43 #[serde(rename = "service.logs.response")]
44 ServiceLogs(ServiceLogsResponse),
45 #[serde(rename = "bundle.list.response")]
46 BundleList(BundleListResponse),
47
48 #[serde(rename = "agent.providers.response")]
49 AgentProviders(AgentProvidersResponse),
50 #[serde(rename = "agent.turn.accepted")]
53 AgentTurnAccepted(AgentTurnAccepted),
54 #[serde(rename = "agent.turn.event")]
56 AgentTurnEvent(super::agent_event::AgentEvent),
57
58 #[serde(rename = "terminal.spawned")]
60 TerminalSpawned(TerminalSpawned),
61 #[serde(rename = "terminal.sessions.response")]
63 TerminalSessions(TerminalSessionsResponse),
64 #[serde(rename = "terminal.attach.accepted")]
66 TerminalAttachAccepted(TerminalAttachAccepted),
67 #[serde(rename = "terminal.output")]
69 TerminalOutput(TerminalOutput),
70 #[serde(rename = "terminal.geometry")]
72 TerminalGeometry(TerminalGeometry),
73 #[serde(rename = "terminal.ack")]
75 TerminalAck(TerminalAck),
76 #[serde(rename = "terminal.closed")]
78 TerminalClosed(TerminalClosed),
79 #[serde(rename = "terminal.killed")]
81 TerminalKilled(TerminalKilled),
82
83 #[serde(rename = "repositories.response")]
85 Repositories(RepositoriesResponse),
86
87 #[serde(rename = "github.runs.response")]
89 GithubRuns(GithubRunsResponse),
90 #[serde(rename = "github.run.jobs.response")]
92 GithubRunJobs(GithubRunJobsResponse),
93 #[serde(rename = "github.prs.response")]
95 GithubPulls(GithubPullsResponse),
96 #[serde(rename = "github.pr.response")]
98 GithubPull(GithubPullResponse),
99 #[serde(rename = "linear.response")]
100 Linear(Box<super::linear::LinearResponse>),
104 #[serde(rename = "agent.usage.response")]
106 AgentUsage(AgentUsageResponse),
107 #[serde(rename = "errors.response")]
109 Errors(ErrorsResponse),
110 #[serde(rename = "timeline.response")]
112 Timeline(TimelineResponse),
113
114 #[serde(rename = "device.retire")]
132 DeviceRetire(DeviceRetire),
133
134 #[serde(rename = "command.error")]
137 CommandError(CommandErrorResponse),
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct DeviceRetire {
149 pub reason: RetireReason,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub enum RetireReason {
155 Unpaired,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase", deny_unknown_fields)]
169pub struct RemoteRepository {
170 pub id: String,
173 pub name: String,
174 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
177 pub selected: bool,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase", deny_unknown_fields)]
182pub struct RepositoriesResponse {
183 pub repositories: Vec<RemoteRepository>,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase", deny_unknown_fields)]
188pub struct GithubRunsResponse {
189 pub runs: Vec<RemoteWorkflowRun>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub branch: Option<String>,
194 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
195 pub truncated: bool,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase", deny_unknown_fields)]
200pub struct GithubRunJobsResponse {
201 pub run_id: String,
202 pub jobs: Vec<RemoteWorkflowJob>,
203 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
204 pub truncated: bool,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase", deny_unknown_fields)]
209pub struct GithubPullsResponse {
210 pub pulls: Vec<RemotePullRequest>,
211 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
212 pub truncated: bool,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase", deny_unknown_fields)]
217pub struct GithubPullResponse {
218 pub pull: RemotePullRequest,
219}
220
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase", deny_unknown_fields)]
227pub struct AgentUsageResponse {
228 pub usage: RemoteAgentUsage,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase", deny_unknown_fields)]
233pub struct ErrorsResponse {
234 pub incidents: Vec<RemoteIncident>,
235 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
236 pub truncated: bool,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase", deny_unknown_fields)]
241pub struct TimelineResponse {
242 pub entries: Vec<RemoteTimelineEntry>,
243 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
244 pub truncated: bool,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase", deny_unknown_fields)]
254pub struct TerminalSpawned {
255 pub session: super::snapshot::RemoteTerminalSession,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "camelCase", deny_unknown_fields)]
260pub struct TerminalSessionsResponse {
261 pub sessions: Vec<super::snapshot::RemoteTerminalSession>,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase", deny_unknown_fields)]
266pub struct TerminalAttachAccepted {
267 pub stream_id: String,
271 pub session_id: String,
272 pub cols: u16,
275 pub rows: u16,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename_all = "camelCase", deny_unknown_fields)]
280pub struct TerminalOutput {
281 pub stream_id: String,
282 pub seq: u64,
286 pub data: super::terminal_bytes::TerminalBytes,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase", deny_unknown_fields)]
309pub struct TerminalGeometry {
310 pub stream_id: String,
311 pub cols: u16,
312 pub rows: u16,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase", deny_unknown_fields)]
324pub struct TerminalAck {
325 pub stream_id: String,
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase", deny_unknown_fields)]
337pub struct TerminalKilled {
338 pub session_id: String,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub enum TerminalCloseReason {
345 Detached,
347 Exited,
349 SessionClosed,
351 Overrun,
354 #[serde(other)]
355 Unknown,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase", deny_unknown_fields)]
360pub struct TerminalClosed {
361 pub stream_id: String,
362 pub reason: TerminalCloseReason,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase", deny_unknown_fields)]
367pub struct SessionHello {
368 pub supported_versions: Vec<u32>,
370 pub daemon_version: String,
371 pub platform: String,
373 pub capabilities: CapabilitySet,
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase", deny_unknown_fields)]
378pub struct DeviceSnapshotResponse {
379 pub device: DeviceSnapshot,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase", deny_unknown_fields)]
384pub struct ServiceListResponse {
385 pub services: Vec<RemoteService>,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase", deny_unknown_fields)]
395pub struct ServiceActionResponse {
396 pub service: String,
397 pub action: super::device_bound::ServiceAction,
398 pub state: super::snapshot::ServiceState,
399}
400
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase", deny_unknown_fields)]
403pub struct ServiceLogsResponse {
404 pub service: String,
405 pub lines: Vec<LogLine>,
406 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
409 pub truncated: bool,
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
413#[serde(rename_all = "camelCase", deny_unknown_fields)]
414pub struct BundleListResponse {
415 pub bundles: Vec<RemoteBundle>,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase", deny_unknown_fields)]
420pub struct AgentProvidersResponse {
421 pub providers: Vec<RemoteAgentProvider>,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425#[serde(rename_all = "camelCase", deny_unknown_fields)]
426pub struct AgentTurnAccepted {
427 pub run_id: String,
428 pub next_seq: u64,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434#[serde(rename_all = "camelCase", deny_unknown_fields)]
435pub struct CommandErrorResponse {
436 pub error: ProtocolError,
437}
438
439impl PlatformBound {
440 pub const KINDS: &'static [&'static str] = &[
442 "session.hello",
443 "session.heartbeat",
444 "device.snapshot.response",
445 "service.list.response",
446 "service.action.response",
447 "service.logs.response",
448 "bundle.list.response",
449 "agent.providers.response",
450 "agent.turn.accepted",
451 "agent.turn.event",
452 "terminal.spawned",
453 "terminal.sessions.response",
454 "terminal.attach.accepted",
455 "terminal.output",
456 "terminal.geometry",
457 "terminal.ack",
458 "terminal.killed",
459 "terminal.closed",
460 "repositories.response",
461 "github.runs.response",
462 "github.run.jobs.response",
463 "github.prs.response",
464 "github.pr.response",
465 "linear.response",
466 "agent.usage.response",
467 "errors.response",
468 "timeline.response",
469 "device.retire",
470 "command.error",
471 ];
472
473 pub fn kind(&self) -> &'static str {
474 match self {
475 Self::SessionHello(_) => "session.hello",
476 Self::SessionHeartbeat(_) => "session.heartbeat",
477 Self::DeviceSnapshot(_) => "device.snapshot.response",
478 Self::ServiceList(_) => "service.list.response",
479 Self::ServiceAction(_) => "service.action.response",
480 Self::ServiceLogs(_) => "service.logs.response",
481 Self::BundleList(_) => "bundle.list.response",
482 Self::AgentProviders(_) => "agent.providers.response",
483 Self::AgentTurnAccepted(_) => "agent.turn.accepted",
484 Self::AgentTurnEvent(_) => "agent.turn.event",
485 Self::TerminalSpawned(_) => "terminal.spawned",
486 Self::TerminalSessions(_) => "terminal.sessions.response",
487 Self::TerminalAttachAccepted(_) => "terminal.attach.accepted",
488 Self::TerminalOutput(_) => "terminal.output",
489 Self::TerminalGeometry(_) => "terminal.geometry",
490 Self::TerminalAck(_) => "terminal.ack",
491 Self::TerminalKilled(_) => "terminal.killed",
492 Self::TerminalClosed(_) => "terminal.closed",
493 Self::Repositories(_) => "repositories.response",
494 Self::GithubRuns(_) => "github.runs.response",
495 Self::GithubRunJobs(_) => "github.run.jobs.response",
496 Self::GithubPulls(_) => "github.prs.response",
497 Self::GithubPull(_) => "github.pr.response",
498 Self::Linear(_) => "linear.response",
499 Self::AgentUsage(_) => "agent.usage.response",
500 Self::Errors(_) => "errors.response",
501 Self::Timeline(_) => "timeline.response",
502 Self::DeviceRetire(_) => "device.retire",
503 Self::CommandError(_) => "command.error",
504 }
505 }
506
507 pub fn requires_reply_to(&self) -> bool {
515 match self {
516 Self::SessionHello(_)
517 | Self::SessionHeartbeat(_)
518 | Self::AgentTurnEvent(_)
519 | Self::TerminalOutput(_)
523 | Self::TerminalGeometry(_)
527 | Self::TerminalClosed(_)
528 | Self::DeviceRetire(_) => false,
531 Self::DeviceSnapshot(_)
532 | Self::ServiceList(_)
533 | Self::ServiceAction(_)
534 | Self::ServiceLogs(_)
535 | Self::BundleList(_)
536 | Self::AgentProviders(_)
537 | Self::AgentTurnAccepted(_)
538 | Self::TerminalSpawned(_)
539 | Self::TerminalSessions(_)
540 | Self::TerminalAttachAccepted(_)
541 | Self::TerminalAck(_)
542 | Self::TerminalKilled(_)
543 | Self::Linear(_)
544 | Self::Repositories(_)
545 | Self::GithubRuns(_)
546 | Self::GithubRunJobs(_)
547 | Self::GithubPulls(_)
548 | Self::GithubPull(_)
549 | Self::AgentUsage(_)
550 | Self::Errors(_)
551 | Self::Timeline(_)
552 | Self::CommandError(_) => true,
553 }
554 }
555
556 pub fn parse(kind: &str, payload: serde_json::Value) -> Result<Self, ProtocolError> {
559 use super::errors::ErrorCode;
560 if !Self::KINDS.contains(&kind) {
561 return Err(
562 ProtocolError::new(ErrorCode::UnknownCommand, "Unknown remote event.")
563 .with_detail(kind),
564 );
565 }
566 let tagged = serde_json::json!({ "type": kind, "payload": payload });
567 serde_json::from_value(tagged).map_err(|error| {
568 ProtocolError::new(ErrorCode::MalformedFrame, "Event payload is not valid.")
569 .with_detail(error.to_string())
570 })
571 }
572
573 pub fn payload(&self) -> serde_json::Value {
575 let tagged = serde_json::to_value(self).expect("an event always serialises");
576 tagged
577 .get("payload")
578 .cloned()
579 .unwrap_or(serde_json::Value::Null)
580 }
581}