Spaces:
Runtime error
Runtime error
Merge pull request #157 from builderz-labs/fix/154-unified-agent-save
Browse files
src/app/api/agents/[id]/route.ts
CHANGED
|
@@ -44,12 +44,12 @@ export async function GET(
|
|
| 44 |
}
|
| 45 |
|
| 46 |
/**
|
| 47 |
-
* PUT /api/agents/[id] - Update agent config with
|
| 48 |
*
|
| 49 |
* Body: {
|
| 50 |
* role?: string
|
| 51 |
* gateway_config?: object - OpenClaw agent config fields to update
|
| 52 |
-
* write_to_gateway?: boolean -
|
| 53 |
* }
|
| 54 |
*/
|
| 55 |
export async function PUT(
|
|
@@ -86,56 +86,78 @@ export async function PUT(
|
|
| 86 |
newConfig = { ...existingConfig, ...gateway_config }
|
| 87 |
}
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
const openclawId = existingConfig.openclawId || agent.name.toLowerCase().replace(/\s+/g, '-')
|
| 110 |
-
|
| 111 |
-
// Build the config to write back (full OpenClaw format)
|
| 112 |
-
const writeBack: any = { id: openclawId }
|
| 113 |
-
if (gateway_config.model) writeBack.model = gateway_config.model
|
| 114 |
-
if (gateway_config.identity) writeBack.identity = gateway_config.identity
|
| 115 |
-
if (gateway_config.sandbox) writeBack.sandbox = gateway_config.sandbox
|
| 116 |
-
if (gateway_config.tools) writeBack.tools = gateway_config.tools
|
| 117 |
-
if (gateway_config.subagents) writeBack.subagents = gateway_config.subagents
|
| 118 |
-
if (gateway_config.memorySearch) writeBack.memorySearch = gateway_config.memorySearch
|
| 119 |
-
|
| 120 |
-
await writeAgentToConfig(writeBack)
|
| 121 |
-
|
| 122 |
-
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
| 123 |
-
logAuditEvent({
|
| 124 |
-
action: 'agent_config_writeback',
|
| 125 |
-
actor: auth.user.username,
|
| 126 |
-
actor_id: auth.user.id,
|
| 127 |
-
target_type: 'agent',
|
| 128 |
-
target_id: agent.id,
|
| 129 |
-
detail: { agent_name: agent.name, openclaw_id: openclawId, fields: Object.keys(gateway_config) },
|
| 130 |
-
ip_address: ipAddress,
|
| 131 |
-
})
|
| 132 |
-
} catch (err: any) {
|
| 133 |
-
// Config update succeeded in DB but gateway write failed
|
| 134 |
-
return NextResponse.json({
|
| 135 |
-
warning: `Agent updated in MC but gateway write failed: ${err.message}`,
|
| 136 |
-
agent: { ...agent, config: newConfig, role: role || agent.role, updated_at: now },
|
| 137 |
-
})
|
| 138 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
// Log activity
|
|
@@ -144,8 +166,8 @@ export async function PUT(
|
|
| 144 |
'agent',
|
| 145 |
agent.id,
|
| 146 |
auth.user.username,
|
| 147 |
-
`Config updated for agent ${agent.name}${
|
| 148 |
-
{ fields: Object.keys(gateway_config || {}), write_to_gateway },
|
| 149 |
workspaceId
|
| 150 |
)
|
| 151 |
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
/**
|
| 47 |
+
* PUT /api/agents/[id] - Update agent config with unified MC + gateway save
|
| 48 |
*
|
| 49 |
* Body: {
|
| 50 |
* role?: string
|
| 51 |
* gateway_config?: object - OpenClaw agent config fields to update
|
| 52 |
+
* write_to_gateway?: boolean - Defaults to true when gateway_config exists
|
| 53 |
* }
|
| 54 |
*/
|
| 55 |
export async function PUT(
|
|
|
|
| 86 |
newConfig = { ...existingConfig, ...gateway_config }
|
| 87 |
}
|
| 88 |
|
| 89 |
+
const shouldWriteToGateway = Boolean(
|
| 90 |
+
gateway_config &&
|
| 91 |
+
(write_to_gateway === undefined || write_to_gateway === null || write_to_gateway === true)
|
| 92 |
+
)
|
| 93 |
+
const openclawId = existingConfig.openclawId || agent.name.toLowerCase().replace(/\s+/g, '-')
|
| 94 |
+
const getWriteBackPayload = (source: Record<string, any>) => {
|
| 95 |
+
const writeBack: any = { id: openclawId }
|
| 96 |
+
if (source.model) writeBack.model = source.model
|
| 97 |
+
if (source.identity) writeBack.identity = source.identity
|
| 98 |
+
if (source.sandbox) writeBack.sandbox = source.sandbox
|
| 99 |
+
if (source.tools) writeBack.tools = source.tools
|
| 100 |
+
if (source.subagents) writeBack.subagents = source.subagents
|
| 101 |
+
if (source.memorySearch) writeBack.memorySearch = source.memorySearch
|
| 102 |
+
return writeBack
|
| 103 |
}
|
| 104 |
|
| 105 |
+
// Unified save: gateway first, then DB. If DB fails after gateway write, attempt rollback.
|
| 106 |
+
if (shouldWriteToGateway) {
|
| 107 |
+
try {
|
| 108 |
+
await writeAgentToConfig(getWriteBackPayload(gateway_config))
|
| 109 |
+
} catch (err: any) {
|
| 110 |
+
return NextResponse.json(
|
| 111 |
+
{ error: `Save failed: unable to update gateway config: ${err.message}` },
|
| 112 |
+
{ status: 502 }
|
| 113 |
+
)
|
| 114 |
+
}
|
| 115 |
}
|
| 116 |
|
| 117 |
+
try {
|
| 118 |
+
// Build update
|
| 119 |
+
const fields: string[] = ['updated_at = ?']
|
| 120 |
+
const values: any[] = [now]
|
| 121 |
|
| 122 |
+
if (role !== undefined) {
|
| 123 |
+
fields.push('role = ?')
|
| 124 |
+
values.push(role)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
+
|
| 127 |
+
if (gateway_config) {
|
| 128 |
+
fields.push('config = ?')
|
| 129 |
+
values.push(JSON.stringify(newConfig))
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
values.push(agent.id, workspaceId)
|
| 133 |
+
db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
|
| 134 |
+
} catch (err: any) {
|
| 135 |
+
if (shouldWriteToGateway) {
|
| 136 |
+
try {
|
| 137 |
+
// Best-effort rollback to preserve consistency if DB update fails after gateway write.
|
| 138 |
+
await writeAgentToConfig(getWriteBackPayload(existingConfig))
|
| 139 |
+
} catch (rollbackErr: any) {
|
| 140 |
+
logger.error({ err: rollbackErr, agent: agent.name }, 'Failed to rollback gateway config after DB failure')
|
| 141 |
+
return NextResponse.json(
|
| 142 |
+
{ error: `Save failed after gateway update and rollback failed: ${err.message}` },
|
| 143 |
+
{ status: 500 }
|
| 144 |
+
)
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
return NextResponse.json({ error: `Save failed: ${err.message}` }, { status: 500 })
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if (shouldWriteToGateway) {
|
| 151 |
+
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
| 152 |
+
logAuditEvent({
|
| 153 |
+
action: 'agent_config_writeback',
|
| 154 |
+
actor: auth.user.username,
|
| 155 |
+
actor_id: auth.user.id,
|
| 156 |
+
target_type: 'agent',
|
| 157 |
+
target_id: agent.id,
|
| 158 |
+
detail: { agent_name: agent.name, openclaw_id: openclawId, fields: Object.keys(gateway_config || {}) },
|
| 159 |
+
ip_address: ipAddress,
|
| 160 |
+
})
|
| 161 |
}
|
| 162 |
|
| 163 |
// Log activity
|
|
|
|
| 166 |
'agent',
|
| 167 |
agent.id,
|
| 168 |
auth.user.username,
|
| 169 |
+
`Config updated for agent ${agent.name}${shouldWriteToGateway ? ' (+ gateway)' : ''}`,
|
| 170 |
+
{ fields: Object.keys(gateway_config || {}), write_to_gateway: shouldWriteToGateway },
|
| 171 |
workspaceId
|
| 172 |
)
|
| 173 |
|
src/components/panels/agent-detail-tabs.tsx
CHANGED
|
@@ -1337,7 +1337,7 @@ export function ConfigTab({
|
|
| 1337 |
})
|
| 1338 |
}
|
| 1339 |
|
| 1340 |
-
const handleSave = async (
|
| 1341 |
setSaving(true)
|
| 1342 |
setError(null)
|
| 1343 |
try {
|
|
@@ -1352,12 +1352,11 @@ export function ConfigTab({
|
|
| 1352 |
headers: { 'Content-Type': 'application/json' },
|
| 1353 |
body: JSON.stringify({
|
| 1354 |
gateway_config: showJson ? JSON.parse(jsonInput) : config,
|
| 1355 |
-
write_to_gateway:
|
| 1356 |
}),
|
| 1357 |
})
|
| 1358 |
const data = await response.json()
|
| 1359 |
if (!response.ok) throw new Error(data.error || 'Failed to save')
|
| 1360 |
-
if (data.warning) setError(data.warning)
|
| 1361 |
setEditing(false)
|
| 1362 |
onSave()
|
| 1363 |
} catch (err: any) {
|
|
@@ -1759,18 +1758,11 @@ export function ConfigTab({
|
|
| 1759 |
{editing && (
|
| 1760 |
<div className="flex gap-3 pt-2">
|
| 1761 |
<button
|
| 1762 |
-
onClick={
|
| 1763 |
disabled={saving}
|
| 1764 |
className="flex-1 bg-primary text-primary-foreground py-2 rounded-md hover:bg-primary/90 disabled:opacity-50 transition-smooth"
|
| 1765 |
>
|
| 1766 |
-
{saving ? 'Saving...' : 'Save
|
| 1767 |
-
</button>
|
| 1768 |
-
<button
|
| 1769 |
-
onClick={() => handleSave(true)}
|
| 1770 |
-
disabled={saving}
|
| 1771 |
-
className="flex-1 bg-green-600 text-white py-2 rounded-md hover:bg-green-700 disabled:opacity-50 transition-smooth"
|
| 1772 |
-
>
|
| 1773 |
-
Save to Gateway
|
| 1774 |
</button>
|
| 1775 |
<button
|
| 1776 |
onClick={() => {
|
|
|
|
| 1337 |
})
|
| 1338 |
}
|
| 1339 |
|
| 1340 |
+
const handleSave = async () => {
|
| 1341 |
setSaving(true)
|
| 1342 |
setError(null)
|
| 1343 |
try {
|
|
|
|
| 1352 |
headers: { 'Content-Type': 'application/json' },
|
| 1353 |
body: JSON.stringify({
|
| 1354 |
gateway_config: showJson ? JSON.parse(jsonInput) : config,
|
| 1355 |
+
write_to_gateway: true,
|
| 1356 |
}),
|
| 1357 |
})
|
| 1358 |
const data = await response.json()
|
| 1359 |
if (!response.ok) throw new Error(data.error || 'Failed to save')
|
|
|
|
| 1360 |
setEditing(false)
|
| 1361 |
onSave()
|
| 1362 |
} catch (err: any) {
|
|
|
|
| 1758 |
{editing && (
|
| 1759 |
<div className="flex gap-3 pt-2">
|
| 1760 |
<button
|
| 1761 |
+
onClick={handleSave}
|
| 1762 |
disabled={saving}
|
| 1763 |
className="flex-1 bg-primary text-primary-foreground py-2 rounded-md hover:bg-primary/90 disabled:opacity-50 transition-smooth"
|
| 1764 |
>
|
| 1765 |
+
{saving ? 'Saving...' : 'Save'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1766 |
</button>
|
| 1767 |
<button
|
| 1768 |
onClick={() => {
|