diff --git a/cc-ci-plan/advisory-scan.py b/cc-ci-plan/advisory-scan.py index 42cbee3..3a818d8 100755 --- a/cc-ci-plan/advisory-scan.py +++ b/cc-ci-plan/advisory-scan.py @@ -1114,19 +1114,24 @@ def markdown(rep: dict) -> str: def _gitea_auth(url: str) -> dict: - """Basic auth for the private mirror, from /srv/cc-ci/.testenv. + """Basic auth for the private mirror, using public config plus .testenv. Sent as a HEADER, never embedded in the URL: in-URL credentials leak into shell history, process lists and error messages, and urllib mis-parses a password containing a colon.""" host = re.sub(r"^https?://", "", url).split("/")[0] env = {} - try: - for ln in open(os.environ.get("CCCI_TESTENV", "/srv/cc-ci/.testenv")): + for path in ( + "/srv/cc-ci/cc-ci/.env.public", + os.environ.get("CCCI_TESTENV", "/srv/cc-ci/.testenv"), + ): + try: + lines = open(path) + except OSError: + continue + for ln in lines: if "=" in ln and not ln.strip().startswith("#"): k, v = ln.strip().split("=", 1) env[k] = v.strip().strip("\"'") - except OSError: - return {} if host != env.get("GITEA_URL", "git.autonomic.zone"): return {} u, pw = env.get("GITEA_USERNAME"), env.get("GITEA_PASSWORD") diff --git a/cc-ci-plan/agents.toml b/cc-ci-plan/agents.toml index 3f22dd3..2e91617 100644 --- a/cc-ci-plan/agents.toml +++ b/cc-ci-plan/agents.toml @@ -34,7 +34,7 @@ supports_resume = false prompt_delivery = "ping" # send after the TUI connects connect_delay = 12 submit_key = "C-m" -preamble = "set -a; . /srv/cc-ci/.testenv; set +a" +preamble = "set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a" stall_idle = 900 active_re = "esc interrupt|thinking|inferring|running tool|tool call|preparing patch|reading|searching" limit_re = "spend limit|usage limit|limit reached|reached your .*limit|out of (credits|tokens)" diff --git a/cc-ci-plan/launch-assistant.py b/cc-ci-plan/launch-assistant.py index 18a6db9..b3cdad6 100644 --- a/cc-ci-plan/launch-assistant.py +++ b/cc-ci-plan/launch-assistant.py @@ -112,7 +112,7 @@ def start(mode="resume"): if not Path(OPENCODE_BIN).exists(): die(f"opencode not found at {OPENCODE_BIN}") cmd = ( - f"set -a; . /srv/cc-ci/.testenv; set +a; " + f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; " f"NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {WORKDIR}" ) log(f"starting {SESSION} (backend=opencode, model={LOOP_MODEL or 'default'})") diff --git a/cc-ci-plan/launch-orchestrator.py b/cc-ci-plan/launch-orchestrator.py index 8c38b0f..7ecb983 100644 --- a/cc-ci-plan/launch-orchestrator.py +++ b/cc-ci-plan/launch-orchestrator.py @@ -154,7 +154,7 @@ def start(mode="resume"): f"OPENCODE_CONFIG_CONTENT={shlex.quote(json.dumps({'model': LOOP_MODEL}))} " ) cmd = ( - f"set -a; . /srv/cc-ci/.testenv; set +a; " + f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; " f"{model_env}NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {shlex.quote(WORKDIR)}" ) log(f"starting {SESSION} (backend=opencode, model={LOOP_MODEL or 'default'})") diff --git a/cc-ci-plan/launch-report.py b/cc-ci-plan/launch-report.py index 3b9b9cd..d954ddc 100755 --- a/cc-ci-plan/launch-report.py +++ b/cc-ci-plan/launch-report.py @@ -109,7 +109,7 @@ def start(mode, date): cwd = "/srv/cc-ci-orch/cc-ci" share_flag = "--share" if OPENCODE_SHARE else "" cmd = ( - f"set -a; . /srv/cc-ci/.testenv; set +a; " + f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; " f"{OPENCODE_BIN} run {model_flag} {share_flag} --attach '{OPENCODE_SERVER}' " f"--title '{SESSION}' --dir {cwd} \"$(cat '{kf}')\"" ) diff --git a/cc-ci-plan/launch-supervisor.py b/cc-ci-plan/launch-supervisor.py index eef0549..fd07b8a 100644 --- a/cc-ci-plan/launch-supervisor.py +++ b/cc-ci-plan/launch-supervisor.py @@ -107,7 +107,7 @@ def spawn_supervisor(sid, reason): kf = Path(LOG_DIR) / f".kickoff-{SUP_SESSION}.txt" kf.write_text(build_kickoff(sid, reason)) share = "--share" if OPENCODE_SHARE else "" - cmd = (f"set -a; . {WORKDIR}/.testenv; set +a; {OPENCODE_BIN} run --model '{MODEL}' {share} " + cmd = (f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; {OPENCODE_BIN} run --model '{MODEL}' {share} " f"--attach '{OPENCODE_SERVER}' --title '{SUP_SESSION}' --dir {WORKDIR} \"$(cat '{kf}')\"") _sh(["tmux", "new-session", "-d", "-s", SUP_SESSION, "-c", WORKDIR, cmd]) _sh(["tmux", "pipe-pane", "-o", "-t", SUP_SESSION, f"cat >> '{LOG_DIR}/{SUP_SESSION}.log'"]) diff --git a/cc-ci-plan/launch-upgrader.py b/cc-ci-plan/launch-upgrader.py index f637f4f..643aaa6 100644 --- a/cc-ci-plan/launch-upgrader.py +++ b/cc-ci-plan/launch-upgrader.py @@ -216,7 +216,7 @@ def start(mode="use-or-create"): # so they must come AFTER `run` (a global `opencode --model X run` is ignored). share_flag = "--share" if OPENCODE_SHARE else "" cmd = ( - f"set -a; . /srv/cc-ci/.testenv; set +a; " + f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; " f"{OPENCODE_BIN} run {model_flag} {share_flag} --attach '{OPENCODE_SERVER}' " f"--title '{SESSION}' \"$(cat '{kf}')\"" ) @@ -521,7 +521,7 @@ def resume(reason="manual"): "write+push the weekly summary, then `python3 /srv/cc-ci/cc-ci-plan/launch-report.py fresh`, print " "'" + DONE_MARKER + "', and go idle.") share = "--share" if OPENCODE_SHARE else "" - cmd = (f"set -a; . /srv/cc-ci/.testenv; set +a; {OPENCODE_BIN} run -s {sid} --continue " + cmd = (f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; {OPENCODE_BIN} run -s {sid} --continue " f"--model '{MODEL}' {share} --attach '{OPENCODE_SERVER}' --dir '{WORKDIR}' \"$(cat '{kf}')\"") subprocess.run(["tmux", "new-session", "-d", "-s", SESSION, "-c", WORKDIR, cmd]) subprocess.run(["tmux", "pipe-pane", "-o", "-t", SESSION, f"cat >> '{LOG_FILE}'"]) diff --git a/cc-ci-plan/launch.py b/cc-ci-plan/launch.py index b48e4b4..9731add 100644 --- a/cc-ci-plan/launch.py +++ b/cc-ci-plan/launch.py @@ -323,7 +323,7 @@ def start_agent(role, session, workdir): # absolute repo paths for builder/adversary work. session_cwd = "/srv/cc-ci-orch/cc-ci" cmd = ( - f"set -a; . /srv/cc-ci/.testenv; set +a; " + f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; " f"NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {session_cwd}" ) log(f"starting {session} (backend=opencode, phase={pid}, model={model or 'default'})") diff --git a/cc-ci-plan/load-env.sh b/cc-ci-plan/load-env.sh new file mode 100755 index 0000000..6b27df2 --- /dev/null +++ b/cc-ci-plan/load-env.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Load cc-ci runtime configuration. Source this file; do not execute it. +# +# Callers enable `set -a` so both files become exported environment variables. +# Public configuration is loaded first, then the secret file. This preserves a +# local secret override only where an emergency migration explicitly needs it. + +if [ -r /srv/cc-ci/cc-ci/.env.public ]; then + . /srv/cc-ci/cc-ci/.env.public +fi + +if [ ! -r /srv/cc-ci/.testenv ]; then + echo "cc-ci secret environment is missing: /srv/cc-ci/.testenv" >&2 + return 1 +fi + +. /srv/cc-ci/.testenv diff --git a/cc-ci-plan/overnight-run.sh b/cc-ci-plan/overnight-run.sh index d779d53..1d51e9b 100644 --- a/cc-ci-plan/overnight-run.sh +++ b/cc-ci-plan/overnight-run.sh @@ -71,7 +71,7 @@ log "writing report $REPORT ..." if [ -f "$SUMMARY" ]; then cat "$SUMMARY"; else echo "(no dated summary at $SUMMARY — /upgrade-all may have stalled on the usage limit or errored; check the cc-ci-upgrader session + $LOG)"; fi echo echo "## Open PRs per recipe (post-run)" - set -a; . /srv/cc-ci/.testenv 2>/dev/null; set +a + set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a G="https://${GITEA_USERNAME}:${GITEA_PASSWORD}@${GITEA_URL}/api/v1" for r in $(curl -fsS "$G/orgs/recipe-maintainers/repos?limit=100" 2>/dev/null | python3 -c "import sys,json;print('\n'.join(sorted(x['name'] for x in json.load(sys.stdin))))" 2>/dev/null); do case "$r" in cc-ci|cc-ci-orchestrator|cc-ci-secrets|archived-*) continue;; esac diff --git a/cc-ci-plan/plan.md b/cc-ci-plan/plan.md index f8097f6..a53c3a4 100644 --- a/cc-ci-plan/plan.md +++ b/cc-ci-plan/plan.md @@ -83,9 +83,10 @@ repo, a commit, a log, or the dashboard** (§9) — reference locations only. | What | Where | How to use | |---|---|---| | **cc-ci SSH (root)** | private key `~/.ssh/cc-ci-root-ed25519`; `Host cc-ci` in `~/.ssh/config` (HostName `100.90.116.4`, no ProxyCommand) | Just run `ssh cc-ci` (logs in as **root**). The orchestrator VM is a direct tailnet peer — direct route, no proxy. Pubkey already in cc-ci's `/root/.ssh/authorized_keys`. | -| **Gitea bot account** | `/srv/cc-ci/.testenv` → `GITEA_USERNAME` (`autonomic-bot`), `GITEA_PASSWORD`, `GITEA_URL` (`git.autonomic.zone`) | Basic-auth to the Gitea API, or mint a scoped token: `POST https://$GITEA_URL/api/v1/users/$GITEA_USERNAME/tokens`. Used to push the `cc-ci` project repo, read recipe repos, comment on PRs, and poll for `!testme` (read-level; the bot does not register webhooks). | +| **Gitea bot account** | `/srv/cc-ci/cc-ci/.env.public` → `GITEA_USERNAME` (`autonomic-bot`); `/srv/cc-ci/.testenv` → `GITEA_PASSWORD`; `GITEA_URL` defaults to `git.autonomic.zone` | Basic-auth to the Gitea API, or mint a scoped token: `POST https://$GITEA_URL/api/v1/users/$GITEA_USERNAME/tokens`. Used to push the `cc-ci` project repo, read recipe repos, comment on PRs, and poll for `!testme` (read-level; the bot does not register webhooks). | -Load them in a shell with: `set -a; . /srv/cc-ci/.testenv; set +a` (don't echo the values). +Load runtime configuration in a shell with: `set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a` +(don't echo secret values). The helper loads tracked public configuration first and then `.testenv`. ### The Tailscale connection (how `ssh cc-ci` works) diff --git a/cc-ci-plan/pr-survey.py b/cc-ci-plan/pr-survey.py index ed8c850..355d244 100755 --- a/cc-ci-plan/pr-survey.py +++ b/cc-ci-plan/pr-survey.py @@ -40,19 +40,22 @@ from datetime import datetime, timezone HERE = os.path.dirname(os.path.abspath(__file__)) USED_RECIPES = os.path.join(HERE, "used-recipes.md") TESTENV = os.environ.get("CCCI_TESTENV", "/srv/cc-ci/.testenv") +PUBLIC_ENV = "/srv/cc-ci/cc-ci/.env.public" NS = "recipe-maintainers" def _env() -> dict: e = {} - try: - for ln in open(TESTENV): + for path in (PUBLIC_ENV, TESTENV): + try: + lines = open(path) + except OSError: + continue + for ln in lines: ln = ln.strip() if "=" in ln and not ln.startswith("#"): k, v = ln.split("=", 1) e[k] = v.strip().strip('"').strip("'") - except OSError: - pass return e diff --git a/cc-ci-plan/recipe-report.py b/cc-ci-plan/recipe-report.py index f73cf78..34f9ea4 100755 --- a/cc-ci-plan/recipe-report.py +++ b/cc-ci-plan/recipe-report.py @@ -46,6 +46,7 @@ from datetime import datetime, timezone LOGDIR = "/srv/cc-ci/.cc-ci-logs" TESTENV = "/srv/cc-ci/.testenv" +PUBLIC_ENV = "/srv/cc-ci/cc-ci/.env.public" INFRA = {"cc-ci", "cc-ci-orchestrator", "cc-ci-secrets"} HOST_REPORTS = "/var/lib/cc-ci-reports" # Both kinds live in ONE archive, distinguished by a suffix on a common title. @@ -56,14 +57,16 @@ KINDS = {"week": ("The Recipe Report", "Week of {d} — full"), def _env(): e = {} - try: - for ln in open(TESTENV): + for path in (PUBLIC_ENV, TESTENV): + try: + lines = open(path) + except FileNotFoundError: + continue + for ln in lines: ln = ln.strip() if "=" in ln and not ln.startswith("#"): k, v = ln.split("=", 1) e[k] = v.strip().strip('"').strip("'") - except FileNotFoundError: - pass return e diff --git a/cc-ci-plan/reconcile-upstream.sh b/cc-ci-plan/reconcile-upstream.sh index 5ecf8f0..d4ff776 100755 --- a/cc-ci-plan/reconcile-upstream.sh +++ b/cc-ci-plan/reconcile-upstream.sh @@ -30,12 +30,11 @@ set -o errexit -o nounset -o pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ORCH="$(dirname "$HERE")" SSH="${SSH:-cc-ci}" -TESTENV="${TESTENV:-/srv/cc-ci/.testenv}" RECONCILE="${RECONCILE:-$ORCH/.claude/skills/recipe-upgrade/open-recipe-pr.sh}" USED_RECIPES="${USED_RECIPES:-$HERE/used-recipes.md}" [ -f "$RECONCILE" ] || { echo "ERROR: reconcile helper not found: $RECONCILE" >&2; exit 1; } -set -a; . "$TESTENV"; set +a +set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a : "${GITEA_USERNAME:?}"; : "${GITEA_PASSWORD:?}"; : "${GITEA_URL:=git.autonomic.zone}" if [ "${1:-}" = "--all" ]; then diff --git a/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix b/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix index fdf3c1a..701370d 100644 --- a/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix +++ b/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix @@ -131,7 +131,8 @@ SSHCFG # opencode web server — one shared instance; agent sessions attach to it for web visibility. # Serves the web UI at http://oc.commoninternet.net (via nginx below, tailscale-only). - # Provider creds are read from /srv/cc-ci/.testenv at startup. + # Public provider configuration is tracked in the cc-ci repo; credentials + # remain in /srv/cc-ci/.testenv. systemd.services.opencode-web = { description = "opencode web server for cc-ci agents"; wantedBy = [ "multi-user.target" ]; @@ -141,7 +142,10 @@ SSHCFG Type = "simple"; User = "loops"; Group = "users"; WorkingDirectory = "/srv/cc-ci-orch/cc-ci"; - EnvironmentFile = "/srv/cc-ci/.testenv"; + EnvironmentFile = [ + "-/srv/cc-ci/cc-ci/.env.public" + "/srv/cc-ci/.testenv" + ]; ExecStartPre = "${pkgs.coreutils}/bin/rm -rf /tmp/opencode"; ExecStart = "/home/loops/.local/bin/opencode serve --hostname 127.0.0.1 --port 4096"; Restart = "on-failure";