Skip to main content

codex_runtime/runtime/client/
profile.rs

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