返回 CodeWhale
policy.rs
根目录 / crates / tui / src / sandbox / policy.rs
1 #![allow(dead_code)]
2
3 //! Sandbox policy definitions for command execution restrictions.
4 //!
5 //! This module defines the policies that control what resources a sandboxed
6 //! process can access. Policies range from full unrestricted access to
7 //! tightly controlled workspace-only write access.
8
9 use serde::{Deserialize, Serialize};
10 use std::fs;
11 use std::io;
12 use std::path::{Path, PathBuf};
13
14 use super::{CommandSpec, ExecEnv};
15 use crate::command_safety::SafetyLevel;
16
17 /// Determines execution restrictions for shell commands.
18 ///
19 /// The sandbox policy controls filesystem access, network access, and other
20 /// system resources for executed commands. Choose the most restrictive policy
21 /// that still allows your command to function.
22 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23 #[serde(tag = "type", rename_all = "kebab-case")]
24 pub enum SandboxPolicy {
25 /// No restrictions whatsoever. Use with extreme caution.
26 ///
27 /// This policy disables all sandboxing and allows full system access.
28 /// Only use this when absolutely necessary and the command source is trusted.
29 #[serde(rename = "danger-full-access")]
30 DangerFullAccess,
31
32 /// Read-only access to the entire filesystem.
33 ///
34 /// The process can read any file but cannot write anywhere.
35 /// Useful for analysis tools that need broad read access.
36 #[serde(rename = "read-only")]
37 ReadOnly,
38
39 /// Indicates the process is already running in an external sandbox.
40 ///
41 /// Use this when CodeWhale is itself running inside a container,
42 /// VM, or other sandboxed environment. This avoids double-sandboxing
43 /// which can cause issues.
44 #[serde(rename = "external-sandbox")]
45 ExternalSandbox {
46 /// Whether network access is allowed in the external sandbox.
47 #[serde(default)]
48 network_access: bool,
49 },
50
51 /// Read-only filesystem access plus write access to specified directories.
52 ///
53 /// This is the default and recommended policy. It allows:
54 /// - Read access to the entire filesystem (for tools, libraries, etc.)
55 /// - Write access only to the current working directory and specified roots
56 /// - Optional network access
57 #[serde(rename = "workspace-write")]
58 WorkspaceWrite {
59 /// Additional directories where writes are allowed.
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
61 writable_roots: Vec<PathBuf>,
62
63 /// Whether outbound network connections are permitted.
64 #[serde(default)]
65 network_access: bool,
66
67 /// Exclude TMPDIR from writable paths.
68 #[serde(default)]
69 exclude_tmpdir: bool,
70
71 /// Exclude /tmp from writable paths.
72 #[serde(default)]
73 exclude_slash_tmp: bool,
74 },
75 }
76
77 impl Default for SandboxPolicy {
78 /// Returns the default policy: workspace-write with no extra roots and no network.
79 fn default() -> Self {
80 SandboxPolicy::WorkspaceWrite {
81 writable_roots: vec![],
82 network_access: false,
83 exclude_tmpdir: false,
84 exclude_slash_tmp: false,
85 }
86 }
87 }
88
89 impl SandboxPolicy {
90 /// Create a workspace-write policy with network access enabled.
91 pub fn workspace_with_network() -> Self {
92 SandboxPolicy::WorkspaceWrite {
93 writable_roots: vec![],
94 network_access: true,
95 exclude_tmpdir: false,
96 exclude_slash_tmp: false,
97 }
98 }
99
100 /// Create a workspace-write policy with additional writable directories.
101 pub fn workspace_with_roots(roots: Vec<PathBuf>, network: bool) -> Self {
102 SandboxPolicy::WorkspaceWrite {
103 writable_roots: roots,
104 network_access: network,
105 exclude_tmpdir: false,
106 exclude_slash_tmp: false,
107 }
108 }
109
110 /// Returns true if the policy allows reading any file on the filesystem.
111 pub fn has_full_disk_read_access() -> bool {
112 // All current policies allow full disk read access
113 true
114 }
115
116 /// Returns true if the policy allows writing to any file on the filesystem.
117 pub fn has_full_disk_write_access(&self) -> bool {
118 matches!(
119 self,
120 SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
121 )
122 }
123
124 /// Returns true if the policy allows outbound network connections.
125 pub fn has_network_access(&self) -> bool {
126 match self {
127 SandboxPolicy::DangerFullAccess => true,
128 SandboxPolicy::ReadOnly => false,
129 SandboxPolicy::ExternalSandbox { network_access }
130 | SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
131 }
132 }
133
134 /// Returns true if the sandbox should be applied (not bypassed).
135 pub fn should_sandbox(&self) -> bool {
136 !matches!(
137 self,
138 SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
139 )
140 }
141
142 /// Compact, deterministic posture label for model- and user-facing
143 /// surfaces (`<turn_meta>`, sandbox-denial hints). Byte-stable for a
144 /// given policy so per-turn metadata stays cache-friendly.
145 #[must_use]
146 pub fn posture_label(&self) -> String {
147 match self {
148 SandboxPolicy::DangerFullAccess => "full access (sandbox disabled)".to_string(),
149 SandboxPolicy::ReadOnly => {
150 "read-only (shell writes are blocked; tool approval cannot lift this)".to_string()
151 }
152 SandboxPolicy::ExternalSandbox { network_access } => format!(
153 "external sandbox (host-managed; network {})",
154 if *network_access {
155 "allowed"
156 } else {
157 "blocked"
158 }
159 ),
160 SandboxPolicy::WorkspaceWrite {
161 writable_roots,
162 network_access,
163 ..
164 } => format!(
165 "workspace-write (writes inside the workspace{}; network {})",
166 if writable_roots.len() > 1 {
167 " and approved roots"
168 } else {
169 ""
170 },
171 if *network_access {
172 "allowed"
173 } else {
174 "blocked"
175 }
176 ),
177 }
178 }
179
180 /// Get the list of writable roots for this policy.
181 ///
182 /// This includes:
183 /// - The current working directory
184 /// - Any explicitly specified `writable_roots`
185 /// - /tmp (unless excluded)
186 /// - TMPDIR (unless excluded)
187 ///
188 /// For policies with full write access, returns an empty vec since
189 /// there's no need to enumerate specific paths.
190 pub fn get_writable_roots(&self, cwd: &Path) -> Vec<WritableRoot> {
191 match self {
192 // Full write access or read-only - no enumeration needed
193 SandboxPolicy::DangerFullAccess
194 | SandboxPolicy::ExternalSandbox { .. }
195 | SandboxPolicy::ReadOnly => vec![],
196
197 // Workspace write - enumerate all writable paths
198 SandboxPolicy::WorkspaceWrite {
199 writable_roots,
200 exclude_tmpdir,
201 exclude_slash_tmp,
202 ..
203 } => {
204 let mut roots: Vec<PathBuf> = writable_roots.clone();
205
206 // Add the current working directory
207 if let Ok(canonical_cwd) = cwd.canonicalize() {
208 roots.push(canonical_cwd);
209 } else {
210 roots.push(cwd.to_path_buf());
211 }
212
213 // Git worktrees keep mutable metadata outside the worktree
214 // directory. Allow only the gitdir and commondir derived from
215 // a workspace `.git` pointer, preserving the workspace boundary
216 // for all other external paths.
217 for root in roots.clone() {
218 roots.extend(resolve_git_worktree_writable_roots(&root));
219 }
220
221 // Add /tmp unless excluded
222 if !exclude_slash_tmp && let Ok(tmp) = Path::new("/tmp").canonicalize() {
223 roots.push(tmp);
224 }
225
226 // Add TMPDIR unless excluded
227 if !exclude_tmpdir
228 && let Ok(tmpdir) = std::env::var("TMPDIR")
229 && let Ok(canonical) = Path::new(&tmpdir).canonicalize()
230 {
231 roots.push(canonical);
232 }
233
234 // Convert to WritableRoot with read-only subpaths
235 roots
236 .into_iter()
237 .map(|root| {
238 let mut read_only_subpaths = Vec::new();
239
240 // Protect .codewhale/ and .deepseek/ directories from modification
241 let codewhale_dir = root.join(".codewhale");
242 if codewhale_dir.is_dir() {
243 read_only_subpaths.push(codewhale_dir);
244 }
245 let deepseek_dir = root.join(".deepseek");
246 if deepseek_dir.is_dir() {
247 read_only_subpaths.push(deepseek_dir);
248 }
249
250 WritableRoot {
251 root,
252 read_only_subpaths,
253 }
254 })
255 .collect()
256 }
257 }
258 }
259 }
260
261 fn resolve_git_worktree_writable_roots(root: &Path) -> Vec<PathBuf> {
262 let Some(pointer) = resolve_gitdir_pointer(root) else {
263 return Vec::new();
264 };
265 let git_dir = pointer.git_dir;
266 let Some(common_dir) = resolve_git_common_dir(&git_dir) else {
267 return Vec::new();
268 };
269 if !git_dir.starts_with(common_dir.join("worktrees")) {
270 return Vec::new();
271 }
272 if !worktree_metadata_points_back_to_workspace(&git_dir, &pointer.git_file) {
273 return Vec::new();
274 }
275
276 vec![git_dir, common_dir]
277 }
278
279 #[derive(Debug)]
280 struct GitDirPointer {
281 git_dir: PathBuf,
282 git_file: PathBuf,
283 }
284
285 fn resolve_gitdir_pointer(root: &Path) -> Option<GitDirPointer> {
286 let search_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
287 for ancestor in search_root.ancestors() {
288 let git_file = ancestor.join(".git");
289 if !git_file.is_file() {
290 continue;
291 }
292
293 let contents = fs::read_to_string(&git_file).ok()?;
294 let value = contents
295 .lines()
296 .find_map(|line| line.strip_prefix("gitdir:"))?
297 .trim();
298 if value.is_empty() {
299 return None;
300 }
301
302 let path = PathBuf::from(value);
303 let resolved = if path.is_absolute() {
304 path
305 } else {
306 ancestor.join(path)
307 };
308
309 return Some(GitDirPointer {
310 git_dir: resolved.canonicalize().ok()?,
311 git_file: git_file.canonicalize().ok()?,
312 });
313 }
314
315 None
316 }
317
318 fn resolve_git_common_dir(git_dir: &Path) -> Option<PathBuf> {
319 let contents = fs::read_to_string(git_dir.join("commondir")).ok()?;
320 let value = contents.lines().next()?.trim();
321 if value.is_empty() {
322 return None;
323 }
324
325 let path = PathBuf::from(value);
326 let resolved = if path.is_absolute() {
327 path
328 } else {
329 git_dir.join(path)
330 };
331
332 resolved.canonicalize().ok()
333 }
334
335 fn worktree_metadata_points_back_to_workspace(git_dir: &Path, expected_git_file: &Path) -> bool {
336 let Some(actual_git_file) = resolve_gitdir_back_pointer(git_dir) else {
337 return false;
338 };
339 actual_git_file == expected_git_file
340 }
341
342 fn resolve_gitdir_back_pointer(git_dir: &Path) -> Option<PathBuf> {
343 let contents = fs::read_to_string(git_dir.join("gitdir")).ok()?;
344 let value = contents.lines().next()?.trim();
345 if value.is_empty() {
346 return None;
347 }
348
349 let path = PathBuf::from(value);
350 let resolved = if path.is_absolute() {
351 path
352 } else {
353 git_dir.join(path)
354 };
355
356 resolved.canonicalize().ok()
357 }
358
359 /// A directory tree where writes are allowed, with optional read-only subpaths.
360 ///
361 /// This allows fine-grained control like "allow writes to /project but not /project/.deepseek".
362 #[derive(Debug, Clone, PartialEq, Eq)]
363 pub struct WritableRoot {
364 /// The root directory where writes are allowed.
365 pub root: PathBuf,
366
367 /// Subdirectories within root that should remain read-only.
368 pub read_only_subpaths: Vec<PathBuf>,
369 }
370
371 impl WritableRoot {
372 /// Create a new writable root with no read-only exceptions.
373 pub fn new(root: PathBuf) -> Self {
374 Self {
375 root,
376 read_only_subpaths: vec![],
377 }
378 }
379
380 /// Create a writable root with specific read-only subpaths.
381 pub fn with_exceptions(root: PathBuf, read_only: Vec<PathBuf>) -> Self {
382 Self {
383 root,
384 read_only_subpaths: read_only,
385 }
386 }
387
388 /// Check if a path is writable under this root.
389 ///
390 /// Returns true if the path is under the root and not under any read-only subpath.
391 pub fn is_path_writable(&self, path: &Path) -> bool {
392 // Must be under the root
393 if !path.starts_with(&self.root) {
394 return false;
395 }
396
397 // Must not be under any read-only subpath
398 for subpath in &self.read_only_subpaths {
399 if path.starts_with(subpath) {
400 return false;
401 }
402 }
403
404 true
405 }
406 }
407
408 /// Unified trait for platform-specific sandbox executors (#2186).
409 ///
410 /// Platform implementations can use this trait to convert a policy into
411 /// wrapper-specific rules. The current `SandboxManager` command path does not
412 /// dispatch through this trait yet.
413 pub trait SandboxExecutor {
414 /// Prepare a sandboxed execution environment from a command spec.
415 ///
416 /// Returns the transformed command, environment, and sandbox metadata
417 /// needed to spawn the process.
418 fn prepare(&self, spec: &CommandSpec) -> io::Result<ExecEnv>;
419
420 /// Check if a command failure was caused by sandbox denial.
421 fn was_denied(&self, exit_code: i32, stderr: &str) -> bool;
422
423 /// Get a human-readable description of why the sandbox blocked the command.
424 fn denial_message(&self, stderr: &str) -> String;
425
426 /// Returns the type of sandbox this executor provides.
427 fn sandbox_type(&self) -> super::SandboxType;
428 }
429
430 /// Map a command safety classification to the appropriate sandbox policy (#2186).
431 ///
432 /// - `Safe` / `WorkspaceSafe` → use the default sandbox policy
433 /// - `RequiresApproval` → user must approve before execution (handled by caller)
434 /// - `Dangerous` → blocked unless in YOLO mode with trust
435 pub fn map_safety_level_to_behavior(
436 level: SafetyLevel,
437 default_policy: &SandboxPolicy,
438 ) -> SandboxPolicyBehavior {
439 match level {
440 SafetyLevel::Safe | SafetyLevel::WorkspaceSafe => {
441 SandboxPolicyBehavior::Sandboxed(default_policy.clone())
442 }
443 SafetyLevel::RequiresApproval => SandboxPolicyBehavior::RequiresApproval,
444 SafetyLevel::Dangerous => SandboxPolicyBehavior::Blocked,
445 }
446 }
447
448 /// Behavior decision for a sandboxed command based on safety level.
449 #[derive(Debug, Clone)]
450 pub enum SandboxPolicyBehavior {
451 /// Execute with the given sandbox policy.
452 Sandboxed(SandboxPolicy),
453 /// User approval required before execution.
454 RequiresApproval,
455 /// Block execution entirely (unless YOLO+trust).
456 Blocked,
457 }
458
459 #[cfg(test)]
460 mod tests {
461 use super::*;
462
463 #[test]
464 fn test_default_policy() {
465 let policy = SandboxPolicy::default();
466 assert!(matches!(policy, SandboxPolicy::WorkspaceWrite { .. }));
467 assert!(!policy.has_network_access());
468 assert!(policy.should_sandbox());
469 }
470
471 #[test]
472 fn test_full_access_policy() {
473 let policy = SandboxPolicy::DangerFullAccess;
474 assert!(policy.has_full_disk_write_access());
475 assert!(policy.has_network_access());
476 assert!(!policy.should_sandbox());
477 }
478
479 #[test]
480 fn posture_labels_name_the_binding_fact() {
481 // DGF-02: the read-only label must state that approval cannot lift
482 // the sandbox — that sentence is what stops the model (and the
483 // user) from treating a denial as a bug to debug.
484 let read_only = SandboxPolicy::ReadOnly.posture_label();
485 assert!(read_only.contains("read-only"), "{read_only}");
486 assert!(read_only.contains("approval cannot lift"), "{read_only}");
487
488 assert!(
489 SandboxPolicy::default()
490 .posture_label()
491 .starts_with("workspace-write"),
492 );
493 assert!(
494 SandboxPolicy::DangerFullAccess
495 .posture_label()
496 .contains("full access"),
497 );
498 assert!(
499 SandboxPolicy::ExternalSandbox {
500 network_access: false
501 }
502 .posture_label()
503 .contains("network blocked"),
504 );
505 }
506
507 #[test]
508 fn test_read_only_policy() {
509 let policy = SandboxPolicy::ReadOnly;
510 assert!(!policy.has_full_disk_write_access());
511 assert!(!policy.has_network_access());
512 assert!(policy.should_sandbox());
513 }
514
515 #[test]
516 fn test_workspace_with_network() {
517 let policy = SandboxPolicy::workspace_with_network();
518 assert!(policy.has_network_access());
519 assert!(policy.should_sandbox());
520 }
521
522 #[test]
523 fn workspace_write_includes_git_worktree_metadata_roots() {
524 let tmp = tempfile::tempdir().expect("tempdir");
525 let common_git_dir = tmp.path().join("main-repo").join(".git");
526 let worktree_git_dir = common_git_dir.join("worktrees").join("feature");
527 let worktree = tmp.path().join("feature-worktree");
528 std::fs::create_dir_all(&worktree_git_dir).expect("mkdir gitdir");
529 std::fs::create_dir_all(&worktree).expect("mkdir worktree");
530 std::fs::write(
531 worktree.join(".git"),
532 format!("gitdir: {}\n", worktree_git_dir.display()),
533 )
534 .expect("write git pointer");
535 std::fs::write(worktree_git_dir.join("commondir"), "../..").expect("write commondir");
536 std::fs::write(
537 worktree_git_dir.join("gitdir"),
538 worktree.join(".git").display().to_string(),
539 )
540 .expect("write gitdir back pointer");
541
542 let policy = SandboxPolicy::WorkspaceWrite {
543 writable_roots: vec![worktree.clone()],
544 network_access: true,
545 exclude_tmpdir: true,
546 exclude_slash_tmp: true,
547 };
548
549 let root_paths: Vec<PathBuf> = policy
550 .get_writable_roots(&worktree)
551 .into_iter()
552 .map(|root| root.root)
553 .collect();
554
555 assert!(root_paths.contains(&worktree.canonicalize().expect("canonical worktree")));
556 assert!(root_paths.contains(&worktree_git_dir.canonicalize().expect("canonical gitdir")));
557 assert!(root_paths.contains(&common_git_dir.canonicalize().expect("canonical common git")));
558 }
559
560 #[test]
561 fn workspace_write_resolves_git_worktree_metadata_from_subdirectory() {
562 let tmp = tempfile::tempdir().expect("tempdir");
563 let common_git_dir = tmp.path().join("main-repo").join(".git");
564 let worktree_git_dir = common_git_dir.join("worktrees").join("feature");
565 let worktree = tmp.path().join("feature-worktree");
566 let nested = worktree.join("crates").join("cli");
567 std::fs::create_dir_all(&worktree_git_dir).expect("mkdir gitdir");
568 std::fs::create_dir_all(&nested).expect("mkdir nested worktree path");
569 std::fs::write(
570 worktree.join(".git"),
571 format!("gitdir: {}\n", worktree_git_dir.display()),
572 )
573 .expect("write git pointer");
574 std::fs::write(worktree_git_dir.join("commondir"), "../..").expect("write commondir");
575 std::fs::write(
576 worktree_git_dir.join("gitdir"),
577 worktree.join(".git").display().to_string(),
578 )
579 .expect("write gitdir back pointer");
580
581 let policy = SandboxPolicy::WorkspaceWrite {
582 writable_roots: vec![],
583 network_access: true,
584 exclude_tmpdir: true,
585 exclude_slash_tmp: true,
586 };
587
588 let root_paths: Vec<PathBuf> = policy
589 .get_writable_roots(&nested)
590 .into_iter()
591 .map(|root| root.root)
592 .collect();
593
594 assert!(root_paths.contains(&nested.canonicalize().expect("canonical nested cwd")));
595 assert!(root_paths.contains(&worktree_git_dir.canonicalize().expect("canonical gitdir")));
596 assert!(root_paths.contains(&common_git_dir.canonicalize().expect("canonical common git")));
597 }
598
599 #[test]
600 fn workspace_write_rejects_non_reciprocal_git_worktree_metadata() {
601 let tmp = tempfile::tempdir().expect("tempdir");
602 let common_git_dir = tmp.path().join("main-repo").join(".git");
603 let worktree_git_dir = common_git_dir.join("worktrees").join("feature");
604 let worktree = tmp.path().join("feature-worktree");
605 let other_worktree = tmp.path().join("other-worktree");
606 std::fs::create_dir_all(&worktree_git_dir).expect("mkdir gitdir");
607 std::fs::create_dir_all(&worktree).expect("mkdir worktree");
608 std::fs::create_dir_all(&other_worktree).expect("mkdir other worktree");
609 std::fs::write(
610 worktree.join(".git"),
611 format!("gitdir: {}\n", worktree_git_dir.display()),
612 )
613 .expect("write git pointer");
614 std::fs::write(worktree_git_dir.join("commondir"), "../..").expect("write commondir");
615 std::fs::write(
616 worktree_git_dir.join("gitdir"),
617 other_worktree.join(".git").display().to_string(),
618 )
619 .expect("write mismatched gitdir back pointer");
620 std::fs::write(
621 other_worktree.join(".git"),
622 "gitdir: /tmp/not-this-worktree\n",
623 )
624 .expect("write other git pointer");
625
626 let policy = SandboxPolicy::WorkspaceWrite {
627 writable_roots: vec![worktree.clone()],
628 network_access: true,
629 exclude_tmpdir: true,
630 exclude_slash_tmp: true,
631 };
632
633 let root_paths: Vec<PathBuf> = policy
634 .get_writable_roots(&worktree)
635 .into_iter()
636 .map(|root| root.root)
637 .collect();
638
639 assert!(root_paths.contains(&worktree.canonicalize().expect("canonical worktree")));
640 assert!(!root_paths.contains(&worktree_git_dir.canonicalize().expect("canonical gitdir")));
641 assert!(
642 !root_paths.contains(&common_git_dir.canonicalize().expect("canonical common git"))
643 );
644 }
645
646 #[test]
647 fn test_writable_root_basic() {
648 let root = WritableRoot::new(PathBuf::from("/project"));
649 assert!(root.is_path_writable(Path::new("/project/src/main.rs")));
650 assert!(!root.is_path_writable(Path::new("/other/file.txt")));
651 }
652
653 #[test]
654 fn test_writable_root_with_exceptions() {
655 let root = WritableRoot::with_exceptions(
656 PathBuf::from("/project"),
657 vec![PathBuf::from("/project/.deepseek")],
658 );
659 assert!(root.is_path_writable(Path::new("/project/src/main.rs")));
660 assert!(!root.is_path_writable(Path::new("/project/.deepseek/config")));
661 }
662
663 #[test]
664 fn test_safety_level_mapping() {
665 let default = SandboxPolicy::default();
666
667 // Safe commands get sandboxed
668 assert!(matches!(
669 map_safety_level_to_behavior(SafetyLevel::Safe, &default),
670 SandboxPolicyBehavior::Sandboxed(_)
671 ));
672 assert!(matches!(
673 map_safety_level_to_behavior(SafetyLevel::WorkspaceSafe, &default),
674 SandboxPolicyBehavior::Sandboxed(_)
675 ));
676
677 // RequiresApproval gets RequiresApproval
678 assert!(matches!(
679 map_safety_level_to_behavior(SafetyLevel::RequiresApproval, &default),
680 SandboxPolicyBehavior::RequiresApproval
681 ));
682
683 // Dangerous gets Blocked
684 assert!(matches!(
685 map_safety_level_to_behavior(SafetyLevel::Dangerous, &default),
686 SandboxPolicyBehavior::Blocked
687 ));
688 }
689
690 #[test]
691 fn test_policy_serialization() {
692 let policy = SandboxPolicy::WorkspaceWrite {
693 writable_roots: vec![PathBuf::from("/extra")],
694 network_access: true,
695 exclude_tmpdir: false,
696 exclude_slash_tmp: false,
697 };
698
699 let json = serde_json::to_string(&policy).unwrap();
700 assert!(json.contains("workspace-write"));
701
702 let parsed: SandboxPolicy = serde_json::from_str(&json).unwrap();
703 assert_eq!(policy, parsed);
704 }
705 }
706
706 lines RUST