@@ -267,10 +267,89 @@
267 267 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
fn := ps.onChange
268 268 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
go fn(snap)
269 269 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
270 270 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return nil
271 271 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
272 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
273 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Merge applies a partial Policies update over the current state. Only
274 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // non-zero fields in the patch overwrite existing values. Behaviors are
275 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // merged by ID — existing behaviors keep their defaults for fields not
276 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // present in the patch.
277 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (ps *PolicyStore) Merge(patch Policies) error {
278 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.mu.Lock()
279 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ defer ps.mu.Unlock()
280 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
281 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if len(patch.Behaviors) > 0 {
282 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ incoming := make(map[string]BehaviorConfig, len(patch.Behaviors))
283 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for _, b := range patch.Behaviors {
284 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ incoming[b.ID] = b
285 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
286 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ for i, existing := range ps.data.Behaviors {
287 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patched, ok := incoming[existing.ID]; ok {
288 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Merge: keep existing defaults, overlay patch fields.
289 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patched.Name != "" {
290 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.Name = patched.Name
291 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
292 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patched.Description != "" {
293 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.Description = patched.Description
294 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
295 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patched.Nick != "" {
296 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.Nick = patched.Nick
297 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
298 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.Enabled = patched.Enabled
299 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.JoinAllChannels = patched.JoinAllChannels
300 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patched.ExcludeChannels != nil {
301 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.ExcludeChannels = patched.ExcludeChannels
302 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
303 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patched.RequiredChannels != nil {
304 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.RequiredChannels = patched.RequiredChannels
305 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
306 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patched.Config != nil {
307 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ existing.Config = patched.Config
308 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
309 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.data.Behaviors[i] = existing
310 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
311 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
312 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
313 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
314 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Merge agent_policy if any field is set.
315 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patch.AgentPolicy.CheckinChannel != "" || patch.AgentPolicy.RequireCheckin || patch.AgentPolicy.RequiredChannels != nil {
316 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patch.AgentPolicy.CheckinChannel != "" {
317 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.data.AgentPolicy.CheckinChannel = patch.AgentPolicy.CheckinChannel
318 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
319 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.data.AgentPolicy.RequireCheckin = patch.AgentPolicy.RequireCheckin
320 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patch.AgentPolicy.RequiredChannels != nil {
321 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.data.AgentPolicy.RequiredChannels = patch.AgentPolicy.RequiredChannels
322 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
323 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
324 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
325 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Merge bridge if set.
326 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patch.Bridge.WebUserTTLMinutes > 0 {
327 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.data.Bridge.WebUserTTLMinutes = patch.Bridge.WebUserTTLMinutes
328 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
329 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
330 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Merge logging if any field is set.
331 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patch.Logging.Dir != "" || patch.Logging.Enabled {
332 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.data.Logging = patch.Logging
333 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
334 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
335 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ // Merge LLM backends if provided.
336 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if patch.LLMBackends != nil {
337 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.data.LLMBackends = patch.LLMBackends
338 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
339 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
340 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ ps.normalize(&ps.data)
341 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if err := ps.save(); err != nil {
342 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return err
343 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
344 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if ps.onChange != nil {
345 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ snap := ps.data
346 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ fn := ps.onChange
347 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ go fn(snap)
348 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
349 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return nil
350 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
272 351 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
273 352 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
func (ps *PolicyStore) normalize(p *Policies) {
274 353 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
if p.Bridge.WebUserTTLMinutes <= 0 {
275 354 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
p.Bridge.WebUserTTLMinutes = ps.defaultBridgeTTLMinutes
276 355 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
@@ -290,8 +369,22 @@
290 369 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
291 370 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
if err := s.policies.Set(p); err != nil {
292 371 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
s.log.Error("save policies", "err", err)
293 372 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
writeError(w, http.StatusInternalServerError, "save failed")
294 373 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
return
374 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
375 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ writeJSON(w, http.StatusOK, s.policies.Get())
376 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
377 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+
378 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ func (s *Server) handlePatchPolicies(w http.ResponseWriter, r *http.Request) {
379 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ var patch Policies
380 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
381 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ writeError(w, http.StatusBadRequest, "invalid request body")
382 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return
383 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ }
384 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ if err := s.policies.Merge(patch); err != nil {
385 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ s.log.Error("merge policies", "err", err)
386 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ writeError(w, http.StatusInternalServerError, "save failed")
387 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
+ return
295 388 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
296 389 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
writeJSON(w, http.StatusOK, s.policies.Get())
297 390 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!
}
298 391 { copied = false; pop = false }, 1000)" :class="copied && 'copied'">Copy link Copied!