1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// Portable harness execution configuration (EVE-881).
//
// Decision: the stored `Harness` persistence record — lifecycle status,
// hierarchy identifiers, built-in flags, organization ownership, timestamps —
// lives in `everruns-platform`. Core keeps only this portable, execution-facing
// projection: the neutral environment configuration the runtime folds as the
// base layer of the harness → agent → session overlay chain. The platform
// loading seam (server repositories, worker adapters, hosted stores) resolves
// parent-chain inheritance, enforces archived/deleted validation, and projects
// stored records into this value before host execution begins — the host never
// requests or receives a stored Harness.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::capability_types::AgentCapabilityConfig;
use crate::mcp_server::{ScopedMcpServers, scoped_mcp_servers_is_empty};
use crate::network_access::NetworkAccessList;
use crate::session_file::InitialFile;
use crate::typed_id::ModelId;
/// Portable harness execution configuration.
///
/// Carries exactly what turn execution consumes: the effective
/// (inheritance-resolved) base environment configuration below the agent and
/// session overlay layers. It is not a persistence record — identity,
/// hierarchy, lifecycle, and display bookkeeping stay in `everruns-platform`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HarnessDefinition {
/// Addressable name (e.g. "generic"). Correlation/bookkeeping only; the
/// stored record's identity and uniqueness live at the platform layer.
pub name: String,
/// System prompt contributed by the harness layer. `None` contributes no
/// base prompt; empty/whitespace values normalize to `None` during merge.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
/// Default LLM model; lowest priority in the chain
/// (controls > session > agent > harness).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model_id: Option<ModelId>,
/// Capabilities enabled for this harness with per-harness configuration.
#[serde(default)]
pub capabilities: Vec<AgentCapabilityConfig>,
/// Starter files copied into each new session for this harness.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub initial_files: Vec<InitialFile>,
/// Network access list merged with agent and session layers.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_access: Option<NetworkAccessList>,
/// Request-level parallel tool calling preference (EVE-598).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
/// Remote MCP servers scoped to this harness and inherited by descendant layers.
#[serde(
default,
rename = "mcpServers",
alias = "mcp_servers",
skip_serializing_if = "scoped_mcp_servers_is_empty"
)]
pub mcp_servers: ScopedMcpServers,
/// Arbitrary key-value metadata injected into LLM requests for
/// observability. Folded root-to-leaf across the harness chain (leaf wins)
/// by the platform projection before this value is built.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub embedder_metadata: HashMap<String, String>,
}
impl HarnessDefinition {
/// Create a definition with the given name and base prompt; all other
/// configuration starts empty. Empty prompts normalize to `None`.
pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
let system_prompt = system_prompt.into();
Self {
name: name.into(),
system_prompt: (!system_prompt.trim().is_empty()).then_some(system_prompt),
..Self::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn definition_preserves_portable_wire_contract() {
let mut definition = HarnessDefinition::new("generic", "You are helpful.");
let mut expected = serde_json::json!({"name": "generic", "system_prompt": "You are helpful.", "capabilities": []});
// Exact shape also excludes product persistence metadata.
assert_eq!(serde_json::to_value(&definition).unwrap(), expected);
definition.capabilities = vec![AgentCapabilityConfig::with_config(
"web_fetch",
serde_json::json!({"timeout_ms": 30000}),
)];
definition
.embedder_metadata
.insert("deployment".into(), "local".into());
definition.parallel_tool_calls = Some(false);
definition.mcp_servers.insert(
"docs".into(),
crate::mcp_server::ScopedMcpServer {
url: "https://docs.example.test/mcp".into(),
..Default::default()
},
);
expected["capabilities"] =
serde_json::json!([{"ref": "web_fetch", "config": {"timeout_ms": 30000}}]);
expected["embedder_metadata"] = serde_json::json!({"deployment": "local"});
expected["parallel_tool_calls"] = serde_json::json!(false);
expected["mcpServers"] =
serde_json::json!({"docs": {"type": "http", "url": "https://docs.example.test/mcp"}});
assert_eq!(serde_json::to_value(&definition).unwrap(), expected);
let parsed: HarnessDefinition = serde_json::from_value(expected.clone()).unwrap();
assert_eq!(serde_json::to_value(parsed).unwrap(), expected);
}
#[test]
fn new_normalizes_empty_prompt_to_none() {
for prompt in ["", " ", "\n\t"] {
assert_eq!(HarnessDefinition::new("h", prompt).system_prompt, None);
}
for prompt in ["prompt", " prompt "] {
assert_eq!(
HarnessDefinition::new("h", prompt).system_prompt.as_deref(),
Some(prompt)
);
}
}
}