KDCube
← Recipes
KDCube Recipes · The Job Card

Host a Coding Agent Inside Your KDCube App

A teammate opens a browser, asks the resident agent to change a file in a git-backed store, watches the work happen as steps, reviews the diff on a desk beside the chat, and commits — under their own identity, with only the capabilities they granted.

14 August 2026RecipesHands-onThe Job Card
resident coding agentclaude codegit-backed storegranted MCPcoding agent inside an apphost claude code in kdcubeagent edits a git repo from chat

WHAT LEAVES THE SHOP

One git-backed store with two doors. An engineer works the checkout on their laptop with their own coding agent. A teammate opens the app in a browser, asks the resident agent to change the same files, watches the work as steps in the chat, sees the diff on a desk beside it, and commits — under their own identity, with only the capabilities they granted.

A coding agent is a filesystem animal: it reads, greps, edits, runs commands, commits. This recipe makes one a citizen of your app — identity, consent, accounting, a conversation that survives a reload — without inventing a second way to work the same repository.

Current commands and descriptors still say bundle in names such as bundles.yaml, bundle reload, and @bundle_entrypoint. In this recipe, app = bundle: one deployable KDCube unit.

YOU WILL NEED
  • A running KDCube (local is fine) and an app you can edit
  • The claude CLI in the runtime image, with credentials reachable by the subprocess
  • A git repository for the content, and a branch namespace for agent session transcripts
  • Connection Hub reachable in that deployment — the grant lane lives there
  • Claude Code with the KDCube plugin, for the guided path
ONE STORE · TWO DOORS DOOR A · your machine a git checkout + your own coding agent edit · grep · run the package procedures review the diff by eye git pull · git commit · git push DOOR B · the shared app the desk + the resident agent, in a browser ask · edit · preview · stage the review no checkout, no terminal, no setup Sync now · Commit & Push one working tree, one history content · metadata · assets — the files themselves WHOEVER EDITS, THE HISTORY IS THE SAME HISTORY the remote what joins two machines a colleague who runs KDCube gets both doors on their box a colleague who does not gets Door B — and that is enough
One store, two doors — the app door has to behave like the laptop door.

OP 10OF 110 Get the assistant that knows this platform

claude codeSHELL
/plugin marketplace add https://github.com/kdcube/agent-plugins
/plugin install kdcube@kdcube
/kdcube:init

The plugin points your own coding agent at the current KDCube source and docs, so the wiring below is generated against the platform you actually run, not a remembered version of it.

CHECK · THE INSPECTOR’S STAMP

Ask it to open the resident-agent recipe from the docs it just connected. If it can read docs/recipes/apps/, you are pointed at the right tree.

OP 20OF 110 Lay out the store, the workspaces, and the toolchain

Three directories, and only one of them is the repository.

the.layoutMAP
<bundle storage>/
  store/                       ← the git checkout the agent edits
  agent_workspaces/
    <conversation_id>/         ← one workspace per CONVERSATION
      .claude/                 ← CLI config dir + session transcript
      .mcp.json                ← generated per turn
      CLAUDE.md                ← generated per turn
      _pulled/                 ← objects the agent pulled, on demand

<machine-local disk, NOT bundle storage>/
  <toolchain>-<sha256(requirements.txt)[:16]>/   ← the operator venv

Two placements look convenient and are traps. A virtualenv inside the package is thousands of files in every git status an operator reads, and it disappears when the app re-materializes its source. A virtualenv on shared storage is thousands of small files over a network filesystem, corruptible by two hosts installing at once. Keep generated things beside the store, never in it, and keep the toolchain on the local disk of the machine that runs it, in a directory keyed by a hash of its requirements file.

CHECK · THE INSPECTOR’S STAMP

git status in the store lists exactly the content changes — no .venv, no workspaces, no caches.

OP 30OF 110 Run the CLI per turn, keep the workspace per conversation

Each turn starts a fresh subprocess and resumes the same session identity. The working directory must NOT move between turns: it is part of the runtime's system prompt, so a new path re-creates that prefix every turn — the stream reports cache_miss_reason: system_changed — and your user waits through a rebuild before the first token.

the.turnPYTHON
result = await run_claude_code_turn(
    agent=agent, prompt=prompt, kind="regular",
    resume_existing=previous_turn_ran,
    session_store=ClaudeCodeSessionStoreConfig(
        implementation="git",              # "local" only on a single-node dev box
        local_root=workspace / ".claude",
        tenant=tenant, project=project,
        user_id=user_id, conversation_id=conversation_id,
        agent_name=agent_name, git_repo=session_repo,
    ),
)

The agent's memory of your conversation is its own transcript, not the platform record. Putting that transcript on a branch per conversation is what lets the next turn run on a different worker and still remember yesterday.

CHECK · THE INSPECTOR’S STAMP

Run two turns in one conversation and compare time to first token. The second must be faster. If it is not, your workspace path moved.

OP 40OF 110 Give it one rulebook, not two

If the store ships a runbook — AGENTS.md, procedures, a README that routes — the resident agent follows that, the same document an engineer follows on a laptop. A second, lane-only instruction set drifts from the first within a week.

one.rulebookTEXT
store/AGENTS.md ─────────────┬────────────►  the engineer's own agent
roles · procedures · layout  │
                             └──►  CLAUDE.md, generated per turn:
                                     "Follow AGENTS.md at <path>.
                                      You are <identity>.
                                      Your surfaces are <…>."

Two consequences: the runbook's directory must be in the agent's working directories, or its links cannot be opened; and the commands it demands must be permitted, because a hosted lane has nobody to answer a permission prompt. Keep the runtime's own toolset a deployment choice — an allow list and a deny list, both empty by default — instead of a constant in code. An allow list grants permission; it does not remove what the CLI ships.

CHECK · THE INSPECTOR’S STAMP

Ask the agent, in chat, to summarize its role from the runbook. It should quote the file, not paraphrase a prompt you wrote.

OP 50OF 110 Publish the app's own MCP surface, and make it grantable

File tools let the agent read the store. They do not let it ask the app a question — search the index, resolve a reference, validate, commit through the guarded lane. That is your app's own MCP surface, reached as the signed-in user. Four declarations must agree.

FOUR DECLARATIONS, ONE GRANT 1 THE SURFACE · the app entrypoint @mcp(alias="ops", route="public", transport="streamable-http", auth_config=...) 2 THE AUTH BLOCK · the app descriptor mode: managed · authority_id: delegated_client selected_tool_grants: true 3 THE CONNECTION · this agent's tools kind: mcp · delegated: true · self_hosted: true url (public) + resource (host-wildcarded) scopes: [read, write, commit] · allowed: [tools] 4 THE CATALOG · the deployment decides connection-hub · delegated_credentials.oauth resources: endpoint → tool → claim capabilities: claim → who may delegate it EACH GAP REPORTS AS SOMETHING ELSE on the operations route, a bearer answers 401 a multi-tool surface through the REST path answers 403 ambiguous catalog half a catalog entry makes the hub answer grants_not_delegable and the CLI side fails silently NAME THE CONFIG · TRUST THE WORKSPACE · AVOID RESERVED SERVER NAMES · ANSWER JSON
Four declarations, one grant — each gap reports as something else.
bundles.yamlYAML
# the surface (entrypoint)
@mcp(alias="ops", route="public", transport="streamable-http",
     auth_config="surfaces.as_provider.mcp.ops.auth")

# the auth block
surfaces.as_provider.mcp.ops.auth:
  mode: managed
  authority_id: delegated_client
  selected_tool_grants: true

# the connection, on the agent
- kind: mcp
  server_id: ops
  delegated: true
  self_hosted: true          # this deployment serves it — dial it locally
  url:      "https://<public host>/…/public/mcp/ops"
  resource: "*/…/public/mcp/ops*"
  scopes:  [<app>:read, <app>:write, <app>:commit]
  allowed: [search, get, save, commit]

Read the fourth declaration twice. An app offers endpoints; the Connection Hub catalog is what the deployment allows, and it is always a subset. An endpoint that is not in the catalog is not grantable — the hub answers delegated_access_grants_not_delegable — and declaring the endpoint without its claim under capabilities fails the same way, silently, from the other half.

Name the consequence. Claims read as <app>:read, :write, :delete, :commit — never after the transport. The person approving the card is deciding what may happen to their work, not which URL is called.

CHECK · THE INSPECTOR’S STAMP

Revoke the grant in Connection Hub and ask the agent to use the surface. Expect a consent card naming your claims — and the same turn's retry to succeed after you approve it.

OP 60OF 110 Make the CLI actually see its tools

Writing .mcp.json is necessary and not sufficient. Four conditions, each of which fails quietly:

the.four.conditionsCHECK
✔ name the config on the command line   --mcp-config <ws>/.mcp.json --strict-mcp-config
    a project config the CLI discovers by itself is approval-scoped,
    and a hosted lane has nobody to approve it

✔ record the workspace as trusted        .claude/.claude.json →
    until trusted, the CLI ignores EVERY     projects["<abs workspace>"]
    permissions.allow entry, MCP included      .hasTrustDialogAccepted = true

✔ avoid reserved server names            `workspace` is taken; the platform's
                                             own local server is `turn_workspace`

✔ answer JSON, not an event stream       a request/response call needs no stream,
                                             and a stream is a shape every hop can break

The last one earns its place with a story. A tool call went out, the app served it in one second — 200, seventeen kilobytes — and the agent waited two minutes and reported the response as lost. Every byte had arrived; a tunnel's HTTP/2 edge closed the stream badly, and a strict client threw the whole response away. The server log said success the entire time.

The same story has a second lesson: when the surface belongs to the deployment that is calling it, say so with self_hosted: true. The declared public URL stays — grants and catalog patterns are written against it, host-wildcarded — and the call is dialed on the runtime's own loopback. No tunnel, no load balancer, no TLS termination between an app and itself. It is the correct route on a laptop and in the cloud alike, because the agent's subprocess and the surface it calls are the same task.

CHECK · THE INSPECTOR’S STAMP

From a shell inside the runtime, call one tool with the agent's own bearer. Require two things: HTTP 200, and a clean connection close. A body that arrives on a broken stream is a failed call.

OP 70OF 110 Hand the turn's objects over as refs, and let the agent pull

A message carries more than text: uploads, pinned objects, references from other apps. Do not write any of it into the workspace up front. Most turns never open it, and a binary copied per turn is paid for per turn.

pull.on.demandFLOW
message ──► turn events ──► prompt says: "attached: conv:fi:…/brief.pdf"
                                     │
                  the agent decides it needs it
                                     ▼
           pull(refs=["conv:fi:…/brief.pdf"])
                                     │
     ┌───────────────────────────────┴──────────────────────────┐
     │ resolves through the platform's own resolver              │
     │ writes bytes into <workspace>/_pulled/                    │
     │ answers: a local path + a time-limited download link      │
     └───────────────────────────────────────────────────────────┘
                                     │
           the agent opens it with its ordinary file tools

Identity for that server travels in the child process's environment, never in a tool argument, so an agent cannot pull as somebody else by naming them. And keep it out of your capability narrowing: namespaces come and go with an administrator's inventory, a user's pick, or a lapsed grant, but none of that may take away an agent's ability to open a file its own conversation carries.

CHECK · THE INSPECTOR’S STAMP

Attach a file to a message and ask a question about it. The steps should show a pull, then a read — not a mysteriously informed answer.

OP 80OF 110 Show the workings, keep the answer clean

A CLI runtime reports every tool result back into its own conversation as a user event. A generic text extractor takes it, and a file the agent read arrives in chat as the agent's answer, line numbers and all.

ONE TURN, END TO END a prompt + its refs on the conversation lane the workspace, per CONVERSATION .mcp.json · CLAUDE.md · settings · trust the CLI subprocess resumes its own session · cwd = the workspace THREE TOOL FAMILIES, THREE DIFFERENT BOUNDARIES file tools, on the store read · grep · edit · the shell permitted by the deployment, because a lane cannot answer a prompt the app's own MCP surface search · resolve · validate · commit as the signed-in user, per-agent grant, dialed locally when we serve it the turn's workspace server turn_workspace · pull / pulled local stdio · identity from the environment · no pick can remove it the CLI's stdout, split two ways tool calls + results → ACTIVITY ROWS · assistant text → THE ANSWER recording — what a reopened conversation is rebuilt from turn log · context objects as events · cost · elapsed · title the session store — what the AGENT is rebuilt from one git branch per conversation
One turn, end to end — what runs, what the reader sees, what survives.

Four rules make that readable. Tool traffic is never answer text. Each call gets its own step key, or the list becomes one row rewriting itself. The row's body travels as the step's markdown, or there is nothing to expand. And surface the waiting: the CLI heartbeats a pending call, so put the elapsed seconds on the row — a call that will never return looks exactly like a slow one until you do.

CHECK · THE INSPECTOR’S STAMP

Ask for something that needs a tool, and watch a row complete — not merely start.

OP 90OF 110 Do not lose the platform's turn

entrypoint.pyPYTHON
async def pre_run_hook(self, *, state, econ_ctx: dict | None = None):
    await super().pre_run_hook(state=state, econ_ctx=econ_ctx or {})   # recording
    await self._ensure_store(reason="pre_run_hook")                    # then your work

The base hook starts the turn's event recording, and that recording is what a reopened conversation is rebuilt from. The failure is silent: cost and elapsed time appear live and vanish on reload. Two siblings of the same class: objects the message carried must be recorded as their own events, so the reopened chip is live rather than prose about a chip; and the conversation title needs a role that resolves to a model — declare it, or every conversation lists as "Untitled" and nothing raises.

CHECK · THE INSPECTOR’S STAMP

Reopen the conversation. Answer, activity rows, cost, elapsed time, title, and any attached object must all still be there. Live and reloaded are different code paths; only the second proves the turn was recorded.

OP 100OF 110 Put a desk beside the chat

A coding agent is good at changing files and poor at showing you thirty of them. Pair the conversation with a small app UI over the same working tree.

the.deskLAYOUT
┌──────────── app scene ─────────────┐
│  desk                │  chat       │
│  ▣ asset.png   NEW   │  ▸ Bash · … │
│  ¶ notes.md   EDITED │  ▸ Read · … │
│  ⚙ meta.yaml         │  the answer │
│  [↻ Reload][⇪ Upload]│             │
│  uncommitted (3)     │             │
│  [note][Commit&Push] │             │
└──────────────────────┴─────────────┘
           both read the SAME working tree

What live use taught: the panel is a reading of a tree other hands also write, so it needs an explicit reload and should say what it shows and when it read it. The file list is the operator's index — let them resize it, one line per file, and keep state marks smaller than the names they annotate. Destructive controls confirm and say what breaks. A structured file needs a structured editor: indent on Tab, keep the level on Enter, line numbers, no soft wrap. And rows should be draggable into chat, because what travels is a ref — exactly what OP 70's pull consumes.

CHECK · THE INSPECTOR’S STAMP

Ask the agent to change a file; the desk shows it as uncommitted after a reload, and committing from the desk shows up in git log from a shell.

OP 110OF 110 Let the agent keep its own toolchain

If the store's procedures run scripts, they need an interpreter. The app names the path and whether it exists. The agent reads the requirements, inspects the environment, and builds or refreshes it on demand, in the conversation, with its own shell. An app that rebuilds in the background pays on turns that never touch the tooling and hides failure in a log nobody reads.

CHECK · THE INSPECTOR’S STAMP

On a fresh machine, ask the agent to prepare the toolchain. It should build it in the machine-local cache and say where — never inside the package.

Verify the whole thing

  • Ask the agent to list its tools: the app's granted tools, the workspace pull, and the built-ins your deployment allows.
  • Ask something that needs the app's surface; the row completes.
  • Revoke the grant; a consent card appears and approving it unblocks the retry.
  • Attach a file; the agent pulls it before answering.
  • Change a file from chat, see it on the desk, commit it, read it in git log.
  • Reopen the conversation: everything is still there.
  • Two turns in one conversation: the second reaches first token faster.
  • One tool call over the wire with the agent's bearer: 200 and a clean close.
FINAL INSPECTION · DONE MEANS
  • A teammate with a browser and no checkout changes files in the store, sees the change, and commits it — as themselves.
  • An engineer with a checkout and their own CLI agent works the same files, the same runbook, the same history.
  • Every capability the agent has is one that person granted, per agent, revocable, and named after its consequence.
  • The workings are visible, the answer is clean, and a reopened conversation shows all of it.
  • Nothing generated lives in the store, and the toolchain is the agent's to maintain.

Read more

KDCube Recipe
№ 2026-08-14 · kdcube.tech