feat(zhulan): add guarded Codex Remote SSH lane
This commit is contained in:
parent
ce92414f07
commit
3a950fe029
7 changed files with 551 additions and 1 deletions
|
|
@ -18,7 +18,14 @@
|
|||
6. 代码门脚本检查当前分支、基线、改动路径、符号链接、超大文件与常见秘密模式;
|
||||
7. 候选提交只进入 `BS-SG-003` 的内部复核库,不在远程会话中持有代码频道写凭证,也不直接发布中央仓库;本机铸渊复核后再走既有发布门。
|
||||
|
||||
这里不提供任意远程 shell。后续手机 Codex 接入只能通过受限执行器或受限工具协议使用这些能力,不能把临时能力降级成长期 root 密钥。
|
||||
审批/MCP 单元本身不提供任意远程 shell。手机开发另走官方 Codex Remote 链路:手机连接一台
|
||||
受信任桌面主机,桌面 App 再连接 `BS-SG-003` 上的 `zhulan-codex` SSH 身份。该身份没有 sudo、
|
||||
生产目录写权、中央代码频道写凭据或 `main` 发布权;`/etc/codex/requirements.toml` 强制只运行
|
||||
管理员钩子,在会话启动、每次提示、工具调用、压缩前后和结束时恢复车道、写检查点并阻断越权。
|
||||
候选只推送到服务器本机 `candidate` remote,仍由本机铸渊复核后走既有发布门。
|
||||
|
||||
手机到桌面的 Remote 配对以及桌面 App 中的 SSH 项目登记必须由冰朔在同一 ChatGPT 账号和工作区
|
||||
完成。源码部署不能冒充二维码已经扫描、远端 Codex 已登录或手机首轮任务已验收。
|
||||
|
||||
## 本地验收
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
#!/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:]' </sys/class/dmi/id/product_uuid)"
|
||||
if [[ "$ACTUAL_HOST" != "$EXPECTED_HOST" || "$ACTUAL_DMI" != "$EXPECTED_DMI" ]]; then
|
||||
echo "TARGET_IDENTITY_MISMATCH" >&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
|
||||
PermitUserEnvironment no
|
||||
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" <<EOF
|
||||
{
|
||||
"schema": "guanghu.zhulan-codex-remote-ssh-install/v1",
|
||||
"development_id": "DEV-20260813-005",
|
||||
"persona_id": "ICE-GL-ZL-001",
|
||||
"runtime_node": "BS-SG-003",
|
||||
"front_node_role": "BS-GZ-006_PROXY_ONLY",
|
||||
"source_commit": "$SOURCE_COMMIT",
|
||||
"codex_version": "$CODEX_VERSION",
|
||||
"remote_user": "$REMOTE_USER",
|
||||
"workspace": "$REPOSITORY_DIR",
|
||||
"sudo": false,
|
||||
"central_push_credentials": false,
|
||||
"managed_hooks": true,
|
||||
"candidate_remote_only": true,
|
||||
"health": "PASS"
|
||||
}
|
||||
EOF
|
||||
chown root:root "$RECEIPT.tmp"
|
||||
chmod 0444 "$RECEIPT.tmp"
|
||||
mv "$RECEIPT.tmp" "$RECEIPT"
|
||||
cat "$RECEIPT"
|
||||
12
server-tools/zhulan-remote-cell/remote/AGENTS.md
Normal file
12
server-tools/zhulan-remote-cell/remote/AGENTS.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# 铸澜手机远程开发车道
|
||||
|
||||
你是铸澜(`ICE-GL-ZL-001`)在受限服务器开发环境中的当前 Codex 载体。先读
|
||||
`/etc/guanghu/zhulan-codex/LANE.hdlp` 和 `.zhulan/continuity/CURRENT.json`,再继续任何工作。
|
||||
|
||||
你只能在当前仓库的 `zhulan/*` 分支开发。服务器身份没有 sudo、生产目录写权、中央代码频道
|
||||
写凭据或 `main` 发布权。完成后运行测试与 `git diff --check`,提交候选并只推送到本机
|
||||
`candidate` remote;本机铸渊负责复核和中央发布。
|
||||
|
||||
`https://guanghulab.com/zhulan/` 的申请、审批和回执是人类治理边界。聊天中的“已连接”、UI
|
||||
显示、一次测试通过或本地提交都不等于冰朔批准、服务器部署或中央发布。上下文压缩不是失忆
|
||||
理由:管理员钩子会在压缩前写检查点,并在压缩后重新注入本车道和最新检查点。
|
||||
25
server-tools/zhulan-remote-cell/remote/LANE.hdlp
Normal file
25
server-tools/zhulan-remote-cell/remote/LANE.hdlp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
zhulan_remote_lane:
|
||||
schema: "guanghu.zhulan-codex-remote-lane/v1"
|
||||
persona_id: "ICE-GL-ZL-001"
|
||||
persona_name: "铸澜"
|
||||
development_mode: "MOBILE_CODEX_REMOTE_OVER_DESKTOP_TO_SSH"
|
||||
runtime_node: "BS-SG-003"
|
||||
public_front_role: "BS-GZ-006_PROXY_ONLY"
|
||||
human_approval_front: "https://guanghulab.com/zhulan/"
|
||||
workspace_root: "/srv/guanghu/zhulan-codex/workspaces"
|
||||
candidate_root: "/srv/guanghu/zhulan-codex/candidates"
|
||||
central_repository_write_credentials: false
|
||||
sudo: false
|
||||
production_write: false
|
||||
central_main_publish: false
|
||||
required_cycle:
|
||||
- "restore this lane and the newest server checkpoint"
|
||||
- "work only in the selected repository under workspace_root"
|
||||
- "run repository tests and git diff --check"
|
||||
- "commit only to a zhulan/* branch"
|
||||
- "push only to the server-local candidate remote"
|
||||
- "leave a receipt for desktop Zhuyuan review and central publication"
|
||||
fail_closed:
|
||||
- "never read or modify /etc/guanghu, /var/lib/guanghu, /opt/guanghu, /root or the production runtime tree"
|
||||
- "never use sudo, systemctl, service managers, SSH pivots or central repository push"
|
||||
- "never treat a prompt, UI state or passing local test as human approval or central publication"
|
||||
80
server-tools/zhulan-remote-cell/remote/requirements.toml
Normal file
80
server-tools/zhulan-remote-cell/remote/requirements.toml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
allow_managed_hooks_only = true
|
||||
allow_login_shell = false
|
||||
allowed_approval_policies = ["on-request", "untrusted"]
|
||||
allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||
allowed_web_search_modes = ["disabled", "cached"]
|
||||
default_permissions = ":workspace"
|
||||
|
||||
[allowed_permission_profiles]
|
||||
":read-only" = true
|
||||
":workspace" = true
|
||||
|
||||
[features]
|
||||
hooks = true
|
||||
computer_use = false
|
||||
browser_use = false
|
||||
browser_use_external = false
|
||||
browser_use_full_cdp_access = false
|
||||
multi_agent = false
|
||||
|
||||
[hooks]
|
||||
managed_dir = "/etc/guanghu/zhulan-codex/hooks"
|
||||
|
||||
[[hooks.SessionStart]]
|
||||
matcher = "startup|resume|clear|compact"
|
||||
|
||||
[[hooks.SessionStart.hooks]]
|
||||
type = "command"
|
||||
command = "/usr/bin/python3 /etc/guanghu/zhulan-codex/hooks/zhulan_guard.py"
|
||||
timeout = 10
|
||||
additionalContextLimit = 5000
|
||||
statusMessage = "恢复铸澜远程开发车道"
|
||||
|
||||
[[hooks.UserPromptSubmit]]
|
||||
|
||||
[[hooks.UserPromptSubmit.hooks]]
|
||||
type = "command"
|
||||
command = "/usr/bin/python3 /etc/guanghu/zhulan-codex/hooks/zhulan_guard.py"
|
||||
timeout = 10
|
||||
additionalContextLimit = 3500
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "Bash|apply_patch|Edit|Write"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "/usr/bin/python3 /etc/guanghu/zhulan-codex/hooks/zhulan_guard.py"
|
||||
timeout = 15
|
||||
statusMessage = "执行铸澜服务器门禁"
|
||||
|
||||
[[hooks.PreCompact]]
|
||||
matcher = "manual|auto"
|
||||
|
||||
[[hooks.PreCompact.hooks]]
|
||||
type = "command"
|
||||
command = "/usr/bin/python3 /etc/guanghu/zhulan-codex/hooks/zhulan_guard.py"
|
||||
timeout = 15
|
||||
|
||||
[[hooks.PostCompact]]
|
||||
matcher = "manual|auto"
|
||||
|
||||
[[hooks.PostCompact.hooks]]
|
||||
type = "command"
|
||||
command = "/usr/bin/python3 /etc/guanghu/zhulan-codex/hooks/zhulan_guard.py"
|
||||
timeout = 10
|
||||
additionalContextLimit = 5000
|
||||
|
||||
[[hooks.Stop]]
|
||||
|
||||
[[hooks.Stop.hooks]]
|
||||
type = "command"
|
||||
command = "/usr/bin/python3 /etc/guanghu/zhulan-codex/hooks/zhulan_guard.py"
|
||||
timeout = 20
|
||||
|
||||
[[hooks.SessionEnd]]
|
||||
matcher = "other"
|
||||
|
||||
[[hooks.SessionEnd.hooks]]
|
||||
type = "command"
|
||||
command = "/usr/bin/python3 /etc/guanghu/zhulan-codex/hooks/zhulan_guard.py"
|
||||
timeout = 15
|
||||
194
server-tools/zhulan-remote-cell/remote/zhulan_guard.py
Normal file
194
server-tools/zhulan-remote-cell/remote/zhulan_guard.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Admin-managed Codex hooks for the bounded Zhulan Remote SSH lane."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
WORKSPACE_ROOT = Path("/srv/guanghu/zhulan-codex/workspaces")
|
||||
LANE_PATH = Path("/etc/guanghu/zhulan-codex/LANE.hdlp")
|
||||
PROTECTED_MARKERS = (
|
||||
"/etc/guanghu",
|
||||
"/var/lib/guanghu",
|
||||
"/opt/guanghu",
|
||||
"/srv/guanghu/zhulan-cell",
|
||||
"/etc/codex",
|
||||
"/root",
|
||||
)
|
||||
FORBIDDEN_COMMANDS = re.compile(
|
||||
r"(^|[;&|()\s])(?:sudo|su|doas|systemctl|service|loginctl|mount|umount|"
|
||||
r"useradd|usermod|userdel|groupadd|visudo|sshd|iptables|nft|reboot|shutdown|"
|
||||
r"ssh|scp|sftp)(?=$|\s)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
GIT_PUSH = re.compile(r"\bgit\b[^\n;&|]*\bpush\b", re.IGNORECASE)
|
||||
CANDIDATE_PUSH = re.compile(r"\bgit\b[^\n;&|]*\bpush\b[^\n;&|]*\bcandidate\b", re.IGNORECASE)
|
||||
PATCH_TARGET = re.compile(r"^\*\*\* (?:Add|Update|Delete) File: (.+)$", re.MULTILINE)
|
||||
|
||||
|
||||
def emit(value: dict[str, Any]) -> 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())
|
||||
78
server-tools/zhulan-remote-cell/tests/test_codex_remote.py
Normal file
78
server-tools/zhulan-remote-cell/tests/test_codex_remote.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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.assertIn("central_push_credentials\": false", installer)
|
||||
self.assertNotIn("NOPASSWD", installer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue