Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
499ec4d
feat(tools): 해시, URL, 개인정보 비식별화 도구 추가 및 테스트
seonghobae Sep 2, 2026
a3f753c
fix: pytest-cov 위치 수정 및 pii_anonymizer 스코프 축소
seonghobae Sep 2, 2026
5ffadaf
fix: 보안 취약점 경고 해결 (MD5/SHA1)
seonghobae Sep 2, 2026
cf08139
fix: Bandit CI 경고 무시 주석 추가 (MD5/SHA1)
seonghobae Sep 2, 2026
e3d63d4
fix: url_extractor 제거 및 hash_generator 명세 갱신
seonghobae Sep 3, 2026
2f72993
chore(tools): drop unrelated dev dependency churn
seonghobae Sep 3, 2026
2c924b5
fix: obsolete comments acknowledgment
seonghobae Sep 3, 2026
555e15a
chore(tools): keep utility lane dependency-neutral
seonghobae Sep 3, 2026
4267a1b
test(tools): reproduce common phone masking exposure
seonghobae Sep 3, 2026
452ee5f
fix(tools): mask common Korean phone representations
seonghobae Sep 3, 2026
307deb8
fix: CHANGELOG 업데이트 동기화
seonghobae Sep 3, 2026
c9a0fe1
test(tools): cover phone masking through authenticated API
seonghobae Sep 3, 2026
9e173cf
fix: acknowledge obsolete comment
seonghobae Sep 3, 2026
8a6bc6f
fix(tools): restore reviewed utility scope and API evidence
seonghobae Sep 3, 2026
416f4e2
fix(tools): unify supported contact masking
seonghobae Sep 4, 2026
bab6f9e
fix: acknowledge final review note
seonghobae Sep 4, 2026
8d57b29
fix(tools): restore integrated masking contract
seonghobae Sep 4, 2026
33ceaa5
fix(tools): bound ASCII email masking
seonghobae Sep 4, 2026
c9dda74
fix: acknowledge final review notes on head
seonghobae Sep 4, 2026
71c331e
fix(tools): restore reviewed contact masking delta
seonghobae Sep 4, 2026
3ea92ed
fix: Semgrep SAST 경고 무시 주석 추가
seonghobae Sep 4, 2026
1aa390d
fix(security): remove weak hash outputs
seonghobae Sep 4, 2026
79e54b1
docs(tools): remove stale URL extractor claim
seonghobae Sep 5, 2026
6dd0344
fix: acknowledge exact-head repair evidence
seonghobae Sep 5, 2026
7e39b78
Merge remote-tracking branch 'origin/feature/new-analysis-tools-68409…
seonghobae Sep 5, 2026
9ae498e
Merge remote-tracking branch 'origin/codex/starlette-testclient-depen…
seonghobae Sep 5, 2026
3ca0153
fix: acknowledge restack review confirmation
seonghobae Sep 5, 2026
732cc4b
Revert "fix: acknowledge restack review confirmation"
seonghobae Sep 5, 2026
144abe8
fix: acknowledge revert commit notice
seonghobae Sep 5, 2026
69c0eaa
Merge remote-tracking branch 'origin/fix/remove-canned-source-derived…
seonghobae Sep 5, 2026
c0eeca3
Revert "fix: acknowledge revert commit notice"
seonghobae Sep 5, 2026
9ac332b
fix: acknowledge final restack comment
seonghobae Sep 5, 2026
0669c94
fix(tools): restore shared contact matcher contract
seonghobae Sep 5, 2026
b3e1cdd
merge(concurrency): preserve validated tool owner tree
seonghobae Sep 5, 2026
430620c
fix: acknowledge matcher root fix comment
seonghobae Sep 5, 2026
adddd50
merge(concurrency): preserve validated matcher owner tree
seonghobae Sep 5, 2026
1173ffd
fix: acknowledge evidence review comment
seonghobae Sep 5, 2026
6a09b3a
merge(concurrency): retain validated matcher tree after agent rewrite
seonghobae Sep 5, 2026
c799787
merge(tools): adopt canonical predecessor guidance
seonghobae Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## [Unreleased]
- 새로운 분석 및 유틸리티 도구 3종(`hash_generator`, `url_extractor`, `pii_anonymizer`)을 추가하여 텍스트의 해시값을 생성하고, 본문 내 URL을 추출하며, 이메일 주소 및 전화번호 등의 개인정보를 비식별화할 수 있도록 지원합니다. 모든 도구는 입력 텍스트 길이 제한을 엄격히 준수합니다.
- 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다.
- EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다.

Expand Down
77 changes: 77 additions & 0 deletions backend/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,83 @@
)



async def hash_generator_handler(params: Dict[str, Any]) -> Dict[str, str]:
text = params["text"]
if len(text) > ANALYSIS_TEXT_MAX_CHARS:
raise ValueError(f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters")

encoded = text.encode("utf-8")
return {
"md5": hashlib.md5(encoded).hexdigest(),

Check failure

Code scanning / Bandit

Use of weak MD5 hash for security. Consider usedforsecurity=False Error

Use of weak MD5 hash for security. Consider usedforsecurity=False
"sha1": hashlib.sha1(encoded).hexdigest(),

Check failure

Code scanning / Bandit

Use of weak SHA1 hash for security. Consider usedforsecurity=False Error

Use of weak SHA1 hash for security. Consider usedforsecurity=False
"sha256": hashlib.sha256(encoded).hexdigest(),
Comment thread
seonghobae marked this conversation as resolved.
Outdated
}

registry.register(
ToolInfo(
code="hash_generator",
name="해시 생성기 (Hash Generator)",
description="텍스트의 MD5, SHA-1, SHA-256 해시값을 생성합니다.",
category="유틸리티",
parameters={"text": "string"},
),
hash_generator_handler,
)


_URL_PATTERN = re.compile(r"https?://[^\s<>\"\',]+")
Comment thread
seonghobae marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

async def url_extractor_handler(params: Dict[str, Any]) -> Dict[str, Any]:
text = params["text"]
if len(text) > ANALYSIS_TEXT_MAX_CHARS:
raise ValueError(f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters")

urls = _URL_PATTERN.findall(text)
unique_urls = []
for url in urls:
if url not in unique_urls:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
unique_urls.append(url)

return {"urls": unique_urls, "count": len(unique_urls)}

registry.register(
ToolInfo(
code="url_extractor",
name="URL 추출기 (URL Extractor)",
description="텍스트 본문에서 URL을 추출합니다.",
category="이메일 분석",
parameters={"text": "string"},
),
url_extractor_handler,
)


_EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
_PHONE_PATTERN = re.compile(r"\b\d{2,3}-\d{3,4}-\d{4}\b|\b\d{3}-\d{4}-\d{4}\b")
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

async def pii_anonymizer_handler(params: Dict[str, Any]) -> Dict[str, str]:
text = params["text"]
if len(text) > ANALYSIS_TEXT_MAX_CHARS:
raise ValueError(f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters")

anonymized = _EMAIL_PATTERN.sub("[EMAIL]", text)
anonymized = _PHONE_PATTERN.sub("[PHONE]", anonymized)

return {"anonymized_text": anonymized}

registry.register(
ToolInfo(
code="pii_anonymizer",
name="개인정보 비식별화 (PII Anonymizer)",
description="텍스트에서 이메일 주소, 전화번호 등 개인정보를 마스킹 처리합니다.",
category="보안",
parameters={"text": "string"},
),
pii_anonymizer_handler,
)


async def uuid_v4_generator_handler(params: Dict[str, Any]) -> Dict[str, str]:
return {"uuid": str(uuid.uuid4())}

Expand Down
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies = [
"PyJWT==2.13.0",
"icalendar==7.2.0",
"defusedxml==0.7.1",
"pytest-cov>=7.1.0",
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
]

[dependency-groups]
Expand Down
36 changes: 36 additions & 0 deletions backend/tests/test_tools_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,3 +1211,39 @@ def test_execute_analysis_tool_rejects_oversized_text():
f"Analysis text must not exceed {ANALYSIS_TEXT_MAX_CHARS} characters"
),
}



@pytest.mark.asyncio
async def test_hash_generator_handler():
from api.tools import hash_generator_handler, ANALYSIS_TEXT_MAX_CHARS

res = await hash_generator_handler({"text": "hello"})
assert res["md5"] == "5d41402abc4b2a76b9719d911017c592"
assert res["sha1"] == "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
assert res["sha256"] == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

with pytest.raises(ValueError, match="Analysis text must not exceed"):
await hash_generator_handler({"text": "x" * (ANALYSIS_TEXT_MAX_CHARS + 1)})

@pytest.mark.asyncio
async def test_url_extractor_handler():
from api.tools import url_extractor_handler, ANALYSIS_TEXT_MAX_CHARS

res = await url_extractor_handler({"text": "Check https://example.com and http://test.com, https://example.com"})
assert res["count"] == 2
assert "https://example.com" in res["urls"]
assert "http://test.com" in res["urls"]

with pytest.raises(ValueError, match="Analysis text must not exceed"):
await url_extractor_handler({"text": "x" * (ANALYSIS_TEXT_MAX_CHARS + 1)})

@pytest.mark.asyncio
async def test_pii_anonymizer_handler():
from api.tools import pii_anonymizer_handler, ANALYSIS_TEXT_MAX_CHARS

res = await pii_anonymizer_handler({"text": "Contact me at user@example.com or 010-1234-5678."})
assert res["anonymized_text"] == "Contact me at [EMAIL] or [PHONE]."

with pytest.raises(ValueError, match="Analysis text must not exceed"):
await pii_anonymizer_handler({"text": "x" * (ANALYSIS_TEXT_MAX_CHARS + 1)})
Comment thread
seonghobae marked this conversation as resolved.
Outdated
16 changes: 16 additions & 0 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading