-
-
Notifications
You must be signed in to change notification settings - Fork 188
fix: Config corruption, gateway.mode & Node.js update #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2f32cf3
9cbfcd1
d92dd0b
201713f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||||||||||||||
| import 'dart:convert'; | ||||||||||||||||||||||||||||||
| import 'dart:io'; | ||||||||||||||||||||||||||||||
| import 'package:dio/dio.dart'; | ||||||||||||||||||||||||||||||
| import 'package:flutter/material.dart'; | ||||||||||||||||||||||||||||||
| import 'package:google_fonts/google_fonts.dart'; | ||||||||||||||||||||||||||||||
| import '../constants.dart'; | ||||||||||||||||||||||||||||||
|
|
@@ -114,8 +115,8 @@ class _SplashScreenState extends State<SplashScreen> | |||||||||||||||||||||||||||||
| setupComplete = false; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Auto-repair: if only bionic-bypass is missing, regenerate it | ||||||||||||||||||||||||||||||
| // instead of forcing full re-setup (#70, #73). | ||||||||||||||||||||||||||||||
| // Auto-repair: if the rootfs and bash exist but other components are | ||||||||||||||||||||||||||||||
| // missing, try to repair them instead of forcing full re-setup (#70, #73, #97). | ||||||||||||||||||||||||||||||
| if (!setupComplete) { | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| final status = await NativeBridge.getBootstrapStatus(); | ||||||||||||||||||||||||||||||
|
|
@@ -125,9 +126,43 @@ class _SplashScreenState extends State<SplashScreen> | |||||||||||||||||||||||||||||
| final openclawOk = status['openclawInstalled'] == true; | ||||||||||||||||||||||||||||||
| final bypassOk = status['bypassInstalled'] == true; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (rootfsOk && bashOk && nodeOk && openclawOk && !bypassOk) { | ||||||||||||||||||||||||||||||
| setState(() => _status = 'Repairing bionic bypass...'); | ||||||||||||||||||||||||||||||
| await NativeBridge.installBionicBypass(); | ||||||||||||||||||||||||||||||
| // Core rootfs must exist — can't repair without it | ||||||||||||||||||||||||||||||
| if (rootfsOk && bashOk) { | ||||||||||||||||||||||||||||||
| // Regenerate bionic bypass if missing | ||||||||||||||||||||||||||||||
| if (!bypassOk) { | ||||||||||||||||||||||||||||||
| setState(() => _status = 'Repairing bionic bypass...'); | ||||||||||||||||||||||||||||||
| await NativeBridge.installBionicBypass(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+131
to
+135
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent error handling: bionic bypass reinstall lacks isolated try/catch. Node.js and OpenClaw reinstalls each have their own Proposed fix // Regenerate bionic bypass if missing
if (!bypassOk) {
setState(() => _status = 'Repairing bionic bypass...');
+ try {
await NativeBridge.installBionicBypass();
+ } catch (_) {}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Reinstall node if binary is missing (#97) | ||||||||||||||||||||||||||||||
| if (!nodeOk) { | ||||||||||||||||||||||||||||||
| setState(() => _status = 'Reinstalling Node.js...'); | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| final arch = await NativeBridge.getArch(); | ||||||||||||||||||||||||||||||
| final nodeTarUrl = AppConstants.getNodeTarballUrl(arch); | ||||||||||||||||||||||||||||||
| final filesDir = await NativeBridge.getFilesDir(); | ||||||||||||||||||||||||||||||
| final nodeTarPath = '$filesDir/tmp/nodejs.tar.xz'; | ||||||||||||||||||||||||||||||
| final dio = Dio(); | ||||||||||||||||||||||||||||||
| await dio.download(nodeTarUrl, nodeTarPath); | ||||||||||||||||||||||||||||||
| await NativeBridge.extractNodeTarball(nodeTarPath); | ||||||||||||||||||||||||||||||
| } catch (_) {} | ||||||||||||||||||||||||||||||
|
Comment on lines
+144
to
+148
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing parent directory creation before download. The Additionally, Proposed fix try {
final arch = await NativeBridge.getArch();
final nodeTarUrl = AppConstants.getNodeTarballUrl(arch);
final filesDir = await NativeBridge.getFilesDir();
final nodeTarPath = '$filesDir/tmp/nodejs.tar.xz';
- final dio = Dio();
+ await Directory('$filesDir/tmp').create(recursive: true);
+ final dio = Dio(BaseOptions(
+ connectTimeout: const Duration(seconds: 30),
+ receiveTimeout: const Duration(minutes: 10),
+ ));
await dio.download(nodeTarUrl, nodeTarPath);
await NativeBridge.extractNodeTarball(nodeTarPath);
} catch (_) {}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Reinstall openclaw if package.json is missing (#97) | ||||||||||||||||||||||||||||||
| if (!openclawOk && nodeOk) { | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||||||||||||||||||||||||||||||
| setState(() => _status = 'Reinstalling OpenClaw...'); | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const wrapper = '/root/.openclaw/node-wrapper.js'; | ||||||||||||||||||||||||||||||
| const nodeRun = 'node $wrapper'; | ||||||||||||||||||||||||||||||
| const npmCli = '/usr/local/lib/node_modules/npm/bin/npm-cli.js'; | ||||||||||||||||||||||||||||||
| await NativeBridge.runInProot( | ||||||||||||||||||||||||||||||
| '$nodeRun $npmCli install -g openclaw', | ||||||||||||||||||||||||||||||
| timeout: 1800, | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| await NativeBridge.createBinWrappers('openclaw'); | ||||||||||||||||||||||||||||||
| } catch (_) {} | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+151
to
+164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic bug: OpenClaw reinstall is skipped when both Node.js and OpenClaw are missing. The condition Trace:
Proposed fix: Re-fetch status after Node.js reinstall // Reinstall node if binary is missing (`#97`)
if (!nodeOk) {
setState(() => _status = 'Reinstalling Node.js...');
try {
final arch = await NativeBridge.getArch();
final nodeTarUrl = AppConstants.getNodeTarballUrl(arch);
final filesDir = await NativeBridge.getFilesDir();
final nodeTarPath = '$filesDir/tmp/nodejs.tar.xz';
- final dio = Dio();
+ await Directory('$filesDir/tmp').create(recursive: true);
+ final dio = Dio(BaseOptions(
+ connectTimeout: const Duration(seconds: 30),
+ receiveTimeout: const Duration(minutes: 10),
+ ));
await dio.download(nodeTarUrl, nodeTarPath);
await NativeBridge.extractNodeTarball(nodeTarPath);
+ // Update nodeOk after successful reinstall
+ final updatedStatus = await NativeBridge.getBootstrapStatus();
+ nodeOk = updatedStatus['nodeInstalled'] == true;
} catch (_) {}
}
// Reinstall openclaw if package.json is missing (`#97`)
if (!openclawOk && nodeOk) {Alternatively, a simpler fix if re-fetching status is expensive: Alternative: Track reinstall success with a local flag // Reinstall node if binary is missing (`#97`)
+ var nodeAvailable = nodeOk;
if (!nodeOk) {
setState(() => _status = 'Reinstalling Node.js...');
try {
// ... download and extract ...
await NativeBridge.extractNodeTarball(nodeTarPath);
+ nodeAvailable = true;
} catch (_) {}
}
// Reinstall openclaw if package.json is missing (`#97`)
- if (!openclawOk && nodeOk) {
+ if (!openclawOk && nodeAvailable) {🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| setupComplete = await NativeBridge.isBootstrapComplete(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } catch (_) {} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,10 @@ class GatewayService { | |
| } | ||
| } catch (_) {} | ||
|
|
||
| // Repair corrupted config before gateway start (#88). | ||
| // This fixes the "Invalid input: expected object, received string" crash loop. | ||
| await _repairConfigFile(); | ||
|
|
||
| final alreadyRunning = await NativeBridge.isGatewayRunning(); | ||
| if (alreadyRunning) { | ||
| // Write allowCommands config so the next gateway restart picks it up, | ||
|
|
@@ -137,9 +141,18 @@ const p = "/root/.openclaw/openclaw.json"; | |
| let c = {}; | ||
| try { c = JSON.parse(fs.readFileSync(p, "utf8")); } catch {} | ||
| if (!c.gateway) c.gateway = {}; | ||
| if (!c.gateway.mode) c.gateway.mode = "local"; | ||
| if (!c.gateway.nodes) c.gateway.nodes = {}; | ||
| c.gateway.nodes.denyCommands = []; | ||
| c.gateway.nodes.allowCommands = $allowJson; | ||
| // Fix config corruption: models entries must be objects, not strings (#83, #88) | ||
| if (c.models && c.models.providers) { | ||
| for (const [pid, prov] of Object.entries(c.models.providers)) { | ||
| if (prov && Array.isArray(prov.models)) { | ||
| prov.models = prov.models.map(m => typeof m === "string" ? { id: m } : m); | ||
| } | ||
| } | ||
| } | ||
| fs.writeFileSync(p, JSON.stringify(c, null, 2)); | ||
| '''; | ||
| var prootOk = false; | ||
|
|
@@ -167,10 +180,14 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2)); | |
| } | ||
| config.putIfAbsent('gateway', () => <String, dynamic>{}); | ||
| final gw = config['gateway'] as Map<String, dynamic>; | ||
| // Ensure gateway.mode=local so the gateway starts without --allow-unconfigured (#93, #90) | ||
| gw.putIfAbsent('mode', () => 'local'); | ||
|
Comment on lines
+183
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Treat null/empty Both Dart repair paths only patch a missing key. Suggested fix- gw.putIfAbsent('mode', () => 'local');
+ final mode = gw['mode'];
+ if (mode is! String || mode.isEmpty) {
+ gw['mode'] = 'local';
+ }
...
- if (!gw.containsKey('mode')) {
- gw['mode'] = 'local';
+ final mode = gw['mode'];
+ if (mode is! String || mode.isEmpty) {
+ gw['mode'] = 'local';
modified = true;
}Also applies to: 219-223 🤖 Prompt for AI Agents |
||
| gw.putIfAbsent('nodes', () => <String, dynamic>{}); | ||
| final nodes = gw['nodes'] as Map<String, dynamic>; | ||
| nodes['denyCommands'] = <String>[]; | ||
| nodes['allowCommands'] = allowCommands; | ||
| // Fix config corruption: models entries must be objects, not strings (#83, #88) | ||
| _repairModelEntries(config); | ||
| configFile.parent.createSync(recursive: true); | ||
| configFile.writeAsStringSync( | ||
| const JsonEncoder.withIndent(' ').convert(config), | ||
|
|
@@ -179,6 +196,81 @@ fs.writeFileSync(p, JSON.stringify(c, null, 2)); | |
| } | ||
| } | ||
|
|
||
| /// Repair openclaw.json on disk — fixes corrupted model entries and ensures | ||
| /// gateway.mode=local is set. Called on init() before any gateway start (#88). | ||
| Future<void> _repairConfigFile() async { | ||
| try { | ||
| final filesDir = await NativeBridge.getFilesDir(); | ||
| final configFile = File('$filesDir/rootfs/ubuntu/root/.openclaw/openclaw.json'); | ||
| if (!configFile.existsSync()) return; | ||
| final content = configFile.readAsStringSync(); | ||
| if (content.isEmpty) return; | ||
|
|
||
| Map<String, dynamic> config; | ||
| try { | ||
| config = Map<String, dynamic>.from(jsonDecode(content) as Map); | ||
| } catch (_) { | ||
| return; // Unparseable — _writeNodeAllowConfig will recreate it | ||
| } | ||
|
Comment on lines
+206
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the init-time repair recover from invalid JSON atomically.
Suggested fix- final content = configFile.readAsStringSync();
- if (content.isEmpty) return;
-
- Map<String, dynamic> config;
- try {
- config = Map<String, dynamic>.from(jsonDecode(content) as Map);
- } catch (_) {
- return; // Unparseable — _writeNodeAllowConfig will recreate it
- }
-
- bool modified = false;
+ final content = configFile.readAsStringSync();
+ bool modified = false;
+
+ Map<String, dynamic> config;
+ if (content.isEmpty) {
+ config = <String, dynamic>{};
+ modified = true;
+ } else {
+ try {
+ config = Map<String, dynamic>.from(jsonDecode(content) as Map);
+ } catch (_) {
+ config = <String, dynamic>{};
+ modified = true;
+ }
+ }
...
- if (modified) {
- configFile.writeAsStringSync(
- const JsonEncoder.withIndent(' ').convert(config),
- );
- }
+ if (modified) {
+ final tmpFile = File('${configFile.path}.tmp');
+ tmpFile.writeAsStringSync(
+ const JsonEncoder.withIndent(' ').convert(config),
+ );
+ tmpFile.renameSync(configFile.path);
+ }Also applies to: 247-250 🤖 Prompt for AI Agents |
||
|
|
||
| bool modified = false; | ||
|
|
||
| // Ensure gateway.mode=local (#93, #90) | ||
| config.putIfAbsent('gateway', () => <String, dynamic>{}); | ||
| final gw = config['gateway'] as Map<String, dynamic>; | ||
| if (!gw.containsKey('mode')) { | ||
| gw['mode'] = 'local'; | ||
| modified = true; | ||
| } | ||
|
|
||
| // Fix model entries: strings → objects (#83, #88) | ||
| final models = config['models'] as Map<String, dynamic>?; | ||
| if (models != null) { | ||
| final providers = models['providers'] as Map<String, dynamic>?; | ||
| if (providers != null) { | ||
| for (final entry in providers.values) { | ||
| if (entry is Map<String, dynamic>) { | ||
| final modelsList = entry['models']; | ||
| if (modelsList is List) { | ||
| for (int i = 0; i < modelsList.length; i++) { | ||
| if (modelsList[i] is String) { | ||
| modelsList[i] = {'id': modelsList[i]}; | ||
| modified = true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (modified) { | ||
| configFile.writeAsStringSync( | ||
| const JsonEncoder.withIndent(' ').convert(config), | ||
| ); | ||
| } | ||
| } catch (_) {} | ||
| } | ||
|
|
||
| /// Fix corrupted model entries: convert bare strings to {id: string} objects (#83, #88). | ||
| static void _repairModelEntries(Map<String, dynamic> config) { | ||
| final models = config['models'] as Map<String, dynamic>?; | ||
| if (models == null) return; | ||
| final providers = models['providers'] as Map<String, dynamic>?; | ||
| if (providers == null) return; | ||
| for (final entry in providers.values) { | ||
| if (entry is Map<String, dynamic>) { | ||
| final modelsList = entry['models']; | ||
| if (modelsList is List) { | ||
| entry['models'] = modelsList.map((m) { | ||
| if (m is String) return {'id': m}; | ||
| return m; | ||
| }).toList(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Read the actual gateway auth token from openclaw.json config file (#74, #82). | ||
| /// This is the source of truth — more reliable than regex-scraping stdout. | ||
| Future<String?> _readTokenFromConfig() async { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Malformed config can remain unrepaired due to swallowed parse errors.
At Line 1266, if JSON parsing fails, the empty catch at Line 1303 leaves
openclaw.jsonuntouched. That can preserve corruption and still missgateway.mode, which conflicts with the repair-on-startup goal.🔧 Proposed fix
🤖 Prompt for AI Agents