Skip to content

feat(adk): set up the session directory, fetch changed repos - #3158

Open
teemow wants to merge 2 commits into
kagent-dev:mainfrom
giantswarm:upstream/adk-session-directory
Open

teemow wants to merge 2 commits into
kagent-dev:mainfrom
giantswarm:upstream/adk-session-directory

Conversation

@teemow

@teemow teemow commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Builds on #3157 (the claude runtime's session directory, which adds go/harness/workspace and the dispatch headers): its commit is the first here and drops out once it merges; review the second commit.

The Go ADK runtime works in its scratch directory /tmp/kagent/<session>. A session that works on repositories shared through a volume needs its tools in the session's own directory of that volume, set up from the volume's bare mirrors, as the claude runtime does with #3157.

The executor reads the turn's dispatch per turn (x-kagent-working-directory, x-kagent-mirrors-directory, x-kagent-changed-repositories). With a working directory:

  • the turn's context carries it (a small go/adk/pkg/turn package);
  • before the first turn in a directory with mirrors, the runtime sets it up with the shared go/harness/workspace: a clone with alternates of every mirror, then a fetch of exactly the changed repositories inside the dispatched turn, under a per-repository timeout and a total budget; AGENTS.md notes say how to start work in a repository, and .kagent/workspace.json makes the setup once;
  • a resumed turn (an answer or an approval) sets up nothing, and a session with earlier turns whose directory is set up again is told so;
  • a clone or fetch that fails is the message of the turn's first working status, naming the repository and the reason, and the turn runs;
  • bash runs in the turn's directory and the file tools resolve relative paths there; the scratch directory stays reachable by its absolute path, and a path outside both is refused.

A turn without a directory keeps the scratch directory, so nothing changes until something dispatches the headers.

Note: #3131 also adds a go/adk/pkg/turn package (a turn's process groups); the two touch different files of the same package.

Validation

  • Executor: a session without a volume sets nothing up, a refused fetch is the first status message and the turn runs, a resumed turn sets nothing up, a returning session is told of a fresh directory.
  • Tools: bash and the file tools in the turn's directory, the scratch directory by its absolute path, containment of paths outside both (TestResolvePathContainment with the turn's context).
  • Locally: go vet and go test -race of ./adk/pkg/a2a/..., ./adk/pkg/tools/..., ./adk/pkg/turn/....

The same change runs as a carried commit on the giantswarm/kagent-upstream fork (giantswarm#403).

The claude runtime runs Claude Code in the fixed /data/workspace. A
session that works on repositories shared through a volume (a directory
of its own beside read-only bare mirrors of the repositories) needs it
to work in that directory, and the directory starts empty.

The runtime now reads, per turn, three dispatch headers (constants in
go/api/a2a): x-kagent-working-directory, the directory the turn works
in; x-kagent-mirrors-directory, a read-only directory of bare mirrors
<owner>/<repository>.git; and x-kagent-changed-repositories, the
repositories whose origin moved since the mirrors' last sync. A turn
without a working directory runs where it always did, so nothing
changes until something dispatches them.

Claude Code is spawned in the dispatched directory. Before the first
turn in a directory with mirrors, the runtime sets it up with a new
shared package, go/harness/workspace, so the Go ADK runtime can reuse
it: every mirror gets a clone with alternates at <owner>/<repository>
(git clone --shared --no-checkout), its origin the mirror's origin URL
without user information, no working tree and no local branch, so the
agent's first git switch checks the default branch out from origin;
checkout.workers, core.untrackedCache and feature.manyFiles suit a
network filesystem. Then exactly the changed repositories are fetched
from origin inside the dispatched turn, so the fetch leaves through the
egress gateway with the turn's credential and the sandbox holds none,
each under a timeout and all under one budget. CLAUDE.md notes say how
the directory is laid out; .kagent/workspace.json records the setup, so
a later turn neither clones nor fetches again and a resumed turn
continues its process without setup.

A clone or fetch that fails is the turn's first status message, naming
the repository and the reason, and the turn still runs: runtime
EventSinks gain Notice, which the shared A2A executor reports as a
working status of the task.

The mirrors belong to the volume's owner, so Claude Code's environment
tells its git to trust them (safe.directory=<mirrors>/* through
GIT_CONFIG_COUNT, nothing written to a file); nothing else is trusted.

The gateway sets these headers once a session's actor mounts a volume
source, which needs Substrate to mount an existing volume at a sub-path
(agent-substrate/substrate#1637); that follows separately, and so does
an e2e of a session on a volume. Tested by unit tests of the workspace
package against real git (the clone layout, the fetch of changed
repositories, a refused and a timed-out fetch, the budget, a set-up
directory left alone, links the agent left, the trust environment), of
the driver (Claude Code in the dispatched directory, the configured
directory without one, the setup's notice and the trusted mirrors), and
of the executor's Notice.

Signed-off-by: Timo Derstappen <teemow@gmail.com>
The Go ADK runtime works in its scratch directory /tmp/kagent/<session>.
A session that works on repositories shared through a volume needs its
tools in the session's own directory of that volume, set up from the
volume's bare mirrors, as the claude runtime now does.

The executor reads the turn's dispatch per turn (x-kagent-working-
directory, x-kagent-mirrors-directory, x-kagent-changed-repositories,
from go/api/a2a). With a working directory, the turn's context carries
it (a small go/adk/pkg/turn package), and before the first turn in a
directory with mirrors the runtime sets it up with the shared
go/harness/workspace package: a clone with alternates of every mirror,
then a fetch of exactly the changed repositories inside the dispatched
turn, under a per-repository timeout and a total budget. AGENTS.md notes
say how to start work in a repository; .kagent/workspace.json makes the
setup once. A resumed turn (an answer or an approval) sets up nothing,
and a session with earlier turns whose directory is set up again is told
so. A clone or fetch that fails is the message of the turn's first
working status, naming the repository and the reason, and the turn runs.

bash runs in the turn's directory and the file tools resolve relative
paths there; the scratch directory stays reachable by its absolute path
and a path outside both is refused. A turn without a directory keeps
the scratch directory, so nothing changes until something dispatches
the headers; the gateway does once a session's actor mounts a volume
source, which follows separately.

Tested by unit tests on the executor (no volume, a refused fetch as the
first status message, a resumed turn, a returning session) and on the
tools (bash and the file tools in the turn's directory, the scratch
directory by its absolute path, the containment).

Builds on the claude runtime's session directory (its first commit
here), which adds go/harness/workspace.

Signed-off-by: Timo Derstappen <teemow@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant