# Verify that remote source builds leave no GitHub credential residue on EC2

This retained non-runtime check verifies one internal security boundary: the
production remote module-download helper removes its temporary token and
AskPass material after both success and failure, never passes the credential in
argv, and does not write it to helper logs.

Run it to produce a security receipt. It is intentionally under `tools/`, not
`spec-tests/`, because it directly exercises an internal helper and stops
before a customer product path. Do not rerun
[`TestFunctional_SourceBuildGitHubApp`](../../spec-tests/source_build_github_app_functional_test.go)
to validate this harness, and do not use this receipt to replace that case's
product verdict.

## Agent execution

**Mode: single agent.** Spawn no sub-agents. No human authorization is needed:
the case uses a non-working fixture token against a deterministic fake `go`
command while exercising the production remote credential helper on an EC2
host.

Run this check only with:

```text
spec-tests/scripts/run-markdown-case.sh tools/github-credential-residue/README.md
```

## Default run

- AWS account `804837308162`, region `us-west-2`, source profile
  `dev-offline-afterworld`, and role `cp9-developer`.
- A fresh `remote-ec2-k3d` host in `tilt.mode=generate-only`; `cp9 up` is
  stopped as soon as the shared SSH path proves the host reachable, before any
  service, source image, private repository, or customer product path deploys.
- The instruction to execute this case authorizes the bounded host, fixture
  credential exercise, read-only residue probes, and exact-session cleanup.
- The machine needs `jq`, `python3`, `ssh`, and AWS CLI. No GitHub Device Flow,
  PAT, App private key, client secret, or real token is used.

## Execute

```bash
set -Eeuo pipefail
export ORIGIN_CP9=$(git rev-parse --show-toplevel)
export RUN_DIR="$ORIGIN_CP9/artifacts/spec-tests/$(date -u +%Y%m%dT%H%M%SZ)-github-credential-residue"
"$ORIGIN_CP9/spec-tests/scripts/default-804-session.sh" prepare security "$RUN_DIR"
source "$RUN_DIR/defaults.env"
cd "$CP9_ROOT"
cleanup_case() {
  local status=0
  set +e
  cd "$ORIGIN_CP9"
  "$ORIGIN_CP9/spec-tests/scripts/default-804-session.sh" cleanup "$RUN_DIR" || status=$?
  set -e
  return "$status"
}
UP_PID=
stop_up() {
  local pid=${UP_PID:-}
  test -n "$pid" || return 0
  kill -0 "$pid" 2>/dev/null && kill -INT "$pid" 2>/dev/null || true
  wait "$pid" 2>/dev/null || true
  UP_PID=
}
cleanup_on_exit() {
  local status=$?
  trap - EXIT INT TERM
  stop_up
  cleanup_case || { test "$status" -ne 0 || status=1; }
  exit "$status"
}
cleanup_on_signal() {
  local status=$1
  trap - EXIT INT TERM
  stop_up
  cleanup_case || true
  exit "$status"
}
trap cleanup_on_exit EXIT
trap 'cleanup_on_signal 130' INT
trap 'cleanup_on_signal 143' TERM
git rev-parse HEAD | tee "$RUN_DIR/cp9-head.txt"
make build 2>&1 | tee "$RUN_DIR/build.log"
bin/cp9 setup checkout=false config="$CP9_CONFIG" 2>&1 | tee "$RUN_DIR/setup.log"
bin/cp9 '{sys.output.format=json}' up dry-run=true config="$CP9_CONFIG" \
  >"$RUN_DIR/dry-run.json"
jq -e '.no_mutation and .session != "" and .owner != "" and
  .aws.account == "804837308162" and .aws.region == "us-west-2" and
  .ttl != ""' "$RUN_DIR/dry-run.json"
jq '{session, owner, account: .aws.account, region: .aws.region, ttl}' \
  "$RUN_DIR/dry-run.json" >"$RUN_DIR/run-scope.json"
printf '%s\n' "$CLEANUP_AUTHORITY" >"$RUN_DIR/cleanup-authority.txt"
bin/cp9 doctor config="$CP9_CONFIG" 2>&1 | tee "$RUN_DIR/doctor.log"
bin/cp9 up config="$CP9_CONFIG" >"$RUN_DIR/up.log" 2>&1 &
UP_PID=$!
remote_ready=false
deadline=$((SECONDS + 900))
while test "$SECONDS" -lt "$deadline"; do
  if CP9_REPO_ROOT="$CP9_ROOT" \
    "$CP9_ROOT/skills/get-started/scripts/session-ssh.sh" \
    "$SESSION_NAME" true >"$RUN_DIR/remote-ready.stdout" \
    2>"$RUN_DIR/remote-ready.stderr"; then
    remote_ready=true
    break
  fi
  kill -0 "$UP_PID" 2>/dev/null || break
  sleep 2
done
test "$remote_ready" = true
stop_up
bin/cp9 '{sys.output.format=json}' status config="$CP9_CONFIG" \
  >"$RUN_DIR/status.json"
jq -e '.host_mode == "remote-ec2-k3d" and .layers.host.status == "configured"' \
  "$RUN_DIR/status.json"
```

Stage only the production remote downloader and its security verifier on the
selected host. Both SSH calls reuse the session's persisted direct-SSH or SSM
transport through `session-ssh.sh`; the case contains no host parsing.

```bash
set -Eeuo pipefail
export REMOTE_SECURITY_ROOT="/tmp/cp9-github-security-$SESSION_NAME"
case "$REMOTE_SECURITY_ROOT" in
  /tmp/cp9-github-security-c9s*) ;;
  *) echo "unexpected remote security root: $REMOTE_SECURITY_ROOT" >&2; exit 1 ;;
esac
CP9_REPO_ROOT="$CP9_ROOT" \
  "$CP9_ROOT/skills/get-started/scripts/session-ssh.sh" \
  "$SESSION_NAME" mkdir -m 700 -- "$REMOTE_SECURITY_ROOT"
tar -cf - -C "$CP9_ROOT" \
  personas/dev-full/build/remote/github-user-module-download.sh \
  tools/github-credential-residue/verify-remote.sh |
  CP9_REPO_ROOT="$CP9_ROOT" \
    "$CP9_ROOT/skills/get-started/scripts/session-ssh.sh" \
    "$SESSION_NAME" tar -xf - -C "$REMOTE_SECURITY_ROOT"

set +e
CP9_REPO_ROOT="$CP9_ROOT" \
  "$CP9_ROOT/skills/get-started/scripts/session-ssh.sh" \
  "$SESSION_NAME" bash \
  "$REMOTE_SECURITY_ROOT/tools/github-credential-residue/verify-remote.sh" \
  "$REMOTE_SECURITY_ROOT/personas/dev-full/build/remote/github-user-module-download.sh" \
  >"$RUN_DIR/remote-security-receipt.json" \
  2>"$RUN_DIR/remote-security.stderr"
security_rc=$?
set -e

security_status=not_run
if test "$security_rc" -eq 0 &&
  jq -e '.status == "pass"' "$RUN_DIR/remote-security-receipt.json" >/dev/null; then
  security_status=pass
elif test "$security_rc" -eq 10 &&
  jq -e '.status == "fail"' "$RUN_DIR/remote-security-receipt.json" >/dev/null; then
  security_status=fail
fi
jq -n --arg status "$security_status" --argjson remote_rc "$security_rc" \
  '{status:$status,remote_rc:$remote_rc}' >"$RUN_DIR/security-verdict.json"

CP9_REPO_ROOT="$CP9_ROOT" \
  "$CP9_ROOT/skills/get-started/scripts/session-ssh.sh" \
  "$SESSION_NAME" rm -rf -- "$REMOTE_SECURITY_ROOT"
trap - EXIT INT TERM
cleanup_case
jq -e '.destroy == "pass" and .key_pair == "pass" and
  (.worktree == "pass" or .worktree == "not_needed")' \
  "$RUN_DIR/cleanup-receipt.json"
test "$security_status" = pass
```

## Verdict

- `PASS`: `security-verdict.json` is `pass`, the success helper returns zero,
  the intentional failure returns nonzero, and every residue count is zero.
- `FAIL`: the remote verifier returns rc `10` with a confirmed credential
  path, cache, AskPass file, environment, argv, or log match. Do not print the
  matching value.
- `NOT RUN`: EC2 setup, SSH/SSM transport, staging, verifier prerequisites, or
  verifier execution fails without a confirmed credential match. A transport
  or harness error is not credential residue.

The receipt is security-only. It does not change the private-source product
verdict. The run destroys its exact session and retains
`security-verdict.json`, `remote-security-receipt.json`, and
`cleanup-receipt.json`.
