Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions examples/dsh-byo/agent.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
apiVersion: api.kagent.dev/v1alpha3
kind: ModelConfig
metadata:
name: dsh-model
namespace: kagent
spec:
provider: OpenAI
model: ${DSH_MODEL}
apiKeySecret: deepseek-api-key
apiKeySecretKey: OPENAI_API_KEY
openAI:
baseUrl: https://api.deepseek.com/v1
---
apiVersion: api.kagent.dev/v1alpha3
kind: Harness
metadata:
name: dsh-byo
namespace: kagent
spec:
byo: {}
workload:
image: ${DSH_BYO_IMAGE}
command: [/usr/local/bin/kagent-dsh-byo]
substrate:
workerPoolRef:
name: kagent-default
snapshotPolicy:
location: ${SNAPSHOT_LOCATION}
---
apiVersion: api.kagent.dev/v1alpha3
kind: Agent
metadata:
name: dsh-agent
namespace: kagent
spec:
harnessRef:
name: dsh-byo
template:
inline:
description: Coding agent running DeepSeek Harness through BYO.
modelConfig:
name: dsh-model
systemPrompt: |
Work in the configured workspace. Explain the changes and report the checks you ran.
1 change: 1 addition & 0 deletions go/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
bin/
.cache/
**/.cache/
harness/byo/dsh/node_modules/

# Test files
*_test.go
Expand Down
2 changes: 2 additions & 0 deletions go/harness/byo/dsh/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules/
!package-lock.json
37 changes: 37 additions & 0 deletions go/harness/byo/dsh/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Build from the go/ directory: docker build -f harness/byo/dsh/Dockerfile .
FROM --platform=$BUILDPLATFORM golang:1.27.0-bookworm@sha256:ded31c68586d2e49e760acc2e65a884b23d032e9bbbed0ae0c55abd3fcaf4452 AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY api/ api/
COPY adk/ adk/
COPY core/ core/
COPY harness/ harness/
COPY pkg/ pkg/
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \
go build -trimpath -o /kagent-dsh-byo ./harness/byo/dsh/cmd

FROM node:24-bookworm-slim@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
RUN apt-get update && apt-get install -y --no-install-recommends \
bash bubblewrap ca-certificates git ripgrep \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/dsh
# Commit the npm lockfile with this example; npm ci checks package integrity.
COPY harness/byo/dsh/package.json harness/byo/dsh/package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund \
&& ln -s /opt/dsh/node_modules/.bin/dsh /usr/local/bin/dsh \
&& dsh --version \
&& groupadd --gid 65532 kagent \
&& useradd --uid 65532 --gid 65532 --no-create-home --home-dir /data kagent \
&& mkdir -p /data/adapter /data/dsh/sessions /data/dsh/storages /data/workspace \
&& chown -R 65532:65532 /data
COPY --from=builder /kagent-dsh-byo /usr/local/bin/kagent-dsh-byo
USER 65532:65532
WORKDIR /data/workspace
EXPOSE 80 8081
LABEL org.opencontainers.image.source="https://github.com/kagent-dev/kagent"
LABEL org.opencontainers.image.description="DeepSeek Harness reference integration for kagent BYO"
ENTRYPOINT ["/usr/local/bin/kagent-dsh-byo"]
220 changes: 220 additions & 0 deletions go/harness/byo/dsh/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# DeepSeek Harness BYO reference integration

This image runs DeepSeek Harness (DSH) `0.2.0-rc.2` behind kagent's existing
`spec.byo` contract. It adds no CRD fields or native runtime type. The bridge
reads `KAGENT_CONFIG_JSON` and `KAGENT_AGENT_CARD_JSON`, uses the shared A2A
executor, and serves private gRPC on port 80 and readiness on port 8081.

## Supported configuration

- OpenAI-compatible Chat Completions ModelConfig, including a custom base URL
and headers. Provider credentials remain gateway-injected Secret references.
- The compiled system prompt and one root conversation per Actor.
- Whole Streamable HTTP MCP servers with compiled headers, HTTP timeouts and
session termination policy, enforced by a loopback Go MCP proxy.
- Text replies and text tool results, including tool errors.
- ACP cancellation and session resumption after the DSH process exits.
- DSH one-shot permission requests mapped to kagent's approval messages.

ACP emits committed semantic updates, rather than raw provider token deltas.
Context occupancy is not per-call token usage, so the bridge does not report
model token totals from ACP usage updates. DSH's own telemetry exporter and
auxiliary web tools are disabled; the kagent wrapper retains its telemetry path.

Unsupported configuration is rejected at startup: Responses API, model
generation overrides, API-key passthrough, custom TLS, SSE/stdio MCP bindings,
tool subsets, SSE read-timeout overrides, forwarded caller headers, and kagent `requireApproval` bindings.
DSH's ACP server has no per-MCP-server approval policy, so accepting that field
would not enforce the requested gate. DSH sandbox permission requests are a
separate channel and can still be answered through kagent.

The proxy discovers the remote tool catalog at each turn. It preserves the
CRD's default 30-second timeout and `terminateOnClose: true`; an explicit false
closes the local client without terminating the remote session. It does not
forward mid-turn catalog notifications, MCP resources, prompts or elicitation.

kagent skills, Agent Plugins, subagent bindings, memory, structured output and
ADK-specific context settings are not translated. DSH keeps its own built-in
tools and plugin composition; those are not equivalent to kagent Agent Plugins.
The first version accepts text input and output only.

Non-object tool input is retained under `args.rawInput` in tool-call events.
DSH validates the original input and returns tool errors to the model, allowing
recovery from arrays, scalars, null and malformed argument strings.

## State and process lifetime

Each turn starts a DSH ACP process and creates or resumes the Actor's private
DSH session. A pending permission request retains that process until the
decision arrives. Completion closes the session and stdin so DSH can flush its
state. Two nested Linux child subreapers own each process tree, including
orphaned tools and tools that create a separate session or process group.

Cancellation interrupts blocked ACP writes by closing stdin. After five seconds
of graceful shutdown, the adapter closes a private lifetime pipe to request forced
cleanup. Adapter death also closes that pipe. If the inner supervisor dies, the
outer guard adopts and reaps its descendants. If the guard dies, its lifetime
pipe closes and the inner supervisor kills DSH and reaps its descendants.
The adapter waits for an explicit cleanup confirmation before releasing the turn;
process exit or closed output pipes alone are insufficient.

During payload execution, this tolerates the loss of either supervisor. If neither
can confirm cleanup, for example after losing both, the adapter logs the failure
and leaves the task unsettled. Replacing the Actor restores process containment;
it does not reconcile the controller's outstanding task. Recovery also needs
controller-owned fencing and task reconciliation, tracked in
[kagent #3010](https://github.com/kagent-dev/kagent/issues/3010).
As a temporary DSH-only mitigation, cancellation with no live execution owner
returns an explicit `TASK_NOT_CANCELABLE` error instead of waiting indefinitely.
It leaves the stored task unchanged; it does not certify cleanup or unblock the
session's lifecycle operations.
[kagent #3011](https://github.com/kagent-dev/kagent/pull/3011) addresses the shared
executor's empty cancel response. Its missing-owner cancellation does not confirm
DSH descendant cleanup, so merging it alone is not sufficient to remove this guard.
Uninterruptible kernel I/O can also delay cleanup. This is
not a guarantee against simultaneous process kills or malicious same-UID tools.

Process supervision requires Linux with readable procfs child lists and permission
to set `PR_SET_CHILD_SUBREAPER`. It runs as the image's unprivileged user without
additional Linux capabilities. Unsupported hosts fail validation; run runtime
tests in Linux rather than launching this adapter natively on macOS. This manages
process lifetime; sandbox and container isolation remain platform responsibilities.

MCP HTTP redirects must retain the configured scheme, hostname and effective
port. Same-origin path redirects are supported, with a ten-redirect limit per
request. Cross-origin redirects, including HTTPS-to-HTTP redirects, are rejected
before sending a request or injecting binding headers.

`/data/adapter/state.json` binds the Actor to one native session. DSH session
logs and storage live under `/data/dsh`, and the workspace is `/data/workspace`.
The CLI profile and compiled overlay use a disposable home outside `/data`.
Do not delete the native session logs while retaining the continuation binding.
Public tasks and history remain in kagent's controller-backed TaskStore.

Pending approvals also need the live ACP process. They cannot currently recover
from a DATA-only snapshot or process replacement. The shared durable-input work
in [kagent #3165](https://github.com/kagent-dev/kagent/pull/3165) is a migration
dependency, but DSH must also support resuming the exact pending call without
replaying a tool before this adapter can adopt that recovery path.
In DSH 0.2.0-rc.2, native session resume closes the interrupted call with an
unknown-outcome tool result. The approval audit log does not reconstruct the
execution point or the ACP permission response. Saving its request ID and
reissuing the original prompt would therefore not implement approval recovery.
Until then, the DSH adapter accepts a send referencing an existing task only
when that task owns the live pending approval. It rejects both approval decisions
and plain replies to a lost task before the SDK appends history or changes task
state. Continue completed work with a new A2A task in the same conversation.
These guards do not replay prompts or apply approvals after ownership is lost;
they provide explicit failure, not automatic approval recovery.

If only the DSH subprocess exits while awaiting approval, the surviving adapter
still owns the pending task and its cleanup confirmation. A later approval or
rejection ends that task as failed; cancellation ends it as canceled. Once the
terminal state is published, send a new message without the old task ID in the
same conversation. The adapter resumes the native session for the new task.
This does not apply the lost approval or replay the interrupted tool. It requires
a subsequent request; there is no background task-recovery worker. Losing the
adapter or the cleanup confirmation still requires the recovery work above.

The local tests verify recovery after a DSH process restart. Cluster acceptance
also covers conversation and workspace continuity across automatic suspension
and explicit suspend/resume; Actor replacement and fork remain unverified.

## Build and deploy

From the repository root:

```sh
docker build -f go/harness/byo/dsh/Dockerfile -t YOUR_REGISTRY/kagent-dsh-byo:dev go
docker push YOUR_REGISTRY/kagent-dsh-byo:dev
```

Record the pushed image's manifest digest. The example requires the full
`YOUR_REGISTRY/kagent-dsh-byo@sha256:...` reference, not its tag.

Use a clean kagent installation whose Agent CRD supports `spec.template.inline`.
This example targets the API at source commit `b42a6c53`; `1.0.0-alpha9` uses
the earlier `templateRef` shape and rejects this Agent manifest. Cluster acceptance
used a control plane built from this checkout and Substrate `0.5.0-alpha2`.
Configure a `kagent-default`
WorkerPool, snapshot storage, and Substrate credential gateway. In namespace
`kagent`, create the Secret `deepseek-api-key` with an `OPENAI_API_KEY` entry
using your normal secret-management workflow. Grant the Substrate atespace
access to that namespace as described in
[credential injection](../../../../docs/architecture/credential-injection.md).
The bridge honors the platform's Node CA and proxy environment.

Set `DSH_MODEL` to a Chat Completions model available to your DeepSeek account,
`DSH_BYO_IMAGE` to the image digest, and `SNAPSHOT_LOCATION` to your installation's
snapshot storage URI. Render only those variables:

```sh
envsubst '${DSH_MODEL} ${DSH_BYO_IMAGE} ${SNAPSHOT_LOCATION}' \
< examples/dsh-byo/agent.yaml.tmpl > /tmp/dsh-agent.yaml
kubectl apply --dry-run=server -f /tmp/dsh-agent.yaml
kubectl apply -f /tmp/dsh-agent.yaml
kubectl get harness,agent -n kagent
```

After preparation succeeds, open `dsh-agent` in the kagent UI and create a
conversation. To bind an MCP server, add a normal `tools[].mcp` binding to the
inline AgentTemplate, omit the `tools` subset, and leave `requireApproval` unset.
The server must use Streamable HTTP and the platform trust bundle.

## Verification

From `go/` in a Linux development environment:

```sh
go test -race ./harness/byo/dsh/... ./harness/runtime/...
```

The Linux subprocess tests exercise permission decisions, cancellation, identity
validation, malformed protocol handling, detached and orphaned tool cleanup,
unrelated-process survival, and adapter death without a model API. Configuration,
event conversion and wire tests also run on non-Linux hosts.
For the published runtime tests, install the locked npm dependencies and run:

```sh
npm ci --prefix harness/byo/dsh --ignore-scripts --no-audit --no-fund
DSH_TEST_BIN="$PWD/harness/byo/dsh/node_modules/.bin/dsh" \
go test -race ./harness/byo/dsh/internal/bridge -run TestRealDSH -count=1
```

These tests use a real DSH process, a local model fixture, and a local MCP
server. A2A gRPC calls use the production TaskStore client and settlement wrapper
against a local controller RPC fixture. They check MCP execution, resumed turns,
runtime identity headers and versioned settlement without provider credentials.
Separate process tests require DSH and detached tools to exit before settlement
on normal completion, forced cleanup and cancellation.

A real-DSH fault test kills the process with SIGKILL while a tool awaits approval.
It checks that allow/reject/cancel can settle the old task after cleanup, a new
task completes in the same native session and A2A context, and the interrupted
tool's marker file is never created. The adapter remains alive throughout this
test; it does not establish recovery after Actor or adapter replacement.

`TestRealDSHRestartClosesPendingCallWithoutReplay` reconstructs the runner from
the durable directory after terminating DSH without a permission response. It
checks that native resume records an unknown outcome for the original call and
does not execute its command. This is a regression check for safe interruption;
it is not a passing test for durable approval continuation.

The durable-workspace test creates, reads and edits a file with DSH's built-in
tools across three fresh adapter instances. It verifies that the native session
identity, conversation history and workspace contents survive reconstruction
from the same durable directory. These checks do not exercise the real controller
database or Substrate snapshots and are not a substitute for cluster acceptance.

Manual cluster acceptance on Linux arm64 with gVisor, Kubernetes `1.37.0` and
Substrate `0.5.0-alpha2` exercised the public A2A API and real PostgreSQL-backed
TaskStore. Three turns completed and automatically suspended; public `GetTask`
returned the completed task after each suspension. After explicit suspend/resume,
the model fixture received the earlier conversation history. Gateway credential
injection and workspace write/read/edit/read across suspensions also passed.
The model was a local Chat Completions fixture, not a live provider.

Before release, complete the remaining cluster checks: MCP success and failure,
cancellation followed by another turn, approval allow/reject/cancel, and continuity
after Actor replacement and fork. The checks above do not establish guarantees
for untested lifecycle paths or model usage reporting.
Loading
Loading