# 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 path** — `tools/oracle-discord/shared/native-tools.mjs` already declares `write_file` (L196–208), `edit_file` (L210–224, a surgical `target`→`replacement` 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 log** — `write_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 path** — `tools/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:

- **`edit_file` is surgical only** (`target`→`replacement`), never whole-file rewrite. This is deliberate: `CLAUDE.md` forbids whole-file rewrites of `oracle.html`/`kaiverse.js` (they have caused truncation), and the existing agent's whole-file `parseFileBlocks` path (agent L245–254, L408–430) is exactly the risky pattern we are moving away from for protected files. If a change is too large for one surgical replace, Kai Coder proposes **several** `propose_edit`s, each separately approved.
- **`propose_edit` is the Phase-A workhorse** — it is `edit_file` with the write suppressed, so trust can be built before any byte is written (§5).
- **`run_shell` covers build/test** (`cargo build`, `node --check`, `npm test`, `pytest`). It reuses the ecosystem runners already in the toolserver (`toolCargo` L394, `toolNpm` L413, `toolNode` L433, `toolPython` L460) rather than raw `execSync`, so cwd/timeout are correct per ecosystem.
- **The old raw `write_file` / `edit_file` / `bash` in native-tools.mjs are removed from Kai Coder's schema when `KAI_CODER_HANDS=1`** (see §4) so there is exactly one write path, not two.

---

## 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:
   - `approve <proposalId>` (or a reply "approve"/"yes" to the proposal message) → proceed to backup+edit.
   - `reject <proposalId>` (or "reject"/"no") → discard, log a `rejected` entry, write nothing.
   - **No reply within `KAI_CODER_APPROVAL_TIMEOUT` (default 300 s)** → auto-reject (fail-safe), log `timeout`, write nothing.
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>
```

- `<UTC>` = `YYYYMMDDTHHMMSSZ` (e.g. `20260709T142530Z`), always UTC, sortable.
- Example: `C:\KAI\tools\oracle-discord\shared\leo.mjs.bak-kaicoder-20260709T142530Z`
- The backup path is captured and becomes the **rollback target** recorded in the log (§2.4).
- (The toolserver's existing `.kai-backups/<name>.<epoch>.bak` backup on `apply`, L253–271, is kept as a redundant safety net but the **canonical rollback source is the `.bak-kaicoder-<UTC>` beside the file**, so rollback is trivially local and human-inspectable.)

### 2.3 Edit (apply)

Only now does the write happen — the surgical `target`→`replacement` 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.

- **`kaicoder rollback <rollback-id>`** — parse `KAI-CODER-CHANGELOG.md`, find the entry with that id, read its `backup:` and `file(s):` lines, copy the backup **back over** the file, then append a new `rollback` entry (so the rollback is itself logged and, in turn, reversible — the pre-rollback state is snapshotted to its own `.bak-kaicoder-<UTC>` first).
- **`kaicoder rollback last`** / "undo last" — same, but resolves to the most recent `edit` entry at the top of the changelog.
- Rollback of a `run_shell` action is generally **not possible** (side effects already happened); the tool says so plainly rather than pretending. Only file edits are reversible.
- Rollback itself is a privileged action and, in Phase B+, still posts a one-line confirm ("about to restore X from backer Y — approve?") unless the owner runs it directly.

---

## 3. Guardrails (hard limits)

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

- Writable roots: **only under `C:\KAI`** (reuse `assertUnderRoot`, toolserver L39–45). Anything resolving outside `C:\KAI` is refused before backup.
- **Never-write list (hard deny, even under C:\KAI):**
  - `tools/oracle-discord/.env` and any `*.env` / secrets file (API keys live here per `CLAUDE.md`). Reads of `.env` are also denied to the hands tools — no echoing secrets, ever.
  - `.git/`, `node_modules/`, `target/`, `dist/`, `build/`, `.kai-backups/`, existing `*.bak*` files.
  - The changelog itself (`KAI-CODER-CHANGELOG.md`) is append-only via the logger, never an `edit_file` target.
- **Extra-confirmation tier (irreversible / high-blast) — see 3.3.**
- **Protected large files** (`oracle.html`, `kaiverse.js`, `The KAI Codex.md`, `leo.mjs`): editable **only** via surgical `edit_file` (never `write_file`/whole-file), and always land in the extra-confirmation tier. Whole-file replacement of these is hard-blocked (`CLAUDE.md` rule).

### 3.2 Command blocklist (`run_shell`)

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

- Recursive/forced delete: `rm -rf`, `del /f /s /q`, `Remove-Item -Recurse -Force`, `rd /s`.
- Disk/format: `format`, `mkfs`, `cipher /w`, disk-part wipes.
- Power/host control: `shutdown`, `reboot`, `Restart-Computer`.
- **Fleet sabotage:** `taskkill`/`Stop-Process` targeting the fleet (node/oracle/kaiverse/bot processes), killing `:3001`/`:3334`/`:3410`/`:3420`/`:8080`/`:8081`, or stopping the training pipeline.
- Secret exfiltration: any command that `cat`/`type`/`Get-Content`s a `.env`, or pipes it outbound.
- Network installs that execute remote code unprompted (`iwr ... | iex`, `curl ... | bash`).

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:

- Any `run_shell` that writes/moves/renames files, `git` state changes (`commit`, `reset`, `checkout`, `push`), package installs, or **restarts** (`Start-KAI.ps1`, `Start-Dashboard.ps1`).
- Edits to protected large files (§3.1) or any file whose blast-radius risk = `high` (reuse `riskScore`, agent L543–548 already refuses high-blast auto-apply — here it upgrades to double-confirm instead).

### 3.4 The human gate is mandatory

- **Never auto-approve writes or shell.** `KAI_AUTOAPPLY` (agent L516–518) stays OFF and is explicitly *not* honored by the hands module — the hands loop's gate is human-only by design. (Auto-repair auto-apply, if ever revived, is a separate track and out of scope here.)
- **Reads stay open** — `read_file`, `list_dir`, `propose_edit` (dry-run), grep, diff, status all run without a gate. Only mutation waits for a yes.

### 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:
   - Add `propose_edit`, `edit_file` (re-pointed), `run_shell`, `list_dir` to the schema, **gated behind `KAI_CODER_HANDS=1`**; when the flag is on, **remove the raw `write_file`/`edit_file`/`bash`** from Kai Coder's visible tools so there's one write path. Extend `PRIVILEGED_TOOL_ACCESS` (L413–423) and `getToolsForBot` (L427–429) accordingly.
   - In `executeToolCall` (L431+), route the new tools to `kai-coder-hands.mjs`. The `propose_edit`/`edit_file`/`run_shell` cases return a **Promise** that resolves on approval, exactly like the `consult_oracle` case (L681–691).

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 `target`→`replacement` 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.*
