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.
- agent/__init__.py +0 -0
- agent/bamboo_git_agent/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/__init__.py +0 -0
- agent/bamboo_git_agent/capabilities/repo.py +189 -0
- agent/bamboo_git_agent/config.py +76 -0
- agent/bamboo_git_agent/controller_client.py +73 -0
- agent/bamboo_git_agent/journal.py +209 -0
- agent/bamboo_git_agent/main.py +164 -0
- agent/bamboo_git_agent/protocol.py +21 -0
- agent/bamboo_git_agent/status.py +59 -0
- api/__init__.py +2 -0
- api/commit.py +82 -0
- api/repository.py +592 -0
- bamboo_coding/__init__.py +3 -0
- bamboo_coding/client/__init__.py +3 -0
- bamboo_coding/client/capabilities/__init__.py +1 -0
- bamboo_coding/client/capabilities/repo.py +279 -0
- bamboo_coding/client/config.py +179 -0
- bamboo_coding/client/controller_client.py +112 -0
- bamboo_coding/client/journal.py +1 -0
- bamboo_coding/client/main.py +425 -0
- bamboo_coding/client/status.py +59 -0
- bamboo_coding/client/terminal_runtime.py +251 -0
- bamboo_coding/server/__init__.py +3 -0
- bamboo_coding/server/core/__init__.py +1 -0
- bamboo_coding/server/core/config.py +1 -0
- bamboo_coding/server/main.py +39 -0
- bamboo_coding/shared/__init__.py +39 -0
- bamboo_coding/shared/protocol.py +1 -0
- bamboo_coding-0.1.0.dist-info/METADATA +284 -0
- bamboo_coding-0.1.0.dist-info/RECORD +62 -0
- bamboo_coding-0.1.0.dist-info/WHEEL +5 -0
- bamboo_coding-0.1.0.dist-info/entry_points.txt +3 -0
- bamboo_coding-0.1.0.dist-info/top_level.txt +6 -0
- controller/__init__.py +0 -0
- controller/app/__init__.py +0 -0
- controller/app/api/__init__.py +0 -0
- controller/app/api/agents/__init__.py +3 -0
- controller/app/api/agents/ws.py +133 -0
- controller/app/api/public/__init__.py +11 -0
- controller/app/api/public/repos.py +207 -0
- controller/app/api/public/terminals.py +64 -0
- controller/app/core/__init__.py +0 -0
- controller/app/core/config.py +20 -0
- controller/app/core/errors.py +14 -0
- controller/app/db/__init__.py +0 -0
- controller/app/db/models.py +42 -0
- controller/app/db/session.py +74 -0
- controller/app/main.py +71 -0
- controller/app/schemas/__init__.py +23 -0
- controller/app/schemas/agent_messages.py +21 -0
- controller/app/schemas/public.py +3 -0
- controller/app/services/__init__.py +0 -0
- controller/app/services/agents.py +108 -0
- controller/app/services/registrations.py +17 -0
- controller/app/services/repositories.py +85 -0
- controller/app/services/router.py +140 -0
- controller/app/services/tasks.py +104 -0
- controller/app/services/terminals.py +148 -0
- git_utils.py +2238 -0
- shared/__init__.py +39 -0
- 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
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
|
+
|