taskboy 0.1.1__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.
- taskboy/__init__.py +0 -0
- taskboy/adapters/__init__.py +0 -0
- taskboy/adapters/_util.py +30 -0
- taskboy/adapters/aws_read.py +107 -0
- taskboy/adapters/confluence.py +88 -0
- taskboy/adapters/github_api.py +564 -0
- taskboy/adapters/issues.py +435 -0
- taskboy/adapters/jira.py +263 -0
- taskboy/adapters/sentry.py +82 -0
- taskboy/adapters/slack_history.py +204 -0
- taskboy/assets.py +27 -0
- taskboy/audit.py +78 -0
- taskboy/broker.py +266 -0
- taskboy/classifier.py +219 -0
- taskboy/cli.py +161 -0
- taskboy/config.py +539 -0
- taskboy/dashboard/__init__.py +5 -0
- taskboy/dashboard/api.py +963 -0
- taskboy/dashboard/app.py +78 -0
- taskboy/dashboard/auth.py +117 -0
- taskboy/dashboard/editors.py +140 -0
- taskboy/dashboard/gitops.py +60 -0
- taskboy/dashboard/render.py +48 -0
- taskboy/debug_feed.py +170 -0
- taskboy/deploy/env.example +17 -0
- taskboy/deploy/git-cred-helper.py +36 -0
- taskboy/deploy/install.sh +101 -0
- taskboy/deploy/remote-update.sh +52 -0
- taskboy/deploy/taskboy-restart.path +10 -0
- taskboy/deploy/taskboy-restart.service +8 -0
- taskboy/deploy/taskboy.service +22 -0
- taskboy/hooks.py +208 -0
- taskboy/issue_runs.py +203 -0
- taskboy/llm.py +125 -0
- taskboy/main.py +269 -0
- taskboy/memory.py +59 -0
- taskboy/models.py +103 -0
- taskboy/mrkdwn.py +19 -0
- taskboy/notify.py +41 -0
- taskboy/orchestrator.py +401 -0
- taskboy/personality.py +17 -0
- taskboy/prompts.py +288 -0
- taskboy/quick.py +234 -0
- taskboy/redact.py +49 -0
- taskboy/repocache.py +117 -0
- taskboy/review_requests.py +388 -0
- taskboy/router.py +146 -0
- taskboy/runner.py +581 -0
- taskboy/scheduler.py +301 -0
- taskboy/secrets.py +83 -0
- taskboy/settings.py +19 -0
- taskboy/setup_checks.py +184 -0
- taskboy/setup_wizard.py +699 -0
- taskboy/skills.py +95 -0
- taskboy/slack.py +666 -0
- taskboy/slack_users.py +32 -0
- taskboy/started_messages.py +33 -0
- taskboy/store.py +1393 -0
- taskboy/task_actions.py +86 -0
- taskboy/templates/config.example.yaml +149 -0
- taskboy/templates/conventions.md +60 -0
- taskboy/templates/help.example.md +29 -0
- taskboy/templates/personality_agent.example.md +5 -0
- taskboy/templates/personality_reviewer.example.md +5 -0
- taskboy/templates/services/aws.yaml +7 -0
- taskboy/templates/services/confluence.yaml +4 -0
- taskboy/templates/services/github.yaml +14 -0
- taskboy/templates/services/jira.yaml +6 -0
- taskboy/templates/services/sentry.yaml +4 -0
- taskboy/templates/services/slack.yaml +9 -0
- taskboy/templates/skills/README.md +33 -0
- taskboy/templates/skills/discoverissues/SKILL.md +46 -0
- taskboy/templates/skills/implementapprovedissues/SKILL.md +41 -0
- taskboy/templates/skills/jira2pr/SKILL.md +66 -0
- taskboy/templates/skills/monitor/SKILL.md +32 -0
- taskboy/templates/skills/monitornew/SKILL.md +27 -0
- taskboy/templates/skills/refineissue/SKILL.md +35 -0
- taskboy/templates/skills/release/SKILL.md +36 -0
- taskboy/templates/skills/review/SKILL.md +89 -0
- taskboy/templates/skills/reviewandmonitor/SKILL.md +22 -0
- taskboy/templates/skills/reviews/SKILL.md +26 -0
- taskboy/templates/skills/slack2jira/SKILL.md +47 -0
- taskboy/templates/skills/slack2pr/SKILL.md +64 -0
- taskboy/templates/skills/spec2pr/SKILL.md +35 -0
- taskboy/templates/slack_app_manifest.yaml +48 -0
- taskboy/templates/task_started_messages.yaml +14 -0
- taskboy/ui_dist/assets/index-DmYxR9Qy.css +1 -0
- taskboy/ui_dist/assets/index-LDDO4iT2.js +15 -0
- taskboy/ui_dist/index.html +15 -0
- taskboy/workspace.py +86 -0
- taskboy-0.1.1.dist-info/METADATA +120 -0
- taskboy-0.1.1.dist-info/RECORD +95 -0
- taskboy-0.1.1.dist-info/WHEEL +5 -0
- taskboy-0.1.1.dist-info/entry_points.txt +2 -0
- taskboy-0.1.1.dist-info/top_level.txt +1 -0
taskboy/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""shared output and error handling for in-process MCP adapters."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from taskboy.redact import redactor
|
|
6
|
+
|
|
7
|
+
OUTPUT_LIMIT = 4000
|
|
8
|
+
TRUNCATION_MARKER = "\n…(output truncated at 4000 chars — narrow the request or use git/the source system for full content)"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _text(text: str) -> dict:
|
|
12
|
+
value = redactor.redact(text)
|
|
13
|
+
if len(value) > OUTPUT_LIMIT:
|
|
14
|
+
value = value[: OUTPUT_LIMIT - len(TRUNCATION_MARKER)] + TRUNCATION_MARKER
|
|
15
|
+
return {"content": [{"type": "text", "text": value}]}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _error(message: str) -> dict:
|
|
19
|
+
return {"content": [{"type": "text", "text": f"error: {redactor.redact(message)}"}], "isError": True}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def wrap(fn: Callable, logger):
|
|
23
|
+
async def call(args: dict) -> dict:
|
|
24
|
+
try:
|
|
25
|
+
return await fn(args)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
logger.exception("adapter tool failed")
|
|
28
|
+
return _error(str(e))
|
|
29
|
+
|
|
30
|
+
return call
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""aws diagnostics as one read-only in-process mcp tool (AWS-001..007).
|
|
2
|
+
|
|
3
|
+
IAM is the real enforcement (AWS-005) — this adapter is the belt on those suspenders:
|
|
4
|
+
service and region allowlists from config (AWS-003) and a read-verb operation gate.
|
|
5
|
+
the harness is deployed once (host account) and reads other environments by assuming that
|
|
6
|
+
environment's configured per-environment diagnostics role per task, with session name
|
|
7
|
+
ar-<task_id> so every cloudtrail entry is task-attributable (AWS-006).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
from taskboy.adapters._util import _error, _text, wrap
|
|
17
|
+
from taskboy.models import Task
|
|
18
|
+
from taskboy.redact import redactor
|
|
19
|
+
from taskboy.store import Store
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("taskboy.aws")
|
|
22
|
+
|
|
23
|
+
READ_VERBS = re.compile(r"^(Get|List|Describe|Lookup|Search|Filter|BatchGet|Head)")
|
|
24
|
+
ASSUME_EXTERNAL_ID = "taskboy" # matches the trust policy in the shell repo's infrastructure/iam.py
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AwsReadAdapter:
|
|
28
|
+
def __init__(self, store: Store, task: Task, allowed_services: list[str], allowed_regions: list[str], role_arns: dict[str, str] | None = None):
|
|
29
|
+
self.store = store
|
|
30
|
+
self.task = task
|
|
31
|
+
self.allowed_services = allowed_services
|
|
32
|
+
self.allowed_regions = allowed_regions
|
|
33
|
+
self.role_arns = role_arns or {} # environment -> diagnostics role arn; empty = local dev default chain
|
|
34
|
+
self._credentials: dict[str, dict] = {}
|
|
35
|
+
|
|
36
|
+
async def aws_read(self, args: dict) -> dict:
|
|
37
|
+
service = str(args.get("service", "")).strip().lower()
|
|
38
|
+
operation = str(args.get("operation", "")).strip()
|
|
39
|
+
environment = str(args.get("environment", "")).strip().lower() or "staging"
|
|
40
|
+
region = str(args.get("region", "")).strip() or (self.allowed_regions[0] if self.allowed_regions else "us-east-1")
|
|
41
|
+
if service not in self.allowed_services:
|
|
42
|
+
return _error(f"service {service!r} is not on the approved list {self.allowed_services}")
|
|
43
|
+
if self.allowed_regions and region not in self.allowed_regions:
|
|
44
|
+
return _error(f"region {region!r} is not on the approved list {self.allowed_regions}")
|
|
45
|
+
if not READ_VERBS.match(operation):
|
|
46
|
+
self.store.add_event(self.task.task_id, "security_denial", {"reason": "non-read aws operation", "service": service, "operation": operation}, tool_name="mcp__aws__aws_read", is_write=True)
|
|
47
|
+
return _error(f"operation {operation!r} is not a read operation; only Get/List/Describe/... are permitted")
|
|
48
|
+
try:
|
|
49
|
+
parameters = json.loads(str(args.get("parameters") or "{}"))
|
|
50
|
+
except json.JSONDecodeError as e:
|
|
51
|
+
return _error(f"parameters must be a json object: {e}")
|
|
52
|
+
|
|
53
|
+
credentials = None
|
|
54
|
+
if self.role_arns:
|
|
55
|
+
if environment not in self.role_arns:
|
|
56
|
+
return _error(f"environment {environment!r} is not configured; available: {sorted(self.role_arns)}")
|
|
57
|
+
credentials = await asyncio.to_thread(self._credentials_for, environment)
|
|
58
|
+
self.store.add_event(self.task.task_id, "tool_call", {"aws": f"{environment}:{region} {service}.{operation}"}, tool_name="mcp__aws__aws_read", is_write=False) # AWS-006
|
|
59
|
+
result = await asyncio.to_thread(self._call, service, operation, region, parameters, credentials)
|
|
60
|
+
return _text(json.dumps(result, default=str, ensure_ascii=False))
|
|
61
|
+
|
|
62
|
+
def _credentials_for(self, environment: str) -> dict:
|
|
63
|
+
cached = self._credentials.get(environment)
|
|
64
|
+
if cached and cached["expiration_ts"] - time.time() > 300:
|
|
65
|
+
return cached
|
|
66
|
+
self._credentials[environment] = self._assume(environment)
|
|
67
|
+
return self._credentials[environment]
|
|
68
|
+
|
|
69
|
+
def _assume(self, environment: str) -> dict:
|
|
70
|
+
"""the sts seam — patched in unit tests."""
|
|
71
|
+
import boto3
|
|
72
|
+
|
|
73
|
+
sts = boto3.client("sts")
|
|
74
|
+
response = sts.assume_role(RoleArn=self.role_arns[environment], RoleSessionName=f"ar-{self.task.task_id}"[:64], ExternalId=ASSUME_EXTERNAL_ID, DurationSeconds=3600)
|
|
75
|
+
credentials = response["Credentials"]
|
|
76
|
+
redactor.register(credentials["SecretAccessKey"])
|
|
77
|
+
redactor.register(credentials["SessionToken"])
|
|
78
|
+
return {
|
|
79
|
+
"aws_access_key_id": credentials["AccessKeyId"],
|
|
80
|
+
"aws_secret_access_key": credentials["SecretAccessKey"],
|
|
81
|
+
"aws_session_token": credentials["SessionToken"],
|
|
82
|
+
"expiration_ts": credentials["Expiration"].timestamp(),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def _call(self, service: str, operation: str, region: str, parameters: dict, credentials: dict | None):
|
|
86
|
+
"""the boto3 seam — patched in unit tests. credentials never reach the model (TOL-007)."""
|
|
87
|
+
import boto3
|
|
88
|
+
from botocore import xform_name
|
|
89
|
+
|
|
90
|
+
kwargs = {key: credentials[key] for key in ("aws_access_key_id", "aws_secret_access_key", "aws_session_token")} if credentials else {}
|
|
91
|
+
client = boto3.client(service, region_name=region, **kwargs)
|
|
92
|
+
response = getattr(client, xform_name(operation))(**parameters)
|
|
93
|
+
response.pop("ResponseMetadata", None)
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_aws_server(adapter: AwsReadAdapter):
|
|
98
|
+
from claude_agent_sdk import create_sdk_mcp_server, tool
|
|
99
|
+
|
|
100
|
+
tools = [
|
|
101
|
+
tool(
|
|
102
|
+
"aws_read",
|
|
103
|
+
"Run a read-only AWS API call in a specific environment (staging|sandbox|production), e.g. environment=production service=logs operation=FilterLogEvents parameters='{\"logGroupName\": ...}'. Writes are denied by IAM and by this tool.",
|
|
104
|
+
{"environment": str, "service": str, "operation": str, "region": str, "parameters": str},
|
|
105
|
+
)(wrap(adapter.aws_read, logger)),
|
|
106
|
+
]
|
|
107
|
+
return create_sdk_mcp_server(name="aws", version="1.0.0", tools=tools)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""read-only Confluence Cloud search and page retrieval tools."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
from html.parser import HTMLParser
|
|
7
|
+
|
|
8
|
+
from taskboy.adapters._util import _error, _text, wrap
|
|
9
|
+
from taskboy.models import Task
|
|
10
|
+
from taskboy.redact import redactor
|
|
11
|
+
from taskboy.store import Store
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("taskboy.confluence")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfluenceAdapter:
|
|
17
|
+
def __init__(self, store: Store, task: Task, site: str, email: str, api_token: str, spaces: list[str]):
|
|
18
|
+
self.store = store
|
|
19
|
+
self.task = task
|
|
20
|
+
self.site = site.rstrip("/")
|
|
21
|
+
self.email = email
|
|
22
|
+
self.api_token = api_token
|
|
23
|
+
self.spaces = spaces
|
|
24
|
+
|
|
25
|
+
async def search_pages(self, args: dict) -> dict:
|
|
26
|
+
cql = str(args.get("cql") or "").strip()
|
|
27
|
+
if self.spaces:
|
|
28
|
+
allowed = ", ".join(f'"{space.replace(chr(34), chr(92) + chr(34))}"' for space in self.spaces)
|
|
29
|
+
cql = f"({cql}) AND space in ({allowed})" if cql else f"space in ({allowed})"
|
|
30
|
+
max_results = min(max(int(args.get("max_results", 10)), 1), 25)
|
|
31
|
+
self.store.add_event(self.task.task_id, "tool_call", {"cql": cql, "max_results": max_results}, tool_name="mcp__confluence__search_pages", is_write=False)
|
|
32
|
+
data = await self._request("GET", "/wiki/rest/api/content/search", params={"cql": cql, "limit": max_results, "expand": "space,version"})
|
|
33
|
+
lines = []
|
|
34
|
+
for page in data.get("results") or []:
|
|
35
|
+
space = (page.get("space") or {}).get("key")
|
|
36
|
+
lines.append(f"{page.get('id')}: {page.get('title')} [{space}] v{(page.get('version') or {}).get('number')}")
|
|
37
|
+
return _text("\n".join(lines) or "no pages matched")
|
|
38
|
+
|
|
39
|
+
async def get_page(self, args: dict) -> dict:
|
|
40
|
+
page_id = str(args.get("page_id") or "").strip()
|
|
41
|
+
self.store.add_event(self.task.task_id, "tool_call", {"page_id": page_id}, tool_name="mcp__confluence__get_page", is_write=False)
|
|
42
|
+
data = await self._request("GET", f"/wiki/rest/api/content/{page_id}", params={"expand": "body.storage,space,version"})
|
|
43
|
+
space = str((data.get("space") or {}).get("key") or "")
|
|
44
|
+
if self.spaces and space not in self.spaces:
|
|
45
|
+
return _error(f"page space {space!r} is not on the approved list {self.spaces}")
|
|
46
|
+
body = _html_to_text(str(((data.get("body") or {}).get("storage") or {}).get("value") or ""))
|
|
47
|
+
result = {"id": data.get("id"), "title": data.get("title"), "space": space, "version": (data.get("version") or {}).get("number"), "body": body}
|
|
48
|
+
return _text(json.dumps(result, ensure_ascii=False))
|
|
49
|
+
|
|
50
|
+
async def _request(self, method: str, path: str, params: dict | None = None) -> dict:
|
|
51
|
+
import aiohttp
|
|
52
|
+
|
|
53
|
+
auth = aiohttp.BasicAuth(self.email, self.api_token)
|
|
54
|
+
async with aiohttp.ClientSession(auth=auth) as session:
|
|
55
|
+
async with session.request(method, self.site + path, params=params, headers={"Accept": "application/json"}) as response:
|
|
56
|
+
if response.status >= 300:
|
|
57
|
+
body = redactor.redact(await response.text())[:300]
|
|
58
|
+
raise RuntimeError(f"confluence api {method} {path} failed: {response.status} — {body}")
|
|
59
|
+
return await response.json()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _StorageText(HTMLParser):
|
|
63
|
+
def __init__(self):
|
|
64
|
+
super().__init__(convert_charrefs=True)
|
|
65
|
+
self.parts: list[str] = []
|
|
66
|
+
|
|
67
|
+
def handle_data(self, data: str) -> None:
|
|
68
|
+
self.parts.append(data)
|
|
69
|
+
|
|
70
|
+
def handle_starttag(self, tag: str, attrs) -> None:
|
|
71
|
+
if tag in {"p", "br", "li", "h1", "h2", "h3", "h4", "h5", "h6", "tr"}:
|
|
72
|
+
self.parts.append("\n")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _html_to_text(value: str) -> str:
|
|
76
|
+
parser = _StorageText()
|
|
77
|
+
parser.feed(value)
|
|
78
|
+
return re.sub(r"\n{3,}", "\n\n", "".join(parser.parts)).strip()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_confluence_server(adapter: ConfluenceAdapter):
|
|
82
|
+
from claude_agent_sdk import create_sdk_mcp_server, tool
|
|
83
|
+
|
|
84
|
+
tools = [
|
|
85
|
+
tool("search_pages", "Search readable Confluence pages with CQL. Configured space restrictions are always applied.", {"cql": str, "max_results": int})(wrap(adapter.search_pages, logger)),
|
|
86
|
+
tool("get_page", "Read one Confluence page as plain text. Off-allowlist spaces are refused.", {"page_id": str})(wrap(adapter.get_page, logger)),
|
|
87
|
+
]
|
|
88
|
+
return create_sdk_mcp_server(name="confluence", version="1.0.0", tools=tools)
|