Architecture

September 8, 2026 · View on GitHub

hera (repo: hera-agent-godot) is a low-token CLI that lets an AI coding agent inspect and control a live Godot 4.7+ editor in real time.

This is a sibling of hera-agent-unity, but it is not a port. Godot's scene tree, editor plugin model, and scripting workflow are different enough that the bridge is designed around Godot-native concepts.


1. High-level model

Two processes talk over localhost HTTP:

 ┌─────────────────────┐         HTTP POST /rpc          ┌──────────────────────────────┐
│   Go CLI             │  ─────────────────────────────▶ │  Godot Editor                │
│  hera-agent-godot    │   { "tool": "...", "params": } │   addons/hera_agent_godot/   │
 │                      │ ◀───────────────────────────── │   @tool EditorPlugin         │
 │  cmd/ internal/      │   { "ok": true, "data": ... }  │   GDScript                   │
 └─────────────────────┘                                 └──────────────────────────────┘
          │                                                            │
          │ scans                                                      │ writes every ~0.5s
          ▼                                                            ▼
   ~/.hera-agent-godot/instances/<pid>.json  ◀──── Heartbeat ──────────┘
  • The CLI is a thin client. It discovers running editors, picks one, and sends a single compact JSON request per command.
  • script validate first resolves the selected editor's engine/project paths, then runs a bounded native --check-only child process to avoid stale script cache results. It captures at most 64 KiB and reports the engine exit status.
  • The addon is a GDScript @tool EditorPlugin. It binds a local HTTP server, queues each request, and executes editor work from the editor main loop.
  • No MCP server — by design, not for lack of one. Godot's MCP-addon ecosystem is active, but MCP pays for breadth in tokens: many tool schemas plus verbose JSON sit in the agent's context every turn. Hera delivers comparable editor reach as a compact-JSON CLI, so any agent that can run a shell command can use it — not only MCP clients.

2. Godot-specific constraints

#Godot realityDesign consequence
1GDScript addons are standard Godot addons under res://addons/<name>/.Distribution is just copying addons/hera_agent_godot/; no .NET SDK or generated project files. The addon lives at the repo root so the Asset Library installs it correctly.
2Editor scripts use @tool and run inside the editor.The plugin entrypoint is hera_agent_plugin.gd, extending EditorPlugin.
3Editor and scene-tree mutation should run on the editor main loop.Network handling enqueues work; _process drains queued requests and calls tools.
4Godot's core concepts are Node, Scene, Resource, Signal, and NodePath.Commands are named scene, node, run, eval, and output; no Unity vocabulary.
5GDScript has native Expression support and the best editor integration.eval follows Godot's GDScript path instead of trying to compile another language.

3. Repository layout

hera-agent-godot/
├── addons/
│   └── hera_agent_godot/        # the distributable addon (ships to users)
│       ├── plugin.cfg
│       ├── hera_agent_plugin.gd
│       ├── LICENSE, README.md
│       ├── core/
│       ├── server/
│       └── tools/               # status, run, scene, node, signal, resource, …
├── project.godot               # dev host project (root, so it loads the addon)
├── scenes/                     # dev fixtures (run/save/screenshot target)
├── main.go
├── go.mod
├── cmd/                        # Go CLI commands
├── internal/                   # client / discovery / protocol
├── docs/
└── .gitattributes              # export-ignore keeps the AssetLib zip addon-only

The Godot dev project lives at the repo root (project.godot + addons/ + scenes/) so it loads the addon during development and so the Asset Library — which installs the repo archive preserving paths — drops addons/hera_agent_godot/ straight into a user's project. .gitattributes export-ignore strips the CLI, docs, CI, and dev project from that archive, leaving only the addon content. The Asset Library ZIP should include addons/hera_agent_godot/LICENSE; it does not need a duplicate LICENSE at the ZIP download root.


4. Request lifecycle

1. CLI: hera run --scene res://Main.tscn --wait
2. CLI parses args and builds Request{ tool:"run", params:{...} }
3. discovery scans ~/.hera-agent-godot/instances/ and picks a live editor
4. client posts JSON to http://127.0.0.1:<port>/rpc
5. addon server reads JSON and enqueues a work item
6. hera_agent_plugin.gd drains the queue in _process
7. ToolRegistry resolves the tool and runs it through EditorInterface / SceneTree
8. addon queues Response{ ok, data/error }; poll sends bounded partial chunks
9. CLI prints compact output

5. Component responsibilities

Go CLI

ComponentResponsibility
cmd/*Parse command flags, build requests, run local helper commands (instances, smoke), and format responses.
internal/discoveryScan ~/.hera-agent-godot/instances/ and return fresh editor instances, keeping expired heartbeat files separate so a stalled editor is not reported as missing.
internal/clientPOST one request to one editor instance with timeout and retry.
internal/protocolRequest / response JSON contract.

Godot addon

ComponentResponsibility
hera_agent_plugin.gd@tool EditorPlugin; owns server, queue, heartbeat, registry, and tiny built-in file/project helper tools.
server/http_server.gdLocal HTTP listener bound to 127.0.0.1, rejecting remote/browser-origin calls.
server/work_queue.gdMain-loop handoff for pending HTTP requests.
server/heartbeat.gdWrites ~/.hera-agent-godot/instances/<pid>.json.
core/tool_registry.gdExplicit tool name to handler mapping.
core/tool_response.gdCompact { ok, data/error } response helpers.
tools/*_tool.gdOne handler per capability: status, run, scene, node, signal, resource, eval, guidance, output, diagnostics, screenshot, batch, and game bridge.
runtime/game_inspector.gdRuntime autoload used by game tree, game ui tree, game ui audit, game instances, game screenshot, game click, game input, game clock, game node get, game node set, game node call, and game assert while a play session is running. It writes per-process heartbeats and request/response files so stale game processes cannot answer current requests; game --pid N selects one fresh process explicitly. Heartbeats include user_data_dir and live viewport sizes because Godot's user:// is per user-data directory, not per process. The autoload uses PROCESS_MODE_ALWAYS and wall-clock heartbeats so game clock --pause / Engine.time_scale do not freeze the inspector.
runtime/game_inspector_export_guard.gdTemporarily removes Hera's owned runtime autoload while Godot collects export dependencies and project settings, then restores it for editor play.
runtime/game_ui_auditor.gd / runtime/game_ui_audit_checks.gdBounded runtime UI traversal, verdict assembly, and generic Godot Control defect checks. Rules derive from live rectangles, clipping, input/focus behavior, minimum sizes, mouse filtering, and sibling geometry.
runtime/game_value_codec.gdRuntime value serialization and argument/property coercion shared by live game node get/set/call.
runtime/game_image_analyzer.gdGeneric runtime screenshot metrics for low-token visual QA (nonblank, dimensions, sampled color count, brightness, per-edge content ratios, asymmetric clipping, and low-detail hints).
runtime/game_assertions.gdGeneric runtime property assertion comparisons for game assert and scenario QA.
runtime/game_input_sequence.gdBounded action replay on physics_frame, buffered input flushing, and held-action cleanup. Clock steps use a selected-phase SceneTreeTimer to pause after node callbacks.

6. Discovery & instance files

  • Directory: ~/.hera-agent-godot/instances/.
  • One file per running editor: <pid>.json.
  • Schema:
{
  "pid": 12345,
  "port": 8770,
  "project_path": "/abs/path/to/project",
  "godot_version": "4.7.stable",
  "scene": "res://Main.tscn",
  "ts": 1750636800
}

The CLI treats an instance as live only if now - ts is within the freshness window. Expired files stay visible as stale on hera instances so a process that stopped publishing is distinct from a missing advertisement. They are not targeted unless a later heartbeat becomes fresh again.

The addon republishes the file by staging it under a temp name and swapping it in with DirAccess.rename_absolute. That swap is atomic on POSIX but not on Windows, where Godot's DirAccess::rename removes an existing destination before MoveFileW — so <pid>.json is briefly absent on every heartbeat. A CLI scan landing in that window would otherwise report "no live Godot editor found" while an editor is running, so discovery rescans with four growing delays when the first pass comes up empty.

HTTP receive and response-write phases each have a five-second deadline. Responses advance by at most 64 KiB per connection per poll; a slow reader cannot block the editor loop. Queued asynchronous work keeps its own tool deadline. Responses arriving after client cleanup are discarded.

Runtime requests are written to a unique temporary file, closed, then renamed to their final JSON path. The runtime parses a complete request before removing it for dispatch. Process targeting and response IDs remain mandatory; the CLI does not automatically retry mutations after a transport failure.

UI summaries, clicks, and viewport-boundary checks share transformed local Control geometry. Bounds are viewport-local axis-aligned rectangles; semantic clicks reject controls in another Viewport instead of injecting into the wrong viewport.


diagnostics and output observe the running project, not the editor. Both read debug/file_logging/log_path (user://logs/godot.log), and Godot never writes that file while running as the editor. From main/main.cpp:

(!log_file.is_empty() || (!project_manager && !editor && GLOBAL_GET("debug/file_logging/enable_file_logging")))

The !editor guard means the RotatedFileLogger is not installed at all in an editor session — so editor-console messages (plugin errors, push_warning, parse errors) never reach these tools no matter how enable_file_logging is set. Confirmed on 4.7: a headless editor with a deliberately broken autoload printed SCRIPT ERROR: Parse Error to the console while its user://logs directory was never even created, before or after a clean exit. Game and project runs do write there, which is what these tools actually cover.

Workaround. The first branch of that condition is an escape hatch: launch the editor with --log-file <path> and the guard is bypassed, so editor output is captured. That is the only way to make editor diagnostics observable through these commands.

7. Security boundaries

  • Listener binds only to 127.0.0.1.
  • Browser-origin requests are rejected.
  • Opt-in shared-token auth: when ~/.hera-agent-godot/token (or HERA_AGENT_GODOT_TOKEN) is set, /rpc requires a matching X-Hera-Token header (401 otherwise). See SECURITY.md for the full threat model.
  • Instance files live under the user's home directory and contain no secrets.
  • Dangerous operations are out of scope for v0 and must become explicit named tools if ever added.

8. Deliberate non-goals

  • No MCP server — a deliberate bet on a low-token, shell-native CLI (see §1), not an MCP gap: comparable editor reach at a fraction of the per-turn tokens.
  • No Godot 3.x support. 4.7 is the fully-QA'd baseline; 4.2 is the verified 4.x floor (see SUPPORT_MATRIX.md).
  • No C#/.NET addon requirement.
  • No reflection-based tool auto-discovery.

See ROADMAP.md for the phased plan, COMMANDS.md for the command surface as it lands, and GODOT_EDITOR_ANALYSIS.md for the source/API/debug analysis workflow used instead of Unity-style binary-first inspection.