Files
hope-voice-api/docs/hermes-installed-contracts.md

16 KiB
Raw Permalink Blame History

Exact installed Hermes contract research

Read-only public-source and image-label research, 2026-10-02. No runtime secrets read, no production mutations, no application code changes. Existing .research-hermes deliberately not trusted.

Verified pins

Repository hermes-webui.Dockerfile:1-10 pins WebUI 62e4d7b9e2560f9d49c0c29c2dc1414fbd4e2d12 and base OCI index nousresearch/hermes-agent@sha256:d4da4a40cd7a28aba983775d9fd31d94cbf153eeb0cb9e844d6d0f612b7c24db. Public Docker Hub index resolves amd64 manifest sha256:d566d4a435767265c2a0172030b778fbdaa1a9ab19442c75ec5aad1df32dec54; public config label org.opencontainers.image.revision is 749220ef0007f8d87bd1531f1c24b0fe93816385. Local Docker daemon unavailable, so no live container inspect performed. Repo qualification records installed pins and authenticated gateway runs; browser approval/cancellation has not been independently qualified.

Exact public checkouts:

  • /tmp/hope-contract-research-agent-749220ef
  • /tmp/hope-contract-research-webui-62e4d7b

Pinned references:

Core implementation recommendation

Use gateway native persisted session APIs, not WebUI-local sidecars, for unified text/voice chat. Hope backend resolves signed-in account private gateway and injects its Bearer key. Never expose credentials or accept client-selected account/gateway URL. Gateway and WebUI both have /api/sessions but entirely different representations/authentication.

Provisioner lines335-346 establishes account /workspace, API loopback, multiplex false, max_concurrent_runs1, progress enabled, terminal/file/web/browser/memory/skills/delegation toolsets. Voice room must not become conversation identity.

Gateway session API

All require Authorization: Bearer <private-key>. Routes lines1603-1650. Handlers2904-3159.

  • GET /api/sessions?limit=50&offset=0&source=...&include_children=false&title=...&include_hidden=false -> {object:"list",data:Session[],limit,offset,has_more}. Limit max200. Title is exact-match; hidden inclusion honored only with title. Pins can be backfilled past limit.
  • POST /api/sessions JSON {id?:string,session_id?:string,title?:string,source?:string,system_prompt?:string,model?:string,provider?:string,model_options?:object,require_model_lock?:boolean} ->201 {object:"hermes.session",session:Session}. ID defaults api_<unix>_<uuid8>, max256/no unsafe path/CR/LF/NUL. Existing409 session_exists.
  • GET /api/sessions/{id} -> {object:"hermes.session",session:Session}; missing404.
  • PATCH /api/sessions/{id} only {title?,end_reason?,pinned?:boolean,archived?:boolean,hidden?:boolean,unread?:boolean} -> same wrapper. Unknown fields400. Pin clears hidden.
  • DELETE /api/sessions/{id} -> {object:"hermes.session.deleted",id,deleted:boolean}.
  • GET /api/sessions/{id}/messages?limit=...&offset=...&order=latest|oldest -> {object:"list",session_id:<resolved-live-tip>,data:Message[],pagination:{limit,offset,order,returned}}. Limit max500. Without limit defaults latest500; explicit limit without order defaults oldest. Nonnegative integer validation.
  • POST /api/sessions/{id}/fork {id?,session_id?,title?} ->201 session wrapper; copies messages to branch and ends parent with branched. NOT resume.
  • Also POST /api/sessions/{id}/chat, /chat/stream, /model exist, but unified run coordinator should use runs consistently.

Session client projection fields (only when present): id,source,user_id,model,title,started_at,ended_at,end_reason,message_count,tool_call_count,input_tokens,output_tokens,cache_read_tokens,cache_write_tokens,reasoning_tokens,estimated_cost_usd,actual_cost_usd,api_call_count,parent_session_id,last_active,preview,_lineage_root_id,pinned,archived,hidden; plus always has_system_prompt:boolean,has_model_config:boolean. No full prompt/config. Message projection fields when present: id,session_id,role,content,tool_call_id,tool_calls,tool_name,timestamp,token_count,finish_reason,reasoning,reasoning_content,display_kind. Projection2840-2866.

Resume identity

Runs identity precedence body.session_id > previous_response_id stored session > declared X-Hermes-Session-Key > fresh run_id. Runs does NOT select history from X-Hermes-Session-Id alone. Explicit/chained session never rebound to header.

History precedence explicit nonempty conversation_history > response snapshot > all-but-last input messages > selected-session SessionDB. For native resume send only current input + session_id, no browser-replayed history. Old session IDs resolve compression/continuation tip, excluding branch/delegate/reset/tool children. History response returns resolved ID; preserve logical root separately if stable UI identity needed. GET session metadata itself does not resolve. Resolver1213-1252, runs638-661.

X-Hermes-Session-Key is optional conversation/memory affinity, not account auth, <=256/no CR/LF/NUL, requires configured API auth. Use stable conversation scope, not ephemeral voice room. WebUI uses webui:<session-id> and explicitly supplies history, unlike recommended native resume.

Runs: exact request/response

POST /v1/runs JSON {input:string|[{role:"user",content:string|multimodal-array}],session_id?:string,model?:string,provider?:string,model_options?:object,instructions?:string,conversation_history?:[{role,content}],previous_response_id?:string}. Nonempty current input required. Last input array element content is current message, earlier elements become history.

Send Bearer key, Content-Type JSON, Idempotency-Key stable unique per admission (1–255 visible ASCII), optional X-Hermes-Session-Key. Returns202 {run_id:"run_<uuidhex>",status:"started",replayed:false}. Exact retry body/session-key/idempotency key returns original202 {run_id,status:<current>,replayed:true} plus Idempotency-Replayed:true; different fingerprint409 idempotency_key_conflict. Persist exact request before admission. Durable status persistence is tied to idempotent records; always use idempotency. Admission567-703, response413-430.

GET /v1/runs/{id} -> {object:"hermes.run",run_id,status,created_at,updated_at,session_id?,model?,output?,usage?,runtime?,error?,approval?,last_event?,shutdown_requested_at?,...}. Status queued/running/waiting_for_approval/stopping/completed/failed/cancelled/interrupted. Terminal output is STRING, not message array. Usage includes input_tokens/output_tokens/total_tokens/cache_read_tokens/cache_write_tokens. Poll output is answer fallback; native messages is transcript fallback.

GET /v1/runs/{id}/events SSE payload envelope {event,run_id,timestamp,...}. Names:

  • message.delta {delta}
  • message.interim {text,already_streamed} (avoid double render)
  • tool.started {tool,preview}
  • tool.completed {tool,duration,error,...} (bounded redacted preview can occur)
  • reasoning.available {text} (never speak as answer)
  • approval.request copied approval data, redacted command, choices + envelope; status approval holds same event
  • approval.responded {choice,request_id?,resolved}
  • run.completed {output:string,usage,runtime,...terminal-fields}
  • run.failed/run.cancelled/run.interrupted terminal fields/error

Events consume one asyncio.Queue, not broadcast or durable replay. No Last-Event-ID cursor contract. One upstream consumer per run; Hope coordinator broadcasts/replays to clients. Disconnect drops transport; use poll/history recovery, not duplicated execution. Gateway restart interrupts run; idempotency prevents duplicate admission, does not resurrect execution. Events1006-1047, completion862-940.

POST /v1/runs/{id}/stop no body required -> {run_id,status:"stopping"}; already terminal -> existing status. Poll terminal. Missing404; notactive409. Stopping is not completion. Source1147-1168.

POST /v1/runs/{id}/approval {choice:"once"|"session"|"always"|"deny",request_id?:string,all?:boolean,resolve_all?:boolean} -> {object:"hermes.run.approval_response",run_id,choice,request_id?,resolved:int}. Prefer exact request_id and once/deny. Request ID nonempty<=256. Aliases approve/approved/allow normalize once. No active/pending409; invalid400. Approval event is exactly approval.request, not approval.required; fields copied from provider approval payload, not fixed required reason/description schema. Room grants constrain once/deny/exact request_id/no all. Source1062-1110; API112-125.

POST /steer {input:string} (message/text aliases) -> {object:"hermes.run.steer",run_id,accepted:true}; running only409 otherwise.

Voice instructions vs temporary privacy

Runs instructions maps to ephemeral_system_prompt, appended to cached native system prompt at API-call time, not persisted and not replacement. Use per-voice-turn concise speech instructions while preserving native tool/system/memory. Composition1279-1286, runs682.

No normal runs no-persistence/no-memory/per-request toolset option. _create_agent always passes SessionDB, configured toolsets and memory manager. A fresh session/run ID is still persisted. Deleting transcript later cannot undo memory/files/tool side effects. Instructions "ignore memory" are advisory, NOT privacy enforcement. Do not claim private temporary chat support on this pin without separately isolated/disabled persistence and side-effect controls. Agent construction2211-2283.

Gateway jobs, capabilities, tools

GET /v1/capabilities inspect runtime availability, including endpoints/feature flags. Static capabilities explicitly admin_config_rw:false, memory_write_api:false. GET /v1/toolsets lists toolset enabled/configured/key/tool state; GET /v1/skills installed skill manifest. Not a connector CRUD/admin API. No invented connector API; configured provider/tool availability is source/runtime dependent.

Gateway cron is /api/jobs, not WebUI /api/crons. Authenticated; missing cron module501. ID12 lowercase hex; name max200, prompt max5000/scanned.

  • GET /api/jobs?include_disabled=true -> {jobs:Job[]}, defaults excludes disabled.
  • POST /api/jobs {name(required),schedule(required,string),prompt?,deliver?:"local",skills?:[],repeat?:positive-int,paused?,paused_reason?} ->200 {job}; scheduler registration error424.
  • GET /api/jobs/{id} -> {job}; PATCH whitelist name,schedule,prompt,deliver,skills,skill,repeat,enabled -> {job}; DELETE -> {ok:true}.
  • POST /pause, /resume -> {job}.
  • POST /run optional {prompt} transient context -> {job}; schedules trigger, not synchronous answer.

Job raw fields: id,name,prompt,skills,skill,model,provider,base_url,script,no_agent,monitor_script,monitor_url,monitor_state,context_from,schedule object,schedule_display,repeat:{times:int|null,completed:int},enabled,state,paused_at,paused_reason,created_at,next_run_at,last_run_at,last_status,last_error,last_delivery_error,last_delivery_unverified,failure_streak,deliver,origin,enabled_toolsets,workdir. Project safe client fields rather than exposing arbitrary future raw config. Jobs3619-3797, record1809-1852.

Files/memory: exact boundary

Gateway has NO direct workspace file browser or memory REST. Native tools can operate inside runs, but do not fabricate /api/files or /api/memory. Browser-control /v1/artifacts/upload|download/{id} is optional gated one-shot scope-bound receipt/download, NOT general upload; provisioner doesn't enable browser extension control. Upload raw MIME bytes/X-Artifact-Filename ->201 provenance receipt; download second read404. API2710-2782.

WebUI auxiliary API uses distinct password/cookie auth and WebUI-local session/workspace state. Gateway-only IDs do not automatically establish WebUI-local sessions. Intentional fixed-root account adapter needed; no arbitrary host-path proxy.

WebUI memory

GET /api/memory -> {memory,user,soul,project_context,memory_path,user_path,soul_path,project_context_path,project_context_name,project_context_workspace,memory_mtime,user_mtime,soul_mtime,project_context_mtime,project_context_shadowed,external_notes_enabled}. Redacted text. Actual home files memories/MEMORY.md, memories/USER.md, SOUL.md. For provisioned accounts HERMES_HOME=/opt/data. Respects memory_enabled/user_profile_enabled.

POST /api/memory/write {section:"memory"|"user"|"soul",content:string} -> {ok:true,section,path}. Full replacement/no conflict version; disabled403; refuses symlink. A safe Hope adapter must resolve account home internally and use fixed section allowlist, not client path. Source routes23359-23418 and30271-30334.

WebUI files/upload

  • GET /api/file?session_id=<WebUI-ID>&path=<relative> -> ordinary text {path,content,size,lines}; separate Office conversion schema. Existing WebUI session workspace confinement. routes22038-22055; workspace.py1866-1887.
  • GET /api/file/raw?session_id&path -> bytes/MIME/disposition. routes21998-22036.
  • POST /api/file/save {session_id,path,content} -> {ok:true,path,size}, existing target only; symlink refusal, Office separate office-save. routes27513-27541.
  • POST /api/file/delete {session_id,path,recursive?} -> {ok:true,path}; directories require recursive. routes27481-27510.
  • POST /api/upload multipart session_id,file -> {filename,path:absolute-private-container-path,size,mime,is_image}. Writes session attachment dir, not workspace; sanitized/no overwrite/symlink. Upload294-340.
  • /api/workspace/upload and /api/upload/extract separate workspace/archive flows. Do not treat attachment absolute paths as browser URLs.

WebUI cron GET /api/crons, history/run/recent/status/output; POST create/update/delete/run/pause/resume. Create {prompt,schedule,name?,deliver?,skills?,model?,provider?,profile?,toast_notifications?,badge_notifications?} -> {ok:true,job}. UI enriches local Hermes cron data/profile state; gateway jobs preferred for basic unified support. routes15302-15361,17419-17454,26815-26855.

Evidence limit

Exact source contracts, not new live execution qualification. Retained config/provider/runtime capability can narrow availability. No test sessions created. Cancellation/approval honest pending/stopping/error states required. Temporary privacy unsupported by ordinary pinned runs API. Public source checkouts and this new doc only.