devtorch-core 3.0.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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
devtorch_core.github — GitHub App + CI/CD integration (Sprint 12).
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
parse_pr_body — parse a PR body string into PRMetadata
|
|
6
|
+
PRMetadata — dataclass holding extracted governance metadata
|
|
7
|
+
build_pr_comment — render a GovernanceSummary as a GitHub PR comment
|
|
8
|
+
GovernanceSummary — dataclass holding governance state for comment rendering
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .pr_parser import PRMetadata, parse_pr_body
|
|
12
|
+
from .comment_builder import GovernanceSummary, build_pr_comment
|
|
13
|
+
from .pat import GitHubPATStore
|
|
14
|
+
from .app import GitHubAppHandler
|
|
15
|
+
from .pr_reporter import (
|
|
16
|
+
MergeGateResult,
|
|
17
|
+
build_reasoning_delta_comment,
|
|
18
|
+
check_merge_gate,
|
|
19
|
+
post_pr_comment,
|
|
20
|
+
run_pr_reporter,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"parse_pr_body",
|
|
25
|
+
"PRMetadata",
|
|
26
|
+
"build_pr_comment",
|
|
27
|
+
"GovernanceSummary",
|
|
28
|
+
"GitHubPATStore",
|
|
29
|
+
"GitHubAppHandler",
|
|
30
|
+
"MergeGateResult",
|
|
31
|
+
"build_reasoning_delta_comment",
|
|
32
|
+
"check_merge_gate",
|
|
33
|
+
"post_pr_comment",
|
|
34
|
+
"run_pr_reporter",
|
|
35
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub App webhook receiver for DevTorch.
|
|
3
|
+
|
|
4
|
+
Receives GitHub webhook events, validates HMAC signatures, and extracts
|
|
5
|
+
DevTorch governance metadata from PR bodies.
|
|
6
|
+
|
|
7
|
+
FastAPI is optional — the module degrades gracefully when it is not installed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import hmac
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import urllib.request
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
from .pr_parser import parse_pr_body
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# ── Optional FastAPI import ───────────────────────────────────────────────────
|
|
26
|
+
try:
|
|
27
|
+
from fastapi import FastAPI, Request, Response
|
|
28
|
+
|
|
29
|
+
HAS_FASTAPI = True
|
|
30
|
+
except ImportError: # pragma: no cover
|
|
31
|
+
HAS_FASTAPI = False
|
|
32
|
+
|
|
33
|
+
_WEBHOOK_SECRET_ENV = "DEVTORCH_GITHUB_WEBHOOK_SECRET"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _verify_signature(payload: bytes, signature_header: str, secret: str) -> bool:
|
|
37
|
+
"""Validate the X-Hub-Signature-256 HMAC header."""
|
|
38
|
+
if not signature_header or not signature_header.startswith("sha256="):
|
|
39
|
+
return False
|
|
40
|
+
expected = (
|
|
41
|
+
"sha256="
|
|
42
|
+
+ hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
|
|
43
|
+
)
|
|
44
|
+
return hmac.compare_digest(expected, signature_header)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_app() -> "FastAPI": # type: ignore[name-defined]
|
|
48
|
+
"""Construct and return the FastAPI application instance."""
|
|
49
|
+
if not HAS_FASTAPI:
|
|
50
|
+
raise RuntimeError(
|
|
51
|
+
"FastAPI is not installed. Install it with: pip install fastapi uvicorn"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
app = FastAPI(title="DevTorch GitHub Webhook Receiver")
|
|
55
|
+
|
|
56
|
+
@app.post("/webhook/github")
|
|
57
|
+
async def github_webhook(request: Request) -> Dict[str, Any]:
|
|
58
|
+
secret = os.environ.get(_WEBHOOK_SECRET_ENV, "")
|
|
59
|
+
if not secret:
|
|
60
|
+
logger.warning("DEVTORCH_GITHUB_WEBHOOK_SECRET is not set; ignoring event.")
|
|
61
|
+
return {"status": "webhook_secret_not_configured"}
|
|
62
|
+
|
|
63
|
+
payload = await request.body()
|
|
64
|
+
sig_header = request.headers.get("X-Hub-Signature-256", "")
|
|
65
|
+
|
|
66
|
+
if not _verify_signature(payload, sig_header, secret):
|
|
67
|
+
logger.warning("Invalid webhook signature received.")
|
|
68
|
+
return Response(content='{"error":"invalid signature"}', status_code=401, media_type="application/json") # type: ignore[return-value]
|
|
69
|
+
|
|
70
|
+
event_type = request.headers.get("X-GitHub-Event", "")
|
|
71
|
+
|
|
72
|
+
if event_type != "pull_request":
|
|
73
|
+
logger.debug("Ignoring non-PR event: %s", event_type)
|
|
74
|
+
return {"status": "ignored"}
|
|
75
|
+
|
|
76
|
+
import json as _json
|
|
77
|
+
|
|
78
|
+
event: Dict[str, Any] = _json.loads(payload)
|
|
79
|
+
action = event.get("action", "")
|
|
80
|
+
|
|
81
|
+
if action not in ("opened", "synchronize"):
|
|
82
|
+
logger.debug("Ignoring PR action: %s", action)
|
|
83
|
+
return {"status": "ignored"}
|
|
84
|
+
|
|
85
|
+
pr = event.get("pull_request", {})
|
|
86
|
+
body = pr.get("body") or ""
|
|
87
|
+
metadata = parse_pr_body(body)
|
|
88
|
+
|
|
89
|
+
logger.info(
|
|
90
|
+
"PR #%s (%s) DevTorch metadata: branch=%r commits=%d sensitivities=%d concepts=%d mcs=%s",
|
|
91
|
+
pr.get("number", "?"),
|
|
92
|
+
action,
|
|
93
|
+
metadata.branch,
|
|
94
|
+
len(metadata.commit_ids),
|
|
95
|
+
len(metadata.sensitivity_refs),
|
|
96
|
+
len(metadata.concept_refs),
|
|
97
|
+
metadata.mcs_score,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"status": "ok",
|
|
102
|
+
"metadata": {
|
|
103
|
+
"branch": metadata.branch,
|
|
104
|
+
"commit_ids": metadata.commit_ids,
|
|
105
|
+
"sensitivity_refs": metadata.sensitivity_refs,
|
|
106
|
+
"concept_refs": metadata.concept_refs,
|
|
107
|
+
"mcs_score": metadata.mcs_score,
|
|
108
|
+
"has_devtorch_section": metadata.has_devtorch_section,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return app
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Module-level app instance — only created when FastAPI is available.
|
|
116
|
+
if HAS_FASTAPI:
|
|
117
|
+
app = _build_app()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# GitHubAppHandler — installation webhooks + access token issuance
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
_APP_KEY = ".devtorch/github_app.json"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class GitHubAppHandler:
|
|
128
|
+
"""
|
|
129
|
+
Handles GitHub App installation webhooks and issues installation access tokens.
|
|
130
|
+
|
|
131
|
+
Requires env vars when get_installation_token() is called:
|
|
132
|
+
GITHUB_APP_ID — numeric GitHub App ID
|
|
133
|
+
GITHUB_APP_PRIVATE_KEY — PEM-format RSA private key (the .pem file contents)
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, backend: Any, webhook_secret: bytes) -> None:
|
|
137
|
+
self._backend = backend
|
|
138
|
+
self._webhook_secret = webhook_secret
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# Webhook handling
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def handle_webhook(self, body: bytes, signature: str, event_type: str) -> dict:
|
|
145
|
+
if not self._verify_signature(body, signature):
|
|
146
|
+
return {"status": "forbidden", "reason": "invalid signature"}
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
payload = json.loads(body)
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
return {"status": "error", "reason": "invalid JSON"}
|
|
152
|
+
|
|
153
|
+
action = payload.get("action", "")
|
|
154
|
+
installation = payload.get("installation", {})
|
|
155
|
+
installation_id = installation.get("id")
|
|
156
|
+
|
|
157
|
+
if event_type == "installation":
|
|
158
|
+
if action == "created":
|
|
159
|
+
repos = [r["full_name"] for r in payload.get("repositories", [])]
|
|
160
|
+
self._store_installation(installation_id=installation_id, repositories=repos)
|
|
161
|
+
elif action == "deleted":
|
|
162
|
+
self._clear_installation()
|
|
163
|
+
|
|
164
|
+
elif event_type == "installation_repositories":
|
|
165
|
+
existing = self._load()
|
|
166
|
+
repos_added = [r["full_name"] for r in payload.get("repositories_added", [])]
|
|
167
|
+
repos_removed = {r["full_name"] for r in payload.get("repositories_removed", [])}
|
|
168
|
+
current = set(existing.get("repositories", []))
|
|
169
|
+
current = (current | set(repos_added)) - repos_removed
|
|
170
|
+
existing["repositories"] = sorted(current)
|
|
171
|
+
self._save(existing)
|
|
172
|
+
|
|
173
|
+
return {"status": "ok"}
|
|
174
|
+
|
|
175
|
+
def _verify_signature(self, body: bytes, signature: str) -> bool:
|
|
176
|
+
if not signature.startswith("sha256="):
|
|
177
|
+
return False
|
|
178
|
+
expected = "sha256=" + hmac.new(self._webhook_secret, body, hashlib.sha256).hexdigest()
|
|
179
|
+
return hmac.compare_digest(expected, signature)
|
|
180
|
+
|
|
181
|
+
def _load(self) -> dict:
|
|
182
|
+
try:
|
|
183
|
+
return json.loads(self._backend.read_bytes(_APP_KEY))
|
|
184
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
185
|
+
return {}
|
|
186
|
+
|
|
187
|
+
def _save(self, data: dict) -> None:
|
|
188
|
+
self._backend.write_bytes(_APP_KEY, json.dumps(data, indent=2).encode())
|
|
189
|
+
|
|
190
|
+
def _store_installation(self, installation_id: int, repositories: list[str]) -> None:
|
|
191
|
+
data = self._load()
|
|
192
|
+
data["installation_id"] = installation_id
|
|
193
|
+
data["repositories"] = sorted(set(data.get("repositories", [])) | set(repositories))
|
|
194
|
+
self._save(data)
|
|
195
|
+
|
|
196
|
+
def _clear_installation(self) -> None:
|
|
197
|
+
self._save({})
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
# Installation token issuance
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def get_installation_id(self) -> Optional[int]:
|
|
204
|
+
return self._load().get("installation_id")
|
|
205
|
+
|
|
206
|
+
def get_installation_token(self) -> Optional[str]:
|
|
207
|
+
"""Issue a short-lived installation access token (valid 1 hour)."""
|
|
208
|
+
installation_id = self.get_installation_id()
|
|
209
|
+
if installation_id is None:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
jwt_token = self._make_jwt()
|
|
213
|
+
url = f"https://api.github.com/app/installations/{installation_id}/access_tokens"
|
|
214
|
+
req = urllib.request.Request(
|
|
215
|
+
url,
|
|
216
|
+
method="POST",
|
|
217
|
+
headers={
|
|
218
|
+
"Authorization": f"Bearer {jwt_token}",
|
|
219
|
+
"Accept": "application/vnd.github+json",
|
|
220
|
+
"User-Agent": "DevTorch-App",
|
|
221
|
+
"Content-Length": "0",
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
225
|
+
data = json.loads(resp.read())
|
|
226
|
+
return data.get("token")
|
|
227
|
+
|
|
228
|
+
def _make_jwt(self) -> str:
|
|
229
|
+
"""Create a signed JWT for the GitHub App (RS256, 10-minute expiry)."""
|
|
230
|
+
try:
|
|
231
|
+
import jwt as _jwt
|
|
232
|
+
except ImportError:
|
|
233
|
+
raise RuntimeError("PyJWT not installed. Install with: pip install 'devtorch-core[cloud]'")
|
|
234
|
+
|
|
235
|
+
app_id = os.environ["GITHUB_APP_ID"]
|
|
236
|
+
private_key = os.environ["GITHUB_APP_PRIVATE_KEY"]
|
|
237
|
+
|
|
238
|
+
now = int(time.time())
|
|
239
|
+
payload = {"iat": now - 60, "exp": now + 600, "iss": int(app_id)}
|
|
240
|
+
return _jwt.encode(payload, private_key, algorithm="RS256")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Builds GitHub PR comment markdown for DevTorch governance summaries.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from .pr_parser import PRMetadata
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class GovernanceSummary:
|
|
15
|
+
"""Governance state to be surfaced in a PR comment."""
|
|
16
|
+
|
|
17
|
+
branch: str
|
|
18
|
+
node_state: str
|
|
19
|
+
top_concepts: List[Tuple[str, float]] = field(default_factory=list)
|
|
20
|
+
sensitivity_count: int = 0
|
|
21
|
+
high_disclosure_count: int = 0
|
|
22
|
+
mcs_score: Optional[float] = None
|
|
23
|
+
invariants_ok: bool = True
|
|
24
|
+
lock_status: str = "unlocked"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_pr_comment(
|
|
28
|
+
summary: GovernanceSummary,
|
|
29
|
+
pr_metadata: Optional[PRMetadata] = None,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Build a GitHub PR comment in Markdown that summarises DevTorch governance state.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
summary: Governance summary data (typically from devtorch doctor output).
|
|
36
|
+
pr_metadata: Optional parsed PR metadata; included in output when provided.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A Markdown string suitable for posting as a GitHub PR comment.
|
|
40
|
+
"""
|
|
41
|
+
lines: List[str] = []
|
|
42
|
+
|
|
43
|
+
# ── Header ────────────────────────────────────────────────────────────────
|
|
44
|
+
lines.append("## 🛡️ DevTorch Governance Report")
|
|
45
|
+
lines.append("")
|
|
46
|
+
|
|
47
|
+
# ── Main table ────────────────────────────────────────────────────────────
|
|
48
|
+
top_concepts_str = (
|
|
49
|
+
", ".join(f"{name} ({conf:.2f})" for name, conf in summary.top_concepts)
|
|
50
|
+
if summary.top_concepts
|
|
51
|
+
else "—"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
mcs_display = f"{summary.mcs_score:.2f}" if summary.mcs_score is not None else "N/A"
|
|
55
|
+
invariants_display = "✅ OK" if summary.invariants_ok else "❌ FAILED"
|
|
56
|
+
|
|
57
|
+
lines.append("| Field | Value |")
|
|
58
|
+
lines.append("|---|---|")
|
|
59
|
+
lines.append(f"| **Branch** | `{summary.branch}` |")
|
|
60
|
+
lines.append(f"| **Node State** | {summary.node_state} |")
|
|
61
|
+
lines.append(f"| **Top Concepts** | {top_concepts_str} |")
|
|
62
|
+
lines.append(f"| **Sensitivity Events** | {summary.sensitivity_count} |")
|
|
63
|
+
lines.append(f"| **MCS Score** | {mcs_display} |")
|
|
64
|
+
lines.append(f"| **Invariants** | {invariants_display} |")
|
|
65
|
+
lines.append(f"| **Lock Status** | {summary.lock_status} |")
|
|
66
|
+
lines.append("")
|
|
67
|
+
|
|
68
|
+
# ── Warnings ──────────────────────────────────────────────────────────────
|
|
69
|
+
if not summary.invariants_ok:
|
|
70
|
+
lines.append("> ⚠️ **Invariant check failed.** One or more DevTorch invariants")
|
|
71
|
+
lines.append("> (I1 commit-backed, I3 semantic grounding) did not pass.")
|
|
72
|
+
lines.append("> Review the invariant trace before merging.")
|
|
73
|
+
lines.append("")
|
|
74
|
+
|
|
75
|
+
if summary.high_disclosure_count > 0:
|
|
76
|
+
lines.append(
|
|
77
|
+
f"> 🔒 **{summary.high_disclosure_count} high-disclosure sensitivity"
|
|
78
|
+
f" event(s) detected.** Ensure downstream consumers comply with"
|
|
79
|
+
f" disclosure policy before merging."
|
|
80
|
+
)
|
|
81
|
+
lines.append("")
|
|
82
|
+
|
|
83
|
+
if summary.lock_status and summary.lock_status.lower() not in ("unlocked", ""):
|
|
84
|
+
lines.append(
|
|
85
|
+
f"> 🔐 **Consensus lock is active** (`{summary.lock_status}`). "
|
|
86
|
+
"Merge may be restricted by governance policy."
|
|
87
|
+
)
|
|
88
|
+
lines.append("")
|
|
89
|
+
|
|
90
|
+
# ── PR metadata section (optional) ────────────────────────────────────────
|
|
91
|
+
if pr_metadata is not None and pr_metadata.has_devtorch_section:
|
|
92
|
+
lines.append("### DevTorch PR Metadata")
|
|
93
|
+
lines.append("")
|
|
94
|
+
if pr_metadata.sensitivity_refs:
|
|
95
|
+
lines.append(
|
|
96
|
+
f"- **Sensitivity refs**: {', '.join(pr_metadata.sensitivity_refs)}"
|
|
97
|
+
)
|
|
98
|
+
if pr_metadata.concept_refs:
|
|
99
|
+
lines.append(f"- **Concept refs**: {', '.join(pr_metadata.concept_refs)}")
|
|
100
|
+
if pr_metadata.commit_ids:
|
|
101
|
+
short_ids = [c[:7] for c in pr_metadata.commit_ids]
|
|
102
|
+
lines.append(f"- **Commits**: {', '.join(short_ids)}")
|
|
103
|
+
if pr_metadata.mcs_score is not None:
|
|
104
|
+
lines.append(f"- **PR MCS Score**: {pr_metadata.mcs_score:.2f}")
|
|
105
|
+
lines.append("")
|
|
106
|
+
|
|
107
|
+
# ── Footer ────────────────────────────────────────────────────────────────
|
|
108
|
+
lines.append(
|
|
109
|
+
"---\n_Generated by DevTorch | "
|
|
110
|
+
"[docs](https://github.com/flotorch-ai/devtorch)_"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_PATS_KEY = ".devtorch/github_pats.json"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _fernet_key(raw: bytes) -> bytes:
|
|
12
|
+
"""Derive a 32-byte Fernet key from arbitrary bytes via SHA-256."""
|
|
13
|
+
import hashlib
|
|
14
|
+
digest = hashlib.sha256(raw).digest() # always 32 bytes
|
|
15
|
+
return base64.urlsafe_b64encode(digest)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitHubPATStore:
|
|
19
|
+
"""
|
|
20
|
+
Stores GitHub Personal Access Tokens encrypted with Fernet (AES-128-CBC + HMAC).
|
|
21
|
+
PATs are stored at {prefix}/.devtorch/github_pats.json in the backend.
|
|
22
|
+
|
|
23
|
+
encryption_key: any bytes — hashed to 32 bytes via SHA-256 internally.
|
|
24
|
+
Generate a stable key with: python -c "import os; print(os.urandom(32).hex())"
|
|
25
|
+
Store as DEVTORCH_PAT_ENCRYPTION_KEY env var (hex string).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, backend: Any, encryption_key: bytes) -> None:
|
|
29
|
+
self._backend = backend
|
|
30
|
+
self._fernet_key = _fernet_key(encryption_key)
|
|
31
|
+
|
|
32
|
+
def _fernet(self):
|
|
33
|
+
from cryptography.fernet import Fernet
|
|
34
|
+
return Fernet(self._fernet_key)
|
|
35
|
+
|
|
36
|
+
def _load(self) -> dict:
|
|
37
|
+
try:
|
|
38
|
+
return json.loads(self._backend.read_bytes(_PATS_KEY))
|
|
39
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
def _save(self, data: dict) -> None:
|
|
43
|
+
self._backend.write_bytes(_PATS_KEY, json.dumps(data, indent=2).encode())
|
|
44
|
+
|
|
45
|
+
def store_pat(self, repo_id: str, pat: str, github_repo: str) -> None:
|
|
46
|
+
"""Encrypt and store the PAT for a repo_id → github_repo mapping."""
|
|
47
|
+
data = self._load()
|
|
48
|
+
encrypted = self._fernet().encrypt(pat.encode()).decode()
|
|
49
|
+
data[repo_id] = {
|
|
50
|
+
"github_repo": github_repo,
|
|
51
|
+
"pat_encrypted": encrypted,
|
|
52
|
+
}
|
|
53
|
+
self._save(data)
|
|
54
|
+
|
|
55
|
+
def get_pat(self, repo_id: str) -> Optional[dict[str, str]]:
|
|
56
|
+
"""Return {"pat": "<decrypted>", "github_repo": "<owner/repo>"} or None."""
|
|
57
|
+
data = self._load()
|
|
58
|
+
entry = data.get(repo_id)
|
|
59
|
+
if not entry:
|
|
60
|
+
return None
|
|
61
|
+
try:
|
|
62
|
+
decrypted = self._fernet().decrypt(entry["pat_encrypted"].encode()).decode()
|
|
63
|
+
except Exception:
|
|
64
|
+
return None
|
|
65
|
+
return {"pat": decrypted, "github_repo": entry["github_repo"]}
|
|
66
|
+
|
|
67
|
+
def list_repos(self) -> list[str]:
|
|
68
|
+
"""Return all repo_ids with stored PATs."""
|
|
69
|
+
return list(self._load().keys())
|
|
70
|
+
|
|
71
|
+
def delete_pat(self, repo_id: str) -> None:
|
|
72
|
+
"""Remove the PAT for a repo_id."""
|
|
73
|
+
data = self._load()
|
|
74
|
+
if repo_id in data:
|
|
75
|
+
del data[repo_id]
|
|
76
|
+
self._save(data)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parses GitHub PR bodies for DevTorch governance metadata.
|
|
3
|
+
Extracts structured data without requiring a .GCC/ directory.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PRMetadata:
|
|
15
|
+
"""Structured DevTorch governance metadata extracted from a PR body."""
|
|
16
|
+
|
|
17
|
+
branch: str = ""
|
|
18
|
+
commit_ids: List[str] = field(default_factory=list)
|
|
19
|
+
sensitivity_refs: List[str] = field(default_factory=list)
|
|
20
|
+
concept_refs: List[str] = field(default_factory=list)
|
|
21
|
+
mcs_score: Optional[float] = None
|
|
22
|
+
has_devtorch_section: bool = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Patterns
|
|
26
|
+
_BRANCH_PATTERN = re.compile(r"Branch:\s*`([^`]+)`")
|
|
27
|
+
_COMMIT_PATTERN = re.compile(r"\b([0-9a-f]{40})\b", re.IGNORECASE)
|
|
28
|
+
_SENSITIVITY_PATTERN = re.compile(r"\[SENSITIVITY:([^\]]+)\]")
|
|
29
|
+
_CONCEPT_PATTERN = re.compile(r"\[CONCEPT:([^\]]+)\]")
|
|
30
|
+
_MCS_PATTERN = re.compile(r"MCS:\s*([\d]+\.[\d]+)")
|
|
31
|
+
_DEVTORCH_HEADING_PATTERN = re.compile(r"^#{2,3}\s+DevTorch\b", re.MULTILINE)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_pr_body(body: str) -> PRMetadata:
|
|
35
|
+
"""
|
|
36
|
+
Parse a GitHub PR body string and extract DevTorch governance metadata.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
body: The full text of the PR description/body.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A PRMetadata instance populated from whatever was found in the body.
|
|
43
|
+
Fields default to empty lists, None, or False when not found.
|
|
44
|
+
"""
|
|
45
|
+
if not body:
|
|
46
|
+
return PRMetadata()
|
|
47
|
+
|
|
48
|
+
# Branch
|
|
49
|
+
branch = ""
|
|
50
|
+
branch_match = _BRANCH_PATTERN.search(body)
|
|
51
|
+
if branch_match:
|
|
52
|
+
branch = branch_match.group(1)
|
|
53
|
+
|
|
54
|
+
# Commit IDs (40-char lowercase hex strings)
|
|
55
|
+
commit_ids = list(dict.fromkeys(m.lower() for m in _COMMIT_PATTERN.findall(body)))
|
|
56
|
+
|
|
57
|
+
# Sensitivity refs
|
|
58
|
+
sensitivity_refs = list(dict.fromkeys(_SENSITIVITY_PATTERN.findall(body)))
|
|
59
|
+
|
|
60
|
+
# Concept refs
|
|
61
|
+
concept_refs = list(dict.fromkeys(_CONCEPT_PATTERN.findall(body)))
|
|
62
|
+
|
|
63
|
+
# MCS score
|
|
64
|
+
mcs_score: Optional[float] = None
|
|
65
|
+
mcs_match = _MCS_PATTERN.search(body)
|
|
66
|
+
if mcs_match:
|
|
67
|
+
try:
|
|
68
|
+
mcs_score = float(mcs_match.group(1))
|
|
69
|
+
except ValueError:
|
|
70
|
+
mcs_score = None
|
|
71
|
+
|
|
72
|
+
# DevTorch section heading
|
|
73
|
+
has_devtorch_section = bool(_DEVTORCH_HEADING_PATTERN.search(body))
|
|
74
|
+
|
|
75
|
+
return PRMetadata(
|
|
76
|
+
branch=branch,
|
|
77
|
+
commit_ids=commit_ids,
|
|
78
|
+
sensitivity_refs=sensitivity_refs,
|
|
79
|
+
concept_refs=concept_refs,
|
|
80
|
+
mcs_score=mcs_score,
|
|
81
|
+
has_devtorch_section=has_devtorch_section,
|
|
82
|
+
)
|