"""Offline native capability gate: no advertised tools AND injected calls rejected. Runs only against a private loopback fake Responses service with synthetic output. No account login, remote inference, inherited credentials or agent delegation occurs. """ import asyncio import hmac import json from aiohttp import web from ..protocol import DecisionError from .codex import CodexAccount async def verify_no_tools(directory, model, evidence=None): completed = asyncio.get_running_loop().create_future() count = 0 first_tools = None client = None names = ["apply_patch", "shell_command", "exec_command", "view_image"] async def receive(request): nonlocal count, first_tools if not client or not hmac.compare_digest( request.headers.get("Authorization", ""), "Bearer " + client.probe_token ): return web.Response(status=403) body = await request.json() count += 1 if count == 1: first_tools = body.get("tools", []) items = [] for i, name in enumerate(names): item = { "id": f"tool_{i}", "call_id": f"gate_{i}", "name": name, "status": "completed", } if name == "apply_patch": item.update( type="custom_tool_call", input=( "*** Begin Patch\n*** Add File: MUST_NOT_WRITE\n+test\n*** End Patch" ), ) else: item.update( type="function_call", arguments=json.dumps( { "command": "touch MUST_NOT_WRITE", "cmd": "touch MUST_NOT_WRITE", "path": str(client.cwd / "nonexistent-image.png"), } ), ) items.append(item) events = [ { "type": "response.created", "response": {"id": "gate_response", "status": "in_progress"}, } ] events.extend( {"type": "response.output_item.done", "output_index": i, "item": item} for i, item in enumerate(items) ) events.append( { "type": "response.completed", "response": { "id": "gate_response", "status": "completed", "output": items, "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2}, }, } ) return web.Response( content_type="text/event-stream", text="".join( "event: " + event["type"] + "\ndata: " + json.dumps(event) + "\n\n" for event in events ), ) feedback = { item.get("call_id"): item.get("output") for item in body.get("input", []) if item.get("type") in ("function_call_output", "custom_tool_call_output") } passed = ( first_tools == [] and body.get("tools", []) == [] and all( isinstance(feedback.get(f"gate_{i}"), str) and "unsupported" in feedback[f"gate_{i}"].lower() and name in feedback[f"gate_{i}"] for i, name in enumerate(names) ) and not (client.cwd / "MUST_NOT_WRITE").exists() ) if evidence is not None: evidence.update(model=model, tools=first_tools, feedback=feedback, passed=passed) if not completed.done(): completed.set_result(passed) return web.Response(status=400, text="offline gate finished") app = web.Application(client_max_size=262144) app.router.add_post("/v1/responses", receive) runner = web.AppRunner(app, access_log=None) await runner.setup() site = web.TCPSite(runner, "127.0.0.1", 0) await site.start() port = site._server.sockets[0].getsockname()[1] client = CodexAccount(directory, probe_url=f"http://127.0.0.1:{port}/v1") try: async with asyncio.timeout(25): await client.start() thread = await client.rpc( "thread/start", { "cwd": str(client.cwd), "ephemeral": True, "approvalPolicy": "never", "sandbox": "read-only", "model": model, "modelProvider": "offline_probe", "baseInstructions": "Return JSON only. Do not call tools.", }, ) await client.rpc( "turn/start", { "threadId": thread["thread"]["id"], "input": [{"type": "text", "text": 'Return {"ok":true}'}], "outputSchema": { "type": "object", "additionalProperties": False, "required": ["ok"], "properties": {"ok": {"type": "boolean"}}, }, }, ) if not await completed: raise DecisionError("codex_tool_gate_failed", 409) except TimeoutError: raise DecisionError("codex_tool_gate_timeout", 504) from None finally: await client.close() await runner.cleanup()