diff --git a/deployment/receipts/DEV-20260813-005-ZHULAN-CODEX-REMOTE-SSH-DEPLOYMENT-20260813.json b/deployment/receipts/DEV-20260813-005-ZHULAN-CODEX-REMOTE-SSH-DEPLOYMENT-20260813.json deleted file mode 100644 index 5b828bd..0000000 --- a/deployment/receipts/DEV-20260813-005-ZHULAN-CODEX-REMOTE-SSH-DEPLOYMENT-20260813.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "schema": "guanghu.zhulan-codex-remote-ssh-deployment-receipt/v1", - "development_id": "DEV-20260813-005", - "persona_id": "ICE-GL-ZL-001", - "deployed_at": "2026-08-13T10:53:00Z", - "source_repository": "https://guanghulab.com/code/bingshuo/guanghu-ice-heart", - "source_commit": "65784b4388e0dfae45cb0f037ded55b9a7201122", - "topology": { - "runtime_node": "BS-SG-003", - "front_node": "BS-GZ-006", - "front_node_role": "DOMAIN_TLS_REVERSE_PROXY_ONLY", - "approval_front": "https://guanghulab.com/zhulan/" - }, - "remote_ssh": { - "desktop_alias": "zhulan-codex-sg", - "remote_user": "zhulan-codex", - "workspace": "/srv/guanghu/zhulan-codex/workspaces/guanghu-ice-heart", - "codex_version": "0.147.0", - "key_algorithm": "ED25519", - "public_key_fingerprint": "SHA256:K3cshcHObzIbwmI6ksc65DFhzo9CA94KLtjgpckO9X0", - "private_key_location": "LOCAL_MACOS_ONLY_NOT_IN_REPOSITORY", - "passphrase_storage": "MACOS_KEYCHAIN_VIA_SSH_ADD", - "password_authentication": false, - "sudo": false, - "agent_forwarding": false, - "tcp_forwarding": false, - "tunnel": false - }, - "repository_boundary": { - "origin_fetch": "PUBLIC_READ_ONLY_HTTPS", - "origin_push": "DISABLED_CENTRAL_PUSH", - "candidate_remote": "SERVER_LOCAL_BARE_REPOSITORY", - "central_write_credentials": false, - "central_main_publish": false, - "candidate_push_smoke": "PASS_AND_TEST_REF_REMOVED", - "central_push_dry_run": "DENIED" - }, - "managed_guard": { - "system_requirements": "/etc/codex/requirements.toml", - "requirements_sha256": "de4f5be194f9a9a3f17e3688af6b957c9c4ee403246fd49fa445d2e5491050c2", - "hook": "/etc/guanghu/zhulan-codex/hooks/zhulan_guard.py", - "hook_sha256": "248edf934be08b8eb88292bbf09b41f31ab85b522de1e28d67e3c0dca34839ea", - "lane": "/etc/guanghu/zhulan-codex/LANE.hdlp", - "lane_sha256": "56e5fb004b7e103903e3450d195575baeb14c01c2af1f745dc268fd1e7d65e6a", - "managed_hooks_only": true, - "events": [ - "SessionStart", - "UserPromptSubmit", - "PreToolUse", - "PreCompact", - "PostCompact", - "Stop", - "SessionEnd" - ], - "negative_probes": { - "sudo": "DENIED", - "production_secret_path": "DENIED", - "central_repository_push": "DENIED", - "outside_workspace_tool_use": "DENIED" - }, - "continuity_probes": { - "precompact_checkpoint": "PASS", - "postcompact_lane_projection": "PASS" - } - }, - "server_owned_receipt": { - "path": "/srv/guanghu/zhulan-codex/install-receipt.json", - "sha256": "8a83e980a09feb2d77fd8e4ddac7449b65c39c72af4ed3c7885cb764cb0dd77e", - "health": "PASS" - }, - "existing_runtime_regression": { - "zhulan_remote_cell": "ACTIVE", - "zhulan_validation_executor": "ACTIVE", - "zhulan_front_tunnel": "ACTIVE", - "public_health": "PASS_PROXY_ONLY" - }, - "source_tests": { - "count": 27, - "result": "PASS", - "git_diff_check": "PASS", - "installer_shell_syntax": "PASS", - "guard_python_compile": "PASS" - }, - "human_only_remaining": { - "remote_codex_login": "NOT_LOGGED_IN", - "desktop_ssh_project_registration": "NOT_PERFORMED_BY_AGENT", - "mobile_qr_pairing": "NOT_PERFORMED_BY_AGENT", - "reason": "These actions bind BingShuo's ChatGPT account, workspace and personal phone and must be completed by BingShuo in the desktop and mobile apps." - }, - "state": "SERVER_AND_DESKTOP_SSH_PREREQUISITES_PASS_HUMAN_ACCOUNT_PAIRING_PENDING" -} diff --git a/server-tools/zhulan-remote-cell/README.md b/server-tools/zhulan-remote-cell/README.md index f845c91..c5c53a4 100644 --- a/server-tools/zhulan-remote-cell/README.md +++ b/server-tools/zhulan-remote-cell/README.md @@ -18,14 +18,7 @@ 6. 代码门脚本检查当前分支、基线、改动路径、符号链接、超大文件与常见秘密模式; 7. 候选提交只进入 `BS-SG-003` 的内部复核库,不在远程会话中持有代码频道写凭证,也不直接发布中央仓库;本机铸渊复核后再走既有发布门。 -审批/MCP 单元本身不提供任意远程 shell。手机开发另走官方 Codex Remote 链路:手机连接一台 -受信任桌面主机,桌面 App 再连接 `BS-SG-003` 上的 `zhulan-codex` SSH 身份。该身份没有 sudo、 -生产目录写权、中央代码频道写凭据或 `main` 发布权;`/etc/codex/requirements.toml` 强制只运行 -管理员钩子,在会话启动、每次提示、工具调用、压缩前后和结束时恢复车道、写检查点并阻断越权。 -候选只推送到服务器本机 `candidate` remote,仍由本机铸渊复核后走既有发布门。 - -手机到桌面的 Remote 配对以及桌面 App 中的 SSH 项目登记必须由冰朔在同一 ChatGPT 账号和工作区 -完成。源码部署不能冒充二维码已经扫描、远端 Codex 已登录或手机首轮任务已验收。 +这里不提供任意远程 shell。后续手机 Codex 接入只能通过受限执行器或受限工具协议使用这些能力,不能把临时能力降级成长期 root 密钥。 ## 本地验收 diff --git a/server-tools/zhulan-remote-cell/deploy/install-codex-remote-ssh.sh b/server-tools/zhulan-remote-cell/deploy/install-codex-remote-ssh.sh deleted file mode 100644 index ca53de6..0000000 --- a/server-tools/zhulan-remote-cell/deploy/install-codex-remote-ssh.sh +++ /dev/null @@ -1,153 +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:-}" -AUTHORIZED_KEY_FILE="${2:-}" -if [[ -z "$SOURCE_DIR" || ! -f "$SOURCE_DIR/remote/zhulan_guard.py" ]]; then - echo "SOURCE_DIR_INVALID" >&2 - exit 1 -fi -if [[ -z "$AUTHORIZED_KEY_FILE" || ! -s "$AUTHORIZED_KEY_FILE" ]]; then - echo "AUTHORIZED_KEY_REQUIRED" >&2 - exit 1 -fi -if grep -qvE '^ssh-ed25519 [A-Za-z0-9+/=]+( .*)?$' "$AUTHORIZED_KEY_FILE"; then - echo "AUTHORIZED_KEY_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 - -REMOTE_USER="zhulan-codex" -REMOTE_HOME="/srv/guanghu/zhulan-codex/home" -REMOTE_ROOT="/srv/guanghu/zhulan-codex" -WORKSPACE_ROOT="$REMOTE_ROOT/workspaces" -CANDIDATE_ROOT="$REMOTE_ROOT/candidates" -POLICY_ROOT="/etc/guanghu/zhulan-codex" -HOOK_ROOT="$POLICY_ROOT/hooks" -REQUIREMENTS_ROOT="/etc/codex" -SSHD_DROPIN="/etc/ssh/sshd_config.d/60-zhulan-codex.conf" -REPOSITORY_URL="https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git" -REPOSITORY_DIR="$WORKSPACE_ROOT/guanghu-ice-heart" -CODEX_VERSION="${ZHULAN_CODEX_VERSION:-0.147.0}" -SOURCE_COMMIT="${ZHULAN_SOURCE_COMMIT:-}" - -if [[ ! "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then - echo "SOURCE_COMMIT_REQUIRED" >&2 - exit 1 -fi - -getent passwd "$REMOTE_USER" >/dev/null || useradd \ - --home-dir "$REMOTE_HOME" \ - --create-home \ - --shell /bin/bash \ - --comment "Zhulan Codex Remote restricted developer" \ - "$REMOTE_USER" -passwd -l "$REMOTE_USER" >/dev/null -gpasswd -d "$REMOTE_USER" sudo >/dev/null 2>&1 || true -gpasswd -d "$REMOTE_USER" adm >/dev/null 2>&1 || true - -install -d -o root -g root -m 0755 "$REMOTE_ROOT" -install -d -o "$REMOTE_USER" -g "$REMOTE_USER" -m 0700 "$REMOTE_HOME" "$WORKSPACE_ROOT" "$CANDIDATE_ROOT" -install -d -o "$REMOTE_USER" -g "$REMOTE_USER" -m 0700 "$REMOTE_HOME/.ssh" -install -d -o "$REMOTE_USER" -g "$REMOTE_USER" -m 0700 "$REMOTE_HOME/.codex" -{ - printf 'restrict,pty ' - cat "$AUTHORIZED_KEY_FILE" -} >"$REMOTE_HOME/.ssh/authorized_keys.tmp" -chown "$REMOTE_USER:$REMOTE_USER" "$REMOTE_HOME/.ssh/authorized_keys.tmp" -chmod 0600 "$REMOTE_HOME/.ssh/authorized_keys.tmp" -mv "$REMOTE_HOME/.ssh/authorized_keys.tmp" "$REMOTE_HOME/.ssh/authorized_keys" - -install -d -o root -g root -m 0755 "$POLICY_ROOT" "$HOOK_ROOT" "$REQUIREMENTS_ROOT" -install -o root -g root -m 0444 "$SOURCE_DIR/remote/LANE.hdlp" "$POLICY_ROOT/LANE.hdlp" -install -o root -g root -m 0555 "$SOURCE_DIR/remote/zhulan_guard.py" "$HOOK_ROOT/zhulan_guard.py" -install -o root -g root -m 0444 "$SOURCE_DIR/remote/requirements.toml" "$REQUIREMENTS_ROOT/requirements.toml" - -cat >"$SSHD_DROPIN.tmp" <<'EOF' -Match User zhulan-codex - AuthenticationMethods publickey - PasswordAuthentication no - KbdInteractiveAuthentication no - PubkeyAuthentication yes - AllowAgentForwarding no - AllowTcpForwarding no - X11Forwarding no - PermitTunnel no - PermitTTY yes -EOF -chmod 0644 "$SSHD_DROPIN.tmp" -mv "$SSHD_DROPIN.tmp" "$SSHD_DROPIN" -/usr/sbin/sshd -t -systemctl reload ssh - -if ! command -v codex >/dev/null 2>&1 || [[ "$(codex --version 2>/dev/null)" != *"$CODEX_VERSION"* ]]; then - npm install --global "@openai/codex@$CODEX_VERSION" -fi - -if [[ ! -d "$REPOSITORY_DIR/.git" ]]; then - runuser -u "$REMOTE_USER" -- git clone "$REPOSITORY_URL" "$REPOSITORY_DIR" -fi -runuser -u "$REMOTE_USER" -- git -C "$REPOSITORY_DIR" fetch --prune origin main -runuser -u "$REMOTE_USER" -- git -C "$REPOSITORY_DIR" remote set-url --push origin DISABLED_CENTRAL_PUSH -if [[ ! -d "$CANDIDATE_ROOT/guanghu-ice-heart.git" ]]; then - runuser -u "$REMOTE_USER" -- git init --bare "$CANDIDATE_ROOT/guanghu-ice-heart.git" -fi -if runuser -u "$REMOTE_USER" -- git -C "$REPOSITORY_DIR" remote get-url candidate >/dev/null 2>&1; then - runuser -u "$REMOTE_USER" -- git -C "$REPOSITORY_DIR" remote set-url candidate "$CANDIDATE_ROOT/guanghu-ice-heart.git" -else - runuser -u "$REMOTE_USER" -- git -C "$REPOSITORY_DIR" remote add candidate "$CANDIDATE_ROOT/guanghu-ice-heart.git" -fi -install -o "$REMOTE_USER" -g "$REMOTE_USER" -m 0644 "$SOURCE_DIR/remote/AGENTS.md" "$REMOTE_HOME/.codex/AGENTS.md" -install -d -o "$REMOTE_USER" -g "$REMOTE_USER" -m 0700 "$REPOSITORY_DIR/.zhulan/continuity" -if ! grep -qxF '/.zhulan/' "$REPOSITORY_DIR/.git/info/exclude"; then - printf '%s\n' '/.zhulan/' >>"$REPOSITORY_DIR/.git/info/exclude" -fi -chown "$REMOTE_USER:$REMOTE_USER" "$REPOSITORY_DIR/.git/info/exclude" -runuser -u "$REMOTE_USER" -- git -C "$REPOSITORY_DIR" config user.name "Zhulan Remote Candidate" -runuser -u "$REMOTE_USER" -- git -C "$REPOSITORY_DIR" config user.email "zhulan-remote@localhost" - -if id -nG "$REMOTE_USER" | tr ' ' '\n' | grep -qx sudo; then - echo "REMOTE_USER_HAS_SUDO" >&2 - exit 1 -fi -if sudo -n -u "$REMOTE_USER" sudo -n true >/dev/null 2>&1; then - echo "REMOTE_USER_CAN_SUDO" >&2 - exit 1 -fi - -RECEIPT="$REMOTE_ROOT/install-receipt.json" -cat >"$RECEIPT.tmp" < None: - print(json.dumps(value, ensure_ascii=False, sort_keys=True)) - - -def deny(reason: str) -> None: - emit( - { - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny", - "permissionDecisionReason": reason, - }, - "systemMessage": reason, - } - ) - - -def under_workspace(path: Path) -> bool: - try: - path.resolve().relative_to(WORKSPACE_ROOT) - return True - except (OSError, ValueError): - return False - - -def git(workspace: Path, *args: str) -> str: - try: - result = subprocess.run( - ["/usr/bin/git", "-C", str(workspace), *args], - capture_output=True, - text=True, - timeout=8, - check=False, - ) - except (OSError, subprocess.TimeoutExpired): - return "unavailable" - return result.stdout.strip()[:12000] if result.returncode == 0 else "unavailable" - - -def checkpoint(payload: dict[str, Any], reason: str) -> Path | None: - cwd = Path(str(payload.get("cwd") or "")) - if not under_workspace(cwd): - return None - repo = Path(git(cwd, "rev-parse", "--show-toplevel")) - if not under_workspace(repo): - return None - store = repo / ".zhulan" / "continuity" - store.mkdir(parents=True, exist_ok=True) - now = dt.datetime.now(dt.timezone.utc) - record = { - "schema": "guanghu.zhulan-server-checkpoint/v1", - "persona_id": "ICE-GL-ZL-001", - "reason": reason, - "created_at": now.isoformat(), - "session_id": payload.get("session_id"), - "turn_id": payload.get("turn_id"), - "repository": repo.name, - "branch": git(repo, "branch", "--show-current"), - "head": git(repo, "rev-parse", "HEAD"), - "status": git(repo, "status", "--short"), - "next_action": "restore LANE.hdlp and CURRENT.json, then continue the same candidate branch", - } - canonical = json.dumps(record, ensure_ascii=False, sort_keys=True).encode() - record["sha256"] = hashlib.sha256(canonical).hexdigest() - target = store / f"checkpoint-{now.strftime('%Y%m%dT%H%M%SZ')}-{record['sha256'][:10]}.json" - target.write_text(json.dumps(record, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") - current = store / "CURRENT.json" - temporary = store / ".CURRENT.json.tmp" - temporary.write_text(target.read_text(encoding="utf-8"), encoding="utf-8") - os.replace(temporary, current) - return target - - -def context(payload: dict[str, Any], event: str) -> str: - lane = LANE_PATH.read_text(encoding="utf-8")[:12000] if LANE_PATH.is_file() else "LANE_MISSING_FAIL_CLOSED" - cwd = Path(str(payload.get("cwd") or "")) - current = "NO_SERVER_CHECKPOINT_YET" - if under_workspace(cwd): - repo = Path(git(cwd, "rev-parse", "--show-toplevel")) - candidate = repo / ".zhulan" / "continuity" / "CURRENT.json" - if under_workspace(repo) and candidate.is_file(): - current = candidate.read_text(encoding="utf-8")[:8000] - return ( - f"ZHULAN_REMOTE_GUARD event={event}. Restore this server-owned lane before acting.\n" - f"{lane}\nLATEST_SERVER_CHECKPOINT:\n{current}" - ) - - -def main() -> int: - try: - payload = json.load(sys.stdin) - except (ValueError, TypeError): - deny("ZHULAN_GUARD_INPUT_INVALID") - return 0 - event = str(payload.get("hook_event_name") or "") - cwd = Path(str(payload.get("cwd") or "")) - - if event == "PreToolUse": - if not under_workspace(cwd): - deny("ZHULAN_WORKSPACE_BOUNDARY: tools may run only under the registered workspace root") - return 0 - tool_input = payload.get("tool_input") - raw = json.dumps(tool_input, ensure_ascii=False) if not isinstance(tool_input, str) else tool_input - lowered = raw.lower() - if any(marker in lowered for marker in PROTECTED_MARKERS): - deny("ZHULAN_PROTECTED_PATH_BOUNDARY: production, secrets, policy and root paths are read/write denied") - return 0 - command = str(tool_input.get("command") if isinstance(tool_input, dict) else raw) - tool_name = str(payload.get("tool_name") or "") - if tool_name == "apply_patch": - for target in PATCH_TARGET.findall(command): - candidate = Path(target.strip()) - resolved = candidate if candidate.is_absolute() else cwd / candidate - if not under_workspace(resolved): - deny("ZHULAN_PATCH_BOUNDARY: patch targets must remain under the registered workspace root") - return 0 - if FORBIDDEN_COMMANDS.search(command): - deny("ZHULAN_PRIVILEGE_BOUNDARY: privilege, service-manager and SSH-pivot commands are denied") - return 0 - if GIT_PUSH.search(command) and not CANDIDATE_PUSH.search(command): - deny("ZHULAN_PUBLICATION_BOUNDARY: only the server-local candidate remote may receive a push") - return 0 - emit( - { - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "additionalContext": "Zhulan OS boundary checked. Candidate work only; approval and central publication remain separate.", - } - } - ) - return 0 - - if event in {"PreCompact", "Stop", "SessionEnd"}: - saved = checkpoint(payload, event) - message = f"Zhulan server checkpoint saved: {saved.name}" if saved else "Zhulan checkpoint skipped outside registered workspace" - emit({"continue": True, "systemMessage": message}) - return 0 - - if event in {"SessionStart", "PostCompact", "UserPromptSubmit"}: - if event == "PostCompact": - checkpoint(payload, event) - emit( - { - "hookSpecificOutput": { - "hookEventName": event, - "additionalContext": context(payload, event), - } - } - ) - return 0 - - emit({"continue": True}) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/server-tools/zhulan-remote-cell/tests/test_codex_remote.py b/server-tools/zhulan-remote-cell/tests/test_codex_remote.py deleted file mode 100644 index ecb8c7c..0000000 --- a/server-tools/zhulan-remote-cell/tests/test_codex_remote.py +++ /dev/null @@ -1,79 +0,0 @@ -import json -import subprocess -import tempfile -import unittest -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[1] -GUARD = ROOT / "remote" / "zhulan_guard.py" - - -def run_guard(event: str, command: str = "git status", cwd: str = "/srv/guanghu/zhulan-codex/workspaces/repo"): - payload = { - "hook_event_name": event, - "session_id": "test-session", - "cwd": cwd, - "tool_name": "Bash", - "tool_input": {"command": command}, - } - return subprocess.run( - ["python3", str(GUARD)], - input=json.dumps(payload), - text=True, - capture_output=True, - check=False, - ) - - -class CodexRemoteContractTest(unittest.TestCase): - def test_managed_requirements_are_fail_closed(self): - requirements = (ROOT / "remote" / "requirements.toml").read_text(encoding="utf-8") - self.assertIn("allow_managed_hooks_only = true", requirements) - self.assertIn('default_permissions = ":workspace"', requirements) - self.assertIn('":workspace" = true', requirements) - self.assertIn('managed_dir = "/etc/guanghu/zhulan-codex/hooks"', requirements) - self.assertIn("[[hooks.PreCompact]]", requirements) - self.assertIn("[[hooks.PostCompact]]", requirements) - self.assertIn("[[hooks.PreToolUse]]", requirements) - - def test_privilege_and_central_push_are_denied(self): - for command in ( - "sudo id", - "systemctl restart ssh", - "git push origin main", - "git -C /tmp/repo push origin main", - "ssh another-host", - ): - result = run_guard("PreToolUse", command) - self.assertEqual(result.returncode, 0) - data = json.loads(result.stdout) - self.assertEqual(data["hookSpecificOutput"]["permissionDecision"], "deny") - - def test_candidate_push_and_normal_development_are_allowed(self): - for command in ("git status", "git diff --check", "git push candidate zhulan/test"): - result = run_guard("PreToolUse", command) - self.assertEqual(result.returncode, 0) - data = json.loads(result.stdout) - self.assertNotIn("permissionDecision", data["hookSpecificOutput"]) - - def test_protected_paths_and_outside_workspace_are_denied(self): - result = run_guard("PreToolUse", "sed -n 1p /etc/guanghu/private") - self.assertEqual(json.loads(result.stdout)["hookSpecificOutput"]["permissionDecision"], "deny") - result = run_guard("PreToolUse", "git status", cwd="/tmp") - self.assertEqual(json.loads(result.stdout)["hookSpecificOutput"]["permissionDecision"], "deny") - - def test_installer_removes_privilege_and_central_push(self): - installer = (ROOT / "deploy" / "install-codex-remote-ssh.sh").read_text(encoding="utf-8") - self.assertIn('gpasswd -d "$REMOTE_USER" sudo', installer) - self.assertIn("remote set-url --push origin DISABLED_CENTRAL_PUSH", installer) - self.assertIn('"$REMOTE_HOME/.codex/AGENTS.md"', installer) - self.assertIn("'/.zhulan/'", installer) - self.assertIn("AllowTcpForwarding no", installer) - self.assertNotIn("PermitUserEnvironment no", installer) - self.assertIn("central_push_credentials\": false", installer) - self.assertNotIn("NOPASSWD", installer) - - -if __name__ == "__main__": - unittest.main()