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.
- agents/base.py +17 -3
- agents/claude.py +12 -2
- agents/codex.py +12 -2
- agents/copilot.py +12 -2
- agents/opencode.py +12 -2
- cli.py +8 -2
- commands/completion.py +345 -0
- commands/config.py +284 -37
- commands/exec.py +21 -1
- commands/profile.py +84 -0
- commands/update.py +89 -7
- completions/__init__.py +1 -0
- completions/bash.sh +210 -0
- completions/fish.fish +134 -0
- completions/powershell.ps1 +184 -0
- completions/zsh.sh +285 -0
- config/__init__.py +11 -0
- config/defaults.py +4 -21
- config/schema.py +3 -29
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/METADATA +51 -21
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/RECORD +34 -20
- policy/defaults.py +3 -19
- policy/profile.py +5 -7
- profiles/__init__.py +23 -0
- profiles/builtins.py +63 -0
- profiles/loader.py +74 -0
- profiles/paths.py +22 -0
- profiles/registry.py +19 -0
- profiles/summary.py +23 -0
- profiles/validation.py +34 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/WHEEL +0 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/entry_points.txt +0 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/licenses/NOTICE +0 -0
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
|
|
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 =
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|