f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
154 lines
5.7 KiB
Python
154 lines
5.7 KiB
Python
"""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()
|