fix: unify agent config save across mission control and gateway

This commit is contained in:
Nyk 2026-03-04 23:18:23 +07:00
parent e3c33a6a0c
commit 914a6c4cd0
2 changed files with 75 additions and 61 deletions

View File

@ -44,12 +44,12 @@ export async function GET(
} }
/** /**
* PUT /api/agents/[id] - Update agent config with optional gateway write-back * PUT /api/agents/[id] - Update agent config with unified MC + gateway save
* *
* Body: { * Body: {
* role?: string * role?: string
* gateway_config?: object - OpenClaw agent config fields to update * gateway_config?: object - OpenClaw agent config fields to update
* write_to_gateway?: boolean - If true, also write to openclaw.json * write_to_gateway?: boolean - Defaults to true when gateway_config exists
* } * }
*/ */
export async function PUT( export async function PUT(
@ -86,66 +86,88 @@ export async function PUT(
newConfig = { ...existingConfig, ...gateway_config } newConfig = { ...existingConfig, ...gateway_config }
} }
// Build update const shouldWriteToGateway = Boolean(
const fields: string[] = ['updated_at = ?'] gateway_config &&
const values: any[] = [now] (write_to_gateway === undefined || write_to_gateway === null || write_to_gateway === true)
)
if (role !== undefined) { const openclawId = existingConfig.openclawId || agent.name.toLowerCase().replace(/\s+/g, '-')
fields.push('role = ?') const getWriteBackPayload = (source: Record<string, any>) => {
values.push(role) const writeBack: any = { id: openclawId }
if (source.model) writeBack.model = source.model
if (source.identity) writeBack.identity = source.identity
if (source.sandbox) writeBack.sandbox = source.sandbox
if (source.tools) writeBack.tools = source.tools
if (source.subagents) writeBack.subagents = source.subagents
if (source.memorySearch) writeBack.memorySearch = source.memorySearch
return writeBack
} }
if (gateway_config) { // Unified save: gateway first, then DB. If DB fails after gateway write, attempt rollback.
fields.push('config = ?') if (shouldWriteToGateway) {
values.push(JSON.stringify(newConfig))
}
values.push(agent.id, workspaceId)
db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
// Write back to openclaw.json if requested
if (write_to_gateway && gateway_config) {
try { try {
const openclawId = existingConfig.openclawId || agent.name.toLowerCase().replace(/\s+/g, '-') await writeAgentToConfig(getWriteBackPayload(gateway_config))
// Build the config to write back (full OpenClaw format)
const writeBack: any = { id: openclawId }
if (gateway_config.model) writeBack.model = gateway_config.model
if (gateway_config.identity) writeBack.identity = gateway_config.identity
if (gateway_config.sandbox) writeBack.sandbox = gateway_config.sandbox
if (gateway_config.tools) writeBack.tools = gateway_config.tools
if (gateway_config.subagents) writeBack.subagents = gateway_config.subagents
if (gateway_config.memorySearch) writeBack.memorySearch = gateway_config.memorySearch
await writeAgentToConfig(writeBack)
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
logAuditEvent({
action: 'agent_config_writeback',
actor: auth.user.username,
actor_id: auth.user.id,
target_type: 'agent',
target_id: agent.id,
detail: { agent_name: agent.name, openclaw_id: openclawId, fields: Object.keys(gateway_config) },
ip_address: ipAddress,
})
} catch (err: any) { } catch (err: any) {
// Config update succeeded in DB but gateway write failed return NextResponse.json(
return NextResponse.json({ { error: `Save failed: unable to update gateway config: ${err.message}` },
warning: `Agent updated in MC but gateway write failed: ${err.message}`, { status: 502 }
agent: { ...agent, config: newConfig, role: role || agent.role, updated_at: now }, )
})
} }
} }
try {
// Build update
const fields: string[] = ['updated_at = ?']
const values: any[] = [now]
if (role !== undefined) {
fields.push('role = ?')
values.push(role)
}
if (gateway_config) {
fields.push('config = ?')
values.push(JSON.stringify(newConfig))
}
values.push(agent.id, workspaceId)
db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
} catch (err: any) {
if (shouldWriteToGateway) {
try {
// Best-effort rollback to preserve consistency if DB update fails after gateway write.
await writeAgentToConfig(getWriteBackPayload(existingConfig))
} catch (rollbackErr: any) {
logger.error({ err: rollbackErr, agent: agent.name }, 'Failed to rollback gateway config after DB failure')
return NextResponse.json(
{ error: `Save failed after gateway update and rollback failed: ${err.message}` },
{ status: 500 }
)
}
}
return NextResponse.json({ error: `Save failed: ${err.message}` }, { status: 500 })
}
if (shouldWriteToGateway) {
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
logAuditEvent({
action: 'agent_config_writeback',
actor: auth.user.username,
actor_id: auth.user.id,
target_type: 'agent',
target_id: agent.id,
detail: { agent_name: agent.name, openclaw_id: openclawId, fields: Object.keys(gateway_config || {}) },
ip_address: ipAddress,
})
}
// Log activity // Log activity
db_helpers.logActivity( db_helpers.logActivity(
'agent_config_updated', 'agent_config_updated',
'agent', 'agent',
agent.id, agent.id,
auth.user.username, auth.user.username,
`Config updated for agent ${agent.name}${write_to_gateway ? ' (+ gateway)' : ''}`, `Config updated for agent ${agent.name}${shouldWriteToGateway ? ' (+ gateway)' : ''}`,
{ fields: Object.keys(gateway_config || {}), write_to_gateway }, { fields: Object.keys(gateway_config || {}), write_to_gateway: shouldWriteToGateway },
workspaceId workspaceId
) )

View File

@ -1337,7 +1337,7 @@ export function ConfigTab({
}) })
} }
const handleSave = async (writeToGateway: boolean = false) => { const handleSave = async () => {
setSaving(true) setSaving(true)
setError(null) setError(null)
try { try {
@ -1352,12 +1352,11 @@ export function ConfigTab({
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
gateway_config: showJson ? JSON.parse(jsonInput) : config, gateway_config: showJson ? JSON.parse(jsonInput) : config,
write_to_gateway: writeToGateway, write_to_gateway: true,
}), }),
}) })
const data = await response.json() const data = await response.json()
if (!response.ok) throw new Error(data.error || 'Failed to save') if (!response.ok) throw new Error(data.error || 'Failed to save')
if (data.warning) setError(data.warning)
setEditing(false) setEditing(false)
onSave() onSave()
} catch (err: any) { } catch (err: any) {
@ -1759,18 +1758,11 @@ export function ConfigTab({
{editing && ( {editing && (
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-2">
<button <button
onClick={() => handleSave(false)} onClick={handleSave}
disabled={saving} disabled={saving}
className="flex-1 bg-primary text-primary-foreground py-2 rounded-md hover:bg-primary/90 disabled:opacity-50 transition-smooth" className="flex-1 bg-primary text-primary-foreground py-2 rounded-md hover:bg-primary/90 disabled:opacity-50 transition-smooth"
> >
{saving ? 'Saving...' : 'Save to MC'} {saving ? 'Saving...' : 'Save'}
</button>
<button
onClick={() => handleSave(true)}
disabled={saving}
className="flex-1 bg-green-600 text-white py-2 rounded-md hover:bg-green-700 disabled:opacity-50 transition-smooth"
>
Save to Gateway
</button> </button>
<button <button
onClick={() => { onClick={() => {