Skip to main content

everruns_core/
execution_snapshot.rs

1// Canonical resolved execution snapshot (EVE-872).
2//
3// Decision: host turn execution consumes this neutral, value-first projection
4// instead of stored Agent/Harness/Session aggregates (the session arrives as the portable `ExecutionSession` projection, EVE-882). Both the Framework
5// in-process runtime and the hosted workers build the same value here, so
6// harness → agent → session precedence is applied by one code path
7// (`AgentConfigOverlay` merge semantics) regardless of which host executes the
8// turn. Credentials, database lifecycle fields, organization records,
9// timestamps, UI metadata, and CRUD status are excluded by construction, so
10// Debug/serialization of this value is secret-free without redaction logic.
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15
16use crate::agent_definition::AgentDefinition;
17use crate::capability_types::AgentCapabilityConfig;
18use crate::config_layer::AgentConfigOverlay;
19use crate::error::{AgentLoopError, Result};
20use crate::harness_definition::HarnessDefinition;
21use crate::mcp_server::{
22    McpProtocolMode, McpServerAuthMode, McpServerTransportType, ScopedMcpServer,
23};
24use crate::network_access::NetworkAccessList;
25use crate::session::ExecutionSession;
26use crate::session_file::InitialFile;
27use crate::tool_types::ToolDefinition;
28use crate::typed_id::{AgentId, HarnessId, ModelId, SessionId, WorkspaceId};
29
30/// Non-secret MCP scope entry retained by the execution snapshot.
31///
32/// Scoped MCP server configs can carry literal credentials in header and env
33/// values (e.g. `Authorization: Bearer …`). The snapshot keeps the scope —
34/// which servers exist and how they authenticate — but only the *names* of
35/// headers and env vars. Execution resolves live connections (including
36/// credential material) through the host's MCP seam, never from this value.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct SnapshotMcpServer {
39    pub transport_type: McpServerTransportType,
40    /// Header names only; header values never enter the snapshot.
41    pub header_names: Vec<String>,
42    /// Env var names only; env values never enter the snapshot.
43    pub env_names: Vec<String>,
44    pub auth_mode: McpServerAuthMode,
45    pub protocol_mode: McpProtocolMode,
46    pub oauth_provider_id: Option<String>,
47    pub tool_discovery: bool,
48}
49
50impl From<&ScopedMcpServer> for SnapshotMcpServer {
51    fn from(server: &ScopedMcpServer) -> Self {
52        let mut header_names: Vec<String> = server.headers.keys().cloned().collect();
53        header_names.sort();
54        let mut env_names: Vec<String> = server.env.keys().cloned().collect();
55        env_names.sort();
56        Self {
57            transport_type: server.transport_type.clone(),
58            header_names,
59            env_names,
60            auth_mode: server.auth_mode.clone(),
61            protocol_mode: server.protocol_mode,
62            oauth_provider_id: server.oauth_provider_id.clone(),
63            tool_discovery: server.tool_discovery,
64        }
65    }
66}
67
68/// Canonical resolved execution value for one session's turn execution.
69///
70/// Contains only turn-relevant configuration plus the typed correlation
71/// values execution needs. Produced by [`ResolvedExecutionSnapshot::project`]
72/// — the platform projection boundary where missing, mismatched, or inactive
73/// stored records fail before host execution begins.
74///
75/// Field order is fixed and map fields are `BTreeMap`s, so serialization of
76/// equal values is byte-identical (deterministic).
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ResolvedExecutionSnapshot {
79    // --- Typed session correlation values required by execution ---
80    pub session_id: SessionId,
81    pub workspace_id: WorkspaceId,
82    pub harness_id: HarnessId,
83    pub agent_id: Option<AgentId>,
84    /// Public (`org_…`) organization id used for execution scoping. This is a
85    /// correlation value, not the organization record.
86    pub organization_id: String,
87
88    // --- Effective configuration (harness → agent → session, applied once) ---
89    /// Effective instructions (merged system prompt, session goal included).
90    pub instructions: Option<String>,
91    /// Effective model specification; `None` selects the host default model.
92    pub default_model_id: Option<ModelId>,
93    /// Configured capability references with per-layer config merged.
94    pub capabilities: Vec<AgentCapabilityConfig>,
95    /// Explicitly configured tools (client-side or capability-provided).
96    pub tools: Vec<ToolDefinition>,
97    /// Effective initial workspace files.
98    pub initial_files: Vec<InitialFile>,
99    /// Effective scoped MCP servers, credential values excluded.
100    pub mcp_servers: BTreeMap<String, SnapshotMcpServer>,
101    /// Effective workspace network policy.
102    pub network_access: Option<NetworkAccessList>,
103    /// Maximum reason iterations per turn.
104    pub max_iterations: Option<usize>,
105    /// Request-level parallel tool calling preference.
106    pub parallel_tool_calls: Option<bool>,
107
108    // --- Non-secret execution metadata ---
109    pub locale: Option<String>,
110    pub tags: Vec<String>,
111    pub blueprint_id: Option<String>,
112    pub blueprint_config: Option<serde_json::Value>,
113    /// Cumulative LLM usage projected from the session for turn accounting.
114    pub cumulative_usage: Option<crate::events::TokenUsage>,
115    /// Embedder metadata folded from the harness chain (root base, leaf wins).
116    pub embedder_metadata: BTreeMap<String, String>,
117}
118
119impl ResolvedExecutionSnapshot {
120    /// Project loaded records into the canonical execution value.
121    ///
122    /// This is the execution projection boundary: it fails when a referenced
123    /// agent is missing or does not match the session's agent id. Harness
124    /// lifecycle and inheritance validation happen one step earlier, at the
125    /// host loading seam (EVE-881): parent-chain loading, cycle/error handling,
126    /// and archived/deleted validation run before only the effective,
127    /// executable [`HarnessDefinition`] reaches this projection. Agent
128    /// lifecycle validation likewise fails at that host seam (EVE-877). Host
129    /// execution never sees a snapshot built from broken record wiring.
130    ///
131    /// Precedence is applied exactly once, through the same
132    /// [`AgentConfigOverlay`] merge semantics the runtime capability
133    /// resolution uses: harness → agent → session, leaf wins.
134    pub fn project(
135        harness: &HarnessDefinition,
136        agent: Option<&AgentDefinition>,
137        session: &ExecutionSession,
138    ) -> Result<Self> {
139        let agent = match (session.agent_id, agent) {
140            (Some(agent_id), Some(agent)) => {
141                if agent.id != agent_id {
142                    return Err(AgentLoopError::config(format!(
143                        "session {} references agent {} but agent {} was provided",
144                        session.id, agent_id, agent.id
145                    )));
146                }
147                Some(agent)
148            }
149            (Some(agent_id), None) => {
150                return Err(AgentLoopError::agent_not_found(agent_id));
151            }
152            (None, _) => None,
153        };
154
155        let agent_layers = agent.into_iter().map(AgentConfigOverlay::from);
156        let effective = AgentConfigOverlay::fold(
157            [AgentConfigOverlay::from(harness)]
158                .into_iter()
159                .chain(agent_layers)
160                .chain([AgentConfigOverlay::from(session)]),
161        );
162
163        // Chain folding (root base, leaf wins) already happened at the platform
164        // seam; the definition carries the effective metadata.
165        let embedder_metadata = harness
166            .embedder_metadata
167            .iter()
168            .map(|(key, value)| (key.clone(), value.clone()))
169            .collect();
170
171        Ok(Self {
172            session_id: session.id,
173            workspace_id: session.workspace_id,
174            harness_id: session.harness_id,
175            agent_id: session.agent_id,
176            organization_id: session.organization_id.clone(),
177            instructions: effective.system_prompt,
178            default_model_id: effective.default_model_id,
179            capabilities: effective.capabilities,
180            tools: effective.tools,
181            initial_files: effective.initial_files,
182            mcp_servers: effective
183                .mcp_servers
184                .iter()
185                .map(|(name, server)| (name.clone(), SnapshotMcpServer::from(server)))
186                .collect(),
187            network_access: effective.network_access,
188            max_iterations: effective.max_iterations,
189            parallel_tool_calls: effective.parallel_tool_calls,
190            locale: session.locale.clone(),
191            tags: session.tags.clone(),
192            blueprint_id: session.blueprint_id.clone(),
193            blueprint_config: session.blueprint_config.clone(),
194            cumulative_usage: session.usage.clone(),
195            embedder_metadata,
196        })
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::network_access::NetworkAccessList;
204    use std::collections::HashMap;
205
206    fn harness() -> HarnessDefinition {
207        HarnessDefinition {
208            capabilities: vec![AgentCapabilityConfig::new("session_file_system")],
209            ..HarnessDefinition::new("base", "Harness prompt.")
210        }
211    }
212
213    fn agent(agent_id: AgentId, _harness_id: HarnessId) -> AgentDefinition {
214        AgentDefinition {
215            max_iterations: Some(8),
216            display_name: Some("Agent".into()),
217            ..AgentDefinition::new(agent_id, "agent", "Agent prompt.")
218        }
219    }
220
221    fn session(
222        session_id: SessionId,
223        harness_id: HarnessId,
224        agent_id: Option<AgentId>,
225    ) -> ExecutionSession {
226        ExecutionSession {
227            agent_id,
228            title: Some("UI-TITLE-MARKER".into()),
229            locale: Some("en-US".into()),
230            tags: vec!["tag-a".into()],
231            ..ExecutionSession::with_own_workspace(session_id, harness_id)
232        }
233    }
234
235    fn ids() -> (HarnessId, AgentId, SessionId) {
236        (
237            HarnessId::from_seed(11),
238            AgentId::from_seed(11),
239            SessionId::from_seed(11),
240        )
241    }
242
243    fn file(path: &str, content: &str) -> InitialFile {
244        InitialFile {
245            path: path.into(),
246            content: content.into(),
247            encoding: "text".into(),
248            is_readonly: false,
249        }
250    }
251
252    // --- Precedence matrix: harness → agent → session, leaf wins ---
253
254    #[test]
255    fn precedence_instructions_concatenate_root_to_leaf() {
256        let (harness_id, agent_id, session_id) = ids();
257        let harness = harness();
258        let agent = agent(agent_id, harness_id);
259        let mut session = session(session_id, harness_id, Some(agent_id));
260        session.system_prompt = Some("Session prompt.".into());
261
262        let snapshot =
263            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
264        assert_eq!(
265            snapshot.instructions.as_deref(),
266            Some("Harness prompt.\n\nAgent prompt.\n\nSession prompt.")
267        );
268    }
269
270    #[test]
271    fn precedence_model_leaf_layer_wins() {
272        let (harness_id, agent_id, session_id) = ids();
273        let mut harness = harness();
274        harness.default_model_id = Some(ModelId::from_seed(1));
275        let mut agent = agent(agent_id, harness_id);
276        agent.default_model_id = Some(ModelId::from_seed(2));
277        let mut session = session(session_id, harness_id, Some(agent_id));
278
279        let snapshot =
280            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
281        assert_eq!(snapshot.default_model_id, Some(ModelId::from_seed(2)));
282
283        session.model_id = Some(ModelId::from_seed(3));
284        let snapshot =
285            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
286        assert_eq!(snapshot.default_model_id, Some(ModelId::from_seed(3)));
287        session.model_id = None;
288        session.agent_id = None;
289        let snapshot = ResolvedExecutionSnapshot::project(&harness, None, &session).unwrap();
290        assert_eq!(snapshot.default_model_id, Some(ModelId::from_seed(1)));
291        harness.default_model_id = None;
292        assert_eq!(
293            ResolvedExecutionSnapshot::project(&harness, None, &session)
294                .unwrap()
295                .default_model_id,
296            None
297        );
298    }
299
300    #[test]
301    fn precedence_capabilities_override_by_id() {
302        let (harness_id, agent_id, session_id) = ids();
303        let mut harness = harness();
304        harness.capabilities = vec![AgentCapabilityConfig::with_config(
305            "web_fetch",
306            serde_json::json!({"enable_file_download": true}),
307        )];
308        let mut agent = agent(agent_id, harness_id);
309        agent.capabilities = vec![AgentCapabilityConfig::with_config(
310            "current_time",
311            serde_json::json!({"zone":"UTC"}),
312        )];
313        let mut session = session(session_id, harness_id, Some(agent_id));
314        session.capabilities = vec![AgentCapabilityConfig::with_config(
315            "web_fetch",
316            serde_json::json!({"enable_file_download": false}),
317        )];
318
319        let snapshot =
320            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
321        assert_eq!(snapshot.capabilities.len(), 2);
322        assert_eq!(
323            snapshot.capabilities[0],
324            AgentCapabilityConfig::with_config(
325                "web_fetch",
326                serde_json::json!({"enable_file_download": false})
327            )
328        );
329        assert_eq!(
330            snapshot.capabilities[1],
331            AgentCapabilityConfig::with_config("current_time", serde_json::json!({"zone":"UTC"}))
332        );
333    }
334
335    #[test]
336    fn capability_ref_round_trips_framework_persistence_and_worker_resolution() {
337        // EVE-873: one neutral reference/config representation. A Framework
338        // `CapabilityRef` serializes to the persisted attachment shape, loads
339        // back as `AgentCapabilityConfig` (the same type), and survives
340        // snapshot projection (the worker resolution input) unchanged.
341        let framework_ref = everruns_capability::CapabilityRef::new("web_fetch")
342            .config(serde_json::json!({"enable_file_download": true}));
343        let persisted = serde_json::to_value(&framework_ref).unwrap();
344        assert_eq!(
345            persisted,
346            serde_json::json!({
347                "ref": "web_fetch",
348                "config": {"enable_file_download": true}
349            }),
350            "wire shape is the persisted attachment row shape"
351        );
352
353        let attachment: AgentCapabilityConfig = serde_json::from_value(persisted).unwrap();
354        assert_eq!(attachment, framework_ref);
355
356        let (harness_id, agent_id, session_id) = ids();
357        let mut harness = harness();
358        harness.capabilities = vec![attachment.clone()];
359        let agent = agent(agent_id, harness_id);
360        let session = session(session_id, harness_id, Some(agent_id));
361        let snapshot =
362            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
363        assert_eq!(snapshot.capabilities, vec![framework_ref.clone()]);
364        assert_eq!(
365            serde_json::to_value(&snapshot.capabilities[0]).unwrap(),
366            serde_json::to_value(&framework_ref).unwrap()
367        );
368    }
369
370    #[test]
371    fn snapshot_debug_redacts_capability_config_values() {
372        // Attachment rows flow through logs and error contexts; their Debug
373        // output must never leak config payloads (which may carry handles or
374        // misplaced credentials).
375        let attachment = AgentCapabilityConfig::with_config(
376            "vendor.search",
377            serde_json::json!({"api_key": "sk-super-secret"}),
378        );
379        let (harness_id, _, session_id) = ids();
380        let mut harness = harness();
381        harness.capabilities = vec![attachment];
382        let snapshot = ResolvedExecutionSnapshot::project(
383            &harness,
384            None,
385            &session(session_id, harness_id, None),
386        )
387        .unwrap();
388        let debug = format!("{snapshot:?}");
389        assert!(debug.contains("vendor.search"));
390        assert!(!debug.contains("sk-super-secret"));
391        assert!(!debug.contains("api_key"));
392    }
393
394    #[test]
395    fn precedence_initial_files_override_by_path() {
396        let (harness_id, agent_id, session_id) = ids();
397        let mut harness = harness();
398        harness.initial_files = vec![file("/config.txt", "harness"), file("/keep.txt", "keep")];
399        let mut agent = agent(agent_id, harness_id);
400        agent.initial_files = vec![file("config.txt", "agent")];
401        agent.initial_files[0].is_readonly = true;
402        agent.initial_files[0].encoding = "base64".into();
403        agent.initial_files[0].content = "YWdlbnQ=".into();
404        let session = session(session_id, harness_id, Some(agent_id));
405
406        let snapshot =
407            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
408        assert_eq!(
409            snapshot.initial_files,
410            vec![
411                agent.initial_files[0].clone(),
412                harness.initial_files[1].clone()
413            ]
414        );
415    }
416
417    #[test]
418    fn precedence_mcp_servers_override_by_name() {
419        let (harness_id, agent_id, session_id) = ids();
420        let mut harness = harness();
421        harness.mcp_servers.insert(
422            "docs".into(),
423            ScopedMcpServer {
424                url: "https://harness.example.com/mcp".into(),
425                ..Default::default()
426            },
427        );
428        harness
429            .mcp_servers
430            .insert("retained".into(), ScopedMcpServer::default());
431        let agent = agent(agent_id, harness_id);
432        let mut session = session(session_id, harness_id, Some(agent_id));
433        session.mcp_servers.insert(
434            "docs".into(),
435            ScopedMcpServer {
436                url: "https://session.example.com/mcp".into(),
437                transport_type: McpServerTransportType::Stdio,
438                headers: [
439                    ("Z-Header".into(), "z-value".into()),
440                    ("A-Header".into(), "a-value".into()),
441                ]
442                .into(),
443                env: [("Z_ENV".into(), "z".into()), ("A_ENV".into(), "a".into())].into(),
444                auth_mode: McpServerAuthMode::OAuth,
445                protocol_mode: McpProtocolMode::V2025June,
446                oauth_provider_id: Some("session-provider".into()),
447                tool_discovery: false,
448                ..Default::default()
449            },
450        );
451
452        let snapshot =
453            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
454        assert_eq!(
455            snapshot.mcp_servers,
456            BTreeMap::from([
457                (
458                    "docs".into(),
459                    SnapshotMcpServer {
460                        transport_type: McpServerTransportType::Stdio,
461                        header_names: vec!["A-Header".into(), "Z-Header".into()],
462                        env_names: vec!["A_ENV".into(), "Z_ENV".into()],
463                        auth_mode: McpServerAuthMode::OAuth,
464                        protocol_mode: McpProtocolMode::V2025June,
465                        oauth_provider_id: Some("session-provider".into()),
466                        tool_discovery: false,
467                    }
468                ),
469                (
470                    "retained".into(),
471                    SnapshotMcpServer {
472                        transport_type: McpServerTransportType::Http,
473                        header_names: vec![],
474                        env_names: vec![],
475                        auth_mode: McpServerAuthMode::None,
476                        protocol_mode: McpProtocolMode::Auto,
477                        oauth_provider_id: None,
478                        tool_discovery: true,
479                    }
480                ),
481            ])
482        );
483    }
484
485    #[test]
486    fn precedence_network_access_narrows() {
487        let (harness_id, agent_id, session_id) = ids();
488        let mut harness = harness();
489        harness.network_access = Some(NetworkAccessList::allow_only([
490            "*.example.com",
491            "api.github.com",
492        ]));
493        harness
494            .network_access
495            .as_mut()
496            .unwrap()
497            .blocked
498            .push("private.example.com".into());
499        let agent = agent(agent_id, harness_id);
500        let mut session = session(session_id, harness_id, Some(agent_id));
501        session.network_access = Some(NetworkAccessList {
502            allowed: vec!["api.example.com".into(), "outside.net".into()],
503            blocked: vec!["blocked.example.com".into()],
504        });
505
506        let snapshot =
507            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
508        let acl = snapshot.network_access.unwrap();
509        assert_eq!(
510            acl,
511            NetworkAccessList {
512                allowed: vec!["api.example.com".into()],
513                blocked: vec!["private.example.com".into(), "blocked.example.com".into()]
514            }
515        );
516        assert!(acl.is_url_allowed("https://api.example.com"));
517        assert!(!acl.is_url_allowed("https://outside.net"));
518    }
519
520    #[test]
521    fn precedence_iteration_controls_leaf_wins() {
522        let (harness_id, agent_id, session_id) = ids();
523        let harness = harness();
524        let mut agent = agent(agent_id, harness_id);
525        agent.max_iterations = Some(40);
526        agent.parallel_tool_calls = Some(true);
527        let mut session = session(session_id, harness_id, Some(agent_id));
528        session.max_iterations = Some(0);
529        session.parallel_tool_calls = Some(false);
530
531        let snapshot =
532            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
533        assert_eq!(snapshot.max_iterations, Some(0));
534        assert_eq!(snapshot.parallel_tool_calls, Some(false));
535    }
536
537    #[test]
538    fn correlation_values_are_copied() {
539        let (harness_id, agent_id, session_id) = ids();
540        let mut harness = harness();
541        harness.embedder_metadata = [("embedder".into(), "fixture-host".into())].into();
542        let agent = agent(agent_id, harness_id);
543        let mut session = session(session_id, harness_id, Some(agent_id));
544        session.workspace_id = WorkspaceId::from_seed(71);
545        session.organization_id = "org_00000000000000000000000000000042".into();
546        session.blueprint_id = Some("review-blueprint".into());
547        session.blueprint_config = Some(serde_json::json!({"region":"eu","enabled":false}));
548        session.usage = Some(crate::events::TokenUsage {
549            input_tokens: 13,
550            output_tokens: 29,
551            cache_read_tokens: Some(7),
552            cache_creation_tokens: Some(3),
553            actual_cost_usd: Some(0.25),
554            estimated_cost_usd: Some(0.5),
555            effective_cost_usd: Some(0.75),
556        });
557
558        let snapshot =
559            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
560        assert_eq!(snapshot.session_id, session_id);
561        assert_eq!(snapshot.workspace_id, session.workspace_id);
562        assert_eq!(snapshot.harness_id, harness_id);
563        assert_eq!(snapshot.agent_id, Some(agent_id));
564        assert_eq!(snapshot.organization_id, session.organization_id);
565        assert_eq!(snapshot.locale.as_deref(), Some("en-US"));
566        assert_eq!(snapshot.tags, vec!["tag-a".to_string()]);
567        assert_eq!(snapshot.blueprint_id, session.blueprint_id);
568        assert_eq!(snapshot.blueprint_config, session.blueprint_config);
569        assert_eq!(
570            serde_json::to_value(snapshot.cumulative_usage).unwrap(),
571            serde_json::to_value(session.usage).unwrap()
572        );
573        assert_eq!(
574            snapshot.embedder_metadata,
575            BTreeMap::from([("embedder".into(), "fixture-host".into())])
576        );
577    }
578
579    // --- Projection failures ---
580    //
581    // Harness lifecycle and inheritance failures (empty chain, chain/leaf
582    // mismatch, archived/deleted records, cycles) moved to the platform
583    // loading seam (EVE-881); they are covered by the `everruns-platform`
584    // `resolve_execution_harness` tests and the hosted store adapters.
585
586    #[test]
587    fn projection_fails_on_missing_agent() {
588        let (harness_id, agent_id, session_id) = ids();
589        let harness = harness();
590        let session = session(session_id, harness_id, Some(agent_id));
591        assert!(
592            matches!(ResolvedExecutionSnapshot::project(&harness, None, &session), Err(AgentLoopError::AgentNotFound(id)) if id == agent_id)
593        );
594    }
595
596    #[test]
597    fn projection_fails_on_mismatched_agent() {
598        let (harness_id, agent_id, session_id) = ids();
599        let harness = harness();
600        let other_agent = agent(AgentId::from_seed(99), harness_id);
601        let session = session(session_id, harness_id, Some(agent_id));
602        match ResolvedExecutionSnapshot::project(&harness, Some(&other_agent), &session)
603            .unwrap_err()
604        {
605            AgentLoopError::Configuration(message) => assert_eq!(
606                message,
607                format!(
608                    "session {session_id} references agent {agent_id} but agent {} was provided",
609                    other_agent.id
610                )
611            ),
612            other => panic!("wrong projection error: {other:?}"),
613        }
614    }
615
616    #[test]
617    fn snapshot_excludes_platform_metadata_and_credential_values() {
618        let (harness_id, agent_id, session_id) = ids();
619        let harness = harness();
620        let mut agent = agent(agent_id, harness_id);
621        agent.description = Some("UI-PREVIEW-MARKER".into());
622        let mut session = session(session_id, harness_id, Some(agent_id));
623        session.mcp_servers.insert(
624            "docs".into(),
625            ScopedMcpServer {
626                url: "https://user:SECRET-URL-MARKER@mcp.example.com".into(),
627                headers: [(
628                    "Authorization".to_string(),
629                    "Bearer SECRET-HEADER-MARKER".to_string(),
630                )]
631                .into_iter()
632                .collect(),
633                env: [("API_KEY".to_string(), "SECRET-ENV-MARKER".to_string())]
634                    .into_iter()
635                    .collect(),
636                command: Some("SECRET-COMMAND-MARKER".into()),
637                args: vec!["--api-key=SECRET-ARG-MARKER".into()],
638                ..Default::default()
639            },
640        );
641
642        let snapshot =
643            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
644
645        let serialized = serde_json::to_string(&snapshot).unwrap();
646        let debugged = format!("{snapshot:?}");
647        for surface in [serialized.as_str(), debugged.as_str()] {
648            // Credential values from scoped MCP config never appear.
649            assert!(!surface.contains("SECRET-HEADER-MARKER"), "{surface}");
650            assert!(!surface.contains("SECRET-ENV-MARKER"), "{surface}");
651            assert!(!surface.contains("SECRET-URL-MARKER"), "{surface}");
652            assert!(!surface.contains("SECRET-COMMAND-MARKER"), "{surface}");
653            assert!(!surface.contains("SECRET-ARG-MARKER"), "{surface}");
654            // UI/platform-only metadata never appears.
655            assert!(!surface.contains("UI-TITLE-MARKER"), "{surface}");
656            assert!(!surface.contains("UI-PREVIEW-MARKER"), "{surface}");
657        }
658
659        // Non-secret scope survives: server name and credential field names.
660        let docs = snapshot.mcp_servers.get("docs").unwrap();
661        assert_eq!(docs.header_names, vec!["Authorization".to_string()]);
662        assert_eq!(docs.env_names, vec!["API_KEY".to_string()]);
663    }
664
665    #[test]
666    fn snapshot_serialization_is_deterministic_and_round_trips() {
667        let (harness_id, agent_id, session_id) = ids();
668        let mut harness = harness();
669        harness.embedder_metadata = HashMap::from([
670            ("zeta".to_string(), "z".to_string()),
671            ("alpha".to_string(), "a".to_string()),
672        ]);
673        let agent = agent(agent_id, harness_id);
674        let session = session(session_id, harness_id, Some(agent_id));
675
676        let first = ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
677        // Rebuild independent input maps in opposite insertion order, not the
678        // same HashMap twice (whose iteration order stays stable).
679        let mut reversed = harness.clone();
680        reversed.embedder_metadata =
681            HashMap::from([("alpha".into(), "a".into()), ("zeta".into(), "z".into())]);
682        let second = ResolvedExecutionSnapshot::project(&reversed, Some(&agent), &session).unwrap();
683
684        let first_json = serde_json::to_string(&first).unwrap();
685        let second_json = serde_json::to_string(&second).unwrap();
686        assert_eq!(first_json, second_json);
687
688        let round_tripped: ResolvedExecutionSnapshot = serde_json::from_str(&first_json).unwrap();
689        assert_eq!(serde_json::to_string(&round_tripped).unwrap(), first_json);
690
691        // Map metadata serializes in sorted key order regardless of input order.
692        let alpha = first_json.find("alpha").unwrap();
693        let zeta = first_json.find("zeta").unwrap();
694        assert!(alpha < zeta);
695    }
696    #[test]
697    fn unreferenced_agent_cannot_contribute_configuration() {
698        let (harness_id, agent_id, session_id) = ids();
699        let harness = harness();
700        let agent = agent(agent_id, harness_id);
701        let session = session(session_id, harness_id, None);
702        let without = ResolvedExecutionSnapshot::project(&harness, None, &session).unwrap();
703        let with_unreferenced =
704            ResolvedExecutionSnapshot::project(&harness, Some(&agent), &session).unwrap();
705        assert_eq!(
706            serde_json::to_value(with_unreferenced).unwrap(),
707            serde_json::to_value(without).unwrap()
708        );
709    }
710}