File size: 4,401 Bytes
051f280 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | """routes_web_agent.py β the door that lets a person TEST the web agent (wave 31, R10 / D-51).
The owner's words are the whole reason this file exists: *"we already laid the foundation of this
but never test anything."* The capability is otherwise reachable only from inside an automation
run, which means the first person to discover it is broken is a customer at 3am.
GET /api/v1/web-agent/capability any session β can this deployment run a web step, and
if not, the SENTENCE saying why
POST /api/v1/web-agent/test ADMIN β run one real `web_read` and show what
came back, or the sentence
β NEITHER ROUTE IS THE SEAM. `automation_engine` calls `web_agent.run_step` directly (contract
C5); these are an operator surface over the same function, so a green test here and a red run
there cannot disagree about anything except the input.
β `POST /test` BLOCKS FOR ~10-30 s and COSTS A FRACTION OF A CENT. It is `def`, not `async def`,
so FastAPI runs it in the threadpool and one test cannot stall the event loop for everybody.
β ON THE URL IT WILL FETCH: the fetch happens inside an ephemeral HF Job on Hugging Face's
network, never from this server, so this is not a door into our own infrastructure. It is still
admin-gated, because it spends money and because D-51 Β§5's authorisation posture ("only systems
the tenant is authorised to use, at their instruction") is not something an ordinary member
should be able to commit the tenant to.
β THIS ROUTER IS NOT MOUNTED YET. `main.py` belongs to another lane this wave, so the one
`app.include_router(routes_web_agent.router)` line is a cross-fence ask β and
`verify_web_agent.py` FAILS until it lands, deliberately: three finished routers once shipped
404-dead behind entirely green gates, and a gate that tolerates it is how that happens twice.
"""
from fastapi import APIRouter, Body, Depends
import web_agent
from deps import Session, require_session, err
from routes_admin import admin_gate
router = APIRouter(prefix="/api/v1")
MAX_URL = 2000
MAX_SELECTOR = 400
@router.get("/web-agent/capability")
def web_agent_capability(session: Session = Depends(require_session)):
"""Can a web step run here at all? Configuration, not liveness β see `web_agent.capability`.
Deliberately NARROW: it answers the question a UI needs ("may I offer this, and what do I say
if not") and withholds the deployment detail (namespace, which token key, the image) that
only an operator has any use for. Nothing here is ever a credential.
"""
cap = web_agent.capability()
return {"ready": bool(cap["ready"]), "reason": cap["reason"],
"runnableKinds": cap["runnableKinds"], "profile": cap["profile"]}
@router.post("/web-agent/test")
def web_agent_test(session: Session = Depends(admin_gate), body: dict = Body(...)):
"""Run ONE real `web_read` and report exactly what the seam returned.
The response mirrors the seam's own contract rather than flattening it: `ok` plus a `value`,
or `ok:false` plus the SENTENCE. A test surface that turns a named failure into "something
went wrong" would hide the one thing it exists to show.
"""
url = str(body.get("url") or "").strip()
selector = str(body.get("selector") or "").strip()
if len(url) > MAX_URL or len(selector) > MAX_SELECTOR:
raise err(400, "too_long", "the URL or selector is longer than this door accepts")
step = {"kind": "web_read", "id": "test", "url": url, "selector": selector,
"attr": (body.get("attr") or "text"), "all": bool(body.get("all")),
"timeoutMs": int(body.get("timeoutMs") or 20000)}
if body.get("waitFor"):
step["waitFor"] = str(body["waitFor"])
notes = []
result, error = web_agent.run_step(
step, {"tenant": session.tenant, "runId": f"test-{session.tenant}-{session.uname}",
"log": notes.append})
if error:
# 200 with `ok:false`, not a 4xx: the request was well-formed and the ANSWER is that the
# web step did not succeed. A 500 here would make an ordinary "the selector matched
# nothing" look like a server fault.
return {"ok": False, "error": error, "log": notes[-6:]}
return {"ok": True, "result": result, "log": notes[-6:]}
|