summaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 18:29:53 +0100
committerHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 18:29:53 +0100
commit0acf9b61c3d3c750e4af39900c43fc1cdd221578 (patch)
tree36bcf3c1ea5fa1c869656db06cf5ceb297562d92 /modules/core
parenta14d8ab15130779e4d612f690cd3c806800875c6 (diff)
fix(newos4): `ui` now works in classic — it was being answered by the AI (v4.6.168)
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.
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js53
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/terminal.js53
3 files changed, 105 insertions, 3 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index a9a8de5..00043bc 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -2716,7 +2716,12 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
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);
@@ -4134,6 +4139,47 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
}
}
+ /** 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 = {
@@ -4723,6 +4769,11 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
}
}
+ // `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) {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index ccdda90..1f2f1c7 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.165';
+ const LATEST_VER = 'v4.6.168';
const CACHE = '?v=' + LATEST_VER;
// Exposed so ext modules can register their own URL with the same cache-busting version
// instead of an unversioned path the browser may already have cached (shell.js did).
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) {