Skip to content

feat(library): P2 — YouTube cheap-tier processor + generic web-page ingestor#2068

Open
hognek wants to merge 7 commits into
jaylfc:devfrom
hognek:feat/library-p2
Open

feat(library): P2 — YouTube cheap-tier processor + generic web-page ingestor#2068
hognek wants to merge 7 commits into
jaylfc:devfrom
hognek:feat/library-p2

Conversation

@hognek

@hognek hognek commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds on PR #2062 (P1 Library core) — adds YouTube cheap-tier ingest and generic web-page ingestor as P2 of epic #2057.

Changes

  • YouTubeProcessor — handles url:youtube items. Fetches metadata, thumbnail, transcript, and chapters via knowledge_fetchers.youtube (yt-dlp). Cheap-tier only: no video download, just text artifacts for collection indexing. Per design doc sections 3–4.
  • WebProcessor — handles url:web items. Fetches HTML (SSRF-guarded via validate_url_or_raise), extracts readable text using readability-lxml (falls back to simple tag-stripping). Auto-titles from \<title\> tag. Produces text artifacts for collection indexing.
  • Registry — both registered in _PROCESSORS dict so run_pipeline dispatches to them.
  • _extract_readable_text() helper — shared readability extraction with readability-lxml priority and tag-stripping fallback.

Files changed

  • tinyagentos/library_pipeline.py — +258 lines (YouTubeProcessor, WebProcessor, _extract_readable_text, registry entries)
  • tests/test_library.py — +290 lines (8 new tests: 4 YouTube, 4 web; 47/47 all pass)

Design doc

docs/design/library-app.md sections 3–4 (cheap tier + YouTube reference flow, steps 1-4)

Testing

All 47 library tests pass. No new imports break existing tests.


Summary by Gitar

  • Library Pipeline & Ingestion:
    • Introduced tinyagentos/library_pipeline.py with multi-kind processor support for file, text, pdf, image, url:youtube, and url:web.
    • Added LibraryStore in tinyagentos/library_store.py for tracking ingested items, artifacts, and async processing jobs.
    • Implemented background pipeline orchestration in tinyagentos/routes/library.py with support for file uploads and URL ingest.
  • Collections Integration:
    • Added tinyagentos/library_collections.py to index text artifacts into taosmd collections for agent queryability.
  • App UI:
    • Created tinyagentos/templates/library.html with drag-and-drop ingestion, progress indicators, and real-time HTMX-based list updates.
  • Test Coverage:
    • Added comprehensive suite in tests/test_library.py covering kind detection, store operations, processor logic, and route API endpoints.

This will update automatically on new commits.

Summary by CodeRabbit

  • New Features
    • Added Library support for uploading files and ingesting web or YouTube URLs.
    • Added item listing, detail viewing, deletion, and reprocessing.
    • Added automatic extraction of metadata, transcripts, chapters, and readable web text.
    • Added background processing with pending, ready, and error statuses.
  • Bug Fixes
    • Improved handling of missing sources, invalid thumbnails, failed processing, oversized uploads, and unsafe redirects.
  • Tests
    • Added comprehensive coverage for file, web, and YouTube library workflows.

@hognek
hognek marked this pull request as ready for review July 20, 2026 16:36
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds YouTube and web URL processors, routes URL-only items through them, introduces library ingestion and item lifecycle endpoints, and adds asynchronous tests covering artifacts, metadata, titles, missing inputs, and pipeline status transitions.

Changes

Library ingestion

Layer / File(s) Summary
YouTube and web processing
tinyagentos/library_pipeline.py
Adds YouTube artifacts, web fetching with SSRF and size guards, readable-text extraction, processor registration, and URL-only routing.
Library API lifecycle
tinyagentos/routes/library.py
Adds ingestion, listing, retrieval, deletion, and reprocessing endpoints with background pipeline execution and filesystem cleanup.
Processor and pipeline validation
tests/test_library.py
Adds mocked asynchronous tests for YouTube and web processing, artifact metadata, title extraction, missing inputs, and ready-state routing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • jaylfc/taOS#2062 — Overlaps in the library pipeline orchestration and processor tests.
  • jaylfc/taOS#2070 — Directly overlaps in YouTube/web processors, routing, and asynchronous test coverage.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LibraryRoutes
  participant LibraryStore
  participant run_pipeline
  participant URLProcessor
  Client->>LibraryRoutes: Submit URL for ingestion
  LibraryRoutes->>LibraryStore: Create pending item
  LibraryRoutes->>run_pipeline: Schedule background processing
  run_pipeline->>URLProcessor: Process YouTube or web URL
  URLProcessor->>LibraryStore: Save generated artifacts
  run_pipeline->>LibraryStore: Mark item ready
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding YouTube processing and generic web-page ingestion to the library pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 20, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Comment thread tinyagentos/routes/library.py Outdated
return (
f'<div class="item-card" id="item-{item_id}">'
f'<div class="info">'
f'<h3>{title}</h3>'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: Reflected XSS — title (and item_id at line 250, kind at 255, status at 254) are interpolated unescaped into HTML.

title originates from untrusted input: the web page <title> tag (WebProcessor), YouTube metadata, or the user-supplied title form field. An attacker who controls a fetched page title (e.g. <title><img src=x onerror=alert(1)></title>) or submits a crafted title value can inject HTML/JS into the library item list, executed in any admin session that views /api/library/items via HTMX.

Escape all interpolated values (e.g. markupsafe.escape / html.escape) before building the fragment.

Suggested change
f'<h3>{title}</h3>'
f'<h3>{escape(title)}</h3>'

(You will need to from markupsafe import escape — apply it to item_id, kind, and status as well.)


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/library_pipeline.py Outdated
r"<title[^>]*>([^<]+)</title>", html, re.IGNORECASE,
)
if m:
title = m.group(1).strip()[:200]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: The extracted <title> text is stored raw (no HTML entity unescaping) and later rendered unescaped in _render_item_card. A page whose title contains HTML entities or markup will (a) yield a garbled title and (b) become the XSS vector described on routes/library.py:252.

Additionally m.group(1) includes the raw inner text but the regex <title[^>]*>([^<]+)</title> only captures up to the first <, so a title containing a < (or HTML entities like &amp;) is captured incorrectly. At minimum, run the captured string through html.unescape(...) before storing.

Suggested change
title = m.group(1).strip()[:200]
title = html.unescape(m.group(1)).strip()[:200]

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

# Stream file in bounded chunks to avoid loading it entirely into
# memory. Reject uploads exceeding 100 MB with HTTP 413.
MAX_SIZE = 100 * 1024 * 1024 # 100 MB
if file.size and file.size > MAX_SIZE:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: file.size is client-reported and frequently None/0 for streamed/chunked multipart uploads, so this pre-check is skipped and the only real protection is the in-loop size > MAX_SIZE counter — which is correct, but the pre-check gives a false sense of defense and will not catch oversized uploads when file.size is missing. Consider relying solely on (and keeping) the in-loop bound, or dropping the pre-check, to avoid implying the cap is enforced up-front.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/library_pipeline.py Outdated
except ImportError:
logger.warning("httpx not available — skipping web fetch for %s",
source_url)
return artifacts

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: A failed fetch for URL items is swallowed: WebProcessor/YouTubeProcessor catch all exceptions and return artifacts (empty). run_pipeline then still calls update_item_status(item_id, "ready"), so a transient network error, DNS failure, or blocked host leaves the item permanently ready with zero artifacts and no error meta. The pipeline-integration tests even assert ready on a no-op fetch. This makes failed ingests indistinguishable from successful ones and there is no retry.

Consider tracking whether any artifact was produced (or the fetch succeeded) for url:* kinds and marking the item error when the source could not be retrieved, consistent with the existing missing-file error handling for file items.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

Code Review Roast 🔥

Verdict: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 0
⚠️ warning 1
💡 suggestion 2
🤏 nitpick 1
Issue Details (click to expand)
File Line Roast
tinyagentos/library_pipeline.py 452 Non-text responses silently become "ready" items — user sees green checkmark, indexer gets nothing
tests/test_library.py 500 WebProcessor security features (redirect validation, size cap, content-type gate) have zero test coverage
tests/test_library.py 360 YouTubeProcessor timeout + subprocess cleanup logic untested
tinyagentos/knowledge_fetchers/youtube.py 134 _cleanup_procs only catches ProcessLookupError; other kill() failures mask root cause
tinyagentos/library_pipeline.py 418 Variable declarations for final response metadata hoisted outside loop — reads like a half-refactor

🏆 Best part: The WebProcessor SSRF redirect hardening is genuinely solid — manual per-hop validation against the blocklist, 10 MB streaming cap, content-type gating. This mirrors knowledge_ingest._download_article correctly and closes a real attack surface. The YouTubeProcessor subprocess tracking is also a nice defensive touch.

💀 Worst part: The content-type gate at library_pipeline.py:452 logs and returns empty artifacts, then the pipeline marks the item ready. A 50 MB PDF upload looks "successful" to the user but produces zero searchable content. That's a silent data-loss UX bug waiting for a support ticket.

📊 Overall: Like a bouncer who checks IDs at the door but then lets anyone with a fake mustache into the VIP section — the perimeter is hardened but the exit criteria are confused. Fix the ready vs error semantics for non-text responses and add tests for the new guards, then it's solid.

Files Reviewed (5 files)
  • tinyagentos/library_pipeline.py - 3 issues
  • tests/test_library.py - 2 issues
  • tinyagentos/knowledge_fetchers/youtube.py - 1 issue
  • tinyagentos/routes/library.py - 0 issues (TOCTOU lock, error-status preservation, reprocess rollback all solid)
  • tinyagentos/library_collections.py - 0 issues (link response validation added)

Fix these issues in Kilo Cloud

Previous Review Summaries (4 snapshots, latest commit 52c3c7a)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 52c3c7a)

Status: No New Issues Found (incremental) | Recommendation: Merge

Overview

This incremental review covers commits after e08f98aac9ebd47e03cec79318891cfb8992887e. The changed lines harden WebProcessor (redirect-safe SSRF validation per hop + 10 MB size cap) and update the test mock to match the real httpx.Response interface. They mirror the established, tested _download_article pattern in knowledge_ingest.py and introduce no new issues.

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Notes (out of incremental scope)

  • The prior SUGGESTION on tinyagentos/routes/library.py:129 (file.size client-reported pre-check) is unchanged by this diff and remains open; not re-reported per incremental-review scope.
  • CodeRabbit findings on library_collections.py:118, library.html:91/96, and library.py:90 are outside the changed files and not re-evaluated here.
Files Reviewed (2 files in increment)
  • tinyagentos/library_pipeline.py - 0 new issues (redirect-safe SSRF + size cap added, mirrors _download_article)
  • tests/test_library.py - 0 new issues (mock updated for is_redirect, encoding, aiter_bytes)

Previous review (commit 3ce9d3c)

Status: No New Issues Found (incremental) | Recommendation: Merge

Overview

This incremental review covers commits after e08f98aac9ebd47e03cec79318891cfb8992887e. The changed lines harden WebProcessor (redirect-safe SSRF validation per hop + 10 MB size cap) and update the test mock to match the real httpx.Response interface. They mirror the established, tested _download_article pattern in knowledge_ingest.py and introduce no new issues.

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Notes (out of incremental scope)

  • The prior SUGGESTION on tinyagentos/routes/library.py:129 (file.size client-reported pre-check) is unchanged by this diff and remains open; not re-reported per incremental-review scope.
  • CodeRabbit findings on library_collections.py:118, library.html:91/96, and library.py:90 are outside the changed files and not re-evaluated here.
Files Reviewed (2 files in increment)
  • tinyagentos/library_pipeline.py - 0 new issues (redirect-safe SSRF + size cap added, mirrors _download_article)
  • tests/test_library.py - 0 new issues (mock updated for is_redirect, encoding, aiter_bytes)

Previous review (commit e08f98a)

Status: No New Issues Found (incremental) | Recommendation: Merge

Overview

This incremental review covers commits after 96abf8b. The changed lines are a targeted fix of previously reported findings and introduce no new issues.

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0

Resolved in this increment

File Previous Line Issue Resolution
tinyagentos/routes/library.py 254 Reflected XSS — unescaped title/item_id/kind/status FIXED — all interpolated fields now wrapped in html.escape(...) (lines 245-256)
tinyagentos/library_pipeline.py 432 Raw <title> text stored without html.unescape FIXED — now _html_mod.unescape(m.group(1)) (line 423)
tinyagentos/library_pipeline.py 413 Failed URL/YouTube fetch swallowed → item silently ready FIXED — broad except Exception removed; errors propagate and run_pipeline marks item error (lines 640-650)

Notes (out of incremental scope)

  • The prior SUGGESTION on tinyagentos/routes/library.py:129 (file.size client-reported pre-check) is unchanged by this diff and remains open; it is not re-reported here per incremental-review scope.
  • CodeRabbit findings on library_collections.py:118, library.html:91/96, and library.py:90 are outside the changed files and not re-evaluated here.
Files Reviewed (2 files in increment)
  • tinyagentos/library_pipeline.py - 0 new issues (3 prior resolved)
  • tinyagentos/routes/library.py - 0 new issues (1 prior resolved)

Previous review (commit 96abf8b)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
tinyagentos/routes/library.py 252 Reflected XSS — title (and item_id/kind/status) interpolated unescaped into HTMX HTML fragment; title comes from untrusted page <title>/YouTube/user input

WARNING

File Line Issue
tinyagentos/library_pipeline.py 432 WebProcessor extracts <title> raw (no html.unescape), feeding the XSS vector and producing garbled titles
tinyagentos/library_pipeline.py 413 URL fetch failures swallowed → item silently marked ready with no artifacts/error (no retry)

SUGGESTION

File Line Issue
tinyagentos/routes/library.py 129 file.size pre-check is client-reported and often None/0 for streamed uploads; real cap is only the in-loop counter
Files Reviewed (7 files)
  • tinyagentos/library_pipeline.py - 3 issues
  • tinyagentos/routes/library.py - 2 issues
  • tinyagentos/library_store.py - 0 issues
  • tinyagentos/library_collections.py - 0 issues
  • tinyagentos/routes/__init__.py - 0 issues
  • tinyagentos/templates/library.html - 0 issues
  • tests/test_library.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by nemotron-3-ultra-550b-a55b:free · Input: 105K · Output: 10.4K · Cached: 278.5K

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
tinyagentos/library_pipeline.py (1)

407-409: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No response-size cap on fetched HTML. resp.text materializes the entire body into memory with only a timeout guarding the call. A large/hostile page can drive high memory use per ingest. Consider streaming with a max-bytes limit (e.g. via client.stream and aborting past a threshold) or checking Content-Length.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_pipeline.py` around lines 407 - 409, Update the
HTML-fetching flow around client.get and resp.text to enforce a maximum response
size before materializing the body. Prefer streaming through the existing
client, accumulate only up to the configured byte limit, and abort or reject
responses exceeding it; use Content-Length as an early check when available
while preserving normal HTML parsing for responses within the limit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tinyagentos/library_collections.py`:
- Around line 60-118: Track artifacts only after their copy to item_dir
succeeds, and build the manifest’s artifacts list from that successfully copied
collection instead of the original text_artifacts list. Update the per-artifact
loop and manifest construction in the surrounding collection function while
preserving ingestion behavior and skipping missing or failed-copy sources.

In `@tinyagentos/library_pipeline.py`:
- Around line 401-409: Update the HTTP fetch flow around validate_url_or_raise
and httpx.AsyncClient so redirects cannot bypass SSRF validation: disable
automatic redirects and manually inspect each redirect Location, validating its
resolved target with validate_url_or_raise before issuing the next request.
Preserve the existing response handling for non-redirect responses.

In `@tinyagentos/routes/library.py`:
- Around line 79-90: The lazy initialization in _get_library_store is vulnerable
to concurrent requests creating multiple LibraryStore instances across the await
store.init() boundary. Add and reuse an application-scoped asyncio lock to guard
the check, initialization, and assignment, rechecking
request.app.state.library_store after acquiring the lock so only one initialized
store is created and returned.
- Around line 239-271: Update _render_item_card to HTML-escape every
interpolated item field, including item_id, title, status, kind, and size_str,
before inserting them into the raw HTML fragment. Preserve the existing
defaults, status-class lookup, formatting, and _render_item_list behavior while
ensuring both ingest responses and polled lists cannot render
attacker-controlled markup.

In `@tinyagentos/templates/library.html`:
- Around line 75-91: Update the ingest requests in the library template,
including the form’s hx-post and the drop-zone hx-post, to send an X-CSRF-Token
header whose value matches the csrf_token cookie required by verify_csrf. Use
the existing client-side CSRF token mechanism if available, and ensure both
upload paths include the header without changing their request behavior.
- Around line 66-96: Remove the htmx request attributes from the outer
div.drop-zone, including hx-post, hx-encoding, hx-target, hx-swap, and
hx-indicator. Keep the corresponding attributes on form#ingest-form so the
existing direct drop-handler submission remains the sole ingest request.

---

Nitpick comments:
In `@tinyagentos/library_pipeline.py`:
- Around line 407-409: Update the HTML-fetching flow around client.get and
resp.text to enforce a maximum response size before materializing the body.
Prefer streaming through the existing client, accumulate only up to the
configured byte limit, and abort or reject responses exceeding it; use
Content-Length as an early check when available while preserving normal HTML
parsing for responses within the limit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 51c9530a-15d8-4af6-8185-1498414c1f72

📥 Commits

Reviewing files that changed from the base of the PR and between e2d931d and 96abf8b.

📒 Files selected for processing (7)
  • tests/test_library.py
  • tinyagentos/library_collections.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/library_store.py
  • tinyagentos/routes/__init__.py
  • tinyagentos/routes/library.py
  • tinyagentos/templates/library.html

Comment thread tinyagentos/library_collections.py Outdated
Comment on lines +60 to +118
handed_off = 0
for art in text_artifacts:
art_path = art.get("path", "")
if not art_path:
continue

src = Path(art_path)
if not src.exists():
continue

# Copy to collections folder
dst = item_dir / src.name
try:
dst.write_bytes(src.read_bytes())
except OSError:
logger.warning("Failed to copy artifact %s → %s", src, dst,
exc_info=True)
continue

# Ingest into taosmd
try:
import taosmd

text_content = src.read_text(encoding="utf-8", errors="replace")
await taosmd.ingest(
text_content,
agent=f"library-{item_id[:12]}",
project=project_id,
)
handed_off += 1
logger.debug("Library item %s artifact %s ingested into taosmd",
item_id, art["kind"])
except ImportError:
logger.debug("taosmd not available — collection indexing skipped")
# Still count as handed off since file is in place
handed_off += 1
except Exception:
logger.warning(
"taosmd ingest failed for item %s artifact %s",
item_id, art["kind"], exc_info=True,
)

# Write a manifest so downstream knows what's here
manifest = {
"item_id": item_id,
"title": item.get("title", ""),
"kind": item.get("kind", ""),
"source_url": item.get("source_url", ""),
"created_at": item.get("created_at", 0),
"artifacts": [
{"kind": a["kind"], "file": Path(a["path"]).name}
for a in text_artifacts if a.get("path")
],
}
manifest_path = item_dir / "manifest.json"
try:
manifest_path.write_text(json.dumps(manifest, indent=2))
except OSError:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Manifest can reference artifacts that were never actually copied.

The per-artifact loop continues (skips copying) when the source file is missing (Line 67-68) or the copy fails (Line 74-77), but the manifest's "artifacts" list is rebuilt afterward from the original text_artifacts collection, filtered only on a.get("path") — not on whether the copy actually succeeded. A manifest.json can therefore list files that don't exist under item_dir, breaking any downstream consumer that trusts the manifest.

🐛 Proposed fix: track successfully copied artifacts instead of re-deriving from the full list
     handed_off = 0
+    copied_artifacts = []
     for art in text_artifacts:
         art_path = art.get("path", "")
         if not art_path:
             continue

         src = Path(art_path)
         if not src.exists():
             continue

         # Copy to collections folder
         dst = item_dir / src.name
         try:
             dst.write_bytes(src.read_bytes())
         except OSError:
             logger.warning("Failed to copy artifact %s → %s", src, dst,
                            exc_info=True)
             continue
+        copied_artifacts.append(art)

         # Ingest into taosmd
         ...

     # Write a manifest so downstream knows what's here
     manifest = {
         ...
         "artifacts": [
-            {"kind": a["kind"], "file": Path(a["path"]).name}
-            for a in text_artifacts if a.get("path")
+            {"kind": a["kind"], "file": Path(a["path"]).name}
+            for a in copied_artifacts
         ],
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handed_off = 0
for art in text_artifacts:
art_path = art.get("path", "")
if not art_path:
continue
src = Path(art_path)
if not src.exists():
continue
# Copy to collections folder
dst = item_dir / src.name
try:
dst.write_bytes(src.read_bytes())
except OSError:
logger.warning("Failed to copy artifact %s → %s", src, dst,
exc_info=True)
continue
# Ingest into taosmd
try:
import taosmd
text_content = src.read_text(encoding="utf-8", errors="replace")
await taosmd.ingest(
text_content,
agent=f"library-{item_id[:12]}",
project=project_id,
)
handed_off += 1
logger.debug("Library item %s artifact %s ingested into taosmd",
item_id, art["kind"])
except ImportError:
logger.debug("taosmd not available — collection indexing skipped")
# Still count as handed off since file is in place
handed_off += 1
except Exception:
logger.warning(
"taosmd ingest failed for item %s artifact %s",
item_id, art["kind"], exc_info=True,
)
# Write a manifest so downstream knows what's here
manifest = {
"item_id": item_id,
"title": item.get("title", ""),
"kind": item.get("kind", ""),
"source_url": item.get("source_url", ""),
"created_at": item.get("created_at", 0),
"artifacts": [
{"kind": a["kind"], "file": Path(a["path"]).name}
for a in text_artifacts if a.get("path")
],
}
manifest_path = item_dir / "manifest.json"
try:
manifest_path.write_text(json.dumps(manifest, indent=2))
except OSError:
pass
handed_off = 0
copied_artifacts = []
for art in text_artifacts:
art_path = art.get("path", "")
if not art_path:
continue
src = Path(art_path)
if not src.exists():
continue
# Copy to collections folder
dst = item_dir / src.name
try:
dst.write_bytes(src.read_bytes())
except OSError:
logger.warning("Failed to copy artifact %s → %s", src, dst,
exc_info=True)
continue
copied_artifacts.append(art)
# Ingest into taosmd
try:
import taosmd
text_content = src.read_text(encoding="utf-8", errors="replace")
await taosmd.ingest(
text_content,
agent=f"library-{item_id[:12]}",
project=project_id,
)
handed_off += 1
logger.debug("Library item %s artifact %s ingested into taosmd",
item_id, art["kind"])
except ImportError:
logger.debug("taosmd not available — collection indexing skipped")
# Still count as handed off since file is in place
handed_off += 1
except Exception:
logger.warning(
"taosmd ingest failed for item %s artifact %s",
item_id, art["kind"], exc_info=True,
)
# Write a manifest so downstream knows what's here
manifest = {
"item_id": item_id,
"title": item.get("title", ""),
"kind": item.get("kind", ""),
"source_url": item.get("source_url", ""),
"created_at": item.get("created_at", 0),
"artifacts": [
{"kind": a["kind"], "file": Path(a["path"]).name}
for a in copied_artifacts
],
}
manifest_path = item_dir / "manifest.json"
try:
manifest_path.write_text(json.dumps(manifest, indent=2))
except OSError:
pass
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 115-115: use jsonify instead of json.dumps for JSON output
Context: json.dumps(manifest, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🪛 Ruff (0.15.21)

[warning] 96-96: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_collections.py` around lines 60 - 118, Track artifacts
only after their copy to item_dir succeeds, and build the manifest’s artifacts
list from that successfully copied collection instead of the original
text_artifacts list. Update the per-artifact loop and manifest construction in
the surrounding collection function while preserving ingestion behavior and
skipping missing or failed-copy sources.

Comment thread tinyagentos/library_pipeline.py Outdated
Comment thread tinyagentos/routes/library.py
Comment thread tinyagentos/routes/library.py Outdated
Comment thread tinyagentos/templates/library.html Outdated
Comment thread tinyagentos/templates/library.html Outdated
Comment on lines +75 to +91
<form class="ingest-form"
id="ingest-form"
hx-post="/api/library/ingest"
hx-encoding="multipart/form-data"
hx-target="#item-list"
hx-swap="afterbegin"
hx-indicator="#ingest-spinner">
<input type="file" name="file" id="file-input" style="display:none"
onchange="document.getElementById('file-name').textContent = this.files[0]?.name || ''">
<button type="button" class="secondary outline"
onclick="document.getElementById('file-input').click()">
Choose File
</button>
<span id="file-name" style="font-size:0.85rem; align-self:center"></span>
<input type="text" name="url" placeholder="Or paste a URL…" style="max-width:300px">
<button type="submit">Ingest</button>
</form>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'def verify_csrf' -A 25 tinyagentos/middleware/csrf.py
rg -n 'csrf' -i tests/conftest.py

Repository: jaylfc/taOS

Length of output: 2381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the library template and nearby scripts for any CSRF header injection.
sed -n '1,220p' tinyagentos/templates/library.html

printf '\n--- CSRF-related occurrences ---\n'
rg -n 'X-CSRF-Token|csrf_token|hx-headers|htmx:configRequest|setRequestHeader|verify_csrf' tinyagentos -g '!**/*.pyc'

printf '\n--- Test-mode bypass context ---\n'
sed -n '1,120p' tests/conftest.py

Repository: jaylfc/taOS

Length of output: 11223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any global client-side code that injects CSRF headers into htmx/fetch requests.
rg -n 'htmx:configRequest|X-CSRF-Token|csrf_token|hx-headers|setRequestHeader|beforeRequest|fetch\(' tinyagentos -g '!**/*.pyc'

printf '\n--- App/template JS files ---\n'
git ls-files 'tinyagentos/**/*.js' 'tinyagentos/**/*.html' 'tinyagentos/**/*.ts' | sed -n '1,200p'

Repository: jaylfc/taOS

Length of output: 1858


Add X-CSRF-Token to the ingest request
verify_csrf requires the X-CSRF-Token header to match the csrf_token cookie, but this template never sets it for either the form or the drop-zone hx-post. The route tests bypass CSRF in tests/conftest.py, so this will fail in normal use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/templates/library.html` around lines 75 - 91, Update the ingest
requests in the library template, including the form’s hx-post and the drop-zone
hx-post, to send an X-CSRF-Token header whose value matches the csrf_token
cookie required by verify_csrf. Use the existing client-side CSRF token
mechanism if available, and ensure both upload paths include the header without
changing their request behavior.

@jaylfc

jaylfc commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Library P1 is merged (#2062, dev sha 4498855). Three rounds of folds, all closed, including a data-loss fix inside an hour. Good work.

This PR is now CONFLICTING against dev and needs a rebase before it can be reviewed. Both P2 and P3 touch the four files P1 substantially rewrote:

  • tinyagentos/library_collections.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/library_store.py
  • tests/test_library.py

Please rebase onto current dev rather than merging dev in, so the diff stays reviewable. Read the merged P1 before resolving, because several things changed underneath you and a mechanical conflict resolution will reintroduce defects that were just fixed:

  1. Collections handoff now calls the real taosmd contract. Create returns {"collection": {...}} with the id nested, index is 202-then-poll rather than synchronous, and the poll response is nested the same way. If your branch still has the older shape anywhere, take P1's version.
  2. Reprocess must never unlink the item's storage_path. The pipeline records the source upload as a metadata artifact, so deleting every artifact path destroys the user's original file. P1 added an explicit guard. Do not let a conflict resolution drop it.
  3. Status transitions go through the compare-and-swap (try_update_item_status in library_store.py), not a read-then-write.
  4. Artifact-count assertions in tests are exact now, not tolerance-based. If your branch has an abs(...) <= N style assertion, replace it: a tolerance cannot distinguish correct behaviour from total loss, which is exactly how the data-loss bug stayed hidden.
  5. File modes are 0o640 for files and 0o2750 for directories, which the cross-user setup on the Pi depends on.

The canonical envelope reference is taosmd docs/collections.md, Response shapes section. Prefer it over anything in my earlier fold lists.

No rush on ordering: P2 first makes sense since P3 builds on it.

hognek added a commit to hognek/tinyagentos that referenced this pull request Jul 21, 2026
…nt TOCTOU race

Two concurrent requests that both see library_store as None could create
and init two separate LibraryStore instances, leaking connections and
racing on early writes. Add _store_init_lock (module-level asyncio.Lock)
with double-checked pattern — same approach as _config_lock in config.py.

Addresses CodeRabbit finding from PR jaylfc#2068 review cycle.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tinyagentos/routes/library.py (1)

167-175: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Skip collections handoff when the pipeline ended in error.

run_pipeline catches processor exceptions, marks the item error, and returns normally. Consequently, this outer try succeeds and can index partial artifacts produced before the failure.

Proposed fix
         await run_pipeline(store, item_id, storage_dir)
+        processed_item = await store.get_item(item_id)
+        if not processed_item or processed_item["status"] != "ready":
+            return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/library.py` around lines 167 - 175, Update the ingest flow
around run_pipeline so it communicates processor failure to the caller, then
make the collections handoff execute only when the pipeline completes
successfully. Preserve the existing error status update and exception logging,
and ensure partial artifacts are not handed off after run_pipeline catches an
error.
tinyagentos/library_collections.py (1)

80-127: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Remove stale collection files during reprocessing.

item_dir persists across runs, but only current artifacts are copied. If a transcript or OCR artifact disappears—or there are no text artifacts on the next run—the old file remains available to taosmd and can continue being indexed.

Rebuild the per-item directory from the successfully copied current artifacts before indexing, including the empty-artifact case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_collections.py` around lines 80 - 127, Update the
artifact-processing flow around item_dir and indexed_paths to rebuild the
per-item directory on every run: remove stale contents before copying current
text artifacts, including clearing it when text_artifacts is empty. Preserve
only files successfully copied from the current artifacts, then continue
indexing from the rebuilt directory.
🧹 Nitpick comments (2)
tests/test_library.py (2)

523-531: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Exercise the SSRF guard instead of only bypassing it.

The success tests mock validate_url_or_raise but never assert it was called. Add assertions plus blocked-URL and redirect-target cases so removal of per-hop validation cannot pass unnoticed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 523 - 531, Add assertions in the tests
around proc.process(item) to verify
tinyagentos.routes.desktop_browser.ssrf.validate_url_or_raise is called with the
expected URL. Add coverage for blocked URLs and redirect targets, asserting both
are rejected, so per-hop SSRF validation is exercised rather than bypassed by
the mock.

932-961: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Synchronize with background-task completion instead of sleeping.

The 0.5-second assumptions are CI-dependent, and the second reprocess is never awaited or verified. Poll for a terminal status with a timeout—or await the tracked task—before each assertion. Construct the conflict test directly in processing state rather than racing an external URL fetch.

Also applies to: 969-983

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 932 - 961, Replace the fixed
asyncio.sleep calls in the reprocessing test with polling for the item’s
terminal status, using a bounded timeout before checking artifact counts or
readiness. Await and verify the second /reprocess request completes without
duplicating artifacts, and update the conflict test to create the item directly
in processing state instead of racing an external URL fetch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_library.py`:
- Around line 415-419: Update the test around updated_title to assert that the
processed item’s title matches the expected YouTube video title, rather than
only retrieving the field. Remove the non-assertive comments and preserve the
existing update_item flow.

In `@tinyagentos/library_collections.py`:
- Around line 326-349: Validate the response returned by the collection-linking
POST in the linking flow before logging success or clearing
collection_retryable. Treat non-success HTTP statuses, including 401, 404, and
500, as failures and route them through the existing warning/error handling so
retry state is preserved; only perform the “Linked collection” log and metadata
update after confirmed success.

In `@tinyagentos/library_pipeline.py`:
- Around line 648-665: Remove the URL-only reference-artifact branch surrounding
the storage_path/source_url checks, including its logger message, ref_meta
construction, and store.add_artifact call. Allow registered processors to handle
URL-only YouTube/web items and fetch their content; only retain equivalent
handling if the implementation explicitly identifies genuinely unsupported URL
kinds.
- Around line 288-290: Update the fetch flow used by the media retrieval call to
impose a per-process timeout on each yt-dlp subprocess, and ensure timed-out or
cancelled processes are terminated, force-killed if necessary, and awaited so no
child remains running. Preserve normal successful fetch behavior and apply
cleanup consistently to all three yt-dlp invocations.
- Around line 415-422: Update the redirect-fetch loop around
validate_url_or_raise and client.get so the validated DNS address is pinned for
the connection, preventing a second hostname resolution; fetch using the pinned
IP while preserving the original Host header and TLS SNI, or alternatively
verify the connected peer address matches the validated result before accepting
the response.
- Around line 418-422: Update the web-fetch flow around httpx.AsyncClient and
client.get to use client.stream() so the response body is consumed
incrementally; enforce _MAX_WEB_BYTES during aiter_bytes() iteration before
accumulating content, while preserving the existing redirect and timeout
settings.

In `@tinyagentos/routes/library.py`:
- Around line 312-355: Make the reprocess flow around try_update_item_status and
artifact cleanup failure-safe: wrap destructive artifact deletion and task
scheduling in preparation error handling, transition the item to error on any
cleanup or scheduling failure, and do not roll back to ready after artifacts are
removed. Return the success response only after _track_background_task or
_create_supervised_task has successfully established task ownership.
- Around line 177-200: Update the item-processing lifecycle around run_pipeline
and handoff_to_collections so the per-item task/lock remains registered until
the collections handoff completes, rather than ending when the item is marked
ready. Make the reprocess endpoint consult that same active per-item state and
reject requests while either pipeline or handoff work is running, preserving
normal reprocessing once the lifecycle finishes.

---

Outside diff comments:
In `@tinyagentos/library_collections.py`:
- Around line 80-127: Update the artifact-processing flow around item_dir and
indexed_paths to rebuild the per-item directory on every run: remove stale
contents before copying current text artifacts, including clearing it when
text_artifacts is empty. Preserve only files successfully copied from the
current artifacts, then continue indexing from the rebuilt directory.

In `@tinyagentos/routes/library.py`:
- Around line 167-175: Update the ingest flow around run_pipeline so it
communicates processor failure to the caller, then make the collections handoff
execute only when the pipeline completes successfully. Preserve the existing
error status update and exception logging, and ensure partial artifacts are not
handed off after run_pipeline catches an error.

---

Nitpick comments:
In `@tests/test_library.py`:
- Around line 523-531: Add assertions in the tests around proc.process(item) to
verify tinyagentos.routes.desktop_browser.ssrf.validate_url_or_raise is called
with the expected URL. Add coverage for blocked URLs and redirect targets,
asserting both are rejected, so per-hop SSRF validation is exercised rather than
bypassed by the mock.
- Around line 932-961: Replace the fixed asyncio.sleep calls in the reprocessing
test with polling for the item’s terminal status, using a bounded timeout before
checking artifact counts or readiness. Await and verify the second /reprocess
request completes without duplicating artifacts, and update the conflict test to
create the item directly in processing state instead of racing an external URL
fetch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b55a9e94-cf49-4081-ba87-5eac9ed94031

📥 Commits

Reviewing files that changed from the base of the PR and between e08f98a and 0ee6447.

📒 Files selected for processing (6)
  • .gitignore
  • tests/test_library.py
  • tinyagentos/library_collections.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/library_store.py
  • tinyagentos/routes/library.py

Comment thread tests/test_library.py
Comment on lines +326 to +349
# 4. Link the collection to the library item.
try:
await http_client.post(
f"{base_url}/collections/{collection_id}/link",
json={"type": "taos", "id": item_id},
headers=auth_headers,
)
logger.debug(
"Linked collection %s to library item %s",
collection_id, item_id,
)
except Exception:
logger.warning(
"taosmd POST /collections/%s/link failed for item %s",
collection_id, item_id, exc_info=True,
)

# Clear retryable on success; the handoff completed.
if indexed > 0:
item_meta.pop("collection_retryable", None)
try:
await store.update_item(item_id, meta_json=item_meta)
except Exception:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the collection-link response before declaring success.

HTTP 401/404/500 responses do not raise here, so the code logs “Linked,” clears retry state, and returns an indexed count although the collection was never linked.

Proposed fix
-                await http_client.post(
+                link_resp = await http_client.post(
                     f"{base_url}/collections/{collection_id}/link",
                     json={"type": "taos", "id": item_id},
                     headers=auth_headers,
                 )
+                if link_resp.status_code >= 400:
+                    logger.warning(
+                        "taosmd POST /collections/%s/link returned %d",
+                        collection_id, link_resp.status_code,
+                    )
+                    await _set_retryable()
+                    return 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 4. Link the collection to the library item.
try:
await http_client.post(
f"{base_url}/collections/{collection_id}/link",
json={"type": "taos", "id": item_id},
headers=auth_headers,
)
logger.debug(
"Linked collection %s to library item %s",
collection_id, item_id,
)
except Exception:
logger.warning(
"taosmd POST /collections/%s/link failed for item %s",
collection_id, item_id, exc_info=True,
)
# Clear retryable on success; the handoff completed.
if indexed > 0:
item_meta.pop("collection_retryable", None)
try:
await store.update_item(item_id, meta_json=item_meta)
except Exception:
pass
# 4. Link the collection to the library item.
try:
link_resp = await http_client.post(
f"{base_url}/collections/{collection_id}/link",
json={"type": "taos", "id": item_id},
headers=auth_headers,
)
if link_resp.status_code >= 400:
logger.warning(
"taosmd POST /collections/%s/link returned %d",
collection_id, link_resp.status_code,
)
await _set_retryable()
return 0
logger.debug(
"Linked collection %s to library item %s",
collection_id, item_id,
)
except Exception:
logger.warning(
"taosmd POST /collections/%s/link failed for item %s",
collection_id, item_id, exc_info=True,
)
# Clear retryable on success; the handoff completed.
if indexed > 0:
item_meta.pop("collection_retryable", None)
try:
await store.update_item(item_id, meta_json=item_meta)
except Exception:
pass
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 337-337: Do not catch blind exception: Exception

(BLE001)


[error] 348-349: try-except-pass detected, consider logging the exception

(S110)


[warning] 348-348: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_collections.py` around lines 326 - 349, Validate the
response returned by the collection-linking POST in the linking flow before
logging success or clearing collection_retryable. Treat non-success HTTP
statuses, including 401, 404, and 500, as failures and route them through the
existing warning/error handling so retry state is preserved; only perform the
“Linked collection” log and metadata update after confirmed success.

Comment on lines +288 to +290
media_dir = self.storage_dir / "youtube"
result = await fetch(source_url, media_dir=media_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline tinyagentos/knowledge_fetchers/youtube.py \
  --items all --type function --match fetch --view expanded
rg -nP -C3 '\b(create_subprocess_exec|communicate|wait_for|timeout|terminate|kill)\b' \
  tinyagentos/knowledge_fetchers/youtube.py tinyagentos/library_pipeline.py

Repository: jaylfc/taOS

Length of output: 5313


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## tinyagentos/knowledge_fetchers/youtube.py outline\n'
ast-grep outline tinyagentos/knowledge_fetchers/youtube.py --items all --type function --match fetch --view expanded

printf '\n## tinyagentos/knowledge_fetchers/youtube.py relevant ranges\n'
sed -n '110,210p' tinyagentos/knowledge_fetchers/youtube.py | cat -n

printf '\n## tinyagentos/library_pipeline.py call site and surrounding logic\n'
sed -n '270,310p' tinyagentos/library_pipeline.py | cat -n

Repository: jaylfc/taOS

Length of output: 6735


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('tinyagentos/knowledge_fetchers/youtube.py')
text = p.read_text()
# probe for cancellation / cleanup patterns
for needle in ['wait_for(', 'asyncio.timeout', 'terminate(', 'kill(', 'try:', 'CancelledError', 'finally:']:
    print(f'{needle}:', needle in text)
PY

Repository: jaylfc/taOS

Length of output: 269


Bound and clean up the yt-dlp subprocesses.
fetch() waits on three yt-dlp invocations with no timeout or cancellation cleanup; a hung child can stall ingestion indefinitely. Add per-process deadlines and terminate/kill/reap timed-out children.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_pipeline.py` around lines 288 - 290, Update the fetch
flow used by the media retrieval call to impose a per-process timeout on each
yt-dlp subprocess, and ensure timed-out or cancelled processes are terminated,
force-killed if necessary, and awaited so no child remains running. Preserve
normal successful fetch behavior and apply cleanup consistently to all three
yt-dlp invocations.

Comment thread tinyagentos/library_pipeline.py Outdated
Comment on lines +415 to +422
for _hop in range(_MAX_WEB_REDIRECTS + 1):
validate_url_or_raise(current_url)

async with httpx.AsyncClient(
timeout=httpx.Timeout(30),
follow_redirects=False,
) as client:
resp = await client.get(current_url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline tinyagentos/routes/desktop_browser/ssrf.py \
  --items all --type function,class \
  --match 'validate_url_or_raise|validate_resolved_addr|SsrfBlockedError'
rg -nP -C3 \
  'getaddrinfo|validate_url_or_raise|AsyncClient|AsyncHTTPTransport|network_stream|get_extra_info' \
  tinyagentos

Repository: jaylfc/taOS

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- tinyagentos/routes/desktop_browser/ssrf.py ---'
sed -n '1,220p' tinyagentos/routes/desktop_browser/ssrf.py

echo
echo '--- tinyagentos/library_pipeline.py (390-460) ---'
sed -n '390,460p' tinyagentos/library_pipeline.py

echo
echo '--- tinyagentos/routes/userspace_apps.py (150-175) ---'
sed -n '150,175p' tinyagentos/routes/userspace_apps.py

Repository: jaylfc/taOS

Length of output: 10653


🏁 Script executed:

#!/bin/bash
echo hi

Repository: jaylfc/taOS

Length of output: 152


Pin the validated IP before fetching tinyagentos/library_pipeline.py:416-422 only validates the DNS result once; client.get(current_url) resolves the hostname again on connect, leaving a DNS-rebinding SSRF gap. Resolve once and fetch the pinned IP while preserving the original Host/SNI, or verify the peer address from the socket.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_pipeline.py` around lines 415 - 422, Update the
redirect-fetch loop around validate_url_or_raise and client.get so the validated
DNS address is pinned for the connection, preventing a second hostname
resolution; fetch using the pinned IP while preserving the original Host header
and TLS SNI, or alternatively verify the connected peer address matches the
validated result before accepting the response.

Comment thread tinyagentos/library_pipeline.py Outdated
Comment thread tinyagentos/library_pipeline.py Outdated
Comment on lines +648 to +665
# URL-only items (no storage_path) are stored as references — the pipeline
# records a reference metadata artifact but does not fetch remote content
# (future: WebFetcherProcessor, #2078). The item gets a "reference" artifact so it
# is not silently empty.
if not item.get("storage_path") and item.get("source_url"):
logger.info(
"Library pipeline: URL-only item %s (%s) — stored as reference, not fetched",
item_id, kind,
)
ref_meta = {
"source_url": item["source_url"],
"kind": kind,
"note": "Reference-only item — content not fetched (TODO: WebFetcherProcessor)",
}
await store.add_artifact(
item_id, kind="reference", path="", meta=ref_meta,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Remove the obsolete reference-only artifact.

Every URL-only YouTube/web item records “content not fetched,” then its registered processor fetches the content. Remove this block or restrict it to genuinely unsupported URL kinds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/library_pipeline.py` around lines 648 - 665, Remove the URL-only
reference-artifact branch surrounding the storage_path/source_url checks,
including its logger message, ref_meta construction, and store.add_artifact
call. Allow registered processors to handle URL-only YouTube/web items and fetch
their content; only retain equivalent handling if the implementation explicitly
identifies genuinely unsupported URL kinds.

Comment thread tinyagentos/routes/library.py
Comment on lines +312 to +355
# Atomic CAS: transition to pending only when NOT already pending/processing.
# Beats the TOCTOU race — two concurrent reprocess requests cannot both
# pass a separate read-then-write guard.
if not await store.try_update_item_status(item_id, "pending",
if_not_in=("pending", "processing")):
return JSONResponse(
{"error": "Item is currently being processed"}, status_code=409
)

# Delete old artifacts so reprocess is idempotent.
# Guard: never unlink the user's original uploaded file (storage_path).
storage_dir = _library_dir(request)
old_artifacts = await store.get_artifacts(item_id)
item_storage_path = item.get("storage_path", "")
for art in old_artifacts:
art_path = art.get("path", "")
if art_path and art_path == item_storage_path:
# This artifact records the source file — skip deletion.
logger.debug("Skipping unlink of source file %s for item %s",
art_path, item_id)
elif art_path and (ap := Path(art_path)).exists():
try:
ap.unlink()
except OSError:
logger.warning("Failed to remove artifact %s for item %s",
art_path, item_id)
await store.delete_artifact(art["id"])

# Status was already atomically set to pending by try_update_item_status above;
# re-queue the pipeline now. If scheduling fails, roll back to ready so the
# item is not stuck pending forever.
try:
task_set = getattr(request.app.state, "_background_tasks", None)
coro = _ingest_task(request.app, item_id, store, storage_dir)
if task_set is None:
_track_background_task(coro)
else:
_create_supervised_task(coro, task_set)
except Exception:
logger.exception("Failed to schedule reprocess for item %s", item_id)
await store.update_item_status(item_id, "ready")
return JSONResponse(
{"error": "Failed to schedule reprocess"}, status_code=500,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make destructive reprocess preparation failure-safe.

After the CAS sets pending, any cleanup exception strands the item partially deleted and pending. If scheduling fails, rolling back to ready is also incorrect because its artifacts have already been removed. Catch preparation failures and move the item to error; only expose a successful queued state once task ownership is established.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/library.py` around lines 312 - 355, Make the reprocess
flow around try_update_item_status and artifact cleanup failure-safe: wrap
destructive artifact deletion and task scheduling in preparation error handling,
transition the item to error on any cleanup or scheduling failure, and do not
roll back to ready after artifacts are removed. Return the success response only
after _track_background_task or _create_supervised_task has successfully
established task ownership.

hognek added 4 commits July 21, 2026 18:27
…ngestor

Add YouTubeProcessor for url:youtube items — fetches metadata, thumbnail,
transcript, and chapters via knowledge_fetchers.youtube (yt-dlp). Cheap-tier
only: no video download, just text artifacts for collection indexing.

Add WebProcessor for url:web items — fetches HTML (SSRF-guarded), extracts
readable text via readability-lxml (falls back to tag-stripping). Auto-titles
from <title> tag.

Both registered in _PROCESSORS dict; run_pipeline dispatches to them
for url:youtube and url:web kinds.

Design doc: docs/design/library-app.md sections 3-4.
Tests: 8 new tests, all 47 library tests pass.
… exceptions

- _render_item_card: escape all user-controlled values (title, kind, id,
  status) with html.escape() to prevent reflected XSS through crafted
  page titles, YouTube metadata, or user form fields.
- WebProcessor: use html.unescape() on extracted <title> text to decode
  HTML entities before storage.
- YouTubeProcessor/WebProcessor: remove bare except Exception that
  swallowed fetch failures. Let network/SSRF/yt-dlp errors propagate to
  run_pipeline which already marks items as 'error' — a failed URL fetch
  must not silently appear 'ready' with zero artifacts.

All 47 library tests pass.
…ponse-size cap

- WebProcessor: disable auto-redirects, manually validate every redirect
  hop with validate_url_or_raise() against the SSRF blocklist (same
  pattern as knowledge_ingest._download_article). Max 5 redirects.
- WebProcessor: cap response body at 10 MB, stream with aiter_bytes()
  to avoid OOM on large/hostile pages.
- Tests: update _mock_httpx_response with is_redirect=False, encoding,
  and aiter_bytes() to match the new fetch flow.

All library tests pass.
…nt TOCTOU race

Two concurrent requests that both see library_store as None could create
and init two separate LibraryStore instances, leaking connections and
racing on early writes. Add _store_init_lock (module-level asyncio.Lock)
with double-checked pattern — same approach as _config_lock in config.py.

Addresses CodeRabbit finding from PR jaylfc#2068 review cycle.
@hognek
hognek force-pushed the feat/library-p2 branch from 88bfcc9 to 613713c Compare July 21, 2026 16:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/test_library.py (2)

883-908: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Docstring claims 6 endpoints, only 5 are tested.

Both the class comment (Line 885-886) and docstring (Line 890) say "6 … endpoints", but endpoints only lists 5 entries (Line 894-900). Either the count is stale or a 6th endpoint's auth check is missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 883 - 908, Align the unauthenticated
endpoint test’s count with the actual coverage: inspect the library routes and
either add the missing sixth endpoint to the endpoints list in
test_unauth_library_endpoints or update the surrounding comment and docstring to
state five endpoints. Keep the 401 assertion behavior unchanged for every listed
route.

817-822: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stub library ingest fetchers in TestLibraryRoutes.
test_ingest_url, test_ingest_and_get, test_filter_by_kind, and test_reprocess_while_processing_returns_409 queue run_pipeline() in the background, and WebProcessor / YouTubeProcessor still hit real httpx.AsyncClient and yt-dlp calls. Add a shared autouse fixture to patch those fetch paths so these cases stay deterministic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 817 - 822, Update TestLibraryRoutes with
a shared autouse fixture that stubs the WebProcessor and YouTubeProcessor fetch
paths used by background run_pipeline() execution, preventing real
httpx.AsyncClient and yt-dlp calls. Ensure the fixture applies to
test_ingest_url, test_ingest_and_get, test_filter_by_kind, and
test_reprocess_while_processing_returns_409 while preserving their existing
assertions and pipeline behavior.
🧹 Nitpick comments (4)
tests/test_library.py (4)

706-793: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant local re-imports of already module-level names.

AsyncMock/patch/MagicMock are imported locally at Line 709 and 727, duplicating the module-level import at Line 8.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 706 - 793, Remove the redundant local
imports of AsyncMock, patch, and MagicMock from test_handoff_with_qmd; reuse the
existing module-level imports while leaving the test setup and behavior
unchanged.

344-344: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer next(...) over indexing a full list comprehension.

Flagged by Ruff RUF015.

Proposed fix
-        thumb_art = [a for a in artifacts if a["kind"] == "thumbnail"][0]
+        thumb_art = next(a for a in artifacts if a["kind"] == "thumbnail")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` at line 344, Update the thumbnail lookup in the
artifact-selection test to use next(...) over a generator expression instead of
building and indexing a full list comprehension, while preserving selection of
the first artifact whose kind is "thumbnail".

Source: Linters/SAST tools


499-544: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

No test coverage for WebProcessor's redirect-loop / SSRF revalidation.

WebProcessor.process implements a bounded redirect loop that re-validates each hop against the SSRF blocklist per "disable auto-redirects, manually validate each hop against the SSRF blocklist, and cap total response bytes" — a security-sensitive path that's entirely untested here (_mock_httpx_response hardcodes is_redirect = False, and _MAX_WEB_REDIRECTS/size-cap paths aren't exercised). Consider extending _mock_httpx_stream to return a redirect response followed by a final response to cover this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 499 - 544, The WebProcessor tests do not
cover manual redirect handling, per-hop SSRF validation, or redirect limits.
Extend test_process_web_url and the _mock_httpx_stream setup to return a
redirect response followed by a final response, assert each hop is validated and
the final content is processed, and add coverage for the bounded
redirect/response-size behavior using the existing _MAX_WEB_REDIRECTS and
size-cap paths.

913-956: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fixed asyncio.sleep(0.5) to await background pipeline completion is inherently flaky.

Both test_reprocess_idempotent and test_reprocess_while_processing_returns_409 assume the background pipeline finishes within 500ms. Under CI load or slower I/O this can race and produce intermittent failures/false negatives (e.g., asserting artifact counts or status before the pipeline actually completes).

♻️ Suggested polling helper instead of fixed sleep
-        # Wait for pipeline to finish (background task)
-        import asyncio
-        await asyncio.sleep(0.5)
+        # Wait for pipeline to finish (background task), polling instead of a fixed sleep
+        import asyncio
+        for _ in range(20):
+            resp = await client.get(f"/api/library/items/{item_id}")
+            if resp.json()["item"].get("status") in ("ready", "error"):
+                break
+            await asyncio.sleep(0.1)

Also applies to: 958-978

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_library.py` around lines 913 - 956, Replace the fixed
asyncio.sleep(0.5) waits in test_reprocess_idempotent and
test_reprocess_while_processing_returns_409 with polling that repeatedly fetches
the item until the expected pipeline status or artifacts are available, using a
bounded timeout and short interval. Preserve the existing assertions and
409-in-progress behavior while making completion checks reliable under slower CI
conditions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tinyagentos/library_pipeline.py`:
- Around line 418-447: Move resp.raise_for_status() and the response body
consumption into the async with client.stream context in the redirect loop.
Preserve redirect handling and only break after the final response has been
validated and fully read, storing the decoded body for subsequent processing.

---

Outside diff comments:
In `@tests/test_library.py`:
- Around line 883-908: Align the unauthenticated endpoint test’s count with the
actual coverage: inspect the library routes and either add the missing sixth
endpoint to the endpoints list in test_unauth_library_endpoints or update the
surrounding comment and docstring to state five endpoints. Keep the 401
assertion behavior unchanged for every listed route.
- Around line 817-822: Update TestLibraryRoutes with a shared autouse fixture
that stubs the WebProcessor and YouTubeProcessor fetch paths used by background
run_pipeline() execution, preventing real httpx.AsyncClient and yt-dlp calls.
Ensure the fixture applies to test_ingest_url, test_ingest_and_get,
test_filter_by_kind, and test_reprocess_while_processing_returns_409 while
preserving their existing assertions and pipeline behavior.

---

Nitpick comments:
In `@tests/test_library.py`:
- Around line 706-793: Remove the redundant local imports of AsyncMock, patch,
and MagicMock from test_handoff_with_qmd; reuse the existing module-level
imports while leaving the test setup and behavior unchanged.
- Line 344: Update the thumbnail lookup in the artifact-selection test to use
next(...) over a generator expression instead of building and indexing a full
list comprehension, while preserving selection of the first artifact whose kind
is "thumbnail".
- Around line 499-544: The WebProcessor tests do not cover manual redirect
handling, per-hop SSRF validation, or redirect limits. Extend
test_process_web_url and the _mock_httpx_stream setup to return a redirect
response followed by a final response, assert each hop is validated and the
final content is processed, and add coverage for the bounded
redirect/response-size behavior using the existing _MAX_WEB_REDIRECTS and
size-cap paths.
- Around line 913-956: Replace the fixed asyncio.sleep(0.5) waits in
test_reprocess_idempotent and test_reprocess_while_processing_returns_409 with
polling that repeatedly fetches the item until the expected pipeline status or
artifacts are available, using a bounded timeout and short interval. Preserve
the existing assertions and 409-in-progress behavior while making completion
checks reliable under slower CI conditions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 54ffe829-c02a-4f03-aa80-1b824889963b

📥 Commits

Reviewing files that changed from the base of the PR and between 88bfcc9 and 2bc0bb8.

📒 Files selected for processing (3)
  • tests/test_library.py
  • tinyagentos/library_pipeline.py
  • tinyagentos/routes/library.py

Comment thread tinyagentos/library_pipeline.py Outdated
…#2068

CRITICAL: fix httpx stream context bug — resp used after async with exits
in WebProcessor redirect loop. Body consumption moved inside stream context.

MAJOR: reprocess failure-safety — roll back to error (not ready) after
artifact deletion to avoid data-less items stuck in ready state.

MAJOR: serialise reprocess against collections handoff — CAS-flip back to
processing in _ingest_task so reprocess sees busy state during handoff.

MAJOR: add asyncio.wait_for(120s) timeout on yt-dlp fetch in YouTubeProcessor.

MAJOR: validate collection-link POST response — raise_for_status() so
HTTP 401/404/500 are caught instead of silently clearing retryable state.

MINOR: replace flaky asyncio.sleep(0.5) with bounded status polling in
reprocess idempotent/409 tests.

MINOR: remove redundant local imports of AsyncMock/MagicMock/patch in
test_handoff_with_qmd; promote AsyncMock to module level.

MINOR: fix docstring/comment from 6 to 5 endpoints in test_unauth_library.

MINOR: prefer next() over list comprehension indexing for thumbnail lookup.

50/50 tests pass.
@jaylfc

jaylfc commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Noted that #2085 was opened for the same content and I have closed it as superseded by this PR, since both pointed at 52c3c7ab with byte-identical diffs. No work lost, and the review history stays in one place.

For the next round: fold findings into the PR that has them rather than opening a sibling. The rebase onto the merged P1 was the right move and this branch has it.

I will review this properly once the CI matrix settles. Worth knowing before then: the base moved substantially under you when P1 merged, so please confirm the conflict resolution preserved P1's four fixes rather than reverting any of them. The one that matters most is the guard that stops reprocess unlinking the item's storage_path, because losing that silently deletes the user's original upload.

@jaylfc

jaylfc commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Reviewed at head 52c3c7ab. The rebase is clean — I checked all six of P1's fixes explicitly and every one survived: the reprocess storage_path unlink guard (routes/library.py:339-348), both nested envelope unwraps, the 202-then-poll indexing, the CAS status transition, exact test assertions with zero abs( remaining, and the 0o2750/0o640 modes. Nothing was reverted by the conflict resolution, which was the thing I was most worried about. The SSRF work is also genuinely good, and I will come back to that.

But this is a HOLD on one new defect that undoes a guarantee an earlier commit in this same PR established.

1. BLOCKING: routes/library.py:216 overwrites failed pipelines to ready.

run_pipeline swallows its own exceptions (library_pipeline.py:712 sets error inside the except and returns normally). _ingest_task therefore sees no exception, passes the CAS at :182 because error is not in ("pending","processing"), and then unconditionally writes ready at :216.

Reproduced on your head with a url:web item pointed at http://127.0.0.1:1/x:

RESULT_STATUS:    ready      (expected: error)
RESULT_ARTIFACTS: 1          (a metadata stub, no content)

This is exactly the failure mode your own commit d4af00c was written to prevent, quoting its message: "a failed URL fetch must not silently appear ready with zero artifacts". Commit 52c3c7a reintroduces it one layer up. It also defeats P1's missing-source-file guard at library_pipeline.py:687. On dev the trailing ready write does not exist, so error stood — this is new in P2.

It is invisible to the suite because test_pipeline_error_status calls run_pipeline directly and never drives _ingest_task. 50 tests pass with the bug present, which is the same assertion-boundary problem that let the P1 data-loss bug stay green. Fix: re-read status after run_pipeline and skip both the CAS and the ready write when it is terminal, or make run_pipeline re-raise. Add a test that drives _ingest_task end to end on a failing item.

2. library_pipeline.py:292 leaks yt-dlp subprocesses on timeout. knowledge_fetchers/youtube.py spawns three create_subprocess_exec calls (:127, :164, :181) with no try/finally, no kill(), no CancelledError handling. When the 120s wait_for fires the coroutine is cancelled but the child keeps running. On a 4GB Pi a few hung fetches matter.

3. library_pipeline.py:473-477 uses resp after its async with has exited. Your commit message claims this was fixed, but only the body read moved inside; resp.headers and resp.status_code are still read outside. It does not raise today (only .text/.content do), so it is fragile rather than broken. Capture status and content-type into locals inside the context.

4. library_pipeline.py:441-455: WebProcessor has no content-type check. Any non-YouTube http(s) URL lands here, so a 10MB binary or video is fetched, .decode(errors="replace")d and stored as a text artifact, then indexed into a taosmd collection. Gate on text/html or text/* before extraction.

5. Three silent scope shortfalls vs #2059 (pitfall 19 — state deferrals explicitly rather than dropping them):

  • The full-page screenshot artifact (issue item 2) is absent entirely.
  • "subtitles/transcript (all languages)" is English only: --sub-lang en at youtube.py:184 and a hardcoded "language": "en" at library_pipeline.py:340.
  • Failure states on the item card (unavailable video, no transcript, geo-block) are not implemented, and fold 1 means they would surface as ready regardless.

Credit where it is due on SSRF, because this is the strongest part of the PR: follow_redirects=False with validate_url_or_raise on every hop, 5-redirect cap raising SsrfBlockedError, 30s timeout, 10MB streamed cap. I verified 169.254.169.254, loopback and RFC1918 are all rejected. The YouTube path has no validator but is host-locked by detect_kind, and I checked the usual bypasses (youtube.com.evil.com, youtube.com@evil.com) — both fail the match because the following character is not /. Kind is set server-side so a client cannot spoof it. Neither processor fetches at request time. That is a careful job.

Bot state, so you do not chase noise: all checks including both test matrices genuinely ran and passed on this fork PR. Kilo's last summary reviewed an earlier increment and explicitly scoped itself out of library_collections.py and routes/library.py, which is where fold 1 lives. CodeRabbit auto-paused after the commit influx; of its earlier set, the reprocess-rollback fix is correct, the stream-context CRITICAL is only partly fixed (fold 3), and the handoff-serialisation change is where fold 1 was introduced.

Fold 1 is the gate.

…process leak, resp-after-exit, content-type gate

BLOCKING (routes/library.py:216): _ingest_task re-reads status after
run_pipeline and aborts collections handoff when the pipeline left the
item in error state.  Commit d4af00c guaranteed that a failed fetch
never silently appears ready with zero artifacts; commit 52c3c7a
reintroduced the bug one layer up.  Added E2E test that drives
_ingest_task on a missing-file item (50 prior tests passed with the
bug present because none exercised the _ingest_task codepath).

Non-blocking fixes:
- youtube.py: track all subprocess handles in a list, kill in finally
  so CancelledError/timeout in the 120s wait_for doesn't leak yt-dlp
  children (pitfall on low-memory hardware)
- library_pipeline.py WebProcessor: capture resp.headers/content-type
  and resp.status_code into locals inside the async with context;
  gate on text/* before body download (10MB binaries were stored as
  text and indexed into collections)
- library_pipeline.py: TO-DO deferrals per jaylfc — screenshot
  artifact, multi-language subtitles, explicit failure sub-states
  (all three referencing jaylfc#2059)

51/51 targeted tests pass.
@hognek

hognek commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Folded all review findings at c8f6416:

BLOCKING fix: routes/library.py — _ingest_task now re-reads status after run_pipeline and aborts collections handoff when the pipeline left the item in error. Added E2E test test_ingest_task_preserves_error_status that drives _ingest_task on a missing-file item.

Non-blocking fixes:

51/51 targeted tests pass.

"for %s (content-type=%s)",
source_url, resp_content_type,
)
return artifacts

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: Non-text responses (PDFs, images, videos, binaries) get a polite logger.info and an empty artifact list — then the pipeline cheerfully marks the item ready. The user sees a green checkmark, the collections indexer gets nothing, and nobody knows the URL pointed to a 50MB ZIP file instead of an article. "Successfully processed! (Just kidding, we skipped it.)"

🩹 The Fix: Either mark the item error with a clear reason (non-text content-type: application/pdf), or introduce a distinct terminal status like skipped/unsupported. At minimum, don't let ready mean "we looked at it and shrugged."

📏 Severity: warning

try:
p.kill()
except ProcessLookupError:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: _cleanup_procs only catches ProcessLookupError. If p.kill() raises PermissionError (container restrictions) or OSError (weird kernel state), the exception escapes the finally block and masks the original CancelledError/TimeoutError. The root cause gets lost in the noise.

🩹 The Fix:

Suggested change
pass
for p in _procs:
if p.returncode is None:
try:
p.kill()
except Exception:
pass

📏 Severity: nitpick

Comment thread tests/test_library.py
assert "transcript" in artifact_kinds


class TestWebProcessor:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: The WebProcessor just got three shiny new security features — manual redirect validation (SSRF per hop), 10 MB streaming size cap, and content-type gating — and the test suite doesn't exercise a single one. test_process_web_url still mocks a happy-path 200 OK text/html response. No test for redirect chains, no test for size-limit ValueError, no test for image/png early return, no test for SsrfBlockedError on redirect loops. These are the exact paths that bite you in production.

🩹 The Fix: Add focused tests for each new guard:

  • test_web_redirect_chain_validated_per_hop
  • test_web_size_cap_raises_error
  • test_web_non_text_content_type_returns_empty (or errors, see library_pipeline.py:452)
  • test_web_too_many_redirects_raises_ssrf_blocked

📏 Severity: suggestion

Comment thread tests/test_library.py
assert len(artifacts) == 0


class TestYouTubeProcessor:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: YouTubeProcessor now wraps the whole fetch() call in asyncio.wait_for(..., timeout=120) and the fetcher tracks subprocesses to kill them on cancellation — but there's no test verifying the timeout fires, no test verifying zombie yt-dlp processes get reaped on CancelledError, and no test for the 120s boundary. The happy path is covered; the failure modes that motivated the code aren't.

🩹 The Fix: Add tests using pytest.mark.asyncio with a patched fetch that await asyncio.sleep(200) to trigger the timeout, and assert the item status becomes error. Also verify _cleanup_procs is called (mock p.kill).

📏 Severity: suggestion

_MAX_WEB_BYTES = 10 * 1024 * 1024 # 10 MB

current_url = source_url
resp_status_code: int = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔥 The Roast: resp_status_code and resp_content_type are captured inside the redirect loop but declared as resp_status_code: int = 0 and resp_content_type: str = "" up top. The names imply they belong to the final response, but they're actually mutated inside the loop. A future maintainer will wonder why they're initialized to dummy values instead of being assigned once after the loop breaks. It works, but it smells like a leftover from when the code lived outside the loop.

🩹 The Fix: Move the declarations inside the non-redirect block where they're actually set, or assign them once after the loop using the final resp (which is still in scope if you restructure slightly). Current pattern works but reads like a half-refactor.

📏 Severity: nitpick

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