From 666454212ac9c3e10c3db927ebcf534f18203864 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 21:09:45 +0800 Subject: [PATCH] fix(runner): harden non110 registration ceremony [skip ci] --- docs/LOGBOOK.md | 16 +++++++++ ops/runner/README.md | 12 +++++++ ops/runner/register-awoooi-non110-runner.sh | 16 ++++++++- .../test_register_awoooi_non110_runner.py | 35 +++++++++++++++++-- 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index b1e4f1942..adf659237 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-06-28 — 21:09 non-110 runner registration ceremony hardening + +**完成內容**: +- `ops/runner/register-awoooi-non110-runner.sh` 補上正式 `--check` / `--dry-run` check-mode。 +- registration helper 現在明確拒絕 `RUNNER_TOKEN` environment,避免 runner token 透過 env / process context 進入 Codex 或 shell 流程;仍只允許互動 TTY silent prompt 與 stdin registration。 +- `ops/runner/README.md` 同步 188 check-mode 與正式註冊邊界:Codex 不接觸 token、不使用 `--token` argv、不列印 token、不讀 `.runner` 內容。 + +**驗證結果**: +- `bash -n ops/runner/register-awoooi-non110-runner.sh ops/runner/install-awoooi-non110-runner-user-service.sh ops/runner/check-awoooi-non110-runner-readiness.sh`:通過。 +- `python3.11 -m pytest ops/runner/test_register_awoooi_non110_runner.py ops/runner/test_check_awoooi_non110_runner_readiness.py ops/runner/test_guard_gitea_runner_pressure.py -q`:`12 passed`。 +- `python3 ops/runner/guard-gitea-runner-pressure.py --root .`:`GITEA_RUNNER_PRESSURE_GUARD_OK workflow_files=10 scheduled_workflows=3 auto_branch_events_on_110=0 generic_runner_labels=0`。 +- 188 registration helper check-mode:`dry_run=true`、`runner_token_in_argv=false`、`raw_runner_registration_read=false`、`safe_next_step=run_this_script_from_interactive_tty_without_capturing_token`。 +- 188 readiness verifier:`READY_CONFIG_COUNT=1`、`READY_BINARY_COUNT=1`、`READY_SERVICE_COUNT=1`、`READY_REGISTRATION_COUNT=0`、`READY_ACTIVE_SERVICE_COUNT=0`、`UNMANAGED_RUNNER_CONTAINERS=none`、`HEAVY_PROCESS_COUNT=0`、`BLOCKER_COUNT=3`。 + +**邊界**:沒有讀 `.runner` 內容、runner token、secret、raw session、SQLite、auth 或 `.env`;沒有啟動 runner service;沒有重開 110 runner;沒有重啟 Docker / Nginx / firewall / K3s / DB;沒有使用 GitHub。 + ## 2026-06-28 — 20:45 Delivery Workbench non-110 runner readiness 投影 **完成內容**: diff --git a/ops/runner/README.md b/ops/runner/README.md index 791f4baef..cb8dbb085 100644 --- a/ops/runner/README.md +++ b/ops/runner/README.md @@ -484,6 +484,18 @@ ssh ollama@192.168.0.188 'bash -s' \ < ops/runner/check-awoooi-non110-runner-readiness.sh ``` +runner registration 只能由 operator 在互動 TTY 執行;Codex 不得接觸 token。 +註冊 helper 的 check-mode 不需要 token,也不得接受 `RUNNER_TOKEN` environment: + +```bash +ssh ollama@192.168.0.188 'bash -s -- --check' \ + < ops/runner/register-awoooi-non110-runner.sh +``` + +正式註冊時,helper 會用 silent TTY 讀取 runner token,透過 stdin 餵給 +`act_runner register`,不使用 `--token` argv、不列印 token、不讀 `.runner` +內容;若 `.runner` 已存在,預設只回報下一步 enable / verifier,不覆寫。 + `--enable` 只允許在 `AWOOOI_NON110_ENABLE=1`、`act_runner` executable、 `config.yaml` present、`.runner` present 且 service 已由 verifier 證明 target / limits 正確後使用。installer 不會讀 `.runner` 內容,也不會寫 runner token;rollback 只會 diff --git a/ops/runner/register-awoooi-non110-runner.sh b/ops/runner/register-awoooi-non110-runner.sh index 7b1616b2b..23fb0fc4c 100755 --- a/ops/runner/register-awoooi-non110-runner.sh +++ b/ops/runner/register-awoooi-non110-runner.sh @@ -17,7 +17,7 @@ RUNNER_REGISTRATION="${RUNNER_REGISTRATION:-${RUNNER_DIR}/.runner}" usage() { cat <<'EOF' Usage: - DRY_RUN=1 ops/runner/register-awoooi-non110-runner.sh + ops/runner/register-awoooi-non110-runner.sh --check ops/runner/register-awoooi-non110-runner.sh Environment overrides: @@ -27,6 +27,7 @@ Environment overrides: Safety contract: - requires an interactive TTY for token entry - reads the runner token with terminal echo disabled + - refuses RUNNER_TOKEN from the environment - feeds the token to act_runner over stdin, not argv - never prints the token or .runner content - refuses to overwrite an existing non-empty registration unless @@ -114,10 +115,23 @@ main() { usage exit 0 fi + if [ "${1:-}" = "--check" ] || [ "${1:-}" = "--dry-run" ]; then + DRY_RUN=1 + export DRY_RUN + shift + fi + if [ "$#" -gt 0 ]; then + usage >&2 + exit 2 + fi umask 077 trap 'unset RUNNER_TOKEN 2>/dev/null || true' EXIT + if [ "${RUNNER_TOKEN+x}" = "x" ]; then + die "runner_token_env_not_allowed" + fi + metadata preflight diff --git a/ops/runner/test_register_awoooi_non110_runner.py b/ops/runner/test_register_awoooi_non110_runner.py index c4027103e..9f7067107 100644 --- a/ops/runner/test_register_awoooi_non110_runner.py +++ b/ops/runner/test_register_awoooi_non110_runner.py @@ -15,11 +15,43 @@ USER_SERVICE_INSTALLER = ROOT / "ops/runner/install-awoooi-non110-runner-user-se def test_register_helper_has_no_token_argv_path() -> None: text = REGISTER.read_text(encoding="utf-8") assert "--token" not in text + assert "runner_token_env_not_allowed" in text assert "runner_token_in_argv=false" in text assert "runner_token_echoed=false" in text assert "raw_runner_registration_read=false" in text +def test_register_helper_rejects_runner_token_env(tmp_path: Path) -> None: + runner_dir = tmp_path / "runner" + runner_dir.mkdir(parents=True) + binary = runner_dir / "act_runner" + binary.write_text("#!/usr/bin/env bash\nexit 99\n", encoding="utf-8") + binary.chmod(0o755) + config = runner_dir / "config.yaml" + config.write_text("runner:\n capacity: 1\n", encoding="utf-8") + + result = subprocess.run( + ["bash", str(REGISTER), "--check"], + check=False, + env={ + **os.environ, + "RUNNER_TOKEN": "fake-token-that-must-not-be-accepted", + "RUNNER_DIR": str(runner_dir), + "RUNNER_BINARY": str(binary), + "RUNNER_CONFIG": str(config), + "RUNNER_REGISTRATION": str(runner_dir / ".runner"), + }, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert result.returncode != 0 + assert "runner_token_env_not_allowed" in result.stderr + assert "fake-token-that-must-not-be-accepted" not in result.stdout + assert "fake-token-that-must-not-be-accepted" not in result.stderr + + def test_user_installer_has_no_token_argv_path() -> None: wrapper = COMPAT_WRAPPER.read_text(encoding="utf-8") installer = USER_SERVICE_INSTALLER.read_text(encoding="utf-8") @@ -39,11 +71,10 @@ def test_register_helper_dry_run_requires_no_token(tmp_path: Path) -> None: config.write_text("runner:\n capacity: 1\n", encoding="utf-8") result = subprocess.run( - ["bash", str(REGISTER)], + ["bash", str(REGISTER), "--check"], check=False, env={ **os.environ, - "DRY_RUN": "1", "RUNNER_DIR": str(runner_dir), "RUNNER_BINARY": str(binary), "RUNNER_CONFIG": str(config),