@@ -237,10 +237,11 @@ def test_a_project_serves_its_commands_with_the_project_bound(tmp_path: Path) ->
237237 ("converge_baseline" , {}),
238238 )
239239
240- assert {"deploy" , "audit" , "operator_approve " , "build_artifact" } <= set (tools )
240+ assert {"deploy" , "audit" , "operator_show " , "build_artifact" } <= set (tools )
241241 assert not {
242242 "init" ,
243243 "decisions_approve" ,
244+ "operator_approve" ,
244245 "operator_run" ,
245246 "dashboard_serve" ,
246247 "cleanup" ,
@@ -253,17 +254,27 @@ def test_a_project_serves_its_commands_with_the_project_bound(tmp_path: Path) ->
253254 assert converge ["error" ]["code" ] == "CONFIRMATION_REQUIRED"
254255
255256
257+ def test_a_project_call_cannot_approve_its_own_proposal (tmp_path : Path ) -> None :
258+ # A person approves a proposal from a shell; an agent that could call
259+ # operator approve would run and verify the change it proposed itself.
260+ _ , (approved ,) = _talk (
261+ _serve ("--project" , str (EXAMPLES ), cwd = tmp_path ),
262+ ("operator_approve" , {"proposal" : "anything" }),
263+ )
264+
265+ assert approved ["error" ]["code" ] == "UNKNOWN_TOOL"
266+
267+
256268def test_a_project_call_cannot_send_a_file_elsewhere_or_write_secrets_out (
257269 tmp_path : Path ,
258270) -> None :
259271 # An agent that chose the Render API URL could send any controller file
260272 # it names as the bearer key to a host of its choosing; one that chose
261- # the env file could write decrypted values outside the project; one
262- # that chose the gateway URL could answer its own verification.
273+ # the env file could write decrypted values outside the project.
263274 secret = tmp_path / "outside.txt"
264275 secret .write_text ("not-a-render-key\n " , encoding = "utf-8" )
265276
266- tools , (sent , rendered , approved ) = _talk (
277+ tools , (sent , rendered ) = _talk (
267278 _serve ("--project" , str (EXAMPLES ), cwd = tmp_path ),
268279 (
269280 "import_render-api" ,
@@ -275,13 +286,11 @@ def test_a_project_call_cannot_send_a_file_elsewhere_or_write_secrets_out(
275286 },
276287 ),
277288 ("secrets_render" , {"component" : "crm" , "output_file" : str (secret )}),
278- ("operator_approve" , {"proposal" : "p" , "gateway_url" : "http://127.0.0.1:9" }),
279289 )
280290
281291 assert "api_url" not in tools ["import_render-api" ].input_schema ["properties" ]
282292 assert "output_file" not in tools ["secrets_render" ].input_schema ["properties" ]
283- assert "gateway_url" not in tools ["operator_approve" ].input_schema ["properties" ]
284- for refused in (sent , rendered , approved ):
293+ for refused in (sent , rendered ):
285294 assert refused ["ok" ] is False
286295 assert refused ["error" ]["code" ] == "ARG_ERROR"
287296 assert secret .read_text (encoding = "utf-8" ) == "not-a-render-key\n "
@@ -309,6 +318,8 @@ def test_a_project_server_takes_its_secrets_directory(tmp_path: Path) -> None:
309318 assert done .returncode == 0 , done .stderr
310319 tools = {tool ["name" ] for tool in json .loads (done .stdout )["tools" ]}
311320 assert "secrets_render" in tools
321+ assert {"operator_list" , "operator_show" } <= tools
322+ assert "operator_approve" not in tools
312323
313324
314325def test_a_project_call_cannot_write_outside_the_project (tmp_path : Path ) -> None :
0 commit comments