nyk commited on
Commit
98f07b9
·
unverified ·
2 Parent(s): aa65898a3ca4cc

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 optional gateway write-back
48
  *
49
  * Body: {
50
  * role?: string
51
  * gateway_config?: object - OpenClaw agent config fields to update
52
- * write_to_gateway?: boolean - If true, also write to openclaw.json
53
  * }
54
  */
55
  export async function PUT(
@@ -86,56 +86,78 @@ export async function PUT(
86
  newConfig = { ...existingConfig, ...gateway_config }
87
  }
88
 
89
- // Build update
90
- const fields: string[] = ['updated_at = ?']
91
- const values: any[] = [now]
92
-
93
- if (role !== undefined) {
94
- fields.push('role = ?')
95
- values.push(role)
 
 
 
 
 
 
 
96
  }
97
 
98
- if (gateway_config) {
99
- fields.push('config = ?')
100
- values.push(JSON.stringify(newConfig))
 
 
 
 
 
 
 
101
  }
102
 
103
- values.push(agent.id, workspaceId)
104
- db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
 
 
105
 
106
- // Write back to openclaw.json if requested
107
- if (write_to_gateway && gateway_config) {
108
- try {
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}${write_to_gateway ? ' (+ gateway)' : ''}`,
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 (writeToGateway: boolean = false) => {
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: writeToGateway,
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={() => handleSave(false)}
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 to MC'}
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={() => {