mirror of
https://github.com/mcloh/codex-chatgpt-mcp-proxy.git
synced 2026-11-05 21:34:11 +00:00
Implement durable OAuth MCP bridge with local Codex worker and deployment guides
This commit is contained in:
36
examples/local_env.py
Normal file
36
examples/local_env.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Generate isolated loopback credentials. Run from the repository root."""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
os.umask(0o077)
|
||||
root = Path.cwd().resolve()
|
||||
private = root / "private"
|
||||
private.mkdir(mode=0o700, exist_ok=True)
|
||||
file = private / "local.env"
|
||||
if file.exists():
|
||||
raise SystemExit("Refusing to overwrite existing local environment")
|
||||
(root / "demo").mkdir(exist_ok=True)
|
||||
worker = secrets.token_urlsafe(48)
|
||||
mcp = secrets.token_urlsafe(48)
|
||||
file.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"BRIDGE_MODE=development",
|
||||
"BRIDGE_PUBLIC_URL=http://127.0.0.1:8000",
|
||||
"BRIDGE_SERVER_URL=http://127.0.0.1:8000",
|
||||
"BRIDGE_WORKER_TOKEN=" + worker,
|
||||
"BRIDGE_DEV_MCP_TOKEN=" + mcp,
|
||||
"BRIDGE_DATABASE=private/server.sqlite",
|
||||
"BRIDGE_WORKER_STATE=private/worker.sqlite",
|
||||
"BRIDGE_WORKSPACES=demo",
|
||||
'BRIDGE_WORKSPACE_MAP=\'{"demo":"' + str(root / "demo") + "\"}'",
|
||||
"BRIDGE_CODEX_MODEL=gpt-6-luna",
|
||||
"BRIDGE_POLL_SECONDS=2",
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
os.chmod(file, 0o600)
|
||||
print("Generated private/local.env; tokens are not printed")
|
||||
108
examples/smoke_real.py
Normal file
108
examples/smoke_real.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Real MCP -> separate worker -> Codex test. Requires running server and worker."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import httpx2
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
|
||||
async def main():
|
||||
url = os.environ.get("BRIDGE_SERVER_URL", "http://127.0.0.1:8000")
|
||||
token = os.environ.get("BRIDGE_MCP_ACCESS_TOKEN") or os.environ["BRIDGE_DEV_MCP_TOKEN"]
|
||||
async with httpx2.AsyncClient(headers={"Authorization": "Bearer " + token}, timeout=30) as http:
|
||||
async with streamable_http_client(url + "/mcp", http_client=http) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
|
||||
async def call(name, args):
|
||||
r = await session.call_tool(name, args)
|
||||
if r.is_error:
|
||||
raise RuntimeError(str(r.content))
|
||||
return r.structured_content
|
||||
|
||||
async def wait(task):
|
||||
deadline = time.monotonic() + 180
|
||||
while time.monotonic() < deadline:
|
||||
r = await call("get_task", {"task_id": task["id"]})
|
||||
if r["state"] in (
|
||||
"succeeded",
|
||||
"failed",
|
||||
"cancelled",
|
||||
"timed_out",
|
||||
"needs_reconciliation",
|
||||
):
|
||||
if r["state"] != "succeeded":
|
||||
raise RuntimeError(
|
||||
f"Task {r['id']} ended {r['state']}: {r['error']}"
|
||||
)
|
||||
return r
|
||||
await asyncio.sleep(2)
|
||||
raise TimeoutError("Smoke deadline exceeded; inspect task and local approvals")
|
||||
|
||||
first = await wait(
|
||||
await call(
|
||||
"submit_task",
|
||||
{
|
||||
"instruction": "Não use ferramentas, não leia arquivos. Responda exatamente: Ponte operacional",
|
||||
"workspace": "demo",
|
||||
"idempotency_key": str(uuid.uuid4()),
|
||||
"timeout_seconds": 120,
|
||||
},
|
||||
)
|
||||
)
|
||||
assert "Ponte operacional" in first["result"], first["result"]
|
||||
print("Real Codex response: OK", flush=True)
|
||||
second = await wait(
|
||||
await call(
|
||||
"continue_task",
|
||||
{
|
||||
"task_id": first["id"],
|
||||
"instruction": "Sem usar ferramentas, recorde a frase exata que você respondeu no turno anterior.",
|
||||
"idempotency_key": str(uuid.uuid4()),
|
||||
"timeout_seconds": 120,
|
||||
},
|
||||
)
|
||||
)
|
||||
assert "Ponte operacional" in second["result"]
|
||||
assert second["thread_id"] == first["thread_id"]
|
||||
print("Thread continuation across dedicated Codex processes: OK", flush=True)
|
||||
filename = "bridge-smoke-" + uuid.uuid4().hex[:12] + ".txt"
|
||||
third = await wait(
|
||||
await call(
|
||||
"submit_task",
|
||||
{
|
||||
"instruction": f"Crie apenas o arquivo {filename} no diretório de trabalho atual com o conteúdo exato Ponte operacional seguido de uma quebra de linha. Não leia outros arquivos e não altere qualquer outro arquivo.",
|
||||
"workspace": "demo",
|
||||
"idempotency_key": str(uuid.uuid4()),
|
||||
"timeout_seconds": 120,
|
||||
},
|
||||
)
|
||||
)
|
||||
workspace = json.loads(os.environ["BRIDGE_WORKSPACE_MAP"])["demo"]
|
||||
assert (Path(workspace) / filename).read_text() == "Ponte operacional\n"
|
||||
print("Harmless file created and checked: OK", flush=True)
|
||||
evidence = {
|
||||
"real_codex": True,
|
||||
"tasks": [
|
||||
{
|
||||
"id": r["id"],
|
||||
"thread_id": r["thread_id"],
|
||||
"turn_id": r["turn_id"],
|
||||
"state": r["state"],
|
||||
}
|
||||
for r in (first, second, third)
|
||||
],
|
||||
"file": filename,
|
||||
"chatgpt_originated": False,
|
||||
}
|
||||
Path("private/real-smoke.json").write_text(json.dumps(evidence, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user