This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

OpenHands

openhands.alwaldend.com

OpenHands deploys as separate components because each owns a distinct responsibility and trust boundary:

ComponentResponsibility
Agent CanvasBrowser client for conversations, files, settings, backends, automations
Agent ServerRuns conversations, agents, tools, and workspace operations
Automation ServerStores schedules and triggers, tracks runs, dispatches conversations

The agent server runs directly on the host with the service account’s permissions. Its workspace is a working directory; operating-system account permissions define its access to the host. Treat the agent server host as trusted infrastructure.

  • tf_setup creates the Agent Canvas, secured agent server, and automation server VMs in the src_infra_openhands Xen Orchestra resource set.
  • ansible deploys the three VM components. The host-bot project owns and deploys its unsecured agent server.
  • dnsconfig.json owns the component hostnames and static addresses.
  • al.lua authenticates with the src_infra_openhands Vault AppRole, signs host keys for Ansible, and injects the component secrets.

There is no tf package: these components keep their state in files and SQLite on their own hosts, so no API-side resources exist to declare.

Canvas serves the packaged browser assets through its static-server service on loopback. Configure the browser’s backend with the canvas origin as its host and the agent server session key as its credential.

Each component host runs Traefik from the shared role with certificates issued by src_infra_openhands_pki_server. The canvas origin proxies /api/*, /api/automation, /sockets, and /server_info to the agent server and automation server, so the browser talks to one origin and issues no cross-origin request.

The public canvas.openhands.alwaldend.com record is a CNAME to ingress.alwaldend.com, and infra/ingress forwards it to dc1.canvas.openhands.alwaldend.com. That ingress keeps its RequireAndVerifyClientCert client-auth policy, so reaching canvas from outside still requires a client certificate.

agent_server holds session_api_key and secret_key; automation holds kv_secret. The session API key is shared by the agent server, the automation server, and the browser because all three authenticate the same X-Session-API-Key value.

The unsecured host-bot server has a separate secret_key reference owned by its host’s AL configuration.

Prepare one private JSON input for each path through the secret-handling workflow. Set OPENHANDS_AGENT_SERVER_JSON, OPENHANDS_AUTOMATION_JSON, and OPENHANDS_HOST_BOT_JSON to those input file paths. Each JSON object contains the fields listed above for its path. Initialize the paths through standard input; -cas=0 prevents overwriting existing data:

bazel_agent bazel run //infra/openhands:vault.kv_put -- -cas=0 \
  alwaldend.com/vault1/approles/src_infra_openhands/agent_server \
  - < "$OPENHANDS_AGENT_SERVER_JSON"
bazel_agent bazel run //infra/openhands:vault.kv_put -- -cas=0 \
  alwaldend.com/vault1/approles/src_infra_openhands/automation \
  - < "$OPENHANDS_AUTOMATION_JSON"
bazel_agent bazel run //users/simeonwarren:vault.kv_put -- -cas=0 \
  alwaldend.com/vault1/approles/user_simeonwarren/openhands \
  - < "$OPENHANDS_HOST_BOT_JSON"
bazel run //infra/openhands/tf_setup:tf_setup.plan
bazel run //infra/openhands/tf_setup:tf_setup.apply
bazel run //infra/openhands/ansible

The component AppRole must first exist in Vault. Follow the XO authentication and resource-set workflow to synchronize its OIDC identity and apply its resource-set assignment before tf_setup runs. The assignment in resource_set_inventory supplies the template, storage, and network by name. Component secrets must be written to Vault before Ansible runs. State and locking use the repository’s tf_backend plugin, which stores them as Vault KV entries and creates the entries on first use, so no separate state bucket is required.

1 - Ansible

OpenHands deployment

Deploys the OpenHands components. The inventory has three hosts in three groups, and the playbook applies one component role per group:

GroupHostComponent
openhands_canvashost1.canvas.openhands.alwaldend.comAgent Canvas
openhands_server_securehost1.server.openhands.alwaldend.comAgent server (secured)
openhands_automationhost1.automation.openhands.alwaldend.comAutomation server

The three XCP-ng VMs exist only for the secured agent server and for the components that must not share a trust boundary with host-bot. The secured group sets openhands_server_session_api_key and hands the same value to the automation server so the automation dispatcher can authenticate its dispatched conversations.

Every component applies host and traefik. Canvas runs the published package’s static server directly on loopback. The operator configures the browser backend with the canvas origin and the shared session key.

The canvas host routes /api/automation to the automation host and the agent server API prefixes to the secured agent server. These routes take precedence over the frontend route, so the configured browser reaches one origin. Downstream routers accept the original canvas Host header.

The unsecured agent server runs on host-bot and is deployed by the host-bot project, which owns that host entirely. It omits OH_SESSION_API_KEYS_0 and binds to loopback without a session API key. Access is local to host-bot; its Traefik and firewall configuration add no route for this server.

The group variables read the session key, secret key, and automation KV secret from the environment the AL Vault injector populates.

2 - OpenSpec

2.1 - Specifications

2.1.1 - Infra openhands

2.1.1.1 - OpenHands infrastructure

Describe the checked-in OpenHands component deployment, host boundaries, ingress routing, and Vault secret references. These requirements describe desired configuration and do not establish live deployment or service health.

The deployment SHALL run Agent Canvas, the agent server, and the automation server as distinct components, because each owns a different responsibility. The agent server SHALL run natively with its service account’s host permissions. The deployment documentation SHALL identify those permissions as the execution boundary and the workspace as its working directory.

  • WHEN a reader inspects the deployment
  • THEN the canvas serves the browser client, the agent server runs conversations and workspace operations, and the automation server stores schedules and dispatches conversations
  • AND the documentation states that the agent server acts with its service account’s permissions on its host

infra/vault/tf SHALL declare the src_infra_openhands AppRole through a module under infra/vault/tf/approle_src_infra_openhands/, including its entity, group, SSH host-key signing role for openhands.alwaldend.com, and server PKI role for the component hostnames. The AppRole SHALL receive only the secrets, SSH, and certificate policies it needs, and the component SHALL authenticate with it rather than a shared or administrative identity.

  • WHEN the Vault configuration is evaluated
  • THEN the AppRole, SSH role, and server PKI role are declared in the AppRole module and referenced by the shared identity groups
  • AND the component’s al.lua selects that AppRole by name

tf_setup SHALL create the canvas, secured agent server, and automation server VMs in the src_infra_openhands Xen Orchestra resource set. Each VM SHALL derive its hostname, address, and MAC from one record in dnsconfig.json. Ansible SHALL deploy the components natively, without containers.

  • WHEN a new component is added
  • THEN one record in dnsconfig.json owns its name and static address
  • AND the provisioning vms map references that record prefix

Component roles SHALL install and run their services on loopback and SHALL NOT declare their own ingress. The deployment SHALL expose them through the shared Traefik role with per-component dynamic configuration.

  • WHEN a component host is deployed
  • THEN the shared traefik role writes its dynamic configuration
  • AND the component role itself declares no router, entry point, or certificate resolver
  • WHEN the canvas service starts
  • THEN the package’s static server serves the packaged browser assets on the declared loopback address
  • AND it starts no agent server, automation server, or additional proxy

The deployment SHALL document configuring the browser backend with the canvas origin and shared session key. The canvas origin SHALL proxy the backend API paths to the agent server and automation server ahead of the frontend route. Backend targets SHALL use site-local hostnames, and downstream routers SHALL accept the forwarded canvas Host header.

  • WHEN the browser uses the canvas origin as its configured backend and requests an automation or agent server API path
  • THEN Traefik forwards it to the matching backend service
  • AND the browser issues no cross-origin request
  • AND the inter-service hop selects the downstream router without traversing the public ingress

The component SHALL declare the public canvas.openhands.alwaldend.com CNAME and the site-local canvas backend address consumed by its ingress service entry. Canvas integration SHALL satisfy the existing ingress routing and client-authentication requirements.

  • WHEN the OpenHands DNS and ingress service declarations are inspected
  • THEN the public canvas CNAME selects the shared ingress and the service entry selects the component’s site-local backend address
  • AND the integration uses the ingress owner’s existing TLS policy

Secret values SHALL be stored in Vault and referenced only by path, environment variable name, or plugin label. The component al.lua SHALL inject them through the repository’s injector plugin, and tasks that could reveal them SHALL use no_log. The session API key SHALL be shared by the agent server, the automation server, and the browser because all three authenticate the same X-Session-API-Key value.

  • WHEN the deployment reads the session key, secret key, or automation KV secret
  • THEN it receives them from the injected environment rather than committed files
  • AND the environment file that receives them is written with no_log

The unsecured agent server SHALL be owned and deployed by the host project that owns its host, not by the OpenHands VM package. It SHALL bind to loopback, receive no session API key, and add no ingress or firewall exposure of its own.

  • WHEN a reader inspects the unsecured agent server deployment
  • THEN the host project’s inventory, playbook, group variables, and secret injection declare it
  • AND the OpenHands Ansible inventory contains only the three VM components

2.1.2 - Owned dns

2.1.2.1 - owned-dns Specification

Define OpenHands DNS management through the owner’s tf_setup root, including canonical declarations, scoped credentials, and offline source checks before adopting live records.

The tf_setup root SHALL consume this owner’s canonical dnsconfig.json through the shared DNS Terraform module, preserving declared record identities and views. DNS resources SHALL default to disabled before adoption and SHALL default to enabled in the reviewed adoption revision. Adoption SHALL bind existing records to their exact provider identities and SHALL preserve their declared attributes. Existing-record adoption SHALL contain no DNS record additions, changes, replacements, or deletions. Verified missing declarations MAY be provisioned through an exact additions-only DNS plan that preserves every existing record and excludes non-DNS managed-resource changes. After deployment, default DNS reconciliation SHALL retain the bindings and preserve unrelated provider records.

  • WHEN the checked-in root is evaluated with default inputs before adoption
  • THEN it reads this owner’s declaration and disables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the reviewed adoption revision is evaluated with default inputs after adoption
  • THEN it reads this owner’s declaration and enables managed DNS records
  • AND the package contains the module and declaration inputs
  • WHEN the owner’s existing records are adopted through its scoped DNS wrappers
  • THEN Terraform binds the records to their exact provider identities
  • AND their declared values, attributes, and views remain unchanged
  • AND the reviewed adoption plan contains no DNS additions, changes, replacements, or deletions
  • WHEN the adopted root plans with default inputs and unchanged declarations and provider records
  • THEN the plan retains the adopted bindings and contains no DNS additions, changes, replacements, or deletions
  • AND unrelated provider records remain unchanged
  • WHEN fresh complete provider inventories prove the owner’s declared records are absent and conflict-free
  • THEN the reviewed DNS plan creates only those declared missing records
  • AND existing provider identities and attributes remain unchanged
  • AND a subsequent DNS plan contains no changes

The dns.plan, dns.show, and dns.apply wrappers SHALL select dns=1 through the repository AL flow using src_infra_openhands, and operate on module.dns and its dependencies in the existing tf_setup root and Vault HTTP backend. Apply SHALL consume the reviewed saved plan. Ordinary VM setup SHALL retain its tf=setup and xoa_login=1 labels, XO authentication, and existing backend paths. Secret values SHALL remain in injected variables. Real DNS credentials and Vault policy grants SHALL be prerequisites for operational DNS calls. RouterOS DNS credentials SHALL remain isolated from unrelated RouterOS resources. Successful scoped DNS execution SHALL not establish VM or service health. The package SHALL expose a format test that does not authenticate to Vault or contact DNS providers.

  • WHEN the package format test executes
  • THEN it checks the packaged Terraform configuration without live credentials
  • AND existing non-DNS authentication and backend paths remain unchanged
  • WHEN the scoped DNS wrapper selects dns=1
  • THEN it selects DNS credential injection through the owner AppRole and existing backend without starting XO login
  • AND the targeted plan and saved-plan apply cover DNS and its dependencies, without establishing VM or service health
  • AND disabled DNS resources do not imply offline provider configuration

3 - Tf setup

Terraform setup

This package creates Xen Orchestra VMs in the src_infra_openhands resource set for the three OpenHands components that run off host-bot: Agent Canvas, the agent server, and the automation server. infra/openhands/al.lua authenticates with OpenHands’ own Vault AppRole and the packaged XO OIDC login plugin. The plugin supplies a temporary XO token and revokes it on shutdown; no infrastructure administrator token is loaded. The setup HTTP backend remains owned by the same OpenHands config.

Before running VM setup, bootstrap the AppRole’s XO OIDC user and apply its resource-set membership through infra/xcp_ng/tf. Subjects are matched by immutable Vault entity UUID under the configured OIDC issuer, not by login name or AppRole group membership. See XO authentication.

vms holds the component definitions, and each entry names the dnsconfig record prefix that owns its hostname and address. Adding a component means adding a record to ../dnsconfig.json and an entry here; the guest hostname, static address, and derived MAC all come from that one record. The agent server is sized larger because it stores every backend conversation, workspace, and bash event.

Defaults select the imported Fedora 44 template, local storage and wired network by name from the OpenHands resource set. Override TF_VAR_xoa_template_name, TF_VAR_xoa_storage_name, and TF_VAR_xoa_network_name for another assignment. Native provider lookups run as OpenHands’ own identity and reject ambiguous matches. The selected network must carry 192.168.10.0/24; gateway and DNS default to 192.168.10.1 (the wired router) and can be overridden.

The PVE-based infra/cloud_init:xen_linux target supplies the shared Ansible user and CA configuration through the repository template rule. Terraform adds the hostname and static network configuration from ../dnsconfig.json. After cloud-init completes, verify that /dev/xvda is the boot disk before running Ansible.

bazel_agent bazel run //infra/openhands/tf_setup:tf_setup.plan
bazel_agent bazel run //infra/openhands/tf_setup:tf_setup.apply

Use this package’s dns.plan, dns.show, and dns.apply targets for the scoped DNS adoption workflow. They select dns=1 and target module.dns in the same root, AppRole, and backend without starting XO login. Apply requires the reviewed saved plan; the shared procedure owns preparation and reconciliation.

dns_enabled defaults to true after verified initial provisioning. Keep it enabled to retain the managed records. Unchanged declarations and provider records produce a no-change DNS plan. Successful DNS checks do not establish VM, OpenHands service, or TLS readiness; the separate service deployment change retains that acceptance work.