|
|
|
|
@ -11,10 +11,14 @@ param(
|
|
|
|
|
[string]$HubFallbackUrls = "",
|
|
|
|
|
[string]$PairingCode = "",
|
|
|
|
|
[string]$MachineName = "",
|
|
|
|
|
[string]$OpsMcpUrl = "",
|
|
|
|
|
[string]$OpsMcpToken = "",
|
|
|
|
|
[string]$OpsMcpServerName = "nodedc_tasker",
|
|
|
|
|
[string]$AppMcpServersJson = "",
|
|
|
|
|
[switch]$NoAutostart,
|
|
|
|
|
[switch]$NoFirewall,
|
|
|
|
|
[switch]$NoCodexLogin,
|
|
|
|
|
[switch]$EnableWatchdog,
|
|
|
|
|
[switch]$EnableWatchdog = $true,
|
|
|
|
|
[switch]$Confirmed
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@ -215,6 +219,10 @@ function Request-Elevation {
|
|
|
|
|
if ($HubFallbackUrls) { $args += @('-HubFallbackUrls', "`"$HubFallbackUrls`"") }
|
|
|
|
|
if ($PairingCode) { $args += @('-PairingCode', "`"$PairingCode`"") }
|
|
|
|
|
if ($MachineName) { $args += @('-MachineName', "`"$MachineName`"") }
|
|
|
|
|
if ($OpsMcpUrl) { $args += @('-OpsMcpUrl', "`"$OpsMcpUrl`"") }
|
|
|
|
|
if ($OpsMcpToken) { $args += @('-OpsMcpToken', "`"$OpsMcpToken`"") }
|
|
|
|
|
if ($OpsMcpServerName) { $args += @('-OpsMcpServerName', "`"$OpsMcpServerName`"") }
|
|
|
|
|
if ($AppMcpServersJson) { $args += @('-AppMcpServersJson', (ConvertTo-PsSingleQuoted $AppMcpServersJson)) }
|
|
|
|
|
|
|
|
|
|
Write-Host 'Opening an Administrator PowerShell window. Approve the Windows UAC prompt and continue there.' -ForegroundColor Yellow
|
|
|
|
|
Start-Process -FilePath 'powershell.exe' -ArgumentList ($args -join ' ') -Verb RunAs -Wait
|
|
|
|
|
@ -506,6 +514,185 @@ function Repair-CodexMcpConfig {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ConvertTo-TomlString {
|
|
|
|
|
param([string]$Value)
|
|
|
|
|
$escaped = ([string]$Value).Replace('\', '\\').Replace('"', '\"').Replace("`r", '\r').Replace("`n", '\n')
|
|
|
|
|
return '"' + $escaped + '"'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ConvertTo-TomlKey {
|
|
|
|
|
param([string]$Key)
|
|
|
|
|
$text = ([string]$Key).Trim()
|
|
|
|
|
if ($text -match '^[A-Za-z0-9_-]+$') { return $text }
|
|
|
|
|
return ConvertTo-TomlString $text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-SafeMcpServerName {
|
|
|
|
|
param([string]$ServerName)
|
|
|
|
|
$candidate = ([string]$ServerName).Trim()
|
|
|
|
|
if ($candidate -match '^[A-Za-z0-9_-]+$') { return $candidate }
|
|
|
|
|
return 'nodedc_tasker'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Remove-CodexMcpServerSections {
|
|
|
|
|
param(
|
|
|
|
|
[string]$Raw,
|
|
|
|
|
[string]$ServerName
|
|
|
|
|
)
|
|
|
|
|
if (-not $Raw) { return "" }
|
|
|
|
|
|
|
|
|
|
$mainHeader = "[mcp_servers.$ServerName]"
|
|
|
|
|
$headersHeader = "[mcp_servers.$ServerName.headers]"
|
|
|
|
|
$httpHeadersHeader = "[mcp_servers.$ServerName.http_headers]"
|
|
|
|
|
$lines = $Raw -split '\r?\n'
|
|
|
|
|
$result = New-Object System.Collections.Generic.List[string]
|
|
|
|
|
$skip = $false
|
|
|
|
|
$inMcpServers = $false
|
|
|
|
|
|
|
|
|
|
foreach ($line in $lines) {
|
|
|
|
|
$headerKey = Normalize-TomlHeader $line
|
|
|
|
|
if ($headerKey.StartsWith('[')) {
|
|
|
|
|
$skip = ($headerKey -eq $mainHeader -or $headerKey -eq $headersHeader -or $headerKey -eq $httpHeadersHeader)
|
|
|
|
|
$inMcpServers = ($headerKey -eq '[mcp_servers]')
|
|
|
|
|
if ($skip) { continue }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$activeLine = Remove-TomlComment $line
|
|
|
|
|
if ($inMcpServers -and $activeLine -match ("^\s*" + [regex]::Escape($ServerName) + "\s*=")) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if (-not $skip) {
|
|
|
|
|
$result.Add($line) | Out-Null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ($result -join ([Environment]::NewLine)).TrimEnd()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Ensure-McpServerConfig {
|
|
|
|
|
param(
|
|
|
|
|
[string]$ServerName,
|
|
|
|
|
[string]$McpUrl,
|
|
|
|
|
[object]$Headers,
|
|
|
|
|
[bool]$Required = $false,
|
|
|
|
|
[int]$StartupTimeoutSec = 20,
|
|
|
|
|
[int]$ToolTimeoutSec = 60,
|
|
|
|
|
[string]$Label = "AI Workspace"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$serverName = Get-SafeMcpServerName $ServerName
|
|
|
|
|
$mcpUrl = ([string]$McpUrl).Trim()
|
|
|
|
|
if (-not $serverName -or -not $mcpUrl) { return }
|
|
|
|
|
|
|
|
|
|
$configDir = Join-Path $env:USERPROFILE '.codex'
|
|
|
|
|
$configPath = Join-Path $configDir 'config.toml'
|
|
|
|
|
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
|
|
|
|
|
|
|
|
|
|
$raw = ""
|
|
|
|
|
if (Test-Path $configPath) {
|
|
|
|
|
try {
|
|
|
|
|
$raw = Get-Content -Path $configPath -Raw -Encoding UTF8
|
|
|
|
|
} catch {
|
|
|
|
|
Write-Warn "Could not read Codex config for MCP setup: $($_.Exception.Message)"
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$next = Remove-CodexMcpServerSections -Raw $raw -ServerName $serverName
|
|
|
|
|
$requiredValue = if ($Required) { "true" } else { "false" }
|
|
|
|
|
$startupTimeout = [Math]::Max(1, [int]$StartupTimeoutSec)
|
|
|
|
|
$toolTimeout = [Math]::Max(1, [int]$ToolTimeoutSec)
|
|
|
|
|
$sectionLines = New-Object System.Collections.Generic.List[string]
|
|
|
|
|
$sectionLines.Add("") | Out-Null
|
|
|
|
|
$sectionLines.Add("[mcp_servers.$serverName]") | Out-Null
|
|
|
|
|
$sectionLines.Add("url = $(ConvertTo-TomlString $mcpUrl)") | Out-Null
|
|
|
|
|
$sectionLines.Add("enabled = true") | Out-Null
|
|
|
|
|
$sectionLines.Add("required = $requiredValue") | Out-Null
|
|
|
|
|
$sectionLines.Add("startup_timeout_sec = $startupTimeout") | Out-Null
|
|
|
|
|
$sectionLines.Add("tool_timeout_sec = $toolTimeout") | Out-Null
|
|
|
|
|
|
|
|
|
|
$headerLines = New-Object System.Collections.Generic.List[string]
|
|
|
|
|
if ($Headers) {
|
|
|
|
|
foreach ($property in $Headers.PSObject.Properties) {
|
|
|
|
|
$headerName = ([string]$property.Name).Trim()
|
|
|
|
|
$headerValue = ([string]$property.Value).Trim()
|
|
|
|
|
if (-not $headerName -or -not $headerValue) { continue }
|
|
|
|
|
$headerLines.Add("$(ConvertTo-TomlKey $headerName) = $(ConvertTo-TomlString $headerValue)") | Out-Null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ($headerLines.Count -gt 0) {
|
|
|
|
|
$sectionLines.Add("") | Out-Null
|
|
|
|
|
$sectionLines.Add("[mcp_servers.$serverName.http_headers]") | Out-Null
|
|
|
|
|
foreach ($line in $headerLines) { $sectionLines.Add($line) | Out-Null }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$section = $sectionLines -join ([Environment]::NewLine)
|
|
|
|
|
$next = ($next + $section).TrimStart()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (Test-Path $configPath) {
|
|
|
|
|
$backupPath = "$configPath.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
|
|
|
|
Copy-Item -Path $configPath -Destination $backupPath -Force
|
|
|
|
|
Write-Ok "Codex config backup created: $backupPath"
|
|
|
|
|
}
|
|
|
|
|
Set-Content -Path $configPath -Value $next -Encoding UTF8
|
|
|
|
|
Write-Ok "Codex MCP configured for $Label ($serverName)."
|
|
|
|
|
} catch {
|
|
|
|
|
Write-Warn "Could not update Codex config for MCP server $serverName`: $($_.Exception.Message)"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Ensure-AppMcpConfigs {
|
|
|
|
|
$rawJson = ([string]$AppMcpServersJson).Trim()
|
|
|
|
|
if (-not $rawJson) { return }
|
|
|
|
|
try {
|
|
|
|
|
$servers = $rawJson | ConvertFrom-Json
|
|
|
|
|
} catch {
|
|
|
|
|
Write-Warn "Could not parse AI Workspace app MCP manifest: $($_.Exception.Message)"
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if ($null -eq $servers) { return }
|
|
|
|
|
if ($servers -isnot [System.Array]) { $servers = @($servers) }
|
|
|
|
|
|
|
|
|
|
foreach ($server in $servers) {
|
|
|
|
|
if ($null -eq $server) { continue }
|
|
|
|
|
$serverName = [string]$server.serverName
|
|
|
|
|
$mcpUrl = [string]$server.url
|
|
|
|
|
$headers = $server.httpHeaders
|
|
|
|
|
if ($null -eq $headers) { $headers = $server.headers }
|
|
|
|
|
$label = [string]$server.appTitle
|
|
|
|
|
if (-not $label) { $label = [string]$server.appId }
|
|
|
|
|
if (-not $label) { $label = "AI Workspace" }
|
|
|
|
|
$startupTimeout = 20
|
|
|
|
|
$toolTimeout = 60
|
|
|
|
|
try { if ($server.startupTimeoutSec) { $startupTimeout = [int]$server.startupTimeoutSec } } catch {}
|
|
|
|
|
try { if ($server.toolTimeoutSec) { $toolTimeout = [int]$server.toolTimeoutSec } } catch {}
|
|
|
|
|
Ensure-McpServerConfig `
|
|
|
|
|
-ServerName $serverName `
|
|
|
|
|
-McpUrl $mcpUrl `
|
|
|
|
|
-Headers $headers `
|
|
|
|
|
-Required ($server.required -eq $true) `
|
|
|
|
|
-StartupTimeoutSec $startupTimeout `
|
|
|
|
|
-ToolTimeoutSec $toolTimeout `
|
|
|
|
|
-Label $label
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Ensure-OpsMcpConfig {
|
|
|
|
|
$mcpUrl = ([string]$OpsMcpUrl).Trim()
|
|
|
|
|
$mcpToken = ([string]$OpsMcpToken).Trim()
|
|
|
|
|
if (-not $mcpUrl -or -not $mcpToken) { return }
|
|
|
|
|
|
|
|
|
|
$serverName = Get-SafeMcpServerName $OpsMcpServerName
|
|
|
|
|
$authorization = "Bearer $mcpToken"
|
|
|
|
|
$headers = [pscustomobject]@{
|
|
|
|
|
Authorization = $authorization
|
|
|
|
|
Accept = "application/json"
|
|
|
|
|
"MCP-Protocol-Version" = "2025-06-18"
|
|
|
|
|
}
|
|
|
|
|
Ensure-McpServerConfig -ServerName $serverName -McpUrl $mcpUrl -Headers $headers -Required $false -StartupTimeoutSec 20 -ToolTimeoutSec 60 -Label "AI Workspace Ops"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Get-WorkspacePath {
|
|
|
|
|
$workspacePath = Get-DefaultWorkspaceCandidate
|
|
|
|
|
Write-Ok "Codex workspace: $workspacePath"
|
|
|
|
|
@ -863,24 +1050,62 @@ function buildPrompt(payload) {
|
|
|
|
|
const userMessage = String(payload?.message || '').trim()
|
|
|
|
|
const history = payload?.resume ? [] : normalizeHistory(payload?.history || [])
|
|
|
|
|
const isNdcAgentCore = String(context.modeId || '').trim() === 'ndc-agent-core'
|
|
|
|
|
const isOpsMode = String(context.modeId || '').trim() === 'ops'
|
|
|
|
|
const guardInstruction = String(context.guardInstruction || payload?.guardInstruction || '').trim()
|
|
|
|
|
const missingContext = Array.isArray(context.missingContext)
|
|
|
|
|
? context.missingContext.map((item) => String(item || '').trim()).filter(Boolean)
|
|
|
|
|
: []
|
|
|
|
|
const contextReady = context.contextReady !== false && missingContext.length === 0
|
|
|
|
|
const engineContext = context.engineContext && typeof context.engineContext === 'object'
|
|
|
|
|
? context.engineContext
|
|
|
|
|
: context.contexts?.engine && typeof context.contexts.engine === 'object'
|
|
|
|
|
? context.contexts.engine
|
|
|
|
|
: {}
|
|
|
|
|
const opsContext = context.opsContext && typeof context.opsContext === 'object'
|
|
|
|
|
? context.opsContext
|
|
|
|
|
: context.contexts?.ops && typeof context.contexts.ops === 'object'
|
|
|
|
|
? context.contexts.ops
|
|
|
|
|
: {}
|
|
|
|
|
const ndcAgentMcpApiBaseUrl = deriveNdcAgentMcpApiBaseUrl(context)
|
|
|
|
|
const lines = [
|
|
|
|
|
'You are connected to NODE.DC AI Workspace through AI Workspace Bridge.',
|
|
|
|
|
'',
|
|
|
|
|
'Context:',
|
|
|
|
|
`- active surface: ${context.surface || 'unknown'}`,
|
|
|
|
|
`- source surface: ${context.sourceSurface || context.surface || 'unknown'}`,
|
|
|
|
|
`- mode: ${context.modeTitle || context.modeId || 'unknown'}`,
|
|
|
|
|
`- workflow: ${context.workflowTitle || context.workflowId || 'unknown'}`,
|
|
|
|
|
`- agent node: ${context.agentNodeTitle || context.agentNodeId || 'unknown'}`,
|
|
|
|
|
`- role: ${context.roleTitle || context.roleId || 'unknown'}`,
|
|
|
|
|
`- workspace: ${context.workspacePath || payload?.workspacePath || CODEX_CWD}`,
|
|
|
|
|
`- context ready: ${contextReady ? 'yes' : 'no'}`,
|
|
|
|
|
...missingContext.length ? [`- missing context: ${missingContext.join(', ')}`] : [],
|
|
|
|
|
`- access mode: ${context.accessMode || 'chat'}`,
|
|
|
|
|
'',
|
|
|
|
|
'Cross-platform contexts:',
|
|
|
|
|
`- Engine workflow: ${engineContext.workflowTitle || engineContext.workflowId || 'not selected'}`,
|
|
|
|
|
`- Engine agent node: ${engineContext.agentNodeTitle || engineContext.agentNodeId || 'not selected'}`,
|
|
|
|
|
`- Engine role: ${engineContext.roleTitle || engineContext.roleId || 'not selected'}`,
|
|
|
|
|
`- Ops workspace: ${opsContext.opsWorkspaceTitle || opsContext.opsWorkspaceSlug || opsContext.opsWorkspaceId || 'not selected'}`,
|
|
|
|
|
`- Ops project: ${opsContext.opsProjectTitle || opsContext.opsProjectIdentifier || opsContext.opsProjectId || 'not selected'}`,
|
|
|
|
|
'- the active/source surface is where the user opened the assistant; it is not a write boundary by itself',
|
|
|
|
|
'- choose the write target from the selected per-app contexts and the user request; cross-app work is allowed when the relevant context is selected',
|
|
|
|
|
'- do not refuse Engine work only because the source surface is Ops, and do not refuse Ops work only because the mode is NDC Agent Core',
|
|
|
|
|
'- use the user language for the final answer and public progress/reasoning summaries; if the user writes in Russian, write them in Russian',
|
|
|
|
|
'- never mention internal n8n/N8N names, endpoints, environment variables, schemas, or runtime identifiers in public answers; call the product and workflow runtime NDC',
|
|
|
|
|
'- while working, provide concise public progress/reasoning summaries when the Codex runtime exposes them',
|
|
|
|
|
'- public progress/reasoning summaries should say what you are checking, reading, comparing, or deciding',
|
|
|
|
|
'- keep public progress/reasoning summaries user-facing; do not include raw command lines, JSON, hidden chain-of-thought, or bridge transport details',
|
|
|
|
|
...isNdcAgentCore ? [
|
|
|
|
|
'',
|
|
|
|
|
'NDC Agent Core mode contract:',
|
|
|
|
|
'- Treat the selected agent node as the only writable target.',
|
|
|
|
|
...!contextReady ? [
|
|
|
|
|
'- The NDC Agent Core context is incomplete. Chat, explanations, reading, and Ops work are still allowed when the required target for that work is selected.',
|
|
|
|
|
'- Do not claim that an Engine agent node is selected when it is missing.',
|
|
|
|
|
'- Do not create, edit, patch, deploy, or write Engine workflow graph changes until the missing Engine agent node is selected.',
|
|
|
|
|
'- If the user asks for Engine development or graph edits, explain briefly that selecting an Engine agent node is required for that specific write action.',
|
|
|
|
|
] : [
|
|
|
|
|
'- Treat the selected Engine agent node as the only writable Engine workflow target.',
|
|
|
|
|
'- Do not write directly to the NDC core runtime and do not modify deployed runtime workflows by hand.',
|
|
|
|
|
'- The Engine second-level workflow file is the source of truth for edits.',
|
|
|
|
|
`- Engine API base for this target: ${ndcAgentMcpApiBaseUrl}`,
|
|
|
|
|
@ -889,11 +1114,33 @@ function buildPrompt(payload) {
|
|
|
|
|
'- Read the second-level graph with GET /subworkflow?workflowId=<workflowId>&nodeId=<agentNodeId>.',
|
|
|
|
|
'- Apply graph edits with POST /subworkflow/patch using JSON: { workflowId, nodeId, intent, operations }.',
|
|
|
|
|
'- Prefer the MCP tools exposed by the ndc_agent_core server: ndc_get_context, ndc_get_subworkflow, ndc_search_nodes, ndc_get_node_definition, ndc_apply_subworkflow_patch, ndc_validate_subworkflow.',
|
|
|
|
|
'- Do not use direct n8n/NDC core MCP servers, local workflow files, or shell probes for graph edits in this mode.',
|
|
|
|
|
'- Do not use direct NDC core runtime MCP servers, local workflow files, or shell probes for graph edits in this mode.',
|
|
|
|
|
'- Use only ndc_agent_core MCP tools for the selected second-level workflow and stop after a cancellation, fetch, or policy error with the exact tool name and error.',
|
|
|
|
|
'- In public answers, use only NDC labels: NDC workflow, NDC node, NDC node type, NDC nodebase, and NDC Agent Core.',
|
|
|
|
|
'- Do not expose internal vendor names, raw implementation field names, backend schema routes, or internal node class names in public answers.',
|
|
|
|
|
'- Supported operations: addNode, upsertNode, updateNode, removeNode, moveNode, addConnection, removeConnection.',
|
|
|
|
|
'- Prefer small patches and preserve existing node ids, node names, positions, parameters, and connections unless the user asks to change them.',
|
|
|
|
|
'- After saving through the Engine API, the open Engine canvas refreshes from dc.subworkflow.json; never edit local files or use shell fallbacks for graph changes in this mode.',
|
|
|
|
|
],
|
|
|
|
|
] : [],
|
|
|
|
|
...(isOpsMode || opsContext.opsWorkspaceSlug || opsContext.opsProjectId ? [
|
|
|
|
|
'',
|
|
|
|
|
'NODE.DC Ops mode contract:',
|
|
|
|
|
'- Use Ops only inside the selected Ops workspace and project.',
|
|
|
|
|
`- Selected Ops workspace slug: ${opsContext.opsWorkspaceSlug || opsContext.opsWorkspaceId || 'unknown'}`,
|
|
|
|
|
`- Selected Ops project id: ${opsContext.opsProjectId || 'unknown'}`,
|
|
|
|
|
`- Selected Ops project identifier: ${opsContext.opsProjectIdentifier || 'unknown'}`,
|
|
|
|
|
'- Prefer the MCP tools exposed for NODE.DC Ops when creating, updating, moving, or reading tasks.',
|
|
|
|
|
'- Before writing Ops tasks, use the Ops MCP project/context tools when available and include a unique idempotency key for write tools.',
|
|
|
|
|
'- If Ops MCP tools are unavailable in the Codex session, say that the Ops context is selected but the Ops MCP tools are unavailable.',
|
|
|
|
|
'- Do not claim that an Engine workflow or Engine agent node is required for Ops task writes.',
|
|
|
|
|
'- If the current request is about Ops tasks and Ops workspace/project are selected, treat that as the writable Ops target.',
|
|
|
|
|
'- In public answers, use NODE.DC/Ops labels and never expose internal vendor names.',
|
|
|
|
|
] : []),
|
|
|
|
|
...guardInstruction ? [
|
|
|
|
|
'',
|
|
|
|
|
'System context guard:',
|
|
|
|
|
guardInstruction,
|
|
|
|
|
] : [],
|
|
|
|
|
'',
|
|
|
|
|
...history.length ? [
|
|
|
|
|
@ -1354,6 +1601,99 @@ function stripTopLevelTomlKeys(raw, keyNames) {
|
|
|
|
|
return result.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractTomlSection(raw, sectionName) {
|
|
|
|
|
const section = String(sectionName || '').trim()
|
|
|
|
|
if (!section) return ''
|
|
|
|
|
const headerKey = `[${section}]`
|
|
|
|
|
const lines = String(raw || '').split(/\r?\n/)
|
|
|
|
|
const result = []
|
|
|
|
|
let collecting = false
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
const header = normalizedTomlHeader(line)
|
|
|
|
|
if (header) {
|
|
|
|
|
if (header === headerKey) {
|
|
|
|
|
collecting = true
|
|
|
|
|
result.push(line)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if (collecting) break
|
|
|
|
|
}
|
|
|
|
|
if (collecting) result.push(line)
|
|
|
|
|
}
|
|
|
|
|
return result.join('\n').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeMcpServerSection(section, serverName) {
|
|
|
|
|
const lines = String(section || '').split(/\r?\n/)
|
|
|
|
|
const result = []
|
|
|
|
|
let hasEnabled = false
|
|
|
|
|
let hasRequired = false
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
const header = normalizedTomlHeader(line)
|
|
|
|
|
if (header) {
|
|
|
|
|
result.push(`[mcp_servers.${serverName}]`)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const activeLine = stripTomlComment(line)
|
|
|
|
|
if (/^\s*transport\s*=/.test(activeLine)) continue
|
|
|
|
|
if (/^\s*required\s*=/.test(activeLine)) {
|
|
|
|
|
result.push('required = false')
|
|
|
|
|
hasRequired = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if (/^\s*enabled\s*=/.test(activeLine)) {
|
|
|
|
|
result.push('enabled = true')
|
|
|
|
|
hasEnabled = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
result.push(line)
|
|
|
|
|
}
|
|
|
|
|
if (!hasEnabled) result.push('enabled = true')
|
|
|
|
|
if (!hasRequired) result.push('required = false')
|
|
|
|
|
return result.join('\n').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeMcpHeadersSection(section, serverName) {
|
|
|
|
|
if (!section) return ''
|
|
|
|
|
const lines = String(section || '').split(/\r?\n/)
|
|
|
|
|
const result = []
|
|
|
|
|
for (const line of lines) {
|
|
|
|
|
const header = normalizedTomlHeader(line)
|
|
|
|
|
if (header) {
|
|
|
|
|
result.push(`[mcp_servers.${serverName}.http_headers]`)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
result.push(line)
|
|
|
|
|
}
|
|
|
|
|
return result.join('\n').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractOptionalOpsMcpConfig(raw) {
|
|
|
|
|
const serverNames = ['nodedc-ops-agent', 'nodedc_tasker']
|
|
|
|
|
const configs = []
|
|
|
|
|
for (const serverName of serverNames) {
|
|
|
|
|
const baseSection = extractTomlSection(raw, `mcp_servers.${serverName}`)
|
|
|
|
|
if (!baseSection) continue
|
|
|
|
|
const headersSection = extractTomlSection(raw, `mcp_servers.${serverName}.http_headers`)
|
|
|
|
|
|| extractTomlSection(raw, `mcp_servers.${serverName}.headers`)
|
|
|
|
|
const normalized = [
|
|
|
|
|
normalizeMcpServerSection(baseSection, serverName),
|
|
|
|
|
normalizeMcpHeadersSection(headersSection, serverName),
|
|
|
|
|
].filter(Boolean).join('\n\n').trim()
|
|
|
|
|
if (normalized) configs.push(normalized)
|
|
|
|
|
}
|
|
|
|
|
return configs.join('\n\n').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readOptionalOpsMcpConfig() {
|
|
|
|
|
try {
|
|
|
|
|
const raw = await fs.readFile(path.join(CODEX_HOME, 'config.toml'), 'utf8')
|
|
|
|
|
return extractOptionalOpsMcpConfig(raw)
|
|
|
|
|
} catch {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyIfExists(from, to) {
|
|
|
|
|
try {
|
|
|
|
|
await fs.copyFile(from, to)
|
|
|
|
|
@ -1380,6 +1720,7 @@ function ndcAgentMcpEnvConfig(mcpContext, cwd) {
|
|
|
|
|
async function prepareNdcAgentCodexHome(mcpContext = {}, cwd = CODEX_CWD) {
|
|
|
|
|
await fs.mkdir(NDC_AGENT_CODEX_HOME, { recursive: true })
|
|
|
|
|
await copyIfExists(path.join(CODEX_HOME, 'auth.json'), path.join(NDC_AGENT_CODEX_HOME, 'auth.json'))
|
|
|
|
|
const opsMcpConfig = await readOptionalOpsMcpConfig()
|
|
|
|
|
const runtimeConfig = [
|
|
|
|
|
'approval_policy = "never"',
|
|
|
|
|
'sandbox_mode = "danger-full-access"',
|
|
|
|
|
@ -1391,7 +1732,12 @@ async function prepareNdcAgentCodexHome(mcpContext = {}, cwd = CODEX_CWD) {
|
|
|
|
|
'startup_timeout_sec = 20',
|
|
|
|
|
'tool_timeout_sec = 120',
|
|
|
|
|
].join('\n')
|
|
|
|
|
await fs.writeFile(path.join(NDC_AGENT_CODEX_HOME, 'config.toml'), `${runtimeConfig}\n\n${ndcConfig}\n${ndcAgentMcpEnvConfig(mcpContext, cwd)}\n`, 'utf8')
|
|
|
|
|
const config = [
|
|
|
|
|
runtimeConfig,
|
|
|
|
|
`${ndcConfig}\n${ndcAgentMcpEnvConfig(mcpContext, cwd)}`,
|
|
|
|
|
opsMcpConfig,
|
|
|
|
|
].filter(Boolean).join('\n\n')
|
|
|
|
|
await fs.writeFile(path.join(NDC_AGENT_CODEX_HOME, 'config.toml'), `${config}\n`, 'utf8')
|
|
|
|
|
return NDC_AGENT_CODEX_HOME
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1600,13 +1946,14 @@ function createActiveMirror(command, payload = {}, meta = {}) {
|
|
|
|
|
const context = payload?.context && typeof payload.context === 'object' ? payload.context : {}
|
|
|
|
|
if (!ACTIVE_MIRROR_ENABLED || command !== 'message' || context.remoteControlMode !== 'active') return null
|
|
|
|
|
const cwd = String(meta.cwd || CODEX_CWD)
|
|
|
|
|
const displayMessage = String(payload.displayMessage || payload.publicUserMessage || payload.message || '')
|
|
|
|
|
const { mirrorDir, currentFile, eventsFile } = activeMirrorPathsForCwd(cwd)
|
|
|
|
|
const startedAt = new Date().toISOString()
|
|
|
|
|
const state = {
|
|
|
|
|
requestId: String(meta.requestId || payload.threadId || `local-${Date.now()}`),
|
|
|
|
|
threadId: String(payload.threadId || ''),
|
|
|
|
|
threadTitle: String(payload.threadTitle || ''),
|
|
|
|
|
userMessage: String(payload.message || ''),
|
|
|
|
|
userMessage: displayMessage,
|
|
|
|
|
modeId: String(context.modeId || ''),
|
|
|
|
|
modeTitle: String(context.modeTitle || ''),
|
|
|
|
|
machineName: MACHINE_NAME,
|
|
|
|
|
@ -2620,10 +2967,16 @@ function engineApiBaseCandidates() {
|
|
|
|
|
...splitList(process.env.AI_BRIDGE_HUB_URLS).map(hubOriginToApiBase),
|
|
|
|
|
]
|
|
|
|
|
const envBase = cleanString(process.env.NDC_AGENT_MCP_API_BASE_URL, 1000).replace(/\/+$/, '')
|
|
|
|
|
const ordered = isLoopbackUrl(explicit) && hubBases.some(Boolean)
|
|
|
|
|
? [...hubBases, envBase, explicit, DEFAULT_API_BASE]
|
|
|
|
|
: [explicit, envBase, ...hubBases, DEFAULT_API_BASE]
|
|
|
|
|
return uniqueStrings(ordered)
|
|
|
|
|
if (explicit && !isLoopbackUrl(explicit)) {
|
|
|
|
|
return uniqueStrings([explicit, envBase])
|
|
|
|
|
}
|
|
|
|
|
if (envBase && !isLoopbackUrl(envBase)) {
|
|
|
|
|
return uniqueStrings([envBase, explicit])
|
|
|
|
|
}
|
|
|
|
|
if (hubBases.some(Boolean)) {
|
|
|
|
|
return uniqueStrings([...hubBases, envBase, explicit])
|
|
|
|
|
}
|
|
|
|
|
return uniqueStrings([explicit, envBase, DEFAULT_API_BASE])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function engineApiUrls(pathname) {
|
|
|
|
|
@ -2886,21 +3239,12 @@ async function handleGetContext() {
|
|
|
|
|
async function handleGetSubworkflow(args) {
|
|
|
|
|
const target = targetFromArgs(args)
|
|
|
|
|
const q = new URLSearchParams(target)
|
|
|
|
|
try {
|
|
|
|
|
const result = await engineFetch(`/subworkflow?${q.toString()}`)
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
...(result && typeof result === 'object' ? result : {}),
|
|
|
|
|
graph: normalizeGraphResult(result?.graph, target.workflowId, target.nodeId),
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (Number(error?.status || 0) !== 404 && !/not_found/.test(String(error?.message || ''))) throw error
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
graph: normalizeGraphResult(null, target.workflowId, target.nodeId),
|
|
|
|
|
missing: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleApplyPatch(args) {
|
|
|
|
|
@ -3767,6 +4111,8 @@ function Main {
|
|
|
|
|
Ensure-Codex
|
|
|
|
|
Ensure-CodexLogin
|
|
|
|
|
Repair-CodexMcpConfig
|
|
|
|
|
Ensure-AppMcpConfigs
|
|
|
|
|
if (-not $AppMcpServersJson) { Ensure-OpsMcpConfig }
|
|
|
|
|
Stop-BridgeNow
|
|
|
|
|
$script:Port = Resolve-BridgePort
|
|
|
|
|
$files = Write-WorkerFiles -Root $InstallRoot -WorkspacePath $workspacePath
|
|
|
|
|
|