sid-harness 0.3.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.
- sid_harness/__init__.py +13 -0
- sid_harness/__main__.py +4 -0
- sid_harness/_native.py +1639 -0
- sid_harness/brave_proof.py +246 -0
- sid_harness/bridge.py +851 -0
- sid_harness/cli.py +937 -0
- sid_harness/client.py +161 -0
- sid_harness/companion.py +151 -0
- sid_harness/config.py +68 -0
- sid_harness/context/__init__.py +40 -0
- sid_harness/context/index.py +267 -0
- sid_harness/context/retrieval.py +74 -0
- sid_harness/editor.py +231 -0
- sid_harness/gateway.py +106 -0
- sid_harness/git_write.py +598 -0
- sid_harness/local_workspace.py +421 -0
- sid_harness/loop.py +394 -0
- sid_harness/lsp.py +598 -0
- sid_harness/policy/__init__.py +44 -0
- sid_harness/policy/repo_policy.py +626 -0
- sid_harness/receipt.py +142 -0
- sid_harness/runner.py +468 -0
- sid_harness/selftest.py +149 -0
- sid_harness/session/__init__.py +33 -0
- sid_harness/session/bounds.py +140 -0
- sid_harness/session/diff_gate.py +93 -0
- sid_harness/session/session.py +427 -0
- sid_harness/session/store.py +88 -0
- sid_harness/tools/__init__.py +52 -0
- sid_harness/tools/mcp_client.py +285 -0
- sid_harness/tools/plan.py +138 -0
- sid_harness/tools/registry.py +159 -0
- sid_harness/tools/skills.py +185 -0
- sid_harness/tools/subagents.py +112 -0
- sid_harness/tui.py +96 -0
- sid_harness/workspace.py +107 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/README.md +46 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/README.md +32 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/extension.js +395 -0
- sid_harness-0.3.0.data/data/share/sid-harness/editors/vscode/package.json +120 -0
- sid_harness-0.3.0.dist-info/METADATA +229 -0
- sid_harness-0.3.0.dist-info/RECORD +45 -0
- sid_harness-0.3.0.dist-info/WHEEL +5 -0
- sid_harness-0.3.0.dist-info/entry_points.txt +2 -0
- sid_harness-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Live Brave proof: pair -> grant write -> run -> review diff -> apply to disk.
|
|
2
|
+
|
|
3
|
+
This drives the real protocol from a page executed by the installed Brave
|
|
4
|
+
browser (headless), against a real loopback bridge. The model call is stubbed
|
|
5
|
+
only because a proof must not spend a customer's SID key; everything else —
|
|
6
|
+
origin allowlist, pairing code exchange, explicit write grant, proposal digest,
|
|
7
|
+
denylist validation, atomic write, receipt — is the production path.
|
|
8
|
+
|
|
9
|
+
Run ``python -m sid_harness.brave_proof`` or ``install/prove_brave_apply.py``.
|
|
10
|
+
Exit 0 on a real applied write. Exit 2 when Brave is not installed (honest
|
|
11
|
+
skip), 1 on any failure.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
import urllib.parse
|
|
25
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from . import bridge as bridge_mod
|
|
29
|
+
|
|
30
|
+
BRAVE_CANDIDATES = (
|
|
31
|
+
r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe",
|
|
32
|
+
r"C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe",
|
|
33
|
+
os.path.expandvars(r"%LOCALAPPDATA%\BraveSoftware\Brave-Browser\Application\brave.exe"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_brave() -> str | None:
|
|
38
|
+
for candidate in BRAVE_CANDIDATES:
|
|
39
|
+
if candidate and Path(candidate).is_file():
|
|
40
|
+
return candidate
|
|
41
|
+
found = shutil.which("brave-browser") or shutil.which("brave") or shutil.which("brave.exe")
|
|
42
|
+
return found
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
PAGE = """<!doctype html>
|
|
46
|
+
<meta charset="utf-8">
|
|
47
|
+
<title>SID bridge Brave proof</title>
|
|
48
|
+
<body>
|
|
49
|
+
<pre id="log">starting</pre>
|
|
50
|
+
<script>
|
|
51
|
+
const q = new URLSearchParams(location.search);
|
|
52
|
+
const bridge = q.get("bridge");
|
|
53
|
+
const code = q.get("code");
|
|
54
|
+
const log = (s) => { document.getElementById("log").textContent += "\\n" + s; };
|
|
55
|
+
async function jpost(path, body, headers) {
|
|
56
|
+
const resp = await fetch(bridge + path, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: Object.assign({ "Content-Type": "application/json" }, headers || {}),
|
|
59
|
+
body: JSON.stringify(body || {}),
|
|
60
|
+
});
|
|
61
|
+
let data = {};
|
|
62
|
+
try { data = await resp.json(); } catch (_) {}
|
|
63
|
+
if (!resp.ok) throw new Error(path + " HTTP " + resp.status + " " + (data.error || ""));
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
(async () => {
|
|
67
|
+
try {
|
|
68
|
+
log("pair");
|
|
69
|
+
const pair = await jpost("/pair", { code: code });
|
|
70
|
+
const auth = { "X-SID-Bridge-Token": pair.token };
|
|
71
|
+
log("grant");
|
|
72
|
+
await jpost("/grant-write", { granted: true, confirm: "enable-write" }, auth);
|
|
73
|
+
log("run");
|
|
74
|
+
const run = await jpost("/run", { task: "update index.html" }, Object.assign({
|
|
75
|
+
"X-SID-Api-Key": "proof-key",
|
|
76
|
+
}, auth));
|
|
77
|
+
if (!run.proposal) throw new Error("no proposal returned");
|
|
78
|
+
log("review " + run.proposal.digest);
|
|
79
|
+
const applied = await jpost("/apply", {
|
|
80
|
+
proposal_id: run.proposal.id,
|
|
81
|
+
digest: run.proposal.digest,
|
|
82
|
+
}, auth);
|
|
83
|
+
log("applied " + applied.written.join(","));
|
|
84
|
+
await fetch("/done?written=" + encodeURIComponent(applied.written.join(",")) +
|
|
85
|
+
"&digest=" + encodeURIComponent(run.proposal.digest));
|
|
86
|
+
} catch (e) {
|
|
87
|
+
log("error " + e.message);
|
|
88
|
+
await fetch("/fail?msg=" + encodeURIComponent(e.message || String(e)));
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
</script>
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _PageHandler(BaseHTTPRequestHandler):
|
|
96
|
+
def log_message(self, fmt, *args):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def do_GET(self): # noqa: N802
|
|
100
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
101
|
+
if parsed.path == "/":
|
|
102
|
+
body = PAGE.encode("utf-8")
|
|
103
|
+
self.send_response(200)
|
|
104
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
105
|
+
self.send_header("Content-Length", str(len(body)))
|
|
106
|
+
self.end_headers()
|
|
107
|
+
self.wfile.write(body)
|
|
108
|
+
return
|
|
109
|
+
if parsed.path in ("/done", "/fail"):
|
|
110
|
+
params = urllib.parse.parse_qs(parsed.query)
|
|
111
|
+
payload = {key: values[0] for key, values in params.items()}
|
|
112
|
+
self.server.proof_result = {"ok": parsed.path == "/done", **payload} # type: ignore[attr-defined]
|
|
113
|
+
self.server.proof_event.set() # type: ignore[attr-defined]
|
|
114
|
+
self.send_response(204)
|
|
115
|
+
self.end_headers()
|
|
116
|
+
return
|
|
117
|
+
self.send_response(404)
|
|
118
|
+
self.end_headers()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _fake_run_task(*, directory, task, api_key, base_url=None, model="auto", test_argv=None):
|
|
122
|
+
return {
|
|
123
|
+
"final_response": "Updated index.html after reading the real tree.",
|
|
124
|
+
"model": "proof-stub",
|
|
125
|
+
"steps": 2,
|
|
126
|
+
"tool_calls": 1,
|
|
127
|
+
"changed": ["index.html"],
|
|
128
|
+
"readback": {"index.html": "<h1>changed by SID through Brave</h1>\n"},
|
|
129
|
+
"diff": "--- a/index.html\n+++ b/index.html\n@@\n-<h1>hello</h1>\n+<h1>changed by SID through Brave</h1>\n",
|
|
130
|
+
"test": {"executed": False, "passed": None},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run_proof(*, timeout: float = 45.0) -> dict:
|
|
135
|
+
brave = find_brave()
|
|
136
|
+
if not brave:
|
|
137
|
+
return {"ok": False, "skipped": True, "error": "Brave is not installed"}
|
|
138
|
+
|
|
139
|
+
tmp = Path(tempfile.mkdtemp(prefix="sid-brave-proof-"))
|
|
140
|
+
workspace = tmp / "workspace"
|
|
141
|
+
workspace.mkdir()
|
|
142
|
+
(workspace / "index.html").write_text("<h1>hello</h1>\n", encoding="utf-8")
|
|
143
|
+
(workspace / "app.js").write_text("console.log('hello')\n", encoding="utf-8")
|
|
144
|
+
|
|
145
|
+
page_server = ThreadingHTTPServer(("127.0.0.1", 0), _PageHandler)
|
|
146
|
+
page_server.daemon_threads = True
|
|
147
|
+
page_server.proof_event = threading.Event() # type: ignore[attr-defined]
|
|
148
|
+
page_server.proof_result = None # type: ignore[attr-defined]
|
|
149
|
+
page_thread = threading.Thread(target=page_server.serve_forever, daemon=True)
|
|
150
|
+
page_thread.start()
|
|
151
|
+
page_origin = f"http://127.0.0.1:{page_server.server_address[1]}"
|
|
152
|
+
|
|
153
|
+
original_run_task = bridge_mod.run_task
|
|
154
|
+
bridge_mod.run_task = _fake_run_task # type: ignore[assignment]
|
|
155
|
+
httpd = bridge_mod.serve(
|
|
156
|
+
workspace,
|
|
157
|
+
port=0,
|
|
158
|
+
token="proof-token",
|
|
159
|
+
write_capable=True,
|
|
160
|
+
pair_code="proof-pair-code",
|
|
161
|
+
pair_ttl=300,
|
|
162
|
+
state_dir=tmp / "state",
|
|
163
|
+
allowed_origins=(page_origin,),
|
|
164
|
+
)
|
|
165
|
+
bridge_thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
166
|
+
bridge_thread.start()
|
|
167
|
+
bridge_url = f"http://127.0.0.1:{httpd.server_address[1]}"
|
|
168
|
+
|
|
169
|
+
profile = tmp / "brave-profile"
|
|
170
|
+
url = (
|
|
171
|
+
f"{page_origin}/?"
|
|
172
|
+
+ urllib.parse.urlencode({"bridge": bridge_url, "code": "proof-pair-code"})
|
|
173
|
+
)
|
|
174
|
+
args = [
|
|
175
|
+
brave,
|
|
176
|
+
"--headless=new",
|
|
177
|
+
"--disable-gpu",
|
|
178
|
+
"--no-first-run",
|
|
179
|
+
"--no-default-browser-check",
|
|
180
|
+
"--disable-background-networking",
|
|
181
|
+
"--disable-sync",
|
|
182
|
+
"--no-proxy-server",
|
|
183
|
+
f"--user-data-dir={profile}",
|
|
184
|
+
url,
|
|
185
|
+
]
|
|
186
|
+
proc = None
|
|
187
|
+
try:
|
|
188
|
+
proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
189
|
+
page_server.proof_event.wait(timeout) # type: ignore[attr-defined]
|
|
190
|
+
result = dict(page_server.proof_result or {}) # type: ignore[attr-defined]
|
|
191
|
+
finally:
|
|
192
|
+
if proc is not None:
|
|
193
|
+
try:
|
|
194
|
+
proc.terminate()
|
|
195
|
+
proc.wait(timeout=10)
|
|
196
|
+
except Exception:
|
|
197
|
+
try:
|
|
198
|
+
proc.kill()
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
httpd.shutdown()
|
|
202
|
+
httpd.server_close()
|
|
203
|
+
page_server.shutdown()
|
|
204
|
+
page_server.server_close()
|
|
205
|
+
bridge_mod.run_task = original_run_task # type: ignore[assignment]
|
|
206
|
+
|
|
207
|
+
if not result:
|
|
208
|
+
return {"ok": False, "error": f"Brave did not finish the flow within {timeout:g}s"}
|
|
209
|
+
disk = (workspace / "index.html").read_text(encoding="utf-8")
|
|
210
|
+
receipts = sorted((tmp / "state" / "receipts").glob("apply_*.json"))
|
|
211
|
+
result.update({
|
|
212
|
+
"brave": brave,
|
|
213
|
+
"bridge": bridge_url,
|
|
214
|
+
"origin": page_origin,
|
|
215
|
+
"disk": disk,
|
|
216
|
+
"receipts": [str(path) for path in receipts],
|
|
217
|
+
"receipt": json.loads(receipts[-1].read_text(encoding="utf-8")) if receipts else None,
|
|
218
|
+
})
|
|
219
|
+
result["ok"] = bool(
|
|
220
|
+
result.get("ok")
|
|
221
|
+
and "changed by SID through Brave" in disk
|
|
222
|
+
and receipts
|
|
223
|
+
)
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main(argv: list[str] | None = None) -> int:
|
|
228
|
+
import argparse
|
|
229
|
+
|
|
230
|
+
ap = argparse.ArgumentParser(description="Prove the Brave end-to-end apply path live.")
|
|
231
|
+
ap.add_argument("--timeout", type=float, default=45.0)
|
|
232
|
+
args = ap.parse_args(argv)
|
|
233
|
+
result = run_proof(timeout=args.timeout)
|
|
234
|
+
if result.get("skipped"):
|
|
235
|
+
print(f"brave_proof: SKIP — {result.get('error')}")
|
|
236
|
+
return 2
|
|
237
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
238
|
+
if result.get("ok"):
|
|
239
|
+
print("brave_proof: PASS — Brave applied the reviewed diff to disk")
|
|
240
|
+
return 0
|
|
241
|
+
print(f"brave_proof: FAIL — {result.get('error', 'unknown failure')}")
|
|
242
|
+
return 1
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__": # pragma: no cover
|
|
246
|
+
raise SystemExit(main())
|