diff --git a/tools/code-exec-harness/README.md b/tools/code-exec-harness/README.md index 22a743ff6207..e5eb3aad2c42 100644 --- a/tools/code-exec-harness/README.md +++ b/tools/code-exec-harness/README.md @@ -12,12 +12,13 @@ shims such as fake `gh` need to write logs and state outside the fixture workspace. Run it only with trusted scenarios. By default, each scenario gets an empty `CODE_HOME`. Pass `--inherit-auth` only -when you want a live model-backed run; it copies auth files into the isolated -run home without copying the rest of your config. +when you want a live model-backed run; it copies Code auth and GitHub CLI config +into the isolated run home without copying the rest of your config. `HOME`, `ZDOTDIR`, `XDG_CONFIG_HOME`, and `XDG_CACHE_HOME` are also redirected inside the run directory so shell startup files and home-directory tooling do -not silently use the real user profile. +not silently use the real user profile. When auth is inherited, `GH_CONFIG_DIR` +points at the copied GitHub CLI config inside that redirected home. ## Run @@ -48,7 +49,7 @@ Scenarios are JSON files. Common fields: `PATH` - `config_toml`: isolated `CODE_HOME/config.toml` contents - `config_overrides`: `-c key=value` arguments passed to `code exec` -- `inherit_auth`: copy auth files from the current `CODE_HOME` for this scenario +- `inherit_auth`: copy Code auth and GitHub CLI config for this scenario - `expect`: simple assertions over the final answer, commands, fake `gh` calls, and exit code diff --git a/tools/code-exec-harness/harness.py b/tools/code-exec-harness/harness.py index cb876fa6e795..f30277fb54f1 100644 --- a/tools/code-exec-harness/harness.py +++ b/tools/code-exec-harness/harness.py @@ -153,12 +153,28 @@ def write_config(scenario: dict[str, Any], paths: RunPaths) -> None: put_text(paths.code_home / "config.toml", config + "\n") -def inherit_auth(paths: RunPaths) -> None: +def gh_config_source() -> Path: + if os.environ.get("GH_CONFIG_DIR"): + return Path(os.environ["GH_CONFIG_DIR"]).expanduser() + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + if xdg_config_home: + return Path(xdg_config_home).expanduser() / "gh" + return Path.home() / ".config" / "gh" + + +def inherit_auth(paths: RunPaths) -> dict[str, str]: + env_overrides: dict[str, str] = {} source_home = Path(os.environ.get("CODE_HOME") or os.environ.get("CODEX_HOME") or Path.home() / ".code") for name in ("auth.json", ".credentials.json"): source = source_home / name if source.is_file(): shutil.copy2(source, paths.code_home / name) + source_gh_config = gh_config_source() + if source_gh_config.is_dir(): + target_gh_config = paths.shell_home / ".config" / "gh" + copy_or_link(source_gh_config, target_gh_config, symlink=False) + env_overrides["GH_CONFIG_DIR"] = str(target_gh_config) + return env_overrides FAKE_GH = r'''#!/usr/bin/env python3 @@ -206,6 +222,25 @@ def finish(stdout="", stderr="", exit_code=0): print(stderr, file=sys.stderr) raise SystemExit(exit_code) +def fields_from_args(): + fields = {} + index = 0 + while index < len(args): + arg = args[index] + if arg in {"-F", "--field", "-f", "--raw-field"} and index + 1 < len(args): + field = args[index + 1] + if "=" in field: + key, value = field.split("=", 1) + fields[key] = value + index += 2 + continue + if index + 2 < len(args): + fields[field] = args[index + 2] + index += 3 + continue + index += 1 + return fields + for response in fixture.get("responses", []): match = response.get("match", {}) matched = True @@ -292,10 +327,7 @@ def finish(stdout="", stderr="", exit_code=0): if args and args[0] == "api": joined = " ".join(args) match = re.search(r"repos/([^/]+)/([^/]+)/issues/(\d+)/sub_issues", joined) - child = None - for index, arg in enumerate(args): - if arg in {"-F", "--field"} and index + 1 < len(args) and args[index + 1].startswith("sub_issue_id="): - child = args[index + 1].split("=", 1)[1] + child = fields_from_args().get("sub_issue_id") if match and child: parent = match.group(3) issue = state["issues"].setdefault(parent, {"number": int(parent), "title": "", "state": "OPEN", "subIssues": []}) @@ -490,8 +522,9 @@ def run_scenario(path: Path, args: argparse.Namespace) -> int: materialize_workspace(scenario, paths) materialize_skills(scenario, paths, scenario_dir, extra_roots) write_config(scenario, paths) + inherited_env = {} if args.inherit_auth or scenario.get("inherit_auth", False): - inherit_auth(paths) + inherited_env = inherit_auth(paths) gh_paths = write_fake_gh(scenario, paths) command = build_command(scenario, args, paths) @@ -506,6 +539,7 @@ def run_scenario(path: Path, args: argparse.Namespace) -> int: "XDG_CONFIG_HOME": str(paths.shell_home / ".config"), "ZDOTDIR": str(paths.shell_home), }) + env.update(inherited_env) for key, value in scenario.get("env", {}).items(): env[str(key)] = str(value) if gh_paths: