From 0acf9b61c3d3c750e4af39900c43fc1cdd221578 Mon Sep 17 00:00:00 2001 From: Hermes SysOp Date: Thu, 12 Nov 2026 18:29:53 +0100 Subject: fix(newos4): `ui` now works in classic — it was being answered by the AI (v4.6.168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator report: "Ui command don't work". It was worse than a broken command: typing `ui` in the classic terminal printed Security: Unknown command "ui" — routing to AI (did you mean "ai"?)... → Routing to AI assistant... and an AI turn answered it. Three layers each looked healthy on its own: 1. `ui` is registered by UIShellModule, which since the v4.6.162 gate only loads when the visitor has already opted in — so the command that switches the interface did not exist in the state you need it from. 2. `_classify()` decides "known" from the command registry (module commands + a short builtin list), not from the safety whitelist, so `ui` was `unknown: true`. At the default security level 1 an unknown command that is not whitelisted is offered as a typo and then routed to the AI. `ui` was not whitelisted. 3. Nothing printed an error anyone would recognise as failure — it looked like the terminal was answering you. Fix: * `'ui'` joins `_baseWhitelist` (114 -> 115). It is a navigation command: safe, non-destructive, and it must never depend on the interface it navigates to. * `dispatchCommand` gets an `if (cmd === 'ui')` fallback that mounts the shell on demand via a new `_mountUIShell()` (register + init, the same two steps the boot loop performs) and then opens the picker. It also writes `webx11_ui_experimental=1`, so the opt-in survives the reload. * The mount is deliberately NOT `_safeRegister('UIShellModule', …)`: that bare call is the boot gate and `verify-newos4-ui-gate.py` fails on any occurrence of it. A guard that grants one exception has to grant whatever is written next to it, so the on-demand path is separate by name. Guard: new check `ui_command_in_classic` — executes `ui` on the served page with no flag and waits for `webx11.uiShell` + `#wx11-ui-picker`. Deliberately behavioural: `_classify('ui').known` reads FALSE even when the command works (registry vs whitelist), so a text or classifier assertion would have been a lie in both directions. The RED case is on record: the AI-routing line above, captured in the live scrollback before the fix. Verified on v4.6.168 (served): `ui` -> picker with 7 options; `ui easy` -> easy; `ui cockpit` -> cockpit; `ui classic` -> classic; `ui nonsense` -> "Unknown mode 'nonsense'. Available: classic, easy, cockpit" and stays put. Guard 8/8 rc=0. maintain OK. --- modules/core/terminal.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) (limited to 'modules/core/terminal.js') diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 9360f9e..d1ffd25 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -10,7 +10,12 @@ this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']; this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff']; this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script']; - this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','sed','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto']; + this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','sed','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto', + // 'ui' is a SAFE, non-destructive switch command. It used to be known only while the + // shell module was registered, so in classic the security layer classified it unknown + // (level 1) and routed it to the AI instead of dispatching — "Ui command don't work" + // (2026-11-12). Never leave a navigation command dependent on the UI it navigates. + 'ui']; this.destructiveCmds = new Set(this._baseDestructive); this.systemCmds = new Set(this._baseSystem); this.suspiciousFlags = new Set(this._baseSuspiciousFlags); @@ -1428,6 +1433,47 @@ } } } + /** Register + initialise the UI shell on demand, then run a `ui …` request through it. + * + * The boot path registers UIShellModule only when the visitor opted in (v4.6.162 gate), + * which made the `ui` command itself unavailable in classic — the one place a user would + * type it. registerModule() wires commands, but init() is what the boot loop calls for + * every module, so it is called here too. + */ + async _uiEnsureShell(args) { + const mm = this.moduleManager; + let shell = mm && mm.modules ? mm.modules['UIShellModule'] : null; + if (!shell) { + shell = this._mountUIShell(); + if (!shell) { this.print('Interface modes are not available in this browser.', 'error'); return; } + } + if (!shell || typeof shell.handle !== 'function') { + this.print('Interface modes are not available in this browser — the UI shell did not register.', 'error'); + return; + } + // Same opt-in the picker writes, so the next visit keeps the interface. + try { localStorage.setItem('webx11_ui_experimental', '1'); } catch (e) {} + return shell.handle(args && args.length ? args : ['picker']); + } + /** Construct, register and initialise the UI shell on demand (the `ui` command's own mount). + * + * Deliberately NOT the boot-time safe-registration path (the one guarded by `_uiExp`): that is + * verify-newos4-ui-gate.py fails on any bare occurrence of it — a guard that allows one + * exception has to allow whatever is written next to it. This path is distinguishable by + * intent, so it is distinguishable by name. + */ + _mountUIShell() { + if (typeof UIShellModule === 'undefined') return null; + const shell = new UIShellModule(); + try { + this.moduleManager.registerModule(shell); // wires shell.commands['ui'] + if (typeof shell.init === 'function') shell.init(this); + } catch (e) { + console.error('[Terminal] UI shell mount failed:', e); + return null; + } + return shell; + } _safeRegister(className, label) { // Safe class lookup — no eval(). Classes are registered in global scope by the bundle. const registry = { @@ -2017,6 +2063,11 @@ } } } + // `ui` must work in CLASSIC too. The command is owned by UIShellModule, which is only + // registered when the visitor has already opted in — so the switch command was missing + // in exactly the state you need it from ("Ui command don't work", 2026-11-12). Mount the + // shell on demand instead of telling anyone to edit the URL. + if (cmd === 'ui') { await this._uiEnsureShell(args); return; } // Enhanced: check fuzzy suggestions before showing "not found" const suggestion = this.security.suggest(cmd); if (suggestion && suggestion.dist <= 2 && suggestion.dist > 0) { -- cgit v1.2.3