-
-
Notifications
You must be signed in to change notification settings - Fork 3
Revise PR #242: bearer token still crosses origins on redirect, plus a missing route #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,6 +91,7 @@ | |
| Adds ``"vector_failures": int`` and ``"degraded": true`` to the | ||
| result when embedding failed for one or more items (archived, | ||
| repairable via reconcile), same as single ingest. | ||
| ``POST /refs/fetch`` ``{"ref": {"uri": "taos://<slug>/files/<path>", "sha256": "<hex>"}, "agent"}`` -> ``{"bytes": "<base64>", "sha256": "<hex>", "size": <int>}`` | ||
| ``POST /search`` ``{"query", "agent", "limit"?, "project"?, "also_include"?, "mode"?, "collection"?, "collections"?: [...], "collections_only"?: bool}`` -> ``{"hits": [...]}`` | ||
| ``collection``/``collections`` add granted collections' indexed | ||
| content to the search (grants enforced per requesting agent; | ||
|
|
@@ -937,6 +938,8 @@ def _dispatch(self, method: str) -> None: | |
| self._handle_ingest() | ||
| elif method == "POST" and path == "/ingest/batch": | ||
| self._handle_ingest_batch() | ||
| elif method == "POST" and path == "/refs/fetch": | ||
| self._handle_refs_fetch() | ||
| elif method == "GET" and path == "/projects": | ||
| self._handle_list_projects() | ||
| elif method == "GET" and path == "/shelves": | ||
|
|
@@ -1104,6 +1107,19 @@ def _handle_ingest_batch(self) -> None: | |
| ) | ||
| self._send_json(200, result) | ||
|
|
||
| def _handle_refs_fetch(self) -> None: | ||
| body = self._read_json_body() | ||
| ref = body.get("ref") | ||
| agent = body.get("agent") | ||
| if not isinstance(ref, dict): | ||
| raise _BadRequest("'ref' (object) is required") | ||
| if not isinstance(agent, str) or not agent: | ||
| raise _BadRequest("'agent' (non-empty string) is required") | ||
| result = runner.run( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Domain exceptions from
Reply with |
||
| service.fetch_by_ref(ref=ref, agent=agent, data_dir=data_dir) | ||
| ) | ||
| self._send_json(200, result) | ||
|
|
||
| def _handle_search_post(self) -> None: | ||
| body = self._read_json_body() | ||
| query = body.get("query") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| """Resolve and fetch taOS Files-backed refs with hash verification. | ||
|
|
||
| A ref uri of the form ``taos://<project-slug>/files/<path>`` is resolved to | ||
| ``GET /api/projects/{slug}/files/{path}`` on the configured controller. The | ||
| fetch helper verifies the returned bytes against the ref's ``sha256`` and | ||
| returns the verified bytes or a typed error. | ||
|
|
||
| No new server storage is introduced: this is purely a client-side helper. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import hashlib | ||
| import os | ||
| import urllib.parse | ||
|
|
||
|
|
||
| class RefFetchError(Exception): | ||
| """Base error for ref fetch failures.""" | ||
|
|
||
|
|
||
| class HashMismatchError(RefFetchError): | ||
| """Raised when the fetched bytes do not match the ref's sha256.""" | ||
|
|
||
|
|
||
| class NotFoundError(RefFetchError): | ||
| """Raised when the Files API reports the resource is missing.""" | ||
|
|
||
|
|
||
| class UnauthorizedError(RefFetchError): | ||
| """Raised when the Files API reports an auth failure.""" | ||
|
|
||
|
|
||
| _SCHEME_PREFIX = "taos://" | ||
| _FILES_SEGMENT = "files/" | ||
|
|
||
|
|
||
| def _reject_dot_segments(path: str) -> None: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Double-encoded path traversal possible in
Reply with |
||
| """Raise ValueError if path contains . or .. segments (literal or encoded).""" | ||
| decoded = urllib.parse.unquote(path) | ||
| for segment in decoded.split("/"): | ||
| if segment in (".", ".."): | ||
| raise ValueError( | ||
| f"invalid taos ref uri: path contains dot segment {segment!r}" | ||
| ) | ||
|
|
||
|
|
||
| def resolve_ref_uri(ref, files_url: str) -> str: | ||
| """Map a taos:// ref uri to the concrete Files API fetch endpoint. | ||
|
|
||
| Args: | ||
| ref: A ref dict with at least a ``uri`` field. | ||
| files_url: Base URL of the taOS controller. | ||
|
|
||
| Returns: | ||
| The full URL for ``GET /api/projects/{slug}/files/{path}``. | ||
|
|
||
| Raises: | ||
| ValueError: If the uri scheme is not ``taos://`` or the shape is invalid. | ||
| """ | ||
| uri = ref.get("uri") if isinstance(ref, dict) else None | ||
| if not isinstance(uri, str) or not uri.startswith(_SCHEME_PREFIX): | ||
| raise ValueError( | ||
| f"unsupported uri scheme: {uri!r} (only taos:// is accepted)" | ||
| ) | ||
| rest = uri[len(_SCHEME_PREFIX):] | ||
| parts = rest.split("/", 1) | ||
| if len(parts) != 2 or not parts[1].startswith(_FILES_SEGMENT): | ||
| raise ValueError( | ||
| f"invalid taos ref uri: {uri!r} (expected taos://<slug>/files/<path>)" | ||
| ) | ||
| slug = urllib.parse.quote(parts[0], safe="") | ||
| path = parts[1][len(_FILES_SEGMENT):] | ||
| if not path: | ||
| raise ValueError( | ||
| f"invalid taos ref uri: {uri!r} (path is empty)" | ||
| ) | ||
| if path.startswith("/"): | ||
| raise ValueError( | ||
| f"invalid taos ref uri: {uri!r} (path must not be absolute)" | ||
| ) | ||
| _reject_dot_segments(path) | ||
| encoded_path = urllib.parse.quote(path, safe="/") | ||
| base = files_url.rstrip("/") if files_url else files_url | ||
| return f"{base}/api/projects/{slug}/files/{encoded_path}" | ||
|
|
||
|
|
||
| async def fetch_by_ref(ref, fetcher, agent, data_dir=None) -> bytes: | ||
| """Fetch bytes for a ref using an injected fetcher and verify the hash. | ||
|
|
||
| Args: | ||
| ref: A ref dict with ``uri`` and ``sha256`` fields. | ||
| fetcher: A callable ``fetcher(url: str, agent: str) -> bytes`` that | ||
| performs the HTTP GET and returns the raw response body. It should | ||
| raise :class:`NotFoundError` or :class:`UnauthorizedError` for | ||
| those HTTP status codes. | ||
| agent: The agent identity (passed to ``fetcher`` for auth context). | ||
| data_dir: Optional data directory used to resolve the files base URL. | ||
|
|
||
| Returns: | ||
| The verified raw bytes. | ||
|
|
||
| Raises: | ||
| ValueError: If the uri cannot be resolved. | ||
| HashMismatchError: If the fetched bytes' sha256 does not match ref.sha256. | ||
| NotFoundError: If the fetcher reports the resource is missing. | ||
| UnauthorizedError: If the fetcher reports an auth failure. | ||
| RefFetchError: For other fetch failures. | ||
| """ | ||
| import asyncio | ||
|
|
||
| files_url = _get_files_url(data_dir) | ||
| url = resolve_ref_uri(ref, files_url) | ||
| loop = asyncio.get_running_loop() | ||
| raw = await loop.run_in_executor(None, fetcher, url, agent) | ||
| expected = ref.get("sha256") if isinstance(ref, dict) else None | ||
| if not expected: | ||
| raise RefFetchError("ref has no sha256") | ||
| actual = hashlib.sha256(raw).hexdigest() | ||
| if actual != expected: | ||
| raise HashMismatchError("sha256 mismatch") | ||
| return raw | ||
|
|
||
|
|
||
| def _get_files_url(data_dir=None) -> str: | ||
| """Resolve the files base URL from env or config. | ||
|
|
||
| Falls back to ``registry_url`` when ``files_url`` is unset, so a | ||
| single-controller install needs only one setting. | ||
| """ | ||
| env = os.environ.get("TAOSMD_FILES_URL") | ||
| if env and env.strip(): | ||
| return env.strip() | ||
| from .config import get_files_url | ||
| url = get_files_url(data_dir) | ||
| if url: | ||
| return url | ||
| from .config import get_registry_url | ||
| url = get_registry_url(data_dir) | ||
| if url: | ||
| return url | ||
| raise RefFetchError( | ||
| "files_url is not configured: set TAOSMD_FILES_URL or files_url in config.json" | ||
| ) | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "RefFetchError", | ||
| "HashMismatchError", | ||
| "NotFoundError", | ||
| "UnauthorizedError", | ||
| "resolve_ref_uri", | ||
| "fetch_by_ref", | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: Missing
_apply_token_bindingon/refs/fetchUnlike other data endpoints (
/ingest,/search), this handler does not call_apply_token_binding(agent, project). Registry-token verification and active-grant checks are skipped, so a bearer-token holder without an active grant can fetch files.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.