onecoder 0.0.2__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 (73) hide show
  1. onecoder/agent.py +95 -0
  2. onecoder/agentic_tool_search/__init__.py +0 -0
  3. onecoder/agentic_tool_search/dynamic_tool_search.py +64 -0
  4. onecoder/agentic_tool_search/registry.py +33 -0
  5. onecoder/agents/__init__.py +7 -0
  6. onecoder/agents/documentation_agent.py +12 -0
  7. onecoder/agents/file_reader_agent.py +19 -0
  8. onecoder/agents/file_writer_agent.py +19 -0
  9. onecoder/agents/orchestrator_agent.py +51 -0
  10. onecoder/agents/refactoring_agent.py +12 -0
  11. onecoder/agents/research_agent.py +31 -0
  12. onecoder/agents/task_suggestion_agent.py +88 -0
  13. onecoder/alignment.py +236 -0
  14. onecoder/api.py +162 -0
  15. onecoder/api_client.py +112 -0
  16. onecoder/backends/base.py +22 -0
  17. onecoder/backends/local_tui.py +65 -0
  18. onecoder/blackboard.py +102 -0
  19. onecoder/cli.py +108 -0
  20. onecoder/commands/__init__.py +1 -0
  21. onecoder/commands/auth.py +78 -0
  22. onecoder/commands/ci.py +29 -0
  23. onecoder/commands/delegate.py +557 -0
  24. onecoder/commands/doctor.py +40 -0
  25. onecoder/commands/issue.py +136 -0
  26. onecoder/commands/logs.py +45 -0
  27. onecoder/commands/project.py +270 -0
  28. onecoder/commands/server.py +170 -0
  29. onecoder/config_manager.py +87 -0
  30. onecoder/constants.py +9 -0
  31. onecoder/diagnostics/__init__.py +2 -0
  32. onecoder/diagnostics/env_scan.py +207 -0
  33. onecoder/discovery.py +101 -0
  34. onecoder/distillation.py +236 -0
  35. onecoder/evaluation/__init__.py +1 -0
  36. onecoder/evaluation/ttu.py +176 -0
  37. onecoder/governance/__init__.py +0 -0
  38. onecoder/governance/probllm.py +91 -0
  39. onecoder/hooks.py +74 -0
  40. onecoder/ipc_auth.py +200 -0
  41. onecoder/issues.py +188 -0
  42. onecoder/jules_client.py +343 -0
  43. onecoder/knowledge.py +106 -0
  44. onecoder/llm.py +61 -0
  45. onecoder/logger.py +42 -0
  46. onecoder/metrics.py +129 -0
  47. onecoder/models/delegation.py +46 -0
  48. onecoder/onboarding.py +264 -0
  49. onecoder/review.py +233 -0
  50. onecoder/services/delegation_service.py +209 -0
  51. onecoder/services/validation_service.py +104 -0
  52. onecoder/sessions.py +186 -0
  53. onecoder/sprint_collector.py +165 -0
  54. onecoder/sync.py +167 -0
  55. onecoder/tmux.py +86 -0
  56. onecoder/tools/__init__.py +10 -0
  57. onecoder/tools/executor.py +53 -0
  58. onecoder/tools/external_tools.py +106 -0
  59. onecoder/tools/file_tools.py +77 -0
  60. onecoder/tools/interface.py +25 -0
  61. onecoder/tools/jules_tools.py +122 -0
  62. onecoder/tools/kit_tools.py +122 -0
  63. onecoder/tools/registry.py +32 -0
  64. onecoder/tui/__init__.py +5 -0
  65. onecoder/tui/app.py +263 -0
  66. onecoder/tui/commands.py +150 -0
  67. onecoder/tui/widgets.py +92 -0
  68. onecoder/worktree.py +186 -0
  69. onecoder-0.0.2.dist-info/METADATA +17 -0
  70. onecoder-0.0.2.dist-info/RECORD +73 -0
  71. onecoder-0.0.2.dist-info/WHEEL +5 -0
  72. onecoder-0.0.2.dist-info/entry_points.txt +2 -0
  73. onecoder-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,176 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional, Dict, Any
3
+ from pathlib import Path
4
+ import json
5
+ import os
6
+
7
+ @dataclass
8
+ class TTUResult:
9
+ """Result of a Time-To-Understand (TTU) evaluation."""
10
+ score: float # 0.0 to 1.0
11
+ passed: bool
12
+ context_found: List[str] = field(default_factory=list)
13
+ missing_context: List[str] = field(default_factory=list)
14
+ failure_modes: List[str] = field(default_factory=list)
15
+ recommendations: List[str] = field(default_factory=list)
16
+ details: Dict[str, Any] = field(default_factory=dict)
17
+
18
+ class TTUEvaluator:
19
+ """Evaluates whether the current context is sufficient to start a task."""
20
+
21
+ def __init__(self, threshold: float = 0.8): # Increased threshold due to strict governance
22
+ self.threshold = threshold
23
+
24
+ def evaluate(self, sprint_path: Optional[Path] = None) -> TTUResult:
25
+ """
26
+ Evaluates the context of the given sprint path.
27
+ If no path is provided, attempts to find the current active sprint.
28
+ """
29
+ repo_root = self._find_repo_root()
30
+
31
+ if sprint_path is None:
32
+ sprint_path = self._find_active_sprint(repo_root)
33
+
34
+ if sprint_path is None:
35
+ return TTUResult(
36
+ score=0.0,
37
+ passed=False,
38
+ missing_context=["Active Sprint Directory"],
39
+ failure_modes=["Uninitialized Environment"],
40
+ recommendations=["Initialize a sprint using 'sprint init' or 'onecoder init'"]
41
+ )
42
+
43
+ score = 0.0
44
+ missing = []
45
+ found = []
46
+ failure_modes = []
47
+ recommendations = []
48
+ details = {}
49
+
50
+ # 0. Check for AGENTS.md (Governance) - Weight: 0.2
51
+ agents_md = repo_root / "AGENTS.md"
52
+ if agents_md.exists():
53
+ score += 0.2
54
+ found.append("AGENTS.md")
55
+ else:
56
+ missing.append("AGENTS.md (Root)")
57
+ failure_modes.append("Governance Blindness")
58
+ recommendations.append("Create AGENTS.md in repo root to define agent policies.")
59
+
60
+ # 1. Check for sprint.json (Metadata) - Weight: 0.2
61
+ sprint_json = sprint_path / "sprint.json"
62
+ if sprint_json.exists():
63
+ found.append("sprint.json")
64
+ try:
65
+ with open(sprint_json, "r") as f:
66
+ data = json.load(f)
67
+
68
+ # Check for goals
69
+ if data.get("goals", {}).get("primary"):
70
+ score += 0.15
71
+ details["goals"] = "Present"
72
+ else:
73
+ missing.append("Primary Goal in sprint.json")
74
+ failure_modes.append("Goal Ambiguity")
75
+ recommendations.append("Define a primary goal in sprint.json")
76
+
77
+ # Check for tasks
78
+ if data.get("tasks"):
79
+ score += 0.05
80
+ details["tasks"] = f"{len(data['tasks'])} tasks found"
81
+ else:
82
+ # Not strictly missing if just starting, but good to have
83
+ details["tasks"] = "None"
84
+
85
+ except Exception as e:
86
+ missing.append(f"Valid sprint.json ({str(e)})")
87
+ failure_modes.append("Corrupt Metadata")
88
+ else:
89
+ missing.append("sprint.json")
90
+ failure_modes.append("Missing Metadata")
91
+ recommendations.append("Run 'sprint init' to generate sprint structure.")
92
+
93
+ # 2. Check for Context/Walkthrough - Weight: 0.3
94
+ # Look for README.md or WALKTHROUGH.md or context folder
95
+ context_files = ["README.md", "WALKTHROUGH.md", "context/context.md"]
96
+ context_found_count = 0
97
+ for cf in context_files:
98
+ if (sprint_path / cf).exists():
99
+ context_found_count += 1
100
+ found.append(cf)
101
+
102
+ if context_found_count > 0:
103
+ score += 0.3
104
+ else:
105
+ missing.append("Context Documentation (README.md or WALKTHROUGH.md)")
106
+ failure_modes.append("Context Blindness")
107
+ recommendations.append("Add a README.md or WALKTHROUGH.md describing the sprint.")
108
+
109
+ # 3. Check for Media/Reference (Optional but good) - Weight: 0.1
110
+ media_dir = sprint_path / "media"
111
+ if media_dir.exists() and any(media_dir.iterdir()):
112
+ score += 0.1
113
+ found.append("media/")
114
+
115
+ # 4. Check for Planning - Weight: 0.2
116
+ planning_dir = sprint_path / "planning"
117
+ if planning_dir.exists():
118
+ score += 0.2
119
+ found.append("planning/")
120
+ else:
121
+ # Maybe TODO.md?
122
+ if (sprint_path / "TODO.md").exists():
123
+ score += 0.2
124
+ found.append("TODO.md")
125
+ else:
126
+ missing.append("Planning Documents")
127
+ failure_modes.append("Unplanned Execution")
128
+ recommendations.append("Create a plan in planning/ or TODO.md")
129
+
130
+ # Normalize score if needed, but here simple sum max is 1.0
131
+
132
+ passed = score >= self.threshold
133
+
134
+ return TTUResult(
135
+ score=min(score, 1.0),
136
+ passed=passed,
137
+ context_found=found,
138
+ missing_context=missing,
139
+ failure_modes=failure_modes,
140
+ recommendations=recommendations,
141
+ details=details
142
+ )
143
+
144
+ def _find_repo_root(self) -> Path:
145
+ """Finds the repository root."""
146
+ curr = Path.cwd()
147
+ root = curr
148
+ # Traverse up
149
+ while root != root.parent:
150
+ if (root / ".git").exists() or (root / ".sprint").exists():
151
+ return root
152
+ root = root.parent
153
+ return curr # Default to cwd if not found
154
+
155
+ def _find_active_sprint(self, root: Optional[Path] = None) -> Optional[Path]:
156
+ # Simple heuristic: Look for .sprint folder in root
157
+ if root:
158
+ sprint_dir = root / ".sprint"
159
+ if sprint_dir.exists():
160
+ # Get latest subdir
161
+ subdirs = sorted([d for d in sprint_dir.iterdir() if d.is_dir()])
162
+ if subdirs:
163
+ return subdirs[-1]
164
+ return None
165
+
166
+ # Fallback if root not provided (legacy)
167
+ cwd = Path.cwd()
168
+ curr_root = cwd
169
+ while curr_root != curr_root.parent:
170
+ sprint_dir = curr_root / ".sprint"
171
+ if sprint_dir.exists():
172
+ subdirs = sorted([d for d in sprint_dir.iterdir() if d.is_dir()])
173
+ if subdirs:
174
+ return subdirs[-1]
175
+ curr_root = curr_root.parent
176
+ return None
File without changes
@@ -0,0 +1,91 @@
1
+ import yaml
2
+ import os
3
+ import re
4
+
5
+ class ProbLLMGuardian:
6
+ """
7
+ Enforces governance policies to prevent ProbLLM vulnerabilities (Prompt Injection,
8
+ Automatic Tool Invocation, Data Exfiltration).
9
+ """
10
+
11
+ def __init__(self, governance_path="governance.yaml"):
12
+ self.governance_path = governance_path
13
+ self.policy = self._load_policy()
14
+
15
+ def _load_policy(self):
16
+ """Loads the governance policy from yaml."""
17
+ if not os.path.exists(self.governance_path):
18
+ return {}
19
+
20
+ try:
21
+ with open(self.governance_path, "r") as f:
22
+ data = yaml.safe_load(f)
23
+ return data.get("probllm_prevention", {})
24
+ except Exception:
25
+ return {}
26
+
27
+ def is_enabled(self):
28
+ """Checks if ProbLLM prevention is enabled."""
29
+ return self.policy.get("enabled", False)
30
+
31
+ def validate_tool_execution(self, tool_name: str, args: dict):
32
+ """
33
+ Validates whether a tool execution is safe based on the policy.
34
+ Returns (is_safe, message).
35
+ """
36
+ if not self.is_enabled():
37
+ return True, "Policy disabled"
38
+
39
+ # 1. Check for High-Risk Tools requiring Confirmation
40
+ restricted_tools = self.policy.get("require_human_confirmation_for_tools", [])
41
+ if tool_name in restricted_tools:
42
+ # In a real CLI, we would prompt here.
43
+ # For now, we return a warning status that the caller must handle (e.g., prompt user).
44
+ return False, f"Tool '{tool_name}' is restricted and requires Human Confirmation."
45
+
46
+ # 2. Check for Secret Exposure in Args (Input Sanitization)
47
+ # (Simplified check: looks for env var patterns like $SECRET or generic key patterns)
48
+ if self.policy.get("block_secret_exposure", True):
49
+ if self._contains_secrets(str(args)):
50
+ return False, "Tool arguments appear to contain secrets or environment variables."
51
+
52
+ return True, "Safe"
53
+
54
+ def validate_output(self, output: str):
55
+ """
56
+ Scans LLM or Tool output for leaked secrets.
57
+ """
58
+ if not self.is_enabled():
59
+ return True, "Policy disabled"
60
+
61
+ if self.policy.get("block_secret_exposure", True):
62
+ if self._contains_secrets(output):
63
+ return False, "Output blocked: Potential secret leakage detected."
64
+
65
+ return True, "Safe"
66
+
67
+ def _contains_secrets(self, text: str) -> bool:
68
+ """
69
+ Heuristic check for secrets.
70
+ """
71
+ # 1. Check for common API Key patterns (simplified)
72
+ # Starts with sk-, gh-, etc. followed by alphanumeric
73
+ patterns = [
74
+ r"sk-[a-zA-Z0-9]{20,}", # OpenAI/Stripe style
75
+ r"gh[pousr]-[a-zA-Z0-9]{20,}", # GitHub tokens
76
+ r"xox[baprs]-[a-zA-Z0-9]{10,}", # Slack tokens
77
+ ]
78
+
79
+ for pattern in patterns:
80
+ if re.search(pattern, text):
81
+ return True
82
+
83
+ # 2. Check if any known environment variable values are present
84
+ # (Be careful not to block common words if an env var is common)
85
+ sensitive_keys = ["API_KEY", "SECRET", "TOKEN", "PASSWORD", "CREDENTIALS"]
86
+ for key, value in os.environ.items():
87
+ if any(s in key for s in sensitive_keys) and len(value) > 8: # Only check long secrets
88
+ if value in text:
89
+ return True
90
+
91
+ return False
onecoder/hooks.py ADDED
@@ -0,0 +1,74 @@
1
+ import os
2
+ import json
3
+ import subprocess
4
+ import fnmatch
5
+ from typing import List, Dict, Any, Optional
6
+ from pathlib import Path
7
+
8
+ class HooksManager:
9
+ def __init__(self, config_filename: str = "onecoder.hooks.json"):
10
+ self.config_filename = config_filename
11
+ self.config = self._load_config()
12
+
13
+ def _load_config(self) -> Dict[str, Any]:
14
+ """Loads the configuration from the hooks file in the current directory."""
15
+ # Look for config in current working directory
16
+ config_path = Path.cwd() / self.config_filename
17
+ if config_path.exists():
18
+ try:
19
+ with open(config_path, "r") as f:
20
+ return json.load(f)
21
+ except json.JSONDecodeError as e:
22
+ print(f"Error parsing {self.config_filename}: {e}")
23
+ return {}
24
+ except Exception as e:
25
+ print(f"Error reading {self.config_filename}: {e}")
26
+ return {}
27
+ return {}
28
+
29
+ def _run_command(self, command: str):
30
+ """Runs a shell command."""
31
+ print(f"[Hooks] Running: {command}")
32
+ try:
33
+ # shell=True is used to allow running complex commands like "cargo check"
34
+ # Security note: command comes from a user-defined config file.
35
+ subprocess.run(command, shell=True, check=True)
36
+ except subprocess.CalledProcessError as e:
37
+ print(f"[Hooks] Command failed: {command} (Exit code: {e.returncode})")
38
+ except Exception as e:
39
+ print(f"[Hooks] Error running command {command}: {e}")
40
+
41
+ def on_file_edit(self, filepath: str):
42
+ """Triggers hooks configured for file edits."""
43
+ # Reload config to pick up changes without restart
44
+ self.config = self._load_config()
45
+
46
+ file_patterns = self.config.get("file_patterns", [])
47
+
48
+ # Determine relative path for matching
49
+ try:
50
+ rel_path = os.path.relpath(filepath, os.getcwd())
51
+ except ValueError:
52
+ # If filepath is on a different drive or invalid, use absolute
53
+ rel_path = filepath
54
+
55
+ for item in file_patterns:
56
+ pattern = item.get("pattern")
57
+ command = item.get("command")
58
+
59
+ if pattern and command:
60
+ if fnmatch.fnmatch(rel_path, pattern):
61
+ print(f"[Hooks] File {rel_path} matches pattern {pattern}")
62
+ self._run_command(command)
63
+
64
+ def on_stop(self):
65
+ """Triggers hooks configured for session stop."""
66
+ self.config = self._load_config()
67
+ commands = self.config.get("on_stop", [])
68
+ if commands:
69
+ print("[Hooks] Running on_stop hooks...")
70
+ for command in commands:
71
+ self._run_command(command)
72
+
73
+ # Global instance
74
+ hooks_manager = HooksManager()
onecoder/ipc_auth.py ADDED
@@ -0,0 +1,200 @@
1
+ import os
2
+ import socket
3
+ import uuid
4
+ import secrets
5
+ import asyncio
6
+ import time
7
+ from typing import Dict, Optional
8
+
9
+
10
+ class TokenStore:
11
+ """Manages session-based tokens with TTL for API authentication."""
12
+
13
+ def __init__(self, ttl_seconds: int = 3600):
14
+ """
15
+ Initialize token store.
16
+
17
+ Args:
18
+ ttl_seconds: Time-to-live for tokens in seconds (default: 1 hour)
19
+ """
20
+ self._tokens: Dict[str, float] = {} # token -> expiry_timestamp
21
+ self.ttl_seconds = ttl_seconds
22
+
23
+ def generate_token(self) -> str:
24
+ """Generate a new session token with TTL."""
25
+ token = secrets.token_urlsafe(32)
26
+ expiry = time.time() + self.ttl_seconds
27
+ self._tokens[token] = expiry
28
+ return token
29
+
30
+ def validate_token(self, token: str) -> bool:
31
+ """
32
+ Validates a token without consuming it (session-based).
33
+
34
+ Args:
35
+ token: The token to validate
36
+
37
+ Returns:
38
+ True if token exists and hasn't expired, False otherwise
39
+ """
40
+ if token not in self._tokens:
41
+ return False
42
+
43
+ # Check if token has expired
44
+ if time.time() > self._tokens[token]:
45
+ # Clean up expired token
46
+ del self._tokens[token]
47
+ return False
48
+
49
+ return True
50
+
51
+ def cleanup_expired(self) -> int:
52
+ """
53
+ Remove expired tokens from the store.
54
+
55
+ Returns:
56
+ Number of tokens cleaned up
57
+ """
58
+ current_time = time.time()
59
+ expired_tokens = [
60
+ token for token, expiry in self._tokens.items() if current_time > expiry
61
+ ]
62
+
63
+ for token in expired_tokens:
64
+ del self._tokens[token]
65
+
66
+ return len(expired_tokens)
67
+
68
+ def revoke_token(self, token: str) -> bool:
69
+ """
70
+ Manually revoke a token.
71
+
72
+ Args:
73
+ token: The token to revoke
74
+
75
+ Returns:
76
+ True if token was revoked, False if it didn't exist
77
+ """
78
+ if token in self._tokens:
79
+ del self._tokens[token]
80
+ return True
81
+ return False
82
+
83
+
84
+ # Global store for the running process
85
+ TOKEN_STORE = TokenStore()
86
+
87
+
88
+ class IPCAuthServer:
89
+ """
90
+ A Unix Domain Socket server that vends one-time tokens to local clients.
91
+ This ensures that only processes with access to the socket (local users)
92
+ can obtain authorization to hit the agent API.
93
+ """
94
+
95
+ def __init__(self, socket_path: str = "/tmp/onecoder_auth.sock"):
96
+ self.socket_path = socket_path
97
+
98
+ async def start(self):
99
+ if os.path.exists(self.socket_path):
100
+ os.remove(self.socket_path)
101
+
102
+ server = await asyncio.start_unix_server(self.handle_client, self.socket_path)
103
+
104
+ # Ensure only the current user can read/write to the socket
105
+ os.chmod(self.socket_path, 0o600)
106
+
107
+ print(f"IPC Auth Server started on {self.socket_path}")
108
+
109
+ # Start periodic cleanup task
110
+ cleanup_task = asyncio.create_task(self._periodic_cleanup())
111
+
112
+ async with server:
113
+ try:
114
+ await server.serve_forever()
115
+ finally:
116
+ cleanup_task.cancel()
117
+
118
+ async def _periodic_cleanup(self):
119
+ """Periodically clean up expired tokens (every 5 minutes)."""
120
+ while True:
121
+ await asyncio.sleep(300) # 5 minutes
122
+ cleaned = TOKEN_STORE.cleanup_expired()
123
+ if cleaned > 0:
124
+ print(f"Cleaned up {cleaned} expired tokens")
125
+
126
+ async def handle_client(
127
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
128
+ ):
129
+ """
130
+ Handles token requests from local UI launchers.
131
+ Protocol:
132
+ - Client sends: "REQUEST_TOKEN\n"
133
+ - Server replies: "<token>\n"
134
+ """
135
+ data = await reader.readline()
136
+ message = data.decode().strip()
137
+
138
+ if message == "REQUEST_TOKEN":
139
+ token = TOKEN_STORE.generate_token()
140
+ writer.write(f"{token}\n".encode())
141
+ await writer.drain()
142
+
143
+ writer.close()
144
+ await writer.wait_closed()
145
+
146
+
147
+ async def get_token_from_ipc(
148
+ socket_path: str = "/tmp/onecoder_auth.sock",
149
+ ) -> Optional[str]:
150
+ """Client utility to fetch a token via the IPC socket."""
151
+ try:
152
+ reader, writer = await asyncio.open_unix_connection(socket_path)
153
+ writer.write(b"REQUEST_TOKEN\n")
154
+ await writer.drain()
155
+
156
+ data = await reader.readline()
157
+ token = data.decode().strip()
158
+
159
+ writer.close()
160
+ await writer.wait_closed()
161
+ return token
162
+ except Exception as e:
163
+ print(f"Error fetching token from IPC: {e}")
164
+ return None
165
+
166
+
167
+ if __name__ == "__main__":
168
+ # Test execution
169
+ async def main():
170
+ server = IPCAuthServer()
171
+ # Run server in background
172
+ server_task = asyncio.create_task(server.start())
173
+ await asyncio.sleep(1) # Wait for startup
174
+
175
+ # Simulate client
176
+ token = await get_token_from_ipc()
177
+ if token:
178
+ print(f"Client fetched token: {token}")
179
+
180
+ # Validate (should work - session-based)
181
+ is_valid = TOKEN_STORE.validate_token(token)
182
+ print(f"Token is valid: {is_valid}")
183
+
184
+ # Validate again (should still work - not consumed)
185
+ is_valid_again = TOKEN_STORE.validate_token(token)
186
+ print(f"Token is still valid after first check: {is_valid_again}")
187
+
188
+ # Revoke token
189
+ revoked = TOKEN_STORE.revoke_token(token)
190
+ print(f"Token revoked: {revoked}")
191
+
192
+ # Validate after revocation (should fail)
193
+ is_valid_after_revoke = TOKEN_STORE.validate_token(token)
194
+ print(f"Token valid after revocation: {is_valid_after_revoke}")
195
+ else:
196
+ print("Failed to fetch token from IPC")
197
+
198
+ server_task.cancel()
199
+
200
+ asyncio.run(main())