gdmcode 0.1.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.
Files changed (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. src/voice/voice_loop.py +156 -0
@@ -0,0 +1,182 @@
1
+ """Tamper-evident audit log using hash chain."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import time
7
+ import threading
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ class AuditEventType(str, Enum):
15
+ TOOL_CALL = "tool_call"
16
+ FILE_EDIT = "file_edit"
17
+ MODEL_CALL = "model_call"
18
+ SESSION_START = "session_start"
19
+ SESSION_END = "session_end"
20
+ PERMISSION_CHECK = "permission_check"
21
+
22
+
23
+ @dataclass
24
+ class AuditEvent:
25
+ event_type: AuditEventType
26
+ actor: str # user or team name
27
+ details: dict[str, Any]
28
+ timestamp: float = field(default_factory=time.time)
29
+ event_id: str = "" # auto-generated SHA256 hash
30
+ prev_hash: str = "" # hash of previous event (chain)
31
+ hash: str = "" # hash of this event
32
+
33
+
34
+ class AuditLog:
35
+ """Append-only tamper-evident audit log. Each entry hashes the previous."""
36
+
37
+ def __init__(self, log_path: str | Path, actor: str = "unknown"):
38
+ self._path = Path(log_path)
39
+ self._actor = actor
40
+ self._lock = threading.Lock()
41
+ self._last_hash = "0" * 64 # genesis hash
42
+
43
+ def log(self, event_type: AuditEventType, details: dict[str, Any]) -> AuditEvent:
44
+ """Append event to log. Thread-safe."""
45
+ with self._lock:
46
+ event = AuditEvent(
47
+ event_type=event_type,
48
+ actor=self._actor,
49
+ details=details,
50
+ )
51
+ # event_id: fingerprint of immutable content (no chain fields)
52
+ event.event_id = self._compute_event_id(event)
53
+ event.prev_hash = self._last_hash
54
+ event.hash = self._compute_hash(event)
55
+ self._last_hash = event.hash
56
+ self._write(event)
57
+ return event
58
+
59
+ def log_tool_call(self, tool_name: str, args: dict, result_summary: str) -> AuditEvent:
60
+ return self.log(AuditEventType.TOOL_CALL, {
61
+ "tool_name": tool_name,
62
+ "args": args,
63
+ "result_summary": result_summary,
64
+ })
65
+
66
+ def log_file_edit(self, file_path: str, change_type: str, lines_changed: int) -> AuditEvent:
67
+ return self.log(AuditEventType.FILE_EDIT, {
68
+ "file_path": file_path,
69
+ "change_type": change_type,
70
+ "lines_changed": lines_changed,
71
+ })
72
+
73
+ def log_model_call(self, model: str, tokens_in: int, tokens_out: int) -> AuditEvent:
74
+ return self.log(AuditEventType.MODEL_CALL, {
75
+ "model": model,
76
+ "tokens_in": tokens_in,
77
+ "tokens_out": tokens_out,
78
+ })
79
+
80
+ def log_session_start(self, session_id: str) -> AuditEvent:
81
+ return self.log(AuditEventType.SESSION_START, {"session_id": session_id})
82
+
83
+ def log_session_end(self, session_id: str, summary: str) -> AuditEvent:
84
+ return self.log(AuditEventType.SESSION_END, {
85
+ "session_id": session_id,
86
+ "summary": summary,
87
+ })
88
+
89
+ def verify_chain(self) -> tuple[bool, str]:
90
+ """Verify hash chain integrity. Returns (valid, error_message)."""
91
+ if not self._path.exists():
92
+ return (True, "")
93
+
94
+ lines = [ln for ln in self._path.read_text(encoding="utf-8").splitlines() if ln.strip()]
95
+ if not lines:
96
+ return (True, "")
97
+
98
+ prev_hash = "0" * 64
99
+ for idx, line in enumerate(lines):
100
+ try:
101
+ data = json.loads(line)
102
+ except json.JSONDecodeError as exc:
103
+ return (False, f"line {idx}: invalid JSON: {exc}")
104
+
105
+ event = _event_from_dict(data)
106
+ stored_hash = event.hash
107
+ stored_event_id = event.event_id
108
+
109
+ # Verify prev_hash linkage
110
+ if event.prev_hash != prev_hash:
111
+ return (False, f"event {idx}: prev_hash mismatch")
112
+
113
+ # Verify event_id
114
+ expected_id = self._compute_event_id(event)
115
+ if event.event_id != expected_id:
116
+ return (False, f"event {idx}: event_id mismatch")
117
+
118
+ # Verify hash
119
+ expected_hash = self._compute_hash(event)
120
+ if stored_hash != expected_hash:
121
+ return (False, f"event {idx}: hash mismatch")
122
+
123
+ prev_hash = stored_hash
124
+
125
+ return (True, "")
126
+
127
+ def tail(self, n: int = 20) -> list[AuditEvent]:
128
+ """Return last N events."""
129
+ if not self._path.exists():
130
+ return []
131
+ lines = [ln for ln in self._path.read_text(encoding="utf-8").splitlines() if ln.strip()]
132
+ return [_event_from_dict(json.loads(ln)) for ln in lines[-n:]]
133
+
134
+ def _compute_event_id(self, event: AuditEvent) -> str:
135
+ """SHA256 of immutable event identity fields (excludes chain fields)."""
136
+ payload = json.dumps({
137
+ "event_type": event.event_type,
138
+ "actor": event.actor,
139
+ "details": event.details,
140
+ "timestamp": event.timestamp,
141
+ }, sort_keys=True, separators=(",", ":"))
142
+ return hashlib.sha256(payload.encode()).hexdigest()
143
+
144
+ def _compute_hash(self, event: AuditEvent) -> str:
145
+ """SHA256 of canonical JSON of the event fields."""
146
+ payload = json.dumps({
147
+ "event_type": event.event_type,
148
+ "actor": event.actor,
149
+ "details": event.details,
150
+ "timestamp": event.timestamp,
151
+ "event_id": event.event_id,
152
+ "prev_hash": event.prev_hash,
153
+ }, sort_keys=True, separators=(",", ":"))
154
+ return hashlib.sha256(payload.encode()).hexdigest()
155
+
156
+ def _write(self, event: AuditEvent) -> None:
157
+ """Append event as JSON line to log file."""
158
+ self._path.parent.mkdir(parents=True, exist_ok=True)
159
+ record = {
160
+ "event_type": event.event_type,
161
+ "actor": event.actor,
162
+ "details": event.details,
163
+ "timestamp": event.timestamp,
164
+ "event_id": event.event_id,
165
+ "prev_hash": event.prev_hash,
166
+ "hash": event.hash,
167
+ }
168
+ with self._path.open("a", encoding="utf-8") as fh:
169
+ fh.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n")
170
+
171
+
172
+ def _event_from_dict(data: dict) -> AuditEvent:
173
+ """Reconstruct AuditEvent from a JSON dict."""
174
+ return AuditEvent(
175
+ event_type=AuditEventType(data["event_type"]),
176
+ actor=data["actor"],
177
+ details=data["details"],
178
+ timestamp=data["timestamp"],
179
+ event_id=data.get("event_id", ""),
180
+ prev_hash=data.get("prev_hash", ""),
181
+ hash=data.get("hash", ""),
182
+ )
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ import subprocess
7
+
8
+
9
+ class IdentityNotFoundError(Exception):
10
+ pass
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Identity:
15
+ actor_id: str
16
+ display_name: str
17
+ roles: list[str]
18
+ token_exp: Optional[float] = None # epoch; None = no expiry
19
+
20
+
21
+ class IdentityProvider(ABC):
22
+ @abstractmethod
23
+ def resolve(self) -> Identity: ...
24
+
25
+
26
+ class LocalIdentity(IdentityProvider):
27
+ """Dev-mode identity from ~/.config/gdm/identity.toml. Falls back to git config."""
28
+
29
+ def __init__(self, identity_file: Path = None):
30
+ self._path = identity_file or Path.home() / ".config" / "gdm" / "identity.toml"
31
+
32
+ def resolve(self) -> Identity:
33
+ if not self._path.exists():
34
+ try:
35
+ email = subprocess.check_output(
36
+ ["git", "config", "user.email"], text=True,
37
+ stderr=subprocess.DEVNULL
38
+ ).strip()
39
+ except Exception:
40
+ email = "unknown@local"
41
+ return Identity(actor_id=email, display_name=email, roles=[])
42
+ try:
43
+ import tomllib
44
+ except ImportError:
45
+ import tomli as tomllib # Python < 3.11 fallback
46
+ with open(self._path, "rb") as f:
47
+ raw = tomllib.load(f)
48
+ data = raw.get("identity", raw)
49
+ return Identity(
50
+ actor_id=data["actor_id"],
51
+ display_name=data.get("display_name", data["actor_id"]),
52
+ roles=data.get("roles", []),
53
+ )
54
+
55
+
56
+ class OIDCIdentity(IdentityProvider):
57
+ """Production OIDC/JWT identity validation. Requires PyJWT[crypto]."""
58
+
59
+ def __init__(self, issuer: str, audience: str, roles_claim: str = "gdm_roles"):
60
+ self._issuer = issuer
61
+ self._audience = audience
62
+ self._roles_claim = roles_claim
63
+
64
+ def resolve(self) -> Identity:
65
+ import os
66
+ token = os.environ.get("GDM_OIDC_TOKEN") or self._load_token_file()
67
+ try:
68
+ import jwt as pyjwt
69
+ except ImportError:
70
+ raise ImportError("PyJWT[cryptography] required for OIDC. pip install 'PyJWT[cryptography]'")
71
+ # In real use: fetch JWKS from issuer and validate signature
72
+ # For now: decode without verification (test mode) or with key
73
+ options = {"verify_signature": False} # override in production with JWKS key
74
+ claims = pyjwt.decode(token, options=options, algorithms=["RS256", "ES256", "HS256"])
75
+ if claims.get("exp") and claims["exp"] < __import__("time").time():
76
+ raise IdentityNotFoundError("OIDC token expired")
77
+ return Identity(
78
+ actor_id=claims["sub"],
79
+ display_name=claims.get("name", claims["sub"]),
80
+ roles=claims.get(self._roles_claim, []),
81
+ token_exp=claims.get("exp"),
82
+ )
83
+
84
+ def _load_token_file(self) -> str:
85
+ path = Path.home() / ".config" / "gdm" / "oidc_token"
86
+ if not path.exists():
87
+ raise IdentityNotFoundError(
88
+ "No OIDC token found. Set GDM_OIDC_TOKEN or run `gdm login`."
89
+ )
90
+ return path.read_text().strip()
src/enterprise/rbac.py ADDED
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+ from src.enterprise.identity import Identity
6
+
7
+
8
+ class RBACDeniedError(Exception):
9
+ pass
10
+
11
+
12
+ class NetworkPolicyViolationError(Exception):
13
+ pass
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class RolePermissions:
18
+ max_autonomy: int
19
+ allowed_tools: list[str] # ["*"] = all
20
+ denied_tools: list[str] = field(default_factory=list)
21
+ can_push: bool = False
22
+ can_admin_config: bool = False
23
+ max_cost_per_session: float = 50.0
24
+
25
+
26
+ BUILTIN_ROLES: dict[str, RolePermissions] = {
27
+ "admin": RolePermissions(
28
+ max_autonomy=5, allowed_tools=["*"], can_push=True, can_admin_config=True,
29
+ ),
30
+ "developer": RolePermissions(
31
+ max_autonomy=4, allowed_tools=["*"], can_push=True,
32
+ ),
33
+ "reviewer": RolePermissions(
34
+ max_autonomy=1,
35
+ allowed_tools=["read_file", "find_symbol", "grep", "review", "list_dir"],
36
+ can_push=False,
37
+ ),
38
+ }
39
+
40
+
41
+ class RBACManager:
42
+ def __init__(self, team_policy_roles: dict = None):
43
+ self._roles = {**BUILTIN_ROLES, **(team_policy_roles or {})}
44
+
45
+ def get_permissions(self, identity: Identity) -> RolePermissions:
46
+ if not identity.roles:
47
+ return RolePermissions(max_autonomy=0, allowed_tools=[], max_cost_per_session=0.0)
48
+ if "admin" in identity.roles:
49
+ return self._roles["admin"]
50
+ best = max(
51
+ (self._roles[r] for r in identity.roles if r in self._roles),
52
+ key=lambda p: p.max_autonomy,
53
+ default=RolePermissions(max_autonomy=0, allowed_tools=[]),
54
+ )
55
+ return best
56
+
57
+ def check_tool(self, identity: Identity, tool_name: str) -> bool:
58
+ perms = self.get_permissions(identity)
59
+ if "*" in perms.allowed_tools:
60
+ return tool_name not in perms.denied_tools
61
+ return tool_name in perms.allowed_tools and tool_name not in perms.denied_tools
62
+
63
+ def assert_tool(self, identity: Identity, tool_name: str) -> None:
64
+ """Raise RBACDeniedError if tool not permitted."""
65
+ if not self.check_tool(identity, tool_name):
66
+ raise RBACDeniedError(
67
+ f"Actor '{identity.actor_id}' with roles {identity.roles} "
68
+ f"is not permitted to use tool '{tool_name}'"
69
+ )
70
+
71
+
72
+ class NetworkPolicyEnforcer:
73
+ _PRIVATE_PATTERNS = [
74
+ re.compile(r"^127\."),
75
+ re.compile(r"^10\."),
76
+ re.compile(r"^172\.(1[6-9]|2\d|3[01])\."),
77
+ re.compile(r"^169\.254\."),
78
+ re.compile(r"^::1$"),
79
+ re.compile(r"^localhost$", re.I),
80
+ re.compile(r"^0\.0\.0\.0$"),
81
+ ]
82
+
83
+ def __init__(self, allowlist: list[str] = None, allow_all: bool = True):
84
+ self._allowlist = allowlist or []
85
+ self._allow_all = allow_all
86
+
87
+ def check(self, url: str) -> None:
88
+ from urllib.parse import urlparse
89
+ host = urlparse(url).hostname or ""
90
+ # Always block private/SSRF targets
91
+ for pattern in self._PRIVATE_PATTERNS:
92
+ if pattern.search(host):
93
+ raise NetworkPolicyViolationError(
94
+ f"Blocked SSRF attempt to private address: {host}"
95
+ )
96
+ if not self._allow_all and self._allowlist:
97
+ if not any(host == a or host.endswith("." + a) for a in self._allowlist):
98
+ raise NetworkPolicyViolationError(
99
+ f"Host '{host}' not in network_allowlist. Allowed: {self._allowlist}"
100
+ )
@@ -0,0 +1,125 @@
1
+ """Team configuration for enterprise gdm deployments."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from fnmatch import fnmatch
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ try:
10
+ import tomllib
11
+ except ImportError:
12
+ import tomli as tomllib # type: ignore
13
+
14
+
15
+ @dataclass
16
+ class ToolPolicy:
17
+ allowlist: list[str] = field(default_factory=list) # empty = allow all
18
+ blocklist: list[str] = field(default_factory=list)
19
+ require_approval: list[str] = field(default_factory=list)
20
+
21
+
22
+ @dataclass
23
+ class ModelOverride:
24
+ default_model: str | None = None
25
+ max_tokens: int | None = None
26
+ temperature: float | None = None
27
+ allowed_models: list[str] = field(default_factory=list) # empty = allow all
28
+
29
+
30
+ @dataclass
31
+ class TeamConfig:
32
+ team_name: str = "default"
33
+ tool_policy: ToolPolicy = field(default_factory=ToolPolicy)
34
+ model_override: ModelOverride = field(default_factory=ModelOverride)
35
+ enforce_conventions: bool = False
36
+ audit_log_enabled: bool = False
37
+ allowed_file_patterns: list[str] = field(default_factory=list) # empty = allow all
38
+ blocked_file_patterns: list[str] = field(default_factory=list)
39
+ extra: dict[str, Any] = field(default_factory=dict)
40
+
41
+
42
+ class TeamConfigLoader:
43
+ """Load and validate team.toml configuration files."""
44
+
45
+ DEFAULT_PATHS = ["team.toml", ".gdm/team.toml", "~/.gdm/team.toml"]
46
+
47
+ def __init__(self, config_path: str | Path | None = None):
48
+ self._path = Path(config_path) if config_path else None
49
+
50
+ def load(self) -> TeamConfig:
51
+ """Load team config from file. Returns default TeamConfig if no file found."""
52
+ paths = [self._path] if self._path else [
53
+ Path(p).expanduser() for p in self.DEFAULT_PATHS
54
+ ]
55
+ for path in paths:
56
+ if path and path.exists():
57
+ content = path.read_text(encoding="utf-8")
58
+ data = self._parse_toml(content)
59
+ return self._build_config(data)
60
+ return TeamConfig()
61
+
62
+ def _build_config(self, data: dict) -> TeamConfig:
63
+ """Build TeamConfig from parsed TOML dict."""
64
+ tp = data.get("tool_policy", {})
65
+ mo = data.get("model_override", {})
66
+
67
+ tool_policy = ToolPolicy(
68
+ allowlist=tp.get("allowlist", []),
69
+ blocklist=tp.get("blocklist", []),
70
+ require_approval=tp.get("require_approval", []),
71
+ )
72
+ model_override = ModelOverride(
73
+ default_model=mo.get("default_model"),
74
+ max_tokens=mo.get("max_tokens"),
75
+ temperature=mo.get("temperature"),
76
+ allowed_models=mo.get("allowed_models", []),
77
+ )
78
+
79
+ known_keys = {
80
+ "team_name", "tool_policy", "model_override",
81
+ "enforce_conventions", "audit_log_enabled",
82
+ "allowed_file_patterns", "blocked_file_patterns",
83
+ }
84
+ extra = {k: v for k, v in data.items() if k not in known_keys}
85
+
86
+ return TeamConfig(
87
+ team_name=data.get("team_name", "default"),
88
+ tool_policy=tool_policy,
89
+ model_override=model_override,
90
+ enforce_conventions=data.get("enforce_conventions", False),
91
+ audit_log_enabled=data.get("audit_log_enabled", False),
92
+ allowed_file_patterns=data.get("allowed_file_patterns", []),
93
+ blocked_file_patterns=data.get("blocked_file_patterns", []),
94
+ extra=extra,
95
+ )
96
+
97
+ def is_tool_allowed(self, tool_name: str, config: TeamConfig) -> bool:
98
+ """Check if tool is allowed by policy."""
99
+ policy = config.tool_policy
100
+ if tool_name in policy.blocklist:
101
+ return False
102
+ if policy.allowlist and tool_name not in policy.allowlist:
103
+ return False
104
+ return True
105
+
106
+ def is_model_allowed(self, model_name: str, config: TeamConfig) -> bool:
107
+ """Check if model is allowed by override policy."""
108
+ allowed = config.model_override.allowed_models
109
+ if allowed and model_name not in allowed:
110
+ return False
111
+ return True
112
+
113
+ def is_file_allowed(self, file_path: str, config: TeamConfig) -> bool:
114
+ """Check file against allowed/blocked patterns using fnmatch."""
115
+ for pattern in config.blocked_file_patterns:
116
+ if fnmatch(file_path, pattern):
117
+ return False
118
+ if config.allowed_file_patterns:
119
+ return any(fnmatch(file_path, p) for p in config.allowed_file_patterns)
120
+ return True
121
+
122
+ @staticmethod
123
+ def _parse_toml(content: str) -> dict:
124
+ """Parse TOML content."""
125
+ return tomllib.loads(content)