feat(guanghu-os): add repository bridge lifecycle

This commit is contained in:
冰朔 2026-08-16 00:08:54 +08:00
commit 0d1ded1196
9 changed files with 326 additions and 4 deletions

View file

@ -14,3 +14,6 @@
当前工程门按顺序关闭:挂根前绑定与失败封门、`/run` 启动周期交接、根内监督进程持续运行、
仓库桥按需唤醒与收回、真实节点启动回执。每一门只能凭当前目标的执行证据升为 100。
跨根持续监督已经在京东同核 QEMU 中完成。当前最小门是使用独立 Forgejo 数据副本证明仓库桥
唤醒、固定主分支读回、失败收回与最终休眠;公网仓库进程不参与该实验。

View file

@ -0,0 +1,18 @@
---
type: ADR
id: "0178"
title: "Guanghu bounded repository bridge lifecycle"
status: accepted
date: 2026-08-16
refines: "0177"
development_id: "DEV-20260815-001"
---
# 光湖有界仓库桥生命周期
根内光湖监督实体只接受登记的 `repository-main-readback` 能力和一次性请求编号。执行前必须核验
当前机器、仍存活的根监督进程和仓库桥休眠状态;随后依次唤醒隔离仓库桥、读回固定 `main`
重复现场验证,并在成功或失败路径上收回到 `DORMANT`。任意阶段不得接受任意 shell 能力。
真实公网 Forgejo 与 PID 760 不参与隔离门。现实验证使用独立数据副本、独立回环端口和独立进程;
只有最终状态重新休眠且公网仓库不受影响时,才能把这一个隔离谓词记为 100。

View file

@ -127,6 +127,15 @@ prove physical boot or bounded wake/reclaim of the real repository bridge, so
those predicates and final master control remain `0`. See ADR-0177 and
`deployments/JD-FD-PRIMARY/CROSS-ROOT-SUPERVISOR-QEMU-RECEIPT-20260815.hldp`.
## Bounded repository bridge lifecycle
`scripts/guanghu-repository-bridge-lifecycle.sh` requires a live root-supervisor
state and one allowlisted repository readback request. It wakes a registered
backend, verifies the pinned `main`, and reclaims the backend on both success
and failure. `scripts/forgejo-repository-shadow-backend.sh` provides a separate
loopback-only Forgejo data plane for physical-node isolation tests; it never
targets the public repository process. See ADR-0178.
## Language-primary boot target
`guanghu-language-primary.target` makes the accepted cognitive-control model

View file

@ -0,0 +1,118 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
die() {
echo "GUANGHU_FORGEJO_SHADOW_FAIL_0: $*" >&2
exit 1
}
test_root=${GH_FORGEJO_SHADOW_TEST_ROOT:-}
if [[ -n ${test_root} ]]; then
test_root=$(readlink -f "${test_root}")
config_path=${GH_FORGEJO_SHADOW_CONFIG:?test config is required}
dmi_path=${GH_FORGEJO_SHADOW_DMI_PATH:?test DMI path is required}
else
[[ ${EUID} == 0 ]] || die "root execution is required"
[[ -z ${GH_FORGEJO_SHADOW_CONFIG:-}${GH_FORGEJO_SHADOW_DMI_PATH:-} ]] ||
die "production path override is forbidden"
config_path=/etc/guanghu/forgejo-repository-shadow.conf
dmi_path=/sys/class/dmi/id/product_uuid
fi
[[ -f ${config_path} && ! -L ${config_path} ]] || die "configuration is missing or unsafe"
schema=
node_id=
instance_id=
backend_id=
forgejo_binary=
work_path=
app_ini=
http_port=
repository_owner=
repository_name=
expected_main=
run_user=
while IFS='=' read -r key value; do
case "${key}" in
schema|node_id|instance_id|backend_id|forgejo_binary|work_path|app_ini|http_port|repository_owner|repository_name|expected_main|run_user)
printf -v "${key}" '%s' "${value}" ;;
''|'#'*) ;;
*) die "unknown configuration field: ${key}" ;;
esac
done <"${config_path}"
[[ ${schema} == guanghu.forgejo-repository-shadow/v1 ]] || die "configuration schema mismatch"
[[ ${node_id} == JD-FD-PRIMARY ]] || die "node identity mismatch"
[[ $(tr '[:upper:]' '[:lower:]' <"${dmi_path}" | tr -d '\r\n') == "${instance_id}" ]] || die "DMI identity mismatch"
[[ ${backend_id} == jd-repository-shadow ]] || die "backend binding mismatch"
[[ ${2:-} == "${backend_id}" ]] || die "requested backend mismatch"
[[ ${3:-} == repository-main-readback ]] || die "capability is not allowlisted"
[[ ${4:-} =~ ^REQ-[A-Za-z0-9._-]+$ ]] || die "request id is malformed"
[[ -x ${forgejo_binary} && -f ${app_ini} ]] || die "Forgejo shadow artifact is unavailable"
[[ ${http_port} =~ ^[0-9]{4,5}$ ]] || die "HTTP port is malformed"
[[ ${repository_owner} =~ ^[a-z0-9-]+$ && ${repository_name} =~ ^[a-z0-9-]+$ ]] || die "repository binding is malformed"
[[ ${expected_main} =~ ^[0-9a-f]{40}$ ]] || die "expected main is malformed"
[[ ${run_user} == guanghu ]] || die "Forgejo shadow user binding mismatch"
[[ -x /usr/sbin/start-stop-daemon ]] || die "process launcher is unavailable"
if [[ -n ${test_root} ]]; then
for path in "${config_path}" "${work_path}" "${app_ini}"; do
resolved=$(readlink -m "${path}")
[[ ${resolved} == "${test_root}" || ${resolved} == "${test_root}"/* ]] || die "test path escapes isolated root"
done
fi
pid_file=${work_path}/forgejo-shadow.pid
readback_file=${work_path}/${4}.main
url=http://127.0.0.1:${http_port}/code/${repository_owner}/${repository_name}.git
process_alive() {
[[ -s ${pid_file} ]] || return 1
pid=$(cat "${pid_file}")
[[ ${pid} =~ ^[0-9]+$ ]] && kill -0 "${pid}" 2>/dev/null
}
observe() {
if process_alive && curl -fsS --max-time 2 "http://127.0.0.1:${http_port}/code/api/v1/version" >/dev/null; then
echo ready
else
echo dormant
fi
}
case "${1:-}" in
preflight)
[[ -d ${work_path}/repositories/${repository_owner}/${repository_name}.git ]] || die "shadow repository is unavailable"
[[ $(git --git-dir="${work_path}/repositories/${repository_owner}/${repository_name}.git" rev-parse refs/heads/main) == "${expected_main}" ]] ||
die "shadow repository main mismatch"
[[ $(observe) == dormant ]] || die "shadow is not dormant"
;;
observe) observe ;;
wake)
[[ $(observe) == dormant ]] || die "shadow is not dormant before wake"
/usr/sbin/start-stop-daemon --start --background --make-pidfile --pidfile "${pid_file}" \
--chuid "${run_user}" --startas "${forgejo_binary}" -- \
web --work-path "${work_path}" --config "${app_ini}"
for _ in {1..100}; do [[ $(observe) == ready ]] && break; sleep 0.1; done
[[ $(observe) == ready ]] || die "Forgejo shadow readiness failed"
;;
execute)
[[ $(observe) == ready ]] || die "Forgejo shadow is not ready"
git ls-remote "${url}" refs/heads/main | awk '{print $1}' >"${readback_file}"
;;
verify)
[[ $(cat "${readback_file}") == "${expected_main}" ]] || die "repository main readback mismatch"
[[ $(git ls-remote "${url}" refs/heads/main | awk '{print $1}') == "${expected_main}" ]] || die "live repository main mismatch"
;;
reclaim)
if process_alive; then
pid=$(cat "${pid_file}")
kill "${pid}" 2>/dev/null || true
for _ in {1..100}; do kill -0 "${pid}" 2>/dev/null || break; sleep 0.05; done
kill -KILL "${pid}" 2>/dev/null || true
fi
rm -f "${pid_file}"
[[ $(observe) == dormant ]] || die "Forgejo shadow reclaim failed"
;;
*) die "usage: $0 <preflight|observe|wake|execute|verify|reclaim> <backend> <capability> <request>" ;;
esac

View file

@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
die() {
echo "GUANGHU_REPOSITORY_BRIDGE_LIFECYCLE_FAIL_0: $*" >&2
exit 1
}
test_root=${GH_REPOSITORY_LIFECYCLE_TEST_ROOT:-}
if [[ -n ${test_root} ]]; then
test_root=$(readlink -f "${test_root}")
config_path=${GH_REPOSITORY_LIFECYCLE_CONFIG:?test config is required}
dmi_path=${GH_REPOSITORY_LIFECYCLE_DMI_PATH:?test DMI path is required}
root_state=${GH_REPOSITORY_LIFECYCLE_ROOT_STATE:?test root state is required}
receipt_root=${GH_REPOSITORY_LIFECYCLE_RECEIPT_ROOT:?test receipt root is required}
for path in "${config_path}" "${dmi_path}" "${root_state}" "${receipt_root}"; do
resolved=$(readlink -f "${path}")
[[ ${resolved} == "${test_root}" || ${resolved} == "${test_root}"/* ]] ||
die "test path escapes isolated root"
done
else
[[ ${EUID} == 0 ]] || die "root execution is required"
for name in GH_REPOSITORY_LIFECYCLE_CONFIG GH_REPOSITORY_LIFECYCLE_DMI_PATH GH_REPOSITORY_LIFECYCLE_ROOT_STATE GH_REPOSITORY_LIFECYCLE_RECEIPT_ROOT; do
[[ -z ${!name:-} ]] || die "production path override is forbidden: ${name}"
done
config_path=/etc/guanghu/repository-bridge-lifecycle.conf
dmi_path=/sys/class/dmi/id/product_uuid
root_state=/run/guanghu/root-supervisor/state.json
receipt_root=/guanghu/receipts/repository-bridge-lifecycle
fi
[[ -f ${config_path} && ! -L ${config_path} ]] || die "configuration is missing or unsafe"
[[ $(stat -c '%a' "${config_path}" 2>/dev/null || stat -f '%Lp' "${config_path}") == 600 ]] ||
die "configuration mode must be 0600"
schema=
node_id=
instance_id=
backend_id=
backend_command=
capability=
while IFS='=' read -r key value; do
case "${key}" in
schema|node_id|instance_id|backend_id|backend_command|capability) printf -v "${key}" '%s' "${value}" ;;
''|'#'*) ;;
*) die "unknown configuration field: ${key}" ;;
esac
done <"${config_path}"
[[ ${schema} == guanghu.repository-bridge-lifecycle/v1 ]] || die "configuration schema mismatch"
[[ ${node_id} == JD-FD-PRIMARY ]] || die "node identity mismatch"
[[ ${backend_id} =~ ^[a-z0-9-]+$ ]] || die "backend id is malformed"
[[ ${capability} == repository-main-readback ]] || die "capability is not allowlisted"
[[ -x ${backend_command} ]] || die "backend command is unavailable"
[[ $(tr '[:upper:]' '[:lower:]' <"${dmi_path}" | tr -d '\r\n') == "${instance_id}" ]] ||
die "DMI identity mismatch"
[[ -f ${root_state} && ! -L ${root_state} ]] || die "root supervisor state is unavailable"
state=$(cat "${root_state}")
for binding in \
'"schema":"guanghu.root-supervisor-state/v1"' \
'"node_id":"JD-FD-PRIMARY"' \
'"control":"GUANGHU_OS"' \
'"stage":"ROOT_SUPERVISOR_ACTIVE"'; do
grep -Fq "${binding}" <<<"${state}" || die "root supervisor binding mismatch"
done
supervisor_pid=$(sed -n 's/.*"pid":\([0-9][0-9]*\).*/\1/p' <<<"${state}")
[[ -n ${supervisor_pid} ]] && kill -0 "${supervisor_pid}" 2>/dev/null ||
die "root supervisor process is not alive"
boot_id=$(sed -n 's/.*"boot_id":"\([0-9a-f-]*\)".*/\1/p' <<<"${state}")
[[ ${boot_id} =~ ^[0-9a-f-]{36}$ ]] || die "root supervisor boot id is malformed"
request_id=${2:-}
[[ ${1:-} == run && ${request_id} =~ ^REQ-[A-Za-z0-9._-]+$ ]] ||
die "usage: $0 run REQ-<id>"
reclaim_needed=0
cleanup() {
if [[ ${reclaim_needed} == 1 ]]; then
"${backend_command}" reclaim "${backend_id}" "${capability}" "${request_id}" >/dev/null || true
fi
}
trap cleanup EXIT
"${backend_command}" preflight "${backend_id}" "${capability}" "${request_id}" >/dev/null
[[ $("${backend_command}" observe "${backend_id}" "${capability}" "${request_id}") == dormant ]] ||
die "repository bridge is not dormant before wake"
"${backend_command}" wake "${backend_id}" "${capability}" "${request_id}" >/dev/null
reclaim_needed=1
[[ $("${backend_command}" observe "${backend_id}" "${capability}" "${request_id}") == ready ]] ||
die "repository bridge readiness readback failed"
"${backend_command}" execute "${backend_id}" "${capability}" "${request_id}" >/dev/null
"${backend_command}" verify "${backend_id}" "${capability}" "${request_id}" >/dev/null
"${backend_command}" reclaim "${backend_id}" "${capability}" "${request_id}" >/dev/null
reclaim_needed=0
[[ $("${backend_command}" observe "${backend_id}" "${capability}" "${request_id}") == dormant ]] ||
die "repository bridge reclaim readback failed"
mkdir -p "${receipt_root}"
temporary=${receipt_root}/${request_id}.json.tmp.$$
printf '%s\n' "{\"schema\":\"guanghu.repository-bridge-lifecycle-receipt/v1\",\"node_id\":\"${node_id}\",\"boot_id\":\"${boot_id}\",\"request_id\":\"${request_id}\",\"capability\":\"${capability}\",\"control\":\"GUANGHU_OS\",\"result\":\"PASS_100\",\"initial_state\":\"DORMANT\",\"active_state\":\"READY\",\"final_state\":\"DORMANT\"}" >"${temporary}"
chmod 0600 "${temporary}"
mv "${temporary}" "${receipt_root}/${request_id}.json"
trap - EXIT
echo "GUANGHU_REPOSITORY_BRIDGE_LIFECYCLE_PASS_100 request=${request_id} final=DORMANT"

View file

@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
controller=${source_root}/scripts/guanghu-repository-bridge-lifecycle.sh
fixture=$(mktemp -d)
trap 'rm -rf "${fixture}"' EXIT
mkdir -p "${fixture}/state" "${fixture}/receipts"
printf '%s\n' f3d4b730-7f02-452f-975b-7091a4800431 >"${fixture}/dmi"
printf '%s\n' "{\"schema\":\"guanghu.root-supervisor-state/v1\",\"node_id\":\"JD-FD-PRIMARY\",\"boot_id\":\"11111111-2222-4333-8444-555555555555\",\"pid\":$$,\"control\":\"GUANGHU_OS\",\"stage\":\"ROOT_SUPERVISOR_ACTIVE\"}" >"${fixture}/root-state.json"
chmod 0600 "${fixture}/root-state.json"
cat >"${fixture}/backend" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
action=${1:-}
case "${action}" in
preflight) ;;
observe) [[ -f ${FAKE_READY} ]] && echo ready || echo dormant ;;
wake) touch "${FAKE_READY}" ;;
execute) [[ -f ${FAKE_READY} ]]; printf '%s\n' 'd2686f8cb3bde2426f79d13ad6581f7ea6ed8385' >"${FAKE_READBACK}" ;;
verify) [[ $(cat "${FAKE_READBACK}") == d2686f8cb3bde2426f79d13ad6581f7ea6ed8385 ]]; [[ ${FAKE_FAIL_VERIFY:-0} == 0 ]] ;;
reclaim) rm -f "${FAKE_READY}" ;;
*) exit 64 ;;
esac
EOF
chmod 0755 "${fixture}/backend"
cat >"${fixture}/controller.conf" <<EOF
schema=guanghu.repository-bridge-lifecycle/v1
node_id=JD-FD-PRIMARY
instance_id=f3d4b730-7f02-452f-975b-7091a4800431
backend_id=jd-repository-shadow
backend_command=${fixture}/backend
capability=repository-main-readback
EOF
chmod 0600 "${fixture}/controller.conf"
export GH_REPOSITORY_LIFECYCLE_TEST_ROOT=${fixture}
export GH_REPOSITORY_LIFECYCLE_CONFIG=${fixture}/controller.conf
export GH_REPOSITORY_LIFECYCLE_DMI_PATH=${fixture}/dmi
export GH_REPOSITORY_LIFECYCLE_ROOT_STATE=${fixture}/root-state.json
export GH_REPOSITORY_LIFECYCLE_RECEIPT_ROOT=${fixture}/receipts
export FAKE_READY=${fixture}/ready
export FAKE_READBACK=${fixture}/readback
"${controller}" run REQ-PASS-001
[[ ! -e ${fixture}/ready ]]
grep -Fq '"result":"PASS_100"' "${fixture}/receipts/REQ-PASS-001.json"
grep -Fq '"final_state":"DORMANT"' "${fixture}/receipts/REQ-PASS-001.json"
export FAKE_FAIL_VERIFY=1
if "${controller}" run REQ-FAIL-001; then
echo 'verification failure was accepted' >&2
exit 1
fi
[[ ! -e ${fixture}/ready ]]
echo GUANGHU_REPOSITORY_BRIDGE_LIFECYCLE_OK

View file

@ -98,6 +98,8 @@ run_gate guanghu_first_boot_supervisor \
"${source_root}/scripts/test-guanghu-first-boot-supervisor.sh"
run_gate guanghu_root_supervisor \
"${source_root}/scripts/test-guanghu-root-supervisor.sh"
run_gate guanghu_repository_bridge_lifecycle \
"${source_root}/scripts/test-guanghu-repository-bridge-lifecycle.sh"
run_gate auditable_line_coverage_100_percent \
bash -c '
cargo llvm-cov clean --workspace --manifest-path "$1/Cargo.toml"