← all documents · raw markdown · 21 KB

KAI Coder — Agent Hands Plan (propose → confirm → backup → edit → log → rollback)

Status: Design spec / plan — DOC ONLY. No code was changed to write this.

Author: engineering handoff for the next implementer.

Scope: turn Kai Coder from a code *advisor* into a real, safe coding agent with a hard human gate, auto-backup, a markdown change-log, and rollback.

Owner gate: Ryan (nastermodx) approves every write/shell action. Reads stay open.

---

0. Honest starting point (read the real code first)

The task framed Kai Coder as "read-only." That is only partly true today, and the plan has to account for what already exists. Two write paths are already in the tree:

1. Raw immediate-write pathtools/oracle-discord/shared/native-tools.mjs already declares write_file (L196–208), edit_file (L210–224, a surgical targetreplacement replace) and bash (L389–402, CODER_TOOLS_SCHEMA). These are gated to only kaicoder/kai via PRIVILEGED_TOOL_ACCESS (L413–423) and a defense-in-depth check in executeToolCall (L439–442). But they execute immediately with no confirm, no backup, and no logwrite_file just fs.writeFileSync (L563–566), edit_file replaces + writes (L568–575), bash runs execSync with only a bot-name check (L588–599). This is the unsafe gap the owner's design closes.

2. Sandbox pipeline pathtools/oracle-discord/shared/kai-coder-agent.mjs + tools/oracle-discord/tools/kai-coder-toolserver.mjs (HTTP tool server, port 3420). This one is much closer to safe already: it stages writes to a sandbox (SANDBOX_ROOT, toolserver L26/L116–126), runs node --check/cargo check validation (agent L432–494), produces a diff (toolserver toolDiff L212–251), and only promotes sandbox→production on an explicit apply, which already makes a backup into .kai-backups/<name>.<epoch>.bak (toolserver toolApply L253–271). It has a KAI_AUTOAPPLY safety brake defaulting OFF (agent L516–518) and a blast-radius gate that refuses to auto-apply high-dependency files (agent L543–548). There is already a HARD_BLOCKED destructive-command regex (toolserver L129).

So this plan is not "add hands from scratch" — it is "unify the two paths behind ONE mandatory safety loop, and disable the raw immediate-write path." Everything the owner asked for (confirm, backup, md-log, rollback) either replaces or wraps the pieces above. Where a real primitive already exists (sandbox, .bak backup, blast radius, HARD_BLOCKED, KAI_AUTOAPPLY gating, per-bot tool allow-list), reuse it rather than reinvent.

The brain is separate and already wired: the coder model (Model 2, BitNet finetune on glaive-code-assistant, training/cloud-bitnet/) served on :8081, reached by the ask_coder tool (training/phase0/tool_runtime.py L361–397, L533–543; default KAI_CODER_BASE=http://127.0.0.1:8081). ask_coder is inference-only and read-only w.r.t. the engine. This plan is Kai Coder's HANDS; ask_coder + Model 2 are the BRAIN. They pair: KAI delegates *what to write* to the coder model, and the hands module below is *how it safely lands on disk*.

---

1. The new "hands" tools (write-capable, gated)

All of these route through a single new module, shared/kai-coder-hands.mjs (§4), and are only visible/executable when the flag KAI_CODER_HANDS=1 is set (default OFF). Reads need no gate.

| Tool | Inputs | Returns | Risk |
|---|---|---|---|
| read_file | path | file contents (capped ~15 KB, as today) | none — already exists (native-tools L558–561); stays open, no confirm |
| list_dir | path | dir entries (name/type/size) | none — reuse toolserver toolList (L65–74) |
| propose_edit | path, target, replacement, why | a unified diff preview (before→after) + a proposalId; writes nothing | low (dry-run only) |
| edit_file | proposalId or (path,target,replacement,why) | after owner approval: backup path, applied confirmation, rollbackId, changelog entry | HIGH — mutates a tracked file |
| run_shell | command, cwd?, why | after owner approval: stdout/stderr (capped), exit code, rollbackId (n/a for pure reads) | HIGH / CRITICAL — arbitrary execution (build/test/etc.) |

Notes:

---

2. The safety loop — propose → confirm → backup → edit → log → rollback

This is the core of the owner's design. Every write or shell action walks this loop in order. There is no path that skips the human gate.

2.1 Propose + confirm (the human gate)

1. Kai Coder (driven by the coder model) produces an exact change: for an edit, a unified diff; for shell, the exact command line and cwd.

2. The hands module posts that proposal to Discord (the work channel, or a DM to Ryan) with a short why, the target path(s), the computed blast-radius risk (reuse agent buildGraph/blastRadius/riskScore, agent L19/L526–535), and a proposalId. Nothing is written yet.

3. Kai Coder's tool call blocks and waits for an explicit reply:

4. Approval must come from an authorized approver (Ryan = full authority; optionally Taz at a lower tier — mirror the existing authority note in agent L91). The approver's identity is recorded in the log.

Dry-run / preview mode: propose_edit and any tool called with dryRun: true runs steps 1–2 and stops — it returns the diff/command and never asks for approval or writes. This is the default behavior in Phase A and the safe way to inspect a change.

Approval channel: Discord is primary (the fleet is Discord-native). The Oracle dashboard (:3001) is an optional secondary approve/reject surface later. Either way the rule is identical: nothing writes without an explicit yes.

> ⚠️ Implementation flag — this is the hardest piece (see §6). The current native tool loop in executeToolCall is synchronous request→execute→return. The only existing tool that pauses and waits for an out-of-band Discord result is consult_oracle (native-tools L681–691): it returns a Promise that resolves via the requestOracleHelp(...) callback when the backend posts an answer. The approval round-trip must be built the same way — a pending-approval registry keyed by proposalId, plus a Discord message listener that resolves the promise on approve/reject. It is *provably doable* (consult_oracle does it) but it is real work, not a one-liner.

2.2 Auto-backup (before any write)

Immediately before the edit is applied, copy the target file to a timestamped backup next to it (not in a side folder), matching the repo's existing .bak-<label>-<timestamp> convention (e.g. tool_runtime.py.bak-toolmask-20260706-090411 already in the tree):

<originalfile>.bak-kaicoder-<UTC>

2.3 Edit (apply)

Only now does the write happen — the surgical targetreplacement replace against the real file (verify target is present and unique first; if not unique or not found, abort and report, never guess). For run_shell, "apply" = actually run the command (after the same approval + a snapshot note of what it touches).

2.4 Change log — C:\KAI\KAI-CODER-CHANGELOG.md

Every action (edit, shell, and also rejected/timeout/rollback) appends one entry, newest-first at the top (same discipline as the Codex CHANGELOG). This file is the audit trail *and* the rollback index.

Entry format:

## <UTC timestamp>  ·  <edit|run|rollback|rejected|timeout>  ·  rollback-id: <id>
- file(s):   <absolute path(s), comma-separated>   (— for pure shell)
- action:    <edit_file | run_shell | rollback>
- summary:   <one-line what changed / command run>
- why:       <the reason given in the proposal>
- backup:    <path to .bak-kaicoder-<UTC>>          (— if no file was written)
- approver:  <discord username / id>                (auto-reject if none)
- result:    <ok | fail: reason>
- rollback:  restore <backup path> → <file>          (— if not reversible)

rollback-id = the UTC timestamp with punctuation stripped (e.g. 20260709T142530Z), unique per action, used by the rollback command.

Concrete example entry:

## 2026-07-09T14:25:30Z  ·  edit  ·  rollback-id: 20260709T142530Z
- file(s):   C:\KAI\tools\oracle-discord\shared\leo.mjs
- action:    edit_file
- summary:   Guarded ElevenLabs TTS call with a null-voice check to stop the crash on empty text.
- why:       Leo threw "Cannot read properties of undefined (reading 'stream')" when text was empty; add a guard.
- backup:    C:\KAI\tools\oracle-discord\shared\leo.mjs.bak-kaicoder-20260709T142530Z
- approver:  nastermodx (Ryan)
- result:    ok
- rollback:  restore leo.mjs.bak-kaicoder-20260709T142530Z → leo.mjs

A run example:

## 2026-07-09T14:31:02Z  ·  run  ·  rollback-id: 20260709T143102Z
- file(s):   —
- action:    run_shell
- summary:   cargo check (RSHL core) — 0 errors, 2 warnings.
- why:       Verify the leo.mjs edit didn't break the workspace build path.
- backup:    —
- approver:  nastermodx (Ryan)
- result:    ok
- rollback:  — (read-only build; nothing to undo)

2.5 Rollback

Because every write entry logs its .bak-kaicoder-<UTC> backup and a rollback-id, undo is deterministic.

---

3. Guardrails (hard limits)

3.1 Scope allow-list (where edits are even *possible*)

3.2 Command blocklist (run_shell)

Start from the existing HARD_BLOCKED regex (toolserver L129) and extend it. Hard-blocked, never runnable:

Blocklist is checked before the approval prompt — a hard-blocked command is refused outright and never offered for approval.

3.3 Requires-extra-confirmation tier (irreversible but sometimes needed)

Not blocked, but requires a second explicit confirm ("type CONFIRM to proceed") on top of normal approval, and a bold warning in the proposal:

3.4 The human gate is mandatory

3.5 Honest limitation

The confirm gate only *actually* protects the system if Ryan reads the diff before approving. A rubber-stamped "approve" on a bad diff still lands the bad change. That residual risk is exactly why backup + log + rollback exist: even a careless approve is reversible in one command, and the changelog makes "what did Kai Coder change and why" auditable after the fact. Defense in depth, not a single wall.

---

4. How it wires in

New + touched files (all edits surgical, flag-gated, reversible):

1. NEW shared/kai-coder-hands.mjs — the whole loop lives here: proposeEdit(), applyEdit(), runShell(), backupFile(), appendChangelog(), rollback(), plus the pending-approval registry + Discord-reply resolver (modeled on requestOracleHelp/consult_oracle). Single source of truth for backup naming, blocklist, allow-list, and log format.

2. shared/native-tools.mjs — schema + executor changes:

3. Approval listener — a small hook in the Kai Coder bot runner (wherever the fleet already listens to Discord messages) that matches approve <id>/reject <id> replies and calls the resolver in kai-coder-hands.mjs. This mirrors the ORACLE_CONSULT_START / requestOracleHelp pattern already in place.

4. Reuse, don't rebuild: assertUnderRoot + sandbox + toolDiff + toolApply backup + HARD_BLOCKED (toolserver) and buildGraph/blastRadius/riskScore (agent) are imported, not duplicated. The toolserver's ecosystem runners (cargo/npm/node/python) back run_shell.

5. Flag: KAI_CODER_HANDS=1 in tools/oracle-discord/.env (default OFF). Optional tuning: KAI_CODER_APPROVAL_TIMEOUT (sec), KAI_CODER_APPROVER_IDS (allow-list of Discord ids).

6. Brain pairing: unchanged. ask_coder (tool_runtime.py) still delegates *what to code* to Model 2 on :8081; the hands module is *how it lands*. When Model 2 proposes a patch, KAI hands it to propose_edit → the loop above.

No versioned files (Codex, Cargo.toml, RELEASE-READINESS.md) are touched by this plan itself — those bumps happen when the code actually ships, per CLAUDE.md.

---

5. Rollout phases (each flag-gated, each earns the next)

Phase A — Propose-only (read + diff, zero writes). Ship read_file, list_dir, propose_edit. Kai Coder can analyze and produce exact unified diffs and command previews but cannot write or run anything. Purpose: prove the diffs are correct and the coder model's patches are trustworthy before a single byte is written. Gate: KAI_CODER_HANDS=1 with a PROPOSE_ONLY sub-flag on.

Phase B — Writes with approval + backup + log. Enable edit_file through the full loop: propose → confirm → .bak-kaicoder-<UTC> → surgical edit → KAI-CODER-CHANGELOG.md entry → rollback available. Still no shell. Gate: drop PROPOSE_ONLY. This is the first phase where Kai Coder changes real files — and every change is one command from undo.

Phase C — Shell / build / test with approval. Enable run_shell (build/test/verify) behind the same gate + the blocklist + the extra-confirmation tier for anything that mutates. Now Kai Coder can edit, then prove the edit compiles/tests, all human-gated.

Phase D — KAI delegates real tasks via ask_coder. With A–C trusted, let KAI route actual coding tasks to Model 2 (the brain) and have the hands module land the result through the loop end-to-end: KAI → ask_coder → Model 2 patch → propose_edit → Ryan approves → backup → edit → log. Full autonomy of *proposal*, full human control of *commit*.

Each phase is independently reversible by clearing its flag; nothing later than the enabled phase can execute.

---

6. Where the current fleet code makes this harder than expected

1. Approval round-trip is the real cost. executeToolCall (native-tools.mjs) is synchronous; only consult_oracle (L681–691) does an async pause-and-resolve-from-Discord. The confirm gate must copy that pattern (pending registry + message listener + timeout). Doable, proven, but it is the single biggest build item — not a config change.

2. Two write paths already exist and must be reconciled, not just added to. Raw write_file/edit_file/bash (native-tools) execute with no safety; the sandbox pipeline (toolserver/agent) is safer but uses a *different* backup convention (.kai-backups/<name>.<epoch>.bak) and a *whole-file* apply. The plan standardizes on surgical edits + .bak-kaicoder-<UTC> + the markdown log, and must actively remove/neuter the raw path or Kai Coder will still have an ungated way to write.

3. Whole-file generation conflicts with the surgical rule. The existing agent produces whole-file rewrites via parseFileBlocks (agent L245–254, L408–430). For protected files (oracle.html, kaiverse.js, Codex) that is explicitly banned by CLAUDE.md. The hands module must force surgical targetreplacement and reject whole-file writes to those paths — a behavior change from how the agent works today.

4. bash in native-tools has almost no guard. It runs execSync with only a bot-name check and a 30 s timeout (L588–599) — no HARD_BLOCKED, no cwd containment. run_shell must not reuse that; it should route through the toolserver runners (which do have HARD_BLOCKED + cwd containment) and add the blocklist/extra-confirm tier.

5. Mount can lie. Per CLAUDE.md, mounted/WSL reads of large files can be stale/truncated. The backup step must read the real file bytes right before writing (and the shape checks in agent L447–458 are a good template) so a backup is never taken from a truncated view.

---

*End of plan. Nothing in the codebase was modified to produce this document.*