PTSIP 0.3.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.
- ptsip/__init__.py +3 -0
- ptsip/__main__.py +4 -0
- ptsip/app/__init__.py +5 -0
- ptsip/app/client.py +53 -0
- ptsip/app/github_client.py +200 -0
- ptsip/app/server.py +130 -0
- ptsip/app/service.py +285 -0
- ptsip/app/store.py +233 -0
- ptsip/artifact_evidence.py +150 -0
- ptsip/build_resolution.py +298 -0
- ptsip/clarification/__init__.py +6 -0
- ptsip/clarification/generator.py +120 -0
- ptsip/clarification/generator_core.py +98 -0
- ptsip/clarification/i18n.py +106 -0
- ptsip/clarification/model.py +33 -0
- ptsip/clarification/render.py +78 -0
- ptsip/clarification/resolution/__init__.py +29 -0
- ptsip/clarification/resolution/model.py +33 -0
- ptsip/clarification/resolution/parser.py +56 -0
- ptsip/clarification/resolution/profile_projection.py +198 -0
- ptsip/clarification/resolution/resolver.py +30 -0
- ptsip/clarification/transports/__init__.py +1 -0
- ptsip/clarification/transports/github_issue.py +162 -0
- ptsip/cli.py +402 -0
- ptsip/conformance.py +531 -0
- ptsip/conformance_audit.py +143 -0
- ptsip/conformance_engine.py +396 -0
- ptsip/constants.py +9 -0
- ptsip/doctor.py +33 -0
- ptsip/inspection/__init__.py +1 -0
- ptsip/inspection/components.py +74 -0
- ptsip/inspection/dependencies.py +496 -0
- ptsip/inspection/dependencies_030.py +69 -0
- ptsip/inspection/dotnet.py +188 -0
- ptsip/inspection/go.py +157 -0
- ptsip/inspection/inventory.py +161 -0
- ptsip/inspection/javascript.py +376 -0
- ptsip/inspection/lexing.py +121 -0
- ptsip/inspection/source_adapters.py +60 -0
- ptsip/lifecycle_evidence.py +245 -0
- ptsip/model.py +105 -0
- ptsip/pilot/__init__.py +1 -0
- ptsip/pilot/runner.py +170 -0
- ptsip/repository/__init__.py +1 -0
- ptsip/repository/discover.py +68 -0
- ptsip/repository/remote.py +54 -0
- ptsip/repository/snapshot.py +173 -0
- ptsip/review_evidence.py +406 -0
- ptsip/spec_identity.py +22 -0
- ptsip/specdata/ptsip-agent-classification.schema.json +35 -0
- ptsip/specdata/ptsip-artifact-evidence.schema.json +68 -0
- ptsip/specdata/ptsip-diagnostic.schema.json +29 -0
- ptsip/specdata/ptsip-profile.schema.json +310 -0
- ptsip/specdata/ptsip-registry.yaml +189 -0
- ptsip/storage/__init__.py +1 -0
- ptsip/storage/local_state.py +32 -0
- ptsip/validation/__init__.py +1 -0
- ptsip/validation/components.py +164 -0
- ptsip/validation/profile.py +172 -0
- ptsip/validation/rules.py +163 -0
- ptsip-0.3.1.dist-info/METADATA +338 -0
- ptsip-0.3.1.dist-info/RECORD +66 -0
- ptsip-0.3.1.dist-info/WHEEL +5 -0
- ptsip-0.3.1.dist-info/entry_points.txt +3 -0
- ptsip-0.3.1.dist-info/licenses/LICENSE +201 -0
- ptsip-0.3.1.dist-info/top_level.txt +1 -0
ptsip/__init__.py
ADDED
ptsip/__main__.py
ADDED
ptsip/app/__init__.py
ADDED
ptsip/app/client.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ControlPlaneError(RuntimeError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ControlPlaneClient:
|
|
15
|
+
def __init__(self, base_url: str | None = None, token: str | None = None):
|
|
16
|
+
self.base_url = (base_url or os.environ.get("PTSIP_CONTROL_PLANE_URL") or "").rstrip("/")
|
|
17
|
+
self.token = token or os.environ.get("PTSIP_CONTROL_PLANE_TOKEN")
|
|
18
|
+
if not self.base_url:
|
|
19
|
+
raise ControlPlaneError("PTSIP control plane URL is not configured; use --control-plane or PTSIP_CONTROL_PLANE_URL")
|
|
20
|
+
if not self.token:
|
|
21
|
+
raise ControlPlaneError("PTSIP_CONTROL_PLANE_TOKEN is required")
|
|
22
|
+
|
|
23
|
+
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
24
|
+
request = urllib.request.Request(
|
|
25
|
+
self.base_url + path,
|
|
26
|
+
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
|
27
|
+
method="POST",
|
|
28
|
+
headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"},
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
32
|
+
raw = response.read()
|
|
33
|
+
except urllib.error.HTTPError as exc:
|
|
34
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
35
|
+
raise ControlPlaneError(f"Control plane request failed: HTTP {exc.code}: {detail}") from exc
|
|
36
|
+
except OSError as exc:
|
|
37
|
+
raise ControlPlaneError(f"Control plane request failed: {exc}") from exc
|
|
38
|
+
parsed = json.loads(raw.decode("utf-8"))
|
|
39
|
+
if not isinstance(parsed, dict):
|
|
40
|
+
raise ControlPlaneError("Control plane returned a non-object response")
|
|
41
|
+
return parsed
|
|
42
|
+
|
|
43
|
+
def gate(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
44
|
+
return self._post("/v1/gate", payload)
|
|
45
|
+
|
|
46
|
+
def decision(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
47
|
+
return self._post("/v1/decision", payload)
|
|
48
|
+
|
|
49
|
+
def resolve(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
50
|
+
return self._post("/v1/resolve", payload)
|
|
51
|
+
|
|
52
|
+
def application(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
53
|
+
return self._post("/v1/application", payload)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class GitHubIssue:
|
|
17
|
+
number: int
|
|
18
|
+
url: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitHubAPIError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GitHubAppClient:
|
|
26
|
+
def __init__(self, app_id: str | None = None, private_key: str | None = None, api_url: str | None = None):
|
|
27
|
+
self.app_id = app_id or os.environ.get("PTSIP_GITHUB_APP_ID")
|
|
28
|
+
configured_key = private_key or os.environ.get("PTSIP_GITHUB_PRIVATE_KEY")
|
|
29
|
+
key_path = os.environ.get("PTSIP_GITHUB_PRIVATE_KEY_PATH")
|
|
30
|
+
if configured_key is None and key_path:
|
|
31
|
+
configured_key = Path(key_path).expanduser().read_text(encoding="utf-8")
|
|
32
|
+
self.private_key = configured_key
|
|
33
|
+
self.api_url = (api_url or os.environ.get("PTSIP_GITHUB_API_URL") or "https://api.github.com").rstrip("/")
|
|
34
|
+
self._tokens: dict[int, tuple[str, float]] = {}
|
|
35
|
+
|
|
36
|
+
def _app_jwt(self) -> str:
|
|
37
|
+
if not self.app_id or not self.private_key:
|
|
38
|
+
raise GitHubAPIError("GitHub App credentials are not configured")
|
|
39
|
+
try:
|
|
40
|
+
import jwt
|
|
41
|
+
except ImportError as exc:
|
|
42
|
+
raise GitHubAPIError("PyJWT[crypto] is required for GitHub App authentication; install ptsip[github-app]") from exc
|
|
43
|
+
now = int(time.time())
|
|
44
|
+
token = jwt.encode(
|
|
45
|
+
{"iat": now - 60, "exp": now + 9 * 60, "iss": self.app_id},
|
|
46
|
+
self.private_key,
|
|
47
|
+
algorithm="RS256",
|
|
48
|
+
)
|
|
49
|
+
return str(token)
|
|
50
|
+
|
|
51
|
+
def _request(
|
|
52
|
+
self,
|
|
53
|
+
method: str,
|
|
54
|
+
path: str,
|
|
55
|
+
token: str,
|
|
56
|
+
payload: dict[str, Any] | None = None,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
data = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
59
|
+
request = urllib.request.Request(
|
|
60
|
+
self.api_url + path,
|
|
61
|
+
data=data,
|
|
62
|
+
method=method,
|
|
63
|
+
headers={
|
|
64
|
+
"Accept": "application/vnd.github+json",
|
|
65
|
+
"Authorization": f"Bearer {token}",
|
|
66
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
"User-Agent": "ptsip-github-app/0.3.1",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
try:
|
|
72
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
73
|
+
raw = response.read()
|
|
74
|
+
except urllib.error.HTTPError as exc:
|
|
75
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
76
|
+
raise GitHubAPIError(f"GitHub API {method} {path} failed: HTTP {exc.code}: {detail}") from exc
|
|
77
|
+
if not raw:
|
|
78
|
+
return {}
|
|
79
|
+
parsed = json.loads(raw.decode("utf-8"))
|
|
80
|
+
if not isinstance(parsed, dict):
|
|
81
|
+
raise GitHubAPIError("GitHub API returned a non-object response")
|
|
82
|
+
return parsed
|
|
83
|
+
|
|
84
|
+
def repository_installation(self, repository: str) -> int:
|
|
85
|
+
if repository.count("/") != 1:
|
|
86
|
+
raise GitHubAPIError("repository must use owner/repository form")
|
|
87
|
+
owner, repo = repository.split("/", 1)
|
|
88
|
+
payload = self._request(
|
|
89
|
+
"GET",
|
|
90
|
+
f"/repos/{urllib.parse.quote(owner)}/{urllib.parse.quote(repo)}/installation",
|
|
91
|
+
self._app_jwt(),
|
|
92
|
+
)
|
|
93
|
+
installation_id = payload.get("id")
|
|
94
|
+
if not isinstance(installation_id, int):
|
|
95
|
+
raise GitHubAPIError("GitHub did not return a repository installation id")
|
|
96
|
+
return installation_id
|
|
97
|
+
|
|
98
|
+
def installation_token(self, installation_id: int) -> str:
|
|
99
|
+
cached = self._tokens.get(installation_id)
|
|
100
|
+
now = time.time()
|
|
101
|
+
if cached and cached[1] - 60 > now:
|
|
102
|
+
return cached[0]
|
|
103
|
+
payload = self._request(
|
|
104
|
+
"POST",
|
|
105
|
+
f"/app/installations/{installation_id}/access_tokens",
|
|
106
|
+
self._app_jwt(),
|
|
107
|
+
{},
|
|
108
|
+
)
|
|
109
|
+
token = str(payload.get("token", ""))
|
|
110
|
+
if not token:
|
|
111
|
+
raise GitHubAPIError("GitHub did not return an installation token")
|
|
112
|
+
self._tokens[installation_id] = (token, now + 50 * 60)
|
|
113
|
+
return token
|
|
114
|
+
|
|
115
|
+
def create_issue(self, repository: str, installation_id: int, title: str, body: str) -> GitHubIssue:
|
|
116
|
+
token = self.installation_token(installation_id)
|
|
117
|
+
payload = self._request("POST", f"/repos/{repository}/issues", token, {"title": title, "body": body})
|
|
118
|
+
return GitHubIssue(int(payload["number"]), str(payload["html_url"]))
|
|
119
|
+
|
|
120
|
+
def update_issue_state(self, repository: str, installation_id: int, issue_number: int, state: str) -> None:
|
|
121
|
+
token = self.installation_token(installation_id)
|
|
122
|
+
self._request("PATCH", f"/repos/{repository}/issues/{issue_number}", token, {"state": state})
|
|
123
|
+
|
|
124
|
+
def add_issue_comment(self, repository: str, installation_id: int, issue_number: int, body: str) -> None:
|
|
125
|
+
token = self.installation_token(installation_id)
|
|
126
|
+
self._request("POST", f"/repos/{repository}/issues/{issue_number}/comments", token, {"body": body})
|
|
127
|
+
|
|
128
|
+
def permission(self, repository: str, installation_id: int, username: str) -> str:
|
|
129
|
+
token = self.installation_token(installation_id)
|
|
130
|
+
payload = self._request(
|
|
131
|
+
"GET", f"/repos/{repository}/collaborators/{urllib.parse.quote(username)}/permission", token
|
|
132
|
+
)
|
|
133
|
+
return str(payload.get("permission", "none"))
|
|
134
|
+
|
|
135
|
+
def branch_head(self, repository: str, installation_id: int, branch: str) -> str:
|
|
136
|
+
token = self.installation_token(installation_id)
|
|
137
|
+
encoded = urllib.parse.quote(branch, safe="")
|
|
138
|
+
payload = self._request("GET", f"/repos/{repository}/git/ref/heads/{encoded}", token)
|
|
139
|
+
obj = payload.get("object")
|
|
140
|
+
if not isinstance(obj, dict) or not obj.get("sha"):
|
|
141
|
+
raise GitHubAPIError("GitHub branch ref did not contain a SHA")
|
|
142
|
+
return str(obj["sha"])
|
|
143
|
+
|
|
144
|
+
def file_text(self, repository: str, installation_id: int, path: str, ref: str) -> str | None:
|
|
145
|
+
token = self.installation_token(installation_id)
|
|
146
|
+
encoded_path = "/".join(urllib.parse.quote(part) for part in path.split("/"))
|
|
147
|
+
try:
|
|
148
|
+
payload = self._request(
|
|
149
|
+
"GET",
|
|
150
|
+
f"/repos/{repository}/contents/{encoded_path}?ref={urllib.parse.quote(ref, safe='')}",
|
|
151
|
+
token,
|
|
152
|
+
)
|
|
153
|
+
except GitHubAPIError as exc:
|
|
154
|
+
if "HTTP 404" in str(exc):
|
|
155
|
+
return None
|
|
156
|
+
raise
|
|
157
|
+
content = payload.get("content")
|
|
158
|
+
if not isinstance(content, str):
|
|
159
|
+
return None
|
|
160
|
+
return base64.b64decode(content.encode("ascii")).decode("utf-8-sig")
|
|
161
|
+
|
|
162
|
+
def commit_file_at_parent(
|
|
163
|
+
self,
|
|
164
|
+
repository: str,
|
|
165
|
+
installation_id: int,
|
|
166
|
+
branch: str,
|
|
167
|
+
parent_sha: str,
|
|
168
|
+
path: str,
|
|
169
|
+
content: str,
|
|
170
|
+
message: str,
|
|
171
|
+
) -> str:
|
|
172
|
+
token = self.installation_token(installation_id)
|
|
173
|
+
parent = self._request("GET", f"/repos/{repository}/git/commits/{parent_sha}", token)
|
|
174
|
+
tree = parent.get("tree")
|
|
175
|
+
if not isinstance(tree, dict) or not tree.get("sha"):
|
|
176
|
+
raise GitHubAPIError("Parent commit did not contain a tree SHA")
|
|
177
|
+
created_tree = self._request(
|
|
178
|
+
"POST",
|
|
179
|
+
f"/repos/{repository}/git/trees",
|
|
180
|
+
token,
|
|
181
|
+
{
|
|
182
|
+
"base_tree": str(tree["sha"]),
|
|
183
|
+
"tree": [{"path": path, "mode": "100644", "type": "blob", "content": content}],
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
commit = self._request(
|
|
187
|
+
"POST",
|
|
188
|
+
f"/repos/{repository}/git/commits",
|
|
189
|
+
token,
|
|
190
|
+
{"message": message, "tree": str(created_tree["sha"]), "parents": [parent_sha]},
|
|
191
|
+
)
|
|
192
|
+
commit_sha = str(commit["sha"])
|
|
193
|
+
encoded_branch = urllib.parse.quote(branch, safe="")
|
|
194
|
+
self._request(
|
|
195
|
+
"PATCH",
|
|
196
|
+
f"/repos/{repository}/git/refs/heads/{encoded_branch}",
|
|
197
|
+
token,
|
|
198
|
+
{"sha": commit_sha, "force": False},
|
|
199
|
+
)
|
|
200
|
+
return commit_sha
|
ptsip/app/server.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .github_client import GitHubAppClient
|
|
13
|
+
from .service import DecisionService
|
|
14
|
+
from .store import DecisionStore
|
|
15
|
+
|
|
16
|
+
MAX_REQUEST_BYTES = 1024 * 1024
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
20
|
+
server_version = "PTSIPControlPlane/0.3.1"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def service(self) -> DecisionService:
|
|
24
|
+
return self.server.service # type: ignore[attr-defined]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def agent_token(self) -> str:
|
|
28
|
+
return self.server.agent_token # type: ignore[attr-defined]
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def webhook_secret(self) -> bytes:
|
|
32
|
+
return self.server.webhook_secret # type: ignore[attr-defined]
|
|
33
|
+
|
|
34
|
+
def _json(self, code: int, payload: dict[str, object]) -> None:
|
|
35
|
+
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
36
|
+
self.send_response(code)
|
|
37
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
38
|
+
self.send_header("Content-Length", str(len(raw)))
|
|
39
|
+
self.end_headers()
|
|
40
|
+
self.wfile.write(raw)
|
|
41
|
+
|
|
42
|
+
def _body(self) -> bytes:
|
|
43
|
+
length = int(self.headers.get("Content-Length", "0"))
|
|
44
|
+
if length < 0 or length > MAX_REQUEST_BYTES:
|
|
45
|
+
raise ValueError("request body is too large")
|
|
46
|
+
return self.rfile.read(length)
|
|
47
|
+
|
|
48
|
+
def _authorized(self) -> bool:
|
|
49
|
+
header = self.headers.get("Authorization", "")
|
|
50
|
+
return bool(self.agent_token) and hmac.compare_digest(header, f"Bearer {self.agent_token}")
|
|
51
|
+
|
|
52
|
+
def do_GET(self) -> None:
|
|
53
|
+
if self.path == "/healthz":
|
|
54
|
+
self._json(200, {"status": "ok", "service": "ptsip-control-plane", "version": "0.3.1"})
|
|
55
|
+
return
|
|
56
|
+
self._json(404, {"error": "not found"})
|
|
57
|
+
|
|
58
|
+
def do_POST(self) -> None:
|
|
59
|
+
try:
|
|
60
|
+
raw = self._body()
|
|
61
|
+
if self.path == "/github/webhook":
|
|
62
|
+
signature = self.headers.get("X-Hub-Signature-256", "")
|
|
63
|
+
expected = "sha256=" + hmac.new(self.webhook_secret, raw, hashlib.sha256).hexdigest()
|
|
64
|
+
if not self.webhook_secret or not hmac.compare_digest(signature, expected):
|
|
65
|
+
self._json(401, {"error": "invalid webhook signature"})
|
|
66
|
+
return
|
|
67
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
68
|
+
if not isinstance(payload, dict):
|
|
69
|
+
raise ValueError("webhook payload must be an object")
|
|
70
|
+
event = self.headers.get("X-GitHub-Event", "")
|
|
71
|
+
self.service.register_installation_event(payload)
|
|
72
|
+
result = self.service.issue_comment(payload) if event == "issue_comment" else {"status": "ACCEPTED"}
|
|
73
|
+
self._json(200, result)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if not self._authorized():
|
|
77
|
+
self._json(401, {"error": "unauthorized"})
|
|
78
|
+
return
|
|
79
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
80
|
+
if not isinstance(payload, dict):
|
|
81
|
+
raise ValueError("request body must be an object")
|
|
82
|
+
if self.path == "/v1/gate":
|
|
83
|
+
self._json(200, self.service.gate(payload))
|
|
84
|
+
elif self.path == "/v1/decision":
|
|
85
|
+
self._json(200, self.service.decision(payload))
|
|
86
|
+
elif self.path == "/v1/resolve":
|
|
87
|
+
self._json(200, self.service.resolve_agent(payload))
|
|
88
|
+
elif self.path == "/v1/application":
|
|
89
|
+
self._json(200, self.service.application(payload))
|
|
90
|
+
else:
|
|
91
|
+
self._json(404, {"error": "not found"})
|
|
92
|
+
except (KeyError, ValueError) as exc:
|
|
93
|
+
self._json(400, {"error": str(exc)})
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
self._json(500, {"error": str(exc)})
|
|
96
|
+
|
|
97
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
98
|
+
if os.environ.get("PTSIP_APP_QUIET") != "1":
|
|
99
|
+
super().log_message(format, *args)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parser() -> argparse.ArgumentParser:
|
|
103
|
+
parser = argparse.ArgumentParser(prog="ptsip-app", description="PTSIP GitHub App decision control plane")
|
|
104
|
+
parser.add_argument("--host", default=os.environ.get("PTSIP_APP_HOST", "127.0.0.1"))
|
|
105
|
+
parser.add_argument("--port", type=int, default=int(os.environ.get("PTSIP_APP_PORT", "8080")))
|
|
106
|
+
parser.add_argument("--db", default=os.environ.get("PTSIP_APP_DB", "ptsip-control-plane.sqlite3"))
|
|
107
|
+
return parser
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(argv: list[str] | None = None) -> int:
|
|
111
|
+
args = _parser().parse_args(argv)
|
|
112
|
+
agent_token = os.environ.get("PTSIP_CONTROL_PLANE_TOKEN", "")
|
|
113
|
+
webhook_secret = os.environ.get("PTSIP_GITHUB_WEBHOOK_SECRET", "")
|
|
114
|
+
if not agent_token:
|
|
115
|
+
raise SystemExit("PTSIP_CONTROL_PLANE_TOKEN is required")
|
|
116
|
+
if not webhook_secret:
|
|
117
|
+
raise SystemExit("PTSIP_GITHUB_WEBHOOK_SECRET is required")
|
|
118
|
+
store = DecisionStore(Path(args.db))
|
|
119
|
+
service = DecisionService(store, GitHubAppClient())
|
|
120
|
+
server = ThreadingHTTPServer((args.host, args.port), _Handler)
|
|
121
|
+
server.service = service # type: ignore[attr-defined]
|
|
122
|
+
server.agent_token = agent_token # type: ignore[attr-defined]
|
|
123
|
+
server.webhook_secret = webhook_secret.encode("utf-8") # type: ignore[attr-defined]
|
|
124
|
+
try:
|
|
125
|
+
server.serve_forever()
|
|
126
|
+
except KeyboardInterrupt:
|
|
127
|
+
pass
|
|
128
|
+
finally:
|
|
129
|
+
server.server_close()
|
|
130
|
+
return 0
|