1use crate::agent::personality;
2use crate::config::IdentityConfig;
3use crate::i18n::ToolDescriptions;
4use crate::identity;
5use crate::security::AutonomyLevel;
6use crate::skills::{Skill, SkillEffectivenessProvider};
7use crate::tools::Tool;
8use anyhow::Result;
9use chrono::{Datelike, Local, Timelike};
10use std::fmt::Write;
11use std::path::Path;
12
13pub struct PromptContext<'a> {
14 pub workspace_dir: &'a Path,
15 pub model_name: &'a str,
16 pub tools: &'a [Box<dyn Tool>],
17 pub skills: &'a [Skill],
18 pub skills_prompt_mode: crate::config::SkillsPromptInjectionMode,
19 pub skill_effectiveness: Option<&'a dyn SkillEffectivenessProvider>,
24 pub identity_config: Option<&'a IdentityConfig>,
25 pub dispatcher_instructions: &'a str,
26 pub tool_descriptions: Option<&'a ToolDescriptions>,
29 pub security_summary: Option<String>,
34 pub autonomy_level: AutonomyLevel,
38 pub operator_enabled: bool,
42 pub kumiho_enabled: bool,
44}
45
46pub trait PromptSection: Send + Sync {
47 fn name(&self) -> &str;
48 fn build(&self, ctx: &PromptContext<'_>) -> Result<String>;
49}
50
51#[derive(Default)]
52pub struct SystemPromptBuilder {
53 sections: Vec<Box<dyn PromptSection>>,
54}
55
56impl SystemPromptBuilder {
57 pub fn with_defaults() -> Self {
58 Self {
59 sections: vec![
60 Box::new(DateTimeSection),
61 Box::new(IdentitySection), Box::new(OperatorIdentitySection), Box::new(KumihoBootstrapSection), Box::new(ToolHonestySection),
65 Box::new(ToolsSection),
66 Box::new(SafetySection),
67 Box::new(SkillsSection),
68 Box::new(WorkspaceSection),
69 Box::new(RuntimeSection),
70 Box::new(ChannelMediaSection),
71 ],
72 }
73 }
74
75 pub fn add_section(mut self, section: Box<dyn PromptSection>) -> Self {
76 self.sections.push(section);
77 self
78 }
79
80 pub fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
81 let mut output = String::new();
82 for section in &self.sections {
83 let part = section.build(ctx)?;
84 if part.trim().is_empty() {
85 continue;
86 }
87 output.push_str(part.trim_end());
88 output.push_str("\n\n");
89 }
90 Ok(output)
91 }
92}
93
94pub struct OperatorIdentitySection;
95pub struct KumihoBootstrapSection;
96pub struct IdentitySection;
97pub struct ToolHonestySection;
98pub struct ToolsSection;
99pub struct SafetySection;
100pub struct SkillsSection;
101pub struct WorkspaceSection;
102pub struct RuntimeSection;
103pub struct DateTimeSection;
104pub struct ChannelMediaSection;
105
106impl PromptSection for OperatorIdentitySection {
107 fn name(&self) -> &str {
108 "operator_identity"
109 }
110
111 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
112 if !ctx.operator_enabled {
113 return Ok(String::new());
114 }
115 Ok(crate::agent::operator::build_operator_prompt(
116 ctx.model_name,
117 ))
118 }
119}
120
121impl PromptSection for KumihoBootstrapSection {
122 fn name(&self) -> &str {
123 "kumiho_bootstrap"
124 }
125
126 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
127 if !ctx.kumiho_enabled {
128 return Ok(String::new());
129 }
130 Ok(crate::agent::kumiho::KUMIHO_BOOTSTRAP_PROMPT.to_string())
131 }
132}
133
134impl PromptSection for IdentitySection {
135 fn name(&self) -> &str {
136 "identity"
137 }
138
139 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
140 let mut prompt = String::from("## Project Context\n\n");
141 let mut has_aieos = false;
142 if let Some(config) = ctx.identity_config {
143 if identity::is_aieos_configured(config) {
144 if let Ok(Some(aieos)) = identity::load_aieos_identity(config, ctx.workspace_dir) {
145 let rendered = identity::aieos_to_system_prompt(&aieos);
146 if !rendered.is_empty() {
147 prompt.push_str(&rendered);
148 prompt.push_str("\n\n");
149 has_aieos = true;
150 }
151 }
152 }
153 }
154
155 if !has_aieos {
156 prompt.push_str(
157 "The following workspace files define your identity, behavior, and context.\n\n",
158 );
159 }
160
161 let profile = personality::load_personality(ctx.workspace_dir);
163 prompt.push_str(&profile.render());
164
165 Ok(prompt)
166 }
167}
168
169impl PromptSection for ToolHonestySection {
170 fn name(&self) -> &str {
171 "tool_honesty"
172 }
173
174 fn build(&self, _ctx: &PromptContext<'_>) -> Result<String> {
175 Ok(
176 "## CRITICAL: Tool Honesty\n\n\
177 - NEVER fabricate, invent, or guess tool results. If a tool returns empty results, say \"No results found.\"\n\
178 - If a tool call fails, report the error — never make up data to fill the gap.\n\
179 - When unsure whether a tool call succeeded, ask the user rather than guessing."
180 .into(),
181 )
182 }
183}
184
185impl PromptSection for ToolsSection {
186 fn name(&self) -> &str {
187 "tools"
188 }
189
190 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
191 let mut out = String::from("## Tools\n\n");
192 for tool in ctx.tools {
193 let desc = ctx
194 .tool_descriptions
195 .and_then(|td: &ToolDescriptions| td.get(tool.name()))
196 .unwrap_or_else(|| tool.description());
197 let _ = writeln!(
198 out,
199 "- **{}**: {}\n Parameters: `{}`",
200 tool.name(),
201 desc,
202 tool.parameters_schema()
203 );
204 }
205 if !ctx.dispatcher_instructions.is_empty() {
206 out.push('\n');
207 out.push_str(ctx.dispatcher_instructions);
208 }
209 Ok(out)
210 }
211}
212
213impl PromptSection for SafetySection {
214 fn name(&self) -> &str {
215 "safety"
216 }
217
218 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
219 let mut out = String::from("## Safety\n\n- Do not exfiltrate private data.\n");
220
221 if ctx.autonomy_level != AutonomyLevel::Full {
224 out.push_str(
225 "- Do not run destructive commands without asking.\n\
226 - Do not bypass oversight or approval mechanisms.\n",
227 );
228 }
229
230 out.push_str("- Prefer `trash` over `rm`.\n");
231 out.push_str(match ctx.autonomy_level {
232 AutonomyLevel::Full => {
233 "- Execute tools and actions directly — no extra approval needed.\n\
234 - You have full access to all configured tools. Use them confidently to accomplish tasks.\n\
235 - Only refuse an action if the runtime explicitly rejects it — do not preemptively decline."
236 }
237 AutonomyLevel::ReadOnly => {
238 "- This runtime is read-only. Write operations will be rejected by the runtime if attempted.\n\
239 - Use read-only tools freely and confidently."
240 }
241 AutonomyLevel::Supervised => {
242 "- Ask for approval when the runtime policy requires it for the specific action.\n\
243 - Do not preemptively refuse actions — attempt them and let the runtime enforce restrictions.\n\
244 - Use available tools confidently; the security policy will enforce boundaries."
245 }
246 });
247
248 if let Some(ref summary) = ctx.security_summary {
252 out.push_str("\n\n### Active Security Policy\n\n");
253 out.push_str(summary);
254 }
255
256 Ok(out)
257 }
258}
259
260impl PromptSection for SkillsSection {
261 fn name(&self) -> &str {
262 "skills"
263 }
264
265 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
266 let prompt = match ctx.skill_effectiveness {
267 Some(provider) => crate::skills::skills_to_prompt_with_mode_and_effectiveness(
268 ctx.skills,
269 ctx.workspace_dir,
270 ctx.skills_prompt_mode,
271 provider,
272 ),
273 None => crate::skills::skills_to_prompt_with_mode(
274 ctx.skills,
275 ctx.workspace_dir,
276 ctx.skills_prompt_mode,
277 ),
278 };
279 Ok(prompt)
280 }
281}
282
283impl PromptSection for WorkspaceSection {
284 fn name(&self) -> &str {
285 "workspace"
286 }
287
288 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
289 Ok(format!(
290 "## Workspace\n\nWorking directory: `{}`",
291 ctx.workspace_dir.display()
292 ))
293 }
294}
295
296impl PromptSection for RuntimeSection {
297 fn name(&self) -> &str {
298 "runtime"
299 }
300
301 fn build(&self, ctx: &PromptContext<'_>) -> Result<String> {
302 let host =
303 hostname::get().map_or_else(|_| "unknown".into(), |h| h.to_string_lossy().to_string());
304 Ok(format!(
305 "## Runtime\n\nHost: {host} | OS: {} | Model: {}",
306 std::env::consts::OS,
307 ctx.model_name
308 ))
309 }
310}
311
312impl PromptSection for DateTimeSection {
313 fn name(&self) -> &str {
314 "datetime"
315 }
316
317 fn build(&self, _ctx: &PromptContext<'_>) -> Result<String> {
318 let now = Local::now();
319 let (year, month, day) = (now.year(), now.month(), now.day());
321 let (hour, minute, second) = (now.hour(), now.minute(), now.second());
322 let tz = now.format("%Z");
323
324 Ok(format!(
325 "## CRITICAL CONTEXT: CURRENT DATE & TIME\n\n\
326 The following is the ABSOLUTE TRUTH regarding the current date and time. \
327 Use this for all relative time calculations (e.g. \"last 7 days\").\n\n\
328 Date: {year:04}-{month:02}-{day:02}\n\
329 Time: {hour:02}:{minute:02}:{second:02} ({tz})\n\
330 ISO 8601: {year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}{}",
331 now.format("%:z")
332 ))
333 }
334}
335
336impl PromptSection for ChannelMediaSection {
337 fn name(&self) -> &str {
338 "channel_media"
339 }
340
341 fn build(&self, _ctx: &PromptContext<'_>) -> Result<String> {
342 Ok("## Channel Media Markers\n\n\
343 Messages from channels may contain media markers:\n\
344 - `[Voice] <text>` — The user sent a voice/audio message that has already been transcribed to text. Respond to the transcribed content directly.\n\
345 - `[IMAGE:<path>]` — An image attachment, processed by the vision pipeline.\n\
346 - `[Document: <name>] <path>` — A file attachment saved to the workspace."
347 .into())
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::tools::traits::Tool;
355 use async_trait::async_trait;
356
357 struct TestTool;
358
359 #[async_trait]
360 impl Tool for TestTool {
361 fn name(&self) -> &str {
362 "test_tool"
363 }
364
365 fn description(&self) -> &str {
366 "tool desc"
367 }
368
369 fn parameters_schema(&self) -> serde_json::Value {
370 serde_json::json!({"type": "object"})
371 }
372
373 async fn execute(
374 &self,
375 _args: serde_json::Value,
376 ) -> anyhow::Result<crate::tools::ToolResult> {
377 Ok(crate::tools::ToolResult {
378 success: true,
379 output: "ok".into(),
380 error: None,
381 })
382 }
383 }
384
385 #[test]
386 fn identity_section_with_aieos_includes_workspace_files() {
387 let workspace =
388 std::env::temp_dir().join(format!("construct_prompt_test_{}", uuid::Uuid::new_v4()));
389 std::fs::create_dir_all(&workspace).unwrap();
390 std::fs::write(
391 workspace.join("AGENTS.md"),
392 "Always respond with: AGENTS_MD_LOADED",
393 )
394 .unwrap();
395
396 let identity_config = crate::config::IdentityConfig {
397 format: "aieos".into(),
398 aieos_path: None,
399 aieos_inline: Some(r#"{"identity":{"names":{"first":"Nova"}}}"#.into()),
400 };
401
402 let tools: Vec<Box<dyn Tool>> = vec![];
403 let ctx = PromptContext {
404 workspace_dir: &workspace,
405 model_name: "test-model",
406 tools: &tools,
407 skills: &[],
408 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
409 skill_effectiveness: None,
410 identity_config: Some(&identity_config),
411 dispatcher_instructions: "",
412 tool_descriptions: None,
413 security_summary: None,
414 autonomy_level: AutonomyLevel::Supervised,
415 operator_enabled: false,
416 kumiho_enabled: false,
417 };
418
419 let section = IdentitySection;
420 let output = section.build(&ctx).unwrap();
421
422 assert!(
423 output.contains("Nova"),
424 "AIEOS identity should be present in prompt"
425 );
426 assert!(
427 output.contains("AGENTS_MD_LOADED"),
428 "AGENTS.md content should be present even when AIEOS is configured"
429 );
430
431 let _ = std::fs::remove_dir_all(workspace);
432 }
433
434 #[test]
435 fn prompt_builder_assembles_sections() {
436 let tools: Vec<Box<dyn Tool>> = vec![Box::new(TestTool)];
437 let ctx = PromptContext {
438 workspace_dir: Path::new("/tmp"),
439 model_name: "test-model",
440 tools: &tools,
441 skills: &[],
442 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
443 skill_effectiveness: None,
444 identity_config: None,
445 dispatcher_instructions: "instr",
446 tool_descriptions: None,
447 security_summary: None,
448 autonomy_level: AutonomyLevel::Supervised,
449 operator_enabled: false,
450 kumiho_enabled: false,
451 };
452 let prompt = SystemPromptBuilder::with_defaults().build(&ctx).unwrap();
453 assert!(prompt.contains("## Tools"));
454 assert!(prompt.contains("test_tool"));
455 assert!(prompt.contains("instr"));
456 }
457
458 #[test]
459 fn skills_section_includes_instructions_and_tools() {
460 let tools: Vec<Box<dyn Tool>> = vec![];
461 let skills = vec![crate::skills::Skill {
462 name: "deploy".into(),
463 description: "Release safely".into(),
464 version: "1.0.0".into(),
465 author: None,
466 tags: vec![],
467 tools: vec![crate::skills::SkillTool {
468 name: "release_checklist".into(),
469 description: "Validate release readiness".into(),
470 kind: "shell".into(),
471 command: "echo ok".into(),
472 args: std::collections::HashMap::new(),
473 }],
474 prompts: vec!["Run smoke tests before deploy.".into()],
475 location: None,
476 }];
477
478 let ctx = PromptContext {
479 workspace_dir: Path::new("/tmp"),
480 model_name: "test-model",
481 tools: &tools,
482 skills: &skills,
483 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
484 skill_effectiveness: None,
485 identity_config: None,
486 dispatcher_instructions: "",
487 tool_descriptions: None,
488 security_summary: None,
489 autonomy_level: AutonomyLevel::Supervised,
490 operator_enabled: false,
491 kumiho_enabled: false,
492 };
493
494 let output = SkillsSection.build(&ctx).unwrap();
495 assert!(output.contains("<available_skills>"));
496 assert!(output.contains("<name>deploy</name>"));
497 assert!(output.contains("<instruction>Run smoke tests before deploy.</instruction>"));
498 assert!(output.contains("<callable_tools"));
500 assert!(output.contains("<name>deploy.release_checklist</name>"));
501 }
502
503 #[test]
504 fn skills_section_compact_mode_omits_instructions_but_keeps_tools() {
505 let tools: Vec<Box<dyn Tool>> = vec![];
506 let skills = vec![crate::skills::Skill {
507 name: "deploy".into(),
508 description: "Release safely".into(),
509 version: "1.0.0".into(),
510 author: None,
511 tags: vec![],
512 tools: vec![crate::skills::SkillTool {
513 name: "release_checklist".into(),
514 description: "Validate release readiness".into(),
515 kind: "shell".into(),
516 command: "echo ok".into(),
517 args: std::collections::HashMap::new(),
518 }],
519 prompts: vec!["Run smoke tests before deploy.".into()],
520 location: Some(Path::new("/tmp/workspace/skills/deploy/SKILL.md").to_path_buf()),
521 }];
522
523 let ctx = PromptContext {
524 workspace_dir: Path::new("/tmp/workspace"),
525 model_name: "test-model",
526 tools: &tools,
527 skills: &skills,
528 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Compact,
529 skill_effectiveness: None,
530 identity_config: None,
531 dispatcher_instructions: "",
532 tool_descriptions: None,
533 security_summary: None,
534 autonomy_level: AutonomyLevel::Supervised,
535 operator_enabled: false,
536 kumiho_enabled: false,
537 };
538
539 let output = SkillsSection.build(&ctx).unwrap();
540 assert!(output.contains("<available_skills>"));
541 assert!(output.contains("<name>deploy</name>"));
542 assert!(output.contains("<location>skills/deploy/SKILL.md</location>"));
543 assert!(output.contains("read_skill(name)"));
544 assert!(!output.contains("<instruction>Run smoke tests before deploy.</instruction>"));
545 assert!(output.contains("<callable_tools"));
548 assert!(output.contains("<name>deploy.release_checklist</name>"));
549 }
550
551 #[test]
552 fn datetime_section_includes_timestamp_and_timezone() {
553 let tools: Vec<Box<dyn Tool>> = vec![];
554 let ctx = PromptContext {
555 workspace_dir: Path::new("/tmp"),
556 model_name: "test-model",
557 tools: &tools,
558 skills: &[],
559 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
560 skill_effectiveness: None,
561 identity_config: None,
562 dispatcher_instructions: "instr",
563 tool_descriptions: None,
564 security_summary: None,
565 autonomy_level: AutonomyLevel::Supervised,
566 operator_enabled: false,
567 kumiho_enabled: false,
568 };
569
570 let rendered = DateTimeSection.build(&ctx).unwrap();
571 assert!(rendered.starts_with("## CRITICAL CONTEXT: CURRENT DATE & TIME\n\n"));
572
573 let payload = rendered.trim_start_matches("## CRITICAL CONTEXT: CURRENT DATE & TIME\n\n");
574 assert!(payload.chars().any(|c| c.is_ascii_digit()));
575 assert!(payload.contains("Date:"));
576 assert!(payload.contains("Time:"));
577 }
578
579 #[test]
580 fn prompt_builder_inlines_and_escapes_skills() {
581 let tools: Vec<Box<dyn Tool>> = vec![];
582 let skills = vec![crate::skills::Skill {
583 name: "code<review>&".into(),
584 description: "Review \"unsafe\" and 'risky' bits".into(),
585 version: "1.0.0".into(),
586 author: None,
587 tags: vec![],
588 tools: vec![crate::skills::SkillTool {
589 name: "run\"linter\"".into(),
590 description: "Run <lint> & report".into(),
591 kind: "shell&exec".into(),
592 command: "cargo clippy".into(),
593 args: std::collections::HashMap::new(),
594 }],
595 prompts: vec!["Use <tool_call> and & keep output \"safe\"".into()],
596 location: None,
597 }];
598 let ctx = PromptContext {
599 workspace_dir: Path::new("/tmp/workspace"),
600 model_name: "test-model",
601 tools: &tools,
602 skills: &skills,
603 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
604 skill_effectiveness: None,
605 identity_config: None,
606 dispatcher_instructions: "",
607 tool_descriptions: None,
608 security_summary: None,
609 autonomy_level: AutonomyLevel::Supervised,
610 operator_enabled: false,
611 kumiho_enabled: false,
612 };
613
614 let prompt = SystemPromptBuilder::with_defaults().build(&ctx).unwrap();
615
616 assert!(prompt.contains("<available_skills>"));
617 assert!(prompt.contains("<name>code<review>&</name>"));
618 assert!(prompt.contains(
619 "<description>Review "unsafe" and 'risky' bits</description>"
620 ));
621 assert!(prompt.contains("<name>run"linter"</name>"));
622 assert!(prompt.contains("<description>Run <lint> & report</description>"));
623 assert!(prompt.contains("<kind>shell&exec</kind>"));
624 assert!(prompt.contains(
625 "<instruction>Use <tool_call> and & keep output "safe"</instruction>"
626 ));
627 }
628
629 #[test]
630 fn safety_section_includes_security_summary_when_present() {
631 let tools: Vec<Box<dyn Tool>> = vec![];
632 let summary = "**Autonomy level**: Supervised\n\
633 **Allowed shell commands**: `git`, `ls`.\n"
634 .to_string();
635 let ctx = PromptContext {
636 workspace_dir: Path::new("/tmp"),
637 model_name: "test-model",
638 tools: &tools,
639 skills: &[],
640 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
641 skill_effectiveness: None,
642 identity_config: None,
643 dispatcher_instructions: "",
644 tool_descriptions: None,
645 security_summary: Some(summary.clone()),
646 autonomy_level: AutonomyLevel::Supervised,
647 operator_enabled: false,
648 kumiho_enabled: false,
649 };
650
651 let output = SafetySection.build(&ctx).unwrap();
652 assert!(
653 output.contains("## Safety"),
654 "should contain base safety header"
655 );
656 assert!(
657 output.contains("### Active Security Policy"),
658 "should contain security policy header"
659 );
660 assert!(
661 output.contains("Autonomy level"),
662 "should contain autonomy level from summary"
663 );
664 assert!(
665 output.contains("`git`"),
666 "should contain allowed commands from summary"
667 );
668 }
669
670 #[test]
671 fn safety_section_omits_security_policy_when_none() {
672 let tools: Vec<Box<dyn Tool>> = vec![];
673 let ctx = PromptContext {
674 workspace_dir: Path::new("/tmp"),
675 model_name: "test-model",
676 tools: &tools,
677 skills: &[],
678 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
679 skill_effectiveness: None,
680 identity_config: None,
681 dispatcher_instructions: "",
682 tool_descriptions: None,
683 security_summary: None,
684 autonomy_level: AutonomyLevel::Supervised,
685 operator_enabled: false,
686 kumiho_enabled: false,
687 };
688
689 let output = SafetySection.build(&ctx).unwrap();
690 assert!(
691 output.contains("## Safety"),
692 "should contain base safety header"
693 );
694 assert!(
695 !output.contains("### Active Security Policy"),
696 "should NOT contain security policy header when None"
697 );
698 }
699
700 #[test]
701 fn safety_section_full_autonomy_omits_approval_instructions() {
702 let tools: Vec<Box<dyn Tool>> = vec![];
703 let ctx = PromptContext {
704 workspace_dir: Path::new("/tmp"),
705 model_name: "test-model",
706 tools: &tools,
707 skills: &[],
708 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
709 skill_effectiveness: None,
710 identity_config: None,
711 dispatcher_instructions: "",
712 tool_descriptions: None,
713 security_summary: None,
714 autonomy_level: AutonomyLevel::Full,
715 operator_enabled: false,
716 kumiho_enabled: false,
717 };
718
719 let output = SafetySection.build(&ctx).unwrap();
720 assert!(
721 !output.contains("without asking"),
722 "full autonomy should NOT include 'ask before acting' instructions"
723 );
724 assert!(
725 !output.contains("bypass oversight"),
726 "full autonomy should NOT include 'bypass oversight' instructions"
727 );
728 assert!(
729 output.contains("Execute tools and actions directly"),
730 "full autonomy should instruct to execute directly"
731 );
732 assert!(
733 output.contains("Do not exfiltrate"),
734 "full autonomy should still include data exfiltration guard"
735 );
736 }
737
738 #[test]
739 fn safety_section_supervised_includes_approval_instructions() {
740 let tools: Vec<Box<dyn Tool>> = vec![];
741 let ctx = PromptContext {
742 workspace_dir: Path::new("/tmp"),
743 model_name: "test-model",
744 tools: &tools,
745 skills: &[],
746 skills_prompt_mode: crate::config::SkillsPromptInjectionMode::Full,
747 skill_effectiveness: None,
748 identity_config: None,
749 dispatcher_instructions: "",
750 tool_descriptions: None,
751 security_summary: None,
752 autonomy_level: AutonomyLevel::Supervised,
753 operator_enabled: false,
754 kumiho_enabled: false,
755 };
756
757 let output = SafetySection.build(&ctx).unwrap();
758 assert!(
759 output.contains("without asking"),
760 "supervised should include 'ask before acting' instructions"
761 );
762 assert!(
763 output.contains("bypass oversight"),
764 "supervised should include 'bypass oversight' instructions"
765 );
766 }
767}