Skip to content

feat: Quick PRD generator with SSRF-safe references and output validation - #213

Closed
AdeRistanto wants to merge 1 commit into
OpenSQZ:mainfrom
AdeRistanto:feat/quick-prd-generator
Closed

AdeRistanto wants to merge 1 commit into
OpenSQZ:mainfrom
AdeRistanto:feat/quick-prd-generator

Conversation

@AdeRistanto

Copy link
Copy Markdown

Pull Request

Description

Adds a Quick PRD mode: a complete Product Requirements Document is generated in one streaming LLM call and shown in a new web UI. Two templates are available: fullstack_nextjs (the default, "PRD Full Stack untuk AI Coding (Starter Next JS)", written in Indonesian) and general. This PR also hardens reference-website fetching against SSRF and validates every generated PRD automatically.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Code refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧪 Test addition or improvement
  • 🔨 Build/CI changes (dependency declaration only)
  • 🎨 Code style/formatting changes

Changes Made

Quick PRD generator (gtplanner/agent/prd_generator.py, POST /api/prd/stream in fastapi_main.py, static/index.html)

  • Streams the PRD as SSE events: status, reference, chunk, validation, done.
  • Fixed template parts are inserted verbatim: the header (usage notes plus a section 0 setup prompt with NGODINGPAKEAI_TOKEN taken from .env) and the footer. The LLM fills prd_templates/fullstack_nextjs/skeleton.md.
  • Revisions: the previous PRD is sent back without its fixed parts and the whole document is regenerated.
  • The latest framework is in the template:
    • architecture flowchart and sequence diagrams for critical flows
    • "used" markers for give-backs
    • provider deadline and cancel rules
    • follow-up flags, owner columns and configurable limits
    • a 29-item checklist and six cross-checks

SSRF hardening of reference fetching (fetch_reference). URLs found in the request are fetched with these rules:

  • Only http/https on ports 80/443. URLs with userinfo are rejected, and so are internal host names (localhost, *.internal, *.local, single-label names).
  • Every DNS answer must be public. If any address in the answer is not public, the request is rejected. Explicit IPv4/IPv6 deny lists cover cloud metadata (169.254.169.254, 168.63.129.16, 100.100.100.200, fd00:ec2::254), CGNAT, ULA, link-local, 6to4 and Teredo. IPv4-mapped and NAT64 addresses are unwrapped first. The check does not rely on is_global alone.
  • The connection is pinned to the validated IP, with the original Host header and sni_hostname. TLS verification stays on.
  • Redirects are handled manually (max 3), and each hop is validated independently.
  • trust_env=False, so proxy environment variables are ignored.
  • Response limits: only text/html, application/xhtml+xml and text/plain; Content-Length over 1 MB is rejected; the streamed body is capped at 1 MB.
  • Timeouts and concurrency: 15 s total per URL, up to 4 concurrent fetches across the server, and references within one request are fetched in parallel.
  • The client only sees generic error messages. Details go to the server log.
  • Kill switch: PRD_REFERENCE_FETCH=0 disables fetching completely, with no network access.

Post-generation validation (validate_prd, shown per item in the UI)

  • finish_reason and max_tokens are reported. length/max_tokens marks the PRD as truncated, and a truncated PRD is never shown as done.
  • Checks:
    • no leftover [placeholder] outside code
    • all required headings (0–12, "Hasil pengecekan silang", "Pertanyaan terbuka")
    • an even number of code fences
    • every actor in sequenceDiagram arrows declared as a participant
    • the repo version (section 0 removed) contains no ngpk_ or NGODINGPAKEAI_TOKEN value

Config

  • settings.toml: llm.max_tokens = 65536. Without it the provider's default cut long PRDs off.

⚠️ Unrelated changes included in this PR

Whole files were committed, so this PR also carries some earlier local edits to the same files that are not part of the feature:

  • pyproject.toml / uv.lock: adds truststore>=0.10. The code that uses it (gtplanner/__init__.py) is not in this PR, so the dependency is currently unused.
  • settings.toml: Chinese comments translated to English; timeout = 600 and max_retries = 1 for llm.
  • fastapi_main.py: everything changed in this file, not only the /api/prd/stream route and serving static/index.html at /.

I can split these into a separate PR if you prefer.

Testing Performed

  • CLI functionality tested
  • API endpoints tested (if applicable)
  • MCP service tested (if applicable)
  • Error handling tested
  • Edge cases tested
uv run pytest tests -q
# 226 passed on the full working tree.
# 188 passed on the committed snapshot alone (test_prd_generator, test_prd_reference_security,
# test_prd_validation, test_basic), verified to import the snapshot's modules.
  • tests/test_prd_reference_security.py (135 tests) runs fully offline: DNS is faked, HTTP goes through httpx.MockTransport, and httpcore's real connect_tcp is replaced by a recorder, so no socket is ever opened. The tests show that the real connection layer connects to the pinned IP and that start_tls receives the original host name with CERT_REQUIRED and check_hostname=True. Mutation check: disabling each control in memory makes the suite fail (IP checks 69, pinning 18, kill switch 6, error sanitising 68 failures; without the size cap a test never finishes).
  • tests/test_prd_validation.py (43 tests) covers a passing and a failing case for each check, plus finish_reason handling. Each check was also disabled in memory to confirm the tests fail.
  • Manual end-to-end run through /api/prd/stream (e-commerce PRD with Midtrans and shipping):
    • glm-5.3-flash: finish_reason=length after about 22 min. The PRD was correctly flagged as truncated, probably because reasoning tokens count against max_tokens.
    • deepseek-v4-flash: finish_reason=stop after about 5.5 min, about 121k characters, all 5 checks passed.

Not verified: real-network behaviour of the SSRF hardening (an actual TLS handshake to a pinned IP, OS/container DNS and IPv6 behaviour). Please test from a staging environment before relying on it in production.

Environment Tested

  • OS: Windows 11
  • Python: 3.11.18 (uv), httpx 0.28.1, httpcore 1.0.9
  • LLM: OpenAI-compatible proxy (glm-5.3-flash, deepseek-v4-flash)

Breaking Changes

  • No breaking changes
  • Yes, breaking changes (describe below)

The existing agent planner endpoints are unchanged. Note that llm.max_tokens now applies to every LLM call.

Documentation

  • Code comments added/updated
  • README.md updated (if needed)
  • CONTRIBUTING.md updated (if needed)
  • API documentation updated (if applicable)
  • No documentation changes needed

Dependencies

  • No new dependencies

  • New dependencies added (list below)

  • httpx>=0.28.1: declared explicitly. It was already installed through openai. The locked version is unchanged (no other package versions changed in uv.lock).

  • truststore>=0.10: see "Unrelated changes" above.

Performance Impact

  • No performance impact
  • Performance improvement
  • Potential performance regression (explain below)

A full-stack PRD takes several minutes to generate and uses a large system prompt (about 12k characters plus the skeleton). Reference fetches add at most 15 s and now run in parallel.

Security Considerations

  • No security implications
  • Security improvements
  • Potential security concerns (explain below)

Out of scope and still open:

  • /api/prd/stream has no authentication.
  • CORS is *.
  • The server binds to 0.0.0.0.
  • Other fetchers (URL_to_Markdown.py, search.py via Jina) are unchanged.
  • Section 0 of the fullstack template contains the NGODINGPAKEAI_TOKEN value. The UI's "Download untuk repo" strips it, and the validation checks that.

🤖 Generated with Claude Code

…lidation

- Quick PRD mode: single streaming LLM call that fills the "PRD Full Stack untuk
  AI Coding (Starter Next JS)" template (header/skeleton/footer) or a general PRD,
  served at POST /api/prd/stream and a new web UI (static/index.html).
- Template updated to the latest framework: architecture diagram, sequence
  diagrams for critical flows, give-back markers, provider deadline/cancel rules,
  follow-up flags, owner columns, configurable limits, 29-item checklist and six
  cross-checks; system prompt aligned.
- Reference website fetching hardened against SSRF: http/https on ports 80/443
  only, no userinfo or internal host names, every DNS answer must be public
  (explicit IPv4/IPv6 deny lists incl. cloud metadata, NAT64/mapped unwrapping),
  connection pinned to the validated IP with Host header + TLS SNI/verification,
  per-hop redirect validation (max 3), trust_env=False, text content types only,
  1 MB body cap, 15 s total timeout per URL, 4 concurrent fetches globally,
  generic client-facing errors, PRD_REFERENCE_FETCH=0 kill switch.
- Post-generation validation: finish_reason/max_tokens reporting with truncation
  detection, leftover [placeholder] scan, required headings, balanced code fences,
  declared sequenceDiagram participants, and no token in the repo version.
  Results are shown per check in the UI; truncated PRDs are not marked done.
- settings.toml: llm.max_tokens = 65536; pyproject/uv.lock: explicit httpx.
- Tests: tests/test_prd_generator.py, test_prd_reference_security.py (offline,
  mocked DNS/transport), test_prd_validation.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@AdeRistanto

Copy link
Copy Markdown
Author

Opened by mistake (intended for a personal repository). Closing, sorry for the noise.

@AdeRistanto AdeRistanto closed this Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants