feat: Chrome extension for one-click resume tailoring#821
feat: Chrome extension for one-click resume tailoring#821gingeekrishna wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a Chrome extension popup UI to detect job descriptions from common job boards and call the backend to upload jobs/tailor resumes, plus backend CORS tweaks to allow calls from the extension.
Changes:
- Introduces MV3 Chrome extension popup (HTML/CSS/JS) with settings, resume upload, job extraction, and results UI.
- Adds extension manifest with required permissions/host permissions.
- Updates backend CORS middleware to accept
chrome-extension://origins.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/chrome-extension/popup/popup.js | Implements popup state, job extraction via chrome.scripting, backend calls, and UI rendering |
| apps/chrome-extension/popup/popup.html | Adds popup layout for settings, main flow, and results |
| apps/chrome-extension/popup/popup.css | Styles popup UI and loading overlay |
| apps/chrome-extension/manifest.json | Defines MV3 extension permissions and popup entrypoint |
| apps/backend/app/base.py | Allows CORS requests originating from Chrome extensions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=settings.ALLOWED_ORIGINS, | ||
| allow_origin_regex=r"chrome-extension://.*", | ||
| allow_credentials=True, | ||
| allow_methods=["*"], | ||
| allow_headers=["*"], |
| "permissions": ["activeTab", "storage", "scripting"], | ||
| "host_permissions": [ | ||
| "http://localhost/*", | ||
| "http://127.0.0.1/*" | ||
| ], |
| ats.missing_keywords.forEach((kw, i) => { | ||
| const chip = document.createElement('span'); | ||
| chip.className = 'chip'; | ||
| chip.textContent = kw; | ||
| chip.setAttribute('key', `kw-${i}`); | ||
| keywordsChips.appendChild(chip); | ||
| }); |
| <label>Resume</label> | ||
| <p class="hint">Upload your master resume once, or paste an existing Resume ID.</p> | ||
| <input id="input-resume-file" type="file" accept=".pdf,.docx" /> | ||
| <button id="btn-upload-resume">Upload Resume</button> |
| <body> | ||
| <header> | ||
| <span class="logo">Resume Matcher</span> | ||
| <button id="btn-settings-toggle" aria-label="Settings">⚙</button> |
| <label>Resume</label> | ||
| <p class="hint">Upload your master resume once, or paste an existing Resume ID.</p> | ||
| <input id="input-resume-file" type="file" accept=".pdf,.docx" /> | ||
| <button id="btn-upload-resume">Upload Resume</button> |
|
|
||
| <label for="input-resume-id">Resume ID</label> | ||
| <input id="input-resume-id" type="text" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" /> | ||
| <button id="btn-save-settings">Save</button> |
| <span id="badge-resume-id" class="mono"></span> | ||
| </div> | ||
|
|
||
| <button id="btn-tailor" disabled>Tailor Resume</button> |
| <button id="btn-open-app">Open in Resume Matcher</button> | ||
| <button id="btn-tailor-again" class="secondary">Tailor Another</button> |
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=settings.ALLOWED_ORIGINS, | ||
| allow_origin_regex=r"chrome-extension://.*", |
There was a problem hiding this comment.
CRITICAL: Overly permissive CORS regex allows ANY Chrome extension to access the backend
allow_origin_regex=r"chrome-extension://.*" matches ALL Chrome extension origins. Any malicious extension installed by a user could make authenticated requests to this backend with allow_credentials=True.
Recommend using a specific extension ID pattern: r"chrome-extension://<your-extension-id>" or moving the extension ID to settings/config.
| ? job.description.slice(0, 280) + (job.description.length > 280 ? '…' : '') | ||
| : ''; | ||
| msgNoJob.hidden = Boolean(hasContent); | ||
| } |
There was a problem hiding this comment.
WARNING: No timeout on fetch requests - backend hangs will freeze the UI indefinitely
All fetch() calls in apiPost and apiUploadFile lack a timeout mechanism. If the backend becomes unresponsive, the extension popup will hang forever.
Consider wrapping fetch with AbortController and a timeout (e.g., 30 seconds).
| resultKeywords.hidden = true; | ||
| } | ||
|
|
||
| if (ats?.recommendations?.length) { |
There was a problem hiding this comment.
WARNING: No file type or size validation before upload
apiUploadFile accepts any file from the file input. Despite accept=".pdf,.docx" in the HTML (which is only a UI hint, not enforced), users could select any file type.
Validate file.type or check the file extension before sending to the backend.
| const jobId = jobRes.job_id; | ||
|
|
||
| setLoading(true, 'Tailoring resume…'); | ||
| const improveRes = await apiPost('/api/v1/resumes/improve', { |
There was a problem hiding this comment.
WARNING: Partial failure state - job created but improve fails leaves dangling data
If apiPost('/api/v1/jobs/upload') succeeds but the subsequent apiPost('/api/v1/resumes/improve') fails, a job is created on the backend but the user never sees results. Consider adding cleanup logic or at least a clearer error message indicating the job was created.
| } catch { | ||
| chrome.tabs.create({ url: 'http://localhost:3000' }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
SUGGESTION: btnTailorAgain doesn't clear state.jobData - stale job info may display
Clicking "Tailor Another" returns to the main panel but state.jobData retains the previous job. The UI will show the old job title/company/description until a new job is extracted.
Consider adding state.jobData = null; renderJobData(null); here.
| if (tab?.id) { | ||
| const [result] = await chrome.scripting.executeScript({ | ||
| target: { tabId: tab.id }, | ||
| func: extractJobFromPage, |
There was a problem hiding this comment.
SUGGESTION: Silent error catch in init() - extraction failures are invisible
The catch {} block silently swallows all errors during job extraction. If chrome.scripting.executeScript fails for a non-internal-page reason (e.g., content blocked, tab closed), the user sees no feedback.
Consider logging to console or showing a subtle hint when extraction fails for unexpected reasons.
| const el = document.querySelector(sel); | ||
| if (el && el.innerText && el.innerText.trim()) return el.innerText.trim(); | ||
| } catch (_) { | ||
| // ignore invalid selectors |
There was a problem hiding this comment.
WARNING: No content size limit - very long job descriptions could exceed backend/API limits
getText() extracts ALL innerText from description containers. Job descriptions on some sites can be 10,000+ characters. Consider truncating description before storing in state.jobData or before sending to the backend.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Incremental ChangesSince the previous review (commit
Resolved Since Last Review
Previously Resolved (from earlier commits)
Files Reviewed (1 file)
Previous Review Summaries (3 snapshots, latest commit 0cbef79)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 0cbef79)Status: 1 Issue Found | Recommendation: Address before merge Overview
Incremental ChangesSince the previous review, 1 commit was added:
Resolved Since Last Review
Issue Details (click to expand)WARNING
Files Reviewed (6 files)
Fix these issues in Kilo Cloud Previous review (commit 78c8c1a)Status: 7 Issues Found (1 CRITICAL, 4 WARNING, 2 SUGGESTION) | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Resolved Since Last Review
Other Observations (not in diff)
Files Reviewed (5 files)
Incremental ChangesSince the last review, 1 commit was added:
Fix these issues in Kilo Cloud Previous review (commit ee9b8ec)Status: 7 Issues Found (1 CRITICAL, 4 WARNING, 2 SUGGESTION) | Recommendation: Address before merge Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Other Observations (not in diff)Issues found in unchanged code or structural concerns that cannot receive inline comments:
Files Reviewed (5 files)
Reviewed by qwen3.7-plus-20260602 · Input: 4.8K · Output: 4K · Cached: 239.7K |
There was a problem hiding this comment.
7 issues found across 5 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/base.py">
<violation number="1" location="apps/backend/app/base.py:49">
P1: Custom agent: **Flag Security Vulnerabilities**
CORS configuration trusts every chrome-extension:// origin, creating an overly broad cross-origin access surface with credentials enabled</violation>
</file>
<file name="apps/chrome-extension/manifest.json">
<violation number="1" location="apps/chrome-extension/manifest.json:8">
P1: The extension allows users to configure a custom `backendUrl` in settings, but `host_permissions` only grants access to `localhost` and `127.0.0.1`. In Manifest V3, `fetch()` from extension pages to origins not listed in `host_permissions` will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=settings.ALLOWED_ORIGINS, | ||
| allow_origin_regex=r"chrome-extension://.*", |
There was a problem hiding this comment.
P1: Custom agent: Flag Security Vulnerabilities
CORS configuration trusts every chrome-extension:// origin, creating an overly broad cross-origin access surface with credentials enabled
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/base.py, line 49:
<comment>CORS configuration trusts every chrome-extension:// origin, creating an overly broad cross-origin access surface with credentials enabled</comment>
<file context>
@@ -46,6 +46,7 @@ def create_app() -> FastAPI:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
+ allow_origin_regex=r"chrome-extension://.*",
allow_credentials=True,
allow_methods=["*"],
</file context>
| @@ -0,0 +1,15 @@ | |||
| { | |||
There was a problem hiding this comment.
P1: The extension allows users to configure a custom backendUrl in settings, but host_permissions only grants access to localhost and 127.0.0.1. In Manifest V3, fetch() from extension pages to origins not listed in host_permissions will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/chrome-extension/manifest.json, line 8:
<comment>The extension allows users to configure a custom `backendUrl` in settings, but `host_permissions` only grants access to `localhost` and `127.0.0.1`. In Manifest V3, `fetch()` from extension pages to origins not listed in `host_permissions` will be blocked by the browser. Either restrict the settings UI to only accept localhost URLs, or document that users must modify the manifest for non-local backends.</comment>
<file context>
@@ -0,0 +1,15 @@
+ "description": "Detect job descriptions on job boards and tailor your resume in one click.",
+ "permissions": ["activeTab", "storage", "scripting"],
+ "host_permissions": [
+ "http://localhost/*",
+ "http://127.0.0.1/*"
+ ],
</file context>
f0ad45b to
55f56f8
Compare
| # Restrict to a specific extension ID (e.g. "chrome-extension://abcdef...") via | ||
| # EXTENSION_CORS_ORIGIN env var. Defaults to allowing any local extension for | ||
| # developer convenience; set an exact ID in shared / production environments. | ||
| extension_cors_origin: str = r"chrome-extension://.*" |
There was a problem hiding this comment.
CRITICAL: Overly permissive CORS regex allows ANY Chrome extension to access the backend with credentials
The default r"chrome-extension://.*" matches ANY Chrome extension origin, not just this specific extension. Combined with allow_credentials=True, this means any malicious Chrome extension installed by the user can make authenticated requests to your backend (including resume uploads, improvements, etc.).
The comment says to set EXTENSION_CORS_ORIGIN for production, but the default is insecure for ANY shared environment. The default should be empty/disabled ("" or None) and developers should explicitly enable it with their extension ID.
|
|
||
| const resultData = improveRes.data ?? improveRes; | ||
| panelMain.hidden = true; | ||
| panelResult.hidden = false; |
There was a problem hiding this comment.
WARNING: Partial failure state - job created but improve fails leaves dangling data
If apiPost('/api/v1/resumes/improve', ...) fails, the job (created via apiPost('/api/v1/jobs/upload', ...)) is never cleaned up. This leaves orphaned job records in the database. The jobId is captured but there's no cleanup attempt on failure.
Consider adding a delete/cleanup call in the catch block or documenting this as expected behavior.
|
|
||
| btnOpenApp.addEventListener('click', () => { | ||
| try { | ||
| const parsed = new URL(state.backendUrl); |
There was a problem hiding this comment.
WARNING: btnOpenApp hardcodes port :3000 for frontend URL - will break in production
If state.backendUrl is http://api.example.com (no port), this creates http://api.example.com:3000 which is likely incorrect. If the frontend runs on a different port (e.g., localhost:3001 for dev), this opens the wrong URL.
Consider making the frontend URL configurable in settings or inferring it from the backend URL more intelligently.
| btnTailorAgain.addEventListener('click', () => { | ||
| panelResult.hidden = true; | ||
| panelMain.hidden = false; | ||
| // Re-extract job from the active tab so stale job data doesn't linger |
There was a problem hiding this comment.
WARNING: Silent error catch in btnTailorAgain - extraction failures are invisible
The .catch(() => {}) completely swallows any errors during job re-extraction. If executeScript fails (e.g., tab navigated away, permissions issue), the user sees no feedback and state.jobData may be left in an inconsistent state.
At minimum, log the error or show a message to the user.
55f56f8 to
23a3df3
Compare
Adds a Manifest V3 Chrome extension that detects job descriptions on job boards and tailors the user's resume in one click, with no copy-pasting. Extension (apps/chrome-extension/): - manifest.json: MV3; permissions: activeTab, storage, scripting; host_permissions limited to http://localhost/* and http://127.0.0.1/* - popup.html: all buttons carry type="button"; <label for> properly associates the Resume label with the file input for accessibility - popup.css: flat Swiss International Style palette (#F0F0E8 canvas, 1 px black borders, Signal Green / Alert Red / Alert Orange) - popup.js: - fetchWithTimeout() wraps every fetch with AbortController (120 s) so LLM-backed calls don't hang the popup indefinitely; AbortError surfaces as a clear 'Request timed out' message - apiUploadFile() validates file.type against the PDF/DOCX allowlist before sending, enforcing what accept='.pdf,.docx' only hints at - Settings: backend URL is validated (must be http/https and localhost/127.0.0.1) matching manifest host_permissions scope - Job description truncated to MAX_JD_CHARS=8000 before upload - Partial-failure messaging: if the job upload succeeds but the improve call fails, the error includes the created job ID so it can be referenced or cleaned up manually - btnOpenApp derives the frontend URL from backendUrl hostname instead of hardcoding ':3000' on an arbitrary base - btnTailorAgain re-extracts the active tab instead of reusing stale job data; catch block logs via console.warn instead of swallowing the error silently - init() catch distinguishes expected 'Cannot access' errors (e.g. chrome:// pages) from unexpected ones and warns for the latter Backend (apps/backend/): - config.py: extension_cors_origin defaults to None; opt-in by setting EXTENSION_CORS_ORIGIN env var to the extension ID (or a regex for local dev); shared/server deployments are unaffected by default - main.py: allow_origin_regex is only added to CORSMiddleware when extension_cors_origin is explicitly set and non-empty
ee9b8ec to
3545c71
Compare
|
Here is a summary of every issue raised before and how it was addressed: CORS / Security (Copilot, kilo-code-bot, cubic P1)
host_permissions vs custom backend URL (Copilot, cubic P1)
Fetch timeout (kilo-code-bot, cubic P2)
File type validation (kilo-code-bot, cubic P2)
Job description size limit (kilo-code-bot)
Partial failure state (kilo-code-bot)
btnTailorAgain stale job data / silent catch (kilo-code-bot)
init() silent extraction catch (kilo-code-bot, cubic P2)
btnOpenApp hardcoded port (kilo-code-bot)
Buttons missing
Label not associated with file input (Copilot)
Commit history
|
| updateTailorBtn(); | ||
| }) | ||
| .catch((e) => { | ||
| console.warn('[Resume Matcher] Re-extraction failed:', e.message); |
There was a problem hiding this comment.
SUGGESTION: Re-extraction failure still provides no user-visible feedback
The console.warn is a good improvement over the previous silent catch, but when re-extraction fails the user sees the main panel with stale job data and no indication anything went wrong. Consider adding a showMsg call so the user knows their job data may be outdated:
.catch((e) => {
console.warn('[Resume Matcher] Re-extraction failed:', e.message);
showMsg(msgMain, 'Could not re-extract job details. Showing previous data.', 'error');
});Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| updateTailorBtn(); | ||
| }) | ||
| .catch((e) => { | ||
| console.warn('[Resume Matcher] Re-extraction failed:', e.message); |
There was a problem hiding this comment.
SUGGESTION: Re-extraction failure still provides no user-visible feedback
The console.warn is a good improvement over the previous silent catch, but when re-extraction fails the user sees the main panel with stale job data and no indication anything went wrong. Consider adding a showMsg call so the user knows their job data may be outdated:
.catch((e) => {
console.warn('[Resume Matcher] Re-extraction failed:', e.message);
showMsg(msgMain, 'Could not re-extract job details. Showing previous data.', 'error');
});Reply with @kilocode-bot fix it to have Kilo Code address this issue.
…smatch - POST /api/v1/jobs/upload returns job_id as list[str]; extract [0] before passing to improve endpoint which expects a single string - Add guard before index: throw if backend returns empty/null job_id - Show user-visible error via showMsg when btnTailorAgain re-extraction fails (previously only logged to console.warn)
0cbef79 to
eed518a
Compare
|
@gingeekrishna I think this project can go into the Resume Matcher organization's projects. |
|
That's exciting to hear, @srbhr! Happy to help with whatever form works best for the org. |
Summary
Closes #772
Adds a Manifest V3 Chrome extension that lets users tailor their resume directly from any job posting page — no copy-pasting required.
What's included
apps/chrome-extension/manifest.json— MV3 extension; permissions:activeTab,storage,scripting; no broad host permissions beyondlocalhostpopup/popup.html— clean popup UI (360 px wide, Swiss International Style palette)popup/popup.css— flat design:#F0F0E8canvas, 1 px black borders, no border radius, Signal Green / Alert Red / Alert Orange for score colourspopup/popup.js— full tailor flow:extractJobFromPage()into the active tab viachrome.scripting.executeScriptto pull job title, company, and description from LinkedIn, Indeed, Greenhouse, Lever, Glassdoor, and WorkdayPOST /api/v1/resumes/upload) or paste an existing Resume ID; persisted tochrome.storage.localPOST /api/v1/jobs/upload→POST /api/v1/resumes/improve:3000for PDF downloadapps/backend/app/base.pyallow_origin_regex=r"chrome-extension://.*"toCORSMiddlewareso the extension popup can reach the local backend without CORS errors (extension IDs are dynamic, making an exact-match list impractical)How to load the extension locally
chrome://extensionsapps/chrome-extension/Test plan
localhost:3000Out of scope (follow-up)
Summary by cubic
Adds a Manifest V3 Chrome extension for one-click resume tailoring and makes backend CORS opt-in via
EXTENSION_CORS_ORIGIN(off by default). Implements #772.New Features
apps/chrome-extension/with popup; permissions:activeTab,storage,scripting;host_permissionslimited tohttp://localhost/*andhttp://127.0.0.1/*; buttons usetype="button"and the Resume label is correctly associated.POST /api/v1/resumes/upload, PDF/DOCX validated) or paste Resume ID; backend URL and Resume ID saved inchrome.storage.local; backend URL must be http/https on localhost/127.0.0.1.POST /api/v1/jobs/upload(JD ≤ 8,000 chars) →POST /api/v1/resumes/improve; shows ATS score, missing keywords, and recommendations; “Open in Resume Matcher” derives the web app URL from the backend host; “Tailor Another” re-extracts the tab and shows an error if it fails.allow_origin_regexwhenEXTENSION_CORS_ORIGINis set; default isNone.Bug Fixes
job_idreturned byPOST /api/v1/jobs/uploadwhen callingPOST /api/v1/resumes/improveto avoid a 422 validation error.Written for commit cbc9df0. Summary will update on new commits.