feat(tui,kernel): deterministic command-approval parse layer (§5.1)
Layer 1 of tui-requirements §5: a no-LLM analyzer that shell-lexes a
proposed command, classifies binaries against a known-command table, and
mechanically flags the constructs that widen blast radius — sudo/doas,
recursive delete, pipe-to-shell (skipping wrappers so `| sudo sh` counts),
redirects outside the workspace, network binaries, base64/eval, and
unparseable input (itself a red flag). Pure + replay-stable: same preview
+ workspaceRoot → same analysis, no environment reads.
The approval band renders command approvals as a card — worst-first flag
chips over the reconstructed command line, each token colored by severity
(T0–T4 ramp). ^x opens a scrollable fullscreen view so a long command is
never truncated. Diff and plain-preview paths are unchanged.
Server: computeToolPreview now renders the shell tool's argv as a full,
shell-quoted command line instead of the 200-char JSON-args fallback,
honouring §3's "full untruncated command". The TUI parser accepts both
the rendered line and the older `{"argv":[...]}` shape for replay.
Layer 2 (model annotation) deliberately deferred per the spec.
This commit is contained in:
+24
@@ -1733,6 +1733,7 @@ abstract class SessionOrchestrator(
|
||||
* that will dispatch it on [Dispatchers.IO].
|
||||
*/
|
||||
private suspend fun computeToolPreview(toolName: String, parameters: Map<String, Any>): String? {
|
||||
if (toolName == "shell") return shellCommandPreview(parameters)
|
||||
if (toolName != "file_write") return null
|
||||
val path = parameters["path"] as? String ?: return null
|
||||
val operation = parameters["operation"] as? String
|
||||
@@ -1751,6 +1752,29 @@ private suspend fun computeToolPreview(toolName: String, parameters: Map<String,
|
||||
return buildDiffString(path, existingContent, proposedContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a shell tool's argv as a full, shell-quoted command line for the approval
|
||||
* card (tui-requirements §5 / §3 "full untruncated command"). Returning the rendered
|
||||
* line — rather than the raw `arguments.take(200)` fallback — keeps the command both
|
||||
* untruncated and human-readable; the TUI's deterministic parser re-tokenizes it.
|
||||
*/
|
||||
private fun shellCommandPreview(parameters: Map<String, Any>): String? {
|
||||
val argv = when (val raw = parameters["argv"]) {
|
||||
is List<*> -> raw.filterIsInstance<String>()
|
||||
is String -> runCatching { Json.decodeFromString<List<String>>(raw) }.getOrElse { emptyList() }
|
||||
else -> emptyList()
|
||||
}
|
||||
if (argv.isEmpty()) return null
|
||||
return argv.joinToString(" ") { shellQuoteToken(it) }
|
||||
}
|
||||
|
||||
/** Single-quote a token when it carries whitespace or shell metacharacters. */
|
||||
private fun shellQuoteToken(token: String): String {
|
||||
if (token.isEmpty()) return "''"
|
||||
val needsQuote = token.any { it.isWhitespace() || it in "\"'\\|&;<>(){}\$`*?[]~#!" }
|
||||
return if (needsQuote) "'" + token.replace("'", "'\\''") + "'" else token
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a unified-diff-style string for a full file replacement.
|
||||
* When the file doesn't exist yet (new file), only `+` lines are shown.
|
||||
|
||||
Reference in New Issue
Block a user