mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-29 11:42:04 +00:00
Some checks failed
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build and Publish / build-amd64 (push) Waiting to run
Docker Build and Publish / build-arm64 (push) Waiting to run
Docker Build and Publish / merge (push) Blocked by required conditions
Lint (ruff + ty) / ruff + ty diff (push) Waiting to run
Lint (ruff + ty) / ruff enforcement (blocking) (push) Waiting to run
Lint (ruff + ty) / Windows footguns (blocking) (push) Waiting to run
Tests / test (1) (push) Waiting to run
Tests / test (2) (push) Waiting to run
Tests / test (3) (push) Waiting to run
Tests / test (4) (push) Waiting to run
Tests / test (5) (push) Waiting to run
Tests / test (6) (push) Waiting to run
Tests / save-durations (push) Blocked by required conditions
Tests / e2e (push) Waiting to run
Typecheck / typecheck (apps/bootstrap-installer) (push) Waiting to run
Typecheck / typecheck (apps/desktop) (push) Waiting to run
Typecheck / typecheck (apps/shared) (push) Waiting to run
Typecheck / typecheck (ui-tui) (push) Waiting to run
Typecheck / typecheck (web) (push) Waiting to run
Typecheck / desktop-build (push) Waiting to run
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
On a remote gateway connection, agent-written files live on the gateway
host, not the desktop's disk, so the Artifacts view's file:// hrefs failed
("Invalid external URL") and image thumbnails broke.
Make mediaExternalUrl() remote-aware in one place: in remote mode it
rewrites gateway-local paths to GET /api/files/download (a new endpoint
that streams the file as a Content-Disposition: attachment). The artifacts
view now resolves through it, and so do the existing chat-media and
generated-image callers, for free.
The download endpoint stays auth-gated; auth_middleware additionally
accepts the session token as a ?token= query param for this one path so a
shell/browser-opened download (which can't set the session header) still
authenticates — the same query-token tradeoff as the /api/pty WebSocket.
It is NOT added to PUBLIC_API_PATHS.
Salvages #46663 (which carried ~19k lines of CRLF noise and made the
endpoint public). Reimplemented on a clean LF base with the security hole
closed and tests added.
Co-authored-by: qingshan89 <qs2816661685@gmail.com>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { $connection } from '@/store/session'
|
|
|
|
import { filePathFromMediaPath, gatewayMediaDataUrl, isRemoteGateway, mediaExternalUrl } from './media'
|
|
|
|
describe('isRemoteGateway', () => {
|
|
afterEach(() => {
|
|
$connection.set(null)
|
|
})
|
|
|
|
it('is false with no connection', () => {
|
|
$connection.set(null)
|
|
expect(isRemoteGateway()).toBe(false)
|
|
})
|
|
|
|
it('is false in local mode', () => {
|
|
$connection.set({ mode: 'local' } as never)
|
|
expect(isRemoteGateway()).toBe(false)
|
|
})
|
|
|
|
it('is true in remote mode', () => {
|
|
$connection.set({ mode: 'remote' } as never)
|
|
expect(isRemoteGateway()).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('filePathFromMediaPath', () => {
|
|
it('passes through a plain path', () => {
|
|
expect(filePathFromMediaPath('/home/u/.hermes/images/a.png')).toBe('/home/u/.hermes/images/a.png')
|
|
})
|
|
|
|
it('decodes a file:// URL with encoded characters', () => {
|
|
expect(filePathFromMediaPath('file:///tmp/a%20b.png')).toBe('/tmp/a b.png')
|
|
})
|
|
})
|
|
|
|
describe('mediaExternalUrl', () => {
|
|
afterEach(() => {
|
|
$connection.set(null)
|
|
})
|
|
|
|
it('passes through http(s) URLs untouched', () => {
|
|
$connection.set({ mode: 'remote', baseUrl: 'https://gw', token: 't' } as never)
|
|
expect(mediaExternalUrl('https://example.com/a.png')).toBe('https://example.com/a.png')
|
|
})
|
|
|
|
it('keeps file:// form in local mode', () => {
|
|
$connection.set({ mode: 'local' } as never)
|
|
expect(mediaExternalUrl('/tmp/a.png')).toBe('file:///tmp/a.png')
|
|
expect(mediaExternalUrl('file:///tmp/a.png')).toBe('file:///tmp/a.png')
|
|
})
|
|
|
|
it('rewrites gateway-local paths to an authenticated download URL', () => {
|
|
$connection.set({ mode: 'remote', baseUrl: 'https://gw', token: 's e/cret' } as never)
|
|
expect(mediaExternalUrl('file:///tmp/a b.png')).toBe(
|
|
'https://gw/api/files/download?path=%2Ftmp%2Fa%20b.png&token=s%20e%2Fcret'
|
|
)
|
|
expect(mediaExternalUrl('/tmp/a b.png')).toBe(
|
|
'https://gw/api/files/download?path=%2Ftmp%2Fa%20b.png&token=s%20e%2Fcret'
|
|
)
|
|
})
|
|
|
|
it('falls back to file:// when remote connection lacks a token', () => {
|
|
$connection.set({ mode: 'remote', baseUrl: 'https://gw' } as never)
|
|
expect(mediaExternalUrl('/tmp/a.png')).toBe('file:///tmp/a.png')
|
|
})
|
|
})
|
|
|
|
describe('gatewayMediaDataUrl', () => {
|
|
const api = vi.fn(async () => ({ data_url: 'data:image/png;base64,ZHVtbXk=' }))
|
|
|
|
beforeEach(() => {
|
|
api.mockClear()
|
|
vi.stubGlobal('window', { hermesDesktop: { api } })
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('requests the encoded gateway path and returns the data URL', async () => {
|
|
const url = await gatewayMediaDataUrl('/home/u/.hermes/images/a b.png')
|
|
|
|
expect(url).toBe('data:image/png;base64,ZHVtbXk=')
|
|
expect(api).toHaveBeenCalledWith({
|
|
path: '/api/media?path=%2Fhome%2Fu%2F.hermes%2Fimages%2Fa%20b.png'
|
|
})
|
|
})
|
|
})
|