bamboo-coding 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 (62) hide show
  1. agent/__init__.py +0 -0
  2. agent/bamboo_git_agent/__init__.py +0 -0
  3. agent/bamboo_git_agent/capabilities/__init__.py +0 -0
  4. agent/bamboo_git_agent/capabilities/repo.py +189 -0
  5. agent/bamboo_git_agent/config.py +76 -0
  6. agent/bamboo_git_agent/controller_client.py +73 -0
  7. agent/bamboo_git_agent/journal.py +209 -0
  8. agent/bamboo_git_agent/main.py +164 -0
  9. agent/bamboo_git_agent/protocol.py +21 -0
  10. agent/bamboo_git_agent/status.py +59 -0
  11. api/__init__.py +2 -0
  12. api/commit.py +82 -0
  13. api/repository.py +592 -0
  14. bamboo_coding/__init__.py +3 -0
  15. bamboo_coding/client/__init__.py +3 -0
  16. bamboo_coding/client/capabilities/__init__.py +1 -0
  17. bamboo_coding/client/capabilities/repo.py +279 -0
  18. bamboo_coding/client/config.py +179 -0
  19. bamboo_coding/client/controller_client.py +112 -0
  20. bamboo_coding/client/journal.py +1 -0
  21. bamboo_coding/client/main.py +425 -0
  22. bamboo_coding/client/status.py +59 -0
  23. bamboo_coding/client/terminal_runtime.py +251 -0
  24. bamboo_coding/server/__init__.py +3 -0
  25. bamboo_coding/server/core/__init__.py +1 -0
  26. bamboo_coding/server/core/config.py +1 -0
  27. bamboo_coding/server/main.py +39 -0
  28. bamboo_coding/shared/__init__.py +39 -0
  29. bamboo_coding/shared/protocol.py +1 -0
  30. bamboo_coding-0.1.0.dist-info/METADATA +284 -0
  31. bamboo_coding-0.1.0.dist-info/RECORD +62 -0
  32. bamboo_coding-0.1.0.dist-info/WHEEL +5 -0
  33. bamboo_coding-0.1.0.dist-info/entry_points.txt +3 -0
  34. bamboo_coding-0.1.0.dist-info/top_level.txt +6 -0
  35. controller/__init__.py +0 -0
  36. controller/app/__init__.py +0 -0
  37. controller/app/api/__init__.py +0 -0
  38. controller/app/api/agents/__init__.py +3 -0
  39. controller/app/api/agents/ws.py +133 -0
  40. controller/app/api/public/__init__.py +11 -0
  41. controller/app/api/public/repos.py +207 -0
  42. controller/app/api/public/terminals.py +64 -0
  43. controller/app/core/__init__.py +0 -0
  44. controller/app/core/config.py +20 -0
  45. controller/app/core/errors.py +14 -0
  46. controller/app/db/__init__.py +0 -0
  47. controller/app/db/models.py +42 -0
  48. controller/app/db/session.py +74 -0
  49. controller/app/main.py +71 -0
  50. controller/app/schemas/__init__.py +23 -0
  51. controller/app/schemas/agent_messages.py +21 -0
  52. controller/app/schemas/public.py +3 -0
  53. controller/app/services/__init__.py +0 -0
  54. controller/app/services/agents.py +108 -0
  55. controller/app/services/registrations.py +17 -0
  56. controller/app/services/repositories.py +85 -0
  57. controller/app/services/router.py +140 -0
  58. controller/app/services/tasks.py +104 -0
  59. controller/app/services/terminals.py +148 -0
  60. git_utils.py +2238 -0
  61. shared/__init__.py +39 -0
  62. shared/protocol.py +253 -0
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from dataclasses import asdict
6
+ from pathlib import Path
7
+ from typing import Awaitable, Callable
8
+
9
+ from agent.bamboo_git_agent.capabilities.repo import execute_task
10
+ from agent.bamboo_git_agent.config import load_config
11
+ from agent.bamboo_git_agent.controller_client import ControllerConnection
12
+ from agent.bamboo_git_agent.journal import RuntimeJournal
13
+ from agent.bamboo_git_agent.status import build_register_message, build_status_message
14
+ from shared.protocol import TaskEventMessage, TaskMessage, TaskResultMessage
15
+
16
+
17
+ TaskExecutor = Callable[[TaskMessage], Awaitable[dict]]
18
+
19
+
20
+ class AgentRuntime:
21
+ def __init__(self, executor: TaskExecutor, config=None):
22
+ self.config = config or load_config()
23
+ self.executor = executor
24
+ self.journal = RuntimeJournal(self.config.journal_path)
25
+ self.connection = ControllerConnection(self.config.controller_url, token=self.config.token)
26
+
27
+ def capabilities(self) -> list[dict]:
28
+ return [
29
+ {"capability": "fs.tree", "actions": ["get"]},
30
+ {"capability": "fs.browse", "actions": ["get"]},
31
+ {"capability": "fs.file.read", "actions": ["get"]},
32
+ {"capability": "fs.file.write", "actions": ["put"]},
33
+ {"capability": "git.diff", "actions": ["get"]},
34
+ {"capability": "git.status", "actions": ["get"]},
35
+ {"capability": "git.commits", "actions": ["list"]},
36
+ {"capability": "git.commit_details", "actions": ["get"]},
37
+ {"capability": "git.stage", "actions": ["post"]},
38
+ {"capability": "git.unstage", "actions": ["post"]},
39
+ {"capability": "git.branches.list", "actions": ["get"]},
40
+ {"capability": "git.branches.create", "actions": ["post"]},
41
+ {"capability": "git.branches.checkout", "actions": ["put"]},
42
+ {"capability": "git.branches.delete", "actions": ["delete"]},
43
+ {"capability": "git.branches.merge", "actions": ["post"]},
44
+ {"capability": "git.commit.create", "actions": ["post"]},
45
+ {"capability": "repo.tree", "actions": ["get"]},
46
+ {"capability": "repo.browse", "actions": ["get"]},
47
+ {"capability": "repo.diff", "actions": ["get"]},
48
+ {"capability": "repo.status", "actions": ["get"]},
49
+ {"capability": "repo.commits", "actions": ["list"]},
50
+ {"capability": "repo.commit_details", "actions": ["get"]},
51
+ {"capability": "repo.file.read", "actions": ["get"]},
52
+ {"capability": "repo.file.write", "actions": ["put"]},
53
+ {"capability": "repo.stage", "actions": ["post"]},
54
+ {"capability": "repo.unstage", "actions": ["post"]},
55
+ {"capability": "repo.branches.list", "actions": ["get"]},
56
+ {"capability": "repo.branches.create", "actions": ["post"]},
57
+ {"capability": "repo.branches.checkout", "actions": ["put"]},
58
+ {"capability": "repo.branches.delete", "actions": ["delete"]},
59
+ {"capability": "repo.branches.merge", "actions": ["post"]},
60
+ {"capability": "repo.commit.create", "actions": ["post"]},
61
+ ]
62
+
63
+ def roots(self) -> list[dict]:
64
+ return [{"path": item.path, "name": item.name} for item in self.config.repositories]
65
+
66
+ def repositories(self) -> list[dict]:
67
+ return [asdict(item) for item in self.config.repositories]
68
+
69
+ def runtime_health(self) -> tuple[int, int, int]:
70
+ active = len(self.journal.list_active_tasks())
71
+ queued = 0
72
+ return active, queued, self.config.max_concurrent_tasks
73
+
74
+ def _persist_token(self, token: str) -> None:
75
+ self.config.token_path.parent.mkdir(parents=True, exist_ok=True)
76
+ self.config.token_path.write_text(json.dumps({"token": token}), encoding="utf-8")
77
+ self.connection.token = token
78
+
79
+ async def register_once(self) -> bool:
80
+ await self.connection.connect()
81
+ active, queued, max_concurrent = self.runtime_health()
82
+ register = build_register_message(
83
+ client_id=self.config.client_id,
84
+ hostname=self.config.hostname,
85
+ version=self.config.version,
86
+ platform_name="python",
87
+ capabilities=self.capabilities(),
88
+ roots=self.roots(),
89
+ active_tasks=active,
90
+ queued_tasks=queued,
91
+ max_concurrent_tasks=max_concurrent,
92
+ connected=True,
93
+ )
94
+ registration = await self.connection.send_register(register)
95
+ approved = bool(registration.get("approved", False))
96
+ token = registration.get("token")
97
+ if approved and isinstance(token, str) and token:
98
+ self._persist_token(token)
99
+ return approved
100
+
101
+ async def run(self) -> None:
102
+ while True:
103
+ try:
104
+ approved = await self.register_once()
105
+ if not approved:
106
+ await self.connection.close()
107
+ await asyncio.sleep(1)
108
+ continue
109
+ await self.connection.listen(self.handle_task)
110
+ except Exception:
111
+ await self.connection.close()
112
+ await asyncio.sleep(1)
113
+ continue
114
+ await self.connection.close()
115
+ await asyncio.sleep(1)
116
+
117
+ async def handle_task(self, task: TaskMessage) -> None:
118
+ self.journal.record_task_received(
119
+ task_id=task.task_id,
120
+ repo_id=task.repo_id,
121
+ capability=task.capability,
122
+ action=task.action,
123
+ params=task.params,
124
+ )
125
+ self.journal.append_event(task_id=task.task_id, event_type="accepted", status="running", payload={"message": "started"})
126
+ await self.connection.send_event(TaskEventMessage(task_id=task.task_id, status="running", message="started"))
127
+ try:
128
+ result = await self.executor(task)
129
+ self.journal.record_terminal_result(task_id=task.task_id, status="succeeded", result=result)
130
+ await self.connection.send_result(TaskResultMessage(task_id=task.task_id, success=True, result=result))
131
+ except Exception as exc:
132
+ self.journal.record_terminal_result(task_id=task.task_id, status="failed", error=str(exc))
133
+ await self.connection.send_result(TaskResultMessage(task_id=task.task_id, success=False, error=str(exc)))
134
+ active, queued, max_concurrent = self.runtime_health()
135
+ await self.connection.send_status(
136
+ build_status_message(
137
+ client_id=self.config.client_id,
138
+ hostname=self.config.hostname,
139
+ version=self.config.version,
140
+ platform_name="python",
141
+ reason="task_update",
142
+ capabilities=self.capabilities(),
143
+ roots=self.roots(),
144
+ active_tasks=active,
145
+ queued_tasks=queued,
146
+ max_concurrent_tasks=max_concurrent,
147
+ connected=True,
148
+ )
149
+ )
150
+
151
+
152
+ def main() -> None:
153
+ config = load_config()
154
+ repositories = {item.repo_id: item.path for item in config.repositories}
155
+
156
+ async def _execute(task: TaskMessage) -> dict:
157
+ return execute_task(task, repositories)
158
+
159
+ runtime = AgentRuntime(_execute, config=config)
160
+ asyncio.run(runtime.run())
161
+
162
+
163
+ if __name__ == "__main__":
164
+ main()
@@ -0,0 +1,21 @@
1
+ from shared.protocol import (
2
+ AgentRegisterMessage,
3
+ ClientStatusMessage,
4
+ RegisterRequestMessage,
5
+ TaskEventMessage,
6
+ TaskMessage,
7
+ TaskQueryMessage,
8
+ TaskQueryResultMessage,
9
+ TaskResultMessage,
10
+ )
11
+
12
+ __all__ = [
13
+ "AgentRegisterMessage",
14
+ "ClientStatusMessage",
15
+ "RegisterRequestMessage",
16
+ "TaskEventMessage",
17
+ "TaskMessage",
18
+ "TaskQueryMessage",
19
+ "TaskQueryResultMessage",
20
+ "TaskResultMessage",
21
+ ]
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from shared.protocol import AgentRegisterMessage, ClientStatusMessage
4
+
5
+
6
+ def _build_common_payload(*, client_id: str, hostname: str, version: str, platform_name: str, capabilities: list[dict], roots: list[dict] | None, repositories: list[dict] | None, active_tasks: int, queued_tasks: int, max_concurrent_tasks: int, connected: bool) -> dict:
7
+ root_payload = roots if roots is not None else (repositories or [])
8
+ return {
9
+ "protocol_version": 2,
10
+ "client_id": client_id,
11
+ "client_info": {
12
+ "hostname": hostname,
13
+ "version": version,
14
+ "platform": platform_name,
15
+ },
16
+ "capabilities": capabilities,
17
+ "roots": root_payload,
18
+ "connectivity": {"controller": {"connected": connected}},
19
+ "runtime_health": {
20
+ "active_tasks": active_tasks,
21
+ "queued_tasks": queued_tasks,
22
+ "max_concurrent_tasks": max_concurrent_tasks,
23
+ },
24
+ }
25
+
26
+
27
+ def build_register_message(*, client_id: str, hostname: str, version: str, platform_name: str, capabilities: list[dict], roots: list[dict] | None = None, repositories: list[dict] | None = None, active_tasks: int, queued_tasks: int, max_concurrent_tasks: int, connected: bool = True) -> AgentRegisterMessage:
28
+ payload = _build_common_payload(
29
+ client_id=client_id,
30
+ hostname=hostname,
31
+ version=version,
32
+ platform_name=platform_name,
33
+ capabilities=capabilities,
34
+ roots=roots,
35
+ repositories=repositories,
36
+ active_tasks=active_tasks,
37
+ queued_tasks=queued_tasks,
38
+ max_concurrent_tasks=max_concurrent_tasks,
39
+ connected=connected,
40
+ )
41
+ return AgentRegisterMessage(**payload)
42
+
43
+
44
+ def build_status_message(*, client_id: str, hostname: str, version: str, platform_name: str, reason: str, capabilities: list[dict], roots: list[dict] | None = None, repositories: list[dict] | None = None, active_tasks: int, queued_tasks: int, max_concurrent_tasks: int, connected: bool) -> ClientStatusMessage:
45
+ payload = _build_common_payload(
46
+ client_id=client_id,
47
+ hostname=hostname,
48
+ version=version,
49
+ platform_name=platform_name,
50
+ capabilities=capabilities,
51
+ roots=roots,
52
+ repositories=repositories,
53
+ active_tasks=active_tasks,
54
+ queued_tasks=queued_tasks,
55
+ max_concurrent_tasks=max_concurrent_tasks,
56
+ connected=connected,
57
+ )
58
+ payload["reason"] = reason
59
+ return ClientStatusMessage(**payload)
api/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """API package initialization."""
2
+
api/commit.py ADDED
@@ -0,0 +1,82 @@
1
+ """
2
+ Commit API Endpoints
3
+
4
+ Provides endpoints for repository commit history operations.
5
+ """
6
+
7
+ from sanic import Blueprint, response
8
+ from sanic.exceptions import InvalidUsage, ServerError, NotFound
9
+
10
+ import git_utils
11
+
12
+
13
+ commit_bp = Blueprint("commit", url_prefix="/api/repo")
14
+
15
+
16
+ @commit_bp.route("/commits")
17
+ async def get_commits(request):
18
+ """
19
+ Get commit history with pagination.
20
+
21
+ Query params:
22
+ limit: Maximum number of commits to return (default 50, max 100)
23
+ offset: Number of commits to skip (default 0)
24
+
25
+ Returns:
26
+ JSON with list of commits and pagination info
27
+ """
28
+ repo_path = request.ctx.repo_path
29
+
30
+ limit = int(request.args.get("limit", "50"))
31
+ offset = int(request.args.get("offset", "0"))
32
+
33
+ # Validate parameters
34
+ if limit < 1 or limit > 100:
35
+ raise InvalidUsage("Limit must be between 1 and 100")
36
+ if offset < 0:
37
+ raise InvalidUsage("Offset must be non-negative")
38
+
39
+ if not git_utils.is_valid_repository(repo_path):
40
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
41
+
42
+ commits = git_utils.get_commit_history(repo_path, limit, offset)
43
+
44
+ return response.json({
45
+ "commits": commits,
46
+ "limit": limit,
47
+ "offset": offset,
48
+ "count": len(commits)
49
+ })
50
+
51
+
52
+ @commit_bp.route("/commit/<commit_hash>")
53
+ async def get_commit_details(request, commit_hash):
54
+ """
55
+ Get detailed information about a specific commit.
56
+
57
+ Path params:
58
+ commit_hash: Full or abbreviated commit hash
59
+
60
+ Returns:
61
+ JSON with commit metadata, changed files, and diffs
62
+ """
63
+ repo_path = request.ctx.repo_path
64
+
65
+ if not git_utils.is_valid_repository(repo_path):
66
+ raise InvalidUsage(f"Not a valid Git repository: {repo_path}")
67
+
68
+ try:
69
+ commit_details = git_utils.get_commit_details(repo_path, commit_hash)
70
+ return response.json(commit_details)
71
+ except ValueError as e:
72
+ # Invalid commit hash or commit not found
73
+ error_msg = str(e)
74
+ if "not found" in error_msg.lower():
75
+ raise NotFound(error_msg)
76
+ else:
77
+ raise InvalidUsage(error_msg)
78
+ except git_utils.GitPermissionError as e:
79
+ raise ServerError(str(e))
80
+ except git_utils.InvalidGitRepositoryError as e:
81
+ raise InvalidUsage(str(e))
82
+