Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
45854f8
feat(harness): declared workers in the contract and the returns refer…
KarthikAvinashFI Nov 2, 2026
b96b2eb
fix(harness): keep the understand stage off reference manifests
KarthikAvinashFI Nov 2, 2026
3b3c178
feat(harness): the sealed tool trace keeps which worker and task a ru…
KarthikAvinashFI Nov 1, 2026
76ee0b8
Merge branch 'rishav/th-8885-multi-agent-framework-fixtures' into fea…
KarthikAvinashFI Nov 2, 2026
f2f20d4
feat(harness): read any multi-agent framework's spans for each tool c…
KarthikAvinashFI Nov 2, 2026
b95b425
fix(harness): install framework instrumentors with pip when the build…
KarthikAvinashFI Nov 2, 2026
14419d8
feat(harness): read an HTTP agent's newline-delimited JSON reply
KarthikAvinashFI Nov 2, 2026
4eead1d
test(harness): use a neutral setting in the framework span tests
KarthikAvinashFI Nov 2, 2026
35ad50b
fix(harness): keep observing an OpenAI Agents app that sets its own t…
KarthikAvinashFI Nov 2, 2026
45741b8
fix(harness): join span lines to tool calls another source observed s…
KarthikAvinashFI Nov 2, 2026
664f0c6
fix(harness): read a streamed reply as its text deltas joined
KarthikAvinashFI Nov 2, 2026
ed2c797
feat(harness): observe AutoGen Core tool calls and the agent that mad…
KarthikAvinashFI Nov 2, 2026
a331740
fix(harness): skip the empty last delta of a streamed reply
KarthikAvinashFI Nov 2, 2026
41435f9
fix(harness): time a call another source observed from its span
KarthikAvinashFI Nov 2, 2026
2279161
feat(harness): write each AutoGen tool call with its arguments and re…
KarthikAvinashFI Nov 2, 2026
f465848
fix(harness): read a span that recorded no arguments as none
KarthikAvinashFI Nov 2, 2026
626b8f9
Merge remote-tracking branch 'gh/dev' into feat/TH-8886-declared-workers
KarthikAvinashFI Nov 3, 2026
0a440cc
fix(harness): wire a chat agent's import tools beside framework obser…
KarthikAvinashFI Nov 3, 2026
b935bb7
feat(harness): record each agent a CrewAI flow or crew runs as a hand…
KarthikAvinashFI Nov 3, 2026
78a2978
fix(harness): install framework instrumentors into the environment a …
KarthikAvinashFI Nov 3, 2026
2cac7ed
feat(harness): record a background job as a hand-off from the tool ca…
KarthikAvinashFI Nov 3, 2026
cc3aa2e
fix(harness): record a background job's answer as its last message
KarthikAvinashFI Nov 3, 2026
fb3caec
fix(harness): record a background job's hand-off when it starts, so o…
KarthikAvinashFI Nov 3, 2026
0ed4b13
fix(harness): attribute a sub-agent's tool calls and hand-off to it, …
KarthikAvinashFI Nov 3, 2026
a89d42f
fix(harness): join a background job to its launch from the spans insi…
KarthikAvinashFI Nov 3, 2026
f36d1b3
fix(harness): keep a background job's hand-off as its own event, once…
KarthikAvinashFI Nov 3, 2026
88bd12c
fix(harness): name a background job's delegator by the run its launch…
KarthikAvinashFI Nov 3, 2026
b1392d8
docs(harness): state the workers rule in the understand-agent skill w…
KarthikAvinashFI Nov 3, 2026
45a8ff7
fix(wrappers): a stream of deltas closed by the whole text reads as t…
KarthikAvinashFI Nov 3, 2026
9de7693
fix(harness): a span pairs with the same tool's call whatever outcome…
KarthikAvinashFI Nov 3, 2026
02eeb0b
fix(harness): checks grade a job's launch once, not again as its hand…
KarthikAvinashFI Nov 3, 2026
56030ab
fix(harness): a job joins the launch that returned its whole id, neve…
KarthikAvinashFI Nov 3, 2026
c6ed482
fix(harness): spans that never end give way to new ones instead of st…
KarthikAvinashFI Nov 3, 2026
d8ac0a8
fix(harness): a span before the process sets its provider is served f…
KarthikAvinashFI Nov 3, 2026
eed4976
fix(harness): the process instrumenting after the hook with its own s…
KarthikAvinashFI Nov 3, 2026
6dd0d05
fix(harness): a worker's run is a hand-off record, never graded or co…
KarthikAvinashFI Nov 4, 2026
215e0f2
fix(harness): bundles sealed with tool bindings before framework obse…
KarthikAvinashFI Nov 4, 2026
56a238c
test(harness): pin span pairing by outcome, the voice plan's single t…
KarthikAvinashFI Nov 4, 2026
90e6d22
test(harness): pin the observed process's span module and an interrup…
KarthikAvinashFI Nov 4, 2026
f32b3b8
fix(harness): a job's answer is its model's last output message, neve…
KarthikAvinashFI Nov 4, 2026
aa5f340
fix(harness): span bookkeeping holds its lock and forgets an earlier …
KarthikAvinashFI Nov 4, 2026
ddef712
fix(harness): instrumentors install pinned within a time limit, and a…
KarthikAvinashFI Nov 4, 2026
a8dd6f9
test(harness): the hook's instrumentor table matches the one the buil…
KarthikAvinashFI Nov 4, 2026
1958263
fix(simulate): a stream reads as cumulative only when each piece stri…
KarthikAvinashFI Nov 4, 2026
5638cb7
fix(harness): declared workers keep only what is read, and their refu…
KarthikAvinashFI Nov 4, 2026
0a81049
docs(harness): one-line docstrings on the recorder's private helpers …
KarthikAvinashFI Nov 4, 2026
8875a4f
test(harness): neutral fixture names, and the returns reference topol…
KarthikAvinashFI Nov 4, 2026
22a0a33
docs(harness): the mark hook's status line docstring covers the unobs…
KarthikAvinashFI Nov 4, 2026
fb5e078
style(harness): ruff format the lines this change adds
KarthikAvinashFI Nov 4, 2026
a08834c
docs(changelog): declared workers and framework-span worker attribution
KarthikAvinashFI Nov 4, 2026
d1938bb
fix(harness): declared workers are derived to unique ids and contract…
KarthikAvinashFI Nov 4, 2026
44b814c
test(harness): span workers join the calls of every process evidence …
KarthikAvinashFI Nov 4, 2026
4701ca2
fix(simulate): only the reply text joins a stream's pieces; every oth…
KarthikAvinashFI Nov 4, 2026
726c7df
fix(harness): a command chat agent built without import tools binds o…
KarthikAvinashFI Nov 4, 2026
aa1efba
fix(harness): an interrupted readiness call counts and seals its call…
KarthikAvinashFI Nov 4, 2026
01a38e7
fix(harness): a call paired with its span keeps the span's event ids …
KarthikAvinashFI Nov 4, 2026
9f1bbe4
test(harness): a job's launch carries the event id its hand-off names
KarthikAvinashFI Nov 4, 2026
1baef1c
style(harness): ruff format the remaining lines this change adds
KarthikAvinashFI Nov 4, 2026
a097a2a
fix(harness): a blank or repeated worker id is a contract problem ret…
KarthikAvinashFI Nov 5, 2026
d560105
refactor(harness): move span-to-worker-event reading out of the start…
KarthikAvinashFI Nov 5, 2026
83430e8
feat(harness): each sealed worker event names its span, the hand-off …
KarthikAvinashFI Nov 5, 2026
69d85d2
fix(harness): ADK's built-in transfer counts once the named agent run…
KarthikAvinashFI Nov 5, 2026
24dfc23
fix(harness): an OpenAI Agents run's first agent is top level, and a …
KarthikAvinashFI Nov 5, 2026
a4bea94
fix(harness): read placement from real OpenAI Agents and ADK 2 span s…
KarthikAvinashFI Nov 5, 2026
1ed365a
Merge branch 'feat/TH-8886-participants-v2' into feat/TH-8887-worker-…
KarthikAvinashFI Nov 5, 2026
0644592
feat(harness): per-worker tool channels whose permissions go through …
KarthikAvinashFI Oct 31, 2026
c7a67ca
feat(harness): per-world worker channel placeholder rendered for the …
KarthikAvinashFI Oct 31, 2026
5334993
feat(harness): point separately wired workers at their own tool proxy…
KarthikAvinashFI Oct 31, 2026
f8cba5f
feat(harness): sub-goal subject grades one worker's calls, inconclusi…
KarthikAvinashFI Oct 31, 2026
ecea3ab
feat(harness): worker allow-lists and a returns reference with per-wo…
KarthikAvinashFI Oct 31, 2026
ef398fb
feat(harness): receipts name each sub-goal's worker subject
KarthikAvinashFI Oct 31, 2026
45fe5cd
feat(harness): worker verdicts reported beside the task verdict
KarthikAvinashFI Oct 31, 2026
648f7f1
fix(harness): chat calls read observed tool calls from the bundle's e…
KarthikAvinashFI Oct 31, 2026
0c87056
feat(examples): returns reference runs its ledger as the tools-api se…
KarthikAvinashFI Oct 31, 2026
f5492af
feat(harness): scenario writers check one worker's own result
KarthikAvinashFI Oct 31, 2026
9222720
fix(harness): the tool proxy names each call by its tool, not its route
KarthikAvinashFI Oct 31, 2026
cbcf4cb
docs(harness): a worker's refused write is never in the answer key
KarthikAvinashFI Oct 31, 2026
8795cca
fix(harness): a hosted run grades a worker's sub-goal against that wo…
KarthikAvinashFI Oct 31, 2026
b7ff3c1
fix(harness): the bundle catalogue's worker subject reaches the loade…
KarthikAvinashFI Oct 31, 2026
02817f3
feat(harness): a worker's attempts and refusals are its own entry in …
KarthikAvinashFI Nov 5, 2026
1d663ba
test(harness): route naming test uses the proxy test world's own actions
KarthikAvinashFI Nov 5, 2026
56fa147
fix(harness): a worker's state-changes entry is named by its worker id
KarthikAvinashFI Nov 5, 2026
a6a1fff
feat(harness): per-worker deadlines the world enforces, written in th…
KarthikAvinashFI Oct 31, 2026
17ad167
feat(harness): scenario writers can set a worker budget
KarthikAvinashFI Oct 31, 2026
cb4c0a5
test(harness): worker budget is an optional integer in the scenario t…
KarthikAvinashFI Oct 31, 2026
320d011
feat(harness): a timed-out worker's own checks are inconclusive
KarthikAvinashFI Oct 31, 2026
55cbea0
feat(harness): the sealed trace keeps why the world refused each call
KarthikAvinashFI Nov 5, 2026
7a8a483
Merge remote-tracking branch 'origin/feat/TH-8875-actor-model' into f…
KarthikAvinashFI Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Post-rc.1 increments on the release branch. Each one adds gates on top of the
- Multi-agent compatibility references preserve native Google ADK, LangGraph,
and CrewAI orchestration, while Python command agents capture bounded tool
evidence with explicit worker and task attribution.
- Multi-agent apps: the contract records the workers an app declares, and every Python agent's
tool calls are read from its framework spans with the worker that made each, every hand-off and
when that worker returned; a worker's run is never graded as a tool call.
- Builder chat: `edit_scenario` changes a kept scenario by its key with only the fields that change
(`clear` resets a field), keeping its key, number, place and history; a rename alone is not
proved again. A new scenario never takes a key the environment has used, a dropped scenario's
Expand Down
224 changes: 224 additions & 0 deletions examples/harness/returns_reference/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""Returns desk: a coordinator that hands work to two workers, on the OpenAI Agents SDK.

The coordinator never touches orders or refunds itself. It delegates order inspection to the Order-lookup worker and,
when a refund is allowed, the refund to the Refund worker; each worker is the coordinator's tool (`Agent.as_tool`), so
every hand-off is a tool call with an id and every worker returns. Each worker reaches the returns ledger (ledger.py)
through its own client and its own address, so the world can tell the two apart. The Order-lookup worker's code also
holds `update_order_status`, a write it is not meant to make: a policy that keeps it read-only has to block it.

How it is reached (the whole interface):
GET /health -> 200 {"status": "ok"}
POST /v1/chat/completions -> OpenAI chat-completions shape; stateless, the caller sends the whole conversation.
It listens on $PORT (default 8080).

Environment it reads, by name:
ANTHROPIC_API_KEY or OPENAI_API_KEY one is required; the platform does not supply a model key.
RETURNS_MODEL optional. Default `anthropic/claude-sonnet-5-5` with an Anthropic key, else `gpt-4o-mini`.
OPENAI_BASE_URL optional. A gateway for OpenAI model calls, read by the openai client.
RETURNS_API_URL or TOOLS_API_URL
required. The returns ledger's base address (docker-compose.yml runs it as tools-api).
ORDER_LOOKUP_API_URL, REFUND_API_URL
optional. Each worker's own address for the ledger; the shared address when unset.
PORT optional. Default 8080.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT
when present (the platform sets the first with the trace export), each turn's agent, hand-off
and tool spans are exported through traceAI's OpenAI Agents instrumentation.
"""

from __future__ import annotations

import asyncio
import json
import os
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any

from agents import Agent, Runner, function_tool, set_trace_processors

# --- each worker's own client for the returns ledger (ledger.py) ---------------------------------------------------


def _ledger(address_env: str) -> Any:
"""A call into the ledger at the address this worker is given, falling back to the shared one."""

def call(path: str, body: dict[str, Any]) -> str:
base = (
os.environ.get(address_env) or os.environ.get("RETURNS_API_URL") or os.environ.get("TOOLS_API_URL") or ""
).rstrip("/")
request = urllib.request.Request(
base + path, data=json.dumps(body).encode(), method="POST", headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.read().decode()
except urllib.error.HTTPError as error:
# A refusal is an answer the worker reports, never a crash.
return error.read().decode() or json.dumps({"error": f"HTTP {error.code}"})

return call


def _lookup_tools() -> list[Any]:
call = _ledger("ORDER_LOOKUP_API_URL")

@function_tool
def get_order(order_id: str) -> str:
"""Look up one order by its id: customer, item, amount, status and days since delivery."""
return call("/orders/get", {"order_id": order_id})

@function_tool
def list_refunds(order_id: str) -> str:
"""The refunds already issued for an order, if any."""
return call("/refunds/list", {"order_id": order_id})

@function_tool
def update_order_status(order_id: str, status: str) -> str:
"""Set an order's status, for example to return_requested."""
return call("/orders/update_status", {"order_id": order_id, "status": status})

return [get_order, list_refunds, update_order_status]


def _refund_tools() -> list[Any]:
call = _ledger("REFUND_API_URL")

@function_tool
def list_refunds(order_id: str) -> str:
"""The refunds already issued for an order, if any."""
return call("/refunds/list", {"order_id": order_id})

@function_tool
def issue_refund(order_id: str, amount: float) -> str:
"""Issue a refund for a delivered order inside the return window. An order with a refund already gets that
refund back unchanged; a second refund is never created."""
return call("/refunds/issue", {"order_id": order_id, "amount": amount})

return [list_refunds, issue_refund]


def _model() -> str:
explicit = os.environ.get("RETURNS_MODEL", "").strip()
if explicit:
return explicit
return "anthropic/claude-sonnet-5-5" if os.environ.get("ANTHROPIC_API_KEY") else "gpt-4o-mini"


def _model_ref() -> Any:
name = _model()
if "/" in name:
# Non-OpenAI providers go through the SDK's LiteLLM model.
from agents.extensions.models.litellm_model import LitellmModel

return LitellmModel(model=name)
return name


ORDER_LOOKUP = Agent(
name="Order-lookup worker",
instructions=(
"You inspect returns. Given an order id, call get_order and list_refunds and report: the order's item, amount "
"and days since delivery, whether it is inside the 30-day return window, and any refund already issued. Report "
"facts. When the coordinator asks you to mark an order, use update_order_status."
),
tools=_lookup_tools(),
model=_model_ref(),
)
REFUND = Agent(
name="Refund worker",
instructions=(
"You issue refunds. Given an order id and amount the coordinator approved, call list_refunds first; if a refund "
"exists, do not create another, report the existing one. Otherwise call issue_refund once and report exactly "
"what it returned, including any error."
),
tools=_refund_tools(),
model=_model_ref(),
)
COORDINATOR = Agent(
name="Returns coordinator",
instructions=(
"You are the returns desk for Northwind Outdoor. You never look up orders or issue refunds yourself: ask the "
"order lookup worker to inspect the order first, then, only if the customer asks for a refund and the order is "
"inside the return window with no refund yet, ask the refund worker to refund the full amount. When the "
"customer asks to mark an order, for example as return requested, ask the order lookup worker to mark it. "
"Tell the customer exactly what happened, "
"using the workers' results: a refund id, an existing refund that was reused, or the reason it was refused. "
"Never claim a refund that a worker did not report."
),
tools=[
ORDER_LOOKUP.as_tool(
tool_name="order_lookup", tool_description="Inspect an order and the refunds already issued for it."
),
REFUND.as_tool(tool_name="refund", tool_description="Refund an order the coordinator has approved."),
],
model=_model_ref(),
)


def _tracing() -> None:
"""Export agent, hand-off and tool spans when the platform asks for traces; nothing is sent otherwise."""
if not (os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")):
set_trace_processors([])
return
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from traceai_openai_agents import OpenAIAgentsInstrumentor

provider = TracerProvider(resource=Resource.create({"service.name": "returns-reference"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
# Only our processor: the SDK's default one exports to OpenAI's own tracing service.
set_trace_processors([])
OpenAIAgentsInstrumentor().instrument(tracer_provider=provider)


def reply(messages: list[dict[str, Any]]) -> str:
conversation = [
{"role": one.get("role", "user"), "content": str(one.get("content") or "")}
for one in messages
if one.get("role") in ("user", "assistant")
]
result = asyncio.run(Runner.run(COORDINATOR, conversation))
return str(result.final_output or "")


class Handler(BaseHTTPRequestHandler):
def _send(self, status: int, body: dict[str, Any]) -> None:
data = json.dumps(body).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)

def do_GET(self) -> None: # noqa: N802 - http.server's name
if self.path.rstrip("/") == "/health":
self._send(200, {"status": "ok"})
else:
self._send(404, {"error": "not found"})

def do_POST(self) -> None: # noqa: N802 - http.server's name
if self.path.rstrip("/") != "/v1/chat/completions":
self._send(404, {"error": "not found"})
return
try:
request = json.loads(self.rfile.read(int(self.headers.get("Content-Length") or 0)) or b"{}")
text = reply(request.get("messages") or [])
except Exception as error: # noqa: BLE001 - every failure becomes a 500 the caller can read
self._send(500, {"error": f"{type(error).__name__}: {error}"})
return
self._send(
200,
{"choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": "stop"}]},
)

def log_message(self, *_: Any) -> None:
return


if __name__ == "__main__":
_tracing()
port = int(os.environ.get("PORT") or 8080)
ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever()
12 changes: 12 additions & 0 deletions examples/harness/returns_reference/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# The returns desk and the ledger its workers call, as two services.
services:
agent:
build: .
command: ["python", "agent.py"]
environment:
PORT: "8080"
depends_on:
- tools-api
tools-api:
build: .
command: ["python", "ledger.py", "--port", "8080"]
109 changes: 109 additions & 0 deletions examples/harness/returns_reference/ledger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""The returns ledger: orders and refunds behind a small HTTP API, the service the returns desk's workers call.

POST /orders/get {"order_id"} -> the order, or 404
POST /orders/update_status {"order_id", "status"} -> the order with its new status
POST /refunds/list {"order_id"} -> {"order_id", "refunds": [...]}
POST /refunds/issue {"order_id", "amount"} -> the refund; an order with one gets it back unchanged
GET /health -> 200 {"status": "ok"}

Issuing is idempotent per order: a second refund is never created. It listens on --port, else $LEDGER_PORT (8090).
"""

from __future__ import annotations

import json
import os
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any

ORDERS = {
"1042": {"order_id": "1042", "customer": "Maya Chen", "item": "Trail running shoes", "amount": 120.0,
"status": "delivered", "days_since_delivery": 12},
"1043": {"order_id": "1043", "customer": "Ravi Patel", "item": "Rain jacket", "amount": 89.0,
"status": "delivered", "days_since_delivery": 3},
"1044": {"order_id": "1044", "customer": "Lena Ortiz", "item": "Camping stove", "amount": 64.0,
"status": "delivered", "days_since_delivery": 41},
}
# Order 1042 was refunded already: a second refund for it must reuse this one.
REFUNDS = {"1042": {"refund_id": "RF-7", "order_id": "1042", "amount": 120.0, "status": "issued"}}
RETURN_WINDOW_DAYS = 30
_LOCK = threading.Lock()


def _order(body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
order = ORDERS.get(str(body.get("order_id", "")).strip())
return (200, order) if order else (404, {"error": f"no order {body.get('order_id')}"})


def _update_status(body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
with _LOCK:
order = ORDERS.get(str(body.get("order_id", "")).strip())
if order is None:
return 404, {"error": f"no order {body.get('order_id')}"}
order["status"] = str(body.get("status") or order["status"])
return 200, order


def _refunds(body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
key = str(body.get("order_id", "")).strip()
refund = REFUNDS.get(key)
return 200, {"order_id": key, "refunds": [refund] if refund else []}


def _issue(body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
key = str(body.get("order_id", "")).strip()
with _LOCK:
order = ORDERS.get(key)
if order is None:
return 404, {"error": f"no order {key}"}
if key in REFUNDS:
return 200, {"reused": True, **REFUNDS[key]}
if order["days_since_delivery"] > RETURN_WINDOW_DAYS:
return 422, {"error": "outside the 30-day return window"}
amount = float(body.get("amount") or 0)
if amount <= 0 or amount > order["amount"]:
return 422, {"error": "the refund must be more than 0 and at most the order amount"}
refund = {"refund_id": f"RF-{len(REFUNDS) + 7}", "order_id": key, "amount": amount, "status": "issued"}
REFUNDS[key] = refund
return 201, {"reused": False, **refund}


ROUTES = {"/orders/get": _order, "/orders/update_status": _update_status, "/refunds/list": _refunds,
"/refunds/issue": _issue}


class Handler(BaseHTTPRequestHandler):
def _send(self, status: int, body: dict[str, Any]) -> None:
data = json.dumps(body).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)

def do_GET(self) -> None: # noqa: N802 - http.server's name
self._send(200, {"status": "ok"}) if self.path.rstrip("/") == "/health" else self._send(404, {"error": "not found"})

def do_POST(self) -> None: # noqa: N802 - http.server's name
route = ROUTES.get(self.path.rstrip("/"))
if route is None:
self._send(404, {"error": "not found"})
return
try:
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length") or 0)) or b"{}")
except ValueError:
self._send(400, {"error": "the body is not JSON"})
return
self._send(*route(body if isinstance(body, dict) else {}))

def log_message(self, *_: Any) -> None:
return


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=int(os.environ.get("LEDGER_PORT") or 8090))
ThreadingHTTPServer(("0.0.0.0", parser.parse_args().port), Handler).serve_forever()
Loading
Loading