loki-mode 11.1.0 → 11.2.1
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.
- package/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/skill-link-heal.sh +54 -0
- package/autonomy/loki +60 -6
- package/autonomy/run.sh +11 -3
- package/bin/loki +8 -0
- package/bin/loki-postinstall.js +3 -1
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
- package/docs/v10/DECISIONS.md +4 -0
- package/docs/v10/FAILURE-CLASSES.md +27 -1
- package/docs/v10/GUIDE.md +85 -0
- package/docs/v11/ROUTER-1.md +15 -13
- package/loki-ts/dist/loki.js +687 -680
- package/mcp/__init__.py +1 -1
- package/mcp/cp_tools.py +333 -0
- package/mcp/server.py +21 -0
- package/mcp/tests/_read_only_wall_probe.py +10 -0
- package/mcp/tests/fixtures/cp_api/artifact_report.md +3 -0
- package/mcp/tests/fixtures/cp_api/cost.json +1 -0
- package/mcp/tests/fixtures/cp_api/repos.json +1 -0
- package/mcp/tests/fixtures/cp_api/run_alpha.json +1 -0
- package/mcp/tests/fixtures/cp_api/run_beta.json +1 -0
- package/mcp/tests/fixtures/cp_api/run_events.json +1 -0
- package/mcp/tests/fixtures/cp_api/runs_list.json +1 -0
- package/mcp/tests/fixtures/cp_api/stats.json +1 -0
- package/mcp/tests/test_cp_tools.py +290 -0
- package/mcp/tests/test_read_only.py +6 -3
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +252 -65
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-Bqx993m_.js → AdminPage-BZ1QNSju.js} +1 -1
- package/web-app/dist/assets/{Avatar-BHjV5Csv.js → Avatar-Dnzzsk2U.js} +1 -1
- package/web-app/dist/assets/{Badge-DGbSCV8L.js → Badge-DWtsAw6M.js} +1 -1
- package/web-app/dist/assets/{Button-CnCZf-0w.js → Button-zEMFjJEr.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-CyZC0Q4e.js → CockpitPage-Ddo5Zhnz.js} +1 -1
- package/web-app/dist/assets/{ComparePage-REwWeDu9.js → ComparePage-kCkyRrrz.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-C0sG0fj4.js → ErrorBoundary-BZrVi8Ui.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-nXcOrxiA.js → EvidenceReceiptPanel-B9eDIWUr.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-D8CUAA7b.js → GitHubIssuesPanel-DX5A2UkV.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-B68Bf309.js → GitHubPRsPanel-DKKxBe6-.js} +1 -1
- package/web-app/dist/assets/{HomePage-mgJEeXH_.js → HomePage-BSn_-fHq.js} +2 -2
- package/web-app/dist/assets/{LoginPage-TM_L3-ws.js → LoginPage-C1BGwj0s.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CoE5Yz73.js → MagicPage-B9JJw_BQ.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-BubxRsx4.js → MetricsPage-B-lNQr8Q.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-udeE-1w6.js → NotFoundPage-DkktBNvI.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BuYFdDIy.js → ProjectPage-DUAraQw9.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-DP4T7hAW.js → ProjectsPage-DgMo4QHm.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-fIgNrA9e.js → SettingsPage-fnL3nqGC.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-9lILj-je.js → ShowcasePage-Bn7rWTXn.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BYBBdIyC.js → SystemSettingsPage-DxrytIXT.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-qhHtMpzb.js → TeamsPage-BQDC12Mn.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DIClsS3d.js → TemplatesPage-C_JEygcZ.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-DKczDGbC.js → TerminalOutput-H0584wn0.js} +1 -1
- package/web-app/dist/assets/{activity-CU5PK4CB.js → activity-Bse7FP2H.js} +1 -1
- package/web-app/dist/assets/{bell-Czwm7c23.js → bell-BMtgCGSW.js} +1 -1
- package/web-app/dist/assets/{bot-BNB-olYj.js → bot-6SW_XTZN.js} +1 -1
- package/web-app/dist/assets/{check-DEmFNYuC.js → check-C3f9aTHx.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DcSPJ4KZ.js → chevron-left-Bi60XjBc.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CQ0-gHsx.js → circle-alert-Bepfb6dn.js} +1 -1
- package/web-app/dist/assets/{clock-CvLXmhWQ.js → clock-jQIU5AuJ.js} +1 -1
- package/web-app/dist/assets/{cloud-6BuQFMTx.js → cloud-BLZn6J9A.js} +1 -1
- package/web-app/dist/assets/{code-xml-BMf3egg0.js → code-xml-CRgKi92z.js} +1 -1
- package/web-app/dist/assets/{database-BBTbPQOn.js → database-DWg9XT6Y.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-B8vHgGQG.js → dollar-sign-Cfif5nlf.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DwAQcHOd.js → file-code-corner-DSiLXED8.js} +1 -1
- package/web-app/dist/assets/{file-plus-BlhGOB-I.js → file-plus-8lIzYfAS.js} +1 -1
- package/web-app/dist/assets/{globe-6a-K3mZa.js → globe-DZkvR077.js} +1 -1
- package/web-app/dist/assets/{hammer-CU_u8tsP.js → hammer-BtQyfpf_.js} +1 -1
- package/web-app/dist/assets/{index-BN6SdA1r.js → index-B-uQiPRR.js} +3 -3
- package/web-app/dist/assets/{layers-VJW1LqHm.js → layers-Dlfo3jwV.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DfL0cxR3.js → loader-circle-BQ2hL7m5.js} +1 -1
- package/web-app/dist/assets/{lock-D2LRX42k.js → lock-LzpJKEAP.js} +1 -1
- package/web-app/dist/assets/{package-_JNG_ljQ.js → package-zp-qZxpp.js} +1 -1
- package/web-app/dist/assets/{plus-qYicH17Y.js → plus-CuiDkGsm.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-Bqb0Vbp-.js → refresh-cw-Dx57uk3-.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Blheo-H1.js → rotate-ccw-Bp1YOkM6.js} +1 -1
- package/web-app/dist/assets/{scroll-text-3OO4nVP7.js → scroll-text-Dc8LjRT5.js} +1 -1
- package/web-app/dist/assets/{server-34C8ObzM.js → server-CE0Mzyfz.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D7-nkuq3.js → shield-alert-DBj71EDP.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CvfrKMma.js → thumbs-up-C0q9J-0y.js} +1 -1
- package/web-app/dist/assets/{trash-2-4bt3fZC1.js → trash-2-YNPSnz_h.js} +1 -1
- package/web-app/dist/assets/{trending-down-C7Uu-rJL.js → trending-down-BlyQB_Gh.js} +1 -1
- package/web-app/dist/assets/{trending-up-B7TF-ynr.js → trending-up-CStScgAm.js} +1 -1
- package/web-app/dist/assets/{upload-YlZG_g5B.js → upload-CLs-rKu9.js} +1 -1
- package/web-app/dist/assets/{usePolling-BDZJZdnI.js → usePolling-BmFkptqQ.js} +1 -1
- package/web-app/dist/assets/{user-C2Z3KTt0.js → user-CV0TBz6A.js} +1 -1
- package/web-app/dist/index.html +1 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""CP-ASK slice 2: the CP data tools, driven only by fixtures (no live Control Plane)."""
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
import urllib.request
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
14
|
+
if _REPO_ROOT not in sys.path:
|
|
15
|
+
sys.path.insert(0, _REPO_ROOT)
|
|
16
|
+
|
|
17
|
+
from mcp import cp_tools # noqa: E402
|
|
18
|
+
|
|
19
|
+
FIX = os.path.join(os.path.dirname(__file__), "fixtures", "cp_api")
|
|
20
|
+
URL = "http://127.0.0.1:7777"
|
|
21
|
+
TOKEN = "s3cret-token-value"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _fx(name):
|
|
25
|
+
with open(os.path.join(FIX, name), "rb") as f:
|
|
26
|
+
return f.read()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ROUTES = {
|
|
30
|
+
"/v1/runs": "runs_list.json",
|
|
31
|
+
"/v1/runs/src-a/e10-aaa111": "run_alpha.json",
|
|
32
|
+
"/v1/runs/src-b/e10-bbb222": "run_beta.json",
|
|
33
|
+
"/v1/runs/src-a/e10-aaa111/events": "run_events.json",
|
|
34
|
+
"/v1/runs/src-a/e10-aaa111/artifact/report.md": "artifact_report.md",
|
|
35
|
+
"/v1/stats": "stats.json",
|
|
36
|
+
"/v1/stats/cost": "cost.json",
|
|
37
|
+
"/v1/repos": "repos.json",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FixtureTransport:
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.calls = []
|
|
44
|
+
|
|
45
|
+
def __call__(self, path, query):
|
|
46
|
+
self.calls.append((path, dict(query)))
|
|
47
|
+
if path in ROUTES:
|
|
48
|
+
return 200, _fx(ROUTES[path])
|
|
49
|
+
return 404, b'{"error":"run not found"}'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _client(transport=None):
|
|
53
|
+
t = transport or FixtureTransport()
|
|
54
|
+
return cp_tools.CpClient(transport=t, base_url=URL, token=TOKEN), t
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CompareTests(unittest.TestCase):
|
|
58
|
+
def test_compare_returns_both_ids_verdicts_and_costs(self):
|
|
59
|
+
c, _ = _client()
|
|
60
|
+
r = cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222")
|
|
61
|
+
self.assertEqual([x["run_id"] for x in r["runs"]], ["e10-aaa111", "e10-bbb222"])
|
|
62
|
+
self.assertEqual([x["verdict"] for x in r["runs"]], ["VERIFIED", "FAILED"])
|
|
63
|
+
self.assertEqual([x["cost_usd"] for x in r["runs"]], [0.42, 1.25])
|
|
64
|
+
self.assertAlmostEqual(r["cost_delta_usd"], 0.83, places=6)
|
|
65
|
+
|
|
66
|
+
def test_compare_unknown_run_is_an_error(self):
|
|
67
|
+
c, _ = _client()
|
|
68
|
+
r = cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "nope")
|
|
69
|
+
self.assertIn("error", r)
|
|
70
|
+
|
|
71
|
+
def test_other_read_tools_return_fixture_data(self):
|
|
72
|
+
c, _ = _client()
|
|
73
|
+
self.assertEqual(cp_tools.runs_search(c)["total"], 2)
|
|
74
|
+
self.assertEqual(cp_tools.run_get(c, "src-b", "e10-bbb222")["verdict"], "FAILED")
|
|
75
|
+
self.assertEqual(len(cp_tools.run_events(c, "src-a", "e10-aaa111")["events"]), 2)
|
|
76
|
+
self.assertEqual(cp_tools.stats(c)["runs_total"], 2)
|
|
77
|
+
self.assertEqual(cp_tools.cost(c)["rows"][0]["runs"], 2)
|
|
78
|
+
|
|
79
|
+
def test_event_text_is_returned_as_data(self):
|
|
80
|
+
c, _ = _client()
|
|
81
|
+
ev = cp_tools.run_events(c, "src-a", "e10-aaa111")["events"][1]
|
|
82
|
+
self.assertIn("Ignore previous instructions", ev["data"]["title"])
|
|
83
|
+
|
|
84
|
+
def test_repos_list_is_names_only(self):
|
|
85
|
+
c, _ = _client()
|
|
86
|
+
self.assertEqual(cp_tools.repos_list(c), {"repos": ["acme/widgets", "acme/gadgets"]})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RefusalTests(unittest.TestCase):
|
|
90
|
+
BAD = ["secrets.json", "../receipt.json", "receipt.json/../../x", "/etc/passwd",
|
|
91
|
+
"evidence/../x.png", "plan.json\x00.md", "evidence/shot.png", "", "report.md.bak",
|
|
92
|
+
"RECEIPT.json", ".loki/state.json"]
|
|
93
|
+
|
|
94
|
+
def test_artifact_outside_allowlist_is_refused_without_a_request(self):
|
|
95
|
+
for name in self.BAD:
|
|
96
|
+
c, t = _client()
|
|
97
|
+
r = cp_tools.run_artifact(c, "src-a", "e10-aaa111", name)
|
|
98
|
+
self.assertIn("error", r, name)
|
|
99
|
+
self.assertNotIn("content", r, name)
|
|
100
|
+
self.assertEqual(t.calls, [], "request made for " + repr(name))
|
|
101
|
+
|
|
102
|
+
def test_allowlisted_artifact_is_read(self):
|
|
103
|
+
c, t = _client()
|
|
104
|
+
r = cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md")
|
|
105
|
+
self.assertEqual(r["content"], "# Report\n\nAll tests passed.\n")
|
|
106
|
+
self.assertEqual(len(t.calls), 1)
|
|
107
|
+
|
|
108
|
+
def test_trailing_newline_is_refused_without_a_request(self):
|
|
109
|
+
c, t = _client()
|
|
110
|
+
self.assertIn("error", cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md\n"))
|
|
111
|
+
self.assertIn("error", cp_tools.run_artifact(c, "src-a\n", "e10-aaa111", "report.md"))
|
|
112
|
+
self.assertIn("error", cp_tools.run_get(c, "src-a", "e10-aaa111\n"))
|
|
113
|
+
self.assertIn("error", cp_tools.run_events(c, "src-a\n", "e10-aaa111"))
|
|
114
|
+
self.assertEqual(t.calls, [])
|
|
115
|
+
|
|
116
|
+
def test_bad_ids_are_refused_without_a_request(self):
|
|
117
|
+
for sid, rid in (("../x", "r"), ("s", "a/b"), ("s", ".."), ("", "r"), ("s", "a b")):
|
|
118
|
+
c, t = _client()
|
|
119
|
+
self.assertIn("error", cp_tools.run_get(c, sid, rid))
|
|
120
|
+
self.assertIn("error", cp_tools.run_artifact(c, sid, rid, "report.md"))
|
|
121
|
+
self.assertEqual(t.calls, [])
|
|
122
|
+
|
|
123
|
+
def test_bad_args_are_refused(self):
|
|
124
|
+
c, t = _client()
|
|
125
|
+
self.assertIn("error", cp_tools.runs_search(c, limit=0))
|
|
126
|
+
self.assertIn("error", cp_tools.runs_search(c, limit=True))
|
|
127
|
+
self.assertIn("error", cp_tools.runs_search(c, since="yesterday"))
|
|
128
|
+
self.assertIn("error", cp_tools.cost(c, group="day; drop"))
|
|
129
|
+
self.assertIn("error", cp_tools.run_events(c, "src-a", "e10-aaa111", after=-5))
|
|
130
|
+
self.assertEqual(t.calls, [])
|
|
131
|
+
|
|
132
|
+
def test_unconfigured_client_refuses(self):
|
|
133
|
+
with mock.patch.dict(os.environ, {}, clear=False):
|
|
134
|
+
for k in ("LOKI_CP_URL", "LOKI_CONTROL_URL"):
|
|
135
|
+
os.environ.pop(k, None)
|
|
136
|
+
c = cp_tools.CpClient()
|
|
137
|
+
self.assertIn("error", cp_tools.stats(c))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class RedactionTests(unittest.TestCase):
|
|
141
|
+
def _dump(self, *results):
|
|
142
|
+
return json.dumps(results)
|
|
143
|
+
|
|
144
|
+
def test_no_url_token_or_path_in_any_tool_output(self):
|
|
145
|
+
c, _ = _client()
|
|
146
|
+
outs = [
|
|
147
|
+
cp_tools.run_get(c, "src-a", "e10-aaa111"),
|
|
148
|
+
cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222"),
|
|
149
|
+
cp_tools.runs_search(c), cp_tools.stats(c), cp_tools.cost(c),
|
|
150
|
+
cp_tools.run_events(c, "src-a", "e10-aaa111"),
|
|
151
|
+
cp_tools.repos_list(c),
|
|
152
|
+
]
|
|
153
|
+
blob = self._dump(*outs)
|
|
154
|
+
for secret in (TOKEN, URL, "127.0.0.1:7777", "/Users/someone/private"):
|
|
155
|
+
self.assertNotIn(secret, blob)
|
|
156
|
+
self.assertIn("[redacted]", json.dumps(outs[0]))
|
|
157
|
+
|
|
158
|
+
def test_transport_failure_does_not_leak_url_or_token(self):
|
|
159
|
+
def boom(path, query):
|
|
160
|
+
raise OSError("connect to %s failed with Bearer %s" % (URL, TOKEN))
|
|
161
|
+
c, _ = _client(boom)
|
|
162
|
+
r = cp_tools.run_get(c, "src-a", "e10-aaa111")
|
|
163
|
+
self.assertEqual(list(r), ["error"])
|
|
164
|
+
self.assertNotIn(TOKEN, json.dumps(r))
|
|
165
|
+
self.assertNotIn(URL, json.dumps(r))
|
|
166
|
+
|
|
167
|
+
def test_error_body_echoing_secrets_is_scrubbed(self):
|
|
168
|
+
def echo(path, query):
|
|
169
|
+
return 500, json.dumps({"error": "bad %s token %s" % (URL, TOKEN)}).encode()
|
|
170
|
+
c, _ = _client(echo)
|
|
171
|
+
blob = json.dumps(cp_tools.run_get(c, "src-a", "e10-aaa111"))
|
|
172
|
+
self.assertNotIn(TOKEN, blob)
|
|
173
|
+
self.assertNotIn(URL, blob)
|
|
174
|
+
|
|
175
|
+
def test_artifact_content_echoing_secrets_is_scrubbed(self):
|
|
176
|
+
def leak(path, query):
|
|
177
|
+
return 200, ("see %s/v1/runs using %s" % (URL, TOKEN)).encode()
|
|
178
|
+
c, _ = _client(leak)
|
|
179
|
+
blob = json.dumps(cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md"))
|
|
180
|
+
self.assertNotIn(TOKEN, blob)
|
|
181
|
+
self.assertNotIn(URL, blob)
|
|
182
|
+
|
|
183
|
+
def test_redirect_is_never_followed(self):
|
|
184
|
+
h = cp_tools._NoRedirect()
|
|
185
|
+
self.assertIsNone(h.redirect_request(None, None, 302, "Found", {}, "http://evil.example/"))
|
|
186
|
+
|
|
187
|
+
def test_non_http_url_is_rejected(self):
|
|
188
|
+
c = cp_tools.CpClient(base_url="file:///etc", token=TOKEN)
|
|
189
|
+
r = cp_tools.stats(c)
|
|
190
|
+
self.assertEqual(list(r), ["error"])
|
|
191
|
+
|
|
192
|
+
def test_default_transport_sends_get_with_bearer(self):
|
|
193
|
+
seen = {}
|
|
194
|
+
|
|
195
|
+
class Resp:
|
|
196
|
+
status = 200
|
|
197
|
+
def __enter__(self): return self
|
|
198
|
+
def __exit__(self, *a): return False
|
|
199
|
+
def read(self, n=-1): return b'{"repos":[]}'
|
|
200
|
+
|
|
201
|
+
class Opener:
|
|
202
|
+
def open(self, req, timeout=None):
|
|
203
|
+
seen["method"] = req.get_method()
|
|
204
|
+
seen["auth"] = req.get_header("Authorization")
|
|
205
|
+
seen["url"] = req.full_url
|
|
206
|
+
return Resp()
|
|
207
|
+
|
|
208
|
+
with mock.patch.object(urllib.request, "build_opener", return_value=Opener()):
|
|
209
|
+
c = cp_tools.CpClient(base_url=URL, token=TOKEN)
|
|
210
|
+
r = cp_tools.repos_list(c)
|
|
211
|
+
self.assertEqual(r, {"repos": []})
|
|
212
|
+
self.assertEqual(seen["method"], "GET")
|
|
213
|
+
self.assertEqual(seen["auth"], "Bearer " + TOKEN)
|
|
214
|
+
self.assertTrue(seen["url"].startswith(URL + "/v1/repos"))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _tree(root):
|
|
218
|
+
out = set()
|
|
219
|
+
for d, _dirs, files in os.walk(root):
|
|
220
|
+
for f in files:
|
|
221
|
+
p = os.path.join(d, f)
|
|
222
|
+
out.add((p, os.stat(p).st_mtime_ns, os.stat(p).st_size))
|
|
223
|
+
return out
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ReadOnlyTests(unittest.TestCase):
|
|
227
|
+
def test_tools_write_nothing_to_disk(self):
|
|
228
|
+
c, _ = _client()
|
|
229
|
+
real_open = builtins.open
|
|
230
|
+
|
|
231
|
+
def guarded(file, mode="r", *a, **k):
|
|
232
|
+
if any(ch in mode for ch in "wax+"):
|
|
233
|
+
raise AssertionError("write open: %r %r" % (file, mode))
|
|
234
|
+
return real_open(file, mode, *a, **k)
|
|
235
|
+
|
|
236
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
237
|
+
before = _tree(tmp)
|
|
238
|
+
old = os.getcwd()
|
|
239
|
+
os.chdir(tmp)
|
|
240
|
+
try:
|
|
241
|
+
with mock.patch.object(builtins, "open", guarded), \
|
|
242
|
+
mock.patch("os.mkdir", side_effect=AssertionError("mkdir")), \
|
|
243
|
+
mock.patch("os.remove", side_effect=AssertionError("remove")), \
|
|
244
|
+
mock.patch("subprocess.Popen", side_effect=AssertionError("spawn")):
|
|
245
|
+
cp_tools.runs_search(c)
|
|
246
|
+
cp_tools.run_get(c, "src-a", "e10-aaa111")
|
|
247
|
+
cp_tools.run_events(c, "src-a", "e10-aaa111")
|
|
248
|
+
cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md")
|
|
249
|
+
cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222")
|
|
250
|
+
cp_tools.stats(c)
|
|
251
|
+
cp_tools.cost(c)
|
|
252
|
+
cp_tools.repos_list(c)
|
|
253
|
+
finally:
|
|
254
|
+
os.chdir(old)
|
|
255
|
+
self.assertEqual(_tree(tmp), before)
|
|
256
|
+
|
|
257
|
+
def test_every_request_is_a_get_of_a_read_route(self):
|
|
258
|
+
c, t = _client()
|
|
259
|
+
cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222")
|
|
260
|
+
cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md")
|
|
261
|
+
for path, _q in t.calls:
|
|
262
|
+
self.assertTrue(path.startswith("/v1/"), path)
|
|
263
|
+
# compare is built from two run reads; the CP has no compare route
|
|
264
|
+
self.assertNotIn("/v1/compare", [p for p, _ in t.calls])
|
|
265
|
+
|
|
266
|
+
def test_module_has_no_write_primitives(self):
|
|
267
|
+
src = open(cp_tools.__file__).read()
|
|
268
|
+
for banned in ("subprocess", "shutil", "os.system", "method=\"POST\"", "method=\"PUT\"",
|
|
269
|
+
"method=\"DELETE\""):
|
|
270
|
+
self.assertNotIn(banned, src)
|
|
271
|
+
self.assertIsNone(re.search(r"(?<![.\w])open\(", src))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class SurfaceTests(unittest.TestCase):
|
|
275
|
+
def test_tool_names_match_registration(self):
|
|
276
|
+
names = []
|
|
277
|
+
|
|
278
|
+
class Fake:
|
|
279
|
+
def tool(self):
|
|
280
|
+
def deco(fn):
|
|
281
|
+
names.append(fn.__name__)
|
|
282
|
+
return fn
|
|
283
|
+
return deco
|
|
284
|
+
|
|
285
|
+
cp_tools.register_cp_tools(Fake())
|
|
286
|
+
self.assertEqual(tuple(names), cp_tools.TOOL_NAMES)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
unittest.main()
|
|
@@ -18,6 +18,9 @@ EXPECTED_ALLOWLIST = {
|
|
|
18
18
|
"loki_quality_report", "loki_code_search", "mem_search", "mem_timeline",
|
|
19
19
|
"mem_get", "loki_get_hotspots", "loki_get_co_changes",
|
|
20
20
|
"loki_get_doc_coverage", "loki_findings", "loki_learnings",
|
|
21
|
+
# CP data tools (CP-ASK slice 2)
|
|
22
|
+
"cp_runs_search", "cp_run_get", "cp_run_events", "cp_run_artifact",
|
|
23
|
+
"cp_runs_compare", "cp_stats", "cp_cost", "cp_repos_list",
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
# Tools that write, spawn processes, or are otherwise not on the allowlist.
|
|
@@ -104,7 +107,7 @@ class ReadOnlyTests(unittest.TestCase):
|
|
|
104
107
|
full = _tools(False)
|
|
105
108
|
self.assertTrue(EXPECTED_ALLOWLIST <= full)
|
|
106
109
|
self.assertTrue(EXCLUDED_TOOLS <= full)
|
|
107
|
-
self.assertGreaterEqual(len(full),
|
|
110
|
+
self.assertGreaterEqual(len(full), 47)
|
|
108
111
|
|
|
109
112
|
def test_resources_and_prompts_are_a_known_list(self):
|
|
110
113
|
listing = _listing(True)
|
|
@@ -119,8 +122,8 @@ class ReadOnlyTests(unittest.TestCase):
|
|
|
119
122
|
self.assertEqual(_main_tools([], value), EXPECTED_ALLOWLIST, value)
|
|
120
123
|
|
|
121
124
|
def test_main_default_is_full_set(self):
|
|
122
|
-
self.assertGreaterEqual(len(_main_tools([])),
|
|
123
|
-
self.assertGreaterEqual(len(_main_tools([], "0")),
|
|
125
|
+
self.assertGreaterEqual(len(_main_tools([])), 47)
|
|
126
|
+
self.assertGreaterEqual(len(_main_tools([], "0")), 47)
|
|
124
127
|
|
|
125
128
|
|
|
126
129
|
def _git(cwd, *args):
|
package/package.json
CHANGED