-
-
Notifications
You must be signed in to change notification settings - Fork 488
proposal: enhance AI functionalities #1040
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||
| import 'package:meta/meta.dart'; | ||
|
|
||
| @immutable | ||
| class AiContextSnapshot { | ||
| const AiContextSnapshot({ | ||
| required this.title, | ||
| required this.scenario, | ||
| required this.blocks, | ||
| this.spiId, | ||
| this.updatedAtMs, | ||
| }); | ||
|
|
||
| final String title; | ||
| final String scenario; | ||
| final List<String> blocks; | ||
| final String? spiId; | ||
| final int? updatedAtMs; | ||
|
|
||
| AiContextSnapshot copyWith({ | ||
| String? title, | ||
| String? scenario, | ||
| List<String>? blocks, | ||
| String? spiId, | ||
| int? updatedAtMs, | ||
| }) { | ||
| return AiContextSnapshot( | ||
| title: title ?? this.title, | ||
| scenario: scenario ?? this.scenario, | ||
| blocks: blocks ?? this.blocks, | ||
| spiId: spiId ?? this.spiId, | ||
| updatedAtMs: updatedAtMs ?? this.updatedAtMs, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| final aiContextProvider = NotifierProvider<AiContextNotifier, AiContextSnapshot>(AiContextNotifier.new); | ||
|
|
||
| class AiContextNotifier extends Notifier<AiContextSnapshot> { | ||
| @override | ||
| AiContextSnapshot build() { | ||
| return const AiContextSnapshot( | ||
| title: 'Ask AI', | ||
| scenario: 'general', | ||
| blocks: [], | ||
| updatedAtMs: 0, | ||
| ); | ||
| } | ||
|
|
||
| void setContext({ | ||
| required String title, | ||
| required String scenario, | ||
| required List<String> blocks, | ||
| String? spiId, | ||
| }) { | ||
| state = AiContextSnapshot( | ||
| title: title, | ||
| scenario: scenario, | ||
| blocks: blocks, | ||
| spiId: spiId, | ||
| updatedAtMs: DateTime.now().millisecondsSinceEpoch, | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,186 @@ | ||||||
| import 'package:meta/meta.dart'; | ||||||
| import 'package:server_box/data/model/server/server_private_info.dart'; | ||||||
|
|
||||||
| @immutable | ||||||
| enum AiRedactionMode { | ||||||
| placeholder, | ||||||
| none, | ||||||
| } | ||||||
|
|
||||||
| @immutable | ||||||
| enum AiCommandRisk { | ||||||
| low, | ||||||
| medium, | ||||||
| high, | ||||||
| } | ||||||
|
|
||||||
| extension AiCommandRiskX on AiCommandRisk { | ||||||
| static AiCommandRisk? tryParse(Object? raw) { | ||||||
| if (raw is! String) return null; | ||||||
| final s = raw.trim().toLowerCase(); | ||||||
| return switch (s) { | ||||||
| 'low' => AiCommandRisk.low, | ||||||
| 'medium' => AiCommandRisk.medium, | ||||||
| 'high' => AiCommandRisk.high, | ||||||
| _ => null, | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| AiCommandRisk max(AiCommandRisk other) => index >= other.index ? this : other; | ||||||
| } | ||||||
|
|
||||||
| abstract final class AiSafety { | ||||||
| const AiSafety._(); | ||||||
|
|
||||||
| static String redact( | ||||||
| String input, { | ||||||
| AiRedactionMode mode = AiRedactionMode.placeholder, | ||||||
| Spi? spi, | ||||||
| }) { | ||||||
| if (mode == AiRedactionMode.none) return input; | ||||||
| if (input.isEmpty) return input; | ||||||
|
|
||||||
| var out = input; | ||||||
|
|
||||||
| out = _redactPrivateKeyBlocks(out); | ||||||
| out = _redactBearerTokens(out); | ||||||
| out = _redactApiKeys(out); | ||||||
|
|
||||||
| if (spi != null) { | ||||||
| out = _redactSpiIdentity(out, spi); | ||||||
| } | ||||||
|
|
||||||
| return out; | ||||||
| } | ||||||
|
|
||||||
| static List<String> redactBlocks( | ||||||
| List<String> blocks, { | ||||||
| AiRedactionMode mode = AiRedactionMode.placeholder, | ||||||
| Spi? spi, | ||||||
| }) { | ||||||
| if (blocks.isEmpty) return const []; | ||||||
| return [ | ||||||
| for (final b in blocks) redact(b, mode: mode, spi: spi), | ||||||
| ]; | ||||||
| } | ||||||
|
|
||||||
| static AiCommandRisk classifyRisk(String command) { | ||||||
| final raw = command.trim(); | ||||||
| if (raw.isEmpty) return AiCommandRisk.low; | ||||||
|
|
||||||
| final s = raw.toLowerCase(); | ||||||
|
|
||||||
| // High-risk destructive patterns. | ||||||
| if (_rxForkBomb.hasMatch(s)) return AiCommandRisk.high; | ||||||
| if (_rxMkfs.hasMatch(s)) return AiCommandRisk.high; | ||||||
| if (_rxDdToBlockDevice.hasMatch(s)) return AiCommandRisk.high; | ||||||
| if (_rxRmRf.hasMatch(s)) return AiCommandRisk.high; | ||||||
| if (_rxChmodChownRoot.hasMatch(s)) return AiCommandRisk.high; | ||||||
| if (_rxIptablesFlush.hasMatch(s) || _rxNftFlush.hasMatch(s)) return AiCommandRisk.high; | ||||||
| if (_rxDockerSystemPruneAll.hasMatch(s) || _rxPodmanSystemPruneAll.hasMatch(s)) return AiCommandRisk.high; | ||||||
|
|
||||||
| // Medium-risk operational patterns. | ||||||
| if (_rxRebootShutdown.hasMatch(s)) return AiCommandRisk.medium; | ||||||
| if (_rxSystemctlStopRestart.hasMatch(s)) return AiCommandRisk.medium; | ||||||
| if (_rxKill.hasMatch(s)) return AiCommandRisk.medium; | ||||||
| if (_rxDockerStopRm.hasMatch(s) || _rxPodmanStopRm.hasMatch(s)) return AiCommandRisk.medium; | ||||||
|
|
||||||
| return AiCommandRisk.low; | ||||||
| } | ||||||
|
|
||||||
| static String _redactPrivateKeyBlocks(String input) { | ||||||
| return input.replaceAllMapped(_rxPrivateKeyBlock, (_) => '<PRIVATE_KEY_BLOCK>'); | ||||||
| } | ||||||
|
|
||||||
| static String _redactBearerTokens(String input) { | ||||||
| var out = input; | ||||||
| out = out.replaceAllMapped( | ||||||
| _rxAuthorizationBearer, | ||||||
| (m) => '${m.group(1)}Bearer <TOKEN>', | ||||||
| ); | ||||||
| out = out.replaceAllMapped( | ||||||
| _rxBearerInline, | ||||||
| (m) => 'Bearer <TOKEN>', | ||||||
| ); | ||||||
| return out; | ||||||
| } | ||||||
|
|
||||||
| static String _redactApiKeys(String input) { | ||||||
| // Keep it conservative; only match common patterns with clear prefixes. | ||||||
| var out = input; | ||||||
| out = out.replaceAllMapped(_rxOpenAiKey, (_) => '<API_KEY>'); | ||||||
| out = out.replaceAllMapped(_rxAwsAccessKeyId, (_) => '<AWS_ACCESS_KEY_ID>'); | ||||||
| return out; | ||||||
| } | ||||||
|
|
||||||
| static String _redactSpiIdentity(String input, Spi spi) { | ||||||
| var out = input; | ||||||
|
|
||||||
| final ip = spi.ip; | ||||||
| final user = spi.user; | ||||||
| final port = spi.port; | ||||||
|
|
||||||
| if (user.isNotEmpty && ip.isNotEmpty) { | ||||||
| out = out.replaceAll('$user@$ip:$port', '<USER_AT_HOST_PORT>'); | ||||||
| out = out.replaceAll('$user@$ip', '<USER_AT_HOST>'); | ||||||
| } | ||||||
|
|
||||||
| if (ip.isNotEmpty) { | ||||||
| out = out.replaceAll(ip, '<IP>'); | ||||||
| } | ||||||
|
|
||||||
| if (user.isNotEmpty) { | ||||||
| out = out.replaceAll(user, '<USER>'); | ||||||
| } | ||||||
|
|
||||||
| return out; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| final _rxPrivateKeyBlock = RegExp( | ||||||
| r'-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----', | ||||||
| multiLine: true, | ||||||
| ); | ||||||
|
|
||||||
| final _rxAuthorizationBearer = RegExp( | ||||||
| r'(authorization\s*:\s*)bearer\s+[^\s\n\r]+', | ||||||
| multiLine: true, | ||||||
| caseSensitive: false, | ||||||
| ); | ||||||
|
|
||||||
| final _rxBearerInline = RegExp( | ||||||
| r'\bbearer\s+[^\s\n\r]+', | ||||||
| caseSensitive: false, | ||||||
| ); | ||||||
|
|
||||||
| final _rxOpenAiKey = RegExp(r'\bsk-[A-Za-z0-9]{16,}\b'); | ||||||
|
|
||||||
| final _rxAwsAccessKeyId = RegExp(r'\bAKIA[0-9A-Z]{16}\b'); | ||||||
|
|
||||||
| final _rxForkBomb = RegExp(r':\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:'); | ||||||
|
|
||||||
| final _rxMkfs = RegExp(r'\bmkfs(\.[a-z0-9_-]+)?\b'); | ||||||
|
|
||||||
| final _rxDdToBlockDevice = RegExp(r'\bdd\b[^\n\r]*\bof\s*=\s*/dev/'); | ||||||
|
|
||||||
| final _rxRmRf = RegExp(r'\brm\b[^\n\r]*\s-[a-z-]*r[a-z-]*f[a-z-]*\b'); | ||||||
|
Contributor
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.
The pattern 🔧 Suggested fix to match both flag orderings-final _rxRmRf = RegExp(r'\brm\b[^\n\r]*\s-[a-z-]*r[a-z-]*f[a-z-]*\b');
+// Match rm with both -r/-R and -f/--force in any order
+final _rxRmRf = RegExp(
+ r'\brm\b[^\n\r]*(?=.*\s-[a-z-]*r)(?=.*\s-[a-z-]*f)',
+);Or use two separate checks in // Check for rm with both recursive and force flags
if (_rxRmRecursive.hasMatch(s) && _rxRmForce.hasMatch(s)) return AiCommandRisk.high;🤖 Prompt for AI Agents |
||||||
|
|
||||||
| final _rxChmodChownRoot = RegExp(r'\b(chmod|chown)\b[^\n\r]*\s-\w*r\w*\b[^\n\r]*\s/\b'); | ||||||
|
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. The current regex for detecting recursive
Suggested change
|
||||||
|
|
||||||
| final _rxIptablesFlush = RegExp(r'\biptables\b[^\n\r]*(\s-(f|x)\b|\s--flush\b)'); | ||||||
|
|
||||||
| final _rxNftFlush = RegExp(r'\bnft\b[^\n\r]*\bflush\s+ruleset\b'); | ||||||
|
|
||||||
| final _rxDockerSystemPruneAll = RegExp(r'\bdocker\b[^\n\r]*\bsystem\s+prune\b[^\n\r]*\s-a\b'); | ||||||
|
|
||||||
| final _rxPodmanSystemPruneAll = RegExp(r'\bpodman\b[^\n\r]*\bsystem\s+prune\b[^\n\r]*\s-a\b'); | ||||||
|
|
||||||
| final _rxRebootShutdown = RegExp(r'\b(reboot|poweroff|halt|shutdown)\b'); | ||||||
|
|
||||||
| final _rxSystemctlStopRestart = RegExp(r'\bsystemctl\b[^\n\r]*\b(stop|restart)\b'); | ||||||
|
|
||||||
| final _rxKill = RegExp(r'\b(kill|killall|pkill)\b'); | ||||||
|
|
||||||
| final _rxDockerStopRm = RegExp(r'\bdocker\b[^\n\r]*\b(stop|rm)\b'); | ||||||
|
|
||||||
| final _rxPodmanStopRm = RegExp(r'\bpodman\b[^\n\r]*\b(stop|rm)\b'); | ||||||
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.
🔴 Risk classification regex fails to detect
rm -frcommand variantThe
_rxRmRfregex pattern inai_safety.dartfails to detect the commonrm -frcommand variant (where-fcomes before-r), allowing it to be classified as low risk instead of high risk.Click to expand
Analysis
The regex pattern at line 166 is:
This pattern requires
rto appear beforefin the flags ([a-z-]*r[a-z-]*f[a-z-]*).Commands that match (high risk detected):
rm -rf /✓rm -rfi /✓Commands that DON'T match (incorrectly classified as low risk):
rm -fr /✗rm -fR /✗ (though case is lowered,-frorder still fails)Impact
rm -fris equally common and dangerous asrm -rf. Users could execute a destructive command suggested by AI without the appropriate high-risk countdown confirmation dialog, sinceAiSafety.classifyRisk('rm -fr /')returnsAiCommandRisk.lowinstead ofAiCommandRisk.high.Recommendation
Modify the regex to match either flag order, e.g.:
Or use two patterns:
Recommendation: Change the regex to detect both
rm -rfandrm -frvariants:RegExp(r'\brm\b[^\n\r]*\s-[a-z-]*(rf|fr)[a-z-]*\b')Was this helpful? React with 👍 or 👎 to provide feedback.