oasr 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
policy/profile.py CHANGED
@@ -11,7 +11,7 @@ from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
12
  from typing import Any
13
13
 
14
- from policy.defaults import SAFE
14
+ from profiles import BUILTIN_PROFILES, merge_profile_data
15
15
 
16
16
 
17
17
  @dataclass
@@ -107,17 +107,15 @@ def load(config: dict[str, Any], profile_name: str, cwd: Path | None = None) ->
107
107
  def _load_impl(config: dict[str, Any], profile_name: str) -> Profile:
108
108
  """Internal implementation of load()."""
109
109
  # Start with safe defaults
110
- profile_data = SAFE.copy()
110
+ profile_data = BUILTIN_PROFILES["safe"].copy()
111
111
 
112
112
  # Try to load user-defined profile
113
113
  try:
114
114
  profiles = config.get("profiles", {})
115
115
  if profile_name in profiles:
116
- user_profile = profiles[profile_name]
117
- # Merge user overrides
118
- for key in SAFE.keys():
119
- if key in user_profile:
120
- profile_data[key] = user_profile[key]
116
+ profile_data = merge_profile_data(profile_data, profiles[profile_name])
117
+ elif profile_name in BUILTIN_PROFILES:
118
+ profile_data = merge_profile_data(profile_data, BUILTIN_PROFILES[profile_name])
121
119
  elif profile_name != "safe":
122
120
  # Warn if non-safe profile doesn't exist, but continue with safe defaults
123
121
  print(
profiles/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """Profile loading utilities for OASR."""
2
+
3
+ from profiles.builtins import BUILTIN_PROFILE_ORDER, BUILTIN_PROFILES
4
+ from profiles.loader import load_profiles, merge_profile_data
5
+ from profiles.paths import ensure_profile_dir, get_profile_dir
6
+ from profiles.registry import get_profiles, list_profiles
7
+ from profiles.summary import format_profile_summary, sorted_profile_names
8
+ from profiles.validation import validate_profile_data, validate_profiles
9
+
10
+ __all__ = [
11
+ "BUILTIN_PROFILES",
12
+ "BUILTIN_PROFILE_ORDER",
13
+ "load_profiles",
14
+ "get_profile_dir",
15
+ "ensure_profile_dir",
16
+ "format_profile_summary",
17
+ "get_profiles",
18
+ "list_profiles",
19
+ "merge_profile_data",
20
+ "sorted_profile_names",
21
+ "validate_profile_data",
22
+ "validate_profiles",
23
+ ]
profiles/builtins.py ADDED
@@ -0,0 +1,63 @@
1
+ """Built-in execution policy profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ SAFE = {
8
+ "fs_read_roots": ["./"],
9
+ "fs_write_roots": ["./out", "./.oasr"],
10
+ "deny_paths": [
11
+ "~/.ssh",
12
+ "~/.aws",
13
+ "~/.gnupg",
14
+ "~/.config",
15
+ ".env",
16
+ "~/.bashrc",
17
+ "~/.zshrc",
18
+ "~/.profile",
19
+ ],
20
+ "allowed_commands": ["rg", "fd", "jq", "cat"],
21
+ "deny_shell": True,
22
+ "network": False,
23
+ "allow_env": False,
24
+ }
25
+
26
+ STRICT = {
27
+ "fs_read_roots": ["./"],
28
+ "fs_write_roots": ["./.oasr"],
29
+ "deny_paths": SAFE["deny_paths"],
30
+ "allowed_commands": ["rg", "cat"],
31
+ "deny_shell": True,
32
+ "network": False,
33
+ "allow_env": False,
34
+ }
35
+
36
+ DEV = {
37
+ "fs_read_roots": ["./", "~/projects"],
38
+ "fs_write_roots": ["./", "./out", "./.oasr", "~/projects"],
39
+ "deny_paths": SAFE["deny_paths"],
40
+ "allowed_commands": ["bash", "python", "node", "git", "curl", "rg", "fd", "jq", "cat", "npm", "pip"],
41
+ "deny_shell": False,
42
+ "network": True,
43
+ "allow_env": True,
44
+ }
45
+
46
+ UNSAFE = {
47
+ "fs_read_roots": ["/"],
48
+ "fs_write_roots": ["/"],
49
+ "deny_paths": [],
50
+ "allowed_commands": ["*"],
51
+ "deny_shell": False,
52
+ "network": True,
53
+ "allow_env": True,
54
+ }
55
+
56
+ BUILTIN_PROFILES: dict[str, dict[str, Any]] = {
57
+ "safe": SAFE,
58
+ "strict": STRICT,
59
+ "dev": DEV,
60
+ "unsafe": UNSAFE,
61
+ }
62
+
63
+ BUILTIN_PROFILE_ORDER = ("safe", "strict", "dev", "unsafe")
profiles/loader.py ADDED
@@ -0,0 +1,74 @@
1
+ """Profile loading and merging utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ if sys.version_info >= (3, 11):
10
+ import tomllib
11
+ else:
12
+ import tomli as tomllib
13
+
14
+ from profiles.builtins import BUILTIN_PROFILES
15
+ from profiles.paths import get_profile_dir
16
+
17
+
18
+ def list_profile_files(profile_dir: Path | None = None) -> list[Path]:
19
+ """Return profile files from ~/.oasr/profile."""
20
+ directory = profile_dir or get_profile_dir()
21
+ if not directory.exists():
22
+ return []
23
+ return sorted(p for p in directory.glob("*.toml") if p.is_file())
24
+
25
+
26
+ def load_profile_file(path: Path) -> dict[str, Any]:
27
+ """Load a profile TOML file containing only profile keys."""
28
+ with open(path, "rb") as f:
29
+ data = tomllib.load(f)
30
+ if not isinstance(data, dict):
31
+ raise ValueError("Profile file must contain a table of profile settings")
32
+ return data
33
+
34
+
35
+ def load_profile_files(profile_dir: Path | None = None) -> dict[str, dict[str, Any]]:
36
+ """Load profile files into a name->profile map."""
37
+ profiles: dict[str, dict[str, Any]] = {}
38
+ for path in list_profile_files(profile_dir):
39
+ name = path.stem
40
+ try:
41
+ profiles[name] = load_profile_file(path)
42
+ except Exception as exc:
43
+ print(f"⚠ Warning: Failed to load profile '{name}': {exc}", file=sys.stderr)
44
+ return profiles
45
+
46
+
47
+ def merge_profiles(
48
+ builtin: dict[str, dict[str, Any]],
49
+ file_profiles: dict[str, dict[str, Any]],
50
+ inline_profiles: dict[str, dict[str, Any]],
51
+ ) -> dict[str, dict[str, Any]]:
52
+ """Merge profiles with precedence: inline > file > builtin."""
53
+ merged: dict[str, dict[str, Any]] = {}
54
+ for source in (builtin, file_profiles, inline_profiles):
55
+ for name, profile in source.items():
56
+ merged[name] = profile.copy()
57
+ return merged
58
+
59
+
60
+ def merge_profile_data(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
61
+ """Merge profile values with override precedence."""
62
+ merged = base.copy()
63
+ merged.update(overrides)
64
+ return merged
65
+
66
+
67
+ def load_profiles(
68
+ inline_profiles: dict[str, dict[str, Any]] | None = None,
69
+ profile_dir: Path | None = None,
70
+ ) -> dict[str, dict[str, Any]]:
71
+ """Load merged profiles from builtin + profile files + inline config."""
72
+ inline_profiles = inline_profiles or {}
73
+ file_profiles = load_profile_files(profile_dir)
74
+ return merge_profiles(BUILTIN_PROFILES, file_profiles, inline_profiles)
profiles/paths.py ADDED
@@ -0,0 +1,22 @@
1
+ """Profile directory paths and helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def get_profile_dir() -> Path:
9
+ """Return ~/.oasr/profile directory (dynamic)."""
10
+ try:
11
+ from config import OASR_DIR
12
+
13
+ return OASR_DIR / "profile"
14
+ except Exception:
15
+ return Path.home() / ".oasr" / "profile"
16
+
17
+
18
+ def ensure_profile_dir() -> Path:
19
+ """Ensure ~/.oasr/profile directory exists."""
20
+ directory = get_profile_dir()
21
+ directory.mkdir(parents=True, exist_ok=True)
22
+ return directory
profiles/registry.py ADDED
@@ -0,0 +1,19 @@
1
+ """Profile registry operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from profiles.loader import load_profiles
8
+ from profiles.summary import sorted_profile_names
9
+
10
+
11
+ def list_profiles(inline_profiles: dict[str, dict[str, Any]] | None = None) -> list[str]:
12
+ """Return ordered profile names with all sources merged."""
13
+ profiles = load_profiles(inline_profiles=inline_profiles or {})
14
+ return sorted_profile_names(profiles)
15
+
16
+
17
+ def get_profiles(inline_profiles: dict[str, dict[str, Any]] | None = None) -> dict[str, dict[str, Any]]:
18
+ """Return merged profiles."""
19
+ return load_profiles(inline_profiles=inline_profiles or {})
profiles/summary.py ADDED
@@ -0,0 +1,23 @@
1
+ """Profile listing and summary helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from profiles.builtins import BUILTIN_PROFILE_ORDER
8
+
9
+
10
+ def sorted_profile_names(profiles: dict[str, Any]) -> list[str]:
11
+ """Return profile names sorted with built-ins first."""
12
+ names = sorted(profiles.keys())
13
+ ordered = [name for name in BUILTIN_PROFILE_ORDER if name in profiles]
14
+ remaining = [name for name in names if name not in ordered]
15
+ return ordered + remaining
16
+
17
+
18
+ def format_profile_summary(name: str, profile: dict[str, Any]) -> str:
19
+ """Format a single-line profile summary."""
20
+ network = "on" if profile.get("network") else "off"
21
+ env = "on" if profile.get("allow_env") else "off"
22
+ shell = "on" if not profile.get("deny_shell", True) else "off"
23
+ return f"{name:12} network={network} env={env} shell={shell}"
profiles/validation.py ADDED
@@ -0,0 +1,34 @@
1
+ """Profile validation helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def validate_profile_data(profile_name: str, profile_data: dict[str, Any]) -> None:
9
+ """Validate a single profile data structure."""
10
+ if "fs_read_roots" in profile_data and not isinstance(profile_data["fs_read_roots"], list):
11
+ raise ValueError(f"Profile '{profile_name}': fs_read_roots must be a list")
12
+ if "fs_write_roots" in profile_data and not isinstance(profile_data["fs_write_roots"], list):
13
+ raise ValueError(f"Profile '{profile_name}': fs_write_roots must be a list")
14
+ if "deny_paths" in profile_data and not isinstance(profile_data["deny_paths"], list):
15
+ raise ValueError(f"Profile '{profile_name}': deny_paths must be a list")
16
+ if "allowed_commands" in profile_data and not isinstance(profile_data["allowed_commands"], list):
17
+ raise ValueError(f"Profile '{profile_name}': allowed_commands must be a list")
18
+ if "deny_shell" in profile_data and not isinstance(profile_data["deny_shell"], bool):
19
+ raise ValueError(f"Profile '{profile_name}': deny_shell must be a boolean")
20
+ if "network" in profile_data and not isinstance(profile_data["network"], bool):
21
+ raise ValueError(f"Profile '{profile_name}': network must be a boolean")
22
+ if "allow_env" in profile_data and not isinstance(profile_data["allow_env"], bool):
23
+ raise ValueError(f"Profile '{profile_name}': allow_env must be a boolean")
24
+
25
+
26
+ def validate_profiles(profiles: dict[str, Any]) -> None:
27
+ """Validate profile map structure."""
28
+ if not isinstance(profiles, dict):
29
+ raise ValueError("profiles must be a table (dictionary)")
30
+
31
+ for profile_name, profile_data in profiles.items():
32
+ if not isinstance(profile_data, dict):
33
+ raise ValueError(f"Profile '{profile_name}' must be a table (dictionary)")
34
+ validate_profile_data(profile_name, profile_data)
File without changes