Skip to main content

Agent hosts

An AgentHost runs one long-lived agent host for one user: a daemon that serves the Agent Host Protocol (AHP) to clients such as the GitHub Copilot desktop app. Sessions, tools, approvals and worktrees stay inside the host; the client is a UI. Orka admits the host, runs it from an operator's AgentHostTemplate, restarts it, suspends and wakes it, and cleans it up. The design is ADR 0035.

Each host is one Pod, a PersistentVolumeClaim for its state, a NetworkPolicy and Secrets in a dedicated host namespace. Orka does not see the host's turns; it governs who may run a host, what it runs, what it may reach and the policy pinned in its template.

One trusted user per host
  • The sample host, copilotd, serves exactly one GitHub user per process.
  • The host's agent holds the user's GitHub token, and every process in the Pod runs as one uid, so code the agent runs can read it. Egress to public TCP 443 is open. Run hosts only for code the user trusts; "public repositories only" is not a sandbox boundary.

Turn it on​

With Helm, set:

controller:
image:
digest: sha256:<controller image digest>
agentHosts:
enabled: true
namespace: orka-agent-hosts

The chart creates the host namespace with the restricted Pod Security profile, the controller's Role in it, the controller flags and the AgentHost admission webhook. The orka-host-agent sidecar comes from the controller image, so pin it by digest, or set controller.agentHosts.agentImage to another digest-pinned image that carries it. Agent hosts need controller.mode: harness-v2, and the host namespace must differ from the release namespace and controller.acpRuntime.namespace.

Before you disable agent hosts, uninstall, or roll back to a release without them, delete every AgentHost and wait until it is gone. Without agent hosts the controller stops reconciling them, so their Pods keep running and their deletion never finishes; an upgrade that disables agent hosts is refused while AgentHosts remain. Helm keeps the host namespace, with the hosts' volumes and the controller's Role in it, so a controller with agent hosts on can still finish cleaning up, including hosts left in an earlier namespace after controller.agentHosts.namespace changes. Delete the namespace yourself once no AgentHost needs it.

With the Kustomize install:

  1. Create the host namespace and the controller's Role in it. The Kustomize install binds the controller's manager role per namespace:

    kubectl apply -k config/agent-hosts
  2. Start the controller with:

    --enable-agent-hosts
    --agent-host-namespace=orka-agent-hosts
    --agent-host-agent-image=<controller image>@sha256:<digest>

    The agent image is normally the controller image itself; it carries the orka-host-agent sidecar. --agent-host-controller-pod-labels selects the controller Pods allowed to read host status through each host's NetworkPolicy; its default, orka.ai/network-role=controller, matches the shipped controller Pods.

With either install, create an AgentHostTemplate (see config/samples/core_v1alpha1_agenthosttemplate.yaml) and grant use on it to the people who may run hosts. Write the Role as a manifest: kubectl create role refuses use because the API does not list that verb.

AgentHosts, their templates and their token Secrets live in the namespace Orka watches (--watch-namespace), because the controller reconciles AgentHosts only there. Everyone's hosts therefore share that namespace, and whoever may update an AgentHost controls it: they can suspend or delete it, and deleting it deletes its volume. They can also point it at a token Secret they can read, which restarts it on its volume as them. So give everyone one shared Role to create hosts, and each person a Role that names only their own host and token Secret:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: agent-host-creator
rules:
- apiGroups: ["core.orka.ai"]
resources: ["agenthosttemplates"]
resourceNames: ["copilotd"]
verbs: ["use"]
- apiGroups: ["core.orka.ai"]
resources: ["agenthosts"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: agent-host-alice
rules:
- apiGroups: ["core.orka.ai"]
resources: ["agenthosts"]
resourceNames: ["alice"]
verbs: ["get", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["alice-github-token"]
verbs: ["get", "update", "patch"]

create cannot be limited by name, so agree on host names before hosts are created, and let an operator create each person's token Secret, or grant create on Secrets only where that is acceptable in the watch namespace. The shared Role grants no list or watch, which would show everyone's hosts; kubectl get agenthost alice and kubectl apply need only get on the named host.

Creating an AgentHost requires use on its template, and naming a token Secret requires get on that Secret, both checked against the caller by an admission policy.

Run a host​

The sample template runs copilotd in relay mode: it registers the host as an environment on the user's GitHub account and accepts clients through GitHub's relay. The cluster needs no inbound path.

In the namespace Orka watches:

kubectl create secret generic alice-github-token --from-literal=token=<GitHub token>
kubectl apply -f config/samples/core_v1alpha1_agenthost.yaml
kubectl get agenthost alice

When the host is Ready, the environment orka-alice appears in the client's Environments list. The token's owner is the only GitHub user the host serves. Orka copies the token into the host namespace and keeps the copy current, so rotating the Secret reaches the running host without a restart. Pointing the host at another Secret or key replaces its Pod on the same volume, because the daemon reads its identity from the token when it starts.

Standalone access​

A template with access: Standalone skips the relay. access changes only what Orka publishes, not the daemon's arguments, so the template must also start the daemon in its standalone mode: for copilotd, --standalone instead of --relay and the Mission Control token options. The client port-forwards to the host's loopback listener and presents the host's connection token, which Orka writes to the Secret named in status.connectionSecretRef:

ns=$(kubectl get secret alice-connection -o jsonpath='{.data.namespace}' | base64 -d)
pod=$(kubectl get secret alice-connection -o jsonpath='{.data.pod}' | base64 -d)
port=$(kubectl get secret alice-connection -o jsonpath='{.data.port}' | base64 -d)
kubectl -n "$ns" port-forward "pod/$pod" "8788:$port"

Add the host as ws://127.0.0.1:8788/?tkn=<token>.

The per-user Role above does not cover these commands. Add the connection Secret, here alice-connection, to its Secret rule, and grant get on the host's Pod and create on its pods/portforward in the host namespace. The Pod keeps its name for the host's life, so grant them once the host exists, using the pod value above:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: agent-host-alice-standalone
namespace: orka-agent-hosts
rules:
- apiGroups: [""]
resources: ["pods"]
resourceNames: ["<pod>"]
verbs: ["get"]
- apiGroups: [""]
resources: ["pods/portforward"]
resourceNames: ["<pod>"]
verbs: ["create"]

Bind it to the same person, in the host namespace.

Hosts from GitHub Connections​

With connectors, people get a host by linking GitHub: no Secret, no YAML and no Kubernetes access. Create a GitHub App with expiring user tokens and the callback URL <callbackBaseUrl>/api/v1/connections/callback, then install with:

controller:
connectors:
enabled: true
callbackBaseUrl: https://orka.example.com
agentHosts:
enabled: true
github:
enabled: true
clientID: Iv23...
clientSecret:
existingSecret: github-app # or value: <the client secret>

The chart renders the github ConnectorProvider and the copilotd template it names; controller.agentHosts.template sets the template's images, storage and resources. The template's spec is immutable: to change it on an existing install, delete the AgentHostTemplate, then upgrade, and hosts move to the new one on their volumes. Without the chart, add spec.agentHost.templateRef to a GitHub provider yourself.

People's Connections outlive an uninstall, and a provider is deleted only once no Connection references it. So Helm keeps the provider, the template and a client Secret the chart created when you uninstall or turn github off, and reinstalling the same release adopts them. To remove them, do it while the controller runs, since it finishes their deletion: delete the Connections that reference the provider, wait until they are gone, then delete the three. After an uninstall, reinstall first.

To reach Orka with no domain, set ingress.enabled: true, ingress.className: tailscale and ingress.tls: [{hosts: [orka]}] with the Tailscale Kubernetes operator installed: the API and dashboard are then served at https://orka.<tailnet>.ts.net on your tailnet.

Each person then runs orka connect github, or links GitHub on the dashboard's Connectors page. orka connect waits for the host and prints the environment to pick in the Copilot app. Afterwards:

orka host status # the host, its phase, and why it is not ready
orka host wake # start it after it idled out
orka host delete --yes # delete it and its volume; while linked, a fresh one follows
  • The provider needs no tools. A dedicated GitHub App with only the permissions the host needs is recommended: a host's token carries all of the app's permissions, and a tool call's refresh would replace the host's token.
  • When a person links the provider, Orka creates an AgentHost named gh-<login> in that namespace, which appears as the environment orka-gh-<login>. The login is lowercased, and a managed user's underscore becomes --: alice_acme gets gh-alice--acme.
  • Orka writes the link's access token into the host and refreshes it 15 minutes before it expires. copilotd picks up the replaced file without a restart. The refresh token never leaves Orka. A token that never expires is refused with GitHubTokenNeverExpires, since code in the host could keep using a copy of it.
  • The host belongs to the first GitHub account it was given. A later token for another account fails it with GitHubIdentityChanged and keeps the volume.
  • Disconnecting stops the host, deletes its token copy and keeps the AgentHost and its volume. Linking the same account again restarts the host on its volume.
  • Settled failures such as a disconnect, a decline, a refused provider, a grant that no longer covers the link, GitHubIdentityChanged, or GitHubTokenNeverExpires stop the host. While a controller catches up with an edit, or a read fails, the host keeps running on the token it has and gets no new one.
  • A suspended host keeps no token copy. Waking it writes a fresh one, after checking the account.
  • Only the controller may create a host fed by a Connection, and only from the template its provider names. A host keeps that template when the provider later names another one. A link annotated orka.ai/agent-host: declined gets no host, and declining stops a host it already has, keeping its volume.
  • The provider must be on github.com: Orka reads the token's account from api.github.com.
  • The template must use access: Relay. A Standalone host's connection Secret sits in the namespace every person's host shares, so these hosts fail with InvalidTemplate instead.
  • Deleting the AgentHost deletes its volume. While the link stays, Orka creates a fresh host for it.
  • At most one provider per namespace may set agentHost. If several do, none provisions. The same happens when the name gh-<login> is taken by another AgentHost. Each case is reported as an event.

Linking such a provider again is required after agentHost is added or changed: it is part of the consent the person gave.

The host contract​

Orka adds to the template's container named host, and promises nothing else:

VariableMeaning
ORKA_HOST_IDThe AgentHost UID, stable for the host's life.
ORKA_HOST_NAMEThe AgentHost name, for display.
ORKA_CONNECTION_TOKEN_FILEA per-host random token in a memory-backed volume mounted only into host. Read it once and delete it.
ORKA_GITHUB_TOKEN_FILEThe user's GitHub token, read-only, when the template's githubToken allows it.
ORKA_STATUS_DIRA directory shared with the Orka host agent for the idle status file.
ORKA_HOST_AGENTA read-only copy of the Orka host agent, for use as the container's init process.

The host must serve AHP on 127.0.0.1:<listenPort>, gated by the connection token, and keep everything that must survive a restart under /workspace. The container's entrypoint is its PID 1, which adopts every orphaned process, such as git's detached maintenance. Agent daemons do not reap processes they never started, so start the daemon with exec "$ORKA_HOST_AGENT" init -- <command>: it reaps them, forwards signals to the daemon, and exits with its status. When the template sets idleStatusFile, the host writes JSON with at least idle and idleSince there, and rewrites it at least every five minutes. Orka judges freshness by its updatedAt, or by the file's modification time when it has none, and treats the status as stale after ten minutes, or when that time or idleSince is more than ten minutes ahead. copilotd's --idle-status-file format fits.

The orka-host-agent sidecar answers the host's probes. The Pod is Ready only after an AHP initialize and ping succeed against the loopback listener; the check never sends authenticate, so it cannot claim a single-user host. Afterwards it checks that the host keeps its idle status file fresh, or, for hosts without one, that the listener accepts connections. It never keeps the host from being idle.

Restarts, suspension and wake​

  • A host Pod that terminates, or whose listener stays down, is replaced with a new one on the same volume. A turn that was running is lost; sessions resume from the volume.

  • spec.suspended: true stops the Pod and keeps the volume and Secrets. Setting it back to false starts the host again.

  • A template's spec cannot change. To change a template, delete it and create it again under the same name: each host on it gets a new Pod from the new template, on its existing volume. A running turn is lost, and storage changes apply only to new hosts.

  • When the host agent image changes, such as after an upgrade of the controller, each running host gets a new Pod with it on its volume once the host reports idle, and at the latest 24 hours later. Until then the host has the condition HostAgentUpdatePending. A host replaced at the deadline loses a running turn.

  • A template's idleSuspend.after suspends hosts that report idle for that long. A client cannot wake a suspended relay host; set spec.wakeRequestedAt to the current time to start it:

    kubectl patch agenthost alice --type merge \
    -p "{\"spec\":{\"wakeRequestedAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}"

Hosts stay on unless a template enables idle suspension.

copilotd counts a session as busy while its runtime refuses to list dynamic-workflow runs, so a host with such a session never reports idle. Its --idle-ignore-unobservable-factories flag lets those sessions reach idle; set it only where dynamic workflows are not in use, because a pending or running workflow can be lost to suspension.

Troubleshooting​

Ready reasonMeaning
ProvisioningThe Pod is starting. The message names a container waiting on an image or volume.
RestartingThe host's Pod failed; Orka replaces it after a backoff that grows from 10 seconds to 5 minutes.
TemplateNotFoundThe template does not exist in the AgentHost's namespace.
InvalidTemplateThe template breaks a rule in the message.
GitHubTokenRequired / GitHubTokenUnavailableThe template needs a token and the referenced Secret or key is missing.
ObjectCollisionAn object with a host's name exists in the host namespace but was not created for it. Orka never adopts or deletes it.
ObjectRejectedThe API server refused one of the host's objects, for example a Pod naming an unknown RuntimeClass, huge pages whose request and limit differ, or a Pod a quota or admission policy denies. The message carries the server's reason; Orka retries on its next resync.
StateVolumeLostThe host's volume was deleted. Delete and recreate the AgentHost.
HostAgentUnavailableThe controller has no --agent-host-agent-image.
HostNamespaceChangedThe controller's --agent-host-namespace changed after the host was created. Delete and recreate it.
ConnectionUnavailableThe host's Connection is gone, unlinked or needs a new consent: linking again restarts the host. Or its provider no longer serves agent hosts: an operator restores the provider's agentHost, then the person links again.
GitHubIdentityChangedThe Connection now links a different GitHub account than the one the host's volume belongs to. Link the original account, or delete the AgentHost.
GitHubTokenNeverExpiresThe host's Connection holds a GitHub token that never expires, which code in the host could read and keep using. Turn on user-token expiration in the GitHub App, then link again.

A host that fails for a missing template or token has its Pod stopped and keeps its volume. When the token's Secret or Connection is gone, Orka also deletes its copy.

Deleting an AgentHost removes every object Orka created for it, and only those.

The Copilot app cannot connect​

If a host is Ready but the Copilot app reports Mission Control connect failed (HTTP 403), GitHub refused the connection before it reached the host. GitHub refuses every connection to a host while one of its sessions is in a repository you cannot push to, such as an archived one, so sessions in other repositories are blocked too. Restarting the host does not help: it registers again as the same environment.

Restore push access to that repository, for example by unarchiving it, then connect and delete the session in the app. You can archive the repository again afterwards. If push access cannot come back, orka host delete --yes replaces the host with one that registers as a new environment, without the old host's sessions and clones.