guanghu-ice-heart/server-tools/zhulan-remote-cell/runtime/zhulan_cell.py

2037 lines
95 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""铸澜手机远程开发审批与能力内核。仅使用 Python 标准库。"""
from __future__ import annotations
import base64
import hashlib
import hmac
import html
import json
import os
import re
import secrets
import socket
import sqlite3
import ssl
import subprocess
import sys
import tempfile
import threading
import time
import urllib.error
import urllib.request
from dataclasses import dataclass
from http import HTTPStatus
from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any
from urllib.parse import parse_qs, urlencode, urlparse
ROOT = Path(__file__).resolve().parents[1]
DEV_ID_RE = re.compile(r"^DEV-[0-9]{8}-[0-9]{3}$")
SHA_RE = re.compile(r"^[0-9a-f]{40}$")
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{0,63}$")
REQUEST_ID_RE = re.compile(r"^ZLR-[0-9]{8}-[A-Z0-9]{10}$")
SAFE_REPO_RE = re.compile(r"^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$")
SECRET_PATTERNS = [
re.compile(rb"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
re.compile(rb"(?i)(?:api[_-]?key|secret|password|token)\s*[:=]\s*['\"][^'\"\s]{12,}"),
re.compile(rb"AKID[A-Z0-9]{13,}"),
]
class OAuthRequiredError(PermissionError):
"""The MCP transport needs to start or renew OAuth."""
def utc_now() -> int:
return int(time.time())
def iso(ts: int | None = None) -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts or utc_now()))
def compact_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def sha256_text(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def b64url(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def b64url_decode(value: str) -> bytes:
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
def safe_path(value: Any) -> str:
if not isinstance(value, str):
raise ValueError("path_type_invalid")
text = value.strip().replace("\\", "/")
if text in ("", "."):
return "."
if text.startswith("/") or "\x00" in text:
raise ValueError("path_absolute_or_null")
parts = [part for part in text.split("/") if part not in ("", ".")]
if not parts or any(part == ".." for part in parts):
raise ValueError("path_traversal")
return "/".join(parts)
def path_within(path: str, prefix: str) -> bool:
return prefix == "." or path == prefix or path.startswith(prefix + "/")
@dataclass(frozen=True)
class Settings:
bind: str
port: int
db_path: Path
secret_file: Path
policy_file: Path
ui_dir: Path
workspace_root: Path
candidate_root: Path
forgejo_verify_url: str
owner_login: str
cookie_secure: bool
cookie_path: str
test_mode: bool
test_password: str
validation_socket: Path
@classmethod
def load(cls) -> "Settings":
return cls(
bind=os.getenv("ZHULAN_BIND", "127.0.0.1"),
port=int(os.getenv("ZHULAN_PORT", "17631")),
db_path=Path(os.getenv("ZHULAN_DB", "/var/lib/guanghu/zhulan-remote-cell/state.sqlite3")),
secret_file=Path(os.getenv("ZHULAN_SECRET_FILE", "/etc/guanghu/secrets/zhulan-remote-cell.secret")),
policy_file=Path(os.getenv("ZHULAN_POLICY", str(ROOT / "policy.example.json"))),
ui_dir=Path(os.getenv("ZHULAN_UI_DIR", str(ROOT / "ui"))),
workspace_root=Path(
os.getenv("ZHULAN_WORKSPACE_ROOT", "/srv/guanghu/zhulan-cell/workspaces")
),
candidate_root=Path(
os.getenv("ZHULAN_CANDIDATE_ROOT", "/srv/guanghu/zhulan-cell/candidates")
),
forgejo_verify_url=os.getenv(
"ZHULAN_FORGEJO_VERIFY_URL", "https://guanghulab.com/code/api/v1/user"
),
owner_login=os.getenv("ZHULAN_OWNER_LOGIN", "bingshuo"),
cookie_secure=os.getenv("ZHULAN_COOKIE_SECURE", "1") != "0",
cookie_path=os.getenv("ZHULAN_COOKIE_PATH", "/zhulan/"),
test_mode=os.getenv("ZHULAN_TEST_MODE", "0") == "1",
test_password=os.getenv("ZHULAN_TEST_OWNER_PASSWORD", ""),
validation_socket=Path(
os.getenv(
"ZHULAN_VALIDATION_SOCKET",
"/run/guanghu/zhulan-validation/executor.sock",
)
),
)
class Store:
def __init__(self, path: Path):
self.path = path
path.parent.mkdir(parents=True, exist_ok=True)
self._init()
def connect(self) -> sqlite3.Connection:
db = sqlite3.connect(self.path, timeout=10)
db.row_factory = sqlite3.Row
db.execute("PRAGMA foreign_keys=ON")
db.execute("PRAGMA journal_mode=WAL")
return db
def _init(self) -> None:
with self.connect() as db:
db.executescript(
"""
CREATE TABLE IF NOT EXISTS requests (
id TEXT PRIMARY KEY,
claim_hash TEXT NOT NULL,
persona_id TEXT NOT NULL,
development_id TEXT NOT NULL,
repository TEXT NOT NULL,
base_sha TEXT NOT NULL,
branch TEXT NOT NULL,
paths_json TEXT NOT NULL,
actions_json TEXT NOT NULL,
description TEXT NOT NULL,
state TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
decided_at INTEGER,
owner_login TEXT,
capability_nonce TEXT,
capability_hash TEXT,
capability_expires_at INTEGER,
picked_up_at INTEGER,
rejection_reason TEXT
);
CREATE INDEX IF NOT EXISTS requests_state_created
ON requests(state, created_at DESC);
CREATE TABLE IF NOT EXISTS owner_sessions (
session_hash TEXT PRIMARY KEY,
owner_login TEXT NOT NULL,
csrf_hash TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS receipts (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
receipt_id TEXT UNIQUE NOT NULL,
request_id TEXT,
event TEXT NOT NULL,
result TEXT NOT NULL,
evidence_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
previous_hash TEXT NOT NULL,
receipt_hash TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS oauth_clients (
client_id TEXT PRIMARY KEY,
client_name TEXT NOT NULL,
redirect_uris_json TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS oauth_codes (
code_hash TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
code_challenge TEXT NOT NULL,
resource TEXT NOT NULL,
request_id TEXT NOT NULL,
scopes_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
used_at INTEGER,
FOREIGN KEY(request_id) REFERENCES requests(id)
);
CREATE TABLE IF NOT EXISTS oauth_tokens (
token_hash TEXT PRIMARY KEY,
request_id TEXT NOT NULL,
client_id TEXT NOT NULL,
scopes_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
revoked_at INTEGER,
FOREIGN KEY(request_id) REFERENCES requests(id)
);
"""
)
def append_receipt(
self, db: sqlite3.Connection, request_id: str | None, event: str, result: str, evidence: dict[str, Any]
) -> dict[str, Any]:
# A receipt query followed by an insert must be one serialized operation;
# otherwise two concurrent read-only callers could fork the evidence chain.
if not db.in_transaction:
db.execute("BEGIN IMMEDIATE")
previous = db.execute("SELECT receipt_hash FROM receipts ORDER BY sequence DESC LIMIT 1").fetchone()
previous_hash = previous[0] if previous else "0" * 64
created = utc_now()
rid = f"ZLC-{time.strftime('%Y%m%d', time.gmtime(created))}-{secrets.token_hex(5).upper()}"
body = {
"receipt_id": rid,
"request_id": request_id,
"event": event,
"result": result,
"evidence": evidence,
"created_at": iso(created),
"previous_hash": previous_hash,
}
receipt_hash = sha256_text(compact_json(body))
db.execute(
"""INSERT INTO receipts
(receipt_id, request_id, event, result, evidence_json, created_at, previous_hash, receipt_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(rid, request_id, event, result, compact_json(evidence), created, previous_hash, receipt_hash),
)
body["receipt_hash"] = receipt_hash
return body
class ZhulanApp:
def __init__(self, settings: Settings):
self.settings = settings
self.policy = json.loads(settings.policy_file.read_text(encoding="utf-8"))
self._validate_policy()
self.secret = self._load_secret()
self.store = Store(settings.db_path)
settings.workspace_root.mkdir(parents=True, exist_ok=True)
settings.candidate_root.mkdir(parents=True, exist_ok=True)
self.rate: dict[str, list[int]] = {}
self.rate_lock = threading.Lock()
self.operation_lock = threading.RLock()
def _validate_policy(self) -> None:
required = {"persona_id", "node_id", "allowed_actions", "repositories"}
if not required.issubset(self.policy):
raise RuntimeError("policy_missing_required_fields")
if self.policy.get("node_id") != "BS-SG-003":
raise RuntimeError("runtime_node_must_be_bs_sg_003")
def _load_secret(self) -> bytes:
if self.settings.secret_file.exists():
raw = self.settings.secret_file.read_bytes().strip()
if len(raw) < 32:
raise RuntimeError("application_secret_too_short")
return raw
if self.settings.test_mode:
return hashlib.sha256(b"zhulan-test-secret-only").digest()
raise RuntimeError(f"application_secret_missing:{self.settings.secret_file}")
def public_config(self) -> dict[str, Any]:
return {
"schema": "guanghu.zhulan-public-config/v1",
"persona_id": self.policy["persona_id"],
"front_door": self.policy.get("front_door"),
"runtime_node": self.policy["node_id"],
"topology": {
"front": "BS-GZ-006 · 仅域名 / TLS / 反向代理",
"runtime": "BS-SG-003 · 真实 UI / 审批 / 能力 / 沙箱 / 门禁 / 回执",
},
"default_ttl_seconds": int(self.policy.get("default_ttl_seconds", 10800)),
"repositories": sorted(self.policy["repositories"]),
"allowed_actions": list(self.policy["allowed_actions"]),
"ui_brains": [
{"id": "GHS-012", "state": "CANDIDATE_READ_ONLY"},
{"id": "GHS-014", "state": "CANDIDATE_READ_ONLY"},
],
}
def rate_ok(self, bucket: str, source: str, limit: int, window_seconds: int) -> bool:
now = utc_now()
key = f"{bucket}:{sha256_text(source)}"
with self.rate_lock:
points = [x for x in self.rate.get(key, []) if x > now - window_seconds]
if len(points) >= limit:
self.rate[key] = points
return False
points.append(now)
self.rate[key] = points
return True
def validate_request(self, data: dict[str, Any]) -> dict[str, Any]:
persona = str(data.get("persona_id", ""))
if persona != self.policy["persona_id"]:
raise ValueError("persona_not_allowed")
dev_id = str(data.get("development_id", ""))
if not DEV_ID_RE.fullmatch(dev_id):
raise ValueError("development_id_invalid")
repository = str(data.get("repository", ""))
if not SAFE_REPO_RE.fullmatch(repository) or repository not in self.policy["repositories"]:
raise ValueError("repository_not_registered")
base_sha = str(data.get("base_sha", "")).lower()
if not SHA_RE.fullmatch(base_sha):
raise ValueError("base_sha_invalid")
branch = str(data.get("branch", ""))
expected = f"zhulan/{dev_id}/"
if (
not branch.startswith(expected)
or not SLUG_RE.fullmatch(branch[len(expected) :])
or ".." in branch
or "@{" in branch
or branch.endswith((".", ".lock"))
):
raise ValueError("candidate_branch_invalid")
raw_paths = data.get("paths")
if not isinstance(raw_paths, list) or not raw_paths:
raise ValueError("paths_required")
max_paths = int(self.policy.get("max_request_paths", 24))
if len(raw_paths) > max_paths:
raise ValueError("too_many_paths")
paths = sorted(set(safe_path(p) for p in raw_paths))
repo_policy = self.policy["repositories"][repository]
allowed_prefixes = [safe_path(p) for p in repo_policy.get("allowed_path_prefixes", ["."])]
if any(not any(path_within(path, prefix) for prefix in allowed_prefixes) for path in paths):
raise ValueError("path_outside_repository_policy")
raw_actions = data.get("actions")
if not isinstance(raw_actions, list) or not raw_actions:
raise ValueError("actions_required")
actions = sorted(set(str(x) for x in raw_actions))
allowed_actions = set(self.policy["allowed_actions"])
if any(action not in allowed_actions for action in actions):
raise ValueError("action_not_allowed")
description = str(data.get("description", "")).strip()
if not (4 <= len(description) <= 500):
raise ValueError("description_invalid")
return {
"persona_id": persona,
"development_id": dev_id,
"repository": repository,
"base_sha": base_sha,
"branch": branch,
"paths": paths,
"actions": actions,
"description": description,
}
def create_request(self, data: dict[str, Any], source: str) -> dict[str, Any]:
if not self.rate_ok(
"request-create", source, int(os.getenv("ZHULAN_PUBLIC_CREATE_LIMIT", "12")), 3600
):
raise PermissionError("public_create_rate_limited")
clean = self.validate_request(data)
now = utc_now()
request_id = f"ZLR-{time.strftime('%Y%m%d', time.gmtime(now))}-{secrets.token_hex(5).upper()}"
claim = secrets.token_urlsafe(32)
expires = now + min(int(self.policy.get("max_ttl_seconds", 86400)), 86400)
with self.store.connect() as db:
db.execute(
"""INSERT INTO requests
(id, claim_hash, persona_id, development_id, repository, base_sha, branch,
paths_json, actions_json, description, state, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'PENDING', ?, ?)""",
(
request_id,
sha256_text(claim),
clean["persona_id"],
clean["development_id"],
clean["repository"],
clean["base_sha"],
clean["branch"],
compact_json(clean["paths"]),
compact_json(clean["actions"]),
clean["description"],
now,
expires,
),
)
receipt = self.store.append_receipt(
db,
request_id,
"request_created",
"PENDING",
{
"persona_id": clean["persona_id"],
"development_id": clean["development_id"],
"repository": clean["repository"],
"branch": clean["branch"],
"source_hash": sha256_text(source),
},
)
return {
"schema": "guanghu.zhulan-request-created/v1",
"request_id": request_id,
"claim_token": claim,
"state": "PENDING",
"request_url": f"{self.policy.get('front_door', '/zhulan/')}?request={request_id}",
"expires_at": iso(expires),
"receipt": receipt,
"warning": "claim_token 仅返回一次,不得写入仓库、日志或长期记忆。",
}
@staticmethod
def row_to_public(row: sqlite3.Row) -> dict[str, Any]:
return {
"id": row["id"],
"persona_id": row["persona_id"],
"development_id": row["development_id"],
"repository": row["repository"],
"base_sha": row["base_sha"],
"branch": row["branch"],
"paths": json.loads(row["paths_json"]),
"actions": json.loads(row["actions_json"]),
"description": row["description"],
"state": row["state"],
"created_at": iso(row["created_at"]),
"expires_at": iso(row["expires_at"]),
"decided_at": iso(row["decided_at"]) if row["decided_at"] else None,
"capability_expires_at": iso(row["capability_expires_at"])
if row["capability_expires_at"]
else None,
"picked_up": bool(row["picked_up_at"]),
"rejection_reason": row["rejection_reason"],
}
def claimed_request(self, request_id: str, claim: str) -> sqlite3.Row:
if not REQUEST_ID_RE.fullmatch(request_id) or not claim:
raise PermissionError("claim_invalid")
with self.store.connect() as db:
row = db.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone()
if not row or not hmac.compare_digest(row["claim_hash"], sha256_text(claim)):
raise PermissionError("claim_invalid")
return row
def request_status(self, request_id: str, claim: str) -> dict[str, Any]:
return self.row_to_public(self.claimed_request(request_id, claim))
def verify_owner_credentials(self, username: str, password: str) -> dict[str, Any]:
if not username or not password or username != self.settings.owner_login:
raise PermissionError("owner_login_invalid")
if self.settings.test_mode:
if not self.settings.test_password or not hmac.compare_digest(password, self.settings.test_password):
raise PermissionError("owner_login_invalid")
return {"login": username, "id": 1}
auth = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
request = urllib.request.Request(
self.settings.forgejo_verify_url,
headers={"Authorization": f"Basic {auth}", "User-Agent": "guanghu-zhulan-cell/1"},
)
try:
context = ssl.create_default_context()
with urllib.request.urlopen(request, timeout=8, context=context) as response:
user = json.loads(response.read(65536).decode("utf-8"))
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, ValueError) as exc:
raise PermissionError("owner_login_invalid") from exc
if user.get("login") != self.settings.owner_login:
raise PermissionError("owner_login_invalid")
return user
def owner_login(
self, username: str, password: str, source: str
) -> tuple[str, str, dict[str, Any]]:
if not self.rate_ok(
"owner-login", source, int(os.getenv("ZHULAN_OWNER_LOGIN_LIMIT", "8")), 900
):
raise PermissionError("owner_login_rate_limited")
user = self.verify_owner_credentials(username, password)
token = secrets.token_urlsafe(32)
csrf = secrets.token_urlsafe(24)
now = utc_now()
expires = now + 10800
with self.store.connect() as db:
db.execute("DELETE FROM owner_sessions WHERE expires_at < ?", (now,))
db.execute(
"INSERT INTO owner_sessions VALUES (?, ?, ?, ?, ?)",
(sha256_text(token), user["login"], sha256_text(csrf), now, expires),
)
self.store.append_receipt(
db,
None,
"owner_login",
"PASS",
{"owner_login": user["login"], "forgejo_user_id": user.get("id")},
)
return token, csrf, {"owner_login": user["login"], "expires_at": iso(expires)}
def owner_session(self, token: str | None, rotate_csrf: bool = False) -> tuple[sqlite3.Row, str | None]:
if not token:
raise PermissionError("owner_session_required")
now = utc_now()
with self.store.connect() as db:
row = db.execute(
"SELECT * FROM owner_sessions WHERE session_hash=? AND expires_at>?",
(sha256_text(token), now),
).fetchone()
if not row:
raise PermissionError("owner_session_required")
csrf = None
if rotate_csrf:
csrf = secrets.token_urlsafe(24)
db.execute(
"UPDATE owner_sessions SET csrf_hash=? WHERE session_hash=?",
(sha256_text(csrf), sha256_text(token)),
)
return row, csrf
def require_csrf(self, session: sqlite3.Row, csrf: str | None) -> None:
if not csrf or not hmac.compare_digest(session["csrf_hash"], sha256_text(csrf)):
raise PermissionError("csrf_invalid")
def owner_requests(self) -> list[dict[str, Any]]:
with self.store.connect() as db:
rows = db.execute("SELECT * FROM requests ORDER BY created_at DESC LIMIT 100").fetchall()
return [self.row_to_public(row) for row in rows]
def make_capability(self, row: sqlite3.Row) -> str:
payload = {
"schema": "guanghu.zhulan-capability/v1",
"request_id": row["id"],
"persona_id": row["persona_id"],
"development_id": row["development_id"],
"node_id": self.policy["node_id"],
"repository": row["repository"],
"base_sha": row["base_sha"],
"branch": row["branch"],
"paths": json.loads(row["paths_json"]),
"actions": json.loads(row["actions_json"]),
"iat": row["decided_at"],
"exp": row["capability_expires_at"],
"nonce": row["capability_nonce"],
}
encoded = b64url(compact_json(payload).encode("utf-8"))
signature = b64url(hmac.new(self.secret, f"v1.{encoded}".encode("ascii"), hashlib.sha256).digest())
return f"v1.{encoded}.{signature}"
def decide(
self,
request_id: str,
owner: str,
approve: bool,
ttl_seconds: int | None = None,
reason: str = "",
) -> dict[str, Any]:
if not REQUEST_ID_RE.fullmatch(request_id):
raise ValueError("request_id_invalid")
now = utc_now()
with self.store.connect() as db:
row = db.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone()
if not row:
raise LookupError("request_not_found")
if row["state"] != "PENDING":
raise RuntimeError("request_already_decided")
if row["expires_at"] <= now:
db.execute("UPDATE requests SET state='EXPIRED', decided_at=? WHERE id=?", (now, request_id))
self.store.append_receipt(db, request_id, "request_expired", "EXPIRED", {})
raise RuntimeError("request_expired")
if approve:
default_ttl = int(self.policy.get("default_ttl_seconds", 10800))
max_ttl = int(self.policy.get("max_ttl_seconds", 86400))
ttl = int(ttl_seconds or default_ttl)
if ttl < 900 or ttl > max_ttl:
raise ValueError("ttl_out_of_policy")
cap_expires = now + ttl
nonce = secrets.token_urlsafe(18)
db.execute(
"""UPDATE requests SET state='APPROVED', decided_at=?, owner_login=?,
capability_nonce=?, capability_expires_at=? WHERE id=?""",
(now, owner, nonce, cap_expires, request_id),
)
updated = db.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone()
token = self.make_capability(updated)
db.execute("UPDATE requests SET capability_hash=? WHERE id=?", (sha256_text(token), request_id))
receipt = self.store.append_receipt(
db,
request_id,
"human_approved",
"APPROVED",
{
"owner_login": owner,
"capability_expires_at": iso(cap_expires),
"binding_hash": sha256_text(
compact_json(
{
"persona": row["persona_id"],
"development_id": row["development_id"],
"repository": row["repository"],
"base_sha": row["base_sha"],
"branch": row["branch"],
"paths": json.loads(row["paths_json"]),
"actions": json.loads(row["actions_json"]),
}
)
),
},
)
return {"state": "APPROVED", "capability_expires_at": iso(cap_expires), "receipt": receipt}
reason = reason.strip()[:240] or "主人拒绝本次申请"
db.execute(
"UPDATE requests SET state='REJECTED', decided_at=?, owner_login=?, rejection_reason=? WHERE id=?",
(now, owner, reason, request_id),
)
receipt = self.store.append_receipt(
db, request_id, "human_rejected", "REJECTED", {"owner_login": owner, "reason": reason}
)
return {"state": "REJECTED", "receipt": receipt}
def pickup(self, request_id: str, claim: str) -> dict[str, Any]:
self.claimed_request(request_id, claim)
now = utc_now()
with self.store.connect() as db:
row = db.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone()
if row["state"] != "APPROVED":
raise RuntimeError(f"request_not_approved:{row['state']}")
if row["capability_expires_at"] <= now:
db.execute("UPDATE requests SET state='EXPIRED' WHERE id=?", (request_id,))
self.store.append_receipt(db, request_id, "capability_expired", "EXPIRED", {})
raise RuntimeError("capability_expired")
if row["picked_up_at"]:
raise RuntimeError("capability_already_picked_up")
token = self.make_capability(row)
if not hmac.compare_digest(row["capability_hash"], sha256_text(token)):
raise RuntimeError("capability_reconstruction_failed")
db.execute("UPDATE requests SET picked_up_at=? WHERE id=?", (now, request_id))
receipt = self.store.append_receipt(
db,
request_id,
"capability_picked_up",
"PASS",
{"capability_hash": row["capability_hash"], "expires_at": iso(row["capability_expires_at"])},
)
return {
"schema": "guanghu.zhulan-capability-pickup/v1",
"capability": token,
"expires_at": iso(row["capability_expires_at"]),
"receipt": receipt,
"warning": "临时能力仅返回一次;只交给受限执行器,不得写入仓库或长期日志。",
}
def revoke(self, request_id: str, owner: str, reason: str = "") -> dict[str, Any]:
if not REQUEST_ID_RE.fullmatch(request_id):
raise ValueError("request_id_invalid")
now = utc_now()
clean_reason = reason.strip()[:240] or "主人主动撤销本次开发能力"
with self.store.connect() as db:
row = db.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone()
if not row:
raise LookupError("request_not_found")
if row["state"] != "APPROVED":
raise RuntimeError("only_approved_request_can_be_revoked")
db.execute(
"""UPDATE requests SET state='REVOKED', capability_expires_at=?,
rejection_reason=? WHERE id=?""",
(now, clean_reason, request_id),
)
db.execute(
"UPDATE oauth_tokens SET revoked_at=? WHERE request_id=? AND revoked_at IS NULL",
(now, request_id),
)
receipt = self.store.append_receipt(
db,
request_id,
"human_revoked",
"REVOKED",
{"owner_login": owner, "reason": clean_reason},
)
return {"state": "REVOKED", "receipt": receipt}
def verify_capability(self, token: str, expected_action: str | None = None) -> dict[str, Any]:
try:
version, encoded, signature = token.split(".", 2)
expected_sig = b64url(
hmac.new(self.secret, f"{version}.{encoded}".encode("ascii"), hashlib.sha256).digest()
)
if version != "v1" or not hmac.compare_digest(signature, expected_sig):
raise ValueError
payload = json.loads(b64url_decode(encoded))
except (ValueError, json.JSONDecodeError, UnicodeDecodeError) as exc:
raise PermissionError("capability_invalid") from exc
now = utc_now()
if payload.get("node_id") != self.policy["node_id"] or int(payload.get("exp", 0)) <= now:
raise PermissionError("capability_expired_or_wrong_node")
if expected_action and expected_action not in payload.get("actions", []):
raise PermissionError("capability_action_denied")
with self.store.connect() as db:
row = db.execute("SELECT * FROM requests WHERE id=?", (payload.get("request_id"),)).fetchone()
if (
not row
or row["state"] != "APPROVED"
or not row["picked_up_at"]
or not hmac.compare_digest(row["capability_hash"], sha256_text(token))
):
raise PermissionError("capability_not_active")
receipt = self.store.append_receipt(
db,
row["id"],
"capability_verified",
"PASS",
{"action": expected_action or "inspect", "capability_hash": row["capability_hash"]},
)
return {"claims": payload, "receipt": receipt}
def logout(self, token: str | None) -> None:
if not token:
return
with self.store.connect() as db:
db.execute("DELETE FROM owner_sessions WHERE session_hash=?", (sha256_text(token),))
def receipts(self, request_id: str | None = None) -> list[dict[str, Any]]:
with self.store.connect() as db:
if request_id is None:
rows = db.execute("SELECT * FROM receipts ORDER BY sequence").fetchall()
else:
rows = db.execute(
"SELECT * FROM receipts WHERE request_id=? ORDER BY sequence", (request_id,)
).fetchall()
return [
{
"sequence": row["sequence"],
"receipt_id": row["receipt_id"],
"request_id": row["request_id"],
"event": row["event"],
"result": row["result"],
"evidence": json.loads(row["evidence_json"]),
"created_at": iso(row["created_at"]),
"previous_hash": row["previous_hash"],
"receipt_hash": row["receipt_hash"],
}
for row in rows
]
@staticmethod
def public_origin() -> str:
return "https://guanghulab.com/zhulan"
def protected_resource_metadata(self) -> dict[str, Any]:
origin = self.public_origin()
return {
"resource": f"{origin}/mcp",
"authorization_servers": [origin],
"scopes_supported": ["zhulan.request", "zhulan.develop"],
"resource_documentation": f"{origin}/",
"bearer_methods_supported": ["header"],
}
def oauth_metadata(self) -> dict[str, Any]:
origin = self.public_origin()
return {
"issuer": origin,
"authorization_endpoint": f"{origin}/oauth/authorize",
"token_endpoint": f"{origin}/oauth/token",
"registration_endpoint": f"{origin}/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["zhulan.request", "zhulan.develop"],
}
@staticmethod
def valid_redirect_uri(uri: str) -> bool:
try:
parsed = urlparse(uri)
except ValueError:
return False
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password or parsed.fragment:
return False
return (
parsed.hostname in {"chatgpt.com", "chat.openai.com"}
and parsed.path.startswith(("/connector/oauth/", "/connector_platform_oauth_redirect"))
)
def register_oauth_client(self, data: dict[str, Any], source: str) -> dict[str, Any]:
if not self.rate_ok(
"oauth-register", source, int(os.getenv("ZHULAN_OAUTH_REGISTER_LIMIT", "20")), 3600
):
raise PermissionError("oauth_registration_rate_limited")
redirects = data.get("redirect_uris")
if not isinstance(redirects, list) or not redirects or len(redirects) > 8:
raise ValueError("redirect_uris_invalid")
redirects = sorted(set(str(uri) for uri in redirects))
if any(not self.valid_redirect_uri(uri) for uri in redirects):
raise ValueError("redirect_uri_not_allowed")
methods = data.get("token_endpoint_auth_method", "none")
if methods != "none":
raise ValueError("token_endpoint_auth_method_not_supported")
client_id = f"zlc_{secrets.token_urlsafe(18)}"
name = str(data.get("client_name", "ChatGPT Zhulan Connector"))[:120]
now = utc_now()
with self.store.connect() as db:
db.execute("DELETE FROM oauth_clients WHERE created_at < ?", (now - 7 * 86400,))
db.execute(
"INSERT INTO oauth_clients VALUES (?, ?, ?, ?)",
(client_id, name, compact_json(redirects), now),
)
return {
"client_id": client_id,
"client_id_issued_at": now,
"client_name": name,
"redirect_uris": redirects,
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
}
def validate_oauth_authorize(self, query: dict[str, list[str]]) -> dict[str, str]:
def one(name: str) -> str:
values = query.get(name, [])
if len(values) != 1:
raise ValueError(f"oauth_{name}_invalid")
return values[0]
client_id = one("client_id")
redirect_uri = one("redirect_uri")
response_type = one("response_type")
state = one("state")
code_challenge = one("code_challenge")
method = one("code_challenge_method")
resource = one("resource")
scope = one("scope")
if response_type != "code" or method != "S256":
raise ValueError("oauth_flow_must_use_code_pkce_s256")
if resource != f"{self.public_origin()}/mcp":
raise ValueError("oauth_resource_invalid")
scopes = sorted(set(scope.split()))
if not scopes or any(item not in {"zhulan.request", "zhulan.develop"} for item in scopes):
raise ValueError("oauth_scope_invalid")
if not re.fullmatch(r"[A-Za-z0-9_-]{43,128}", code_challenge):
raise ValueError("oauth_code_challenge_invalid")
with self.store.connect() as db:
client = db.execute("SELECT * FROM oauth_clients WHERE client_id=?", (client_id,)).fetchone()
if not client or redirect_uri not in json.loads(client["redirect_uris_json"]):
raise PermissionError("oauth_client_or_redirect_invalid")
return {
"client_id": client_id,
"redirect_uri": redirect_uri,
"state": state,
"code_challenge": code_challenge,
"resource": resource,
"scope": " ".join(scopes),
}
def authorize_oauth_request(
self,
owner_token: str | None,
csrf: str | None,
oauth: dict[str, str],
request_id: str,
) -> str:
session, _ = self.owner_session(owner_token)
self.require_csrf(session, csrf)
if not REQUEST_ID_RE.fullmatch(request_id):
raise ValueError("request_id_invalid")
now = utc_now()
with self.store.connect() as db:
row = db.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone()
if not row or row["state"] != "APPROVED" or int(row["capability_expires_at"] or 0) <= now:
raise PermissionError("approved_request_required")
if not row["picked_up_at"]:
db.execute("UPDATE requests SET picked_up_at=? WHERE id=?", (now, request_id))
code = secrets.token_urlsafe(32)
db.execute(
"""INSERT INTO oauth_codes
(code_hash, client_id, redirect_uri, code_challenge, resource, request_id,
scopes_json, created_at, expires_at, used_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)""",
(
sha256_text(code),
oauth["client_id"],
oauth["redirect_uri"],
oauth["code_challenge"],
oauth["resource"],
request_id,
compact_json(oauth["scope"].split()),
now,
now + 300,
),
)
self.store.append_receipt(
db,
request_id,
"plugin_connection_authorized",
"PASS",
{
"owner_login": session["owner_login"],
"client_id_hash": sha256_text(oauth["client_id"]),
"scopes": oauth["scope"].split(),
},
)
separator = "&" if "?" in oauth["redirect_uri"] else "?"
return f"{oauth['redirect_uri']}{separator}{urlencode({'code': code, 'state': oauth['state']})}"
def exchange_oauth_code(self, data: dict[str, str]) -> dict[str, Any]:
if data.get("grant_type") != "authorization_code":
raise ValueError("unsupported_grant_type")
required = ["code", "client_id", "redirect_uri", "code_verifier", "resource"]
if any(not data.get(key) for key in required):
raise ValueError("oauth_token_request_incomplete")
if data["resource"] != f"{self.public_origin()}/mcp":
raise ValueError("oauth_resource_invalid")
verifier = data["code_verifier"]
if not re.fullmatch(r"[A-Za-z0-9._~-]{43,128}", verifier):
raise ValueError("oauth_code_verifier_invalid")
challenge = b64url(hashlib.sha256(verifier.encode("ascii")).digest())
now = utc_now()
with self.store.connect() as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT * FROM oauth_codes WHERE code_hash=?", (sha256_text(data["code"]),)).fetchone()
if (
not row
or row["used_at"]
or row["expires_at"] <= now
or row["client_id"] != data["client_id"]
or row["redirect_uri"] != data["redirect_uri"]
or row["resource"] != data["resource"]
or not hmac.compare_digest(row["code_challenge"], challenge)
):
raise PermissionError("invalid_grant")
request_row = db.execute("SELECT * FROM requests WHERE id=?", (row["request_id"],)).fetchone()
if not request_row or request_row["state"] != "APPROVED" or request_row["capability_expires_at"] <= now:
raise PermissionError("approved_request_expired")
token = secrets.token_urlsafe(42)
expires = min(request_row["capability_expires_at"], now + 10800)
db.execute("UPDATE oauth_codes SET used_at=? WHERE code_hash=?", (now, row["code_hash"]))
db.execute(
"INSERT INTO oauth_tokens VALUES (?, ?, ?, ?, ?, ?, NULL)",
(sha256_text(token), row["request_id"], row["client_id"], row["scopes_json"], now, expires),
)
self.store.append_receipt(
db,
row["request_id"],
"plugin_access_token_issued",
"PASS",
{"client_id_hash": sha256_text(row["client_id"]), "expires_at": iso(expires)},
)
return {
"access_token": token,
"token_type": "Bearer",
"expires_in": expires - now,
"scope": " ".join(json.loads(row["scopes_json"])),
}
def oauth_binding(self, bearer: str | None, required_scope: str) -> sqlite3.Row:
if not bearer:
raise PermissionError("oauth_required")
now = utc_now()
with self.store.connect() as db:
row = db.execute(
"""SELECT t.*, r.state AS request_state, r.capability_expires_at,
r.capability_nonce, r.capability_hash, r.picked_up_at,
r.id, r.persona_id, r.development_id, r.repository, r.base_sha,
r.branch, r.paths_json, r.actions_json, r.decided_at
FROM oauth_tokens t JOIN requests r ON r.id=t.request_id
WHERE t.token_hash=?""",
(sha256_text(bearer),),
).fetchone()
if (
not row
or row["revoked_at"]
or row["expires_at"] <= now
or row["request_state"] != "APPROVED"
or row["capability_expires_at"] <= now
or required_scope not in json.loads(row["scopes_json"])
):
raise PermissionError("oauth_token_invalid_or_insufficient_scope")
return row
def capability_for_oauth(self, row: sqlite3.Row) -> str:
# OAuth is the transport/session identity. The execution kernel still
# verifies the exact approved capability on every operation.
request = self._request_row_for_claims(row["request_id"])
token = self.make_capability(request)
if not hmac.compare_digest(request["capability_hash"], sha256_text(token)):
raise PermissionError("oauth_capability_binding_invalid")
return token
def _git(self, workspace: Path, *args: str, timeout: int = 60) -> str:
env = {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"HOME": str(self.settings.workspace_root),
"LANG": "C.UTF-8",
"GIT_TERMINAL_PROMPT": "0",
}
result = subprocess.run(
["git", "-C", str(workspace), *args],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout,
env=env,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip().splitlines()[-1:] or ["git_failed"]
raise RuntimeError(f"git_failed:{detail[0][:240]}")
return result.stdout.strip()
def _claims(self, capability: str, action: str) -> dict[str, Any]:
return self.verify_capability(capability, action)["claims"]
def _workspace(self, claims: dict[str, Any]) -> Path:
owner, repo = claims["repository"].split("/", 1)
name = f"{owner}__{repo}"
workspace = (
self.settings.workspace_root / claims["development_id"] / name
).resolve()
workspace.relative_to(self.settings.workspace_root.resolve())
return workspace
def _candidate_repo(self, claims: dict[str, Any]) -> Path:
owner, repo = claims["repository"].split("/", 1)
candidate = (self.settings.candidate_root / f"{owner}__{repo}.git").resolve()
candidate.relative_to(self.settings.candidate_root.resolve())
return candidate
def _ensure_candidate_repo(self, claims: dict[str, Any]) -> Path:
candidate = self._candidate_repo(claims)
if candidate.exists() and not candidate.joinpath("HEAD").is_file():
raise RuntimeError("candidate_store_not_bare_repository")
if not candidate.exists():
candidate.parent.mkdir(parents=True, exist_ok=True)
result = subprocess.run(
["git", "init", "--bare", "--initial-branch=main", str(candidate)],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=30,
env={"PATH": "/usr/local/bin:/usr/bin:/bin", "LANG": "C.UTF-8"},
)
if result.returncode != 0:
raise RuntimeError("candidate_store_init_failed")
subprocess.run(
["git", "--git-dir", str(candidate), "config", "receive.denyNonFastForwards", "true"],
check=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=10,
)
return candidate
@staticmethod
def _ensure_claim_path(claims: dict[str, Any], path_value: Any) -> str:
path = safe_path(path_value)
if path == ".git" or path.startswith(".git/"):
raise PermissionError("git_metadata_is_server_owned")
if path == "." or not any(path_within(path, prefix) for prefix in claims["paths"]):
raise PermissionError("path_outside_capability")
return path
def _request_row_for_claims(self, request_id: str) -> sqlite3.Row:
with self.store.connect() as db:
row = db.execute("SELECT * FROM requests WHERE id=?", (request_id,)).fetchone()
if not row:
raise PermissionError("request_not_found")
return row
def _operation_receipt(
self, request_id: str, event: str, result: str, evidence: dict[str, Any]
) -> dict[str, Any]:
with self.store.connect() as db:
return self.store.append_receipt(db, request_id, event, result, evidence)
def restore_lane(self, capability: str) -> dict[str, Any]:
claims = self._claims(capability, "read")
workspace = self._workspace(claims)
git_state: dict[str, Any] = {"prepared": workspace.joinpath(".git").is_dir()}
if git_state["prepared"]:
try:
git_state.update(
{
"branch": self._git(workspace, "branch", "--show-current"),
"head": self._git(workspace, "rev-parse", "HEAD"),
"changes": self._git(workspace, "status", "--short").splitlines(),
}
)
except RuntimeError:
git_state["status"] = "BROKEN_FAIL_CLOSED"
recent = self.receipts(claims["request_id"])[-12:]
return {
"schema": "guanghu.zhulan-lane-restore/v1",
"persona_identity": claims["persona_id"],
"development_id": claims["development_id"],
"topology": {
"front": "BS-GZ-006_PROXY_ONLY",
"runtime": "BS-SG-003_REAL_RUNTIME",
},
"repository": claims["repository"],
"base_sha": claims["base_sha"],
"candidate_branch": claims["branch"],
"allowed_paths": claims["paths"],
"allowed_actions": claims["actions"],
"expires_at": iso(claims["exp"]),
"workspace": git_state,
"candidate_store": "BS-SG-003_INTERNAL_REVIEW_ONLY",
"recent_receipts": recent,
"hard_rule": "只在已批准边界内继续;不确定、越界或证据未知时失败关闭并重新申请。",
}
def prepare_workspace(self, capability: str) -> dict[str, Any]:
claims = self._claims(capability, "read")
workspace = self._workspace(claims)
binding_core = {
key: claims[key]
for key in (
"persona_id",
"development_id",
"repository",
"base_sha",
"branch",
"paths",
"actions",
)
}
binding = {"request_id": claims["request_id"], **binding_core}
if workspace.exists() and (
workspace.joinpath(".git").is_symlink()
or not workspace.joinpath(".git").is_dir()
):
raise RuntimeError("workspace_exists_without_git")
binding_path = workspace / ".git" / "zhulan-binding.json"
if binding_path.exists():
old = json.loads(binding_path.read_text(encoding="utf-8"))
old_core = {key: old.get(key) for key in binding_core}
if old_core != binding_core:
raise RuntimeError("workspace_binding_mismatch")
if old.get("request_id") != claims["request_id"]:
binding["renewed_from_request_id"] = old.get("request_id")
binding_path.write_text(compact_json(binding) + "\n", encoding="utf-8")
binding_path.chmod(0o600)
repo_url = self.policy.get("repository_base_url", "https://guanghulab.com/code/").rstrip("/")
repo_url = f"{repo_url}/{claims['repository']}.git"
if not workspace.exists():
workspace.parent.mkdir(parents=True, exist_ok=True)
env = {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"HOME": str(self.settings.workspace_root),
"LANG": "C.UTF-8",
"GIT_TERMINAL_PROMPT": "0",
}
result = subprocess.run(
["git", "clone", "--no-tags", "--filter=blob:none", repo_url, str(workspace)],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=180,
env=env,
)
if result.returncode != 0:
raise RuntimeError("repository_clone_failed")
initial_remotes = self._git(workspace, "remote").splitlines()
source_remote = "upstream" if "upstream" in initial_remotes else "origin"
current_branch = self._git(workspace, "branch", "--show-current")
status = self._git(workspace, "status", "--porcelain")
if status and current_branch != claims["branch"]:
raise RuntimeError("workspace_dirty_on_other_branch")
try:
self._git(workspace, "cat-file", "-e", f"{claims['base_sha']}^{{commit}}")
except RuntimeError:
self._git(workspace, "fetch", "--no-tags", source_remote, "main", timeout=180)
self._git(workspace, "cat-file", "-e", f"{claims['base_sha']}^{{commit}}")
if current_branch != claims["branch"]:
self._git(workspace, "checkout", "-B", claims["branch"], claims["base_sha"])
candidate = self._ensure_candidate_repo(claims)
remotes = self._git(workspace, "remote").splitlines()
if "upstream" not in remotes:
if "origin" in remotes and self._git(workspace, "remote", "get-url", "origin") != str(candidate):
self._git(workspace, "remote", "rename", "origin", "upstream")
remotes = self._git(workspace, "remote").splitlines()
elif "origin" not in remotes:
self._git(workspace, "remote", "add", "upstream", repo_url)
remotes = self._git(workspace, "remote").splitlines()
if "origin" in remotes:
self._git(workspace, "remote", "set-url", "origin", str(candidate))
else:
self._git(workspace, "remote", "add", "origin", str(candidate))
if not binding_path.exists():
binding_path.write_text(compact_json(binding) + "\n", encoding="utf-8")
binding_path.chmod(0o600)
receipt = self._operation_receipt(
claims["request_id"],
"workspace_prepared",
"PASS",
{"repository": claims["repository"], "branch": claims["branch"], "base_sha": claims["base_sha"]},
)
return {
"prepared": True,
"repository": claims["repository"],
"branch": self._git(workspace, "branch", "--show-current"),
"head": self._git(workspace, "rev-parse", "HEAD"),
"clean": not bool(self._git(workspace, "status", "--porcelain")),
"candidate_store": "BS-SG-003_INTERNAL_REVIEW_ONLY",
"receipt": receipt,
}
def list_files(self, capability: str, prefix: str = ".") -> dict[str, Any]:
claims = self._claims(capability, "read")
workspace = self._workspace(claims)
if not workspace.joinpath(".git").is_dir():
raise RuntimeError("workspace_not_prepared")
requested = safe_path(prefix)
if requested != "." and not any(path_within(requested, item) or path_within(item, requested) for item in claims["paths"]):
raise PermissionError("path_outside_capability")
tracked = self._git(workspace, "ls-files").splitlines()
untracked = self._git(workspace, "ls-files", "--others", "--exclude-standard").splitlines()
files = sorted(
{
path
for path in tracked + untracked
if path
and (requested == "." or path_within(path, requested))
and any(path_within(path, allowed) for allowed in claims["paths"])
}
)[:5000]
return {"files": files, "count": len(files), "truncated": len(files) >= 5000}
def read_file(self, capability: str, path_value: Any) -> dict[str, Any]:
claims = self._claims(capability, "read")
relative = self._ensure_claim_path(claims, path_value)
workspace = self._workspace(claims)
target = workspace / relative
if target.is_symlink() or not target.is_file():
raise LookupError("file_not_found_or_symlink")
target.resolve().relative_to(workspace.resolve())
max_bytes = int(self.policy.get("max_read_file_bytes", 262144))
raw = target.read_bytes()
if len(raw) > max_bytes:
raise ValueError("file_too_large_to_read")
try:
content = raw.decode("utf-8")
except UnicodeDecodeError as exc:
raise ValueError("binary_file_not_readable") from exc
return {
"path": relative,
"content": content,
"sha256": hashlib.sha256(raw).hexdigest(),
"bytes": len(raw),
}
def write_file(
self, capability: str, path_value: Any, content: Any, expected_sha256: str | None
) -> dict[str, Any]:
claims = self._claims(capability, "edit")
relative = self._ensure_claim_path(claims, path_value)
if not isinstance(content, str):
raise ValueError("content_must_be_text")
raw = content.encode("utf-8")
max_bytes = int(self.policy.get("max_changed_file_bytes", 20 * 1024 * 1024))
if len(raw) > max_bytes:
raise ValueError("file_too_large")
workspace = self._workspace(claims)
target = workspace / relative
parent = target.parent
parent.mkdir(parents=True, exist_ok=True)
parent.resolve().relative_to(workspace.resolve())
if target.exists() and (target.is_symlink() or not target.is_file()):
raise PermissionError("target_not_regular_file")
before = target.read_bytes() if target.exists() else b""
before_hash = hashlib.sha256(before).hexdigest()
expected = str(expected_sha256 or "")
if target.exists() and (not expected or not hmac.compare_digest(before_hash, expected)):
raise RuntimeError("expected_sha256_mismatch")
if not target.exists() and expected not in ("", hashlib.sha256(b"").hexdigest()):
raise RuntimeError("new_file_expected_sha256_mismatch")
temp_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
dir=target.parent, prefix=".zhulan-write-", delete=False
) as handle:
temp_path = Path(handle.name)
handle.write(raw)
handle.flush()
os.fsync(handle.fileno())
temp_path.chmod(0o600)
temp_path.replace(target)
temp_path = None
finally:
if temp_path is not None:
temp_path.unlink(missing_ok=True)
after_hash = hashlib.sha256(raw).hexdigest()
receipt = self._operation_receipt(
claims["request_id"],
"workspace_file_written",
"PASS",
{"path": relative, "before_sha256": before_hash, "after_sha256": after_hash, "bytes": len(raw)},
)
return {"path": relative, "sha256": after_hash, "bytes": len(raw), "receipt": receipt}
def git_status(self, capability: str) -> dict[str, Any]:
claims = self._claims(capability, "read")
workspace = self._workspace(claims)
return {
"branch": self._git(workspace, "branch", "--show-current"),
"head": self._git(workspace, "rev-parse", "HEAD"),
"base_sha": claims["base_sha"],
"changes": self._git(workspace, "status", "--short").splitlines(),
"diff_stat": self._git(workspace, "diff", "--stat", claims["base_sha"]),
}
def enforce_workspace_gate(self, claims: dict[str, Any]) -> list[str]:
workspace = self._workspace(claims)
self._git(workspace, "merge-base", "--is-ancestor", claims["base_sha"], "HEAD")
blocks = [
self._git(workspace, "diff", "--name-only", f"{claims['base_sha']}...HEAD"),
self._git(workspace, "diff", "--name-only", "HEAD"),
self._git(workspace, "ls-files", "--others", "--exclude-standard"),
]
files = sorted({line for block in blocks for line in block.splitlines() if line})
max_bytes = int(self.policy.get("max_changed_file_bytes", 20 * 1024 * 1024))
for relative in files:
if not any(path_within(relative, allowed) for allowed in claims["paths"]):
raise PermissionError(f"changed_path_outside_capability:{relative}")
target = workspace / relative
if not target.exists():
continue
if target.is_symlink():
try:
target.resolve().relative_to(workspace.resolve())
except ValueError as exc:
raise PermissionError(f"symlink_outside_workspace:{relative}") from exc
continue
if not target.is_file():
raise PermissionError(f"changed_path_not_regular_file:{relative}")
size = target.stat().st_size
if size > max_bytes:
raise PermissionError(f"changed_file_too_large:{relative}:{size}")
if size <= 2_000_000:
raw = target.read_bytes()
if any(pattern.search(raw) for pattern in SECRET_PATTERNS):
raise PermissionError(f"possible_secret:{relative}")
return files
def run_validation(self, capability: str) -> dict[str, Any]:
claims = self._claims(capability, "test")
workspace = self._workspace(claims)
files = self.enforce_workspace_gate(claims)
commands = self.policy["repositories"][claims["repository"]].get("validation_commands", [])
results: list[dict[str, Any]] = []
env = {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"HOME": str(self.settings.workspace_root),
"LANG": "C.UTF-8",
"PYTHONDONTWRITEBYTECODE": "1",
"GIT_TERMINAL_PROMPT": "0",
}
passed = True
for command in commands:
if not isinstance(command, list) or not command or not all(isinstance(x, str) for x in command):
raise RuntimeError("validation_policy_invalid")
if command == ["git", "diff", "--check"]:
argv = ["git", "-C", str(workspace), "diff", "--check"]
cwd = None
elif self.settings.test_mode:
argv = command
cwd = workspace
else:
request = compact_json(
{
"operation": "run",
"request_id": claims["request_id"],
"repository": claims["repository"],
"development_id": claims["development_id"],
"workspace": str(workspace),
"paths": claims["paths"],
"command": command,
}
).encode("utf-8") + b"\n"
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as broker:
broker.settimeout(190)
broker.connect(str(self.settings.validation_socket))
broker.sendall(request)
broker.shutdown(socket.SHUT_WR)
raw = b""
while len(raw) <= 131072:
chunk = broker.recv(16384)
if not chunk:
break
raw += chunk
if b"\n" in raw:
break
except (OSError, TimeoutError) as exc:
raise RuntimeError("validation_executor_unavailable") from exc
if len(raw) > 131072:
raise RuntimeError("validation_executor_response_too_large")
try:
response = json.loads(raw)
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
raise RuntimeError("validation_executor_response_invalid") from exc
if not response.get("ok"):
raise RuntimeError(f"validation_executor_rejected:{response.get('error', 'unknown')}")
output = str(response.get("output", ""))[-12000:]
exit_code = int(response.get("exit_code", 1))
results.append({"command": command, "exit_code": exit_code, "output": output})
if exit_code != 0:
passed = False
break
continue
result = subprocess.run(
argv,
cwd=cwd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=180,
env=env,
)
output = result.stdout[-12000:]
results.append({"command": command, "exit_code": result.returncode, "output": output})
if result.returncode != 0:
passed = False
break
head = self._git(workspace, "rev-parse", "HEAD")
clean = not bool(self._git(workspace, "status", "--porcelain"))
receipt = self._operation_receipt(
claims["request_id"],
"workspace_validation",
"PASS" if passed else "FAIL",
{
"head": head,
"clean": clean,
"changed_files": files,
"commands": [{"argv": item["command"], "exit_code": item["exit_code"]} for item in results],
},
)
return {
"decision": "PASS" if passed else "FAIL",
"head": head,
"clean": clean,
"changed_files": files,
"results": results,
"receipt": receipt,
}
def commit_candidate(self, capability: str, message: Any) -> dict[str, Any]:
claims = self._claims(capability, "commit")
if not isinstance(message, str) or not (4 <= len(message.strip()) <= 160) or "\n" in message.strip():
raise ValueError("commit_message_invalid")
workspace = self._workspace(claims)
if self._git(workspace, "branch", "--show-current") != claims["branch"]:
raise PermissionError("candidate_branch_mismatch")
files = self.enforce_workspace_gate(claims)
if not files:
raise RuntimeError("nothing_to_commit")
self._git(workspace, "diff", "--check")
pathspecs = claims["paths"]
self._git(workspace, "add", "--all", "--", *pathspecs)
staged = self._git(workspace, "diff", "--cached", "--name-only").splitlines()
if not staged:
raise RuntimeError("nothing_to_commit")
if any(not any(path_within(path, allowed) for allowed in claims["paths"]) for path in staged):
self._git(workspace, "reset")
raise PermissionError("staged_path_outside_capability")
env = {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"HOME": str(self.settings.workspace_root),
"LANG": "C.UTF-8",
"GIT_AUTHOR_NAME": "Zhulan Remote Cell",
"GIT_AUTHOR_EMAIL": "zhulan@guanghulab.invalid",
"GIT_COMMITTER_NAME": "Zhulan Remote Cell",
"GIT_COMMITTER_EMAIL": "zhulan@guanghulab.invalid",
"GIT_TERMINAL_PROMPT": "0",
}
result = subprocess.run(
["git", "-C", str(workspace), "commit", "-m", message.strip()],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=60,
env=env,
)
if result.returncode != 0:
raise RuntimeError("git_commit_failed")
commit_sha = self._git(workspace, "rev-parse", "HEAD")
receipt = self._operation_receipt(
claims["request_id"],
"candidate_committed",
"PASS",
{"commit_sha": commit_sha, "branch": claims["branch"], "files": staged},
)
return {"commit_sha": commit_sha, "branch": claims["branch"], "files": staged, "receipt": receipt}
def push_candidate(self, capability: str) -> dict[str, Any]:
claims = self._claims(capability, "push_candidate")
workspace = self._workspace(claims)
branch = self._git(workspace, "branch", "--show-current")
if branch != claims["branch"] or branch in ("main", "master"):
raise PermissionError("push_branch_denied")
if self._git(workspace, "status", "--porcelain"):
raise RuntimeError("workspace_dirty_before_push")
before = self._git(workspace, "rev-parse", "HEAD")
self.enforce_workspace_gate(claims)
with self.store.connect() as db:
validation = db.execute(
"""SELECT result, evidence_json FROM receipts
WHERE request_id=? AND event='workspace_validation'
ORDER BY sequence DESC LIMIT 1""",
(claims["request_id"],),
).fetchone()
if not validation or validation["result"] != "PASS":
raise PermissionError("passing_validation_required_before_push")
evidence = json.loads(validation["evidence_json"])
if evidence.get("head") != before or evidence.get("clean") is not True:
raise PermissionError("validation_not_bound_to_clean_current_head")
self._git(workspace, "push", "origin", f"HEAD:refs/heads/{branch}", timeout=180)
remote = self._git(workspace, "ls-remote", "--heads", "origin", f"refs/heads/{branch}")
if not remote.startswith(before):
raise RuntimeError("remote_readback_mismatch")
receipt = self._operation_receipt(
claims["request_id"],
"candidate_pushed",
"PASS",
{
"repository": claims["repository"],
"branch": branch,
"commit_sha": before,
"candidate_store": "BS-SG-003_INTERNAL_REVIEW_ONLY",
"central_publication": "NOT_PERFORMED_REQUIRES_ZHUYUAN_REVIEW",
},
)
return {
"repository": claims["repository"],
"branch": branch,
"commit_sha": before,
"candidate_store": "BS-SG-003_INTERNAL_REVIEW_ONLY",
"central_publication": "NOT_PERFORMED_REQUIRES_ZHUYUAN_REVIEW",
"receipt": receipt,
}
def mcp_tools(self) -> list[dict[str, Any]]:
noauth = [{"type": "noauth"}]
oauth = [{"type": "oauth2", "scopes": ["zhulan.develop"]}]
tools = [
{
"name": "zhulan_request_development",
"description": "创建一张没有执行权的铸澜开发申请。返回的 claim_token 只用于查询和一次领取。",
"securitySchemes": noauth,
"inputSchema": {
"type": "object",
"additionalProperties": False,
"required": ["persona_id", "development_id", "repository", "base_sha", "branch", "paths", "actions", "description"],
"properties": {
"persona_id": {"type": "string", "const": "ICE-GL-ZL-001"},
"development_id": {"type": "string"},
"repository": {"type": "string"},
"base_sha": {"type": "string"},
"branch": {"type": "string"},
"paths": {"type": "array", "items": {"type": "string"}, "minItems": 1},
"actions": {"type": "array", "items": {"type": "string"}, "minItems": 1},
"description": {"type": "string"},
},
},
},
{
"name": "zhulan_request_status",
"description": "用一次性领取凭证查看申请是否已被主人批准。",
"securitySchemes": noauth,
"inputSchema": {"type": "object", "additionalProperties": False, "required": ["request_id", "claim_token"], "properties": {"request_id": {"type": "string"}, "claim_token": {"type": "string"}}},
},
*[
{
"name": name,
"description": description,
"securitySchemes": oauth,
"inputSchema": schema,
}
for name, description, schema in [
("zhulan_restore_lane", "每次恢复或上下文变长后先调用读取服务器绑定的当前车道、Git 状态和最近回执。", {"type": "object", "additionalProperties": False, "properties": {}}),
("zhulan_prepare_workspace", "按批准的仓库、基线和候选分支准备或恢复唯一工作区。", {"type": "object", "additionalProperties": False, "properties": {}}),
("zhulan_list_files", "列出能力路径内的文件。", {"type": "object", "additionalProperties": False, "properties": {"prefix": {"type": "string", "default": "."}}}),
("zhulan_read_file", "读取能力路径内的 UTF-8 文本文件。", {"type": "object", "additionalProperties": False, "required": ["path"], "properties": {"path": {"type": "string"}}}),
("zhulan_write_file", "以预期 SHA-256 乐观锁写入能力路径内的文本文件。", {"type": "object", "additionalProperties": False, "required": ["path", "content", "expected_sha256"], "properties": {"path": {"type": "string"}, "content": {"type": "string"}, "expected_sha256": {"type": "string"}}}),
("zhulan_git_status", "读取候选分支、基线和实际改动。", {"type": "object", "additionalProperties": False, "properties": {}}),
("zhulan_run_validation", "只运行仓库策略登记的验收命令,不接受任意命令。", {"type": "object", "additionalProperties": False, "properties": {}}),
("zhulan_commit_candidate", "只在批准路径内创建候选提交。", {"type": "object", "additionalProperties": False, "required": ["message"], "properties": {"message": {"type": "string"}}}),
("zhulan_push_candidate", "只把当前候选分支推到新加坡内部复核库并回读精确提交;永不直接发布代码频道或推 main。", {"type": "object", "additionalProperties": False, "properties": {}}),
]
],
]
for tool in tools:
tool["annotations"] = {
"readOnlyHint": tool["name"] in {"zhulan_request_status", "zhulan_restore_lane", "zhulan_list_files", "zhulan_read_file", "zhulan_git_status"},
"destructiveHint": False,
"openWorldHint": tool["name"] in {"zhulan_request_development", "zhulan_request_status"},
}
return tools
def call_mcp_tool(
self, name: str, args: dict[str, Any], source: str, bearer: str | None
) -> dict[str, Any]:
public_calls = {
"zhulan_request_development": lambda: self.create_request(args, source),
"zhulan_request_status": lambda: self.request_status(str(args.get("request_id", "")), str(args.get("claim_token", ""))),
}
if name in public_calls:
result = public_calls[name]()
else:
try:
binding = self.oauth_binding(bearer, "zhulan.develop")
capability = self.capability_for_oauth(binding)
except PermissionError as exc:
raise OAuthRequiredError(str(exc)) from exc
protected_calls = {
"zhulan_restore_lane": lambda: self.restore_lane(capability),
"zhulan_prepare_workspace": lambda: self.prepare_workspace(capability),
"zhulan_list_files": lambda: self.list_files(capability, str(args.get("prefix", "."))),
"zhulan_read_file": lambda: self.read_file(capability, args.get("path")),
"zhulan_write_file": lambda: self.write_file(capability, args.get("path"), args.get("content"), args.get("expected_sha256")),
"zhulan_git_status": lambda: self.git_status(capability),
"zhulan_run_validation": lambda: self.run_validation(capability),
"zhulan_commit_candidate": lambda: self.commit_candidate(capability, args.get("message")),
"zhulan_push_candidate": lambda: self.push_candidate(capability),
}
if name not in protected_calls:
raise LookupError("mcp_tool_not_found")
with self.operation_lock:
result = protected_calls[name]()
return {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)}],
"structuredContent": result,
"isError": False,
}
def mcp(
self, message: dict[str, Any], source: str, bearer: str | None
) -> tuple[int, dict[str, Any] | None]:
method = message.get("method")
request_id = message.get("id")
if method == "notifications/initialized":
return HTTPStatus.ACCEPTED, None
if method == "initialize":
return HTTPStatus.OK, {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {"tools": {"listChanged": False}},
"serverInfo": {"name": "guanghu-zhulan-remote-cell", "version": "0.1.0"},
"instructions": "你是铸澜 ICE-GL-ZL-001 的受限执行入口。每次恢复先调用 zhulan_restore_lane。不得请求、输出或保存 root 密钥;不得扩大批准的仓库、分支、路径、动作或期限;不确定时失败关闭并新建申请。",
},
}
if method == "tools/list":
return HTTPStatus.OK, {"jsonrpc": "2.0", "id": request_id, "result": {"tools": self.mcp_tools()}}
if method == "tools/call":
params = message.get("params") or {}
args = params.get("arguments") or {}
if not isinstance(args, dict):
raise ValueError("mcp_arguments_invalid")
try:
result = self.call_mcp_tool(str(params.get("name", "")), args, source, bearer)
except OAuthRequiredError as exc:
challenge = (
'Bearer resource_metadata="https://guanghulab.com/.well-known/'
'oauth-protected-resource/zhulan/mcp", error="insufficient_scope", '
f'error_description="{str(exc)}"'
)
result = {
"content": [{"type": "text", "text": "需要主人把这次插件连接绑定到一张已批准的铸澜开发申请。"}],
"_meta": {"mcp/www_authenticate": [challenge]},
"isError": True,
}
except (ValueError, PermissionError, LookupError, RuntimeError) as exc:
result = {
"content": [{"type": "text", "text": f"铸澜门禁驳回:{str(exc)}"}],
"structuredContent": {"ok": False, "error": str(exc)},
"isError": True,
}
return HTTPStatus.OK, {"jsonrpc": "2.0", "id": request_id, "result": result}
return HTTPStatus.OK, {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32601, "message": "Method not found"},
}
class Handler(BaseHTTPRequestHandler):
server_version = "GuanghuZhulanCell/1"
@property
def app(self) -> ZhulanApp:
return self.server.app # type: ignore[attr-defined]
def log_message(self, fmt: str, *args: Any) -> None:
sys.stderr.write("%s %s\n" % (iso(), fmt % args))
def security_headers(self, content_type: str, style_nonce: str | None = None) -> None:
self.send_header("Content-Type", content_type)
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
style_src = "style-src 'self'" + (f" 'nonce-{style_nonce}'" if style_nonce else "")
self.send_header(
"Content-Security-Policy",
f"default-src 'self'; {style_src}; script-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
)
def json_response(self, status: int, payload: Any, cookie: str | None = None) -> None:
raw = compact_json(payload).encode("utf-8")
self.send_response(status)
self.security_headers("application/json; charset=utf-8")
if cookie:
self.send_header("Set-Cookie", cookie)
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def html_response(self, status: int, markup: str, style_nonce: str | None = None) -> None:
raw = markup.encode("utf-8")
self.send_response(status)
self.security_headers("text/html; charset=utf-8", style_nonce)
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def redirect_response(self, location: str) -> None:
self.send_response(HTTPStatus.FOUND)
self.security_headers("text/plain; charset=utf-8")
self.send_header("Location", location)
self.send_header("Content-Length", "0")
self.end_headers()
def error(self, status: int, code: str) -> None:
self.json_response(status, {"ok": False, "error": code})
def body(self, limit: int = 65536) -> dict[str, Any]:
length = int(self.headers.get("Content-Length", "0"))
if length <= 0 or length > limit:
raise ValueError("body_size_invalid")
value = json.loads(self.rfile.read(length).decode("utf-8"))
if not isinstance(value, dict):
raise ValueError("body_must_be_object")
return value
def form_body(self, limit: int = 65536) -> dict[str, str]:
length = int(self.headers.get("Content-Length", "0"))
if length <= 0 or length > limit:
raise ValueError("body_size_invalid")
parsed = parse_qs(self.rfile.read(length).decode("utf-8"), keep_blank_values=True)
if any(len(values) != 1 for values in parsed.values()):
raise ValueError("form_field_repeated")
return {key: values[0] for key, values in parsed.items()}
def session_token(self) -> str | None:
cookie = SimpleCookie(self.headers.get("Cookie", ""))
morsel = cookie.get("zhulan_owner")
return morsel.value if morsel else None
def client_source(self) -> str:
forwarded = self.headers.get("X-Forwarded-For", "").split(",", 1)[0].strip()
return forwarded or self.client_address[0]
def route(self) -> str:
return urlparse(self.path).path.rstrip("/") or "/"
def bearer(self) -> str | None:
match = re.fullmatch(r"Bearer\s+([^\s]+)", self.headers.get("Authorization", ""))
return match.group(1) if match else None
def oauth_authorize_page(self, oauth: dict[str, str], csrf: str, style_nonce: str) -> str:
requests = [
item
for item in self.app.owner_requests()
if item["state"] == "APPROVED" and item["capability_expires_at"]
]
options = "".join(
f'<option value="{html.escape(item["id"])}">'
f'{html.escape(item["development_id"])} · {html.escape(item["repository"])} · '
f'{html.escape(item["branch"])} · 至 {html.escape(item["capability_expires_at"])}</option>'
for item in requests
)
hidden = "".join(
f'<input type="hidden" name="{html.escape(key)}" value="{html.escape(value)}">'
for key, value in oauth.items()
)
disabled = "" if requests else " disabled"
return f"""<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>铸澜 · 绑定插件连接</title>
<style nonce="{html.escape(style_nonce)}">color-scheme:dark;*{{box-sizing:border-box}}body{{margin:0;min-height:100vh;display:grid;place-items:center;padding:20px;color:#f2f1ed;background:#090b1a;font-family:-apple-system,BlinkMacSystemFont,'PingFang SC',sans-serif}}main{{width:min(560px,100%);padding:30px;border:1px solid #303552;border-radius:20px;background:#11152e}}p{{color:#a3a9be;line-height:1.8}}label{{display:block;margin:24px 0 8px;font-size:12px;color:#b1b3ff}}select,button{{width:100%;min-height:48px;border-radius:12px;border:1px solid #3b4267;padding:0 13px;font:inherit}}select{{color:#f2f1ed;background:#0b0e20}}button{{margin-top:18px;border:0;color:#11152e;background:#eee6d5;font-weight:750}}small{{display:block;margin-top:18px;color:#7f879f;line-height:1.7}}</style></head><body><main>
<p>ZHULAN · OAUTH 2.1 · PKCE</p><h1>把插件连接绑定到已批准边界</h1>
<p>这不会把 SSH 密钥或临时能力交给手机。连接只能使用你选中的人格、仓库、候选分支、路径、动作和到期时间。</p>
<form method=\"post\" action=\"/zhulan/oauth/authorize\">{hidden}
<input type=\"hidden\" name=\"csrf\" value=\"{html.escape(csrf)}\">
<label for=\"request_id\">已批准的铸澜开发申请</label><select id=\"request_id\" name=\"request_id\">{options}</select>
<button type=\"submit\"{disabled}>确认绑定这一次连接</button></form>
<small>{'没有仍在有效期内的已批准申请。请返回审批端先批准一张申请。' if not requests else '到期、拒绝或扩大边界后,这个连接都会失效。'}</small>
</main></body></html>"""
def serve_asset(self, path: Path, content_type: str) -> None:
if not path.is_file():
self.error(HTTPStatus.NOT_FOUND, "not_found")
return
raw = path.read_bytes()
self.send_response(HTTPStatus.OK)
self.security_headers(content_type)
self.send_header("Cache-Control", "public, max-age=300")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def do_GET(self) -> None:
route = self.route()
try:
if route == "/":
return self.serve_asset(self.app.settings.ui_dir / "index.html", "text/html; charset=utf-8")
if route == "/assets/styles.css":
return self.serve_asset(self.app.settings.ui_dir / "styles.css", "text/css; charset=utf-8")
if route == "/assets/app.js":
return self.serve_asset(self.app.settings.ui_dir / "app.js", "application/javascript; charset=utf-8")
if route == "/health":
return self.json_response(
HTTPStatus.OK,
{
"ok": True,
"service": "zhulan-remote-cell",
"front_role": "PROXY_ONLY",
"runtime_node": self.app.policy["node_id"],
"time": iso(),
},
)
if route == "/.well-known/oauth-protected-resource":
return self.json_response(HTTPStatus.OK, self.app.protected_resource_metadata())
if route in ("/.well-known/oauth-authorization-server", "/.well-known/openid-configuration"):
return self.json_response(HTTPStatus.OK, self.app.oauth_metadata())
if route == "/oauth/authorize":
oauth = self.app.validate_oauth_authorize(parse_qs(urlparse(self.path).query, keep_blank_values=True))
try:
_, csrf = self.app.owner_session(self.session_token(), rotate_csrf=True)
except PermissionError:
return_path = urlparse(self.path).path + "?" + urlparse(self.path).query
encoded = b64url(return_path.encode("utf-8"))
return self.redirect_response(f"/zhulan/?oauth_return={encoded}")
style_nonce = secrets.token_urlsafe(18)
return self.html_response(
HTTPStatus.OK,
self.oauth_authorize_page(oauth, str(csrf), style_nonce),
style_nonce,
)
if route == "/api/v1/public/config":
return self.json_response(HTTPStatus.OK, self.app.public_config())
if route == "/api/v1/owner/session":
session, csrf = self.app.owner_session(self.session_token(), rotate_csrf=True)
return self.json_response(
HTTPStatus.OK,
{"authenticated": True, "owner_login": session["owner_login"], "csrf": csrf},
)
if route == "/api/v1/owner/requests":
self.app.owner_session(self.session_token())
return self.json_response(HTTPStatus.OK, {"requests": self.app.owner_requests()})
if route == "/api/v1/owner/receipts":
self.app.owner_session(self.session_token())
return self.json_response(HTTPStatus.OK, {"receipts": self.app.receipts()})
match = re.fullmatch(r"/api/v1/owner/requests/([^/]+)/receipts", route)
if match:
self.app.owner_session(self.session_token())
return self.json_response(HTTPStatus.OK, {"receipts": self.app.receipts(match.group(1))})
self.error(HTTPStatus.NOT_FOUND, "not_found")
except PermissionError as exc:
self.error(HTTPStatus.UNAUTHORIZED, str(exc))
except Exception as exc: # fail closed without exposing internals
self.log_message("GET failed: %s", exc)
self.error(HTTPStatus.INTERNAL_SERVER_ERROR, "internal_error")
def do_POST(self) -> None:
route = self.route()
try:
content_type = self.headers.get("Content-Type", "").split(";", 1)[0].strip().lower()
if route in ("/oauth/token", "/oauth/authorize"):
data = self.form_body()
else:
data = self.body(limit=1024 * 1024 if route == "/mcp" else 65536)
if route == "/oauth/register":
return self.json_response(
HTTPStatus.CREATED,
self.app.register_oauth_client(data, self.client_source()),
)
if route == "/oauth/token":
return self.json_response(HTTPStatus.OK, self.app.exchange_oauth_code(data))
if route == "/oauth/authorize":
oauth_keys = ("client_id", "redirect_uri", "state", "code_challenge", "resource", "scope")
oauth = {key: str(data.get(key, "")) for key in oauth_keys}
location = self.app.authorize_oauth_request(
self.session_token(), str(data.get("csrf", "")), oauth, str(data.get("request_id", ""))
)
return self.redirect_response(location)
if route == "/mcp":
status, result = self.app.mcp(data, self.client_source(), self.bearer())
if result is None:
self.send_response(status)
self.send_header("Content-Length", "0")
self.end_headers()
return
return self.json_response(status, result)
if route == "/api/v1/public/requests":
result = self.app.create_request(data, self.client_source())
return self.json_response(HTTPStatus.CREATED, result)
match = re.fullmatch(r"/api/v1/public/requests/([^/]+)/status", route)
if match:
return self.json_response(
HTTPStatus.OK, self.app.request_status(match.group(1), str(data.get("claim_token", "")))
)
match = re.fullmatch(r"/api/v1/public/requests/([^/]+)/pickup", route)
if match:
return self.json_response(
HTTPStatus.OK, self.app.pickup(match.group(1), str(data.get("claim_token", "")))
)
if route == "/api/v1/owner/login":
token, csrf, result = self.app.owner_login(
str(data.get("username", "")),
str(data.get("password", "")),
self.client_source(),
)
flags = [
f"zhulan_owner={token}",
f"Path={self.app.settings.cookie_path}",
"HttpOnly",
"SameSite=Strict",
"Max-Age=10800",
]
if self.app.settings.cookie_secure:
flags.append("Secure")
result["csrf"] = csrf
return self.json_response(HTTPStatus.OK, result, "; ".join(flags))
if route == "/api/v1/owner/logout":
token = self.session_token()
session, _ = self.app.owner_session(token)
self.app.require_csrf(session, self.headers.get("X-CSRF-Token"))
self.app.logout(token)
return self.json_response(
HTTPStatus.OK,
{"ok": True},
f"zhulan_owner=; Path={self.app.settings.cookie_path}; HttpOnly; SameSite=Strict; Max-Age=0",
)
match = re.fullmatch(r"/api/v1/owner/requests/([^/]+)/(approve|reject)", route)
if match:
session, _ = self.app.owner_session(self.session_token())
self.app.require_csrf(session, self.headers.get("X-CSRF-Token"))
approve = match.group(2) == "approve"
result = self.app.decide(
match.group(1),
session["owner_login"],
approve,
data.get("ttl_seconds"),
str(data.get("reason", "")),
)
return self.json_response(HTTPStatus.OK, result)
match = re.fullmatch(r"/api/v1/owner/requests/([^/]+)/revoke", route)
if match:
session, _ = self.app.owner_session(self.session_token())
self.app.require_csrf(session, self.headers.get("X-CSRF-Token"))
result = self.app.revoke(
match.group(1), session["owner_login"], str(data.get("reason", ""))
)
return self.json_response(HTTPStatus.OK, result)
if route == "/api/v1/runtime/verify":
result = self.app.verify_capability(
str(data.get("capability", "")), str(data.get("action")) if data.get("action") else None
)
return self.json_response(HTTPStatus.OK, result)
self.error(HTTPStatus.NOT_FOUND, "not_found")
except json.JSONDecodeError:
self.error(HTTPStatus.BAD_REQUEST, "json_invalid")
except ValueError as exc:
self.error(HTTPStatus.BAD_REQUEST, str(exc))
except PermissionError as exc:
self.error(HTTPStatus.FORBIDDEN, str(exc))
except LookupError as exc:
self.error(HTTPStatus.NOT_FOUND, str(exc))
except RuntimeError as exc:
self.error(HTTPStatus.CONFLICT, str(exc))
except Exception as exc:
self.log_message("POST failed: %s", exc)
self.error(HTTPStatus.INTERNAL_SERVER_ERROR, "internal_error")
class Server(ThreadingHTTPServer):
def __init__(self, address: tuple[str, int], app: ZhulanApp):
super().__init__(address, Handler)
self.app = app
def main() -> None:
settings = Settings.load()
app = ZhulanApp(settings)
server = Server((settings.bind, settings.port), app)
print(
compact_json(
{
"service": "zhulan-remote-cell",
"bind": settings.bind,
"port": settings.port,
"runtime_node": app.policy["node_id"],
}
),
flush=True,
)
server.serve_forever()
if __name__ == "__main__":
main()