feat(zhulan): add restricted remote development cell
This commit is contained in:
parent
5f9e83e1b7
commit
366b8911e4
22 changed files with 4503 additions and 0 deletions
32
server-tools/zhulan-remote-cell/tests/test_code_gate.py
Normal file
32
server-tools/zhulan-remote-cell/tests/test_code_gate.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "runtime"))
|
||||
from zhulan_code_gate import scan_file, within # noqa: E402
|
||||
|
||||
|
||||
class CodeGateTest(unittest.TestCase):
|
||||
def test_paths_fail_closed(self):
|
||||
self.assertTrue(within("server-tools/zhulan-remote-cell/ui/app.js", ["server-tools/zhulan-remote-cell"]))
|
||||
self.assertFalse(within("server-tools/lake-lamp-authz/server.js", ["server-tools/zhulan-remote-cell"]))
|
||||
|
||||
def test_private_key_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
workspace = Path(root)
|
||||
target = workspace / "leak.txt"
|
||||
target.write_text("-----BEGIN OPENSSH PRIVATE KEY-----\nnot-real\n", encoding="utf-8")
|
||||
self.assertIn("possible_secret", scan_file(workspace, target, 1024 * 1024))
|
||||
|
||||
def test_symlink_outside_workspace_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as root, tempfile.TemporaryDirectory() as outside:
|
||||
workspace = Path(root)
|
||||
target = workspace / "outside-link"
|
||||
target.symlink_to(Path(outside) / "secret")
|
||||
self.assertIn("symlink_outside_workspace", scan_file(workspace, target, 1024 * 1024))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class DeploymentContractTest(unittest.TestCase):
|
||||
def test_runtime_upgrade_restarts_service(self):
|
||||
installer = (ROOT / "deploy" / "install-runtime.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("systemctl enable zhulan-remote-cell.service", installer)
|
||||
self.assertIn("systemctl restart zhulan-remote-cell.service", installer)
|
||||
self.assertIn("systemctl restart zhulan-validation-executor.service", installer)
|
||||
self.assertNotIn("systemctl enable --now zhulan-remote-cell.service", installer)
|
||||
|
||||
def test_bwrap_exception_is_scoped_to_runtime_copy(self):
|
||||
env = (ROOT / "deploy" / "zhulan-remote-cell.env.example").read_text(encoding="utf-8")
|
||||
profile = (ROOT / "deploy" / "zhulan-bwrap.apparmor").read_text(encoding="utf-8")
|
||||
installer = (ROOT / "deploy" / "install-runtime.sh").read_text(encoding="utf-8")
|
||||
private_copy = "/opt/guanghu/zhulan-remote-cell/runtime/zhulan-bwrap"
|
||||
self.assertIn(f"ZHULAN_BWRAP={private_copy}", env)
|
||||
self.assertIn(f"profile zhulan-remote-cell-bwrap {private_copy}", profile)
|
||||
self.assertIn(" userns,", profile)
|
||||
self.assertIn('install -o root -g zhulan-runtime -m 0750 /usr/bin/bwrap', installer)
|
||||
self.assertNotIn("kernel.apparmor_restrict_unprivileged_userns=0", installer)
|
||||
self.assertNotIn("chmod 4755", installer)
|
||||
|
||||
def test_only_executor_allows_bwrap_required_netlink_addition(self):
|
||||
service = (ROOT / "deploy" / "zhulan-remote-cell.service").read_text(encoding="utf-8")
|
||||
executor = (ROOT / "deploy" / "zhulan-validation-executor.service").read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6",
|
||||
service,
|
||||
)
|
||||
self.assertNotIn("AF_NETLINK", service)
|
||||
self.assertIn("RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK", executor)
|
||||
self.assertIn("RestrictNamespaces=user mnt pid net ipc uts", service)
|
||||
|
||||
def test_public_runtime_keeps_no_new_privileges_and_uses_local_executor(self):
|
||||
public_service = (ROOT / "deploy" / "zhulan-remote-cell.service").read_text(encoding="utf-8")
|
||||
executor_service = (ROOT / "deploy" / "zhulan-validation-executor.service").read_text(encoding="utf-8")
|
||||
runtime = (ROOT / "runtime" / "zhulan_cell.py").read_text(encoding="utf-8")
|
||||
self.assertIn("NoNewPrivileges=true", public_service)
|
||||
self.assertIn("ProtectKernelTunables=true", public_service)
|
||||
self.assertIn("Requires=zhulan-validation-executor.service", public_service)
|
||||
self.assertNotIn("NoNewPrivileges=true", executor_service)
|
||||
self.assertNotIn("ProtectKernelTunables=true", executor_service)
|
||||
self.assertIn("RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK", executor_service)
|
||||
self.assertIn("ReadOnlyPaths=/srv/guanghu/zhulan-cell/workspaces", executor_service)
|
||||
self.assertIn("PrivateNetwork=true", executor_service)
|
||||
self.assertIn("InaccessiblePaths=/etc/guanghu/secrets", executor_service)
|
||||
self.assertIn("ReadOnlyPaths=/var/lib/guanghu/zhulan-remote-cell", executor_service)
|
||||
self.assertIn("InaccessiblePaths=/srv/guanghu/zhulan-cell/candidates", executor_service)
|
||||
self.assertIn("socket.AF_UNIX", runtime)
|
||||
self.assertNotIn('argv = [\n str(self.settings.bwrap_path)', runtime)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import json
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = spec_from_file_location(
|
||||
"zhulan_validation_executor",
|
||||
ROOT / "runtime" / "zhulan_validation_executor.py",
|
||||
)
|
||||
MODULE = module_from_spec(SPEC)
|
||||
assert SPEC and SPEC.loader
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class ValidationExecutorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
root = Path(self.temp.name)
|
||||
workspace_root = root / "workspaces"
|
||||
self.workspace = workspace_root / "DEV-20260813-099" / "bingshuo__guanghu-ice-heart"
|
||||
(self.workspace / ".git").mkdir(parents=True)
|
||||
self.policy = root / "policy.json"
|
||||
self.policy.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"repositories": {
|
||||
"bingshuo/guanghu-ice-heart": {
|
||||
"allowed_path_prefixes": ["server-tools/zhulan-remote-cell"],
|
||||
"validation_commands": [["python3", "-m", "unittest"]],
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.bwrap = root / "zhulan-bwrap"
|
||||
self.bwrap.write_text("test", encoding="utf-8")
|
||||
self.state_db = root / "state.sqlite3"
|
||||
with sqlite3.connect(self.state_db) as db:
|
||||
db.execute(
|
||||
"""CREATE TABLE requests (
|
||||
id TEXT PRIMARY KEY, state TEXT, picked_up_at INTEGER,
|
||||
capability_expires_at INTEGER, development_id TEXT,
|
||||
repository TEXT, paths_json TEXT, actions_json TEXT
|
||||
)"""
|
||||
)
|
||||
db.execute(
|
||||
"INSERT INTO requests VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
"ZLR-20260813-AAAAAAAAAA",
|
||||
"APPROVED",
|
||||
int(time.time()),
|
||||
int(time.time()) + 3600,
|
||||
"DEV-20260813-099",
|
||||
"bingshuo/guanghu-ice-heart",
|
||||
json.dumps(["server-tools/zhulan-remote-cell"]),
|
||||
json.dumps(["read", "test"]),
|
||||
),
|
||||
)
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"ZHULAN_WORKSPACE_ROOT": str(workspace_root),
|
||||
"ZHULAN_POLICY": str(self.policy),
|
||||
"ZHULAN_BWRAP": str(self.bwrap),
|
||||
"ZHULAN_DB": str(self.state_db),
|
||||
},
|
||||
):
|
||||
self.executor = MODULE.ValidationExecutor()
|
||||
|
||||
def tearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
def valid(self):
|
||||
return {
|
||||
"operation": "run",
|
||||
"request_id": "ZLR-20260813-AAAAAAAAAA",
|
||||
"repository": "bingshuo/guanghu-ice-heart",
|
||||
"development_id": "DEV-20260813-099",
|
||||
"workspace": str(self.workspace),
|
||||
"paths": ["server-tools/zhulan-remote-cell"],
|
||||
"command": ["python3", "-m", "unittest"],
|
||||
}
|
||||
|
||||
def test_accepts_only_exact_registered_request(self):
|
||||
workspace, paths, command = self.executor.validate(self.valid())
|
||||
self.assertEqual(workspace, self.workspace.resolve())
|
||||
self.assertEqual(paths, ["server-tools/zhulan-remote-cell"])
|
||||
self.assertEqual(command, ["python3", "-m", "unittest"])
|
||||
|
||||
def test_rejects_extra_fields_arbitrary_command_and_path(self):
|
||||
for key, value in [
|
||||
("extra", True),
|
||||
("command", ["sh", "-c", "id"]),
|
||||
("paths", ["deployment"]),
|
||||
]:
|
||||
request = self.valid()
|
||||
request[key] = value
|
||||
with self.assertRaises(PermissionError):
|
||||
self.executor.validate(request)
|
||||
|
||||
def test_rejects_workspace_repository_mismatch(self):
|
||||
request = self.valid()
|
||||
wrong = self.workspace.parent / "bingshuo__other"
|
||||
(wrong / ".git").mkdir(parents=True)
|
||||
request["workspace"] = str(wrong)
|
||||
with self.assertRaises(PermissionError):
|
||||
self.executor.validate(request)
|
||||
|
||||
def test_rejects_inactive_or_mismatched_approval_binding(self):
|
||||
for key, value in [
|
||||
("request_id", "ZLR-20260813-BBBBBBBBBB"),
|
||||
("development_id", "DEV-20260813-098"),
|
||||
]:
|
||||
request = self.valid()
|
||||
request[key] = value
|
||||
with self.assertRaises(PermissionError):
|
||||
self.executor.validate(request)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
589
server-tools/zhulan-remote-cell/tests/test_zhulan_cell.py
Normal file
589
server-tools/zhulan-remote-cell/tests/test_zhulan_cell.py
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
import http.cookiejar
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def free_port():
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return sock.getsockname()[1]
|
||||
|
||||
|
||||
class ZhulanCellTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temp = tempfile.TemporaryDirectory()
|
||||
temp_root = Path(cls.temp.name)
|
||||
seed = temp_root / "seed"
|
||||
forge_root = temp_root / "forge"
|
||||
bare = forge_root / "bingshuo" / "guanghu-ice-heart.git"
|
||||
allowed = seed / "server-tools" / "zhulan-remote-cell"
|
||||
allowed.mkdir(parents=True)
|
||||
(allowed / "hello.txt").write_text("before\n", encoding="utf-8")
|
||||
subprocess.run(["git", "init", "-b", "main", str(seed)], check=True, capture_output=True)
|
||||
subprocess.run(["git", "-C", str(seed), "add", "."], check=True, capture_output=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git", "-C", str(seed),
|
||||
"-c", "user.name=Zhulan Test",
|
||||
"-c", "user.email=zhulan-test@invalid",
|
||||
"commit", "-m", "test source",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
cls.source_sha = subprocess.run(
|
||||
["git", "-C", str(seed), "rev-parse", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
bare.parent.mkdir(parents=True)
|
||||
subprocess.run(["git", "clone", "--bare", str(seed), str(bare)], check=True, capture_output=True)
|
||||
policy = json.loads((ROOT / "policy.example.json").read_text(encoding="utf-8"))
|
||||
policy["repository_base_url"] = forge_root.as_uri()
|
||||
policy["repositories"]["bingshuo/guanghu-ice-heart"]["validation_commands"] = [
|
||||
["git", "diff", "--check"]
|
||||
]
|
||||
cls.policy_file = temp_root / "policy.json"
|
||||
cls.policy_file.write_text(json.dumps(policy), encoding="utf-8")
|
||||
cls.port = free_port()
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"ZHULAN_BIND": "127.0.0.1",
|
||||
"ZHULAN_PORT": str(cls.port),
|
||||
"ZHULAN_DB": str(Path(cls.temp.name) / "state.sqlite3"),
|
||||
"ZHULAN_SECRET_FILE": str(Path(cls.temp.name) / "missing-test-secret"),
|
||||
"ZHULAN_POLICY": str(cls.policy_file),
|
||||
"ZHULAN_UI_DIR": str(ROOT / "ui"),
|
||||
"ZHULAN_WORKSPACE_ROOT": str(Path(cls.temp.name) / "workspaces"),
|
||||
"ZHULAN_CANDIDATE_ROOT": str(Path(cls.temp.name) / "candidates"),
|
||||
"ZHULAN_COOKIE_SECURE": "0",
|
||||
"ZHULAN_COOKIE_PATH": "/",
|
||||
"ZHULAN_TEST_MODE": "1",
|
||||
"ZHULAN_TEST_OWNER_PASSWORD": "correct-horse-test-only",
|
||||
}
|
||||
)
|
||||
cls.process = subprocess.Popen(
|
||||
["python3", str(ROOT / "runtime" / "zhulan_cell.py")],
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
cls.base = f"http://127.0.0.1:{cls.port}"
|
||||
deadline = time.time() + 8
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(cls.base + "/health", timeout=0.5) as response:
|
||||
if response.status == 200:
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.08)
|
||||
else:
|
||||
out, err = cls.process.communicate(timeout=2)
|
||||
raise RuntimeError(f"test server did not start\n{out}\n{err}")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.process.terminate()
|
||||
cls.process.wait(timeout=5)
|
||||
if cls.process.stdout:
|
||||
cls.process.stdout.close()
|
||||
if cls.process.stderr:
|
||||
cls.process.stderr.close()
|
||||
cls.temp.cleanup()
|
||||
|
||||
def setUp(self):
|
||||
self.jar = http.cookiejar.CookieJar()
|
||||
self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.jar))
|
||||
|
||||
def request(self, path, body=None, headers=None, expected=200, form=False):
|
||||
if form and body is not None:
|
||||
from urllib.parse import urlencode
|
||||
|
||||
raw = urlencode(body).encode()
|
||||
else:
|
||||
raw = json.dumps(body).encode() if body is not None else None
|
||||
request = urllib.request.Request(
|
||||
self.base + path,
|
||||
data=raw,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded" if form else "application/json", **(headers or {})},
|
||||
)
|
||||
try:
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
self.assertEqual(response.status, expected)
|
||||
return json.loads(response.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
with exc:
|
||||
self.assertEqual(exc.code, expected)
|
||||
return json.loads(exc.read())
|
||||
|
||||
def create(
|
||||
self,
|
||||
dev="DEV-20260813-005",
|
||||
slug="approval-ui",
|
||||
base_sha="5f9e83e1b716fd50ee2880b24ea47f83b75d1f8f",
|
||||
):
|
||||
return self.request(
|
||||
"/api/v1/public/requests",
|
||||
{
|
||||
"persona_id": "ICE-GL-ZL-001",
|
||||
"development_id": dev,
|
||||
"repository": "bingshuo/guanghu-ice-heart",
|
||||
"base_sha": base_sha,
|
||||
"branch": f"zhulan/{dev}/{slug}",
|
||||
"paths": ["server-tools/zhulan-remote-cell"],
|
||||
"actions": ["read", "edit", "test", "commit", "push_candidate"],
|
||||
"description": "完善铸澜手机审批入口与代码门禁",
|
||||
},
|
||||
expected=201,
|
||||
)
|
||||
|
||||
def login(self):
|
||||
result = self.request(
|
||||
"/api/v1/owner/login",
|
||||
{"username": "bingshuo", "password": "correct-horse-test-only"},
|
||||
)
|
||||
return result["csrf"]
|
||||
|
||||
def approved_oauth_token(self, created):
|
||||
csrf = self.login()
|
||||
self.request(
|
||||
f"/api/v1/owner/requests/{created['request_id']}/approve",
|
||||
{"ttl_seconds": 10800},
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
registered = self.request(
|
||||
"/oauth/register",
|
||||
{
|
||||
"client_name": "ChatGPT test connector",
|
||||
"redirect_uris": ["https://chatgpt.com/connector/oauth/test-callback"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
},
|
||||
expected=201,
|
||||
)
|
||||
verifier = "v" * 64
|
||||
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
|
||||
oauth = {
|
||||
"client_id": registered["client_id"],
|
||||
"redirect_uri": registered["redirect_uris"][0],
|
||||
"state": "state-test-123",
|
||||
"code_challenge": challenge,
|
||||
"resource": "https://guanghulab.com/zhulan/mcp",
|
||||
"scope": "zhulan.develop",
|
||||
"csrf": self.request("/api/v1/owner/session")["csrf"],
|
||||
"request_id": created["request_id"],
|
||||
}
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
|
||||
request = urllib.request.Request(
|
||||
self.base + "/oauth/authorize",
|
||||
data=__import__("urllib.parse").parse.urlencode(oauth).encode(),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
no_redirect = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(self.jar), NoRedirect()
|
||||
)
|
||||
try:
|
||||
no_redirect.open(request, timeout=3)
|
||||
self.fail("authorize should redirect")
|
||||
except urllib.error.HTTPError as exc:
|
||||
with exc:
|
||||
self.assertEqual(exc.code, 302)
|
||||
location = exc.headers["Location"]
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
code = parse_qs(urlparse(location).query)["code"][0]
|
||||
token = self.request(
|
||||
"/oauth/token",
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": registered["client_id"],
|
||||
"redirect_uri": registered["redirect_uris"][0],
|
||||
"code_verifier": verifier,
|
||||
"resource": "https://guanghulab.com/zhulan/mcp",
|
||||
},
|
||||
form=True,
|
||||
)
|
||||
return token["access_token"]
|
||||
|
||||
def mcp_tool(self, token, name, arguments=None):
|
||||
response = self.request(
|
||||
"/mcp",
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 91,
|
||||
"method": "tools/call",
|
||||
"params": {"name": name, "arguments": arguments or {}},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
return response["result"]
|
||||
|
||||
def test_public_config_declares_front_and_runtime(self):
|
||||
config = self.request("/api/v1/public/config")
|
||||
self.assertEqual(config["runtime_node"], "BS-SG-003")
|
||||
self.assertIn("BS-GZ-006", config["topology"]["front"])
|
||||
self.assertIn("GHS-012", [x["id"] for x in config["ui_brains"]])
|
||||
|
||||
def test_wrong_persona_is_denied(self):
|
||||
result = self.request(
|
||||
"/api/v1/public/requests",
|
||||
{
|
||||
"persona_id": "ICE-GL-ZY001",
|
||||
"development_id": "DEV-20260813-005",
|
||||
"repository": "bingshuo/guanghu-ice-heart",
|
||||
"base_sha": "5f9e83e1b716fd50ee2880b24ea47f83b75d1f8f",
|
||||
"branch": "zhulan/DEV-20260813-005/nope",
|
||||
"paths": ["server-tools"],
|
||||
"actions": ["read"],
|
||||
"description": "错误人格不得进入铸澜车道",
|
||||
},
|
||||
expected=400,
|
||||
)
|
||||
self.assertEqual(result["error"], "persona_not_allowed")
|
||||
|
||||
def test_approval_pickup_and_capability_verification(self):
|
||||
created = self.create(slug=f"flow-{int(time.time())}")
|
||||
self.assertRegex(created["request_id"], r"^ZLR-[0-9]{8}-[A-F0-9]{10}$")
|
||||
status = self.request(
|
||||
f"/api/v1/public/requests/{created['request_id']}/status",
|
||||
{"claim_token": created["claim_token"]},
|
||||
)
|
||||
self.assertEqual(status["state"], "PENDING")
|
||||
|
||||
csrf = self.login()
|
||||
listing = self.request("/api/v1/owner/requests")
|
||||
self.assertTrue(any(item["id"] == created["request_id"] for item in listing["requests"]))
|
||||
approved = self.request(
|
||||
f"/api/v1/owner/requests/{created['request_id']}/approve",
|
||||
{"ttl_seconds": 10800},
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
self.assertEqual(approved["state"], "APPROVED")
|
||||
|
||||
pickup = self.request(
|
||||
f"/api/v1/public/requests/{created['request_id']}/pickup",
|
||||
{"claim_token": created["claim_token"]},
|
||||
)
|
||||
verified = self.request(
|
||||
"/api/v1/runtime/verify",
|
||||
{"capability": pickup["capability"], "action": "push_candidate"},
|
||||
)
|
||||
self.assertEqual(verified["claims"]["branch"], status["branch"])
|
||||
self.assertEqual(verified["claims"]["node_id"], "BS-SG-003")
|
||||
|
||||
second = self.request(
|
||||
f"/api/v1/public/requests/{created['request_id']}/pickup",
|
||||
{"claim_token": created["claim_token"]},
|
||||
expected=409,
|
||||
)
|
||||
self.assertEqual(second["error"], "capability_already_picked_up")
|
||||
|
||||
filtered = self.request(f"/api/v1/owner/requests/{created['request_id']}/receipts")
|
||||
self.assertGreaterEqual(len(filtered["receipts"]), 4)
|
||||
receipts = self.request("/api/v1/owner/receipts")
|
||||
previous = "0" * 64
|
||||
for receipt in receipts["receipts"]:
|
||||
self.assertEqual(receipt["previous_hash"], previous)
|
||||
previous = receipt["receipt_hash"]
|
||||
|
||||
def test_csrf_is_required_for_decision(self):
|
||||
created = self.create(slug=f"csrf-{int(time.time())}")
|
||||
self.login()
|
||||
result = self.request(
|
||||
f"/api/v1/owner/requests/{created['request_id']}/approve",
|
||||
{"ttl_seconds": 10800},
|
||||
expected=403,
|
||||
)
|
||||
self.assertEqual(result["error"], "csrf_invalid")
|
||||
|
||||
def test_owner_can_revoke_active_oauth_and_capability(self):
|
||||
created = self.create(slug=f"revoke-{int(time.time())}")
|
||||
token = self.approved_oauth_token(created)
|
||||
restored = self.mcp_tool(token, "zhulan_restore_lane")
|
||||
self.assertFalse(restored["isError"])
|
||||
session = self.request("/api/v1/owner/session")
|
||||
revoked = self.request(
|
||||
f"/api/v1/owner/requests/{created['request_id']}/revoke",
|
||||
{"reason": "test owner stop"},
|
||||
headers={"X-CSRF-Token": session["csrf"]},
|
||||
)
|
||||
self.assertEqual(revoked["state"], "REVOKED")
|
||||
denied = self.mcp_tool(token, "zhulan_restore_lane")
|
||||
self.assertTrue(denied["isError"])
|
||||
self.assertIn("mcp/www_authenticate", denied["_meta"])
|
||||
|
||||
def test_mcp_requires_oauth_for_development_tools(self):
|
||||
response = self.request(
|
||||
"/mcp",
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"method": "tools/call",
|
||||
"params": {"name": "zhulan_restore_lane", "arguments": {}},
|
||||
},
|
||||
)
|
||||
result = response["result"]
|
||||
self.assertTrue(result["isError"])
|
||||
self.assertIn("mcp/www_authenticate", result["_meta"])
|
||||
|
||||
tools = self.request(
|
||||
"/mcp", {"jsonrpc": "2.0", "id": 5, "method": "tools/list", "params": {}}
|
||||
)["result"]["tools"]
|
||||
protected = next(tool for tool in tools if tool["name"] == "zhulan_restore_lane")
|
||||
public = next(tool for tool in tools if tool["name"] == "zhulan_request_development")
|
||||
self.assertEqual(protected["securitySchemes"][0]["type"], "oauth2")
|
||||
self.assertEqual(public["securitySchemes"][0]["type"], "noauth")
|
||||
self.assertNotIn("capability", protected["inputSchema"]["properties"])
|
||||
|
||||
def test_oauth_pkce_binds_to_approved_request_and_code_is_single_use(self):
|
||||
created = self.create(slug=f"oauth-{int(time.time())}")
|
||||
csrf = self.login()
|
||||
self.request(
|
||||
f"/api/v1/owner/requests/{created['request_id']}/approve",
|
||||
{"ttl_seconds": 10800},
|
||||
headers={"X-CSRF-Token": csrf},
|
||||
)
|
||||
registered = self.request(
|
||||
"/oauth/register",
|
||||
{
|
||||
"client_name": "ChatGPT test connector",
|
||||
"redirect_uris": ["https://chatgpt.com/connector/oauth/test-callback"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
},
|
||||
expected=201,
|
||||
)
|
||||
verifier = "v" * 64
|
||||
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
|
||||
oauth = {
|
||||
"client_id": registered["client_id"],
|
||||
"redirect_uri": registered["redirect_uris"][0],
|
||||
"state": "state-test-123",
|
||||
"code_challenge": challenge,
|
||||
"resource": "https://guanghulab.com/zhulan/mcp",
|
||||
"scope": "zhulan.develop",
|
||||
}
|
||||
session = self.request("/api/v1/owner/session")
|
||||
oauth["csrf"] = session["csrf"]
|
||||
oauth["request_id"] = created["request_id"]
|
||||
request = urllib.request.Request(
|
||||
self.base + "/oauth/authorize",
|
||||
data=__import__("urllib.parse").parse.urlencode(oauth).encode(),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
no_redirect = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(self.jar),
|
||||
urllib.request.HTTPHandler(),
|
||||
)
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
|
||||
no_redirect = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.jar), NoRedirect())
|
||||
try:
|
||||
no_redirect.open(request, timeout=3)
|
||||
self.fail("authorize should redirect")
|
||||
except urllib.error.HTTPError as exc:
|
||||
with exc:
|
||||
self.assertEqual(exc.code, 302)
|
||||
location = exc.headers["Location"]
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
code = parse_qs(urlparse(location).query)["code"][0]
|
||||
token_request = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"client_id": registered["client_id"],
|
||||
"redirect_uri": registered["redirect_uris"][0],
|
||||
"code_verifier": verifier,
|
||||
"resource": "https://guanghulab.com/zhulan/mcp",
|
||||
}
|
||||
token = self.request("/oauth/token", token_request, form=True)
|
||||
self.assertEqual(token["token_type"], "Bearer")
|
||||
replay = self.request("/oauth/token", token_request, expected=403, form=True)
|
||||
self.assertEqual(replay["error"], "invalid_grant")
|
||||
|
||||
restored = self.request(
|
||||
"/mcp",
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"method": "tools/call",
|
||||
"params": {"name": "zhulan_restore_lane", "arguments": {}},
|
||||
},
|
||||
headers={"Authorization": f"Bearer {token['access_token']}"},
|
||||
)
|
||||
content = restored["result"]["structuredContent"]
|
||||
self.assertEqual(content["development_id"], "DEV-20260813-005")
|
||||
self.assertEqual(content["topology"]["front"], "BS-GZ-006_PROXY_ONLY")
|
||||
|
||||
def test_oauth_registration_rejects_untrusted_callback(self):
|
||||
result = self.request(
|
||||
"/oauth/register",
|
||||
{
|
||||
"client_name": "evil",
|
||||
"redirect_uris": ["https://example.com/steal"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
},
|
||||
expected=400,
|
||||
)
|
||||
self.assertEqual(result["error"], "redirect_uri_not_allowed")
|
||||
|
||||
def test_existing_workspace_binding_mismatch_fails_before_git_changes(self):
|
||||
created = self.create(
|
||||
dev="DEV-20260813-098", slug="binding", base_sha=self.source_sha
|
||||
)
|
||||
token = self.approved_oauth_token(created)
|
||||
prepared = self.mcp_tool(token, "zhulan_prepare_workspace")["structuredContent"]
|
||||
workspace = (
|
||||
Path(self.temp.name)
|
||||
/ "workspaces"
|
||||
/ "DEV-20260813-098"
|
||||
/ "bingshuo__guanghu-ice-heart"
|
||||
)
|
||||
before = subprocess.run(
|
||||
["git", "-C", str(workspace), "rev-parse", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
binding = workspace / ".git" / "zhulan-binding.json"
|
||||
altered = json.loads(binding.read_text(encoding="utf-8"))
|
||||
altered["branch"] = "zhulan/DEV-20260813-098/tampered"
|
||||
binding.write_text(json.dumps(altered), encoding="utf-8")
|
||||
denied = self.mcp_tool(token, "zhulan_prepare_workspace")
|
||||
self.assertTrue(denied["isError"])
|
||||
self.assertEqual(denied["structuredContent"]["error"], "workspace_binding_mismatch")
|
||||
after = subprocess.run(
|
||||
["git", "-C", str(workspace), "rev-parse", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
self.assertEqual(before, prepared["head"])
|
||||
self.assertEqual(after, before)
|
||||
|
||||
def test_exact_reapproval_resumes_same_workspace(self):
|
||||
dev = "DEV-20260813-097"
|
||||
first = self.create(dev=dev, slug="renew", base_sha=self.source_sha)
|
||||
first_token = self.approved_oauth_token(first)
|
||||
first_prepared = self.mcp_tool(first_token, "zhulan_prepare_workspace")[
|
||||
"structuredContent"
|
||||
]
|
||||
second = self.create(dev=dev, slug="renew", base_sha=self.source_sha)
|
||||
second_token = self.approved_oauth_token(second)
|
||||
resumed = self.mcp_tool(second_token, "zhulan_prepare_workspace")[
|
||||
"structuredContent"
|
||||
]
|
||||
self.assertEqual(resumed["head"], first_prepared["head"])
|
||||
binding = (
|
||||
Path(self.temp.name)
|
||||
/ "workspaces"
|
||||
/ dev
|
||||
/ "bingshuo__guanghu-ice-heart"
|
||||
/ ".git"
|
||||
/ "zhulan-binding.json"
|
||||
)
|
||||
rebound = json.loads(binding.read_text(encoding="utf-8"))
|
||||
self.assertEqual(rebound["request_id"], second["request_id"])
|
||||
self.assertEqual(rebound["renewed_from_request_id"], first["request_id"])
|
||||
|
||||
def test_full_workspace_candidate_flow_is_scoped_and_repeatable(self):
|
||||
dev = "DEV-20260813-099"
|
||||
created = self.request(
|
||||
"/api/v1/public/requests",
|
||||
{
|
||||
"persona_id": "ICE-GL-ZL-001",
|
||||
"development_id": dev,
|
||||
"repository": "bingshuo/guanghu-ice-heart",
|
||||
"base_sha": self.source_sha,
|
||||
"branch": f"zhulan/{dev}/full-flow",
|
||||
"paths": ["server-tools/zhulan-remote-cell"],
|
||||
"actions": ["read", "edit", "test", "commit", "push_candidate"],
|
||||
"description": "端到端验证受限工作区和内部候选复核库",
|
||||
},
|
||||
expected=201,
|
||||
)
|
||||
token = self.approved_oauth_token(created)
|
||||
|
||||
prepared = self.mcp_tool(token, "zhulan_prepare_workspace")["structuredContent"]
|
||||
self.assertEqual(prepared["head"], self.source_sha)
|
||||
self.assertEqual(prepared["candidate_store"], "BS-SG-003_INTERNAL_REVIEW_ONLY")
|
||||
|
||||
read = self.mcp_tool(
|
||||
token,
|
||||
"zhulan_read_file",
|
||||
{"path": "server-tools/zhulan-remote-cell/hello.txt"},
|
||||
)["structuredContent"]
|
||||
self.assertEqual(read["content"], "before\n")
|
||||
written = self.mcp_tool(
|
||||
token,
|
||||
"zhulan_write_file",
|
||||
{
|
||||
"path": "server-tools/zhulan-remote-cell/hello.txt",
|
||||
"content": "after\n",
|
||||
"expected_sha256": read["sha256"],
|
||||
},
|
||||
)["structuredContent"]
|
||||
self.assertEqual(written["sha256"], hashlib.sha256(b"after\n").hexdigest())
|
||||
|
||||
escaped = self.mcp_tool(
|
||||
token,
|
||||
"zhulan_write_file",
|
||||
{"path": "../escape", "content": "no", "expected_sha256": ""},
|
||||
)
|
||||
self.assertTrue(escaped["isError"])
|
||||
self.assertEqual(escaped["structuredContent"]["error"], "path_traversal")
|
||||
|
||||
initial_validation = self.mcp_tool(token, "zhulan_run_validation")["structuredContent"]
|
||||
self.assertEqual(initial_validation["decision"], "PASS")
|
||||
self.assertFalse(initial_validation["clean"])
|
||||
committed = self.mcp_tool(
|
||||
token, "zhulan_commit_candidate", {"message": "test: update bounded file"}
|
||||
)["structuredContent"]
|
||||
|
||||
premature = self.mcp_tool(token, "zhulan_push_candidate")
|
||||
self.assertTrue(premature["isError"])
|
||||
final_validation = self.mcp_tool(token, "zhulan_run_validation")["structuredContent"]
|
||||
self.assertTrue(final_validation["clean"])
|
||||
self.assertEqual(final_validation["head"], committed["commit_sha"])
|
||||
pushed = self.mcp_tool(token, "zhulan_push_candidate")["structuredContent"]
|
||||
self.assertEqual(pushed["commit_sha"], committed["commit_sha"])
|
||||
self.assertEqual(pushed["central_publication"], "NOT_PERFORMED_REQUIRES_ZHUYUAN_REVIEW")
|
||||
|
||||
prepared_again = self.mcp_tool(token, "zhulan_prepare_workspace")["structuredContent"]
|
||||
self.assertEqual(prepared_again["head"], committed["commit_sha"])
|
||||
candidate = Path(self.temp.name) / "candidates" / "bingshuo__guanghu-ice-heart.git"
|
||||
remote_sha = subprocess.run(
|
||||
["git", "--git-dir", str(candidate), "rev-parse", f"refs/heads/zhulan/{dev}/full-flow"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
self.assertEqual(remote_sha, committed["commit_sha"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue