Skip to content

A2A ref resolution + fetch auth: make a #212 ref fetchable cross-machine (reduced file-sharing, 1481) - #238

Closed
jaylfc wants to merge 1 commit into
masterfrom
exec/tsk-doocq4
Closed

A2A ref resolution + fetch auth: make a #212 ref fetchable cross-machine (reduced file-sharing, 1481)#238
jaylfc wants to merge 1 commit into
masterfrom
exec/tsk-doocq4

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 13, 2026

Copy link
Copy Markdown
Owner

CARD TITLE (intent, not commit subject): A2A ref resolution + fetch auth: make a #212 ref fetchable cross-machine (reduced file-sharing, 1481)

Autonomous build of board card tsk-doocq4.

Adds a client-side fetch-by-ref helper that resolves taos:///files/
URIs to the controller Files API endpoint, fetches bytes using the registry
credential path, and verifies the returned content against the ref sha256.

  • taosmd/config.py: new files_url config key (env TAOSMD_FILES_URL, config.json
    files_url), falls back to registry_url for single-controller installs
  • taosmd/ref_fetch.py: resolve_ref_uri maps taos:// URIs to /api/projects/{slug}/files/{path},
    fetch_by_ref verifies sha256 and returns bytes or typed errors (mismatch,
    not-found, unauthorized)
  • taosmd/service.py: thin wrapper that builds a registry-token-authenticated
    fetcher and returns base64-encoded verified bytes
  • taosmd/remote.py: RemoteClient.fetch_by_ref proxy endpoint
  • taosmd/mcp_server.py: a2a_fetch_ref MCP tool
  • taosmd/cli.py: a2a-fetch-ref subcommand with --output flag
  • tests: unit tests for resolver, fetch-match, fetch-mismatch-rejected,
    non-taos-refused, and files_url config round-trip

Files:
taosmd/config.py | 52 +++++++++++++++
taosmd/mcp_server.py | 12 ++++
taosmd/ref_fetch.py | 147 +++++++++++++++++++++++++++++++++++++++++
taosmd/remote.py | 7 ++
taosmd/service.py | 57 ++++++++++++++++
tests/test_config_files_url.py | 42 ++++++++++++
tests/test_ref_fetch.py | 146 ++++++++++++++++++++++++++++++++++++++++
8 files changed, 510 insertions(+)

Summary by CodeRabbit

  • New Features

    • Added support for fetching and verifying referenced files through the CLI, MCP tools, and remote service.
    • Added configuration for the Files service URL, including environment-variable and saved settings support.
    • Fetched content includes verified SHA-256 metadata and size information.
    • Added clear handling for invalid references, missing files, authorization failures, and verification mismatches.
  • Tests

    • Added coverage for Files URL configuration, reference resolution, fetching, validation, and error handling.

…cation

Adds a client-side fetch-by-ref helper that resolves taos://<slug>/files/<path>
URIs to the controller Files API endpoint, fetches bytes using the registry
credential path, and verifies the returned content against the ref sha256.

- taosmd/config.py: new files_url config key (env TAOSMD_FILES_URL, config.json
  files_url), falls back to registry_url for single-controller installs
- taosmd/ref_fetch.py: resolve_ref_uri maps taos:// URIs to /api/projects/{slug}/files/{path},
  fetch_by_ref verifies sha256 and returns bytes or typed errors (mismatch,
  not-found, unauthorized)
- taosmd/service.py: thin wrapper that builds a registry-token-authenticated
  fetcher and returns base64-encoded verified bytes
- taosmd/remote.py: RemoteClient.fetch_by_ref proxy endpoint
- taosmd/mcp_server.py: a2a_fetch_ref MCP tool
- taosmd/cli.py: a2a-fetch-ref subcommand with --output flag
- tests: unit tests for resolver, fetch-match, fetch-mismatch-rejected,
  non-taos-refused, and files_url config round-trip
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds taos:// reference fetching with Files URL configuration, URI validation, SHA-256 verification, service and remote APIs, plus CLI and MCP entry points.

Changes

Reference fetching

Layer / File(s) Summary
Files URL configuration and URI resolution
taosmd/config.py, taosmd/ref_fetch.py, tests/test_config_files_url.py, tests/test_ref_fetch.py
Adds Files URL configuration with environment precedence, validates taos:// references, and constructs encoded Files API URLs.
Verified reference retrieval
taosmd/ref_fetch.py, tests/test_ref_fetch.py
Fetches reference bytes asynchronously, requires SHA-256 metadata, verifies content hashes, and raises typed errors.
Service and remote fetch integration
taosmd/service.py, taosmd/remote.py
Adds authenticated reference retrieval, HTTP error mapping, base64 response metadata, and the /refs/fetch remote proxy.
CLI and MCP entry points
taosmd/cli.py, taosmd/mcp_server.py
Adds the a2a-fetch-ref CLI command and the a2a_fetch_ref MCP tool. The CLI writes bytes to a file or stdout and reports input and fetch errors.

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

Mergeability Score: 🟠 High · up to f4e2f

This PR adds cross-machine reference fetching, but the current implementation can fail normal CLI downloads, mishandle malformed references, send credentials to unsafe controller URLs or resolve paths outside the intended files route, and fail with custom or registry-only configuration. The new entry points also omit required session recording, so the PR is not merge-ready until these correctness, security, configuration, and state-handling issues are fixed.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant taosmd.service
  participant taosmd.ref_fetch
  participant taOS_Files_API
  Client->>taosmd.service: Submit reference and agent
  taosmd.service->>taosmd.ref_fetch: Resolve and fetch reference
  taosmd.ref_fetch->>taOS_Files_API: Request encoded Files URL
  taOS_Files_API-->>taosmd.ref_fetch: Return bytes
  taosmd.ref_fetch-->>taosmd.service: Return verified bytes
  taosmd.service-->>Client: Return base64 content, hash, and size
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.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 summarizes the main changes: A2A reference resolution and authenticated cross-machine fetching.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-doocq4

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 Aug 13, 2026

Copy link
Copy Markdown

Important

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

Gitar

Comment thread taosmd/service.py
from . import config as _config
from .ref_fetch import HashMismatchError, NotFoundError, RefFetchError, UnauthorizedError, fetch_by_ref as _fetch_by_ref

files_url = _config.get_files_url(data_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.

WARNING: Missing _get_remote dispatch — this function never checks for a configured remote server, unlike every other service function in this file.

All sibling functions (a2a_send, search, stats, etc.) start with remote = _get_remote(data_dir) and forward to RemoteClient when a server URL is configured. fetch_by_ref skips this entirely, so when a remote server is configured the call always goes to the local controller directly. RemoteClient.fetch_by_ref (added in remote.py:403) is unreachable dead code in that configuration.


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

Comment thread taosmd/service.py
from . import config as _config
from .ref_fetch import HashMismatchError, NotFoundError, RefFetchError, UnauthorizedError, fetch_by_ref as _fetch_by_ref

files_url = _config.get_files_url(data_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.

WARNING: get_files_url check is stricter than the fallback logic in ref_fetch._get_files_url.

service.fetch_by_ref calls _config.get_files_url(data_dir) and raises if it returns falsy, but _get_files_url() in ref_fetch.py falls back to registry_url when files_url is unset. This means service.fetch_by_ref raises "files_url is not configured" when only registry_url is set, while the underlying ref_fetch.fetch_by_ref would succeed via that fallback. The pre-check and the actual resolution have inconsistent behavior.


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

Comment thread taosmd/ref_fetch.py
Raises:
ValueError: If the uri scheme is not ``taos://`` or the shape is invalid.
"""
uri = ref.get("uri", "")

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: No type validation on ref parameter — crashes if ref is not a dict.

ref.get("uri", "") raises AttributeError when ref is None, a list, or any non-dict object. A public-facing resolver should validate its input and raise ValueError with a clear message rather than leaking an internal attribute error.


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

Comment thread taosmd/ref_fetch.py
f"invalid taos ref uri: {uri!r} (path is empty)"
)
encoded_path = urllib.parse.quote(path, safe="/")
base = files_url.rstrip("/")

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: No validation on files_url before calling .rstrip() — crashes on None.

files_url.rstrip("/") raises AttributeError if files_url is None. The function signature types it as str but provides no runtime guard. A None value (or any non-string) produces an unhelpful crash instead of a clear ValueError.


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

Comment thread taosmd/ref_fetch.py
url = get_files_url()
if url:
return url
except Exception:

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: Bare except Exception: pass silently swallows all errors (lines 126–127 and 133–134).

If config.py has an import error, a runtime bug in get_files_url/get_registry_url, or any other exception, the user receives the generic "files_url is not configured" message — which is misleading and makes root-cause diagnosis nearly impossible. At minimum, log the exception before falling back.


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

Comment thread taosmd/cli.py
print(f"error: {exc}", file=sys.stderr)
return 2

raw = base64.b64decode(result["bytes"])

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: base64.b64decode can raise binascii.Error for malformed input.

raw = base64.b64decode(result["bytes"]) is not wrapped in a try/except. If the service layer ever returns malformed base64 (e.g., due to a bug or manual construction), the CLI crashes with a traceback instead of printing a clean error to stderr and returning a non-zero exit code.


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

@kilo-code-bot

kilo-code-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 6 Issues Found | Recommendation: Address before merge

Overview

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

WARNING

File Line Issue
taosmd/service.py 365 fetch_by_ref does not forward to _get_remote, so RemoteClient.fetch_by_ref is unreachable when a remote server is configured — inconsistent with every other service function
taosmd/service.py 365 get_files_url pre-check raises "not configured" when only registry_url is set, but _get_files_url() in ref_fetch.py falls back to registry_url — inconsistent behavior between the check and the actual resolution
taosmd/ref_fetch.py 52 ref.get("uri", "") raises AttributeError if ref is not a dict — no runtime type validation on public function input
taosmd/ref_fetch.py 70 files_url.rstrip("/") raises AttributeError if files_url is None — no validation that files_url is a string
taosmd/ref_fetch.py 126 Bare except Exception: pass (lines 126–127 and 133–134) silently swallows all errors, making debugging impossible when config.py has issues

SUGGESTION

File Line Issue
taosmd/cli.py 508 base64.b64decode(result["bytes"]) can raise binascii.Error for malformed input but is not caught — CLI crashes with traceback instead of a clean error
Files Reviewed (6 files)
  • taosmd/ref_fetch.py - 4 issues
  • taosmd/service.py - 2 issues
  • taosmd/cli.py - 1 issue
  • taosmd/config.py
  • taosmd/remote.py
  • taosmd/mcp_server.py

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 95.2K · Output: 18.8K · Cached: 329K

@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: 5

🤖 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 `@taosmd/cli.py`:
- Around line 508-510: Import pathlib.Path in the module before the
output-writing logic in the command handling the fetched result, so the existing
Path(args.output).write_bytes(raw) call works whenever --output is provided.
- Around line 496-506: Validate the parsed ref in the CLI flow before calling
service.fetch_by_ref, rejecting non-dict JSON values with the existing
invalid-input error and exit code 2. Extend the exception handling around
fetch_by_ref to catch ValueError for unsupported URIs alongside RefFetchError,
reporting the error and returning 2.

In `@taosmd/mcp_server.py`:
- Around line 254-264: Update taosmd/mcp_server.py lines 254-264 in
a2a_fetch_ref to capture the MCP tool input before _dispatch and archive its
response or error afterward, configuring manual transcript capture when FastMCP
does not provide it; run process_conversation_turn(...), crystallize the
session, and keep the session catalogue current. Update taosmd/cli.py lines
487-514 to archive the command input, output, and errors, run
process_conversation_turn(...) after user input, crystallize the session, and
update the archive catalogue before exit.

In `@taosmd/ref_fetch.py`:
- Around line 64-71: Update the ref URI resolver around path validation and
files_url handling: reject any path component equal to "." or ".." before
quoting, and validate files_url locally so only HTTP(S) controller URLs are
accepted, including environment-provided values that bypass set_files_url.
Preserve the existing empty-path rejection and URL construction behavior for
valid inputs.

In `@taosmd/service.py`:
- Around line 365-392: Update the fetch flow around _fetcher and _fetch_by_ref
so configuration is resolved once for the supplied data_dir, selecting files_url
or the documented registry_url fallback. Pass the resolved controller URL into
the lower helper instead of allowing taosmd.ref_fetch.fetch_by_ref to re-resolve
configuration without data_dir, while preserving the existing token headers and
HTTP error handling.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 58e08721-ce69-4617-9c9b-a963166d0c11

📥 Commits

Reviewing files that changed from the base of the PR and between 25b35af and f4e2fd5.

📒 Files selected for processing (8)
  • taosmd/cli.py
  • taosmd/config.py
  • taosmd/mcp_server.py
  • taosmd/ref_fetch.py
  • taosmd/remote.py
  • taosmd/service.py
  • tests/test_config_files_url.py
  • tests/test_ref_fetch.py

Comment thread taosmd/cli.py
Comment on lines +496 to +506
try:
ref = json.loads(args.ref_json)
except (json.JSONDecodeError, TypeError) as exc:
print(f"error: invalid ref JSON: {exc}", file=sys.stderr)
return 2

try:
result = asyncio.run(service.fetch_by_ref(ref, agent="cli", data_dir=data_dir))
except RefFetchError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2

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 | 🟡 Minor | ⚡ Quick win

Validate the reference object and handle invalid URIs.

Line 497 accepts JSON arrays and scalars. Those values later cause an unhandled AttributeError when the resolver calls ref.get. A dict with an unsupported URI raises ValueError, but line 504 does not catch it. Return exit code 2 for both input cases.

Proposed fix
     try:
         ref = json.loads(args.ref_json)
     except (json.JSONDecodeError, TypeError) as exc:
         print(f"error: invalid ref JSON: {exc}", file=sys.stderr)
         return 2
+    if not isinstance(ref, dict):
+        print("error: ref JSON must be an object", file=sys.stderr)
+        return 2
 
     try:
         result = asyncio.run(service.fetch_by_ref(ref, agent="cli", data_dir=data_dir))
-    except RefFetchError as exc:
+    except (RefFetchError, ValueError) as exc:
         print(f"error: {exc}", file=sys.stderr)
         return 2
📝 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
try:
ref = json.loads(args.ref_json)
except (json.JSONDecodeError, TypeError) as exc:
print(f"error: invalid ref JSON: {exc}", file=sys.stderr)
return 2
try:
result = asyncio.run(service.fetch_by_ref(ref, agent="cli", data_dir=data_dir))
except RefFetchError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
try:
ref = json.loads(args.ref_json)
except (json.JSONDecodeError, TypeError) as exc:
print(f"error: invalid ref JSON: {exc}", file=sys.stderr)
return 2
if not isinstance(ref, dict):
print("error: ref JSON must be an object", file=sys.stderr)
return 2
try:
result = asyncio.run(service.fetch_by_ref(ref, agent="cli", data_dir=data_dir))
except (RefFetchError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
🤖 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 `@taosmd/cli.py` around lines 496 - 506, Validate the parsed ref in the CLI
flow before calling service.fetch_by_ref, rejecting non-dict JSON values with
the existing invalid-input error and exit code 2. Extend the exception handling
around fetch_by_ref to catch ValueError for unsupported URIs alongside
RefFetchError, reporting the error and returning 2.

Comment thread taosmd/cli.py
Comment on lines +508 to +510
raw = base64.b64decode(result["bytes"])
if args.output:
Path(args.output).write_bytes(raw)

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

Import Path before using --output.

Line 510 references Path without a binding. Every --output invocation raises NameError instead of writing the fetched bytes.

Proposed fix
 def _a2a_fetch_ref_cmd(args: argparse.Namespace) -> int:
     """Handle ``taosmd a2a-fetch-ref``: fetch bytes for a taOS Files-backed ref."""
     import asyncio  # noqa: PLC0415
     import base64  # noqa: PLC0415
+    from pathlib import Path  # noqa: PLC0415
📝 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
raw = base64.b64decode(result["bytes"])
if args.output:
Path(args.output).write_bytes(raw)
def _a2a_fetch_ref_cmd(args: argparse.Namespace) -> int:
"""Handle ``taosmd a2a-fetch-ref``: fetch bytes for a taOS Files-backed ref."""
import asyncio # noqa: PLC0415
import base64 # noqa: PLC0415
from pathlib import Path # noqa: PLC0415
raw = base64.b64decode(result["bytes"])
if args.output:
Path(args.output).write_bytes(raw)
🧰 Tools
🪛 Ruff (0.16.1)

[error] 510-510: Undefined name Path

(F821)

🤖 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 `@taosmd/cli.py` around lines 508 - 510, Import pathlib.Path in the module
before the output-writing logic in the command handling the fetched result, so
the existing Path(args.output).write_bytes(raw) call works whenever --output is
provided.

Source: Linters/SAST tools

Comment thread taosmd/mcp_server.py
Comment on lines +254 to +264
@mcp.tool()
async def a2a_fetch_ref(ref: dict, agent: str) -> dict:
"""Fetch bytes for a taOS Files-backed ref and verify its sha256.

``ref`` must be a dict with ``uri`` (a ``taos://`` uri) and ``sha256``.
Returns ``{"bytes": <base64-str>, "sha256": <hash>, "size": <int>}``
on success, or raises a typed error (mismatch / not-found / unauthorized).
"""
return await _dispatch(
service.fetch_by_ref(ref, agent=agent, data_dir=data_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.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Add the required taOSmd conversation lifecycle to both reference-fetch entry points.

Neither entry point archives the tool call, result, or error. Neither entry point runs fact extraction, updates the session catalogue, or crystallizes the completed session.

  • taosmd/mcp_server.py#L254-L264: Record the MCP tool input before dispatch and the response or error afterward. Configure manual transcript capture if FastMCP does not provide it.
  • taosmd/cli.py#L487-L514: Record the command input, output, and error. Run fact extraction after the user input. Crystallize the session and update the archive catalogue before exit.

As per coding guidelines, archive every user message, assistant response, tool call, and error; run process_conversation_turn(...); crystallize each session; and keep the session catalogue current.

📍 Affects 2 files
  • taosmd/mcp_server.py#L254-L264 (this comment)
  • taosmd/cli.py#L487-L514
🤖 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 `@taosmd/mcp_server.py` around lines 254 - 264, Update taosmd/mcp_server.py
lines 254-264 in a2a_fetch_ref to capture the MCP tool input before _dispatch
and archive its response or error afterward, configuring manual transcript
capture when FastMCP does not provide it; run process_conversation_turn(...),
crystallize the session, and keep the session catalogue current. Update
taosmd/cli.py lines 487-514 to archive the command input, output, and errors,
run process_conversation_turn(...) after user input, crystallize the session,
and update the archive catalogue before exit.

Source: Coding guidelines

Comment thread taosmd/ref_fetch.py
Comment on lines +64 to +71
path = parts[1][len(_FILES_SEGMENT):]
if not path:
raise ValueError(
f"invalid taos ref uri: {uri!r} (path is empty)"
)
encoded_path = urllib.parse.quote(path, safe="/")
base = files_url.rstrip("/")
return f"{base}/api/projects/{slug}/files/{encoded_path}"

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 | 🟠 Major | ⚡ Quick win

Reject traversal segments and non-HTTP(S) controller URLs.

Line 69 encodes unsafe characters but preserves . and .. segments. A ref can therefore create a URL outside the Files API route after path normalization. The service sends the registry bearer token with this request.

Reject . and .. path components. Validate files_url in this resolver because environment values bypass set_files_url.

Proposed fix
     path = parts[1][len(_FILES_SEGMENT):]
     if not path:
         raise ValueError(
             f"invalid taos ref uri: {uri!r} (path is empty)"
         )
+    if any(segment in {".", ".."} for segment in path.split("/")):
+        raise ValueError(
+            f"invalid taos ref uri: {uri!r} (path traversal is not allowed)"
+        )
+    parsed_files_url = urllib.parse.urlsplit(files_url)
+    if parsed_files_url.scheme not in {"http", "https"} or not parsed_files_url.netloc:
+        raise ValueError("files_url must be an absolute HTTP(S) URL")
     encoded_path = urllib.parse.quote(path, safe="/")
📝 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
path = parts[1][len(_FILES_SEGMENT):]
if not path:
raise ValueError(
f"invalid taos ref uri: {uri!r} (path is empty)"
)
encoded_path = urllib.parse.quote(path, safe="/")
base = files_url.rstrip("/")
return f"{base}/api/projects/{slug}/files/{encoded_path}"
path = parts[1][len(_FILES_SEGMENT):]
if not path:
raise ValueError(
f"invalid taos ref uri: {uri!r} (path is empty)"
)
if any(segment in {".", ".."} for segment in path.split("/")):
raise ValueError(
f"invalid taos ref uri: {uri!r} (path traversal is not allowed)"
)
parsed_files_url = urllib.parse.urlsplit(files_url)
if parsed_files_url.scheme not in {"http", "https"} or not parsed_files_url.netloc:
raise ValueError("files_url must be an absolute HTTP(S) URL")
encoded_path = urllib.parse.quote(path, safe="/")
base = files_url.rstrip("/")
return f"{base}/api/projects/{slug}/files/{encoded_path}"
🤖 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 `@taosmd/ref_fetch.py` around lines 64 - 71, Update the ref URI resolver around
path validation and files_url handling: reject any path component equal to "."
or ".." before quoting, and validate files_url locally so only HTTP(S)
controller URLs are accepted, including environment-provided values that bypass
set_files_url. Preserve the existing empty-path rejection and URL construction
behavior for valid inputs.

Source: Linters/SAST tools

Comment thread taosmd/service.py
Comment on lines +365 to +392
files_url = _config.get_files_url(data_dir)
if not files_url:
raise RefFetchError(
"files_url is not configured: set TAOSMD_FILES_URL or files_url in config.json"
)
registry_token = _config.get_registry_token(data_dir)

def _fetcher(url: str, agent: str) -> bytes:
import urllib.error
import urllib.request

headers = {"Accept": "application/octet-stream"}
if registry_token:
headers["Authorization"] = f"Bearer {registry_token}"
req = urllib.request.Request(url, headers=headers, method="GET")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.read()
except urllib.error.HTTPError as exc:
if exc.code in (401, 403):
raise UnauthorizedError(f"HTTP {exc.code} from {url}") from exc
if exc.code == 404:
raise NotFoundError(f"HTTP 404 from {url}") from exc
raise
except urllib.error.URLError as exc:
raise RefFetchError(f"fetch failed for {url}: {exc}") from exc

raw = await _fetch_by_ref(ref, _fetcher, agent)

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

Pass the resolved controller URL to the verifier.

Line 365 resolves configuration for data_dir, but line 392 re-resolves configuration through taosmd.ref_fetch.fetch_by_ref without that directory. A custom CLI or MCP data directory then fails even when it contains files_url. Registry-only configuration also fails at lines 365-369 despite the documented fallback.

Resolve files_url or registry_url once. Pass that value into the lower helper.

Proposed fix
-async def fetch_by_ref(ref: dict, fetcher, agent: str) -> bytes:
+async def fetch_by_ref(
+    ref: dict, fetcher, agent: str, *, files_url: str | None = None
+) -> bytes:
 ...
-    files_url = _get_files_url()
+    files_url = files_url or _get_files_url()
-    files_url = _config.get_files_url(data_dir)
+    files_url = _config.get_files_url(data_dir) or _config.get_registry_url(data_dir)
     if not files_url:
         raise RefFetchError(
             "files_url is not configured: set TAOSMD_FILES_URL or files_url in config.json"
         )
 ...
-    raw = await _fetch_by_ref(ref, _fetcher, agent)
+    raw = await _fetch_by_ref(ref, _fetcher, agent, files_url=files_url)
📝 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
files_url = _config.get_files_url(data_dir)
if not files_url:
raise RefFetchError(
"files_url is not configured: set TAOSMD_FILES_URL or files_url in config.json"
)
registry_token = _config.get_registry_token(data_dir)
def _fetcher(url: str, agent: str) -> bytes:
import urllib.error
import urllib.request
headers = {"Accept": "application/octet-stream"}
if registry_token:
headers["Authorization"] = f"Bearer {registry_token}"
req = urllib.request.Request(url, headers=headers, method="GET")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.read()
except urllib.error.HTTPError as exc:
if exc.code in (401, 403):
raise UnauthorizedError(f"HTTP {exc.code} from {url}") from exc
if exc.code == 404:
raise NotFoundError(f"HTTP 404 from {url}") from exc
raise
except urllib.error.URLError as exc:
raise RefFetchError(f"fetch failed for {url}: {exc}") from exc
raw = await _fetch_by_ref(ref, _fetcher, agent)
files_url = _config.get_files_url(data_dir) or _config.get_registry_url(data_dir)
if not files_url:
raise RefFetchError(
"files_url is not configured: set TAOSMD_FILES_URL or files_url in config.json"
)
registry_token = _config.get_registry_token(data_dir)
def _fetcher(url: str, agent: str) -> bytes:
import urllib.error
import urllib.request
headers = {"Accept": "application/octet-stream"}
if registry_token:
headers["Authorization"] = f"Bearer {registry_token}"
req = urllib.request.Request(url, headers=headers, method="GET")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.read()
except urllib.error.HTTPError as exc:
if exc.code in (401, 403):
raise UnauthorizedError(f"HTTP {exc.code} from {url}") from exc
if exc.code == 404:
raise NotFoundError(f"HTTP 404 from {url}") from exc
raise
except urllib.error.URLError as exc:
raise RefFetchError(f"fetch failed for {url}: {exc}") from exc
raw = await _fetch_by_ref(ref, _fetcher, agent, files_url=files_url)
🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 380-380: Request-controlled URL passed to urlopen; validate against an allowlist to prevent SSRF.
Context: urllib.request.urlopen(req, timeout=30)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(urlopen-unsanitized-data)

🪛 Ruff (0.16.1)

[error] 379-379: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)


[error] 381-381: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.

(S310)

🤖 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 `@taosmd/service.py` around lines 365 - 392, Update the fetch flow around
_fetcher and _fetch_by_ref so configuration is resolved once for the supplied
data_dir, selecting files_url or the documented registry_url fallback. Pass the
resolved controller URL into the lower helper instead of allowing
taosmd.ref_fetch.fetch_by_ref to re-resolve configuration without data_dir,
while preserving the existing token headers and HTTP error handling.

@jaylfc

jaylfc commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Review: CHANGES REQUESTED

Three blocking findings. One is a security defect that no bot flagged; two are correctness bugs that Kilo raised and I verified independently against the source rather than taking the bot's word.

1. BLOCKING (security): resolve_ref_uri emits URLs outside the Files scope, and the registry token follows them

ref_fetch.py:70 builds the path with urllib.parse.quote(path, safe="/"). quote never escapes ., and safe="/" preserves separators, so dot segments survive into the constructed URL. Verified by running the shipped function:

taos://proj/files/../../admin/secrets
  -> https://controller/api/projects/proj/files/../../admin/secrets
taos://proj/files/a/../../../api/registry/tokens
  -> https://controller/api/projects/api/registry/tokens   (after normalization)

service.py:372 then attaches Authorization: Bearer <registry_token> to whatever that resolver returned. Confirmed on a local listener: the request goes out with the dot segments literal and the bearer token attached. urllib does not normalize, so whether the request lands outside /files/ depends on the controller or any reverse proxy in front of it normalizing the path, which is not a property taosmd controls or asserts anywhere.

Why this matters here specifically: refs are attacker-supplied. Per #221 and #222, service.a2a_send() stores refs verbatim with no redaction and no validation, since the ref-kind enum and caps live only in the HTTP handler. So a crafted ref in a received message is enough, and the trigger is a user or agent calling a2a_fetch_ref on that message.

The sha256 check limits this but does not close it, and the error message reopens it: HashMismatchError reports the observed hash (f"sha256 mismatch: expected {expected}, got {actual}"). That turns the helper into a content oracle for arbitrary controller endpoints, and the distinct NotFoundError / UnauthorizedError / mismatch outcomes make it an endpoint enumerator as well.

Fix direction (implementer's call, but the constraint is fixed): the resolved URL must not be able to leave /api/projects/{slug}/files/. Percent-encoding the path as a single segment (safe="") or rejecting any ./.. segment outright both achieve that. Separately, do not put the observed hash in the error text; that a mismatch occurred is all a caller needs. Please add a red-first test for .. traversal: the current suite covers scheme rejection (non-taos-refused) but nothing covers path shape.

2. BLOCKING (correctness): the documented registry_url fallback cannot work through the service path

The PR description and config.get_files_url's own docstring both promise that an unset files_url falls back to registry_url "so a single-controller install needs only one setting". config.get_files_url (config.py:425) returns None in that case; the fallback lives only in ref_fetch._get_files_url. But service.fetch_by_ref (service.py:365) pre-checks if not files_url: raise RefFetchError("files_url is not configured...") before ref_fetch is ever reached, and the service path is the only path MCP and the CLI use. So on a single-controller install the feature raises "not configured" while the config it names is present, and the fallback code is unreachable in practice.

3. BLOCKING (correctness): RemoteClient.fetch_by_ref is dead code, so this is not cross-machine

remote.py gains fetch_by_ref, but service.fetch_by_ref never calls _get_remote(data_dir). Every other remote-capable service function does (24 call sites, e.g. service.py:98, 115, 131, 143). With a remote server configured, the fetch silently runs locally against the local node's config and token instead of routing to the remote. For a change whose stated purpose is "make a #212 ref fetchable cross-machine", the cross-machine path is exactly the one that does not execute.

Also worth fixing, not blocking

Kilo's remaining three are fair: ref.get("uri", "") and files_url.rstrip("/") both raise AttributeError on non-dict / None input, and the two bare except Exception: pass blocks in _get_files_url swallow real config errors. The type guards become more valuable once finding 1 is fixed, since input validation is then load-bearing.

On the check status

All four checks report green and that reads as four reviews. It is one. Qodo is billing-blocked and paused, Gitar is on the free plan with code review disabled, and Kilo reports SUCCESS while its own summary says "6 Issues Found | Recommendation: Address before merge". Only CodeRabbit produced a walkthrough, and none of the four looked at the path shape. Flagging it because the merge record would otherwise suggest scrutiny that did not happen (same pattern as #212, issue #216).

@jaylfc

jaylfc commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Closing in favour of card tsk-7xcsh5 (p4, fleet:claimable, security).

Not a judgement on the effort, and the shape of the feature is right. But three findings block it and one of them is a security defect in the URL construction itself, so the branch needs rebuilding rather than patching: the traversal fix changes how the path is built, the registry_url fallback contradiction has to be resolved in one place rather than two, and the remote route was never wired up so the cross-machine path has no test at all.

The card carries the full review, the reproduction, the constraint the fix must satisfy (the resolved URL must not be able to leave /api/projects/{slug}/files/), and the four red-first tests the original suite was missing. Whoever claims it should start from master rather than this branch.

Review for reference: #238 (comment)

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