Skip to main content

codex_runtime/runtime/client/
profile.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use serde_json::Value;
5
6use crate::plugin::{PostHook, PreHook};
7use crate::runtime::api::{
8    ApprovalPolicy, PromptAttachment, PromptRunParams, ReasoningEffort, SandboxPolicy,
9    SandboxPreset, ThreadStartParams, DEFAULT_REASONING_EFFORT,
10};
11use crate::runtime::hooks::RuntimeHookConfig;
12
13#[derive(Clone, Debug, PartialEq)]
14struct ProfileCore {
15    model: Option<String>,
16    effort: ReasoningEffort,
17    approval_policy: ApprovalPolicy,
18    sandbox_policy: SandboxPolicy,
19    privileged_escalation_approved: bool,
20    attachments: Vec<PromptAttachment>,
21    timeout: Duration,
22    output_schema: Option<Value>,
23    hooks: RuntimeHookConfig,
24}
25
26impl Default for ProfileCore {
27    fn default() -> Self {
28        Self {
29            model: None,
30            effort: DEFAULT_REASONING_EFFORT,
31            approval_policy: ApprovalPolicy::Never,
32            sandbox_policy: SandboxPolicy::Preset(SandboxPreset::ReadOnly),
33            privileged_escalation_approved: false,
34            attachments: Vec::new(),
35            timeout: Duration::from_secs(120),
36            output_schema: None,
37            hooks: RuntimeHookConfig::default(),
38        }
39    }
40}
41
42impl ProfileCore {
43    fn into_run_profile(self) -> RunProfile {
44        RunProfile {
45            model: self.model,
46            effort: self.effort,
47            approval_policy: self.approval_policy,
48            sandbox_policy: self.sandbox_policy,
49            privileged_escalation_approved: self.privileged_escalation_approved,
50            attachments: self.attachments,
51            timeout: self.timeout,
52            output_schema: self.output_schema,
53            hooks: self.hooks,
54        }
55    }
56
57    fn into_session_config(self, cwd: String) -> SessionConfig {
58        SessionConfig {
59            cwd,
60            model: self.model,
61            effort: self.effort,
62            approval_policy: self.approval_policy,
63            sandbox_policy: self.sandbox_policy,
64            privileged_escalation_approved: self.privileged_escalation_approved,
65            attachments: self.attachments,
66            timeout: self.timeout,
67            output_schema: self.output_schema,
68            hooks: self.hooks,
69        }
70    }
71
72    fn into_prompt_params(self, cwd: String, prompt: String) -> PromptRunParams {
73        PromptRunParams {
74            cwd,
75            prompt,
76            model: self.model,
77            effort: Some(self.effort),
78            approval_policy: self.approval_policy,
79            sandbox_policy: self.sandbox_policy,
80            privileged_escalation_approved: self.privileged_escalation_approved,
81            attachments: self.attachments,
82            timeout: self.timeout,
83            output_schema: self.output_schema,
84        }
85    }
86
87    fn into_thread_start_params(self, cwd: String) -> ThreadStartParams {
88        ThreadStartParams {
89            model: self.model,
90            cwd: Some(cwd),
91            approval_policy: Some(self.approval_policy),
92            sandbox_policy: Some(self.sandbox_policy),
93            privileged_escalation_approved: self.privileged_escalation_approved,
94            ..ThreadStartParams::default()
95        }
96    }
97}
98
99impl From<RunProfile> for ProfileCore {
100    fn from(profile: RunProfile) -> Self {
101        Self {
102            model: profile.model,
103            effort: profile.effort,
104            approval_policy: profile.approval_policy,
105            sandbox_policy: profile.sandbox_policy,
106            privileged_escalation_approved: profile.privileged_escalation_approved,
107            attachments: profile.attachments,
108            timeout: profile.timeout,
109            output_schema: profile.output_schema,
110            hooks: profile.hooks,
111        }
112    }
113}
114
115impl From<&SessionConfig> for ProfileCore {
116    fn from(config: &SessionConfig) -> Self {
117        Self {
118            model: config.model.clone(),
119            effort: config.effort,
120            approval_policy: config.approval_policy,
121            sandbox_policy: config.sandbox_policy.clone(),
122            privileged_escalation_approved: config.privileged_escalation_approved,
123            attachments: config.attachments.clone(),
124            timeout: config.timeout,
125            output_schema: config.output_schema.clone(),
126            hooks: config.hooks.clone(),
127        }
128    }
129}
130
131macro_rules! impl_profile_builder_methods {
132    () => {
133        /// Set explicit model override.
134        /// Allocation: one String. Complexity: O(model length).
135        pub fn with_model(mut self, model: impl Into<String>) -> Self {
136            self.model = Some(model.into());
137            self
138        }
139
140        /// Set explicit reasoning effort.
141        /// Allocation: none. Complexity: O(1).
142        pub fn with_effort(mut self, effort: ReasoningEffort) -> Self {
143            self.effort = effort;
144            self
145        }
146
147        /// Set approval policy override.
148        /// Allocation: none. Complexity: O(1).
149        pub fn with_approval_policy(mut self, approval_policy: ApprovalPolicy) -> Self {
150            self.approval_policy = approval_policy;
151            self
152        }
153
154        /// Set sandbox policy override.
155        /// Allocation: depends on payload move/clone at callsite. Complexity: O(1).
156        pub fn with_sandbox_policy(mut self, sandbox_policy: SandboxPolicy) -> Self {
157            self.sandbox_policy = sandbox_policy;
158            self
159        }
160
161        /// Explicitly approve privileged sandbox escalation.
162        pub fn allow_privileged_escalation(mut self) -> Self {
163            self.privileged_escalation_approved = true;
164            self
165        }
166
167        /// Set timeout.
168        /// Allocation: none. Complexity: O(1).
169        pub fn with_timeout(mut self, timeout: Duration) -> Self {
170            self.timeout = timeout;
171            self
172        }
173
174        /// Set one optional JSON Schema for the final assistant message.
175        pub fn with_output_schema(mut self, output_schema: Value) -> Self {
176            self.output_schema = Some(output_schema);
177            self
178        }
179
180        /// Add one generic attachment.
181        /// Allocation: amortized O(1) push. Complexity: O(1).
182        pub fn with_attachment(mut self, attachment: PromptAttachment) -> Self {
183            self.attachments.push(attachment);
184            self
185        }
186
187        /// Add one `@path` attachment.
188        /// Allocation: one String. Complexity: O(path length).
189        pub fn attach_path(self, path: impl Into<String>) -> Self {
190            self.with_attachment(PromptAttachment::AtPath {
191                path: path.into(),
192                placeholder: None,
193            })
194        }
195
196        /// Add one `@path` attachment with placeholder.
197        /// Allocation: two Strings. Complexity: O(path + placeholder length).
198        pub fn attach_path_with_placeholder(
199            self,
200            path: impl Into<String>,
201            placeholder: impl Into<String>,
202        ) -> Self {
203            self.with_attachment(PromptAttachment::AtPath {
204                path: path.into(),
205                placeholder: Some(placeholder.into()),
206            })
207        }
208
209        /// Add one remote image attachment.
210        /// Allocation: one String. Complexity: O(url length).
211        pub fn attach_image_url(self, url: impl Into<String>) -> Self {
212            self.with_attachment(PromptAttachment::ImageUrl { url: url.into() })
213        }
214
215        /// Add one local image attachment.
216        /// Allocation: one String. Complexity: O(path length).
217        pub fn attach_local_image(self, path: impl Into<String>) -> Self {
218            self.with_attachment(PromptAttachment::LocalImage { path: path.into() })
219        }
220
221        /// Add one skill attachment.
222        /// Allocation: two Strings. Complexity: O(name + path length).
223        pub fn attach_skill(self, name: impl Into<String>, path: impl Into<String>) -> Self {
224            self.with_attachment(PromptAttachment::Skill {
225                name: name.into(),
226                path: path.into(),
227            })
228        }
229
230        /// Replace hook configuration.
231        /// Allocation: O(h), h = hook count. Complexity: O(1) move.
232        pub fn with_hooks(mut self, hooks: RuntimeHookConfig) -> Self {
233            self.hooks = hooks;
234            self
235        }
236
237        /// Register one pre hook.
238        /// Allocation: amortized O(1) push. Complexity: O(1).
239        pub fn with_pre_hook(mut self, hook: Arc<dyn PreHook>) -> Self {
240            self.hooks.pre_hooks.push(hook);
241            self
242        }
243
244        /// Register one post hook.
245        /// Allocation: amortized O(1) push. Complexity: O(1).
246        pub fn with_post_hook(mut self, hook: Arc<dyn PostHook>) -> Self {
247            self.hooks.post_hooks.push(hook);
248            self
249        }
250
251        /// Register one pre-tool-use hook (fires via the internal approval loop).
252        /// Allocation: amortized O(1) push. Complexity: O(1).
253        pub fn with_pre_tool_use_hook(mut self, hook: Arc<dyn PreHook>) -> Self {
254            self.hooks.pre_tool_use_hooks.push(hook);
255            self
256        }
257    };
258}
259
260#[derive(Clone, Debug, PartialEq)]
261pub struct RunProfile {
262    pub model: Option<String>,
263    pub effort: ReasoningEffort,
264    pub approval_policy: ApprovalPolicy,
265    pub sandbox_policy: SandboxPolicy,
266    /// Explicit opt-in gate for privileged sandbox usage (SEC-004).
267    pub privileged_escalation_approved: bool,
268    pub attachments: Vec<PromptAttachment>,
269    pub timeout: Duration,
270    pub output_schema: Option<Value>,
271    pub hooks: RuntimeHookConfig,
272}
273
274impl Default for RunProfile {
275    fn default() -> Self {
276        ProfileCore::default().into_run_profile()
277    }
278}
279
280impl RunProfile {
281    /// Create reusable run/session profile with safe defaults.
282    /// Allocation: none. Complexity: O(1).
283    pub fn new() -> Self {
284        Self::default()
285    }
286
287    impl_profile_builder_methods!();
288}
289
290#[derive(Clone, Debug, PartialEq)]
291pub struct SessionConfig {
292    pub cwd: String,
293    pub model: Option<String>,
294    pub effort: ReasoningEffort,
295    pub approval_policy: ApprovalPolicy,
296    pub sandbox_policy: SandboxPolicy,
297    /// Explicit opt-in gate for privileged sandbox usage (SEC-004).
298    pub privileged_escalation_approved: bool,
299    pub attachments: Vec<PromptAttachment>,
300    pub timeout: Duration,
301    pub output_schema: Option<Value>,
302    pub hooks: RuntimeHookConfig,
303}
304
305impl SessionConfig {
306    /// Create session config with safe defaults.
307    /// Allocation: one String for cwd. Complexity: O(cwd length).
308    pub fn new(cwd: impl Into<String>) -> Self {
309        Self::from_profile(cwd, RunProfile::default())
310    }
311
312    /// Create session config from one reusable run profile.
313    /// Allocation: one String for cwd + profile field moves. Complexity: O(cwd length).
314    pub fn from_profile(cwd: impl Into<String>, profile: RunProfile) -> Self {
315        ProfileCore::from(profile).into_session_config(cwd.into())
316    }
317
318    /// Materialize profile view of this session defaults.
319    /// Allocation: clones Strings/attachments. Complexity: O(n), n = attachment count + string sizes.
320    pub fn profile(&self) -> RunProfile {
321        ProfileCore::from(self).into_run_profile()
322    }
323
324    impl_profile_builder_methods!();
325}
326
327/// Pure transform from reusable session config + prompt into one prompt-run request.
328/// Allocation: clones config-owned Strings/vectors + one prompt String. Complexity: O(n), n = attachment count + field sizes.
329pub(super) fn session_prompt_params(
330    config: &SessionConfig,
331    prompt: impl Into<String>,
332) -> PromptRunParams {
333    ProfileCore::from(config).into_prompt_params(config.cwd.clone(), prompt.into())
334}
335
336/// Pure transform from reusable profile + turn input into one prompt-run request.
337/// Allocation: moves profile-owned Strings/vectors + one prompt String. Complexity: O(n), n = attachment count + field sizes.
338pub(super) fn profile_to_prompt_params(
339    cwd: String,
340    prompt: impl Into<String>,
341    profile: RunProfile,
342) -> PromptRunParams {
343    ProfileCore::from(profile).into_prompt_params(cwd, prompt.into())
344}
345
346/// Split one profile into prompt params and scoped hooks without cloning hooks.
347pub(super) fn profile_to_prompt_params_with_hooks(
348    cwd: String,
349    prompt: impl Into<String>,
350    mut profile: RunProfile,
351) -> (PromptRunParams, RuntimeHookConfig) {
352    let hooks = std::mem::take(&mut profile.hooks);
353    let params = profile_to_prompt_params(cwd, prompt, profile);
354    (params, hooks)
355}
356
357/// Pure transform from session defaults into thread-start/resume overrides.
358/// Allocation: clones Strings/policy payloads from config. Complexity: O(n), n = field sizes.
359pub(super) fn session_thread_start_params(config: &SessionConfig) -> ThreadStartParams {
360    ProfileCore::from(config).into_thread_start_params(config.cwd.clone())
361}