mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
feat(apply): verify posting source host against installed portals and known ATS apexes (#431) (#467)
- Add source host verification rule to apply.md Step 1 before drafting - Check posting URL host against installed portals and six official ATS apexes - Enforce fail-closed look-alike parsing (prefix, suffix, userinfo spoofing) - Plainly flag unverified third-party hosts in evaluation output - Add tests/test_apply_host_check.py and update CHANGELOG.md
This commit is contained in:
@@ -44,13 +44,29 @@ python salary_lookup.py "<Company Name>" --json
|
|||||||
|
|
||||||
If the posting specifies a city, add `--city "<City>"` to narrow results. Parse the JSON output and include the salary benchmark in the evaluation. If the tool is not configured or returns an error, skip the salary benchmark.
|
If the posting specifies a city, add `--city "<City>"` to narrow results. Parse the JSON output and include the salary benchmark in the evaluation. If the tool is not configured or returns an error, skip the salary benchmark.
|
||||||
|
|
||||||
|
### Source Host Verification (when input is a URL)
|
||||||
|
|
||||||
|
Before proceeding to drafting, inspect the posting URL's hostname to verify provenance (#431). Classify the host into one of three categories:
|
||||||
|
|
||||||
|
1. **Installed portal board:** the host matches any configured job portal in `.agents/skills/` (e.g. `jobindex.dk`, `linkedin.com`, `jobnet.dk`, `jobbank.dk`, `jobdanmark.dk`, `freehire.me`, or any portal added by `/add-portal`).
|
||||||
|
2. **Known official ATS apex:** the host matches or is a valid subdomain of one of the six standard ATS domains:
|
||||||
|
- `greenhouse.io`
|
||||||
|
- `lever.co`
|
||||||
|
- `myworkdayjobs.com` (or `workday.com`)
|
||||||
|
- `ashbyhq.com`
|
||||||
|
- `smartrecruiters.com`
|
||||||
|
- `workable.com`
|
||||||
|
*Look-alike parsing:* the host must match the apex exactly or end with `.<apex>`. Look-alike prefix tricks (e.g. `evil-greenhouse.io`), suffix spoofing (e.g. `job-boards.greenhouse.io.evil.com`), userinfo tricks (`https://greenhouse.io@evil.com/`), and unfamiliar subdomains fail closed and must not be classified as an official ATS.
|
||||||
|
3. **Neither (Unverified host):** name the host plainly in the evaluation output as unverified (`⚠ Unverified source host: <hostname> - not an installed portal board or known ATS apex`). Alert the user to verify the employer and link legitimacy before committing time and tokens to drafting.
|
||||||
|
|
||||||
Present the evaluation to the user with:
|
Present the evaluation to the user with:
|
||||||
|
|
||||||
1. **Skills match** - which required/preferred skills match vs. gaps
|
1. **Source host verification** - installed portal board, official ATS, or ⚠ unverified source host (named plainly)
|
||||||
2. **Experience match** - how work history maps to the role
|
2. **Skills match** - which required/preferred skills match vs. gaps
|
||||||
3. **Behavioral/culture match** - how behavioral profile fits the role/company culture
|
3. **Experience match** - how work history maps to the role
|
||||||
4. **Salary benchmark** - salary index for the company (if available)
|
4. **Behavioral/culture match** - how behavioral profile fits the role/company culture
|
||||||
5. **Overall fit score** and recommendation (strong fit / moderate fit / weak fit)
|
5. **Salary benchmark** - salary index for the company (if available)
|
||||||
|
6. **Overall fit score** and recommendation (strong fit / moderate fit / weak fit)
|
||||||
|
|
||||||
After presenting the evaluation, ask the user:
|
After presenting the evaluation, ask the user:
|
||||||
> "Should I proceed with drafting the CV and cover letter for this role?"
|
> "Should I proceed with drafting the CV and cover letter for this role?"
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Source host verification in `/apply` Step 1** (#431, `.claude/commands/apply.md`,
|
||||||
|
`tests/test_apply_host_check.py`) - before proceeding to draft CV and cover letters,
|
||||||
|
Step 1 verifies the posting URL's provenance against installed portal boards and the
|
||||||
|
six standard ATS apex domains (`greenhouse.io`, `lever.co`, `myworkdayjobs.com`/`workday.com`,
|
||||||
|
`ashbyhq.com`, `smartrecruiters.com`, `workable.com`). Look-alike prefix/suffix spoofing
|
||||||
|
fails closed, and unrecognized hosts are plainly flagged as unverified in the evaluation
|
||||||
|
output (`⚠ Unverified source host: <hostname>`) before drafting tokens are spent.
|
||||||
|
|
||||||
- **`/expand` project and portfolio expansion** (`.claude/commands/expand.md`,
|
- **`/expand` project and portfolio expansion** (`.claude/commands/expand.md`,
|
||||||
`tests/test_expand_command.py`) - expands candidate discovery
|
`tests/test_expand_command.py`) - expands candidate discovery
|
||||||
to technical projects from public GitHub repositories, extracting structured summaries
|
to technical projects from public GitHub repositories, extracting structured summaries
|
||||||
@@ -171,6 +179,7 @@ per-file diff commands.
|
|||||||
Existing state files need no migration: Step 2's candidate filter matches a posting to a stored
|
Existing state files need no migration: Step 2's candidate filter matches a posting to a stored
|
||||||
entry by URL regardless of that entry's key, so a workspace whose entries predate the helper does
|
entry by URL regardless of that entry's key, so a workspace whose entries predate the helper does
|
||||||
not see its still-live postings re-presented as new.
|
not see its still-live postings re-presented as new.
|
||||||
|
|
||||||
## [1.7.1] - 2026-09-06
|
## [1.7.1] - 2026-09-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""Guards for /apply's source host verification rule in Step 1 (#431).
|
||||||
|
|
||||||
|
Pins the invariants from the maintainer design in issue #431:
|
||||||
|
- URLs must be verified before drafting against installed portal boards or known ATS apexes.
|
||||||
|
- The 6 standard ATS apex domains must be checked: greenhouse.io, lever.co,
|
||||||
|
myworkdayjobs.com (or workday.com), ashbyhq.com, smartrecruiters.com, workable.com.
|
||||||
|
- Look-alike attacks (prefixes, suffixes, userinfo tricks) must fail closed.
|
||||||
|
- Any other host must be named plainly in the output as unverified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
APPLY_COMMAND_FILE = REPO / ".claude" / "commands" / "apply.md"
|
||||||
|
|
||||||
|
KNOWN_ATS_APEXES = {
|
||||||
|
"greenhouse.io",
|
||||||
|
"lever.co",
|
||||||
|
"myworkdayjobs.com",
|
||||||
|
"workday.com",
|
||||||
|
"ashbyhq.com",
|
||||||
|
"smartrecruiters.com",
|
||||||
|
"workable.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
SHIPPED_PORTAL_HOSTS = {
|
||||||
|
"jobindex.dk",
|
||||||
|
"linkedin.com",
|
||||||
|
"jobnet.dk",
|
||||||
|
"jobbank.dk",
|
||||||
|
"jobdanmark.dk",
|
||||||
|
"freehire.me",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def classify_posting_host(url_str: str, installed_portals: set[str] = SHIPPED_PORTAL_HOSTS) -> tuple[str, str]:
|
||||||
|
"""Reference implementation of the host provenance rule in /apply Step 1.
|
||||||
|
|
||||||
|
Returns (tier, host), where tier is one of:
|
||||||
|
- 'installed_portal'
|
||||||
|
- 'official_ats'
|
||||||
|
- 'unverified'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url_str)
|
||||||
|
host = (parsed.hostname or "").lower().strip()
|
||||||
|
except Exception:
|
||||||
|
return "unverified", ""
|
||||||
|
|
||||||
|
if not host:
|
||||||
|
return "unverified", ""
|
||||||
|
|
||||||
|
# Check installed portal boards (exact match or subdomain match)
|
||||||
|
for portal in installed_portals:
|
||||||
|
if host == portal or host.endswith(f".{portal}"):
|
||||||
|
return "installed_portal", host
|
||||||
|
|
||||||
|
# Check known official ATS apexes (exact match or subdomain match)
|
||||||
|
for apex in KNOWN_ATS_APEXES:
|
||||||
|
if host == apex or host.endswith(f".{apex}"):
|
||||||
|
return "official_ats", host
|
||||||
|
|
||||||
|
return "unverified", host
|
||||||
|
|
||||||
|
|
||||||
|
class ApplyHostVerificationSpecTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.text = APPLY_COMMAND_FILE.read_text(encoding="utf-8")
|
||||||
|
step1_match = re.search(r"## Step 1: DRAFTER - Evaluate Fit(.*?)(?=## Step 2:)", self.text, re.DOTALL)
|
||||||
|
self.assertTrue(step1_match, "Step 1 must exist in apply.md")
|
||||||
|
self.step1_text = step1_match.group(1)
|
||||||
|
|
||||||
|
def test_step1_contains_source_host_verification_heading(self):
|
||||||
|
self.assertIn("Source Host Verification", self.step1_text)
|
||||||
|
|
||||||
|
def test_step1_documents_all_six_ats_apexes(self):
|
||||||
|
for apex in ["greenhouse.io", "lever.co", "myworkdayjobs.com", "ashbyhq.com", "smartrecruiters.com", "workable.com"]:
|
||||||
|
self.assertIn(apex, self.step1_text, f"Step 1 must specify ATS apex: {apex}")
|
||||||
|
|
||||||
|
def test_step1_documents_look_alike_fail_closed_rules(self):
|
||||||
|
self.assertIn("evil-greenhouse.io", self.step1_text)
|
||||||
|
self.assertIn("fail closed", self.step1_text)
|
||||||
|
|
||||||
|
def test_step1_requires_unverified_hosts_to_be_named_plainly(self):
|
||||||
|
self.assertIn("Unverified source host", self.step1_text)
|
||||||
|
|
||||||
|
def test_classifier_identifies_official_ats_subdomains(self):
|
||||||
|
urls = [
|
||||||
|
"https://boards.greenhouse.io/acme/jobs/12345",
|
||||||
|
"https://job-boards.greenhouse.io/acme/jobs/12345",
|
||||||
|
"https://jobs.lever.co/corp/67890",
|
||||||
|
"https://acme.myworkdayjobs.com/en-US/Careers/job/1",
|
||||||
|
"https://jobs.ashbyhq.com/startup/abc-123",
|
||||||
|
"https://jobs.smartrecruiters.com/Enterprise/456",
|
||||||
|
"https://apply.workable.com/tech-corp/j/789/",
|
||||||
|
]
|
||||||
|
for url in urls:
|
||||||
|
tier, host = classify_posting_host(url)
|
||||||
|
self.assertEqual(tier, "official_ats", f"{url} should classify as official_ats, got {tier}")
|
||||||
|
|
||||||
|
def test_classifier_identifies_installed_portal_hosts(self):
|
||||||
|
urls = [
|
||||||
|
"https://www.jobindex.dk/jobannonce/12345",
|
||||||
|
"https://www.linkedin.com/jobs/view/999999",
|
||||||
|
"https://jobnet.dk/find-job/8888",
|
||||||
|
"https://jobbank.dk/job/7777",
|
||||||
|
"https://freehire.me/job/6666",
|
||||||
|
]
|
||||||
|
for url in urls:
|
||||||
|
tier, host = classify_posting_host(url)
|
||||||
|
self.assertEqual(tier, "installed_portal", f"{url} should classify as installed_portal, got {tier}")
|
||||||
|
|
||||||
|
def test_classifier_fails_closed_on_look_alikes_and_unverified_hosts(self):
|
||||||
|
suspicious = [
|
||||||
|
"https://evil-greenhouse.io/job/1",
|
||||||
|
"https://boards.greenhouse.io.evil.com/job/1",
|
||||||
|
"https://boards.greenhouse.io@evil-domain.com/job/1",
|
||||||
|
"https://myworkdayjobs.com.phishing.net/login",
|
||||||
|
"https://lever.co.attacker.org/apply",
|
||||||
|
"https://unknown-board.example.com/posting/123",
|
||||||
|
]
|
||||||
|
for url in suspicious:
|
||||||
|
tier, host = classify_posting_host(url)
|
||||||
|
self.assertEqual(tier, "unverified", f"{url} must fail closed as unverified, got {tier}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user