Skip to content

Commit 9ad5f5e

Browse files
Merge pull request #187 from EvanProgramming/codex/issue-186-sanitize-provider-plans
fix: sanitize provider plan metadata
2 parents 8dd5272 + 618823b commit 9ad5f5e

2 files changed

Lines changed: 40 additions & 20 deletions

File tree

‎interaction.py‎

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -195,35 +195,46 @@ def normalize_provider_control(text: str) -> tuple[str, dict[str, Any]] | None:
195195
if not isinstance(value, dict):
196196
return None
197197

198-
executable_keys = {"action", "args", "arguments", "command", "tool", "tool_calls"}
199-
stack: list[Any] = [value]
200-
while stack:
201-
item = stack.pop()
202-
if isinstance(item, dict):
203-
if executable_keys & {str(key).lower() for key in item}:
204-
return None
205-
stack.extend(item.values())
206-
elif isinstance(item, list):
207-
stack.extend(item)
208-
198+
executable_keys = {
199+
"action", "args", "arguments", "command", "commands", "content", "path", "tool", "tool_calls",
200+
}
209201
mode = str(value.get("mode") or "").strip().lower()
210202
if mode == "plan" or value.get("do_not_execute_until_approved") is True:
203+
if executable_keys & {str(key).lower() for key in value}:
204+
return None
211205
steps = []
212206
for index, step in enumerate(value.get("steps") or [], 1):
213207
if not isinstance(step, dict):
214208
return None
209+
description = step.get("description") or step.get("details") or step.get("title")
210+
if not description:
211+
return None
215212
steps.append({
216213
"id": str(step.get("id") or step.get("step_id") or f"step-{step.get('step', index)}"),
217-
"title": step.get("title") or f"Step {index}",
218-
"description": step.get("description") or step.get("details"),
219-
"acceptance": step.get("acceptance") or step.get("acceptance_criteria"),
214+
"title": step.get("title") or str(description)[:120],
215+
"description": description,
216+
"acceptance": (
217+
step.get("acceptance") or step.get("acceptance_criteria")
218+
or ["Step completed with observable evidence"]
219+
),
220220
})
221221
return "PlanProposal", {
222222
"title": value.get("title") or value.get("plan_name"),
223-
"summary": value.get("summary") or value.get("overview"),
223+
"summary": value.get("summary") or value.get("overview") or value.get("goal"),
224224
"assumptions": value.get("assumptions") or [],
225225
"steps": steps,
226226
}
227+
228+
stack: list[Any] = [value]
229+
while stack:
230+
item = stack.pop()
231+
if isinstance(item, dict):
232+
if executable_keys & {str(key).lower() for key in item}:
233+
return None
234+
stack.extend(item.values())
235+
elif isinstance(item, list):
236+
stack.extend(item)
237+
227238
if mode == "ask" or value.get("ask_user") is True:
228239
return "AskUser", {"questions": value.get("questions")}
229240
return None

‎tests/test_interaction.py‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def test_exact_acceptance_phrases_and_control_parser(self):
187187
'PlanProposal\n```json\n{"steps": []}\n```', "PlanProposal",
188188
), {"steps": []})
189189

190-
def test_provider_shaped_plan_is_normalized_but_actions_are_not(self):
190+
def test_provider_shaped_plan_discards_executable_step_metadata(self):
191191
shaped = json.dumps({
192192
"plan_name": "Status page", "mode": "plan",
193193
"do_not_execute_until_approved": True,
@@ -201,12 +201,21 @@ def test_provider_shaped_plan_is_normalized_but_actions_are_not(self):
201201
self.assertEqual(parsed["tool_calls"], [])
202202

203203
executable = json.dumps({
204-
"mode": "plan", "plan_name": "Unsafe", "overview": "Write now.",
204+
"mode": "plan", "plan_name": "Safe proposal", "goal": "Write after acceptance.",
205205
"steps": [{"title": "Write", "details": "Write README.",
206-
"acceptance_criteria": ["written"], "action": "write_file"}],
206+
"action": "write_file", "path": "README.md", "content": "unsafe"}],
207207
})
208-
rejected = main._observe_model_response(executable)
209-
self.assertIsNone(rejected["plan_proposal"])
208+
sanitized = main._observe_model_response(executable)
209+
self.assertEqual(sanitized["tool_calls"], [])
210+
self.assertEqual(sanitized["plan_proposal"]["title"], "Safe proposal")
211+
self.assertEqual(
212+
set(sanitized["plan_proposal"]["steps"][0]),
213+
{"id", "title", "description", "acceptance"},
214+
)
215+
self.assertEqual(
216+
sanitized["plan_proposal"]["steps"][0]["acceptance"],
217+
["Step completed with observable evidence"],
218+
)
210219

211220
def test_plan_mode_converts_plain_plan_and_discards_early_actions(self):
212221
mode_token = main._active_interaction_mode.set("plan")

0 commit comments

Comments
 (0)