Skip to content

Tailor improvements v2#723

Open
Paranoidal97 wants to merge 11 commits into
srbhr:devfrom
Paranoidal97:tailor-improvements-v2
Open

Tailor improvements v2#723
Paranoidal97 wants to merge 11 commits into
srbhr:devfrom
Paranoidal97:tailor-improvements-v2

Conversation

@Paranoidal97
Copy link
Copy Markdown

@Paranoidal97 Paranoidal97 commented Mar 23, 2026

Pull Request Title

Related Issue

Description

copilot:summary

Type

  • Bug Fix
  • Feature Enhancement
  • Documentation Update
  • Code Refactoring
  • Other (please specify):

Proposed Changes

Screenshots / Code Snippets (if applicable)

How to Test

Checklist

  • The code compiles successfully without any errors or warnings
  • The changes have been tested and verified
  • The documentation has been updated (if applicable)
  • The changes follow the project's coding guidelines and best practices
  • The commit messages are descriptive and follow the project's guidelines
  • All tests (if applicable) pass successfully
  • This pull request has been linked to the related issue (if applicable)

Additional Information

copilot:walkthrough


Summary by cubic

Improves the tailoring flow with partial acceptance, live streaming previews, and URL-based job fetching with JS-aware extraction. Adds dynamic model discovery (including local ollama) with a searchable picker, plus stronger factual protection, parsing, health checks, and network safety.

  • New Features

    • Diff preview: accept/reject individual suggestions with select all/none and counters; confirm supports partial_confirm.
    • Streaming preview: SSE at /resumes/improve/preview/stream with frontend streaming support for slow models.
    • Job from URL: POST /jobs/fetch-url extracts descriptions via site CSS rules; Playwright fallback runs in a background thread for JS-heavy sites; LLM post-filter removes UI chrome; longest selector match avoids partial extraction; UI toggle “Paste Text” / “From URL”.
    • Granular workflow: “Standard” vs “Granular” two-pass mode (skills and experience) with updated prompts and schema examples.
    • Model discovery & picker: /config/provider-models and /config/ollama-models; Settings shows a searchable model dropdown and fetches provider models using stored keys.
  • Bug Fixes

    • Factual protection: restore immutable fields (names, titles, dates, institutions) and preserve skills; validators coerce nulls to empty strings.
    • Stability: cached LLM health checks (30s TTL, keyed per provider/model), default ollama base to 127.0.0.1, tighter litellm/httpx timeouts.
    • Robust fetch & security: strict URL validation with private IP blocking; enrichment guards for non-dict items.
    • Quality: normalize month names to English 3-letter abbreviations and unify “Present” variants; improved keyword matching with proper boundaries (C++, C#, Node.js); updated tests.

Written for commit 9746217. Summary will update on new commits.

Paranoidal97 and others added 2 commits March 11, 2026 00:49
…orkflow

## Partial change acceptance (diff preview)
- Users can now toggle individual AI suggestions on/off in the diff preview modal
- Added "Select all / Deselect all" controls and accepted/total count indicator
- New `onConfirmPartial(acceptedIndices)` callback on DiffPreviewModal
- Backend `/resumes/improve/confirm` accepts `partial_confirm` flag and
  filters improvements list to only accepted changes

## Job description from URL
- New `POST /jobs/fetch-url` endpoint extracts job description from a public URL
- Frontend tailor page gains a tab toggle: "Paste Text" / "From URL"
- URL input with Fetch button; on success populates textarea and switches to text mode
- New schemas: `FetchJobUrlRequest`, `FetchJobUrlResponse`
- New i18n keys in all 5 languages (en, es, zh, ja, pt-BR): `inputMode`, `urlInput`, error messages

## Playwright-based extraction for JS-rendered portals
- Rule-based `_SmartExtractor` (httpx) for static HTML sites
- `SITE_MAP` with site-specific CSS selectors for known portals:
  - theprotocol.it → `[id='section-offerView']`
  - nofluffjobs.com → `[id='posting-description']`
  - pracuj.pl, justjoin.it, linkedin.com, indeed.com also configured
- Playwright runs in `ThreadPoolExecutor` (sync API) — avoids Windows asyncio
  subprocess limitation with uvicorn SelectorEventLoop
- All selectors are evaluated, longest match wins (prevents partial extraction)

## Granular LLM workflow
- New `improve_resume_granular()`: two focused LLM calls instead of one large call
  - Call A: summary + technicalSkills only (IMPROVE_RESUME_PROMPT_SKILLS)
  - Call B: workExperience bullet descriptions only (IMPROVE_RESUME_PROMPT_EXPERIENCE)
- New prompt templates and JSON schema examples in `prompts/templates.py`
- Frontend workflow selector: "Standard" vs "Granular" mode passed as `workflow_mode`
- Reduces hallucination risk by keeping each prompt small and focused

## Factual field protection
- `_restore_factual_fields()`: after any LLM improvement, institution names,
  degree titles, employment dates, company names and job titles are always
  restored from the original resume — LLM cannot alter factual metadata

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread apps/backend/app/routers/config.py Outdated
Comment thread apps/backend/app/routers/health.py Outdated
return None


def _is_private_host(hostname: str) -> bool:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: SSRF check can be bypassed with DNS rebinding attacks

The _is_private_host function resolves the hostname once at validation time, but a malicious DNS server could return a public IP initially and then rebind to a private IP for the actual request. This is a Time-Of-Check-To-Time-Of-Use (TOCTOU) vulnerability.

Consider:

  1. Using httpx's built-in limits with max_redirects=0 and validating after connection
  2. Resolving DNS yourself and passing the IP directly to httpx
  3. Using a library designed for SSRF prevention

Comment thread apps/backend/app/routers/jobs.py Outdated
Comment thread apps/backend/app/routers/jobs.py Outdated
Comment thread apps/backend/app/routers/config.py Outdated
raise HTTPException(status_code=400, detail=f"Unsupported provider: {provider}")

stored = _load_config()
api_key: str = stored.get("api_key", "") or settings.llm_api_key or ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: API key may be empty or missing when fetching provider models

If stored.get("api_key") is empty string and settings.llm_api_key is also empty, the code will send requests with an empty Bearer token or query param. This should explicitly fail fast with a clear error message.

if provider != "anthropic" and not api_key:
    raise HTTPException(
        status_code=400,
        detail=f"API key required to fetch models for {provider}. Please configure your API key first."
    )

# ---------------------------------------------------------------------------


_playwright_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="playwright")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: ThreadPoolExecutor may not be properly cleaned up

While atexit.register is used, this doesn't guarantee cleanup in all cases (e.g., SIGKILL, exceptions during shutdown). Consider using a context manager pattern or FastAPI's lifespan events for proper resource management.

Comment thread apps/backend/app/llm.py Outdated
Comment thread apps/backend/app/routers/resumes.py Outdated
Comment thread apps/backend/app/services/improver.py Outdated
@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented Mar 23, 2026

Code Review Summary

Status: 1 New Issue | 2 Issues Carried Forward | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1

Changes Since Last Review (abb24a6...83ebf97)

Changed File Analysis
apps/backend/app/routers/health.py ⚠️ New issue — Single-flight pattern implemented to coalesce concurrent health checks. However, task cancellation (e.g., client disconnect) leaves stale entry in _health_inflight dict, causing subsequent requests to await a cancelled task.
New Issues (this review)

WARNING

File Line Issue
apps/backend/app/routers/health.py 27 Task cancellation leaves stale entry in _health_inflight — if CancelledError propagates, the cleanup in the lock block is skipped, causing subsequent requests to await a cancelled task
Carried Forward Issues (unchanged code)

WARNING

File Line Issue
apps/backend/app/routers/jobs.py 135 SSRF check still vulnerable to DNS rebinding on initial request (TOCTOU between DNS lookup and HTTP request). The redirect guard only protects redirects, not the initial resolution.
apps/backend/app/schemas/models.py 593 partial_confirm field defined but never used in backend — frontend sends partial_confirm: true but the /improve/confirm endpoint doesn't check it

SUGGESTION

File Line Issue
apps/backend/app/routers/jobs.py 204 ThreadPoolExecutor cleanup could use FastAPI lifespan events instead of atexit
Previous Fixes Verified (from earlier commits)
  • Thread-safety fix: health.py uses asyncio.Lock() to protect the cache from concurrent access ✅
  • Lock held during I/O: health.py now releases the lock during the slow health check I/O, preventing head-of-line blocking ✅
  • API key validation: config.py validates API key presence before calling provider APIs (Anthropic exempt as it uses static list) ✅
  • Code readability: llm.py line split for better readability ✅
  • Provider-specific keys: config.py now uses resolve_api_key() to correctly resolve provider-specific API keys ✅
Files Reviewed in Incremental Diff (1 file)
  • apps/backend/app/routers/health.py — Single-flight pattern for health check coalescing (1 new issue)

Fix these issues in Kilo Cloud


Reviewed by claude-opus-4.5 · 259,264 tokens

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

18 issues found across 23 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/app/routers/jobs.py">

<violation number="1" location="apps/backend/app/routers/jobs.py:138">
P1: SSRF guard only checks a single IPv4 resolution via gethostbyname, so hosts with multiple A/AAAA records (including private/IPv6) can bypass the check if the resolved address differs from the one validated.</violation>

<violation number="2" location="apps/backend/app/routers/jobs.py:157">
P2: The multiline noise regex strips any leading number on every line, which will remove legitimate numeric prefixes in job descriptions (e.g., numbered requirements or "5+ years"). This degrades extracted content.</violation>

<violation number="3" location="apps/backend/app/routers/jobs.py:576">
P0: Custom agent: **Flag Security Vulnerabilities**

SSRF bypass via open redirect: the private-host check only validates the *initial* URL, but `follow_redirects=True` lets httpx follow a 302 to an internal/cloud-metadata IP. An attacker's public URL can redirect to `http://169.254.169.254/...` to exfiltrate cloud credentials.

Either disable automatic redirects or add an httpx `event_hook` on `response` that re-validates each redirect target against `_is_private_host` before following it.</violation>
</file>

<file name="apps/backend/app/routers/resumes.py">

<violation number="1" location="apps/backend/app/routers/resumes.py:234">
P2: Index-based restoration of education/workExperience factual fields can corrupt data if the LLM reorders entries; entries should be matched by id (or another stable key) instead of list position.</violation>

<violation number="2" location="apps/backend/app/routers/resumes.py:610">
P2: Immutable factual fields are restored before `refine_resume`, but refinement can mutate those fields (LLM keyword injection and recursive phrase removal). Since the refined output overwrites `improved_data` without a post-refinement restore, the final output can drift from the original factual data.</violation>

<violation number="3" location="apps/backend/app/routers/resumes.py:799">
P2: Custom agent: **Flag Security Vulnerabilities**

Client-controlled `partial_confirm` flag bypasses the server-side hash integrity check that ensures confirmed data matches a prior preview. Any client can set `partial_confirm: true` to submit arbitrary `improved_data` that was never generated or validated by the server, undermining the preview/confirm integrity flow. Consider enforcing the hash check server-side unconditionally, or storing partial edits server-side and validating them against the original preview.</violation>

<violation number="4" location="apps/backend/app/routers/resumes.py:1286">
P2: DB update failures are silently swallowed in retry failure path, allowing API to return `processing_status='failed'` even when that status was not persisted.</violation>
</file>

<file name="apps/backend/app/routers/health.py">

<violation number="1" location="apps/backend/app/routers/health.py:24">
P2: LLM health cache is global and not keyed by config, so `/status` can return health results for a previous provider/model/api_key when config changes within the TTL.</violation>

<violation number="2" location="apps/backend/app/routers/health.py:32">
P2: Cache timestamp is recorded before the long health check and then stored unchanged, so slow/timeout checks write an already-expired cache entry and force repeated expensive probes.</violation>
</file>

<file name="apps/frontend/components/ui/dropdown.tsx">

<violation number="1" location="apps/frontend/components/ui/dropdown.tsx:46">
P2: Filtering is enabled with `search.trim()` but matching uses the untrimmed `search`, so leading/trailing spaces can hide valid results.</violation>

<violation number="2" location="apps/frontend/components/ui/dropdown.tsx:130">
P2: New searchable dropdown UI strings are hardcoded in English instead of using the existing translation system.</violation>
</file>

<file name="apps/backend/app/schemas/models.py">

<violation number="1" location="apps/backend/app/schemas/models.py:387">
P2: customSections pre-validator drops valid CustomSection instances because it only preserves dict values, causing silent data loss when models are passed in before coercion.</violation>
</file>

<file name="apps/frontend/components/tailor/diff-preview-modal.tsx">

<violation number="1" location="apps/frontend/components/tailor/diff-preview-modal.tsx:40">
P2: acceptedIndices is initialized from detailedChanges only on first render and never resynced, so reopening the modal or receiving new detailedChanges can leave stale selections and submit indices from a previous diff.</violation>
</file>

<file name="apps/frontend/app/(default)/settings/page.tsx">

<violation number="1" location="apps/frontend/app/(default)/settings/page.tsx:305">
P2: Async model fetch results can overwrite the model list after the user switches providers, because the response is applied without verifying the provider is still current or canceling in‑flight requests.</violation>

<violation number="2" location="apps/frontend/app/(default)/settings/page.tsx:785">
P2: User-facing text for the new model fetch UI is hardcoded instead of using the page’s translation system, causing inconsistent localization.</violation>
</file>

<file name="apps/backend/app/llm.py">

<violation number="1" location="apps/backend/app/llm.py:373">
P2: Ollama health-check fallback still hardcodes localhost, bypassing the Windows loopback normalization when api_base is unset.</violation>
</file>

<file name="apps/backend/app/routers/config.py">

<violation number="1" location="apps/backend/app/routers/config.py:255">
P2: `/provider-models` ignores provider-specific API keys stored in `config["api_keys"]` and always uses the legacy global `api_key`. This breaks multi-provider setups and causes wrong-auth/401s when switching providers.</violation>
</file>

<file name="apps/backend/app/services/improver.py">

<violation number="1" location="apps/backend/app/services/improver.py:282">
P2: Call B merges workExperience descriptions by list index even though each entry has an id. If the LLM reorders or inserts entries, descriptions can be written onto the wrong experience record, corrupting resume content.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/backend/app/routers/jobs.py
Comment thread apps/backend/app/routers/jobs.py Outdated
Comment thread apps/backend/app/routers/resumes.py Outdated
Comment thread apps/backend/app/routers/health.py Outdated
Comment thread apps/backend/app/routers/health.py Outdated
Comment thread apps/backend/app/routers/jobs.py Outdated
Comment thread apps/backend/app/services/improver.py Outdated
Comment thread apps/backend/app/routers/resumes.py
Comment thread apps/backend/app/routers/resumes.py Outdated
Comment thread apps/backend/app/routers/resumes.py Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

24 issues found across 100 files (changes from recent commits).

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".claude/skills/brainstorming/scripts/helper.js">

<violation number="1" location=".claude/skills/brainstorming/scripts/helper.js:2">
P2: Custom agent: **Flag Security Vulnerabilities**

WebSocket URL falls back to insecure `ws://` when not on HTTPS, violating the requirement that network requests use HTTPS/TLS.</violation>
</file>

<file name="apps/frontend/app/(default)/tailor/page.tsx">

<violation number="1" location="apps/frontend/app/(default)/tailor/page.tsx:156">
P2: Custom agent: **React Performance and Best Practices**

Unused `elapsed` state is updated every second, forcing unnecessary re-renders during generate/regenerate without affecting UI or logic.</violation>
</file>

<file name="apps/backend/app/services/parser.py">

<violation number="1" location="apps/backend/app/services/parser.py:61">
P2: Month restoration fails for ongoing ranges like "2021 - Present" because the lookup key only uses numeric years. When markdown has "May 2021 - Present", the key becomes "2021", so parsed entries with "2021 - Present" never match and months are not restored for current roles.</violation>
</file>

<file name="apps/frontend/messages/pt-BR.json">

<violation number="1" location="apps/frontend/messages/pt-BR.json:331">
P2: Date placeholder uses Portuguese month abbreviations, but resume date strings must use English month abbreviations ('Mon YYYY') for downstream parsing.</violation>
</file>

<file name="apps/frontend/messages/ja.json">

<violation number="1" location="apps/frontend/messages/ja.json:297">
P2: Japanese date placeholders now use numeric month text (e.g., "2020年1月 - 現在"), but the app requires English month abbreviations in "Mon YYYY" format; this placeholder will drive invalid input for parsers/normalizers.</violation>

<violation number="2" location="apps/frontend/messages/ja.json:331">
P2: Education placeholder uses Japanese numeric month formatting instead of the required English month abbreviations (Mon YYYY), which can lead to incompatible date parsing.</violation>

<violation number="3" location="apps/frontend/messages/ja.json:346">
P2: Project date placeholder uses a Japanese numeric month format, but date strings must use English month abbreviations ('Mon YYYY') for parser compatibility.</violation>
</file>

<file name="apps/frontend/lib/api/client.ts">

<violation number="1" location="apps/frontend/lib/api/client.ts:65">
P2: apiFetch always overrides any caller-provided AbortSignal, so external cancellation no longer works for callers that pass `options.signal`.</violation>
</file>

<file name="apps/backend/app/config_cache.py">

<violation number="1" location="apps/backend/app/config_cache.py:45">
P2: Cache TTL is bypassed when the cached config is an empty dict because the cache hit check relies on `_config_cache` truthiness. This causes repeated disk reads/logging even within the TTL when config.json is empty or missing.</violation>
</file>

<file name=".claude/skills/brainstorming/scripts/start-server.sh">

<violation number="1" location=".claude/skills/brainstorming/scripts/start-server.sh:125">
P2: Fixed 5-second readiness timeout can report failure while leaving a detached server process running.</violation>
</file>

<file name="apps/frontend/messages/es.json">

<violation number="1" location="apps/frontend/messages/es.json:297">
P2: Resume date placeholders now use Spanish month abbreviations (e.g., "Ene", "Ago"), but the parser expects English 'Mon YYYY' format only. Users following these placeholders can enter dates that won’t parse correctly.</violation>
</file>

<file name=".claude/skills/brainstorming/scripts/stop-server.sh">

<violation number="1" location=".claude/skills/brainstorming/scripts/stop-server.sh:24">
P2: Prefix-only check for /tmp can be bypassed with `..` or symlinks, allowing `rm -rf` to delete paths outside /tmp.</violation>
</file>

<file name="apps/backend/tests/conftest.py">

<violation number="1" location="apps/backend/tests/conftest.py:57">
P2: Education years uses numeric range instead of required 'Mon YYYY' format, which can break date validation/regex expectations for ResumeData fixtures.</violation>
</file>

<file name=".claude/skills/systematic-debugging/find-polluter.sh">

<violation number="1" location=".claude/skills/systematic-debugging/find-polluter.sh:29">
P2: `find -path` won’t match the example glob pattern (`src/**/*.test.ts`) because it matches `./`-prefixed paths and doesn’t support globstar semantics. This can incorrectly report “No test files found” even when tests exist.</violation>

<violation number="2" location=".claude/skills/systematic-debugging/find-polluter.sh:40">
P2: Unquoted iteration over `$TEST_FILES` causes word-splitting, breaking test paths containing whitespace.</violation>
</file>

<file name="apps/frontend/messages/zh.json">

<violation number="1" location="apps/frontend/messages/zh.json:297">
P2: Chinese numeric date placeholders now suggest formats the parser doesn’t recognize; date parsing/normalization only matches English month names/abbreviations, so these placeholders can lead to unparseable input.</violation>
</file>

<file name="apps/backend/app/routers/config.py">

<violation number="1" location="apps/backend/app/routers/config.py:303">
P2: Provider model discovery uses legacy `api_key` instead of provider-specific stored keys, so `/config/provider-models` can call provider APIs with the wrong or empty key.</violation>
</file>

<file name="apps/backend/app/services/refiner.py">

<violation number="1" location="apps/backend/app/services/refiner.py:299">
P2: Word-boundary matching in _keyword_in_text fails for skills with non-word characters (e.g., C++, C#, Node.js). The new alignment check uses this helper to decide whether to flag a skill as fabricated; false negatives will remove legitimate skills.</violation>
</file>

<file name="apps/backend/app/prompts/templates.py">

<violation number="1" location="apps/backend/app/prompts/templates.py:169">
P1: Prompt date rules preserve source month text without enforcing English month abbreviations, conflicting with English-only month parsing logic downstream.</violation>
</file>

<file name=".claude/skills/brainstorming/scripts/server.js">

<violation number="1" location=".claude/skills/brainstorming/scripts/server.js:66">
P1: decodeFrame allocates a buffer based solely on the client-provided payload length without enforcing a maximum frame size, allowing a large frame to exhaust memory and crash the server.</violation>

<violation number="2" location=".claude/skills/brainstorming/scripts/server.js:281">
P2: knownFiles is never updated on deletions, so re-created screen files with the same name are treated as updates and won’t reset .events.</violation>
</file>

<file name="apps/backend/app/routers/resumes.py">

<violation number="1">
P2: Preview hash is computed from raw LLM output while confirm hashes the Pydantic-normalized payload, so defaults/coercions can change the data and cause valid confirmations to be rejected due to hash mismatch.</violation>

<violation number="2" location="apps/backend/app/routers/resumes.py:716">
P2: Factual-field restoration for education/work history was removed; after LLM output only personalInfo is preserved, so company/title/years and education fields can now be mutated and persisted without correction.</violation>
</file>

<file name="apps/backend/app/routers/enrichment.py">

<violation number="1" location="apps/backend/app/routers/enrichment.py:63">
P2: `_extract_item_from_resume` can crash on malformed list elements because it calls `.get()` on `entries[index]` without verifying the entry is a dict.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/backend/app/prompts/templates.py Outdated
if (buffer.length < totalLen) return null;

const mask = buffer.slice(maskOffset, dataOffset);
const data = Buffer.alloc(payloadLen);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: decodeFrame allocates a buffer based solely on the client-provided payload length without enforcing a maximum frame size, allowing a large frame to exhaust memory and crash the server.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .claude/skills/brainstorming/scripts/server.js, line 66:

<comment>decodeFrame allocates a buffer based solely on the client-provided payload length without enforcing a maximum frame size, allowing a large frame to exhaust memory and crash the server.</comment>

<file context>
@@ -0,0 +1,338 @@
+  if (buffer.length < totalLen) return null;
+
+  const mask = buffer.slice(maskOffset, dataOffset);
+  const data = Buffer.alloc(payloadLen);
+  for (let i = 0; i < payloadLen; i++) {
+    data[i] = buffer[dataOffset + i] ^ mask[i % 4];
</file context>
Fix with Cubic

Comment thread .claude/skills/brainstorming/scripts/helper.js
Comment thread apps/frontend/app/(default)/tailor/page.tsx Outdated
Comment thread apps/backend/app/services/parser.py
Comment thread apps/backend/app/services/refiner.py
debounceTimers.delete(filename);
const filePath = path.join(SCREEN_DIR, filename);

if (!fs.existsSync(filePath)) return; // file was deleted
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: knownFiles is never updated on deletions, so re-created screen files with the same name are treated as updates and won’t reset .events.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .claude/skills/brainstorming/scripts/server.js, line 281:

<comment>knownFiles is never updated on deletions, so re-created screen files with the same name are treated as updates and won’t reset .events.</comment>

<file context>
@@ -0,0 +1,338 @@
+      debounceTimers.delete(filename);
+      const filePath = path.join(SCREEN_DIR, filename);
+
+      if (!fs.existsSync(filePath)) return; // file was deleted
+      touchActivity();
+
</file context>
Fix with Cubic

Comment thread apps/backend/app/routers/resumes.py
Comment thread apps/backend/app/routers/resumes.py
Comment thread apps/backend/app/routers/enrichment.py
# Conflicts:
#	apps/backend/app/llm.py
#	apps/backend/app/prompts/templates.py
#	apps/backend/app/routers/resumes.py
#	apps/backend/app/services/improver.py
#	apps/frontend/app/(default)/tailor/page.tsx
#	apps/frontend/components/tailor/diff-preview-modal.tsx
#	apps/frontend/next.config.ts
Comment thread apps/backend/app/routers/resumes.py Outdated
return f"data: {json.dumps(payload)}\n\n"


def _load_config() -> dict:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Local function shadows imported alias

This _load_config() function shadows the load_config as _load_config imported from config_cache at line 17. The imported version provides 5-minute TTL caching, but this local version reads directly from disk on every call.

Impact:

  • The import at line 17 becomes dead code
  • _load_feature_config() and _get_content_language() below use this uncached version
  • Config is read from disk on every request instead of using the cache

Recommended fix: Remove lines 73-94 (local _load_config, _load_feature_config, and _get_content_language functions) and use the imported versions consistently:

# At line 17, change to:
from app.config_cache import get_content_language, load_config

# Then use load_config() and get_content_language() throughout

Comment thread apps/backend/app/routers/resumes.py Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/app/services/refiner.py">

<violation number="1" location="apps/backend/app/services/refiner.py:55">
P1: ASCII-only boundary checks regress Unicode word-boundary matching and can produce false positives in non-English text.</violation>
</file>

<file name="apps/backend/app/services/parser.py">

<violation number="1" location="apps/backend/app/services/parser.py:70">
P2: Ongoing date restoration is incomplete: only `"YYYY - Present"` is mapped, so other accepted ongoing tokens/casing fail exact lookup and months are not restored.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/backend/app/services/refiner.py Outdated
Comment thread apps/backend/app/services/parser.py
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/app/routers/health.py">

<violation number="1" location="apps/backend/app/routers/health.py:32">
P2: Global cache lock is held across awaited health check I/O, serializing concurrent `/status` requests and causing head-of-line blocking.</violation>
</file>

<file name="apps/backend/app/routers/config.py">

<violation number="1" location="apps/backend/app/routers/config.py:267">
P1: New API-key guard can incorrectly reject valid provider configs because it checks only legacy `api_key` instead of provider-resolved keys.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/backend/app/routers/config.py
Comment thread apps/backend/app/routers/health.py Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/app/routers/health.py">

<violation number="1" location="apps/backend/app/routers/health.py:39">
P2: Cache stampede risk: moving health check outside the lock allows concurrent `/status` requests to trigger duplicate expensive LLM health checks when cache is cold/expired.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/backend/app/routers/health.py
async def _run_health_check(cache_key: str, config) -> dict: # type: ignore[no-untyped-def]
"""Execute a health check, store the result, and remove the in-flight marker."""
try:
result = await asyncio.wait_for(check_llm_health(config), timeout=60.0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: Task cancellation leaves stale entry in _health_inflight

If the task is cancelled (e.g., client disconnect, server shutdown), asyncio.CancelledError propagates from this await, skipping the cleanup in the async with _health_cache_lock block below. The cancelled task remains in _health_inflight, causing all subsequent requests to await a cancelled task that immediately raises CancelledError.

Consider handling CancelledError explicitly to clean up _health_inflight:

async def _run_health_check(cache_key: str, config) -> dict:
    try:
        result = await asyncio.wait_for(check_llm_health(config), timeout=60.0)
    except asyncio.CancelledError:
        async with _health_cache_lock:
            _health_inflight.pop(cache_key, None)
        raise
    except Exception as e:
        logger.warning("LLM health check failed: %s", e)
        result = {"healthy": False, "error": "health check timed out or failed"}
    async with _health_cache_lock:
        _health_cache[cache_key] = {"result": result, "ts": time.monotonic()}
        _health_inflight.pop(cache_key, None)
    return result

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/app/routers/health.py">

<violation number="1" location="apps/backend/app/routers/health.py:56">
P1: Shared in-flight health task can be cancelled by a requester and left poisoned in `_health_inflight`, causing repeated `/status` failures.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if task is None:
task = asyncio.ensure_future(_run_health_check(cache_key, config))
_health_inflight[cache_key] = task
return await task
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Shared in-flight health task can be cancelled by a requester and left poisoned in _health_inflight, causing repeated /status failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/health.py, line 56:

<comment>Shared in-flight health task can be cancelled by a requester and left poisoned in `_health_inflight`, causing repeated `/status` failures.</comment>

<file context>
@@ -16,33 +16,44 @@
+        if task is None:
+            task = asyncio.ensure_future(_run_health_check(cache_key, config))
+            _health_inflight[cache_key] = task
+    return await task
 
 
</file context>
Suggested change
return await task
return await asyncio.shield(task)
Fix with Cubic

@srbhr
Copy link
Copy Markdown
Owner

srbhr commented Apr 1, 2026

Code Review Report — PR #723: Tailor Improvements v2

Reviewer: @srbhr (via automated code review)
Date: 2026-04-01
Status: Changes requested — not merging until issues are addressed.


Summary

Severity Backend Frontend Total
CRITICAL 2 2 4
HIGH 6 4 10
MEDIUM 8 5 13
LOW 9 5 14

CRITICAL (must fix before merge)

C1. SSRF bypass via IPv6 — apps/backend/app/routers/jobs.py

_is_private_host() uses socket.gethostbyname() which only resolves IPv4. Attackers can bypass with IPv6 literals (e.g. http://[::1]/) or IPv4-mapped IPv6 (http://[::ffff:127.0.0.1]/). Also vulnerable to DNS rebinding (TOCTOU — resolve happens before httpx connects).

Fix: Use socket.getaddrinfo() to cover both IPv4/IPv6, and consider pinning the resolved IP for the actual request.

C2. SSRF via Playwright — apps/backend/app/routers/jobs.py

The Playwright path (_run_playwright_sync) does NOT go through the _is_private_host() check. A domain matching a SITE_MAP entry could redirect Playwright to a private IP. The .endswith() subdomain matching also widens the attack surface.

Fix: Run _is_private_host() before the Playwright path too.

C3. Missing i18n key — apps/frontend/app/(default)/tailor/page.tsx

t('tailor.errors.networkError') is referenced but never added to any of the 5 locale files (en, es, ja, pt-BR, zh). Users will see the raw key string at runtime.

Fix: Add tailor.errors.networkError to all locale files.

C4. Stale acceptedIndices on modal reopen — apps/frontend/components/tailor/diff-preview-modal.tsx

useState initializer runs only once. If the modal reopens with new detailedChanges, the checkbox state reflects old data.

Fix: Add a useEffect to reset acceptedIndices when detailedChanges changes:

useEffect(() => {
  setAcceptedIndices(new Set(detailedChanges?.map((_, i) => i) ?? []));
}, [detailedChanges]);

HIGH (should fix before merge)

H1. Missing type hints — apps/backend/app/routers/health.py

_run_health_check and _get_cached_llm_health use config with no type annotation and # type: ignore. All Python functions must have type hints per project rules.

H2. Internal URL leak in error response — apps/backend/app/routers/config.py

get_ollama_models leaks api_base (potentially an internal address) to the client in the ConnectError handler. Per project rules: log details server-side, return generic messages to clients.

H3. Error message leak — apps/backend/app/routers/jobs.py

httpx.RequestError exception is included in the 502 response detail. Could expose internal network info (resolved IPs, hostnames from redirects).

H4. No URL validation on FetchJobUrlRequestapps/backend/app/schemas/models.py

The url field is a bare str with no length limit or format validation. Add Field(max_length=2048) or use Pydantic's HttpUrl.

H5. Duplicate docstring — apps/backend/app/services/improver.py

_check_for_truncation docstring has duplicate paragraphs about personalInfo. Also claims "Raises ValueError" but the function only logs warnings.

H6. workflow_mode and partial_confirm fields unused — apps/backend/app/schemas/models.py

These fields are accepted in the request schema but never read by any backend code. Dead fields.

H7. No SSE abort on unmount — apps/frontend/lib/api/resume.ts

AbortController is internal to previewImproveResumeStream. The caller cannot cancel the stream on navigation/unmount. Zombie streams can run for up to 30 minutes.

Fix: Accept an optional AbortSignal parameter and link it to the internal controller.

H8. proxyTimeout removal — apps/frontend/next.config.ts

Removing proxyTimeout: 240_000 may break non-streaming paths for slow models (e.g. local Ollama). The default proxy timeout could cut connections before the backend responds.

H9. Hardcoded English strings — apps/frontend/components/ui/dropdown.tsx and settings/page.tsx

"Search models...", "No models found", "Fetch models", "Loading..." bypass the i18n system. Will display in English for all locales.

H10. O(n²) indexOf lookups — apps/frontend/components/tailor/diff-preview-modal.tsx

Every change item in every section calls detailedChanges.indexOf(change) for a linear scan. Use a Map computed once via useMemo.


MEDIUM

# File Issue
M1 jobs.py No response size limit on fetched HTML — a malicious URL could serve 100MB+ and consume server memory
M2 jobs.py _LLM_FILTER_PROMPT concatenates raw web page text without sanitization — prompt injection risk
M3 jobs.py Playwright ThreadPoolExecutor(max_workers=2) has no queue depth limit — potential DoS under burst
M4 jobs.py _SmartExtractor._in_skip() scans entire stack per character event — O(depth × nodes)
M5 health.py asyncio.ensure_future() is deprecated — use asyncio.create_task()
M6 config.py _ANTHROPIC_MODELS list is hardcoded and will become stale
M7 config.py Error message echoes raw provider user input back in response
M8 jobs.py _apply_site_cleanup never reads trim_before_last config key — dead config
M9 tailor/page.tsx Frontend enforces HTTPS-only but backend accepts HTTP — inconsistent validation
M10 tailor/page.tsx JSON.parse(JSON.stringify()) for deep clone — structuredClone() is more robust
M11 tailor/page.tsx revertChange array removal via indexOf may remove wrong duplicate entry
M12 config.ts provider param not URL-encoded in API fetch call
M13 tests/diff-preview-modal.test.tsx No new tests for checkbox toggle, select all/none, partial confirm, disabled confirm button

LOW

# File Issue
L1 refiner.py Regex compiled inside _keyword_in_text() on every call — move to module level
L2 parser.py _ongoing_re compiled inside function body — move to module level
L3 resumes.py generate() return type is None but function is an AsyncGenerator
L4 resumes.py Inner function named generate could shadow imports
L5 llm.py / config.py Ollama URL "http://127.0.0.1:11434" hardcoded in 4+ places — extract to constant
L6 tailor/page.tsx handleConfirmPartial duplicates most of handleConfirmChanges — extract common logic
L7 tailor/page.tsx result as unknown as ResumeData double cast bypasses type checking after mutations
L8 dropdown.tsx searchable prop is backwards-compatible, no issues
L9 tailor/page.tsx Timer/interval cleanup is correctly implemented

What's Good

  • SSE streaming implementation is well-structured with proper buffer parsing
  • Timer cleanup and useEffect teardown are correct
  • SITE_MAP approach for known job boards is solid
  • Fallback chain (CSS selectors → Playwright → LLM filter) is reasonable
  • Dropdown searchable prop is backwards-compatible

Recommendation: Fix the 4 CRITICAL issues and the HIGH-severity security items (H2, H3, H4) before this can be merged. The SSRF issues in particular need attention since this PR introduces a URL-fetching endpoint that accepts user-provided URLs.

@srbhr
Copy link
Copy Markdown
Owner

srbhr commented Apr 1, 2026

@Paranoidal97 I think we need to split up the PR into smaller chunks.

And maybe introduce a plugin ecosystem that people can use to perform more actions on top of this.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 14 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/app/routers/config.py">

<violation number="1" location="apps/backend/app/routers/config.py:211">
P2: `logger` is not defined in this module; this will raise `NameError` in the ConnectError handler and mask the intended 503 response.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

model_names: list[str] = [m["name"] for m in data.get("models", [])]
return {"models": sorted(model_names)}
except httpx.ConnectError:
logger.warning("Cannot connect to Ollama at %s", api_base)
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: logger is not defined in this module; this will raise NameError in the ConnectError handler and mask the intended 503 response.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/config.py, line 211:

<comment>`logger` is not defined in this module; this will raise `NameError` in the ConnectError handler and mask the intended 503 response.</comment>

<file context>
@@ -208,9 +208,10 @@ async def get_ollama_models() -> dict:
             model_names: list[str] = [m["name"] for m in data.get("models", [])]
             return {"models": sorted(model_names)}
     except httpx.ConnectError:
+        logger.warning("Cannot connect to Ollama at %s", api_base)
         raise HTTPException(
             status_code=503,
</file context>
Suggested change
logger.warning("Cannot connect to Ollama at %s", api_base)
logging.warning("Cannot connect to Ollama at %s", api_base)
Fix with Cubic

@Paranoidal97
Copy link
Copy Markdown
Author

@srbhr What specific chunks would you like to break it down into?

Regarding point 2, “Plugin ecosystem,” could you elaborate on what potential extensions you see here? Personally, I see two potential extensions at the moment

  • Custom prompt plugins — users could add their own prompt templates
  • Post-processing hooks — after generating a tailored resume, run a plugin (e.g., export to LinkedIn, ATS
    checker, formatting)

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