xworkmate-app/lib/runtime/codex_config_bridge.dart
Haitao Pan e26ffb2116 feat: integrate Codex CLI as built-in code agent
- Add CodexRuntime for process management and JSON-RPC communication
- Add CodexConfigBridge for AI Gateway configuration
- Add ModeSwitcher for OpenClaw Gateway mode switching (local/remote/offline)
- Add AgentRegistry for agent registration and discovery
- Add RuntimeCoordinator for unified coordination
- Add Rust FFI bindings for native integration
- Add comprehensive test coverage

Phase 1-4 features:
- Configuration bridging to AI Gateway
- Mode switching between local/remote/offline
- Agent registration protocol
- Cloud memory sync capability
- Offline fallback support

CI/CD:
- GitHub Actions workflow for Rust FFI build
- Build scripts for macOS universal binary
- Integration with Flutter build process

Co-authored-by: Codex CLI Integration <codex@openai.com>
2026-03-14 00:10:27 +08:00

320 lines
8.7 KiB
Dart

import 'dart:convert';import 'dart:io';
/// Bridge for generating Codex configuration files.
///
/// This class generates `~/.codex/config.toml` and `~/.codex/auth.json`
/// to configure Codex CLI to use XWorkmate's AI Gateway.
class CodexConfigBridge {
final String codexHome;
CodexConfigBridge({String? codexHome})
: codexHome = codexHome ??
Platform.environment['CODEX_HOME'] ??
'${Platform.environment['HOME']}/.codex';
/// Generate config.toml to use XWorkmate AI Gateway.
Future<void> configureForGateway({
required String gatewayUrl,
required String apiKey,
String providerName = 'xworkmate',
String defaultModel = 'gpt-4.1',
CodexSandboxMode sandbox = CodexSandboxMode.workspaceWrite,
CodexApprovalPolicy approval = CodexApprovalPolicy.suggest,
Map<String, String>? extraConfig,
}) async {
final configDir = Directory(codexHome);
if (!await configDir.exists()) {
await configDir.create(recursive: true);
}
final configFile = File('$codexHome/config.toml');
// Read existing config to preserve non-conflicting settings
String existingConfig = '';
if (await configFile.exists()) {
existingConfig = await configFile.readAsString();
}
// Check if our provider already exists
final providerSection = _buildProviderSection(
providerName: providerName,
gatewayUrl: gatewayUrl,
apiKey: apiKey,
);
final config = StringBuffer();
// Add provider section
config.writeln('# Generated by XWorkmate - AI Gateway Configuration');
config.writeln('# Last updated: ${DateTime.now().toIso8601String()}');
config.writeln();
config.writeln(providerSection);
config.writeln();
// Model configuration
config.writeln('[model]');
config.writeln('model = "$defaultModel"');
config.writeln();
// Approval policy
config.writeln('[approval_policy]');
config.writeln('policy = "${approval.value}"');
config.writeln();
// Sandbox mode
config.writeln('[sandbox]');
config.writeln('mode = "${sandbox.value}"');
config.writeln();
// Features
config.writeln('[features]');
config.writeln('child_agents_md = true');
config.writeln('realtime = false');
config.writeln();
// Extra config
if (extraConfig != null && extraConfig.isNotEmpty) {
config.writeln('# Custom configuration');
for (final entry in extraConfig.entries) {
config.writeln('${entry.key} = "${entry.value}"');
}
}
await configFile.writeAsString(config.toString());
}
String _buildProviderSection({
required String providerName,
required String gatewayUrl,
required String apiKey,
}) {
final buffer = StringBuffer();
buffer.writeln('[model_providers.$providerName]');
buffer.writeln('name = "XWorkmate AI Gateway"');
buffer.writeln('base_url = "$gatewayUrl"');
// Use experimental_bearer_token for API key
if (apiKey.isNotEmpty) {
buffer.writeln('experimental_bearer_token = "$apiKey"');
}
buffer.writeln('wire_api = "responses"');
buffer.writeln('supports_websockets = false');
return buffer.toString();
}
/// Generate auth.json for ChatGPT OAuth authentication.
Future<void> configureAuth({
required String accessToken,
String? refreshToken,
DateTime? expiresAt,
String? email,
String? plan,
}) async {
final authFile = File('$codexHome/auth.json');
final auth = <String, dynamic>{
'access_token': accessToken,
'last_refresh': DateTime.now().toIso8601String(),
};
if (refreshToken != null && refreshToken.isNotEmpty) {
auth['refresh_token'] = refreshToken;
}
if (expiresAt != null) {
auth['expires_at'] = expiresAt.millisecondsSinceEpoch;
}
if (email != null && email.isNotEmpty) {
auth['email'] = email;
}
if (plan != null && plan.isNotEmpty) {
auth['plan'] = plan;
}
await authFile.writeAsString(
JsonEncoder.withIndent(' ').convert(auth),
);
}
/// Configure MCP servers for Codex.
Future<void> configureMcpServers({
required List<CodexMcpServer> servers,
bool append = true,
}) async {
final configFile = File('$codexHome/config.toml');
String existingConfig = '';
if (await configFile.exists()) {
existingConfig = await configFile.readAsString();
}
final buffer = StringBuffer();
if (append && existingConfig.isNotEmpty) {
buffer.writeln(existingConfig);
buffer.writeln();
}
buffer.writeln('# MCP Servers');
for (final server in servers) {
buffer.writeln('[mcp_servers.${server.name}]');
buffer.writeln('command = "${server.command}"');
if (server.args.isNotEmpty) {
buffer.writeln('args = ${_formatTomlArray(server.args)}');
}
if (server.env.isNotEmpty) {
buffer.writeln('[mcp_servers.${server.name}.env]');
for (final entry in server.env.entries) {
buffer.writeln('${entry.key} = "${entry.value}"');
}
}
buffer.writeln();
}
await configFile.writeAsString(buffer.toString());
}
String _formatTomlArray(List<String> items) {
if (items.isEmpty) return '[]';
if (items.length == 1) return '["${items[0]}"]';
return '[${items.map((s) => '"$s"').join(', ')}]';
}
/// Generate configuration for OpenClaw Gateway integration.
Future<void> configureOpenClawGateway({
required String gatewayUrl,
required String token,
String providerName = 'openclaw',
}) async {
await configureForGateway(
gatewayUrl: gatewayUrl,
apiKey: token,
providerName: providerName,
);
// Add MCP server for OpenClaw
await configureMcpServers(
servers: [
CodexMcpServer(
name: 'openclaw',
command: 'openclaw-mcp',
args: ['--gateway', gatewayUrl],
env: {'OPENCLAW_TOKEN': token},
),
],
append: true,
);
}
/// Check if Codex configuration exists.
Future<bool> hasConfig() async {
final configFile = File('$codexHome/config.toml');
return configFile.exists();
}
/// Check if auth.json exists.
Future<bool> hasAuth() async {
final authFile = File('$codexHome/auth.json');
return authFile.exists();
}
/// Read current model provider configuration.
Future<Map<String, dynamic>?> readProviderConfig(String providerName) async {
final configFile = File('$codexHome/config.toml');
if (!await configFile.exists()) {
return null;
}
final content = await configFile.readAsString();
return _parseTomlSection(content, 'model_providers.$providerName');
}
/// Parse a TOML section into a Map.
Map<String, dynamic>? _parseTomlSection(String content, String section) {
final lines = content.split('\n');
final result = <String, dynamic>{};
bool inSection = false;
for (final line in lines) {
final trimmed = line.trim();
if (trimmed.isEmpty || trimmed.startsWith('#')) continue;
if (trimmed.startsWith('[')) {
final sectionName = trimmed.substring(1, trimmed.length - 1);
inSection = sectionName == section;
continue;
}
if (inSection) {
final eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
final key = trimmed.substring(0, eqIndex).trim();
var value = trimmed.substring(eqIndex + 1).trim();
// Remove quotes
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length - 1);
}
result[key] = value;
}
}
}
return inSection && result.isNotEmpty ? result : null;
}
/// Clear all Codex configuration.
Future<void> clearConfig() async {
final configDir = Directory(codexHome);
if (await configDir.exists()) {
await configDir.delete(recursive: true);
}
}
}
/// Codex sandbox mode for configuration.
enum CodexSandboxMode {
readOnly('read-only'),
workspaceWrite('workspace-write'),
dangerFullAccess('danger-full-access');
final String value;
const CodexSandboxMode(this.value);
}
/// Codex approval policy for configuration.
enum CodexApprovalPolicy {
suggest('suggest'),
autoEdit('auto-edit'),
fullAuto('full-auto');
final String value;
const CodexApprovalPolicy(this.value);
}
/// MCP server configuration for Codex.
class CodexMcpServer {
final String name;
final String command;
final List<String> args;
final Map<String, String> env;
const CodexMcpServer({
required this.name,
required this.command,
this.args = const [],
this.env = const {},
});
}