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.
---
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*.
---
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.
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_edits, 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.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.---
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.
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.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.
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.C:\KAI\tools\oracle-discord\shared\leo.mjs.bak-kaicoder-20260709T142530Z.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.)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).
C:\KAI\KAI-CODER-CHANGELOG.mdEvery 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)
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.run_shell action is generally not possible (side effects already happened); the tool says so plainly rather than pretending. Only file edits are reversible.---
C:\KAI (reuse assertUnderRoot, toolserver L39–45). Anything resolving outside C:\KAI is refused before backup.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.KAI-CODER-CHANGELOG.md) is append-only via the logger, never an edit_file target.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).run_shell)Start from the existing HARD_BLOCKED regex (toolserver L129) and extend it. Hard-blocked, never runnable:
rm -rf, del /f /s /q, Remove-Item -Recurse -Force, rd /s.format, mkfs, cipher /w, disk-part wipes.shutdown, reboot, Restart-Computer.taskkill/Stop-Process targeting the fleet (node/oracle/kaiverse/bot processes), killing :3001/:3334/:3410/:3420/:8080/:8081, or stopping the training pipeline.cat/type/Get-Contents a .env, or pipes it outbound.iwr ... | iex, curl ... | bash).Blocklist is checked before the approval prompt — a hard-blocked command is refused outright and never offered for approval.
Not blocked, but requires a second explicit confirm ("type CONFIRM to proceed") on top of normal approval, and a bold warning in the proposal:
run_shell that writes/moves/renames files, git state changes (commit, reset, checkout, push), package installs, or restarts (Start-KAI.ps1, Start-Dashboard.ps1).high (reuse riskScore, agent L543–548 already refuses high-blast auto-apply — here it upgrades to double-confirm instead).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.)read_file, list_dir, propose_edit (dry-run), grep, diff, status all run without a gate. Only mutation waits for a yes.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.
---
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:
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.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.
---
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.
---
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.*