xworkmate-app/lib/runtime/codex_config_bridge.dart
2026-03-19 12:33:50 +08:00

415 lines
12 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'platform_environment.dart';
/// 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 {
static const String _managedBlockStart = '# BEGIN XWORKMATE MANAGED BLOCK';
static const String _managedBlockEnd = '# END XWORKMATE MANAGED BLOCK';
static const String _managedMcpBlockStart =
'# BEGIN XWORKMATE MANAGED MCP BLOCK';
static const String _managedMcpBlockEnd =
'# END XWORKMATE MANAGED MCP BLOCK';
final String codexHome;
CodexConfigBridge({String? codexHome})
: codexHome = codexHome ?? resolveCodexHomeDirectory();
/// 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');
final existingConfig = await configFile.exists()
? await configFile.readAsString()
: '';
final preserved = _stripManagedBlock(existingConfig).trimRight();
final managedBlock = _buildManagedBlock(
gatewayUrl: gatewayUrl,
apiKey: apiKey,
providerName: providerName,
defaultModel: defaultModel,
sandbox: sandbox,
approval: approval,
extraConfig: extraConfig,
);
final merged = preserved.isEmpty
? '$managedBlock\n'
: '$preserved\n\n$managedBlock\n';
await configFile.writeAsString(merged);
}
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();
}
String _buildManagedBlock({
required String gatewayUrl,
required String apiKey,
required String providerName,
required String defaultModel,
required CodexSandboxMode sandbox,
required CodexApprovalPolicy approval,
Map<String, String>? extraConfig,
}) {
final providerSection = _buildProviderSection(
providerName: providerName,
gatewayUrl: gatewayUrl,
apiKey: apiKey,
);
final config = StringBuffer();
config.writeln(_managedBlockStart);
config.writeln('# Generated by XWorkmate - AI Gateway Configuration');
config.writeln('# Last updated: ${DateTime.now().toIso8601String()}');
config.writeln();
config.writeln(providerSection);
config.writeln();
config.writeln('[model]');
config.writeln('model = "$defaultModel"');
config.writeln();
config.writeln('[approval_policy]');
config.writeln('policy = "${approval.value}"');
config.writeln();
config.writeln('[sandbox]');
config.writeln('mode = "${sandbox.value}"');
config.writeln();
config.writeln('[features]');
config.writeln('child_agents_md = true');
config.writeln('realtime = false');
config.writeln();
if (extraConfig != null && extraConfig.isNotEmpty) {
config.writeln('# Custom configuration');
for (final entry in extraConfig.entries) {
config.writeln('${entry.key} = "${entry.value}"');
}
config.writeln();
}
config.writeln(_managedBlockEnd);
return config.toString().trimRight();
}
String _stripManagedBlock(String content) {
return _stripBlock(content, _managedBlockStart, _managedBlockEnd);
}
String _stripManagedMcpBlock(String content) {
return _stripBlock(content, _managedMcpBlockStart, _managedMcpBlockEnd);
}
String _stripBlock(String content, String startMarker, String endMarker) {
if (content.isEmpty) {
return content;
}
var remaining = content;
while (true) {
final start = remaining.indexOf(startMarker);
if (start < 0) {
break;
}
final end = remaining.indexOf(endMarker, start);
if (end < 0) {
remaining = remaining.substring(0, start);
break;
}
remaining =
remaining.substring(0, start) +
remaining.substring(end + endMarker.length);
}
return remaining;
}
/// 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());
}
Future<void> configureManagedMcpServers({
required List<CodexMcpServer> servers,
}) async {
final configDir = Directory(codexHome);
if (!await configDir.exists()) {
await configDir.create(recursive: true);
}
final configFile = File('$codexHome/config.toml');
final existingConfig = await configFile.exists()
? await configFile.readAsString()
: '';
final preserved = _stripManagedMcpBlock(existingConfig).trimRight();
final managedBlock = _buildManagedMcpBlock(servers);
final merged = preserved.isEmpty
? '$managedBlock\n'
: '$preserved\n\n$managedBlock\n';
await configFile.writeAsString(merged);
}
String _buildManagedMcpBlock(List<CodexMcpServer> servers) {
final buffer = StringBuffer()
..writeln(_managedMcpBlockStart)
..writeln('# Generated by XWorkmate - Managed MCP Server Configuration')
..writeln('# Last updated: ${DateTime.now().toIso8601String()}')
..writeln();
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();
}
buffer.writeln(_managedMcpBlockEnd);
return buffer.toString().trimRight();
}
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 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 {},
});
}