mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(acp): an overlay HERMES_HOME stages prompt images in the owner's cache
An overlay task home attaches to the profile its symlinked lock names, but image staging still used the overlay's ambient HERMES_HOME. The overlay does not share cache/images, and the owner admits attachments only from its own image cache, so every image prompt failed with invalid_params. Staging now runs under the resolved owner home (context-local home override); the owner's path check is unchanged. Review finding on #140190 (Greptile) Refs #134889 (ACP-18) Refs #140138
This commit is contained in:
1 parent
82f58120a9
commit
a042d5a2c9
3 files changed
+37
-3
No files matched your search
@@ -39,16 +39,26 @@ def _take(path):
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
async def stage_user_content(content):
|
||||
async def stage_user_content(content, home=None):
|
||||
"""Shared-converter output -> ``(text, attachments)`` for ``prompt.submit``.
|
||||
|
||||
Text-only prompts stay a plain string. Media parts (image ``data:`` URLs from
|
||||
image blocks, image resource links and embedded blobs, and remote image URLs once
|
||||
fetched) are staged as bytes in the profile image cache; the authority commits them
|
||||
at admission. A remote image URL that cannot be fetched is kept as text so the model
|
||||
still sees it. Only a remote image awaits (its download); every other part stages inline."""
|
||||
still sees it. Only a remote image awaits (its download); every other part stages inline.
|
||||
|
||||
``home`` is the owner's profile home: a per-task overlay ``HERMES_HOME`` does not share
|
||||
``cache/images``, and admission accepts only the owner's cache (never relaxed here)."""
|
||||
if isinstance(content, str):
|
||||
return content, []
|
||||
if home is not None:
|
||||
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
||||
token = set_hermes_home_override(home)
|
||||
try:
|
||||
return await stage_user_content(content)
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
import base64
|
||||
import binascii
|
||||
from gateway.platforms.base import cache_image_from_bytes
|
||||
|
||||
@@ -632,7 +632,7 @@ class GatewayACPAgent(GatewayReplyMixin, acp.Agent):
|
||||
# submit itself, and the prompt then ends without being sent.
|
||||
self._submitting.add(session_id)
|
||||
try:
|
||||
staged = await stage_user_content(content)
|
||||
staged = await stage_user_content(content, self._home)
|
||||
finally:
|
||||
self._submitting.discard(session_id)
|
||||
if session_id in self._pending_cancels:
|
||||
|
||||
@@ -39,3 +39,27 @@ async def test_overlay_home_attaches_to_the_owner_its_lock_names(daemon, tmp_pat
|
||||
saved = await ws.rpc('session.resume', session_id=sid)
|
||||
assert 'WS_SHARED overlay' in json.dumps(saved['messages'])
|
||||
assert not (overlay[0] / 'gateway.sock').exists() and not (overlay[0] / 'gateway.pid').exists()
|
||||
|
||||
|
||||
_ONE_PX_PNG = bytes.fromhex(
|
||||
'89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4'
|
||||
'890000000a49444154789c6300010000000500010d0a2db40000000049454e44ae426082')
|
||||
|
||||
|
||||
@pytest.mark.platforms('linux')
|
||||
@pytest.mark.asyncio
|
||||
async def test_overlay_home_image_prompt_is_staged_where_the_owner_admits_it(daemon, tmp_path, model_peer):
|
||||
"""The overlay does not share ``cache/images``: an image is staged in the owner's cache, the only
|
||||
place its admission accepts (#140190 review)."""
|
||||
import base64
|
||||
overlay = _overlay(daemon, tmp_path)
|
||||
async with editor(overlay, tmp_path) as acp:
|
||||
await acp.rpc('initialize', protocolVersion=1, clientCapabilities={})
|
||||
sid = (await acp.rpc('session/new', cwd=str(tmp_path), mcpServers=[]))['result']['sessionId']
|
||||
turn = await acp.rpc('session/prompt', sessionId=sid, prompt=[
|
||||
{'type': 'text', 'text': 'WS_SHARED overlay image'},
|
||||
{'type': 'image', 'data': base64.b64encode(_ONE_PX_PNG).decode(), 'mimeType': 'image/png'}])
|
||||
assert 'result' in turn, (turn, (tmp_path / 'acp.log').read_text()[-2000:])
|
||||
assert turn['result']['stopReason'] == 'end_turn', turn
|
||||
assert any(daemon[0].glob('cache/images/img_*.png')), list(daemon[0].rglob('*.png'))
|
||||
assert not any(overlay[0].glob('cache/images/*')), list(overlay[0].rglob('*.png'))
|
||||
Reference in new issue
Block a user