refactor: extract shared hardened gh runner for all gh spawn paths (#342) - #2407
Conversation
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThis is a behavior-preserving refactor extracting three duplicated gh-runner copies into a shared
No residual-class defect (no reachable injection/traversal/auth-bypass/credential-exposure, no added crash/data-loss, no removed guard without replacement) and no No findings. [OPUS-REVIEWED] 7bd7a78 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSAdvisory design-level review of Design-Verdict: PASS Root-cause consolidation of a real, drift-caused secret leak; right layer (top-level module dissolving the apps→dashboard private import), fully backward-compatible re-exports. [DESIGN-REVIEWED] 7bd7a78 |
|
Addressed both failing checks in GPT 5.6 blocking finding (sage tests writing real SEL): valid — while Cross-Platform Portability: the one in-scope added line was a Design Review watch item (docstring overstating |
f5b1508 to
0f28835
Compare
) Three gh-spawning surfaces (dashboard PR sidebar, Issue Radar, Code Review Sage) each carried a copy of the hardened-runner pattern — trusted-binary resolution, minimal child env, SEL spawn audit — so a hardening fix had to land three times and drift had already set in: sage's pipeline.list_open_prs spawned with the gateway's FULL environment (no env= at all), sage's discovery spawns emitted no SEL audit, and the three env allowlists and override semantics disagreed. kiro_crew/github_runner.py now owns the policy once, with no dashboard imports (dissolving issue_radar's and sage's cross-layer import of the private _validate_provider_executable): - validate_provider_executable / provider_executable_candidates moved verbatim from source_providers (public names, re-exported there under the historical private ones) - resolve_gh: caller override, then KIROCREW_GH_BIN, then candidates; a SET-but-empty override fails loudly; noisy last-error filtering; cached with reset_cache() - gh_env: one canonical gh-scoped passthrough union on minimal_env, ambient ssh-agent/git-ssh identity stripped, optional GH_HOST pin - run_gh: the single sync spawn chokepoint — absolute trusted binary required, minimal env, bounded timeout, SEL audit on ok/failure/ timeout for every caller - parse_github_repo_url + RepoUrlError moved from issue_radar (github_client re-exports; errors.RepoUrlError is the same class) pipeline.list_open_prs, discovery.run_gh_json/current_login, and issue_radar._gh_run now route through run_gh, closing the env leak and audit gaps. Four spawn-audit exemptions collapse into one. Closes #342
0f28835 to
7bd7a78
Compare
|
GPT 5.6 blocking finding on
Caller mapping: Issue Radar surfaces the refusal as a retryable |
…irodotdev#342) (kirodotdev#2407) Three gh-spawning surfaces (dashboard PR sidebar, Issue Radar, Code Review Sage) each carried a copy of the hardened-runner pattern — trusted-binary resolution, minimal child env, SEL spawn audit — so a hardening fix had to land three times and drift had already set in: sage's pipeline.list_open_prs spawned with the gateway's FULL environment (no env= at all), sage's discovery spawns emitted no SEL audit, and the three env allowlists and override semantics disagreed. kiro_crew/github_runner.py now owns the policy once, with no dashboard imports (dissolving issue_radar's and sage's cross-layer import of the private _validate_provider_executable): - validate_provider_executable / provider_executable_candidates moved verbatim from source_providers (public names, re-exported there under the historical private ones) - resolve_gh: caller override, then KIROCREW_GH_BIN, then candidates; a SET-but-empty override fails loudly; noisy last-error filtering; cached with reset_cache() - gh_env: one canonical gh-scoped passthrough union on minimal_env, ambient ssh-agent/git-ssh identity stripped, optional GH_HOST pin - run_gh: the single sync spawn chokepoint — absolute trusted binary required, minimal env, bounded timeout, SEL audit on ok/failure/ timeout for every caller - parse_github_repo_url + RepoUrlError moved from issue_radar (github_client re-exports; errors.RepoUrlError is the same class) pipeline.list_open_prs, discovery.run_gh_json/current_login, and issue_radar._gh_run now route through run_gh, closing the env leak and audit gaps. Four spawn-audit exemptions collapse into one. Closes kirodotdev#342
Summary
Closes #342.
Three
gh-spawning surfaces (dashboard PR sidebarsource_providers, Issue Radargithub_client, Code Review Sagediscovery/pipeline) each carried their own copy of the hardened-runner pattern — trusted-binary resolution, minimal child env, SEL spawn audit — so every hardening fix had to land three times, and drift had already produced latent bugs:code_review_sage/sage_lib/pipeline.py::list_open_prsspawnedsubprocess.run(argv)with noenv=at all — the child inherited the gateway's full environment, AWS/Slack/SSH secrets included.discovery.run_gh_json/current_loginemitted no SEL audit events.The shared module
New
src/kiro_crew/github_runner.py— top-level, nokiro_crew.dashboard.*imports ever (dissolving Issue Radar's and Sage's cross-layer import of the private_validate_provider_executable). Heavy deps (sel,apps.registry.minimal_env,config.loader) stay lazy so Sage's standalonesage_libimport path keeps working.validate_provider_executable/provider_executable_candidates+ constants: moved verbatim fromsource_providersas public symbols;source_providersre-exports them under the historical private names (identity-asserted in tests), sogitlab_client's glab resolution and all existing importers are untouched.resolve_gh(override_env=, cache=): caller-specific override → genericKIROCREW_GH_BIN→ well-known dirs →PATH. Strictest-superset semantics: a SET-but-empty override fails loudly (D4), "path does not exist" noise is filtered from setup messages (D5), win32 refused, cached withreset_cache().gh_env(pin_host=): one canonical gh-scoped passthrough union on theminimal_envbase (D3), plusGH_PAGER=cat/NO_COLOR=1, with ambient ssh-agent/git-ssh identity stripped (gh apiis HTTPS/token auth; the gateway's ssh identity was pure surplus credential surface).run_gh(argv, *, timeout, input_text, audit_caller, pin_host): the single sync spawn chokepoint — absolute trusted binary required (no caller can regress to a PATH lookup),env=gh_env(), bounded timeout, SEL audit on ok / non-zero exit / timeout / spawnOSError, tagged per caller so Issue Radar keeps its historicalissue_radar.gh_runSEL operation identity. Errors re-raise transparently so each caller keeps its own taxonomy.parse_github_repo_url+RepoUrlError: moved fromgithub_client, re-exported there;errors.RepoUrlErroraliases the same class so all route-levelexceptclauses keep catching it.Call-site migration (behavior-preserving)
source_providers: async_run_json, caching, glab behavior, base env keys, andPATH/GH_HOSTpins unchanged; only the validation/candidates source of truth moved, and its gh auth key set now derives from the canonical union (minus the enterprise tokens its github.com-pinned child can never use).issue_radar:_gh_bin/_gh_env/_gh_runare thin wrappers keepingGhSetupError(reason=)/GhCliErrormapping and_stderr_tailsanitization;_gh_runadditionally pinsGH_HOST=github.com(Issue Radar is github.com-only by design and its API paths never pass--hostname).code_review_sage:gh_bin/gh_envdelegate (guarded import preserves the standalone path);run_gh_json/current_login/pipeline.list_open_prsroute throughrun_gh, closing D1 and D2.test/test_spawn_audit.py: four benign-spawn exemptions collapse into onegithub_runner.py::run_ghentry.Tests
New
test/test_github_runner.py(resolver precedence/caching/fail-loud override, exact env-set assertions with a polluted gateway env, SEL audit on ok/exit/timeout/OSError, pin-host semantics, re-export identity seams). D1 lock-in test added to sage'sTestListOpenPrsasserting the spawn now carries the minimal env. Existing suites pass; monkeypatch targets that pinned the old private import locations were retargeted to the runner's public names.Pre-push review fleet
OSErrorescaped unaudited) — fixed (audited with coarse exception class, then re-raised).pin_hostwired for Issue Radar, fixture delenv + stale comment cleanup).Verification
isort/flake8/mypyclean; fullpython -m pytestrun: 39,517 passed — remaining failures reproduce identically on unmodified main in this environment (host hasnobody-owned/tmpancestry andnobody-owned/usr/bin/gh, which the ownership validation correctly refuses; CI runners are unaffected). Touched-area suites (sage, issue_radar, source_providers, spawn audit, env consolidation): green.scripts/check_brand_name.pyandscripts/docs-lint.shpass. Docs updated in the same commit (issue-radar.md,learn-cron-dashboard.md).