Skip to content

Commit c050a1d

Browse files
authored
refactor: enforce canonical server module ownership (#2744)
* chore: add server module boundary harness * refactor: establish Studio server module foundation * refactor: move Studio update behind module boundary * refactor: move shared server infrastructure into Studio * refactor: move Studio state into repositories * refactor: classify common Studio services * refactor: migrate Studio theme vertical slice * refactor: migrate Studio network and app relay modules * refactor: migrate Studio device networking slice * refactor: migrate Studio social messages slice * refactor: isolate coding agents module * refactor: isolate Ekko agent module * refactor: establish Hermes runtime history and kanban modules * refactor: migrate Hermes profiles jobs bundles and files * refactor: isolate Hermes bridge gateway and providers * refactor: migrate Hermes control plane slices * refactor: migrate Hermes skills plugins and files * refactor: migrate runtime webhooks and monitoring slices * refactor: centralize cross-agent webhooks pets and logs * refactor: centralize Studio voice and media services * refactor: isolate Hermes terminal socket * refactor: migrate Studio workflow orchestration * refactor: migrate Studio single chat runtime * refactor: migrate Studio global agent runtime * refactor: migrate Studio group chat runtime * refactor: migrate Studio auth and context services * refactor: migrate Studio session orchestration * refactor: isolate Hermes profile model adapters * refactor: complete server bootstrap boundaries * refactor: enforce canonical server module ownership * chore: consolidate server migration harness * fix: preserve legacy App API compatibility * fix: preserve Studio session listing semantics * fix: retain delegated OpenAPI request bodies
1 parent bcfaacb commit c050a1d

1,199 files changed

Lines changed: 15815 additions & 15417 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ jobs:
2323
steps:
2424
- name: Checkout repository
2525
uses: actions/checkout@v4
26-
with:
27-
fetch-depth: 0
2826

2927
- name: Setup Node.js
3028
uses: actions/setup-node@v4

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and keep this file small enough to fit into every task context.
1111
- `docs/harness/validation.md` - which checks to run for each change type.
1212
- `docs/harness/worktree-runbook.md` - isolated local dev and test setup.
1313
- `docs/harness/pr-review.md` - self-review checklist before pushing.
14+
- `docs/harness/server-module-boundaries.md` - target backend modules, ownership, and dependency rules.
1415

1516
## Common Commands
1617

@@ -38,6 +39,7 @@ Use the smallest relevant check while iterating. Before a broad PR, run
3839
## Hard Rules
3940

4041
- Keep routes thin: put request handling in controllers and reusable behavior in services.
42+
- Put new server code under `modules/studio`, `modules/hermes`, `modules/ekko`, or `modules/coding-agents`; compose modules only from `bootstrap`.
4143
- Keep Web UI state under `HERMES_WEB_UI_HOME` or `HERMES_WEBUI_STATE_DIR`.
4244
- Keep Hermes Agent state separate from Web UI state.
4345
- Register local API routes before proxy catch-all routes.

ARCHITECTURE.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ backend, and an Electron desktop distribution around Hermes Agent.
1717

1818
1. The browser loads the Vite-built client from the Koa server.
1919
2. Client modules call API helpers from `packages/client/src/api`.
20-
3. Server routes in `packages/server/src/routes` wire HTTP paths to controllers.
20+
3. Server routes in `packages/server/src/modules/*/routes` wire HTTP paths to controllers.
2121
4. Controllers validate request concerns and delegate reusable behavior to services.
2222
5. Services own side effects: files, SQLite, Hermes profiles, subprocesses, bridges, and credentials.
2323
6. Long-running chat and group-chat flows use Socket.IO namespaces managed by server services.
@@ -36,17 +36,22 @@ should not duplicate server persistence rules.
3636

3737
## Server Structure
3838

39-
- `routes/` registers HTTP and WebSocket entry points.
40-
- `controllers/` handles request-level behavior.
41-
- `services/` owns reusable IO, domain behavior, external process calls, and integration logic.
42-
- `db/` owns SQLite schemas and stores.
43-
- `middleware/` owns request middleware such as user auth.
44-
- `shared/` contains cross-server constants and helpers.
39+
Server code is separated by business ownership under `modules/studio`,
40+
`modules/hermes`, `modules/ekko`, and `modules/coding-agents`; concrete module
41+
composition belongs in `bootstrap`.
42+
See `docs/harness/server-module-boundaries.md` for the complete target tree,
43+
ownership decisions, allowed dependency matrix, and migration rules.
44+
45+
- Module `routes/` register HTTP entry points; `sockets/` own Socket.IO transports.
46+
- Module `controllers/` handle request-level behavior.
47+
- Module `services/` own reusable IO, domain behavior, processes, and integrations.
48+
- Studio `repositories/` and `infrastructure/` own application persistence.
49+
- Studio `middleware/legacy-app-api.ts` is the only old-App URL compatibility map.
4550

4651
Architecture rules:
4752

4853
- Register local API routes before proxy catch-all routes.
49-
- Keep auth behavior centralized in `packages/server/src/services/auth.ts`.
54+
- Keep auth behavior under `packages/server/src/modules/studio/services/auth`.
5055
- Prefer `execFile` or `spawn` with argument arrays over shell command strings.
5156
- Use structured file and YAML/JSON parsers when editing structured data.
5257

bin/hermes-studio-mcp.mjs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ function validateRequiredObjectFields(schema, value, location) {
303303
}
304304

305305
async function validateApiRequest(method, path, args) {
306-
if (pathWithoutQuery(path) === '/api/openapi.json' || pathWithoutQuery(path) === '/api/hermes/openapi.json') return null
306+
if (pathWithoutQuery(path) === '/api/openapi.json') return null
307307
const openapi = await openApiDocument(withAuthArgs(args))
308308
const match = findOpenApiOperation(openapi, method, path)
309309
if (!match) return `Unknown endpoint in OpenAPI document: ${method} ${pathWithoutQuery(path)}`
@@ -923,7 +923,7 @@ const tools = [
923923
inputSchema: inputSchema({
924924
path: {
925925
type: 'string',
926-
description: 'Optional exact endpoint path filter for on-demand details, for example /api/chat-run/runs.',
926+
description: 'Optional exact endpoint path filter for on-demand details, for example /api/studio/chat-run/runs.',
927927
},
928928
method: {
929929
type: 'string',
@@ -943,7 +943,7 @@ const tools = [
943943
{
944944
name: 'hermes_studio_api_request',
945945
toolset: 'api',
946-
description: 'Execute a Hermes Studio operation by calling an endpoint path. Use hermes_studio_api_openapi_get first as the operation manual to inspect method, parameters, requestBody, and responses. Do not use /api/chat-run/* or /api/hermes/sessions/* as an internal delegation mechanism.',
946+
description: 'Execute a Hermes Studio operation by calling an endpoint path. Use hermes_studio_api_openapi_get first as the operation manual to inspect method, parameters, requestBody, and responses. Do not use /api/studio/chat-run/* or /api/studio/sessions/* as an internal delegation mechanism.',
947947
inputSchema: inputSchema({
948948
method: {
949949
type: 'string',
@@ -952,7 +952,7 @@ const tools = [
952952
},
953953
path: {
954954
type: 'string',
955-
description: 'Relative Hermes Studio endpoint path from the operation manual, for example /api/hermes/sessions?limit=20. Full URLs and // paths are rejected.',
955+
description: 'Relative Hermes Studio endpoint path from the operation manual, for example /api/studio/sessions?limit=20. Full URLs and // paths are rejected.',
956956
},
957957
body: {
958958
type: ['object', 'array', 'string', 'number', 'boolean', 'null'],
@@ -1814,7 +1814,7 @@ async function callTool(name, args = {}) {
18141814
return jsonText(await requestEnvelope(path, options))
18151815
}
18161816
case 'hermes_studio_use_chat_run':
1817-
return jsonText(await request('/api/chat-run/runs', withAuthArgs(args, {
1817+
return jsonText(await request('/api/studio/chat-run/runs', withAuthArgs(args, {
18181818
method: 'POST',
18191819
body: pickDefined(args, [
18201820
'input',
@@ -1838,34 +1838,34 @@ async function callTool(name, args = {}) {
18381838
]),
18391839
})))
18401840
case 'hermes_studio_use_sessions_list':
1841-
return jsonText(await request('/api/hermes/sessions', withAuthArgs(args, {
1841+
return jsonText(await request('/api/studio/sessions', withAuthArgs(args, {
18421842
query: pickDefined(args, ['limit', 'source']),
18431843
})))
18441844
case 'hermes_studio_use_sessions_count':
1845-
return jsonText(await request('/api/hermes/sessions/count', withAuthArgs(args, {
1845+
return jsonText(await request('/api/studio/sessions/count', withAuthArgs(args, {
18461846
query: pickDefined(args, ['source']),
18471847
})))
18481848
case 'hermes_studio_use_usage_stats':
1849-
return jsonText(await request('/api/hermes/usage/stats', withAuthArgs(args, {
1849+
return jsonText(await request('/api/studio/usage/stats', withAuthArgs(args, {
18501850
query: pickDefined(args, ['days']),
18511851
})))
18521852
case 'hermes_studio_use_session_get':
1853-
return jsonText(await request(`/api/hermes/sessions/${encodeURIComponent(args.session_id)}`, withAuthArgs(args)))
1853+
return jsonText(await request(`/api/studio/sessions/${encodeURIComponent(args.session_id)}`, withAuthArgs(args)))
18541854
case 'hermes_studio_use_session_messages':
1855-
return jsonText(await request(`/api/hermes/sessions/conversations/${encodeURIComponent(args.session_id)}/messages`, withAuthArgs(args, {
1855+
return jsonText(await request(`/api/studio/sessions/conversations/${encodeURIComponent(args.session_id)}/messages`, withAuthArgs(args, {
18561856
query: args.include_internal ? { humanOnly: '0' } : undefined,
18571857
})))
18581858
case 'hermes_studio_use_session_context':
18591859
return jsonText(cleanSessionContextPayload(
1860-
await request(`/api/hermes/sessions/${encodeURIComponent(args.session_id)}/context`, withAuthArgs(args)),
1860+
await request(`/api/studio/sessions/${encodeURIComponent(args.session_id)}/context`, withAuthArgs(args)),
18611861
args,
18621862
))
18631863
case 'hermes_studio_use_session_delete':
1864-
return jsonText(await request(`/api/hermes/sessions/${encodeURIComponent(args.session_id)}`, withAuthArgs(args, {
1864+
return jsonText(await request(`/api/studio/sessions/${encodeURIComponent(args.session_id)}`, withAuthArgs(args, {
18651865
method: 'DELETE',
18661866
})))
18671867
case 'hermes_studio_use_session_rename':
1868-
return jsonText(await request(`/api/hermes/sessions/${encodeURIComponent(args.session_id)}/rename`, withAuthArgs(args, {
1868+
return jsonText(await request(`/api/studio/sessions/${encodeURIComponent(args.session_id)}/rename`, withAuthArgs(args, {
18691869
method: 'POST',
18701870
body: { title: args.title },
18711871
})))
@@ -1901,16 +1901,16 @@ async function callTool(name, args = {}) {
19011901
})))
19021902
case 'hermes_studio_use_worker_status':
19031903
return jsonText(summarizeWorkerRuntime(
1904-
await request('/api/hermes/performance/runtime', withAuthArgs(args)),
1904+
await request('/api/studio/performance/runtime', withAuthArgs(args)),
19051905
))
19061906
case 'hermes_studio_use_workflows_list':
1907-
return jsonText(await request('/api/hermes/workflows', withAuthArgs(args, {
1907+
return jsonText(await request('/api/studio/workflows', withAuthArgs(args, {
19081908
query: pickDefined(args, ['profile']),
19091909
})))
19101910
case 'hermes_studio_use_workflow_get':
1911-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}`, withAuthArgs(args)))
1911+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}`, withAuthArgs(args)))
19121912
case 'hermes_studio_use_workflow_create':
1913-
return jsonText(await request('/api/hermes/workflows', withAuthArgs(args, {
1913+
return jsonText(await request('/api/studio/workflows', withAuthArgs(args, {
19141914
method: 'POST',
19151915
body: pickDefined(args, [
19161916
'name',
@@ -1922,7 +1922,7 @@ async function callTool(name, args = {}) {
19221922
]),
19231923
})))
19241924
case 'hermes_studio_use_workflow_update':
1925-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}`, withAuthArgs(args, {
1925+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}`, withAuthArgs(args, {
19261926
method: 'PATCH',
19271927
body: pickDefined(args, [
19281928
'name',
@@ -1933,15 +1933,15 @@ async function callTool(name, args = {}) {
19331933
]),
19341934
})))
19351935
case 'hermes_studio_use_workflow_delete':
1936-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}`, withAuthArgs(args, {
1936+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}`, withAuthArgs(args, {
19371937
method: 'DELETE',
19381938
})))
19391939
case 'hermes_studio_use_workflow_runs_list':
1940-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}/runs`, withAuthArgs(args, {
1940+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}/runs`, withAuthArgs(args, {
19411941
query: pickDefined(args, ['limit']),
19421942
})))
19431943
case 'hermes_studio_use_workflow_run_start':
1944-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}/run`, withAuthArgs(args, {
1944+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}/run`, withAuthArgs(args, {
19451945
method: 'POST',
19461946
body: pickDefined(args, [
19471947
'start_node_ids',
@@ -1950,11 +1950,11 @@ async function callTool(name, args = {}) {
19501950
]),
19511951
})))
19521952
case 'hermes_studio_use_workflow_run_stop':
1953-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(args.run_id)}/stop`, withAuthArgs(args, {
1953+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(args.run_id)}/stop`, withAuthArgs(args, {
19541954
method: 'POST',
19551955
})))
19561956
case 'hermes_studio_use_workflow_rerun_node':
1957-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(args.run_id)}/rerun-from-node`, withAuthArgs(args, {
1957+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(args.run_id)}/rerun-from-node`, withAuthArgs(args, {
19581958
method: 'POST',
19591959
body: pickDefined(args, [
19601960
'node_id',
@@ -1963,7 +1963,7 @@ async function callTool(name, args = {}) {
19631963
]),
19641964
})))
19651965
case 'hermes_studio_use_workflow_run_delete':
1966-
return jsonText(await request(`/api/hermes/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(args.run_id)}`, withAuthArgs(args, {
1966+
return jsonText(await request(`/api/studio/workflows/${encodeURIComponent(args.workflow_id)}/runs/${encodeURIComponent(args.run_id)}`, withAuthArgs(args, {
19671967
method: 'DELETE',
19681968
})))
19691969
case 'hermes_studio_lan_devices_list':

docs/agent-runner.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Coding Agent services
22

3-
`packages/server/src/services/coding-agents/` is the implementation home for
3+
`packages/server/src/modules/coding-agents/` is the implementation home for
44
managed Claude Code, Codex, and Pi runs. The services share one canonical event
55
pipeline, one stream subscription model, and one persistence path while keeping
66
agent-specific process and protocol behavior in named subdirectories.
@@ -353,6 +353,3 @@ Minimum checks for each phase:
353353
- `npm run test -- tests/server/run-chat-content-blocks.test.ts` when touching
354354
chat input conversion.
355355
- `npm run build` before merging shared TypeScript contracts.
356-
357-
For chat session behavior changes, also add a fragment under
358-
`docs/chat-chain-changes/` according to the repository validation guide.

docs/app-relay.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ existing takeover behavior so a restarted desktop can recover stale connections.
115115

116116
## Forwarded protocols
117117

118-
- HTTP RPC accepts Studio `/api/**`, `/upload`, and `/health` paths.
118+
- HTTP RPC accepts Studio `/api/**` and `/health` paths.
119119
- Request headers, methods, paths, and Socket.IO client events are allowlisted.
120120
- Request and response bodies are capped at 20 MiB.
121121
- Socket RPC accepts `/chat-run` and `/group-chat` only.

docs/chat-chain-changes/2026-07-01-global-agent-streaming-tts.md

Lines changed: 0 additions & 8 deletions
This file was deleted.

docs/chat-chain-changes/2026-07-01-pr1871-desktop-mcp-shutdown.md

Lines changed: 0 additions & 9 deletions
This file was deleted.

docs/chat-chain-changes/2026-07-01-pr1883-chat-input-toolbar.md

Lines changed: 0 additions & 9 deletions
This file was deleted.

docs/chat-chain-changes/2026-07-02-issue-1881-bridge-terminal-errors.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)