Skip to main content

supercode_interchange/orchestration/
worker.rs

1//! The worker: which harness runs a profile's conversations, and how (§2.2).
2
3use std::collections::BTreeMap;
4
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::ontology::{HarnessId, SecretRef};
9
10/// A worker environment value: a literal, or a secret by reference.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12#[serde(untagged)]
13pub enum EnvValue {
14    /// A plain, non-secret value.
15    Literal(String),
16    /// A secret, named and never held.
17    Secret(SecretRef),
18}
19
20/// What happens to a worker's permission prompt when no human answers.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
22#[serde(rename_all = "snake_case")]
23pub enum PermissionDefault {
24    /// Refuse after the timeout.
25    #[default]
26    Deny,
27    /// Allow after the timeout.
28    Allow,
29}
30
31/// What a prompt raised where nobody attends (a cron fire, a webhook turn) is answered with, at
32/// once (Hermes `approvals.cron_mode`).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
34#[serde(rename_all = "snake_case")]
35pub enum PermissionUnattended {
36    /// Refuse it.
37    #[default]
38    Deny,
39    /// Approve it.
40    Approve,
41}
42
43impl PermissionUnattended {
44    fn is_deny(&self) -> bool {
45        *self == Self::Deny
46    }
47}
48
49/// How prompts are answered when no human is reachable (§4.6).
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51pub struct PermissionPolicy {
52    /// Seconds a relayed prompt waits for an answer.
53    #[serde(default = "default_permission_timeout")]
54    pub timeout_seconds: u32,
55    /// The answer given when nobody replies in time.
56    #[serde(default)]
57    pub default: PermissionDefault,
58    /// The answer given at once where nobody attends: a cron fire or a webhook turn.
59    #[serde(default, skip_serializing_if = "PermissionUnattended::is_deny")]
60    pub unattended: PermissionUnattended,
61}
62
63fn default_permission_timeout() -> u32 {
64    300
65}
66
67impl Default for PermissionPolicy {
68    fn default() -> Self {
69        Self {
70            timeout_seconds: 300,
71            default: PermissionDefault::Deny,
72            unattended: PermissionUnattended::Deny,
73        }
74    }
75}
76
77/// Whose Claude home (or harness home) a worker runs in.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
79#[serde(rename_all = "snake_case")]
80pub enum WorkerHome {
81    /// The profile folder is the harness's whole config home (its persona, skills and MCP servers only).
82    #[default]
83    Profile,
84    /// The user's own harness home (their instructions, skills, hooks, plugins, MCP servers and login), with the
85    /// profile's persona and skills brought in beside it for the session.
86    User,
87}
88
89impl WorkerHome {
90    fn is_profile(&self) -> bool {
91        matches!(self, Self::Profile)
92    }
93}
94
95/// How a board task's session runs when the board dispatches it to this profile.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
97#[serde(rename_all = "snake_case")]
98pub enum WorkerSurface {
99    /// Hermes's worker: one query, headless, closed when it answers.
100    #[default]
101    Headless,
102    /// An interactive session in a terminal pane on the machine the card names, started with a
103    /// session id the dispatcher chose, kept open, resumed when lost and replaced from the card only
104    /// when it cannot be resumed.
105    Pane,
106}
107
108impl WorkerSurface {
109    fn is_headless(&self) -> bool {
110        matches!(self, Self::Headless)
111    }
112}
113
114/// The worker specification (O-record; Hermes has no worker choice).
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
116pub struct WorkerSpec {
117    /// Any registry id whose runtime can start a session.
118    pub harness: HarnessId,
119    /// Model, where the harness's door accepts one.
120    #[serde(default)]
121    pub model: Option<String>,
122    /// supercode preset name.
123    #[serde(default)]
124    pub preset: Option<String>,
125    /// Relative to the profile dir; `.` by default.
126    #[serde(default = "default_cwd")]
127    pub cwd: String,
128    /// Extra environment for the worker process; secrets by reference.
129    #[serde(default)]
130    pub env: BTreeMap<String, EnvValue>,
131    /// Permission prompt policy.
132    #[serde(default)]
133    pub permission: PermissionPolicy,
134    /// Whose harness home the worker runs in.
135    #[serde(default, skip_serializing_if = "WorkerHome::is_profile")]
136    pub home: WorkerHome,
137    /// How the board runs this profile's tasks.
138    #[serde(default, skip_serializing_if = "WorkerSurface::is_headless")]
139    pub surface: WorkerSurface,
140    /// At most this many of the profile's board tasks run at once; the rest wait in `ready`.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub capacity: Option<u32>,
143}
144
145fn default_cwd() -> String {
146    ".".to_string()
147}