mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-12 12:37:32 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f118fc11bf | |||
| d88c8cbacf | |||
| a35384e68f | |||
| 3064819e3d | |||
| 54f1d015b5 | |||
| 2d8177035b | |||
| c67deaa60a |
@@ -76,7 +76,7 @@ jobs:
|
|||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
# Byte-compile sources — catches syntax errors without installing deps.
|
# Byte-compile sources — catches syntax errors without installing deps.
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
echo "docs_only=false" >> "$GITHUB_OUTPUT"
|
echo "docs_only=false" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
if: steps.docs-check.outputs.docs_only != 'true'
|
if: steps.docs-check.outputs.docs_only != 'true'
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
|
|
||||||
# Build without pushing so a broken Dockerfile is caught here, and the
|
# Build without pushing so a broken Dockerfile is caught here, and the
|
||||||
# exact image we ship is what gets scanned.
|
# exact image we ship is what gets scanned.
|
||||||
- name: Build image
|
- name: Build image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
@@ -98,10 +98,10 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
|
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
|
||||||
|
|
||||||
- name: Upload Trivy results
|
- name: Upload Trivy results
|
||||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||||
with:
|
with:
|
||||||
sarif_file: trivy-results.sarif
|
sarif_file: trivy-results.sarif
|
||||||
category: trivy-image
|
category: trivy-image
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
|||||||
@@ -49,16 +49,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
@@ -103,16 +103,16 @@ jobs:
|
|||||||
pattern: digest-*
|
pattern: digest-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Set up Buildx
|
- name: Set up Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Compute tags
|
- name: Compute tags
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import os
|
|||||||
|
|
||||||
from src.runtime_paths import get_app_root, get_default_data_dir
|
from src.runtime_paths import get_app_root, get_default_data_dir
|
||||||
|
|
||||||
APP_VERSION = "1.0.2"
|
APP_VERSION = "1.0.1"
|
||||||
|
|
||||||
# Base paths
|
# Base paths
|
||||||
BASE_DIR = os.path.join(get_app_root(), "")
|
BASE_DIR = os.path.join(get_app_root(), "")
|
||||||
|
|||||||
+4
-1
@@ -230,7 +230,10 @@ def request_flags(messages) -> tuple:
|
|||||||
"""
|
"""
|
||||||
msgs = messages or []
|
msgs = messages or []
|
||||||
last = msgs[-1] if msgs else None
|
last = msgs[-1] if msgs else None
|
||||||
agent = bool(last) and last.get("role") != "user"
|
# A message element can be a non-dict (clients send `"messages": ["hi"]`);
|
||||||
|
# the vision loop below already guards each element with isinstance, so do
|
||||||
|
# the same here rather than call .get() on a bare string.
|
||||||
|
agent = isinstance(last, dict) and last.get("role") != "user"
|
||||||
vision = False
|
vision = False
|
||||||
for m in msgs:
|
for m in msgs:
|
||||||
content = m.get("content") if isinstance(m, dict) else None
|
content = m.get("content") if isinstance(m, dict) else None
|
||||||
|
|||||||
@@ -440,7 +440,10 @@ def build_user_content(
|
|||||||
try:
|
try:
|
||||||
with open(path, "rb") as image_file:
|
with open(path, "rb") as image_file:
|
||||||
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
|
||||||
image_format = ext[1:]
|
# Extensionless uploads (e.g. a pasted screenshot) have no ext,
|
||||||
|
# so fall back to the resolved MIME subtype rather than emitting
|
||||||
|
# an invalid "data:image/;base64," with an empty subtype.
|
||||||
|
image_format = ext[1:] or (mime.split("/", 1)[1] if mime.startswith("image/") else "png")
|
||||||
content.append({
|
content.append({
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {"url": f"data:image/{image_format};base64,{encoded_string}"},
|
"image_url": {"url": f"data:image/{image_format};base64,{encoded_string}"},
|
||||||
@@ -456,7 +459,7 @@ def build_user_content(
|
|||||||
try:
|
try:
|
||||||
with open(path, "rb") as audio_file:
|
with open(path, "rb") as audio_file:
|
||||||
encoded_string = base64.b64encode(audio_file.read()).decode("utf-8")
|
encoded_string = base64.b64encode(audio_file.read()).decode("utf-8")
|
||||||
audio_format = ext[1:]
|
audio_format = ext[1:] or (mime.split("/", 1)[1] if mime.startswith("audio/") else "mpeg")
|
||||||
content.append({
|
content.append({
|
||||||
"type": "audio",
|
"type": "audio",
|
||||||
"audio": {"url": f"data:audio/{audio_format};base64,{encoded_string}"},
|
"audio": {"url": f"data:audio/{audio_format};base64,{encoded_string}"},
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ def test_request_flags_vision():
|
|||||||
assert vision is True
|
assert vision is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_flags_non_dict_last_message_does_not_crash():
|
||||||
|
# A client can send a bare-string (non-dict) last element; before the
|
||||||
|
# isinstance guard this raised AttributeError on last.get("role").
|
||||||
|
assert copilot.request_flags(["hi"]) == (False, False)
|
||||||
|
assert copilot.request_flags([{"role": "user"}, "trailing"]) == (False, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_flags_empty_and_none():
|
||||||
|
assert copilot.request_flags([]) == (False, False)
|
||||||
|
assert copilot.request_flags(None) == (False, False)
|
||||||
|
|
||||||
|
|
||||||
def test_apply_request_headers_mutates():
|
def test_apply_request_headers_mutates():
|
||||||
h = {"X-GitHub-Api-Version": "v"}
|
h = {"X-GitHub-Api-Version": "v"}
|
||||||
copilot.apply_request_headers(h, [{"role": "tool", "content": "x"}])
|
copilot.apply_request_headers(h, [{"role": "tool", "content": "x"}])
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""Regression: extensionless image/audio uploads must get a valid MIME subtype.
|
||||||
|
|
||||||
|
The data-URL subtype was derived only from the stored file's extension
|
||||||
|
(`image_format = ext[1:]`). A pasted screenshot or any file whose stored id
|
||||||
|
carries no extension yields `ext == ""`, so the emitted URL was
|
||||||
|
`data:image/;base64,...` — an empty MIME subtype (invalid per RFC 2046) that
|
||||||
|
vision/audio endpoints reject, silently dropping the attachment. When the
|
||||||
|
extension is missing, fall back to the resolved MIME subtype. Extensions that
|
||||||
|
are present are unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class _Handler:
|
||||||
|
def __init__(self, uploads, image=False, audio=False):
|
||||||
|
self.uploads = uploads
|
||||||
|
self._image = image
|
||||||
|
self._audio = audio
|
||||||
|
|
||||||
|
def resolve_upload(self, fid, owner=None):
|
||||||
|
return self.uploads.get(fid)
|
||||||
|
|
||||||
|
def _inside_upload_dir(self, path):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_image_file(self, name, mime):
|
||||||
|
return self._image and (mime or "").startswith("image/")
|
||||||
|
|
||||||
|
def is_audio_file(self, name, mime):
|
||||||
|
return self._audio and (mime or "").startswith("audio/")
|
||||||
|
|
||||||
|
def is_document_file(self, name, mime):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _blocks(content, block_type):
|
||||||
|
return [b for b in content if isinstance(b, dict) and b.get("type") == block_type]
|
||||||
|
|
||||||
|
|
||||||
|
def test_extensionless_image_uses_mime_subtype(tmp_path):
|
||||||
|
import src.document_processor as dp
|
||||||
|
|
||||||
|
p = tmp_path / ("a" * 32) # bare id, no extension
|
||||||
|
p.write_bytes(b"\x89PNG\r\n\x1a\nfake")
|
||||||
|
uploads = {"img": {"path": str(p), "name": "screenshot", "mime": "image/png"}}
|
||||||
|
|
||||||
|
content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t")
|
||||||
|
imgs = _blocks(content, "image_url")
|
||||||
|
assert imgs, content
|
||||||
|
assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,")
|
||||||
|
|
||||||
|
|
||||||
|
def test_extensionless_audio_uses_mime_subtype(tmp_path):
|
||||||
|
import src.document_processor as dp
|
||||||
|
|
||||||
|
p = tmp_path / ("b" * 32)
|
||||||
|
p.write_bytes(b"fakeaudio")
|
||||||
|
uploads = {"aud": {"path": str(p), "name": "recording", "mime": "audio/mpeg"}}
|
||||||
|
|
||||||
|
content = dp.build_user_content("listen", ["aud"], str(tmp_path), _Handler(uploads, audio=True), owner="t")
|
||||||
|
auds = _blocks(content, "audio")
|
||||||
|
assert auds, content
|
||||||
|
assert auds[0]["audio"]["url"].startswith("data:audio/mpeg;base64,")
|
||||||
|
|
||||||
|
|
||||||
|
def test_extension_present_is_unchanged(tmp_path):
|
||||||
|
import src.document_processor as dp
|
||||||
|
|
||||||
|
p = tmp_path / "pic.png"
|
||||||
|
p.write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||||
|
uploads = {"img": {"path": str(p), "name": "pic.png", "mime": "image/png"}}
|
||||||
|
|
||||||
|
content = dp.build_user_content("look", ["img"], str(tmp_path), _Handler(uploads, image=True), owner="t")
|
||||||
|
imgs = _blocks(content, "image_url")
|
||||||
|
assert imgs[0]["image_url"]["url"].startswith("data:image/png;base64,")
|
||||||
Reference in New Issue
Block a user