fix: unify agent config save across mission control and gateway
This commit is contained in:
parent
e3c33a6a0c
commit
914a6c4cd0
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue