compute guard: line-scoped allow entries; allow MindKeychain.jsx's two OAuth endpoints only
Allow entries may carry matches: (regexes); then only matching lines are allowed, so an allowed file can't smuggle in a new call. windy-pro #609 MindKeychain.jsx: openrouter.ai/auth? and /api/v1/auth/keys (BYOK key acquisition via OAuth PKCE, no inference; successor of the MindPanel allow, ADR-064). An inference call in the same file still flags (tested). Orchestrator-approved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,3 +191,32 @@ def test_a_commit_not_fetched_yet_is_skipped_not_an_error(tmp_path, monkeypatch)
|
|||||||
(tmp_path / "windy-chat.git").symlink_to(bare)
|
(tmp_path / "windy-chat.git").symlink_to(bare)
|
||||||
assert cg.check("windy-chat", "f" * 40, "main", True) is None # pushed after the fetch
|
assert cg.check("windy-chat", "f" * 40, "main", True) is None # pushed after the fetch
|
||||||
assert [f.kind for f in cg.check("windy-chat", sha, "main", True)] == ["provider SDK"]
|
assert [f.kind for f in cg.check("windy-chat", sha, "main", True)] == ["provider SDK"]
|
||||||
|
|
||||||
|
|
||||||
|
KEYCHAIN = "src/client/web/src/pages/panels/MindKeychain.jsx"
|
||||||
|
|
||||||
|
|
||||||
|
def test_scoped_allow_admits_only_the_oauth_endpoints():
|
||||||
|
ok = [
|
||||||
|
" window.location.href = `https://openrouter.ai/auth?callback_url=${encodeURIComponent(callback)}`",
|
||||||
|
" const res = await fetch('https://openrouter.ai/api/v1/auth/keys', {",
|
||||||
|
]
|
||||||
|
for line in ok:
|
||||||
|
assert cg.allowed("windy-pro", KEYCHAIN, ALLOW, line)
|
||||||
|
# an inference call smuggled into the same file still flags
|
||||||
|
assert not cg.allowed("windy-pro", KEYCHAIN, ALLOW,
|
||||||
|
" await fetch('https://openrouter.ai/api/v1/chat/completions', {")
|
||||||
|
# a scoped entry never allows a line it can't see
|
||||||
|
assert not cg.allowed("windy-pro", KEYCHAIN, ALLOW)
|
||||||
|
|
||||||
|
|
||||||
|
def test_scoped_allow_in_a_real_diff():
|
||||||
|
diff = f"""--- /dev/null
|
||||||
|
+++ b/{KEYCHAIN}
|
||||||
|
@@ -0,0 +1,3 @@
|
||||||
|
+ window.location.href = `https://openrouter.ai/auth?callback_url=x`
|
||||||
|
+ const res = await fetch('https://openrouter.ai/api/v1/auth/keys', {{
|
||||||
|
+ await fetch('https://openrouter.ai/api/v1/chat/completions', {{
|
||||||
|
"""
|
||||||
|
fs = cg.parse_added("windy-pro", diff, ALLOW)
|
||||||
|
assert [(f.line, f.match) for f in fs] == [(3, "openrouter.ai")]
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ allow:
|
|||||||
paths: ["src/client/web/src/pages/panels/MindPanel.jsx"]
|
paths: ["src/client/web/src/pages/panels/MindPanel.jsx"]
|
||||||
reason: "Validates the USER's own OpenRouter key for BYOK (audit #10); spends no house money."
|
reason: "Validates the USER's own OpenRouter key for BYOK (audit #10); spends no house money."
|
||||||
|
|
||||||
|
- repo: windy-pro
|
||||||
|
paths: ["src/client/web/src/pages/panels/MindKeychain.jsx"]
|
||||||
|
# ONLY these two endpoints: any other openrouter.ai call in this file (e.g.
|
||||||
|
# /api/v1/chat, i.e. inference) still flags. Orchestrator-approved 09-23.
|
||||||
|
matches: ['openrouter\.ai/auth\?', 'openrouter\.ai/api/v1/auth/keys']
|
||||||
|
reason: "BYOK key acquisition via OpenRouter OAuth PKCE; no inference; successor of MindPanel allow (ADR-064)."
|
||||||
|
|
||||||
- repo: windy-git
|
- repo: windy-git
|
||||||
paths: ["scripts/compute_guard.py", "ci/compute-guard-allow.yml"]
|
paths: ["scripts/compute_guard.py", "ci/compute-guard-allow.yml"]
|
||||||
reason: "The guard's own pattern list and this file."
|
reason: "The guard's own pattern list and this file."
|
||||||
|
|||||||
@@ -98,9 +98,18 @@ def load_allow(path: Path = ALLOW_FILE) -> list[dict]:
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def allowed(repo: str, path: str, allow: list[dict]) -> bool:
|
def allowed(repo: str, path: str, allow: list[dict], text: str | None = None) -> bool:
|
||||||
|
"""An entry may carry `matches:` (regexes): then only lines matching one of
|
||||||
|
them are allowed, so an allowed file can't smuggle in a NEW call (e.g. an
|
||||||
|
OAuth sign-in endpoint is allowed, an inference endpoint in the same file
|
||||||
|
still flags). Entries without `matches` cover the whole path."""
|
||||||
for e in allow:
|
for e in allow:
|
||||||
if e["repo"] == repo and any(fnmatch.fnmatch(path, g) for g in e["paths"]):
|
if e["repo"] != repo or not any(fnmatch.fnmatch(path, g) for g in e["paths"]):
|
||||||
|
continue
|
||||||
|
pats = e.get("matches")
|
||||||
|
if not pats:
|
||||||
|
return True
|
||||||
|
if text is not None and any(re.search(rx, text) for rx in pats):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -157,7 +166,7 @@ def scan_tree(repo: str, bare: Path, sha: str, allow: list[dict], *, line_fn=Non
|
|||||||
_, path, line, text = raw.split(":", 3)
|
_, path, line, text = raw.split(":", 3)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
if not path_ok(path) or allowed(repo, path, allow):
|
if not path_ok(path) or allowed(repo, path, allow, text):
|
||||||
continue
|
continue
|
||||||
for kind, match in line_fn(path, text):
|
for kind, match in line_fn(path, text):
|
||||||
found.append(Finding(path, int(line), kind, match))
|
found.append(Finding(path, int(line), kind, match))
|
||||||
@@ -190,7 +199,7 @@ def parse_added(repo: str, diff: str, allow: list[dict], *, line_fn=None, path_o
|
|||||||
if path is None or raw.startswith("--- "):
|
if path is None or raw.startswith("--- "):
|
||||||
continue
|
continue
|
||||||
if raw.startswith("+"):
|
if raw.startswith("+"):
|
||||||
if path_ok(path) and not allowed(repo, path, allow):
|
if path_ok(path) and not allowed(repo, path, allow, raw[1:]):
|
||||||
for kind, match in line_fn(path, raw[1:]):
|
for kind, match in line_fn(path, raw[1:]):
|
||||||
found.append(Finding(path, line, kind, match))
|
found.append(Finding(path, line, kind, match))
|
||||||
line += 1
|
line += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user