Skip to content

feat(library): P2 rebased — YouTube + Web processors with streaming, content-type gate, timeout guards - #2177

Merged
jaylfc merged 1 commit into
jaylfc:devfrom
hognek:feat/library-p2-reland
Jul 28, 2026
Merged

feat(library): P2 rebased — YouTube + Web processors with streaming, content-type gate, timeout guards#2177
jaylfc merged 1 commit into
jaylfc:devfrom
hognek:feat/library-p2-reland

Conversation

@hognek

@hognek hognek commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Rebase of #2068 onto origin/dev (c5b1a6f)

This is the re-land jaylfc requested. Keeps ONLY the P2 delta on top of current dev.

What was changed from the old PR

P2 delta (kept):

  • YouTubeProcessor: metadata, thumbnail, transcript, chapters
  • WebProcessor: HTML fetch, readability extraction, SSRF-guarded redirects
  • _extract_readable_text() helper
  • url:youtube + url:web in _PROCESSORS
  • run_pipeline reference-artifact removal
  • _store_init_lock (asyncio.Lock on lazy LibraryStore init)
  • "chapters" in _TEXT_ARTIFACT_KINDS

P1 re-application (dropped):

  • templates/library.html — not added (dev dropped the server-rendered page)
  • All P1 store/route/collections changes — dev's versions preserved

Fixes from jaylfc review:

Fix Status
Streaming read: client.stream() instead of client.get() Done
10 MB cap enforced during streaming (OOM-safe) Done
Content-type gate: non-text/*ValueError (→ error status) Done
60s wall-clock deadline on WebProcessor fetch Done
_cleanup_procs() kill tracked subprocesses on timeout Done
120s asyncio.wait_for on YouTubeProcessor with cleanup Done

Preserved from dev (not reverted):

  • try_update_item_status CAS
  • Reprocess 409 guard + artifact cleanup + rollback-to-ready
  • taosmd HTTP collections flow (memory_url, admin-token, idempotent collection_id)
  • logger.warning OSError handlers
  • path="" on metadata artifacts (unlink guard)
  • test_library_page_gone, test_unauth_library_endpoints,
    test_reprocess_idempotent, test_reprocess_while_processing_returns_409

Tests: 54/54 pass (42 existing dev tests + 12 new): 3 YouTube, 4 web happy-path, 2 SSRF block, 1 redirect-hop validation, 1 size cap, 1 content-type gate.

Closes #2068.

…content-type gate, timeout guards

Rebase feat/library-p2 onto origin/dev (c5b1a6f) keeping ONLY P2 delta:
- YouTubeProcessor: metadata, thumbnail, transcript, chapters via yt-dlp
- WebProcessor: HTML fetch, readability extraction, SSRF-guarded redirects
- _extract_readable_text() helper (readability-lxml with tag-stripping fallback)
- url:youtube + url:web entries in _PROCESSORS
- run_pipeline reference-artifact removal (URL items now get processed)
- _store_init_lock: asyncio.Lock on lazy LibraryStore init (TOCTOU fix)
- 'chapters' added to _TEXT_ARTIFACT_KINDS

Fixes from jaylfc review of jaylfc#2068:
- Streaming read via client.stream() — OOM-safe, cap enforced before buffering
- Content-type gate: non-text/* responses raise ValueError (→ error status)
- 60s wall-clock deadline on WebProcessor fetch
- _cleanup_procs() in youtube.py: kill tracked subprocesses on timeout
- 120s asyncio.wait_for on YouTubeProcessor fetch with cleanup on TimeoutError

Preserves all P1 hardening from dev:
- try_update_item_status CAS (library_store.py untouched)
- reprocess 409 guard + artifact cleanup + rollback-to-ready
- taosmd HTTP collections flow (memory_url, admin-token, idempotent collection_id)
- logger.warning OSError handlers (not bare pass)
- path='' on metadata artifacts (unlink guard)
- test_library_page_gone (404), test_unauth_library_endpoints,
  test_reprocess_idempotent, test_reprocess_while_processing_returns_409

Tests: 54/54 pass (42 existing + 12 new: 3 YouTube, 4 web happy-path,
2 SSRF block, 1 redirect-hop validation, 1 size cap, 1 content-type gate)
@hognek
hognek marked this pull request as ready for review July 27, 2026 23:48
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hognek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fe8f6f7-1e6f-4d66-81a2-398aea4ae760

📥 Commits

Reviewing files that changed from the base of the PR and between 768aad5 and b242af4.

📒 Files selected for processing (5)
  • tests/test_library.py
  • tinyagentos/knowledge_fetchers/youtube.py
  • tinyagentos/library_collections.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/routes/library.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@jaylfc

jaylfc commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Reviewed against every blocking finding from #2068. This is the right rebase and I will merge it once CI is green. Thank you for doing it properly rather than patching the old branch.

# finding from #2068 status in #2177
B1 branch silently reverted merged P1 hardening FIXED
H1 10MB cap buffered the whole body first FIXED
Bot-1 no content-type gate, redirect to PDF gave green "ready" FIXED
M1 force-push dropped _cleanup_procs + yt-dlp timeout FIXED
M3 duplicate server-rendered UI + CDN without SRI FIXED (not re-added)
E no tests for the security-relevant paths FIXED, 12 added

The silent revert is gone, and I checked it the same way I caught it, by diffing symbols against origin/dev rather than reading the diff:

try_update_item_status : dev=1  pr2177=1
test_library_page_gone : dev=1  pr2177=1

Both survive. And the shape of the diff tells the story on its own: 769 additions, 21 deletions, against a branch that previously deleted the CAS, the reprocess guards, the collections flow and six tests. This is now additive.

The streaming fix is correct, not merely different. library_pipeline.py:495-521:

async with client.stream("GET", current_url) as resp:
    content_type = resp.headers.get("content-type", "")
    if ct_base and not ct_base.startswith("text/"):
        raise ...
    async for chunk in resp.aiter_bytes(8192):
        total += len(chunk)
        if total > self._MAX_WEB_BYTES:
            raise ...

Three things right at once: the client stays open while reading, the content-type is gated before the body is consumed, and the cap is checked inside the chunk loop against a running total. That closes the OOM vector I measured at 130MB peak heap for a claimed 10MB cap.

The tests are real, which is the part I care about most. All five I asked for are present (test_web_ssrf_block_loopback, test_web_ssrf_block_private, test_web_redirect_hop_validated, test_web_size_cap, test_web_content_type_gate) and I read their bodies rather than trusting the names. test_web_size_cap overrides _MAX_WEB_BYTES = 100 and then yields b"x" * 200, so it genuinely trips the cap. That is exactly the right way to test a limit: shrink the limit rather than allocate 60MB in CI. The old suite's mock yielded one small chunk and could never have failed, which is why the cap shipped broken.

Two notes, neither blocking:

The _MAX_WEB_BYTES override in the test reaches into a private attribute. It works and I would not hold the PR for it, but if the cap ever becomes a constructor arg or config value, that test will silently stop testing the real thing. Worth a comment saying so.

CodeRabbit reports pass with "Review rate limited", so it has not actually read this head. That is expected (it is dropped) and Gitar and CLA are green, so I am not blocking on it, but it is the fake-green pattern and worth remembering when a bot's tick is the only evidence you have.

Merging when the remaining shards finish. Good work on the SSRF chain surviving the rebase intact, since that was always the strongest part of the original PR and the reason it was worth rebasing rather than abandoning.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Library: add YouTube + Web URL processors with streaming and timeout guards

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add YouTube and web URL processors producing metadata, text/transcript, thumbnail, and chapters
 artifacts.
• Harden web fetch with SSRF-validated redirects, streaming 10MB cap, content-type gate, deadlines.
• Fix library store lazy-init race and route URL items through processors; add tests.
Diagram

graph TD
  A["Library routes"] --> B["LibraryStore"] --> C["run_pipeline"]
  C --> D["YouTubeProcessor"] --> G["yt-dlp subprocess"] --> B
  C --> E["WebProcessor"] --> F["SSRF guard"] --> B
  B --> H["taosmd collections"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reuse Desktop Browser proxy fetch path for WebProcessor
  • ➕ Centralizes URL fetching policy (headers, TLS config, proxying) in one place
  • ➕ Keeps SSRF validation and redirect handling consistent across features
  • ➖ Couples library ingestion to the browser proxy runtime and its auth model
  • ➖ Adds an extra hop/endpoint dependency vs direct httpx streaming in-process
2. Use process-group based cleanup for yt-dlp invocations
  • ➕ More reliably terminates child/grandchild processes spawned by yt-dlp
  • ➕ Reduces risk of orphaned processes under cancellation/timeout
  • ➖ Platform-specific behavior (POSIX vs Windows) and more complex implementation
  • ➖ Harder to unit-test deterministically compared to tracking Process objects
3. Implement redirect validation via custom httpx transport/middleware
  • ➕ Avoids manual redirect loop code while still validating each hop
  • ➕ Can standardize size/content-type enforcement at the client layer
  • ➖ More code/complexity than the current explicit loop
  • ➖ Requires deeper httpx internals knowledge and careful testing

Recommendation: The PR’s approach is the right default for P2: explicit redirect walking enables per-hop SSRF validation, and streaming with a hard byte cap prevents OOM on hostile responses. Consider process-group cleanup for yt-dlp only if you observe orphaned subprocesses in production; otherwise the current tracked-process cleanup is a reasonable, low-complexity safeguard.

Files changed (5) +769 / -21

Enhancement (2) +313 / -19
library_collections.pyIndex YouTube chapter text into collections +1/-1

Index YouTube chapter text into collections

• Extends the set of text artifact kinds eligible for taosmd indexing to include "chapters", enabling chapter summaries to be searchable alongside transcripts and extracted text.

tinyagentos/library_collections.py

library_pipeline.pyAdd YouTubeProcessor/WebProcessor with streaming fetch guards +312/-18

Add YouTubeProcessor/WebProcessor with streaming fetch guards

• Adds processors for url:youtube (yt-dlp metadata/thumbnail/transcript/chapters) and url:web (SSRF-guarded HTML fetch + readability extraction). Implements streaming download with a 10MB cap, text/* content-type gate, redirect hop validation, and wall-clock fetch deadlines; removes the prior URL reference-only artifact behavior so URL items are actually processed.

tinyagentos/library_pipeline.py

Bug fix (2) +35 / -2
youtube.pyTrack yt-dlp subprocesses and kill them on timeout/cancel +17/-0

Track yt-dlp subprocesses and kill them on timeout/cancel

• Introduces a global tracked process list and a cleanup routine to kill outstanding yt-dlp subprocesses. Ensures each created subprocess is registered so higher-level timeouts can terminate work promptly.

tinyagentos/knowledge_fetchers/youtube.py

library.pyLock lazy LibraryStore initialization to avoid double-init race +18/-2

Lock lazy LibraryStore initialization to avoid double-init race

• Wraps app.state LibraryStore lazy initialization in an asyncio.Lock to prevent concurrent requests from constructing/initializing multiple stores. Preserves the existing lazy-init behavior while making it race-safe.

tinyagentos/routes/library.py

Tests (1) +421 / -0
test_library.pyAdd YouTube/Web processor coverage and guard-path tests +421/-0

Add YouTube/Web processor coverage and guard-path tests

• Adds unit + integration tests for url:youtube and url:web pipeline routing, artifact creation, and meta_json updates. Introduces mock helpers for async return values and httpx streaming, plus tests for SSRF blocking, redirect hop validation, size caps, and content-type gating.

tests/test_library.py

@jaylfc
jaylfc enabled auto-merge (squash) July 27, 2026 23:50
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Global proc tracker race 🐞 Bug ☼ Reliability
Description
youtube.py tracks yt-dlp subprocesses in a module-global list and _cleanup_procs() kills *all*
tracked processes, so one timed-out YouTube fetch can terminate other concurrent fetches and the
list grows unbounded across successful runs. _cleanup_procs() also kills without awaiting process
exit, risking zombie/orphaned processes on timeout/cancel paths.
Code

tinyagentos/knowledge_fetchers/youtube.py[R17-28]

+# Tracked subprocesses so they can be killed on timeout or cancel.
+_tracked_procs: list[asyncio.subprocess.Process] = []
+
+
+def _cleanup_procs() -> None:
+    """Kill all tracked subprocesses (called on timeout or cancel)."""
+    for p in _tracked_procs:
+        try:
+            p.kill()
+        except Exception:
+            pass
+    _tracked_procs.clear()
Relevance

⭐⭐⭐ High

Team accepted kill+await subprocess cleanup to avoid orphans/timeouts in PR #269; similar
reliability concern.

PR-#269

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR introduces a module-global _tracked_procs and _cleanup_procs() that kills every tracked
process. Each yt-dlp subprocess is appended to this global list and never removed after
communicate(), and YouTubeProcessor calls _cleanup_procs() on timeout—so timeouts can kill
other in-flight fetches and completed processes remain referenced forever.

tinyagentos/knowledge_fetchers/youtube.py[17-29]
tinyagentos/knowledge_fetchers/youtube.py[140-146]
tinyagentos/knowledge_fetchers/youtube.py[177-211]
tinyagentos/knowledge_fetchers/youtube.py[266-280]
tinyagentos/library_pipeline.py[348-363]
PR-#269

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`tinyagentos.knowledge_fetchers.youtube` stores every spawned `yt-dlp` subprocess in a module-global `_tracked_procs` list and `_cleanup_procs()` kills everything in that global list. This can (a) kill unrelated concurrent fetches and (b) leak references to completed processes indefinitely. Additionally, `kill()` is not followed by `await proc.wait()`, which can leave zombies/orphans.

### Issue Context
- `YouTubeProcessor` wraps `fetch()` in `asyncio.wait_for()` and calls `_cleanup_procs()` on timeout.
- `fetch()`/`download_video()` append subprocesses to `_tracked_procs` but never remove them on normal completion.

### Fix Focus Areas
- tinyagentos/knowledge_fetchers/youtube.py[17-29]
- tinyagentos/knowledge_fetchers/youtube.py[140-146]
- tinyagentos/knowledge_fetchers/youtube.py[177-211]
- tinyagentos/knowledge_fetchers/youtube.py[266-280]
- tinyagentos/library_pipeline.py[348-363]

### What to change
- Replace the module-global `_tracked_procs` with **per-invocation** tracking (e.g., local `procs: list[Process]` inside `fetch()` / `download_video()`), and ensure each proc is removed/cleared in a `finally` block.
- Make cleanup only kill processes belonging to that invocation. (If the processor needs cleanup access, return a cleanup callback/object from `fetch()` or manage the timeout inside `fetch()`.)
- In cleanup, after `proc.kill()` also `await proc.wait()` (or `communicate()` if appropriate) to avoid zombies.
- Handle `asyncio.CancelledError` as well as timeout, ensuring subprocesses are terminated on cancellation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Em dashes in library_pipeline.py 📜 Skill insight ✧ Quality
Description
New docstrings/comments and a raised error message include em dashes (), which the checklist
forbids in public-facing text. This can leak into logs/UI/error surfaces and violates the project
style requirement.
Code

tinyagentos/library_pipeline.py[R323-347]

+class YouTubeProcessor(Processor):
+    """YouTube URL processor — cheap tier: metadata, thumbnail, transcript, chapters.
+
+    Uses yt-dlp via the knowledge_fetchers.youtube module to fetch video
+    metadata and captions without downloading the video file. Produces
+    artifacts that flow into taosmd collections for agent querying.
+
+    The cheap tier (per the design doc, docs/design/library-app.md section 4)
+    covers steps 1-4: canonical link, title, channel, description, thumbnail,
+    duration, upload date, subtitles/transcript, chapters.
+    """
+
+    _YTDLP_TIMEOUT = 120  # seconds
+
+    async def process(self, item: dict) -> list[dict]:
+        item_id = item["id"]
+        source_url = item.get("source_url", "")
+        artifacts: list[dict] = []
+
+        if not source_url:
+            return artifacts
+
+        # Only catch ImportError (missing yt-dlp).  Let fetch errors
+        # propagate so run_pipeline marks the item as "error" — a failed
+        # yt-dlp invocation must not silently look successful.
Relevance

⭐⭐ Medium

Em-dash style rule was only partially enforced previously; PR #2122 shows mixed acceptance for
replacing em dashes.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2212258 forbids using em dashes () in public-facing text (including docstrings,
comments, and user-facing strings). The cited added lines contain multiple , including in a
ValueError message (... — only text/* is supported) and new processor docstrings/comments.

tinyagentos/library_pipeline.py[323-347]
tinyagentos/library_pipeline.py[483-507]
tinyagentos/library_pipeline.py[614-616]
tests/test_library.py[642-643]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces em dashes (`—`) in docstrings/comments and in a user-visible exception message.

## Issue Context
Compliance rule PR Compliance ID 2212258 forbids em dashes in public-facing text; use `-` (hyphen) or rewrite.

## Fix Focus Areas
- tinyagentos/library_pipeline.py[323-347]
- tinyagentos/library_pipeline.py[483-507]
- tinyagentos/library_pipeline.py[614-616]
- tests/test_library.py[642-643]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. SSRF check not pinned 🐞 Bug ⛨ Security
Description
WebProcessor validates each redirect hop with validate_url_or_raise() but then performs the actual
request via httpx using the original hostname URL, allowing a DNS-rebinding TOCTOU window where the
connection can resolve to a different (potentially private/loopback) IP than was validated. This
undermines the intended SSRF defense for the new library URL-fetching pipeline.
Code

tinyagentos/library_pipeline.py[R488-496]

+            for _hop in range(self._MAX_WEB_REDIRECTS + 1):
+                validate_url_or_raise(current_url)
+
+                async with httpx.AsyncClient(
+                    timeout=httpx.Timeout(30),
+                    follow_redirects=False,
+                ) as client:
+                    async with client.stream("GET", current_url) as resp:
+                        status_code = resp.status_code
Relevance

⭐⭐ Medium

Repo strongly hardens SSRF/redirect validation (PRs #301, #304), but no prior evidence of DNS
pinning requirement.

PR-#301
PR-#304

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
WebProcessor validates the URL before each request, but then performs the request with httpx using
the same hostname URL; the SSRF validator performs DNS resolution internally and provides no
mechanism to force the request to use the validated IP(s), leaving a DNS change window between
validation and connect.

tinyagentos/library_pipeline.py[486-496]
tinyagentos/routes/desktop_browser/ssrf.py[58-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`WebProcessor` calls `validate_url_or_raise(current_url)` (which resolves DNS and validates the resulting IPs) but then passes `current_url` to `httpx.AsyncClient.stream()`. Because `validate_url_or_raise()` returns no resolved address information and `httpx` will resolve DNS again at connection time, there is a DNS TOCTOU window (DNS rebinding) where the validated hostname can later resolve to a blocked/internal address.

### Issue Context
- This PR introduces a new server-side fetching path (library WebProcessor) that relies on SSRF checks.
- The SSRF helper validates by DNS resolution (`socket.getaddrinfo`) but does not bind the subsequent connection to the validated resolution.

### Fix Focus Areas
- tinyagentos/library_pipeline.py[486-496]
- tinyagentos/routes/desktop_browser/ssrf.py[58-119]

### What to change
Implement a fetch path where **the IP address that is validated is the IP address that is connected to**, e.g.:
- Introduce a custom httpx/httpcore transport or resolver that performs resolution + `validate_resolved_addr()` inside the connection routine and rejects any selected address that is blocked.
- Alternatively, resolve+validate in `WebProcessor`, then connect to a validated address explicitly while preserving HTTP `Host` and (for HTTPS) SNI/cert verification in a safe way.
- At minimum, add a second validation step as close to connection establishment as possible (though true pinning is preferred).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +323 to +347
class YouTubeProcessor(Processor):
"""YouTube URL processor — cheap tier: metadata, thumbnail, transcript, chapters.

Uses yt-dlp via the knowledge_fetchers.youtube module to fetch video
metadata and captions without downloading the video file. Produces
artifacts that flow into taosmd collections for agent querying.

The cheap tier (per the design doc, docs/design/library-app.md section 4)
covers steps 1-4: canonical link, title, channel, description, thumbnail,
duration, upload date, subtitles/transcript, chapters.
"""

_YTDLP_TIMEOUT = 120 # seconds

async def process(self, item: dict) -> list[dict]:
item_id = item["id"]
source_url = item.get("source_url", "")
artifacts: list[dict] = []

if not source_url:
return artifacts

# Only catch ImportError (missing yt-dlp). Let fetch errors
# propagate so run_pipeline marks the item as "error" — a failed
# yt-dlp invocation must not silently look successful.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Em dashes in library_pipeline.py 📜 Skill insight ✧ Quality

New docstrings/comments and a raised error message include em dashes (), which the checklist
forbids in public-facing text. This can leak into logs/UI/error surfaces and violates the project
style requirement.
Agent Prompt
## Issue description
The PR introduces em dashes (`—`) in docstrings/comments and in a user-visible exception message.

## Issue Context
Compliance rule PR Compliance ID 2212258 forbids em dashes in public-facing text; use `-` (hyphen) or rewrite.

## Fix Focus Areas
- tinyagentos/library_pipeline.py[323-347]
- tinyagentos/library_pipeline.py[483-507]
- tinyagentos/library_pipeline.py[614-616]
- tests/test_library.py[642-643]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +17 to +28
# Tracked subprocesses so they can be killed on timeout or cancel.
_tracked_procs: list[asyncio.subprocess.Process] = []


def _cleanup_procs() -> None:
"""Kill all tracked subprocesses (called on timeout or cancel)."""
for p in _tracked_procs:
try:
p.kill()
except Exception:
pass
_tracked_procs.clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Global proc tracker race 🐞 Bug ☼ Reliability

youtube.py tracks yt-dlp subprocesses in a module-global list and _cleanup_procs() kills *all*
tracked processes, so one timed-out YouTube fetch can terminate other concurrent fetches and the
list grows unbounded across successful runs. _cleanup_procs() also kills without awaiting process
exit, risking zombie/orphaned processes on timeout/cancel paths.
Agent Prompt
### Issue description
`tinyagentos.knowledge_fetchers.youtube` stores every spawned `yt-dlp` subprocess in a module-global `_tracked_procs` list and `_cleanup_procs()` kills everything in that global list. This can (a) kill unrelated concurrent fetches and (b) leak references to completed processes indefinitely. Additionally, `kill()` is not followed by `await proc.wait()`, which can leave zombies/orphans.

### Issue Context
- `YouTubeProcessor` wraps `fetch()` in `asyncio.wait_for()` and calls `_cleanup_procs()` on timeout.
- `fetch()`/`download_video()` append subprocesses to `_tracked_procs` but never remove them on normal completion.

### Fix Focus Areas
- tinyagentos/knowledge_fetchers/youtube.py[17-29]
- tinyagentos/knowledge_fetchers/youtube.py[140-146]
- tinyagentos/knowledge_fetchers/youtube.py[177-211]
- tinyagentos/knowledge_fetchers/youtube.py[266-280]
- tinyagentos/library_pipeline.py[348-363]

### What to change
- Replace the module-global `_tracked_procs` with **per-invocation** tracking (e.g., local `procs: list[Process]` inside `fetch()` / `download_video()`), and ensure each proc is removed/cleared in a `finally` block.
- Make cleanup only kill processes belonging to that invocation. (If the processor needs cleanup access, return a cleanup callback/object from `fetch()` or manage the timeout inside `fetch()`.)
- In cleanup, after `proc.kill()` also `await proc.wait()` (or `communicate()` if appropriate) to avoid zombies.
- Handle `asyncio.CancelledError` as well as timeout, ensuring subprocesses are terminated on cancellation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +488 to +496
for _hop in range(self._MAX_WEB_REDIRECTS + 1):
validate_url_or_raise(current_url)

async with httpx.AsyncClient(
timeout=httpx.Timeout(30),
follow_redirects=False,
) as client:
async with client.stream("GET", current_url) as resp:
status_code = resp.status_code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Ssrf check not pinned 🐞 Bug ⛨ Security

WebProcessor validates each redirect hop with validate_url_or_raise() but then performs the actual
request via httpx using the original hostname URL, allowing a DNS-rebinding TOCTOU window where the
connection can resolve to a different (potentially private/loopback) IP than was validated. This
undermines the intended SSRF defense for the new library URL-fetching pipeline.
Agent Prompt
### Issue description
`WebProcessor` calls `validate_url_or_raise(current_url)` (which resolves DNS and validates the resulting IPs) but then passes `current_url` to `httpx.AsyncClient.stream()`. Because `validate_url_or_raise()` returns no resolved address information and `httpx` will resolve DNS again at connection time, there is a DNS TOCTOU window (DNS rebinding) where the validated hostname can later resolve to a blocked/internal address.

### Issue Context
- This PR introduces a new server-side fetching path (library WebProcessor) that relies on SSRF checks.
- The SSRF helper validates by DNS resolution (`socket.getaddrinfo`) but does not bind the subsequent connection to the validated resolution.

### Fix Focus Areas
- tinyagentos/library_pipeline.py[486-496]
- tinyagentos/routes/desktop_browser/ssrf.py[58-119]

### What to change
Implement a fetch path where **the IP address that is validated is the IP address that is connected to**, e.g.:
- Introduce a custom httpx/httpcore transport or resolver that performs resolution + `validate_resolved_addr()` inside the connection routine and rejects any selected address that is blocked.
- Alternatively, resolve+validate in `WebProcessor`, then connect to a validated address explicitly while preserving HTTP `Host` and (for HTTPS) SNI/cert verification in a safe way.
- At minimum, add a second validation step as close to connection establishment as possible (though true pinning is preferred).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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