kagent BYO Harness: Running Factory Droid as an A2A Agent

Part of: Agent Runtimes

What kagent's byo harness requires, and how kagent-droid runs Factory Droid behind it: a Go A2A front, one droid exec per turn, session continuity on /data, and read-only k8s-tools.

The Codex post covered a runtime kagent ships for you. byo is the opposite: kagent runs your image and treats whatever’s inside as opaque. To see what that takes for a real agent, I put Factory Droid on it. The result is kagent-droid , a Droid-as-BYO-agent for DevOps and platform work. This post is the contract plus what that repo had to do to meet it.

Disclosure: I work at Solo.io, which created kagent (now an open-source project) and sells Solo Enterprise for kagent. I built kagent-droid. I have no relationship with Factory. Sources: the public kagent BYO docs , harness docs , Factory’s Droid Exec docs and the kagent-droid README. Lab results are from a kind cluster running kagent-enterprise 1.0.0-alpha4 and Substrate 0.2.0-beta5 on 2026-10-16. kagent 1.0 is alpha.

What byo actually requires

The docs call it an A2A contract. The details are stricter than “speaks A2A”:

RequirementWhen it’s checkedFailure
spec.workload.command setOn applyBYO harnesses must specify workload.command
spec.workload.image pinned by sha256On applyRejected, a bare tag fails
gRPC lf.a2a.v1.A2AService on port 80First invokeConnect: tunnel failed
GET /readyz on port 8081Actor start (30s timeout)Actor never ready
gRPC, not JSON-RPCFirst invokeNever reached

The gotcha that will cost you an afternoon: /readyz passing says nothing about port 80. The agent shows READY, and the first call dies with a tunnel error that doesn’t name a port. Read runtime.port from the config kagent delivers, or bind 80 yourself.

Two other rules: config comes in through KAGENT_CONFIG_JSON, and cross-turn state goes under /data so it survives suspend and resume. You also lose structured output and UI file attachments on byo. kagent ships Go and Python helpers for the server side.

Harness shape (trimmed from the repo’s deploy/harness.yaml):

apiVersion: api.kagent.dev/v1alpha3
kind: Harness
metadata:
  name: droid-harness
  namespace: kagent
spec:
  byo: {}
  workload:
    image: <your-registry>/kagent-droid@sha256:<digest>
    command: ["/usr/local/bin/droid-harness"]
  substrate:
    workerPoolRef:
      name: kagent-default
    snapshotPolicy:
      location: s3://ate-snapshots/kagent/
---
apiVersion: api.kagent.dev/v1alpha3
kind: AgentTemplate
metadata:
  name: droid-template
  namespace: kagent
spec:
  description: Factory Droid as a DevOps agent (see deploy/ in kagent-droid for the full template).
---
apiVersion: api.kagent.dev/v1alpha3
kind: Agent
metadata:
  name: droid-agent
  namespace: kagent
spec:
  template:
    ref:
      name: droid-template
  harnessRef:
    name: droid-harness

Droid doesn’t speak A2A, so wrap it

Factory documents MCP for tools and a headless droid exec mode with its own streaming output, not A2A or gRPC. So kagent-droid puts a small Go front in the image. It serves lf.a2a.v1.A2AService on :80 and /readyz on :8081. Each A2A turn runs one droid exec -o stream-json. A kagent Session maps to one droid session ID, and later turns continue it with -s. The AgentTemplate supplies the system prompt, model and MCP tools, so the image doesn’t change per agent.

FileWhat
front/config.goReads KAGENT_CONFIG_JSON, writes ~/.factory/mcp.json, maps model IDs, wires the Factory credential
front/executor.goOne droid exec per turn. stream-json events become A2A status updates (function_call / function_response DataParts), an artifact and a final state
front/tasks.goAdopts the task IDs kagent creates before it sends the message
front/main.gogRPC + readyz servers, agent card
DockerfileStatic Go front, droid 0.242.0 (checksum verified), kubectl, helm, git/jq/rg on debian:trixie-slim

Why Go, and why droid runs per turn

This is the most useful lesson in the repo. Substrate checkpoints the Actor right after /readyz passes and again after every turn, then restores from that snapshot for the next request. On the lab cluster, a Python process didn’t survive the restore. readyz was refused, and the router’s 5s ResumeActor deadline cancelled every attempt. Probe images on the same base image isolated it:

ProbeRestore
Static Go readyz server2.0s, heartbeat resumes
Python asyncio readyz, no grpcioFails
Python blocking readyz on a threadFails
Python asyncio + GLIBC_TUNABLES=glibc.pthread.rseq=0Fails

So the Go front is the only process alive at checkpoint time. droid runs only during a turn, and its process group is killed when the turn ends. The Actor checkpoints in ~0.26s and restores in 0.4–0.6s. If you build a byo image, test restore early. “It passes readyz” isn’t enough.

Mapping

AgentTemplate / HarnessDroid
systemPrompt--append-system-prompt
modelConfig.model-m, mapped to Factory IDs (e.g. claude-haiku-4-5 → claude-haiku-4-5-20251001), or DROID_MODEL on the Harness
tools[].mcp (HTTP/SSE)~/.factory/mcp.json. requireApproval servers are left out (no HITL path yet)
Harness DROID_AUTONOMY--auto (default high, because the gVisor sandbox is the boundary)

Credentials and egress

Droid talks to two Factory hosts, and each one needs an exact-hostname entry for credential injection:

HostDeclared byCredential
api.factory.aiOpenAI-type ModelConfig with baseUrl: https://api.factory.aiFACTORY_API_KEY Secret
prem.factory.aiA RemoteMCPServer used only for egressAuthorization: Bearer ... Secret

The Actor never holds the real key. It sees the placeholder kagent-credential-injected in OPENAI_API_KEY, the front passes that to droid as FACTORY_API_KEY, and the gateway swaps in the real header on each declared host. Things that didn’t work: Secret-backed Harness env (rejected), wildcards like https://*.factory.ai (“credential injection requires an exact DNS hostname”), and an egress allowlist field (there isn’t one). telemetry.factory.ai stays blocked, and droid tolerates that.

Cluster access without a kubeconfig

The sandbox has no kubeconfig and can only reach declared hosts, so kubectl inside Droid can’t reach a cluster. Cluster access comes from kagent-tools 0.3.0 installed read-only instead: a get/list/watch ClusterRole without Secrets, plus --read-only, attached as the k8s-tools RemoteMCPServer. One footgun: the chart’s default metrics port collides with the MCP port, so the values file moves metrics to 8085.

State that must outlive a turn lives on /data: droid’s HOME (session store, mcp.json), /data/workspace, and the context-to-session map in /data/droid-sessions.json. Process memory does not carry over.

Status from the README

CheckResult
Local: two turns on one context, pre-assigned task IDsPass, turn 2 remembers turn 1
Golden snapshotPass, checkpoint 0.26s
Turn 1 through kagent, real Factory keyPass, droid created /data/workspace/notes.txt
Turn 2 on the same ActorPass, same droid session, read the file back
“List the namespaces you can see”Pass, 8 namespaces via k8s-tools, matching kubectl get ns

Open items

  • Human-in-the-loop for requireApproval MCP servers (A2A input-required).
  • A2A task history lives in the front’s in-memory store, not the kagent controller, so the kagent UI may not show past turns.
  • Write access. k8s-tools is read-only on purpose. Flip it only with a human approval step in the loop.

When byo is the right call

If the agent already exists and works, byo beats rewriting it as a declarative kagent agent. You keep Substrate sessions, digest-pinned revisions and gateway-held credentials. The trade-off is that the front is now code you own, and restore behavior is part of your contract. If you just want a strong coding loop on Kubernetes, the claude and codex harnesses ship with kagent. All four are compared in the harness hub . The code, deploy manifests and invoke scripts are in themsquared/kagent-droid .

Frequently asked questions

What does a kagent BYO agent have to implement?

A container image pinned by sha256 digest, with workload.command set on the Harness. It must serve the A2A service lf.a2a.v1.A2AService over gRPC on port 80 and answer GET /readyz on port 8081. An image that serves A2A over JSON-RPC only is never reached.

Can Factory Droid run as a kagent agent?

Yes, through the byo harness and a wrapper. Droid doesn’t speak A2A itself. The open-source kagent-droid project puts a small Go A2A gRPC front in front of it, runs one droid exec per turn, and maps each kagent Session to a droid session ID so later turns continue it. It passed two-turn continuity and k8s-tools checks on a kagent-enterprise 1.0.0-alpha4 lab cluster.

Why is the kagent-droid front written in Go instead of Python?

Agent Substrate checkpoints the Actor after /readyz passes and after every turn, then restores it for the next request. In kagent-droid’s tests, Python readyz servers did not survive the restore, while a static Go server restored in about 2 seconds. So the Go front is the only long-lived process, and droid runs only during a turn. The Actor checkpoints in about 0.26s and restores in 0.4 to 0.6s.

Why does my kagent BYO agent show READY but fail with 'Connect: tunnel failed'?

The readiness probe on port 8081 passes even when the A2A service on port 80 is wrong or missing. Bind gRPC A2A on the port in the runtime.port field of kagent’s config, or on port 80 if you ignore the config.

Disagree or have a war story? Reply on X