fix: prevent story detection device auth in non-tty#1959
fix: prevent story detection device auth in non-tty#1959prompt-driven-github[bot] wants to merge 7 commits into
Conversation
Re-align the issue #1923 fix with the prompt-first source of truth and keep it scoped to story validation instead of globally changing device-flow auth. - Prompts (source of truth) now describe the behavior: analysis (scope PDD_NO_INTERACTIVE around story validation when non-TTY), llm_invoke (_interactive_credential_acquisition_allowed honors PDD_NO_INTERACTIVE), and user_story_tests (fail closed with credential guidance on validation errors). - Revert the global _is_noninteractive() isatty change: it broke 'pdd auth login' with piped stdin and the existing non-interactive-auth test. The scoped PDD_NO_INTERACTIVE env set by 'detect --stories' is sufficient. - Add tests/test_issue_1923_detect_stories_noninteractive.py: end-to-end regression that drives the real command -> real cloud-auth guard and asserts GitHub device-flow is never started in a non-TTY run (red on main, green now). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Independent verification — PR #1959 (fixes #1923)Ran an isolated worktree off ✅ What works
❌ Blocking — two CI regressions (both PASS on
|
…e point Address adversarial review of the #1923 fix: - Move the non-interactive scoping from the detect command into run_user_story_tests so ALL callers (detect --stories, change, agentic change, drift) are protected, not just detect. Force PDD_NO_INTERACTIVE=1 around each auth-sensitive detect_change call and restore the caller's prior value, closing the gap where a falsy preset (PDD_NO_INTERACTIVE=""/"0") left the downstream truthiness guard unset and reopened the device-login hang. - Unify the non-interactive truthy set to 1/true/yes/on across the guards (get_jwt_token._is_noninteractive now includes 'on'). - Make the fail-closed story error conditional ("If this is an auth/credential error ...") so genuine bugs are not misattributed to missing credentials. - Sync prompts (analysis, get_jwt_token, user_story_tests) with the above. - Rewrite the #1923 regression tests to drive the REAL run_user_story_tests and real get_jwt_token guard (device-flow trapped), covering falsy/on presets, the opt-in and real-TTY cases, and the end-to-end no-device-flow path. - docs: soften the unconditional claim and document PDD_ALLOW_INTERACTIVE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uard Round-2 review follow-ups (non-blocking hardening): - get_jwt_token._is_noninteractive: strip() env values for whitespace consistency with the other guards (prompt updated to match). - user_story_tests: escape rich markup in the fail-closed error print so a malformed exception string can't crash the fail-closed path. - Add a thread-safety note on the scoped os.environ mutation. - Tests: add a direct 'on'/whitespace assertion for the get_jwt_token guard and correct the e2e test docstring about the regression condition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…noninteractive-auth
|
/gcbrun |
1 similar comment
|
/gcbrun |
✅ Independent verification — APPROVED (head
|
…noninteractive-auth
How this PR was reviewed and hardenedThis PR was taken through two rounds of adversarial review plus an independent reproducer. Summary of what was found and how it was addressed: Design correction (prompt-first): The initial version edited generated code without touching the owning Findings addressed:
Validation: |
|
/gcbrun |
✅ Re-verified at head
|
Summary
pdd detect --stories(and the other commands that run story validation through the shared runner —pdd change, agentic change, and drift fixing) could block forever on an interactive GitHub device-login prompt when run from an agent or CI-style non-interactive shell. This PR makes story validation non-interactive by default when there is no TTY, so it never opens a browser/device-login flow; instead it uses available credentials or fails fast with clear guidance.Fixes #1923.
The bug (before)
When an agent/CI runs story validation with cloud enabled but no cached credentials, the story-validation LLM call reaches cloud authentication and prints a GitHub device code, then waits for a human who will never come:
Required story validation can never complete, so the whole run is stuck.
After
In a non-interactive (non-TTY) shell, story validation no longer starts device login. It either uses existing credentials, or fails fast and non-zero with actionable guidance:
pdd auth login(explicit authentication) is unaffected, even with piped stdin.PDD_ALLOW_INTERACTIVE=1.What changed
This is a prompt-first repo, so the behavior is specified in the
.promptsources and the generated code follows:run_user_story_testsis the single choke point for story validation (detect --stories,pdd change, agentic change, and drift fixing all call it). The non-interactive guard now lives there: when stdin is not a TTY (and the caller hasn't setPDD_ALLOW_INTERACTIVE), it setsPDD_NO_INTERACTIVE=1around each auth-sensitivedetect_changecall and restores the caller's prior value afterward. This protects every caller, not justdetect.get_jwt_token._is_noninteractive) and interactive API-key acquisition (llm_invoke) both honorPDD_NO_INTERACTIVE, so neither a GitHub device flow nor an API-keyinput()prompt can start.detect_changeraises (auth/model/credential/network), the run reports a failed story and exits non-zero instead of crashing or hanging (preserves the Regression: pdd detect --stories exits 0 despite failed stories #1872 non-zero-exit contract).1/true/yes/onacross the guards.docs/generating_user_stories.md) with the supported credential paths and thePDD_ALLOW_INTERACTIVEopt-in.Prompt/code/test/example/doc are all in sync (prompts:
user_story_tests_python.prompt,commands/analysis_python.prompt,get_jwt_token_python.prompt,llm_invoke_python.prompt).Testing
End-to-end / high-level scenarios covered by
tests/test_issue_1923_detect_stories_noninteractive.pyand updated existing tests:run_user_story_testsdrives the realdetect_change → llm_invoke → get_jwt_tokenpath withDeviceFlowtrapped; the trap is never hit and the run fails closed. This is red onmain(device flow starts) and green here.PDD_NO_INTERACTIVE="","0","false","on","1"are all forced to a guard-honored"1"during validation, then restored.PDD_ALLOW_INTERACTIVE=1and a real TTY do not force non-interactive mode.Validation / mergeability
ontruthy value, per-story env restore acrossfail_fast/continue/exception, opt-in, and real-TTY. Story validation is single-threaded per command, so the scopedos.environset/restore is safe.detect_changedirectly (run_user_story_fix, used bypdd fix <story>, and story prompt-link caching) are pre-existing and outside this issue's acceptance criteria; they are tracked in a separate follow-up so this PR stays scoped to pdd detect --stories blocks on interactive GitHub device auth in non-interactive runs #1923.