Skip to content

fix: batch dedupe must ignore TTL expiry (zero-loss); scope depends_on edges on task create (security) - #190

Merged
jaylfc merged 2 commits into
masterfrom
fix/audit-followups
Jul 9, 2026
Merged

fix: batch dedupe must ignore TTL expiry (zero-loss); scope depends_on edges on task create (security)#190
jaylfc merged 2 commits into
masterfrom
fix/audit-followups

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Two follow-ups from a full cross-cutting audit of the tree (both confirmed by @taOS-dev on the bus, with fix directions). TDD: failing test first, watched it fail for the right reason, then fixed.

Bug 1 (major, zero-loss + broken batch idempotency) — taosmd/vector_memory.py

The earlier forget_after work taught _load_active_rows to hide TTL-expired rows from recall. But existing_source_ids() (the set that makes POST /ingest/batch idempotent) is built on _load_active_rows, so once a batch item's forget_after passed, its source_id dropped out of the dedup set. Re-POSTing the same id then re-ingested it, writing a second archive and vector row on every re-POST — unbounded duplication of hidden-but-still-present content, a zero-loss violation.

Fix: dedup now reads all physically-present rows via a new include_expired path on _load_active_rows. A TTL-expired row is only hidden from recall; it still exists on disk, so its id still dedupes. Superseded rows (valid_to set) stay excluded from the set — intentionally-cleared content should re-add on re-import — and recall-time TTL behavior is untouched (search still hides expired rows).

Bug 2 (major, security scoping gap) — taosmd/http_server.py _handle_task_create

Create was the one graph-mutating path that skipped _enforce_edge_project_scope. It applied token binding, then passed depends_on straight into create_task, which makes a blocks edge with no scope check. A project-scoped registry token could therefore create cross-project edges and enumerate foreign task existence (real id → 200, bogus id → 400 naming the missing task).

Fix: when a token binds a project, each depends_on id is checked to belong to that project before create_task, returning the same non-enumerating 403 the edge endpoints use (foreign and nonexistent ids indistinguishable). Tokenless/standalone unchanged.

Tests

  • tests/test_ttl_filter.py: test_ingest_batch_expired_item_still_dedupes (re-POST an expired id → ingested=0/skipped=1, one physical row), test_existing_source_ids_includes_expired_rows.
  • tests/test_http_server_registry_auth.py: cross-project depends_on → 403; foreign vs bogus → identical 403 (non-enumerating); same-project → success; tokenless → unchanged.

Full suite: 1086 passed.

Summary by CodeRabbit

  • Bug Fixes
    • Batch ingestion now prevents duplicate records even when previously stored items have expired.
    • Task creation now enforces project boundaries for dependencies, blocking cross-project references without revealing whether referenced tasks exist.
    • Same-project and tokenless dependency workflows continue to operate as expected.

…n edges on task create (security)

Two follow-ups from a cross-cutting audit.

Zero-loss: existing_source_ids() backed batch idempotency on
_load_active_rows, which hides TTL-expired rows. Once an item's
forget_after passed, its source_id left the dedup set and a re-POST
re-ingested it, writing a second archive/vector row every time. Dedup
now reads all physically-present rows (new include_expired path);
superseded rows stay excluded so cleared content still re-adds, and
recall-time TTL behavior is unchanged.

Security: POST /tasks with depends_on skipped _enforce_edge_project_scope,
letting a project-scoped token create cross-project blocks edges and
enumerate foreign task existence. Each depends_on id is now scope-checked
against the bound project with the same non-enumerating 403 as the edge
endpoints. Tokenless/standalone unchanged.
@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 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 58 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7616f98a-da95-40fb-bd6c-1586d64e7180

📥 Commits

Reviewing files that changed from the base of the PR and between 8a84b8c and caf7aae.

📒 Files selected for processing (2)
  • taosmd/http_server.py
  • tests/test_http_server_registry_auth.py
📝 Walkthrough

Walkthrough

The change preserves expired physical rows for batch deduplication while keeping them hidden from recall, and adds project-scope validation for depends_on task creation with non-enumerating 403 responses.

Changes

TTL deduplication

Layer / File(s) Summary
Expired-row deduplication
taosmd/vector_memory.py, tests/test_ttl_filter.py
existing_source_ids() includes non-superseded rows after TTL expiry, while recall filtering remains unchanged; regression tests verify duplicate batches are skipped and expired source IDs are reported.

Task dependency project scope

Layer / File(s) Summary
Scoped dependency validation
taosmd/http_server.py, tests/test_http_server_registry_auth.py, CHANGELOG.md
POST /tasks validates each depends_on ID against the bound project, returns matching 403 responses for foreign and missing targets, and preserves same-project and tokenless behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • jaylfc/taosmd#155: Both changes modify TTL filtering in VectorMemory and related tests.
  • jaylfc/taosmd#186: Both changes enforce token-bound project scope for referenced task IDs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes both main changes: TTL-aware batch deduplication and project-scoped depends_on enforcement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
  • Commit unit tests in branch fix/audit-followups

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 9, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@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.

🧹 Nitpick comments (2)
tests/test_ttl_filter.py (2)

313-315: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert archive deduplication too.

This test only verifies the vector table. The PR contract also requires no duplicate archive row; assert the agent’s conversation archive still contains exactly one record after the second POST.

🤖 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_ttl_filter.py` around lines 313 - 315, Extend the test after the
vector_memory count assertion to query the conversation archive table and assert
it contains exactly one record for the same id after the second POST, verifying
archive deduplication alongside physical-row deduplication.

318-339: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Cover superseded rows explicitly.

Add a source-ID row, supersede it, and assert its ID is absent. This ensures include_expired=True does not accidentally make intentionally cleared rows block future re-imports.

🤖 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_ttl_filter.py` around lines 318 - 339, Extend
test_existing_source_ids_includes_expired_rows to add a source-ID row, supersede
or clear it using the store’s existing supersession API, then assert its ID is
absent from existing_source_ids(agent="a"). Preserve the existing assertions for
expired and active rows so intentionally superseded rows are excluded while
expired rows still dedupe.
🤖 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.

Nitpick comments:
In `@tests/test_ttl_filter.py`:
- Around line 313-315: Extend the test after the vector_memory count assertion
to query the conversation archive table and assert it contains exactly one
record for the same id after the second POST, verifying archive deduplication
alongside physical-row deduplication.
- Around line 318-339: Extend test_existing_source_ids_includes_expired_rows to
add a source-ID row, supersede or clear it using the store’s existing
supersession API, then assert its ID is absent from
existing_source_ids(agent="a"). Preserve the existing assertions for expired and
active rows so intentionally superseded rows are excluded while expired rows
still dedupe.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a96ca555-cf8b-47ad-b9b9-778f134f8b52

📥 Commits

Reviewing files that changed from the base of the PR and between cc7bd47 and 8a84b8c.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • taosmd/http_server.py
  • taosmd/vector_memory.py
  • tests/test_http_server_registry_auth.py
  • tests/test_ttl_filter.py

Comment thread taosmd/http_server.py Outdated
# project's tasks. Foreign and nonexistent ids yield the identical
# 403 so task existence cannot be probed. Unbound (tokenless /
# standalone) requests pass through untouched.
if project is not None and depends_on:

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: Scope guard also fires for tokenless requests that include a project in the body, contradicting the PR's "tokenless/standalone unchanged" claim.

project here is the value returned by _apply_token_binding, which for a tokenless request is just the caller-supplied body project (not a token-bound value). So a standalone client that names project in the body and supplies a cross-project depends_on will now be rejected with 403, whereas before this PR (and unlike _handle_task_add_edge, which forces project=None for tokenless so it never enforces) such requests succeeded. This is inconsistent with the sibling edge endpoints and the stated guarantee. Consider gating the guard on actual token binding rather than merely project is not None.


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

Comment thread taosmd/http_server.py Outdated
# standalone) requests pass through untouched.
if project is not None and depends_on:
for dep_id in depends_on:
if not self._enforce_edge_project_scope(project, dep_id, dep_id):

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: Two minor issues in this loop.

  • N+1 query: each dep_id triggers a separate task_projects(...) lookup. The edge endpoints batch both ids into a single task_projects([from_id, to_id]) call; collapsing the whole depends_on list into one lookup (then checking each result) avoids O(N) DB round-trips.
  • Input validation: depends_on is only checked to be a list, not that its elements are strings. A non-string element is passed straight into task_projects and yields a misleading 403 ("task not available in the token's project scope") instead of a 400. Consider validating isinstance(dep_id, str) and raising _BadRequest.

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

Comment thread taosmd/vector_memory.py
out: set[str] = set()
for row in self._load_active_rows(search_agents=[agent] if agent else None):
for row in self._load_active_rows(
search_agents=[agent] if agent else None, include_expired=True

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: Re-POST with a refreshed (future) forget_after is now skipped, so TTL cannot be extended via idempotent re-ingest.

Because existing_source_ids now reports expired-but-present rows, a caller who re-POSTs the same source_id with a new future forget_after gets skipped (idempotency) and the old row stays expired/hidden from recall. This is the intended zero-loss behavior, but it means re-ingest can't revive or extend TTL on an existing id — callers needing to refresh expiry must use a new id or an explicit update path. Worth calling out in docs.


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

@kilo-code-bot

kilo-code-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

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

SUGGESTION

File Line Issue
taosmd/http_server.py 1368 Per-depends_on loop still does an N+1 task_projects lookup and does not type-validate elements (non-string yields a misleading 403).
Files Reviewed (2 files)
  • taosmd/http_server.py - 1 issue (the prior tokenless-scope WARNING was resolved by keying the guard on token_project)
  • tests/test_http_server_registry_auth.py - no new issues

Fix these issues in Kilo Cloud

Previous Review Summary (commit 8a84b8c)

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

Previous review (commit 8a84b8c)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
taosmd/http_server.py 1352 Scope guard also fires for tokenless requests that include a project in the body, contradicting the PR's "tokenless/standalone unchanged" claim and differing from the edge endpoints (which force project=None for tokenless).

SUGGESTION

File Line Issue
taosmd/http_server.py 1354 N+1 task_projects lookups per depends_on; depends_on elements not type-validated (non-string yields misleading 403).
taosmd/vector_memory.py 842 Re-POST with a refreshed future forget_after is skipped, so TTL can't be extended via idempotent re-ingest.
Files Reviewed (5 files)
  • CHANGELOG.md
  • taosmd/http_server.py
  • taosmd/vector_memory.py
  • tests/test_http_server_registry_auth.py
  • tests/test_ttl_filter.py

Fix these issues in Kilo Cloud


Reviewed by hy3-20260706:free · Input: 59.3K · Output: 9.5K · Cached: 295.9K

Review FIX-FIRST on #190: the depends_on guard keyed on the project
returned by _apply_token_binding, which for a tokenless caller is the
body-supplied tag, not a token binding. A tokenless caller tagging its
task one project while depending on a task in another got a 403 where
master returned 200 -- a real standalone regression, contradicting the
"tokenless unchanged" claim and diverging from the /edges handlers,
which derive scope purely from the token.

_apply_token_binding now surfaces the verified project_id claim on
self._token_project (reset per call; None for tokenless/global). The
guard enforces only when token_project is not None, mirroring /edges.
No second authorize()/grants pass. The tokenless test now uses a
genuinely cross-project dep and asserts 200, proving standalone is
truly unrestricted.
@jaylfc
jaylfc merged commit 2c0a751 into master Jul 9, 2026
2 checks passed
@jaylfc
jaylfc deleted the fix/audit-followups branch July 9, 2026 23:32
Comment thread taosmd/http_server.py
# (``token_project is None``) pass through untouched.
if token_project is not None and depends_on:
for dep_id in depends_on:
if not self._enforce_edge_project_scope(token_project, dep_id, dep_id):

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: Two issues remain in this per-dependency loop (carried from the prior review; the tokenless scope bug it raised was fixed by keying on token_project).

  • N+1 lookup: each dep_id triggers its own task_projects([dep_id, dep_id]) round-trip (see _enforce_edge_project_scope at line 1494). The sibling edge endpoints pass both ids to a single task_projects(...) call; collapsing the whole depends_on list into one lookup (then checking each result) avoids O(N) DB round-trips.
  • Input validation: depends_on is only checked to be a list (line 1354), not that its elements are strings. A non-string element is passed straight into _enforce_edge_project_scope (which expects str) and, since task_projects won't match a non-string key, yields a misleading 403 ("task not available in the token's project scope") instead of a 400. Validate isinstance(dep_id, str) and raise _BadRequest.

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

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.

1 participant