diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 17:53:39 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 17:53:39 +0200 |
| commit | 9ae724e87a6471ce503783615ef6083d991531ce (patch) | |
| tree | da5523a5c8d3550c4236ead87cf878067a9cedbe /modules/core/terminal.js | |
| parent | 83e64b88910c952c7469cee57cc5c5a8a07ad902 (diff) | |
fix(newos4): refuse shell composition, wc takes multiple paths (v4.6.58)
Packet 1 and 3 (silent wrong answers from a shell-less terminal).
- detectComposition() in the security gate refuses ; && || | before dispatch.
`;` used to fail loudly, but `&&`/`||` folded the right-hand side into the left
command's argv (wc -l f && grep -c x f returned "42 42"), and a `|` arriving from
the AI run_command tool bypassed splitPipe entirely and degraded the pattern to
empty -- which matches every line. Operators inside quotes are untouched.
- wc accepts multiple paths: one line per file plus a total, per-file errors for
missing paths. `wc -c a b` used to print only `a` and exit 0.
- help.html: "There is no shell" callout at the top of the command reference,
plus the v4.6.57/58 changelog entry.
Verified live on v4.6.58 by replaying the exact call sequence used by both paths:
interactive `wc -c a b` -> "14 a / 10 b / 24 total"; `&&` and `;` refused;
`echo "a && b"` still echoes; AI run_command semantics with `|` and `&&` refused;
single command and multi-file wc still return real output.
Diffstat (limited to 'modules/core/terminal.js')
| -rw-r--r-- | modules/core/terminal.js | 26 |
1 files changed, 26 insertions, 0 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js index ad4b105..10608d1 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -176,7 +176,33 @@ const unknown = !known; return { cmd, args, known, destructive, system, unknown, hasSuspicious }; } + // v4.6.58: this terminal has no shell. `;` at least failed loudly, but `&&`/`||` + // folded the right-hand side into the left command's argv, and a `|` arriving from + // the AI run_command tool (which bypasses splitPipe) degraded to an empty pattern + // matching every line. Both are silent wrong answers — refuse instead (packet §1). + detectComposition(input) { + const s = String(input || ''); + let inSingle = false, inDouble = false; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; } + if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; } + if (inSingle || inDouble) continue; + if (ch === '&' && s[i + 1] === '&') return '&&'; + if (ch === '|' && s[i + 1] === '|') return '||'; + if (ch === '|') return '|'; + if (ch === ';') return ';'; + } + return null; + } async checkAndRun(rawInput) { + const badOp = this.detectComposition(rawInput); + if (badOp) { + this.terminal.print('⚠️ No shell here — `' + badOp + '` is not interpreted. ' + + 'Run one command per line (the interactive terminal supports `cmd | cmd`; ' + + 'AI run_command does not).', 'error'); + return; + } const { cmd, args, known, destructive, system, unknown, hasSuspicious } = this._classify(rawInput); // Level 3 Audit: just log and allow if (this.level === 3) { |