feat(update): refuse updates that would delete existing configuration - #18
Conversation
Phase 1 of #14. The API replaces objects rather than merging, so a payload narrower than the agent silently deletes the difference. agent-config.json cannot express a second tool, tool-level fields like mode, or per-index searchControls, so those losses are never something the user asked for — they are the config model being narrower than the API. _removals() reports exactly those inexpressible losses and cmd_update refuses unless --force. Index membership is deliberately excluded: the config names its indices, so dropping one is a choice rather than an accident. Fields already at their API default are skipped, since omitting those changes nothing. --dry-run is not blocked; it writes nothing and its job is to report. _tool_key and _TOOL_IDENTITY move to module scope so the guard and _diff share one definition instead of duplicating the rules. Verified end to end against a live throwaway agent: without --force the update is refused and the agent is left byte-for-byte intact; with --force every warned-about loss actually occurs, so the warnings are accurate rather than defensive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a safety guard to algolia-agent update to prevent accidental destructive updates caused by the Agent Studio API’s replace-not-merge PATCH behavior, requiring explicit --force to proceed when the payload would drop unmodelable existing agent configuration.
Changes:
- Introduces
_removals()to detect configuration losses that the local config model can’t intentionally express, and blocksupdateunless--forceis provided. - Refactors tool identity helpers (
_tool_key,_TOOL_IDENTITY) to module scope so_diff()and the new guard share the same identity rules. - Adds unit tests covering
_removals()behavior andcmd_updaterefusal/--force/--dry-runinteractions.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/algolia_agent/cli.py |
Adds destructive-update detection + --force override and refactors shared tool-identity logic. |
tests/test_cli.py |
Adds tests validating removal detection and update refusal/force/dry-run behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| + "\n\nThe Agent Studio API replaces these fields instead of merging them, so\n" | ||
| "anything missing from the payload is lost. agent-config.json cannot express\n" | ||
| "them, which is why they are absent.\n\n" | ||
| "Either add the values to your config, or pass --force to accept the removals.\n" |
| for field in sorted(dropped): | ||
| val = curr_tools[key][field] | ||
| if val is None or val == _TOOL_FIELD_DEFAULTS.get(field, _MISSING): | ||
| continue # already at its default; omitting it changes nothing | ||
| out.append(f" - {key}.{field} would revert to its default (now {_fmt(val)})") |
Review feedback on this PR. The refusal message hard-coded "agent-config.json", but update takes --config with any path and can run with no config file at all, so the message was sometimes simply wrong. It now names the path in use, or "the fields you supplied" when there is no config file. "would revert to its default (now 'dynamic')" put the word default next to the current value, reading as though dynamic were the default. Both defaults are known, so name them: "'dynamic' would revert to its default of 'static'". For a field with no known default, say it would be dropped rather than inventing one — which also keeps the message honest as the API grows fields we have not probed. Extended the same phrasing to the matching _diff line, slightly beyond the review's scope: it carried the identical ambiguity from the identical source, and two different wordings for one phenomenon is worse than either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both findings applied in the latest commit. Hard-coded
For a field with no known default the message says it "would be dropped from the payload" rather than implying knowledge we lack. That matters going forward: the dashboard is shipping fields faster than we can probe them, so the honest fallback is the one that stays correct. One deliberate extension beyond the review's scope: the matching line in 133 tests pass (3 new, covering the custom-path message and both known/unknown-default branches). |
Phase 1 of #14.
Problem
PATCH /agents/{id}replaces object fields rather than merging them, so anything absent from the payload is destroyed.agent-config.jsoncannot express a second tool, tool-level fields likemode, or per-indexsearchControls— soupdatedeletes them, and until #13 it did so silently.#13 made the loss visible in
--dry-run. This makes it impossible to do by accident.Behaviour
What counts as a removal
The guard fires on losses the config model cannot ask for, since those can only be accidental:
mode,allowUnlistedIndices)searchControlsthat would be wipedconfigkeys the payload omitsDeliberately not flagged:
mode: "static"changes nothing, verified against the API--dry-runis never blocked: it writes nothing, and refusing to report would defeat its purpose.Refactor
_tool_key()and_TOOL_IDENTITYmove from locals inside_diff()to module scope, so the guard and the diff share one definition of tool identity rather than duplicating the rules.Verification
130 tests pass (8 new), CI on 3.10 / 3.11 / 3.13.
Live end-to-end against a throwaway agent carrying all four categories (created and deleted; nothing existing touched):
--forcemode: dynamic,allowUnlisted: True, searchControls present, 4 config keys--forcedynamic→static,True→False, searchControls wiped, config 4 keys→1The
--forcehalf matters as much as the refusal: it confirms every warned-about loss is real, so the guard is accurate rather than over-cautious.Next
Phase 2 is the
snapshotcommand, after which the destructive path is only reachable from a hand-written partial config plus--force. The error message will then recommendsnapshotas the remedy.🤖 Generated with Claude Code