diff --git a/deployment/receipts/DEV-20260813-005-ZHULAN-REMOTE-CELL-DEPLOYMENT-20260813.json b/deployment/receipts/DEV-20260813-005-ZHULAN-REMOTE-CELL-DEPLOYMENT-20260813.json deleted file mode 100644 index 9f3c44f..0000000 --- a/deployment/receipts/DEV-20260813-005-ZHULAN-REMOTE-CELL-DEPLOYMENT-20260813.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "schema": "guanghu.zhulan-remote-development-cell-deployment-receipt/v1", - "receipt_id": "DEV-20260813-005-ZHULAN-REMOTE-CELL-DEPLOYMENT-20260813", - "development_id": "DEV-20260813-005", - "persona": { - "persona_id": "ICE-GL-ZL-001", - "name": "Zhulan", - "role": "mobile_restricted_remote_development", - "separate_from": "ICE-GL-ZY001_ZHUYUAN_LOCAL_REVIEW_AND_PUBLICATION" - }, - "result": "PASS_DEPLOYED_WITH_OWNER_AND_DEVICE_ACCEPTANCE_PENDING", - "recorded_at": "2026-08-13T10:03:44Z", - "source": { - "repository": "REPO-012", - "baseline": "5f9e83e1b716fd50ee2880b24ea47f83b75d1f8f", - "deployed_source_commit": "366b8911e416828038b4344353cc22f5fec7b7cf", - "deployed_source_tree_hash": "d284e9c58a887db06c8f12a10d40f1dea20cacaf2382e22830498c0a23377e13", - "scope": "server-tools/zhulan-remote-cell", - "changed_files": 22, - "source_publication": "TO_BE_BOUND_BY_FINALIZATION_RECEIPT" - }, - "topology": { - "front_node": { - "node_id": "BS-GZ-006", - "role": "DOMAIN_TLS_AND_REVERSE_PROXY_ONLY", - "public_entry": "https://guanghulab.com/zhulan/", - "state_or_source_ownership": false, - "upstream_listener": "LOOPBACK_ONLY", - "nginx_configuration_sha256": "4e9961952a3aafdef5687d0d76795cce40429fe81e4a577bac71d6d543f41a9a" - }, - "runtime_node": { - "node_id": "BS-SG-003", - "role": "UI_APPROVAL_OAUTH_MCP_STATE_SANDBOX_GATES_RECEIPTS", - "runtime_listener": "LOOPBACK_ONLY", - "runtime_service": "zhulan-remote-cell.service", - "validation_service": "zhulan-validation-executor.service", - "front_tunnel_service": "zhulan-front-tunnel.service" - } - }, - "runtime_readback": { - "runtime_service_active": true, - "validation_service_active": true, - "front_tunnel_active": true, - "nginx_active": true, - "nginx_configuration_test": "PASS", - "public_ui": "HTTP_200", - "public_health": "PASS", - "public_health_runtime_node": "BS-SG-003", - "owner_endpoint_without_session": "HTTP_401", - "private_runtime_endpoint_publicly_routed": false, - "installed_source_commit_readback": "366b8911e416828038b4344353cc22f5fec7b7cf", - "server_install_receipt_sha256": "62dc2a162c17d714caf1296a2f73f3f52e5eb9491b9f7b71218e8bec311c3857" - }, - "approval_and_execution_gates": { - "approval_binding": [ - "persona_id", - "development_id", - "repository", - "base_sha", - "candidate_branch", - "paths", - "actions", - "expiry" - ], - "owner_decision_required": true, - "owner_can_revoke": true, - "remote_session_has_central_repository_write_credential": false, - "candidate_destination": "BS-SG-003_INTERNAL_REVIEW_ONLY", - "central_publication_requires_zhuyuan_review": true, - "arbitrary_shell_exposed": false, - "mcp_tool_count": 11, - "registered_validation_commands_only": true, - "pending_request_execution": "DENIED_validation_approval_not_active", - "arbitrary_command_execution": "DENIED_validation_command_not_registered_for_broker", - "unexpected_broker_field": "DENIED_validation_request_fields_invalid" - }, - "sandbox_readback": { - "public_runtime_no_new_privileges": true, - "validation_executor_public_listener": false, - "validation_executor_private_network": true, - "validation_subprocess_network_routes": 0, - "workspace_projection": "READ_ONLY", - "state_projection": "READ_ONLY", - "secret_projection": "INACCESSIBLE_EMPTY_VIEW", - "candidate_projection": "INACCESSIBLE_EMPTY_VIEW", - "system_bwrap_setuid": false, - "private_bwrap_scope": "ROOT_OWNED_ZHULAN_RUNTIME_GROUP_EXECUTABLE", - "host_unprivileged_user_namespace_restriction_kept_enabled": true, - "apparmor_exception_scope": "PRIVATE_BWRAP_PATH_ONLY" - }, - "validation": { - "current_source_tests": "PASS_22_OF_22", - "isolated_server_validation_tests": "PASS_21_OF_21", - "git_diff_check": "PASS", - "mcp_tools_list": "PASS_EXACT_11_CONTROLLED_TOOLS", - "mcp_protected_call_without_oauth": "DENIED_WITH_OAUTH_CHALLENGE", - "oauth_metadata": "PASS", - "preexisting_public_routes_unchanged": true, - "preexisting_fifth_domain_route_status": "HTTP_502_UNCHANGED_NOT_CAUSED_BY_THIS_DEPLOYMENT", - "preexisting_authz_route_status": "HTTP_404_UNCHANGED_NOT_CAUSED_BY_THIS_DEPLOYMENT" - }, - "acceptance_boundary": { - "mobile_layout_source_and_responsive_gate": "PASS", - "actual_mobile_codex_connection": "NOT_RUN_USER_DEVICE_REQUIRED", - "actual_mobile_browser_interaction": "NOT_RUN_USER_DEVICE_REQUIRED", - "owner_approved_positive_end_to_end_execution": "NOT_RUN_REQUIRES_OWNER_APPROVAL", - "owner_approval_was_forged_for_testing": false, - "smoke_request_state": "PENDING_UNAPPROVED_NON_EXECUTING" - }, - "truth_boundary": { - "source_implementation": true, - "server_installation": true, - "runtime_health": true, - "negative_gate_enforcement": true, - "mechanical_sandbox_execution": true, - "mobile_codex_acceptance": false, - "owner_approved_positive_acceptance": false, - "central_source_publication_at_receipt_creation": false - } -} diff --git a/server-tools/zhulan-remote-cell/README.md b/server-tools/zhulan-remote-cell/README.md deleted file mode 100644 index c5c53a4..0000000 --- a/server-tools/zhulan-remote-cell/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 铸澜受限远程开发单元 - -`zhulan-remote-cell` 是 `ICE-GL-ZL-001` 的手机远程开发运行入口。它把公网前门与真实运行体明确分开: - -- `BS-GZ-006` 只承载 `guanghulab.com/zhulan/` 的域名、TLS 和反向代理,不保存或运行审批 UI; -- `BS-SG-003` 承载真实 UI、申请、主人登录、批准、临时能力、策略校验、SQLite 状态与哈希回执链; -- UI 只投影运行层返回的事实,不拥有执行权; -- 公开申请本身没有任何服务器或仓库权限; -- 一次批准绑定 `persona + development_id + repository + base_sha + branch + paths + actions + expiry`,不得在会话内扩大。 - -## 当前能力 - -1. 铸澜可创建无执行权申请并取得一次性领取凭证; -2. 冰朔使用光湖代码频道账号登录审批端,密码仅用于当次 Forgejo 验证,不写入本地状态; -3. 批准后由运行层生成三小时临时能力,磁盘只保存摘要和重建所需的非秘密声明; -4. 临时能力只能领取一次,可由受限执行器通过回环 API 校验; -5. 所有申请、批准、拒绝、领取与校验事件写入前后哈希相连的回执链; -6. 代码门脚本检查当前分支、基线、改动路径、符号链接、超大文件与常见秘密模式; -7. 候选提交只进入 `BS-SG-003` 的内部复核库,不在远程会话中持有代码频道写凭证,也不直接发布中央仓库;本机铸渊复核后再走既有发布门。 - -这里不提供任意远程 shell。后续手机 Codex 接入只能通过受限执行器或受限工具协议使用这些能力,不能把临时能力降级成长期 root 密钥。 - -## 本地验收 - -```bash -python3 -m unittest discover -s tests -v -``` - -## 运行配置 - -生产安装读取 `/etc/guanghu/zhulan-remote-cell.env`,参考 `deploy/zhulan-remote-cell.env.example`。真实应用秘密、代码频道密码和临时能力都不得提交到仓库。 - -运行层默认仅监听 `127.0.0.1:17631`。广州 Nginx 只允许代理到一条专用的回环隧道,不得把新加坡运行端口直接暴露到公网。 - -Ubuntu 24.04 的生产验收命令通过专用的 `zhulan-bwrap` 副本和本机 Unix 套接字执行器运行。公网审批/MCP 服务继续保持 `NoNewPrivileges=true`;只有无公网监听、只接受策略登记命令的执行器负责创建 Bubblewrap 用户命名空间。安装器只为 root 持有、`zhulan-runtime` 组可执行的副本路径加载 AppArmor `userns` 例外;不得关闭全机的未授权用户命名空间限制,也不得把系统 `bwrap` 改成 setuid。 diff --git a/server-tools/zhulan-remote-cell/contracts/SOURCE.hdlp b/server-tools/zhulan-remote-cell/contracts/SOURCE.hdlp deleted file mode 100644 index 9bd86bf..0000000 --- a/server-tools/zhulan-remote-cell/contracts/SOURCE.hdlp +++ /dev/null @@ -1,17 +0,0 @@ -runtime_contract_projection: - development_id: "DEV-20260813-005" - persona: "ICE-GL-ZL-001" - source_repository: "https://guanghulab.com/code/bingshuo/guanghu-ice-heart" - source_commit: "5f9e83e1b716fd50ee2880b24ea47f83b75d1f8f" - architecture: "HLP-CURRENT-ARCH-001@2026-08-13.1" - ui_brains: - - id: "GHS-012" - state: "CANDIDATE_READ_ONLY" - purpose: "Fifth Domain private engineering projection constraints" - - id: "GHS-014" - state: "CANDIDATE_READ_ONLY" - purpose: "Guanghu client UI projection constraints" - topology: - BS-GZ-006: "DOMAIN_TLS_REVERSE_PROXY_ONLY" - BS-SG-003: "REAL_APPROVAL_CAPABILITY_SANDBOX_GATE_RECEIPT_RUNTIME" - boundary: "UI and skill brains guide projection; neither owns execution authority." diff --git a/server-tools/zhulan-remote-cell/deploy/guanghulab-zhulan.nginx.conf b/server-tools/zhulan-remote-cell/deploy/guanghulab-zhulan.nginx.conf deleted file mode 100644 index 188e6e0..0000000 --- a/server-tools/zhulan-remote-cell/deploy/guanghulab-zhulan.nginx.conf +++ /dev/null @@ -1,41 +0,0 @@ -# BS-GZ-006 only: domain/TLS/reverse proxy. UI and all real services remain on BS-SG-003. -# The local upstream port must be supplied by a dedicated restricted tunnel. -location = /zhulan { - return 308 /zhulan/; -} - -# Runtime-only verification is reachable from the BS-SG-003 loopback executor, -# never through the Guangzhou public projection. -location ^~ /zhulan/api/v1/runtime/ { - return 404; -} - -location ^~ /zhulan/ { - proxy_pass http://127.0.0.1:17631/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header X-Forwarded-Prefix /zhulan; - # Do not inherit a client-supplied X-Forwarded-For chain. The runtime uses - # this value for per-source rate limits. - proxy_set_header X-Forwarded-For $remote_addr; - proxy_connect_timeout 5s; - proxy_read_timeout 30s; - proxy_send_timeout 30s; - client_max_body_size 1m; -} - -# RFC 8414 path-aware discovery aliases for issuer -# https://guanghulab.com/zhulan. These are metadata projections only. -location = /.well-known/oauth-authorization-server/zhulan { - proxy_pass http://127.0.0.1:17631/.well-known/oauth-authorization-server; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto https; -} - -location = /.well-known/oauth-protected-resource/zhulan/mcp { - proxy_pass http://127.0.0.1:17631/.well-known/oauth-protected-resource; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto https; -} diff --git a/server-tools/zhulan-remote-cell/deploy/install-front-proxy.sh b/server-tools/zhulan-remote-cell/deploy/install-front-proxy.sh deleted file mode 100755 index fa9c834..0000000 --- a/server-tools/zhulan-remote-cell/deploy/install-front-proxy.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ "$(id -u)" -ne 0 ]]; then - echo "INSTALL_REQUIRES_ROOT" >&2 - exit 1 -fi - -SOURCE_DIR="${1:-}" -TUNNEL_PUBLIC_KEY_FILE="${2:-}" -if [[ -z "$SOURCE_DIR" || ! -f "$SOURCE_DIR/deploy/guanghulab-zhulan.nginx.conf" ]]; then - echo "SOURCE_DIR_INVALID" >&2 - exit 1 -fi -if [[ -z "$TUNNEL_PUBLIC_KEY_FILE" || ! -f "$TUNNEL_PUBLIC_KEY_FILE" ]]; then - echo "TUNNEL_PUBLIC_KEY_FILE_INVALID" >&2 - exit 1 -fi - -EXPECTED_HOST="${ZHULAN_EXPECTED_HOST:-VM-0-16-ubuntu}" -EXPECTED_DMI="${ZHULAN_EXPECTED_DMI:-b2ecf109-1999-4643-b223-e67e24d7667d}" -ACTUAL_HOST="$(hostname)" -ACTUAL_DMI="$(tr '[:upper:]' '[:lower:]' &2 - exit 1 -fi - -SITE_FILE="/etc/nginx/sites-enabled/guanghulab" -SNIPPET_FILE="/etc/nginx/snippets/guanghu-zhulan.conf" -SSHD_DROPIN="/etc/ssh/sshd_config.d/80-zhulan-proxy.conf" -PROXY_HOME="/var/lib/guanghu/zhulan-proxy" -AUTH_KEYS="$PROXY_HOME/.ssh/authorized_keys" -BACKUP_ROOT="/var/backups/guanghu/zhulan-front" -STAMP="$(date -u +%Y%m%dT%H%M%SZ)" - -if [[ ! -f "$SITE_FILE" ]]; then - echo "GUANGHULAB_SITE_NOT_FOUND" >&2 - exit 1 -fi -if ! grep -q 'include /etc/nginx/snippets/guanghu-ai-discovery.conf;' "$SITE_FILE"; then - echo "GUANGHULAB_INCLUDE_ANCHOR_NOT_FOUND" >&2 - exit 1 -fi - -getent passwd zhulan-proxy >/dev/null || { - echo "PROXY_USER_MUST_BE_PRECREATED" >&2 - exit 1 -} - -PUBLIC_KEY="$(tr -d '\r\n' <"$TUNNEL_PUBLIC_KEY_FILE")" -if [[ "$PUBLIC_KEY" != ssh-ed25519\ * ]]; then - echo "TUNNEL_PUBLIC_KEY_NOT_ED25519" >&2 - exit 1 -fi - -install -d -o root -g root -m 0700 "$BACKUP_ROOT" -install -m 0644 "$SITE_FILE" "$BACKUP_ROOT/guanghulab-$STAMP" -[[ ! -f "$SNIPPET_FILE" ]] || install -m 0644 "$SNIPPET_FILE" "$BACKUP_ROOT/snippet-$STAMP" -[[ ! -f "$SSHD_DROPIN" ]] || install -m 0600 "$SSHD_DROPIN" "$BACKUP_ROOT/sshd-$STAMP" - -install -d -o zhulan-proxy -g zhulan-proxy -m 0700 "$PROXY_HOME" "$PROXY_HOME/.ssh" -{ - printf '%s %s\n' \ - 'restrict,port-forwarding,permitlisten="127.0.0.1:17631"' \ - "$PUBLIC_KEY" -} >"$AUTH_KEYS.tmp" -chown zhulan-proxy:zhulan-proxy "$AUTH_KEYS.tmp" -chmod 0600 "$AUTH_KEYS.tmp" -mv "$AUTH_KEYS.tmp" "$AUTH_KEYS" - -cat >"$SSHD_DROPIN.tmp" <<'EOF' -Match User zhulan-proxy - AuthenticationMethods publickey - PasswordAuthentication no - KbdInteractiveAuthentication no - AllowAgentForwarding no - AllowTcpForwarding remote - GatewayPorts no - PermitListen 127.0.0.1:17631 - PermitOpen none - PermitTTY no - X11Forwarding no - PermitTunnel no - PermitUserRC no - MaxSessions 0 -EOF -chown root:root "$SSHD_DROPIN.tmp" -chmod 0600 "$SSHD_DROPIN.tmp" -mv "$SSHD_DROPIN.tmp" "$SSHD_DROPIN" -sshd -t - -install -o root -g root -m 0644 \ - "$SOURCE_DIR/deploy/guanghulab-zhulan.nginx.conf" "$SNIPPET_FILE" -if ! grep -q 'include /etc/nginx/snippets/guanghu-zhulan.conf;' "$SITE_FILE"; then - sed -i \ - '/include \/etc\/nginx\/snippets\/guanghu-ai-discovery.conf;/a\ include /etc/nginx/snippets/guanghu-zhulan.conf;' \ - "$SITE_FILE" -fi - -if ! nginx -t; then - install -m 0644 "$BACKUP_ROOT/guanghulab-$STAMP" "$SITE_FILE" - nginx -t - echo "NGINX_VALIDATION_FAILED_ROLLED_BACK" >&2 - exit 1 -fi - -systemctl reload ssh -systemctl reload nginx -printf '{"schema":"guanghu.zhulan-front-install-receipt/v1","development_id":"DEV-20260813-005","front_node":"BS-GZ-006","role":"DOMAIN_TLS_REVERSE_PROXY_ONLY","listener":"127.0.0.1:17631","installed_at":"%s"}\n' "$STAMP" diff --git a/server-tools/zhulan-remote-cell/deploy/install-runtime.sh b/server-tools/zhulan-remote-cell/deploy/install-runtime.sh deleted file mode 100755 index d958669..0000000 --- a/server-tools/zhulan-remote-cell/deploy/install-runtime.sh +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ "$(id -u)" -ne 0 ]]; then - echo "INSTALL_REQUIRES_ROOT" >&2 - exit 1 -fi - -SOURCE_DIR="${1:-}" -if [[ -z "$SOURCE_DIR" || ! -f "$SOURCE_DIR/runtime/zhulan_cell.py" ]]; then - echo "SOURCE_DIR_INVALID" >&2 - exit 1 -fi - -EXPECTED_HOST="${ZHULAN_EXPECTED_HOST:-VM-12-12-ubuntu}" -EXPECTED_DMI="${ZHULAN_EXPECTED_DMI:-7a500f2d-9aed-4b93-b3b8-59c87c65d031}" -ACTUAL_HOST="$(hostname)" -ACTUAL_DMI="$(tr '[:upper:]' '[:lower:]' &2 - exit 1 -fi - -if ! command -v bwrap >/dev/null 2>&1 || ! command -v apparmor_parser >/dev/null 2>&1; then - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends apparmor bubblewrap -fi - -INSTALL_ROOT="/opt/guanghu/zhulan-remote-cell" -STATE_ROOT="/var/lib/guanghu/zhulan-remote-cell" -WORKSPACE_ROOT="/srv/guanghu/zhulan-cell/workspaces" -CANDIDATE_ROOT="/srv/guanghu/zhulan-cell/candidates" -SECRET_ROOT="/etc/guanghu/secrets" -ENV_FILE="/etc/guanghu/zhulan-remote-cell.env" -SERVICE_FILE="/etc/systemd/system/zhulan-remote-cell.service" -EXECUTOR_SERVICE_FILE="/etc/systemd/system/zhulan-validation-executor.service" -APPARMOR_FILE="/etc/apparmor.d/zhulan-remote-cell-bwrap" -BACKUP_ROOT="/var/backups/guanghu/zhulan-remote-cell" -STAMP="$(date -u +%Y%m%dT%H%M%SZ)" - -getent passwd zhulan-runtime >/dev/null || { - echo "RUNTIME_USER_MUST_BE_PRECREATED" >&2 - exit 1 -} - -install -d -o root -g root -m 0755 "$INSTALL_ROOT" "$BACKUP_ROOT" -install -d -o zhulan-runtime -g zhulan-runtime -m 0700 "$STATE_ROOT" -install -d -o zhulan-runtime -g zhulan-runtime -m 0700 "$WORKSPACE_ROOT" -install -d -o zhulan-runtime -g zhulan-runtime -m 0700 "$CANDIDATE_ROOT" -install -d -o root -g zhulan-runtime -m 0750 "$SECRET_ROOT" - -if [[ -d "$INSTALL_ROOT/runtime" ]]; then - tar -C "$INSTALL_ROOT" -czf "$BACKUP_ROOT/runtime-$STAMP.tgz" runtime ui policy.json 2>/dev/null || true -fi -if [[ -f "$ENV_FILE" ]]; then - install -m 0600 "$ENV_FILE" "$BACKUP_ROOT/env-$STAMP" -fi -if [[ -f "$SERVICE_FILE" ]]; then - install -m 0644 "$SERVICE_FILE" "$BACKUP_ROOT/service-$STAMP" -fi -if [[ -f "$EXECUTOR_SERVICE_FILE" ]]; then - install -m 0644 "$EXECUTOR_SERVICE_FILE" "$BACKUP_ROOT/executor-service-$STAMP" -fi -if [[ -f "$APPARMOR_FILE" ]]; then - install -m 0644 "$APPARMOR_FILE" "$BACKUP_ROOT/apparmor-$STAMP" -fi - -install -d -o root -g root -m 0755 "$INSTALL_ROOT/runtime" "$INSTALL_ROOT/ui" "$INSTALL_ROOT/contracts" -install -o root -g root -m 0755 "$SOURCE_DIR/runtime/zhulan_cell.py" "$INSTALL_ROOT/runtime/zhulan_cell.py" -install -o root -g root -m 0755 "$SOURCE_DIR/runtime/zhulan_code_gate.py" "$INSTALL_ROOT/runtime/zhulan_code_gate.py" -install -o root -g root -m 0755 "$SOURCE_DIR/runtime/zhulan_validation_executor.py" "$INSTALL_ROOT/runtime/zhulan_validation_executor.py" -# Keep the distro bwrap binary non-setuid. A dedicated group-executable copy -# lets AppArmor grant userns only to this runtime path instead of weakening the -# host-wide unprivileged-userns policy or opening /usr/bin/bwrap for all users. -install -o root -g zhulan-runtime -m 0750 /usr/bin/bwrap "$INSTALL_ROOT/runtime/zhulan-bwrap" -install -o root -g root -m 0644 "$SOURCE_DIR/ui/index.html" "$INSTALL_ROOT/ui/index.html" -install -o root -g root -m 0644 "$SOURCE_DIR/ui/styles.css" "$INSTALL_ROOT/ui/styles.css" -install -o root -g root -m 0644 "$SOURCE_DIR/ui/app.js" "$INSTALL_ROOT/ui/app.js" -install -o root -g root -m 0644 "$SOURCE_DIR/policy.example.json" "$INSTALL_ROOT/policy.json" -install -o root -g root -m 0644 "$SOURCE_DIR/README.md" "$INSTALL_ROOT/README.md" - -if [[ -d "$SOURCE_DIR/contracts" ]]; then - find "$SOURCE_DIR/contracts" -maxdepth 1 -type f -print0 | while IFS= read -r -d '' file; do - install -o root -g root -m 0444 "$file" "$INSTALL_ROOT/contracts/$(basename "$file")" - done -fi - -# Install current Fifth Domain brains and Zhulan entry as read-only runtime -# contracts from the same exact repository checkout as this installer. -REPO_ROOT="$(cd "$SOURCE_DIR/../.." && pwd)" -SOURCE_COMMIT="${ZHULAN_SOURCE_COMMIT:-}" -if [[ -z "$SOURCE_COMMIT" ]] && git -C "$REPO_ROOT" rev-parse HEAD >/dev/null 2>&1; then - SOURCE_COMMIT="$(git -C "$REPO_ROOT" rev-parse HEAD)" -fi -if [[ ! "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then - echo "SOURCE_COMMIT_REQUIRED" >&2 - exit 1 -fi -for relative in \ - "skills/shared/guanghu-ui-brain/SKILL.md" \ - "skills/shared/guanghu-ui-brain/BRAIN.hdlp" \ - "skills/shared/guanghu-client-ui-brain/SKILL.md" \ - "skills/shared/guanghu-client-ui-brain/BRAIN.hdlp" \ - "光之湖/ICE-GL-ZL-001-铸澜/WAKE.hdlp" \ - "光之湖/ICE-GL-ZL-001-铸澜/INDEX.hdlp" \ - "光之湖/ICE-GL-ZL-001-铸澜/CURRENT.hdlp" \ - "光之湖/ICE-GL-ZL-001-铸澜/PERMANENT-MEMORY-WRITE-PROTOCOL.hdlp"; do - source_file="$REPO_ROOT/$relative" - if [[ ! -f "$source_file" ]]; then - echo "CONTRACT_SOURCE_MISSING $relative" >&2 - exit 1 - fi - safe_name="$(printf '%s' "$relative" | tr '/ ' '__')" - install -o root -g root -m 0444 "$source_file" "$INSTALL_ROOT/contracts/$safe_name" -done - -printf '%s\n' "$SOURCE_COMMIT" >"$INSTALL_ROOT/contracts/REPOSITORY-SOURCE-SHA" -chmod 0444 "$INSTALL_ROOT/contracts/REPOSITORY-SOURCE-SHA" - -SECRET_FILE="$SECRET_ROOT/zhulan-remote-cell.secret" -if [[ ! -f "$SECRET_FILE" ]]; then - umask 0177 - openssl rand -hex 48 >"$SECRET_FILE" -fi -chown root:zhulan-runtime "$SECRET_FILE" -chmod 0640 "$SECRET_FILE" - -install -o root -g zhulan-runtime -m 0640 "$SOURCE_DIR/deploy/zhulan-remote-cell.env.example" "$ENV_FILE" -install -o root -g root -m 0644 "$SOURCE_DIR/deploy/zhulan-remote-cell.service" "$SERVICE_FILE" -install -o root -g root -m 0644 "$SOURCE_DIR/deploy/zhulan-validation-executor.service" "$EXECUTOR_SERVICE_FILE" -install -o root -g root -m 0644 "$SOURCE_DIR/deploy/zhulan-bwrap.apparmor" "$APPARMOR_FILE" -apparmor_parser -r "$APPARMOR_FILE" - -systemctl daemon-reload -systemctl enable zhulan-validation-executor.service -systemctl restart zhulan-validation-executor.service -for _ in $(seq 1 30); do - if [[ -S /run/guanghu/zhulan-validation/executor.sock ]]; then - break - fi - sleep 1 -done -if [[ ! -S /run/guanghu/zhulan-validation/executor.sock ]]; then - systemctl status zhulan-validation-executor.service --no-pager >&2 || true - echo "VALIDATION_EXECUTOR_HEALTH_FAILED" >&2 - exit 1 -fi -systemctl enable zhulan-remote-cell.service -systemctl restart zhulan-remote-cell.service - -for _ in $(seq 1 30); do - if curl --fail --silent --show-error --max-time 2 http://127.0.0.1:17631/health >"$STATE_ROOT/health.json.tmp"; then - mv "$STATE_ROOT/health.json.tmp" "$STATE_ROOT/health.json" - break - fi - sleep 1 -done - -if [[ ! -s "$STATE_ROOT/health.json" ]]; then - systemctl status zhulan-remote-cell.service --no-pager >&2 || true - echo "RUNTIME_HEALTH_FAILED" >&2 - exit 1 -fi - -SOURCE_SHA256="$(find \ - "$INSTALL_ROOT/runtime" \ - "$INSTALL_ROOT/ui" \ - "$INSTALL_ROOT/contracts" \ - "$INSTALL_ROOT/policy.json" \ - "$INSTALL_ROOT/README.md" \ - -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')" -cat >"$STATE_ROOT/install-receipt.json.tmp" <&2 - exit 1 -fi - -FRONT_HOST="${1:-}" -KNOWN_HOSTS_SOURCE="${2:-}" -if [[ ! "$FRONT_HOST" =~ ^[0-9A-Fa-f:.]+$ ]]; then - echo "FRONT_HOST_MUST_BE_LITERAL_IP" >&2 - exit 1 -fi -if [[ -z "$KNOWN_HOSTS_SOURCE" || ! -f "$KNOWN_HOSTS_SOURCE" ]]; then - echo "KNOWN_HOSTS_SOURCE_INVALID" >&2 - exit 1 -fi - -EXPECTED_HOST="${ZHULAN_EXPECTED_HOST:-VM-12-12-ubuntu}" -EXPECTED_DMI="${ZHULAN_EXPECTED_DMI:-7a500f2d-9aed-4b93-b3b8-59c87c65d031}" -ACTUAL_HOST="$(hostname)" -ACTUAL_DMI="$(tr '[:upper:]' '[:lower:]' &2 - exit 1 -fi - -CONFIG_ROOT="/etc/guanghu/zhulan-front-tunnel" -KEY_FILE="$CONFIG_ROOT/id_ed25519" -KNOWN_HOSTS="$CONFIG_ROOT/known_hosts" -SSH_CONFIG="$CONFIG_ROOT/ssh_config" -SERVICE_FILE="/etc/systemd/system/zhulan-front-tunnel.service" -if [[ ! -f "$KEY_FILE" || ! -f "$KEY_FILE.pub" ]]; then - echo "TUNNEL_IDENTITY_NOT_PREPARED" >&2 - exit 1 -fi -if ! getent passwd zhulan-tunnel >/dev/null; then - echo "TUNNEL_USER_NOT_PREPARED" >&2 - exit 1 -fi - -install -o root -g zhulan-tunnel -m 0640 "$KNOWN_HOSTS_SOURCE" "$KNOWN_HOSTS" -if ! ssh-keygen -F "$FRONT_HOST" -f "$KNOWN_HOSTS" >/dev/null; then - echo "PINNED_FRONT_HOST_KEY_MISSING" >&2 - exit 1 -fi -EXPECTED_FRONT_HOST_KEY="${ZHULAN_EXPECTED_FRONT_HOST_KEY:-SHA256:P2EtuYFg8hptha4s0auP4yzp+Wg+H27bw4mdqzk1Cvk}" -ACTUAL_FRONT_HOST_KEY="$(ssh-keygen -F "$FRONT_HOST" -f "$KNOWN_HOSTS" | awk 'NF && $1 !~ /^#/ {print $2, $3}' | ssh-keygen -lf - 2>/dev/null | awk '{print $2}' | head -1)" -if [[ "$ACTUAL_FRONT_HOST_KEY" != "$EXPECTED_FRONT_HOST_KEY" ]]; then - echo "PINNED_FRONT_HOST_KEY_MISMATCH" >&2 - exit 1 -fi - -cat >"$SSH_CONFIG.tmp" <"$SERVICE_FILE.tmp" <<'EOF' -[Unit] -Description=Zhulan SG003 to GZ006 loopback-only reverse proxy tunnel -After=network-online.target zhulan-remote-cell.service -Wants=network-online.target -Requires=zhulan-remote-cell.service - -[Service] -Type=simple -User=zhulan-tunnel -Group=zhulan-tunnel -ExecStart=/usr/bin/ssh -F /etc/guanghu/zhulan-front-tunnel/ssh_config -NT guanghu-zhulan-front -Restart=always -RestartSec=5 -UMask=0077 -NoNewPrivileges=true -PrivateTmp=true -ProtectSystem=strict -ProtectHome=true -ProtectKernelTunables=true -ProtectKernelModules=true -ProtectControlGroups=true -RestrictSUIDSGID=true -LockPersonality=true -MemoryDenyWriteExecute=true -RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 -ReadOnlyPaths=/etc/guanghu/zhulan-front-tunnel -StateDirectory=guanghu/zhulan-front-tunnel - -[Install] -WantedBy=multi-user.target -EOF -chown root:root "$SERVICE_FILE.tmp" -chmod 0644 "$SERVICE_FILE.tmp" -mv "$SERVICE_FILE.tmp" "$SERVICE_FILE" - -systemctl daemon-reload -systemctl enable --now zhulan-front-tunnel.service -for _ in $(seq 1 20); do - if systemctl is-active --quiet zhulan-front-tunnel.service; then - systemctl show zhulan-front-tunnel.service -p ActiveState -p SubState -p MainPID - exit 0 - fi - sleep 1 -done -systemctl status zhulan-front-tunnel.service --no-pager >&2 || true -exit 1 diff --git a/server-tools/zhulan-remote-cell/deploy/prepare-tunnel-identity.sh b/server-tools/zhulan-remote-cell/deploy/prepare-tunnel-identity.sh deleted file mode 100755 index 9ea0479..0000000 --- a/server-tools/zhulan-remote-cell/deploy/prepare-tunnel-identity.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ "$(id -u)" -ne 0 ]]; then - echo "INSTALL_REQUIRES_ROOT" >&2 - exit 1 -fi - -EXPECTED_HOST="${ZHULAN_EXPECTED_HOST:-VM-12-12-ubuntu}" -EXPECTED_DMI="${ZHULAN_EXPECTED_DMI:-7a500f2d-9aed-4b93-b3b8-59c87c65d031}" -ACTUAL_HOST="$(hostname)" -ACTUAL_DMI="$(tr '[:upper:]' '[:lower:]' &2 - exit 1 -fi - -CONFIG_ROOT="/etc/guanghu/zhulan-front-tunnel" -KEY_FILE="$CONFIG_ROOT/id_ed25519" - -getent passwd zhulan-tunnel >/dev/null || { - echo "TUNNEL_USER_MUST_BE_PRECREATED" >&2 - exit 1 -} -install -d -o root -g zhulan-tunnel -m 0750 "$CONFIG_ROOT" -install -d -o zhulan-tunnel -g zhulan-tunnel -m 0700 /var/lib/guanghu/zhulan-front-tunnel - -if [[ ! -f "$KEY_FILE" ]]; then - ssh-keygen -q -t ed25519 -N '' -C 'zhulan-sg003-to-gz006-loopback-only' -f "$KEY_FILE" -fi -chown root:zhulan-tunnel "$KEY_FILE" "$KEY_FILE.pub" -chmod 0640 "$KEY_FILE" -chmod 0644 "$KEY_FILE.pub" - -ssh-keygen -lf "$KEY_FILE.pub" -echo "TUNNEL_PUBLIC_KEY_FILE=$KEY_FILE.pub" diff --git a/server-tools/zhulan-remote-cell/deploy/zhulan-bwrap.apparmor b/server-tools/zhulan-remote-cell/deploy/zhulan-bwrap.apparmor deleted file mode 100644 index 07fbfa7..0000000 --- a/server-tools/zhulan-remote-cell/deploy/zhulan-bwrap.apparmor +++ /dev/null @@ -1,10 +0,0 @@ -# Ubuntu 24.04 restricts unprivileged user namespaces unless the executable -# has an AppArmor profile that explicitly allows userns. This profile applies -# only to the root-owned Zhulan bwrap copy; that binary is executable only by -# root and the zhulan-runtime group. -abi , -include - -profile zhulan-remote-cell-bwrap /opt/guanghu/zhulan-remote-cell/runtime/zhulan-bwrap flags=(unconfined) { - userns, -} diff --git a/server-tools/zhulan-remote-cell/deploy/zhulan-remote-cell.env.example b/server-tools/zhulan-remote-cell/deploy/zhulan-remote-cell.env.example deleted file mode 100644 index 6b25a25..0000000 --- a/server-tools/zhulan-remote-cell/deploy/zhulan-remote-cell.env.example +++ /dev/null @@ -1,17 +0,0 @@ -ZHULAN_BIND=127.0.0.1 -ZHULAN_PORT=17631 -ZHULAN_DB=/var/lib/guanghu/zhulan-remote-cell/state.sqlite3 -ZHULAN_SECRET_FILE=/etc/guanghu/secrets/zhulan-remote-cell.secret -ZHULAN_POLICY=/opt/guanghu/zhulan-remote-cell/policy.json -ZHULAN_UI_DIR=/opt/guanghu/zhulan-remote-cell/ui -ZHULAN_WORKSPACE_ROOT=/srv/guanghu/zhulan-cell/workspaces -ZHULAN_CANDIDATE_ROOT=/srv/guanghu/zhulan-cell/candidates -ZHULAN_FORGEJO_VERIFY_URL=https://guanghulab.com/code/api/v1/user -ZHULAN_OWNER_LOGIN=bingshuo -ZHULAN_COOKIE_SECURE=1 -ZHULAN_COOKIE_PATH=/zhulan/ -ZHULAN_PUBLIC_CREATE_LIMIT=12 -ZHULAN_OWNER_LOGIN_LIMIT=8 -ZHULAN_OAUTH_REGISTER_LIMIT=20 -ZHULAN_BWRAP=/opt/guanghu/zhulan-remote-cell/runtime/zhulan-bwrap -ZHULAN_VALIDATION_SOCKET=/run/guanghu/zhulan-validation/executor.sock diff --git a/server-tools/zhulan-remote-cell/deploy/zhulan-remote-cell.service b/server-tools/zhulan-remote-cell/deploy/zhulan-remote-cell.service deleted file mode 100644 index a271835..0000000 --- a/server-tools/zhulan-remote-cell/deploy/zhulan-remote-cell.service +++ /dev/null @@ -1,37 +0,0 @@ -[Unit] -Description=Guanghu Zhulan Restricted Remote Development Cell -After=network-online.target zhulan-validation-executor.service -Wants=network-online.target -Requires=zhulan-validation-executor.service - -[Service] -Type=simple -User=zhulan-runtime -Group=zhulan-runtime -EnvironmentFile=/etc/guanghu/zhulan-remote-cell.env -ExecStart=/usr/bin/python3 /opt/guanghu/zhulan-remote-cell/runtime/zhulan_cell.py -Restart=on-failure -RestartSec=3 -UMask=0077 -NoNewPrivileges=true -PrivateTmp=true -PrivateDevices=true -ProtectSystem=strict -ProtectHome=true -ProtectKernelTunables=true -ProtectKernelModules=true -ProtectControlGroups=true -RestrictSUIDSGID=true -RestrictNamespaces=user mnt pid net ipc uts -LockPersonality=true -MemoryDenyWriteExecute=true -RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 -SystemCallArchitectures=native -ReadOnlyPaths=/opt/guanghu/zhulan-remote-cell /etc/guanghu/zhulan-remote-cell.env /etc/guanghu/secrets/zhulan-remote-cell.secret -ReadWritePaths=/var/lib/guanghu/zhulan-remote-cell -ReadWritePaths=/srv/guanghu/zhulan-cell/workspaces -ReadWritePaths=/srv/guanghu/zhulan-cell/candidates -StateDirectory=guanghu/zhulan-remote-cell - -[Install] -WantedBy=multi-user.target diff --git a/server-tools/zhulan-remote-cell/deploy/zhulan-validation-executor.service b/server-tools/zhulan-remote-cell/deploy/zhulan-validation-executor.service deleted file mode 100644 index 7d7cae0..0000000 --- a/server-tools/zhulan-remote-cell/deploy/zhulan-validation-executor.service +++ /dev/null @@ -1,37 +0,0 @@ -[Unit] -Description=Zhulan local-only allowlisted validation executor -After=local-fs.target -Before=zhulan-remote-cell.service - -[Service] -Type=simple -User=zhulan-runtime -Group=zhulan-runtime -EnvironmentFile=/etc/guanghu/zhulan-remote-cell.env -ExecStart=/usr/bin/python3 /opt/guanghu/zhulan-remote-cell/runtime/zhulan_validation_executor.py -Restart=always -RestartSec=3 -UMask=0077 -PrivateTmp=true -PrivateDevices=true -PrivateNetwork=true -ProtectSystem=strict -ProtectHome=true -ProtectKernelModules=true -ProtectControlGroups=true -RestrictSUIDSGID=true -LockPersonality=true -MemoryDenyWriteExecute=true -RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK -RestrictNamespaces=user mnt pid net ipc uts -SystemCallArchitectures=native -RuntimeDirectory=guanghu/zhulan-validation -RuntimeDirectoryMode=0700 -ReadOnlyPaths=/opt/guanghu/zhulan-remote-cell /etc/guanghu/zhulan-remote-cell.env -ReadOnlyPaths=/srv/guanghu/zhulan-cell/workspaces -InaccessiblePaths=/etc/guanghu/secrets -ReadOnlyPaths=/var/lib/guanghu/zhulan-remote-cell -InaccessiblePaths=/srv/guanghu/zhulan-cell/candidates - -[Install] -WantedBy=multi-user.target diff --git a/server-tools/zhulan-remote-cell/policy.example.json b/server-tools/zhulan-remote-cell/policy.example.json deleted file mode 100644 index c41b902..0000000 --- a/server-tools/zhulan-remote-cell/policy.example.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "schema": "guanghu.zhulan-remote-cell-policy/v1", - "persona_id": "ICE-GL-ZL-001", - "node_id": "BS-SG-003", - "front_door": "https://guanghulab.com/zhulan/", - "default_ttl_seconds": 10800, - "max_ttl_seconds": 86400, - "max_request_paths": 24, - "max_changed_file_bytes": 20971520, - "max_read_file_bytes": 262144, - "workspace_root": "/srv/guanghu/zhulan-cell/workspaces", - "candidate_root": "/srv/guanghu/zhulan-cell/candidates", - "repository_base_url": "https://guanghulab.com/code/", - "allowed_actions": [ - "read", - "edit", - "test", - "commit", - "push_candidate" - ], - "repositories": { - "bingshuo/guanghu-ice-heart": { - "candidate_branch_prefix": "zhulan/", - "allowed_path_prefixes": ["."], - "validation_commands": [ - ["git", "diff", "--check"], - ["python3", "-m", "unittest", "discover", "-s", "server-tools/zhulan-remote-cell/tests", "-v"] - ] - }, - "bingshuo/hololake-system-architecture": { - "candidate_branch_prefix": "zhulan/", - "allowed_path_prefixes": ["."], - "validation_commands": [["git", "diff", "--check"]] - }, - "bingshuo/grok-build-upstream-mirror": { - "candidate_branch_prefix": "zhulan/", - "allowed_path_prefixes": ["."], - "validation_commands": [["git", "diff", "--check"]] - } - } -} diff --git a/server-tools/zhulan-remote-cell/runtime/zhulan_cell.py b/server-tools/zhulan-remote-cell/runtime/zhulan_cell.py deleted file mode 100644 index eb3b24b..0000000 --- a/server-tools/zhulan-remote-cell/runtime/zhulan_cell.py +++ /dev/null @@ -1,2037 +0,0 @@ -#!/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'' - for item in requests - ) - hidden = "".join( - f'' - for key, value in oauth.items() - ) - disabled = "" if requests else " disabled" - return f""" -铸澜 · 绑定插件连接 -
-

ZHULAN · OAUTH 2.1 · PKCE

把插件连接绑定到已批准边界

-

这不会把 SSH 密钥或临时能力交给手机。连接只能使用你选中的人格、仓库、候选分支、路径、动作和到期时间。

-
{hidden} - - -
-{'没有仍在有效期内的已批准申请。请返回审批端先批准一张申请。' if not requests else '到期、拒绝或扩大边界后,这个连接都会失效。'} -
""" - - 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() diff --git a/server-tools/zhulan-remote-cell/runtime/zhulan_code_gate.py b/server-tools/zhulan-remote-cell/runtime/zhulan_code_gate.py deleted file mode 100644 index c525eaa..0000000 --- a/server-tools/zhulan-remote-cell/runtime/zhulan_code_gate.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -"""铸澜工作区代码门:在 commit / candidate push 前校验能力与实际 Git 变化。""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import re -import subprocess -import sys -import urllib.error -import urllib.request -from pathlib import Path -from typing import Any - - -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,}"), -] - - -def run_git(workspace: Path, *args: str) -> str: - result = subprocess.run( - ["git", "-C", str(workspace), *args], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - timeout=30, - ) - return result.stdout.strip() - - -def within(path: str, prefixes: list[str]) -> bool: - return any(prefix == "." or path == prefix or path.startswith(prefix + "/") for prefix in prefixes) - - -def verify_remote(url: str, token: str, action: str) -> dict[str, Any]: - raw = json.dumps({"capability": token, "action": action}).encode("utf-8") - request = urllib.request.Request( - url.rstrip("/") + "/api/v1/runtime/verify", - data=raw, - headers={"Content-Type": "application/json", "User-Agent": "zhulan-code-gate/1"}, - ) - try: - with urllib.request.urlopen(request, timeout=8) as response: - return json.loads(response.read(262144)) - except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, ValueError) as exc: - raise RuntimeError("capability_verify_failed") from exc - - -def changed_files(workspace: Path, base_sha: str) -> list[str]: - run_git(workspace, "merge-base", "--is-ancestor", base_sha, "HEAD") - committed = run_git(workspace, "diff", "--name-only", f"{base_sha}...HEAD") - pending = run_git(workspace, "diff", "--name-only", "HEAD") - untracked = run_git(workspace, "ls-files", "--others", "--exclude-standard") - return sorted({line for block in (committed, pending, untracked) for line in block.splitlines() if line}) - - -def scan_file(workspace: Path, path: Path, max_bytes: int) -> list[str]: - errors: list[str] = [] - if path.is_symlink(): - target = path.resolve() - try: - target.relative_to(workspace) - except ValueError: - errors.append("symlink_outside_workspace") - return errors - if not path.is_file(): - return errors - size = path.stat().st_size - if size > max_bytes: - return [f"file_too_large:{size}"] - if size > 2_000_000: - return errors - raw = path.read_bytes() - for pattern in SECRET_PATTERNS: - if pattern.search(raw): - errors.append("possible_secret") - break - return errors - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--workspace", required=True) - parser.add_argument("--capability-file", required=True) - parser.add_argument("--action", choices=["edit", "test", "commit", "push_candidate"], required=True) - parser.add_argument("--verify-url", default=os.getenv("ZHULAN_VERIFY_URL", "http://127.0.0.1:17631")) - parser.add_argument("--max-file-bytes", type=int, default=20 * 1024 * 1024) - args = parser.parse_args() - - workspace = Path(args.workspace).resolve() - token = Path(args.capability_file).read_text(encoding="utf-8").strip() - verified = verify_remote(args.verify_url, token, args.action) - claims = verified["claims"] - errors: list[dict[str, str]] = [] - - if run_git(workspace, "rev-parse", "--is-inside-work-tree") != "true": - errors.append({"gate": "git", "error": "not_a_worktree"}) - branch = run_git(workspace, "branch", "--show-current") - if branch != claims["branch"]: - errors.append({"gate": "branch", "error": f"expected:{claims['branch']}:actual:{branch}"}) - - try: - files = changed_files(workspace, claims["base_sha"]) - except subprocess.CalledProcessError: - files = [] - errors.append({"gate": "base_sha", "error": "base_not_ancestor"}) - - for name in files: - if not within(name, claims["paths"]): - errors.append({"gate": "path", "error": name}) - continue - for finding in scan_file(workspace, workspace / name, args.max_file_bytes): - errors.append({"gate": finding, "error": name}) - - result = { - "schema": "guanghu.zhulan-code-gate-receipt/v1", - "decision": "PASS" if not errors else "FAIL", - "request_id": claims["request_id"], - "development_id": claims["development_id"], - "repository": claims["repository"], - "branch": branch, - "base_sha": claims["base_sha"], - "changed_files": files, - "errors": errors, - "workspace_fingerprint": hashlib.sha256(str(workspace).encode()).hexdigest(), - "capability_receipt": verified["receipt"], - } - print(json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2)) - return 0 if not errors else 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/server-tools/zhulan-remote-cell/runtime/zhulan_validation_executor.py b/server-tools/zhulan-remote-cell/runtime/zhulan_validation_executor.py deleted file mode 100644 index 365b658..0000000 --- a/server-tools/zhulan-remote-cell/runtime/zhulan_validation_executor.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env python3 -"""Local-only allowlisted Bubblewrap validation broker for the Zhulan runtime.""" - -from __future__ import annotations - -import json -import os -import signal -import socketserver -import sqlite3 -import subprocess -import time -from pathlib import Path -from typing import Any - - -MAX_REQUEST_BYTES = 64 * 1024 - - -def safe_relative(value: Any) -> str: - if not isinstance(value, str): - raise PermissionError("validation_path_invalid") - raw = value.replace("\\", "/").strip("/") - if raw in {"", "."}: - return "." - parts = raw.split("/") - if any(part in {"", ".", ".."} for part in parts): - raise PermissionError("validation_path_invalid") - return "/".join(parts) - - -def path_within(path: str, prefix: str) -> bool: - return prefix == "." or path == prefix or path.startswith(prefix + "/") - - -class ValidationExecutor: - def __init__(self) -> None: - self.workspace_root = Path( - os.getenv("ZHULAN_WORKSPACE_ROOT", "/srv/guanghu/zhulan-cell/workspaces") - ).resolve() - self.policy_path = Path( - os.getenv("ZHULAN_POLICY", "/opt/guanghu/zhulan-remote-cell/policy.json") - ) - self.state_db = Path( - os.getenv("ZHULAN_DB", "/var/lib/guanghu/zhulan-remote-cell/state.sqlite3") - ) - self.bwrap = Path( - os.getenv( - "ZHULAN_BWRAP", "/opt/guanghu/zhulan-remote-cell/runtime/zhulan-bwrap" - ) - ) - - def policy(self) -> dict[str, Any]: - return json.loads(self.policy_path.read_text(encoding="utf-8")) - - def validate(self, request: dict[str, Any]) -> tuple[Path, list[str], list[str]]: - if set(request) != { - "operation", - "request_id", - "repository", - "development_id", - "workspace", - "paths", - "command", - }: - raise PermissionError("validation_request_fields_invalid") - if request.get("operation") != "run": - raise PermissionError("validation_operation_invalid") - repository = str(request.get("repository", "")) - request_id = str(request.get("request_id", "")) - development_id = str(request.get("development_id", "")) - command = request.get("command") - paths = request.get("paths") - if not isinstance(command, list) or not command or not all( - isinstance(item, str) for item in command - ): - raise PermissionError("validation_command_invalid") - if not isinstance(paths, list) or not paths: - raise PermissionError("validation_paths_invalid") - clean_paths = sorted(set(safe_relative(item) for item in paths)) - policy = self.policy() - repository_policy = policy.get("repositories", {}).get(repository) - if not isinstance(repository_policy, dict): - raise PermissionError("validation_repository_not_registered") - allowed = repository_policy.get("validation_commands", []) - if command not in allowed or command == ["git", "diff", "--check"]: - raise PermissionError("validation_command_not_registered_for_broker") - allowed_prefixes = [safe_relative(item) for item in repository_policy.get("allowed_path_prefixes", ["."])] - if any(not any(path_within(path, prefix) for prefix in allowed_prefixes) for path in clean_paths): - raise PermissionError("validation_path_outside_repository_policy") - uri = f"file:{self.state_db}?mode=ro" - with sqlite3.connect(uri, uri=True, timeout=5) as db: - db.row_factory = sqlite3.Row - row = db.execute( - """SELECT id, state, picked_up_at, capability_expires_at, - development_id, repository, paths_json, actions_json - FROM requests WHERE id=?""", - (request_id,), - ).fetchone() - if ( - not row - or row["state"] != "APPROVED" - or not row["picked_up_at"] - or int(row["capability_expires_at"] or 0) <= int(time.time()) - ): - raise PermissionError("validation_approval_not_active") - if ( - row["development_id"] != development_id - or row["repository"] != repository - or json.loads(row["paths_json"]) != clean_paths - or "test" not in json.loads(row["actions_json"]) - ): - raise PermissionError("validation_approval_binding_mismatch") - workspace = Path(str(request.get("workspace", ""))).resolve() - workspace.relative_to(self.workspace_root) - if ( - not workspace.is_dir() - or not (workspace / ".git").is_dir() - or (workspace / ".git").is_symlink() - ): - raise PermissionError("validation_workspace_invalid") - owner, name = repository.split("/", 1) - if ( - workspace.name != f"{owner}__{name}" - or workspace.parent.name != development_id - ): - raise PermissionError("validation_workspace_repository_mismatch") - if not self.bwrap.is_file(): - raise RuntimeError("validation_bwrap_missing") - return workspace, clean_paths, command - - @staticmethod - def projection(workspace: Path, paths: list[str]) -> list[str]: - if paths == ["."]: - return ["--ro-bind", str(workspace), "/workspace"] - result: list[str] = [] - created = {"/workspace"} - for relative in paths: - target = workspace / relative - destination = Path("/workspace") / relative - parents = [item for item in reversed(destination.parents) if str(item).startswith("/workspace")] - for parent in parents: - text = str(parent) - if text not in created: - result.extend(["--dir", text]) - created.add(text) - if target.exists(): - target.resolve().relative_to(workspace) - result.extend(["--ro-bind", str(target), str(destination)]) - elif str(destination) not in created: - result.extend(["--dir", str(destination)]) - created.add(str(destination)) - return result - - def execute(self, request: dict[str, Any]) -> dict[str, Any]: - if request.get("operation") == "ping": - if set(request) != {"operation"}: - raise PermissionError("validation_ping_fields_invalid") - return {"ok": True, "executor": "zhulan-validation"} - workspace, paths, command = self.validate(request) - argv = [ - str(self.bwrap), - "--die-with-parent", - "--new-session", - "--unshare-user", - "--unshare-pid", - "--unshare-net", - "--unshare-ipc", - "--unshare-uts", - "--ro-bind", - "/usr", - "/usr", - "--ro-bind", - "/bin", - "/bin", - "--ro-bind-try", - "/lib", - "/lib", - "--ro-bind-try", - "/lib64", - "/lib64", - "--proc", - "/proc", - "--dev", - "/dev", - "--tmpfs", - "/tmp", - "--dir", - "/home", - "--dir", - "/home/zhulan", - *self.projection(workspace, paths), - "--chdir", - "/workspace", - "--clearenv", - "--setenv", - "HOME", - "/home/zhulan", - "--setenv", - "PATH", - "/usr/local/bin:/usr/bin:/bin", - "--", - *command, - ] - result = subprocess.run( - argv, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - timeout=180, - env={"PATH": "/usr/local/bin:/usr/bin:/bin", "LANG": "C.UTF-8"}, - ) - return {"ok": True, "exit_code": result.returncode, "output": result.stdout[-12000:]} - - -class Handler(socketserver.StreamRequestHandler): - def handle(self) -> None: - raw = self.rfile.readline(MAX_REQUEST_BYTES + 1) - if not raw or len(raw) > MAX_REQUEST_BYTES: - return - try: - request = json.loads(raw) - if not isinstance(request, dict): - raise ValueError("request_not_object") - response = self.server.executor.execute(request) # type: ignore[attr-defined] - except Exception as exc: - response = {"ok": False, "error": str(exc)[:300]} - self.wfile.write((json.dumps(response, sort_keys=True) + "\n").encode("utf-8")) - - -class Server(socketserver.UnixStreamServer): - allow_reuse_address = True - - -def main() -> None: - socket_path = Path( - os.getenv( - "ZHULAN_VALIDATION_SOCKET", "/run/guanghu/zhulan-validation/executor.sock" - ) - ) - socket_path.parent.mkdir(parents=True, exist_ok=True) - if socket_path.exists(): - socket_path.unlink() - server = Server(str(socket_path), Handler) - server.executor = ValidationExecutor() # type: ignore[attr-defined] - os.chmod(socket_path, 0o600) - def stop(_signum: int, _frame: object) -> None: - server.server_close() - raise SystemExit(0) - - signal.signal(signal.SIGTERM, stop) - signal.signal(signal.SIGINT, stop) - try: - server.serve_forever() - finally: - server.server_close() - socket_path.unlink(missing_ok=True) - - -if __name__ == "__main__": - main() diff --git a/server-tools/zhulan-remote-cell/tests/test_code_gate.py b/server-tools/zhulan-remote-cell/tests/test_code_gate.py deleted file mode 100644 index cb6b91f..0000000 --- a/server-tools/zhulan-remote-cell/tests/test_code_gate.py +++ /dev/null @@ -1,32 +0,0 @@ -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() diff --git a/server-tools/zhulan-remote-cell/tests/test_deployment_contract.py b/server-tools/zhulan-remote-cell/tests/test_deployment_contract.py deleted file mode 100644 index 707daab..0000000 --- a/server-tools/zhulan-remote-cell/tests/test_deployment_contract.py +++ /dev/null @@ -1,59 +0,0 @@ -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() diff --git a/server-tools/zhulan-remote-cell/tests/test_validation_executor.py b/server-tools/zhulan-remote-cell/tests/test_validation_executor.py deleted file mode 100644 index 42da6fb..0000000 --- a/server-tools/zhulan-remote-cell/tests/test_validation_executor.py +++ /dev/null @@ -1,129 +0,0 @@ -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() diff --git a/server-tools/zhulan-remote-cell/tests/test_zhulan_cell.py b/server-tools/zhulan-remote-cell/tests/test_zhulan_cell.py deleted file mode 100644 index 2fe467c..0000000 --- a/server-tools/zhulan-remote-cell/tests/test_zhulan_cell.py +++ /dev/null @@ -1,589 +0,0 @@ -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() diff --git a/server-tools/zhulan-remote-cell/ui/app.js b/server-tools/zhulan-remote-cell/ui/app.js deleted file mode 100644 index 538f407..0000000 --- a/server-tools/zhulan-remote-cell/ui/app.js +++ /dev/null @@ -1,213 +0,0 @@ -const $ = (id) => document.getElementById(id); -const state = { csrf: "", requests: [], selected: null }; -const eventNames = { - request_created: "铸澜提交申请", - human_approved: "主人批准边界", - human_rejected: "主人拒绝申请", - human_revoked: "主人撤销能力", - capability_picked_up: "铸澜领取临时能力", - capability_verified: "运行层校验能力", - capability_expired: "临时能力到期", - request_expired: "申请单到期" -}; - -async function api(path, options = {}) { - const headers = { "Content-Type": "application/json", ...(options.headers || {}) }; - const response = await fetch(path, { credentials: "same-origin", ...options, headers }); - let data = {}; - try { data = await response.json(); } catch (_) {} - if (!response.ok) throw new Error(data.error || `HTTP_${response.status}`); - return data; -} - -function toast(message) { - const node = $("toast"); - node.textContent = message; - node.classList.add("show"); - window.setTimeout(() => node.classList.remove("show"), 2600); -} - -function humanAction(value) { - return ({ read: "读取", edit: "编辑", test: "运行测试", commit: "提交", push_candidate: "推送候选分支" })[value] || value; -} - -function stateLabel(value) { - return ({ PENDING: "等待批准", APPROVED: "已批准", REJECTED: "已拒绝", REVOKED: "已撤销", EXPIRED: "已过期" })[value] || value; -} - -function renderRequest(request) { - state.selected = request; - $("empty-state").classList.add("hidden"); - $("request-card").classList.remove("hidden"); - $("request-id").textContent = request.id; - $("request-description").textContent = request.description; - $("persona").textContent = request.persona_id; - $("development-id").textContent = request.development_id; - $("repository").textContent = request.repository; - $("branch").textContent = request.branch; - $("base-sha").textContent = request.base_sha; - $("actions").replaceChildren(...request.actions.map((action) => { - const node = document.createElement("span"); - node.className = "chip"; - node.textContent = humanAction(action); - return node; - })); - $("paths").replaceChildren(...request.paths.map((path) => { - const node = document.createElement("span"); - node.className = "path"; - node.textContent = path; - return node; - })); - const label = stateLabel(request.state); - for (const id of ["request-state", "card-state"]) { - const node = $(id); - node.textContent = label; - node.className = `state ${request.state === "APPROVED" ? "approved" : request.state === "REJECTED" ? "rejected" : "waiting"}`; - } - $("decision-area").classList.toggle("hidden", request.state !== "PENDING"); - $("revoke-area").classList.toggle("hidden", request.state !== "APPROVED"); - loadReceipts(request.id); -} - -function renderEmpty(message, detail) { - state.selected = null; - $("request-card").classList.add("hidden"); - const empty = $("empty-state"); - empty.classList.remove("hidden"); - empty.querySelector("h3").textContent = message; - empty.querySelector("p").textContent = detail; - $("request-state").textContent = "湖面安静"; -} - -async function loadReceipts(requestId) { - try { - const data = await api(`api/v1/owner/requests/${encodeURIComponent(requestId)}/receipts`); - const nodes = data.receipts.map((receipt) => { - const li = document.createElement("li"); - li.className = receipt.result === "PASS" || receipt.result === "APPROVED" || receipt.result === "PENDING" ? "pass" : "fail"; - const dot = document.createElement("i"); - const body = document.createElement("div"); - const title = document.createElement("strong"); - title.textContent = eventNames[receipt.event] || receipt.event; - const detail = document.createElement("span"); - detail.textContent = `${receipt.created_at} · ${receipt.result} · ${receipt.receipt_hash.slice(0, 12)}`; - body.append(title, detail); - li.append(dot, body); - return li; - }); - $("timeline").replaceChildren(...nodes); - } catch (_) { - $("timeline").innerHTML = "
  • 回执读取失败运行层没有返回可验证结果。
  • "; - } -} - -async function loadRequests(preferredId = "") { - const data = await api("api/v1/owner/requests"); - state.requests = data.requests; - const preferred = preferredId && data.requests.find((item) => item.id === preferredId); - const pending = data.requests.find((item) => item.state === "PENDING"); - const selected = preferred || pending || data.requests[0]; - if (selected) renderRequest(selected); - else renderEmpty("暂无真实申请", "铸澜提交结构化申请后会出现在这里;公开建单本身没有执行权。"); -} - -async function restoreSession() { - try { - const session = await api("api/v1/owner/session"); - state.csrf = session.csrf; - $("auth-title").textContent = `主人 ${session.owner_login} · 已验证`; - $("auth-detail").textContent = "审批会话有效。任何扩大仓库、路径、动作或期限的请求都必须重新批准。"; - $("login-open").classList.add("hidden"); - $("logout").classList.remove("hidden"); - const preferred = new URLSearchParams(location.search).get("request") || ""; - await loadRequests(preferred); - const oauthReturn = new URLSearchParams(location.search).get("oauth_return"); - if (oauthReturn) { - try { - const normalized = oauthReturn.replace(/-/g, "+").replace(/_/g, "/"); - const decoded = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4)); - if (decoded.startsWith("/oauth/authorize?")) location.assign(`/zhulan${decoded}`); - } catch (_) {} - } - } catch (_) { - state.csrf = ""; - $("login-open").classList.remove("hidden"); - $("logout").classList.add("hidden"); - } -} - -$("login-open").addEventListener("click", () => $("login-dialog").showModal()); -$("login-form").addEventListener("submit", async (event) => { - event.preventDefault(); - $("login-error").textContent = ""; - const button = event.currentTarget.querySelector("button[type=submit]"); - button.disabled = true; - try { - const result = await api("api/v1/owner/login", { - method: "POST", - body: JSON.stringify({ username: $("username").value, password: $("password").value }) - }); - state.csrf = result.csrf; - $("password").value = ""; - $("login-dialog").close(); - toast("代码频道身份验证通过"); - await restoreSession(); - } catch (_) { - $("login-error").textContent = "账号验证失败。没有创建审批会话。"; - } finally { button.disabled = false; } -}); - -$("approve").addEventListener("click", async () => { - if (!state.selected || state.selected.state !== "PENDING") return; - const exact = `${state.selected.repository}\n${state.selected.branch}\n${state.selected.paths.join(",")}`; - if (!window.confirm(`批准这一段开发?\n\n${exact}\n\n批准不会开放 root 或任意 shell。`)) return; - $("approve").disabled = true; - try { - await api(`api/v1/owner/requests/${state.selected.id}/approve`, { - method: "POST", - headers: { "X-CSRF-Token": state.csrf }, - body: JSON.stringify({ ttl_seconds: Number($("ttl").value) }) - }); - toast("已批准。铸澜可领取一次临时能力。"); - await restoreSession(); - } catch (error) { toast(`批准失败:${error.message}`); } - finally { $("approve").disabled = false; } -}); - -$("reject").addEventListener("click", async () => { - if (!state.selected || state.selected.state !== "PENDING") return; - const reason = window.prompt("写一句拒绝原因,铸澜会据此修正申请:", "范围需要重新确认"); - if (reason === null) return; - try { - await api(`api/v1/owner/requests/${state.selected.id}/reject`, { - method: "POST", - headers: { "X-CSRF-Token": state.csrf }, - body: JSON.stringify({ reason }) - }); - toast("已拒绝并留下回执。"); - await restoreSession(); - } catch (error) { toast(`拒绝失败:${error.message}`); } -}); - -$("revoke").addEventListener("click", async () => { - if (!state.selected || state.selected.state !== "APPROVED") return; - if (!window.confirm("立即撤销这段开发能力?已连接会话会在下一次调用时被服务器驳回。")) return; - try { - await api(`api/v1/owner/requests/${state.selected.id}/revoke`, { - method: "POST", - headers: { "X-CSRF-Token": state.csrf }, - body: JSON.stringify({ reason: "主人在审批端主动撤销" }) - }); - toast("能力已撤销,原连接不再有效。"); - await restoreSession(); - } catch (error) { toast(`撤销失败:${error.message}`); } -}); - -$("logout").addEventListener("click", async () => { - try { - await api("api/v1/owner/logout", { method: "POST", headers: { "X-CSRF-Token": state.csrf }, body: "{}" }); - } catch (_) {} - location.reload(); -}); - -restoreSession(); diff --git a/server-tools/zhulan-remote-cell/ui/index.html b/server-tools/zhulan-remote-cell/ui/index.html deleted file mode 100644 index 34c8818..0000000 --- a/server-tools/zhulan-remote-cell/ui/index.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - 铸澜 · 远程开发入口 - - - - -
    - 光湖 - 返回国内入口 -
    - -
    -
    -

    ZHULAN · HUMAN APPROVAL

    -

    铸澜 · 远程开发入口

    -

    广州只提供域名、TLS 与反向代理;本页面和你看到的每一项申请、批准、回执,都来自新加坡真实运行单元。

    -
    - 广州 仅域名 · TLS · 反向代理 - 新加坡 真实 UI · 审批 · 能力 · 沙箱 · 门禁 · 回执 -
    -
    - -
    -
    -

    HUMAN IDENTITY

    - 主人尚未登录 - 使用光湖代码频道账号完成一次验证,密码不会保存。 -
    - - -
    - -
    -
    -
    -

    CURRENT REQUEST

    -

    当前开发申请

    -
    - 等待登录 -
    - -
    - -

    登录后读取真实申请

    -

    这里不会生成演示工单。没有来自铸澜的真实申请时,湖面保持安静。

    -
    - - -
    - -
    -
    -
    -

    LIVE RECEIPTS

    -

    同一条真实回执链

    -
    -
    -
      -
    1. 尚未读取登录后显示新加坡运行层的真实事件。
    2. -
    -
    - - -
    - - -
    - -

    HUMAN IDENTITY

    -

    登录人类审批端

    -

    使用光湖代码频道主人账号。密码只参与这一次验证,不写入服务器状态。

    - - - - -

    - -
    -
    - -
    -
    - 铸澜 ICE-GL-ZL-001 · 私有审批投影 - 前门 ≠ 运行体 · UI ≠ 权限 -
    - - - diff --git a/server-tools/zhulan-remote-cell/ui/styles.css b/server-tools/zhulan-remote-cell/ui/styles.css deleted file mode 100644 index 042cf86..0000000 --- a/server-tools/zhulan-remote-cell/ui/styles.css +++ /dev/null @@ -1,205 +0,0 @@ -:root { - color-scheme: dark; - --ink: #070916; - --ink-soft: #0c1025; - --panel: rgba(16, 20, 47, .76); - --panel-strong: rgba(20, 25, 58, .94); - --line: rgba(226, 231, 255, .13); - --line-bright: rgba(236, 239, 255, .28); - --text: #f2f1ed; - --muted: #9fa6bf; - --quiet: #69718f; - --star: #efe7d5; - --violet: #a6a5ff; - --lake: #86b9de; - --ok: #9cd9bd; - --warn: #e6c68d; - --bad: #e19791; - --shadow: 0 30px 80px rgba(0, 0, 0, .38); - font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Microsoft YaHei", sans-serif; -} - -* { box-sizing: border-box; } -html { min-height: 100%; background: var(--ink); } -body { - min-height: 100vh; - margin: 0; - color: var(--text); - background: - radial-gradient(circle at 14% 9%, rgba(99, 95, 185, .18), transparent 31%), - radial-gradient(circle at 84% 20%, rgba(73, 124, 176, .13), transparent 28%), - radial-gradient(ellipse at 50% 105%, rgba(68, 102, 166, .22), transparent 46%), - linear-gradient(155deg, #070915 0%, #0a0d21 44%, #070915 100%); - overflow-x: hidden; -} - -body::after { - content: ""; - position: fixed; - inset: auto 0 0; - height: 34vh; - pointer-events: none; - background: linear-gradient(to bottom, transparent, rgba(45, 66, 124, .08)); - mask-image: linear-gradient(to bottom, transparent, black); -} - -.stars, .stars::before, .stars::after { - position: fixed; - inset: 0; - content: ""; - pointer-events: none; - background-image: - radial-gradient(circle at 9% 17%, rgba(255,255,255,.75) 0 1px, transparent 1.4px), - radial-gradient(circle at 34% 8%, rgba(255,255,255,.48) 0 1px, transparent 1.4px), - radial-gradient(circle at 72% 13%, rgba(255,255,255,.62) 0 1px, transparent 1.4px), - radial-gradient(circle at 91% 35%, rgba(255,255,255,.40) 0 1px, transparent 1.4px), - radial-gradient(circle at 18% 68%, rgba(255,255,255,.35) 0 1px, transparent 1.4px), - radial-gradient(circle at 79% 79%, rgba(255,255,255,.46) 0 1px, transparent 1.4px); - opacity: .55; -} -.stars::before { transform: translate(5vw, 8vh) scale(.78); opacity: .34; } -.stars::after { transform: translate(-8vw, 15vh) scale(1.22); opacity: .22; } - -a { color: inherit; } -button, input, select { font: inherit; } -button { color: inherit; } -.hidden { display: none !important; } - -.topbar, main, footer { width: min(760px, calc(100% - 36px)); margin-inline: auto; position: relative; z-index: 1; } -.topbar { - min-height: 68px; - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid var(--line); -} -.brand { display: inline-flex; align-items: center; gap: 9px; text-decoration: none; font-weight: 740; letter-spacing: .08em; } -.brand span { color: var(--star); font-size: 27px; text-shadow: 0 0 18px rgba(239, 231, 213, .45); } -.back { color: var(--muted); font-size: 12px; text-decoration: none; letter-spacing: .07em; } - -main { padding: 54px 0 78px; } -.intro { text-align: center; } -.eyebrow, .label { margin: 0; color: var(--violet); font: 700 10px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .19em; } -.intro h1 { margin: 15px 0 0; font-size: clamp(31px, 7vw, 48px); line-height: 1.15; letter-spacing: -.04em; } -.intro > p:last-of-type { max-width: 610px; margin: 20px auto 0; color: var(--muted); font-size: 14px; line-height: 1.9; } -.topology { margin: 28px auto 0; display: inline-flex; flex-wrap: wrap; justify-content: center; gap: 10px 20px; color: var(--muted); font-size: 11px; } -.topology span { display: inline-flex; align-items: center; gap: 7px; } -.topology b { color: var(--text); } -.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--lake); box-shadow: 0 0 12px rgba(134,185,222,.75); } -.dot.runtime { background: var(--ok); box-shadow: 0 0 12px rgba(156,217,189,.75); } - -.auth-strip, .workspace, .timeline-section, .why { - margin-top: 28px; - border: 1px solid var(--line); - border-radius: 20px; - background: linear-gradient(160deg, rgba(22, 26, 58, .74), rgba(10, 13, 31, .83)); - box-shadow: var(--shadow); - backdrop-filter: blur(18px); -} -.auth-strip { min-height: 92px; padding: 18px 20px; display: flex; align-items: center; gap: 16px; } -.auth-strip > div { flex: 1; min-width: 0; } -.auth-strip strong { display: block; margin-top: 7px; font-size: 15px; } -.auth-strip small { display: block; margin-top: 5px; color: var(--muted); line-height: 1.55; } - -.button { - min-height: 42px; - padding: 0 18px; - border: 1px solid var(--line-bright); - border-radius: 999px; - background: rgba(255,255,255,.045); - font-weight: 700; - font-size: 13px; - cursor: pointer; - transition: transform .18s ease, background .18s ease, border-color .18s ease; -} -.button:hover { transform: translateY(-1px); border-color: rgba(255,255,255,.5); } -.button:active { transform: translateY(0) scale(.98); } -.button:disabled { opacity: .45; cursor: wait; } -.button.primary { color: #111426; background: var(--star); border-color: var(--star); box-shadow: 0 8px 26px rgba(239,231,213,.13); } -.button.ghost { color: var(--muted); } -.button.full { width: 100%; margin-top: 22px; } -.text-button { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 12px; } - -.workspace, .timeline-section { padding: 24px; } -.section-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; padding-bottom: 20px; border-bottom: 1px solid var(--line); } -.section-head.compact { padding-bottom: 16px; } -.section-head h2, .why h2 { margin: 8px 0 0; font-size: 21px; letter-spacing: -.02em; } -.state { display: inline-flex; align-items: center; min-height: 28px; padding: 0 11px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); font: 700 10px/1 ui-monospace, monospace; letter-spacing: .09em; } -.state.waiting { color: var(--warn); border-color: rgba(230,198,141,.28); } -.state.approved { color: var(--ok); border-color: rgba(156,217,189,.3); } -.state.rejected { color: var(--bad); border-color: rgba(225,151,145,.3); } - -.empty { padding: 58px 20px 48px; text-align: center; } -.empty-star { color: var(--star); font-size: 25px; text-shadow: 0 0 23px rgba(239,231,213,.5); } -.empty h3 { margin: 14px 0 0; font-size: 17px; } -.empty p { max-width: 420px; margin: 10px auto 0; color: var(--muted); font-size: 13px; line-height: 1.8; } - -.request { padding-top: 22px; } -.request-lead { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; } -.request-lead h3 { max-width: 520px; margin: 9px 0 0; font-size: 18px; line-height: 1.55; } -.facts { margin: 24px 0 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--line); } -.facts div { min-width: 0; padding: 16px 12px 16px 0; border-bottom: 1px solid var(--line); } -.facts div:nth-child(even) { padding-left: 18px; border-left: 1px solid var(--line); } -.facts dt { color: var(--quiet); font-size: 11px; } -.facts dd { margin: 7px 0 0; overflow-wrap: anywhere; font-size: 13px; font-weight: 650; line-height: 1.55; } -.mono { font: 600 11px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); } -.binding-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 22px 0; border-bottom: 1px solid var(--line); } -.chips, .paths { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } -.chip, .path { padding: 7px 9px; border-radius: 8px; color: var(--muted); background: rgba(255,255,255,.045); font: 600 10px/1.3 ui-monospace, monospace; } -.path { width: 100%; overflow-wrap: anywhere; } - -.decision { display: flex; justify-content: space-between; align-items: flex-end; gap: 20px; padding-top: 22px; } -select { min-width: 150px; margin-top: 9px; padding: 10px 34px 10px 11px; color: var(--text); border: 1px solid var(--line); border-radius: 10px; background: var(--ink-soft); } -.decision-buttons { display: flex; gap: 10px; } - -.timeline { margin: 0; padding: 19px 0 2px; list-style: none; } -.timeline li { position: relative; display: grid; grid-template-columns: 14px 1fr; gap: 12px; padding: 8px 0 14px; } -.timeline li:not(:last-child)::after { content: ""; position: absolute; left: 4px; top: 21px; bottom: -4px; width: 1px; background: var(--line); } -.timeline i { width: 9px; height: 9px; margin-top: 4px; border-radius: 50%; background: var(--quiet); box-shadow: 0 0 0 4px rgba(105,113,143,.08); } -.timeline li.pass i { background: var(--ok); box-shadow: 0 0 11px rgba(156,217,189,.6); } -.timeline li.fail i { background: var(--bad); box-shadow: 0 0 11px rgba(225,151,145,.55); } -.timeline strong { display: block; font-size: 13px; } -.timeline span { display: block; margin-top: 5px; color: var(--muted); font-size: 11px; line-height: 1.6; overflow-wrap: anywhere; } - -.why { padding: 28px; text-align: center; } -.why p:last-child { max-width: 600px; margin: 14px auto 0; color: var(--muted); font-size: 13px; line-height: 1.9; } - -dialog { width: min(430px, calc(100% - 28px)); padding: 0; color: var(--text); border: 1px solid var(--line-bright); border-radius: 20px; background: var(--panel-strong); box-shadow: 0 38px 100px rgba(0,0,0,.65); } -dialog::backdrop { background: rgba(3,5,15,.78); backdrop-filter: blur(8px); } -dialog form { position: relative; padding: 30px; } -dialog h2 { margin: 10px 0 0; font-size: 23px; } -dialog p:not(.label):not(.form-error) { margin: 12px 0 20px; color: var(--muted); font-size: 13px; line-height: 1.7; } -dialog label { display: block; margin: 15px 0 7px; color: var(--muted); font-size: 11px; } -dialog input { width: 100%; min-height: 45px; padding: 0 13px; border: 1px solid var(--line); border-radius: 10px; outline: 0; color: var(--text); background: rgba(255,255,255,.045); } -dialog input:focus { border-color: var(--line-bright); box-shadow: 0 0 0 3px rgba(166,165,255,.08); } -.dialog-close { position: absolute; top: 15px; right: 18px; border: 0; background: transparent; color: var(--muted); font-size: 24px; cursor: pointer; } -.form-error { min-height: 18px; margin: 10px 0 0; color: var(--bad); font-size: 12px; } -.toast { position: fixed; z-index: 10; left: 50%; bottom: 24px; transform: translate(-50%, 16px); max-width: calc(100% - 28px); padding: 11px 18px; opacity: 0; pointer-events: none; border: 1px solid var(--line-bright); border-radius: 999px; background: var(--panel-strong); box-shadow: var(--shadow); transition: .25s ease; font-size: 12px; text-align: center; } -.toast.show { opacity: 1; transform: translate(-50%, 0); } - -footer { min-height: 90px; display: flex; justify-content: space-between; align-items: center; gap: 18px; border-top: 1px solid var(--line); color: var(--quiet); font-size: 10px; } - -@media (max-width: 640px) { - .topbar, main, footer { width: min(100% - 24px, 760px); } - main { padding-top: 38px; } - .intro h1 { font-size: 31px; } - .intro > p:last-of-type { font-size: 13px; } - .topology { align-items: flex-start; flex-direction: column; text-align: left; } - .auth-strip { align-items: flex-start; flex-wrap: wrap; } - .auth-strip .button { width: 100%; } - .workspace, .timeline-section { padding: 19px; } - .section-head { align-items: center; } - .request-lead { flex-direction: column; } - .facts { grid-template-columns: 1fr; } - .facts div:nth-child(even) { padding-left: 0; border-left: 0; } - .binding-grid { grid-template-columns: 1fr; gap: 20px; } - .decision { align-items: stretch; flex-direction: column; } - select { width: 100%; } - .decision-buttons { display: grid; grid-template-columns: .8fr 1.4fr; } - .decision-buttons .button { padding-inline: 12px; } - footer { padding: 24px 0; align-items: flex-start; flex-direction: column; justify-content: center; } -} - -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } -}