File size: 7,846 Bytes
6ff09d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env python
"""Scripted-heuristic smoke test β€” no LLM API key required.

Drives the env with a deterministic policy that roughly mimics what a
well-prompted stock LLM would do in the first ~30 probes: hit /users without
auth (discover 401), hit with the starter token, list docs, test a state
transition, observe some 403 responses, submit a modest belief graph, finalize.

Target reward range: 0.2 - 0.5. If the actual reward falls OUTSIDE this band,
the env or matcher is miscalibrated for baseline training.

Run:
    python scripts/smoke_test_scripted.py
or against a deployed Space:
    SPACE_URL=https://you-protocol_one_env.hf.space python scripts/smoke_test_scripted.py
"""

from __future__ import annotations

import os
import sys
import threading
import time

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
if ROOT not in sys.path:
    sys.path.insert(0, ROOT)


def _start_local_server(port: int) -> None:
    import uvicorn
    from server.app import app
    cfg = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning")
    server = uvicorn.Server(cfg)
    threading.Thread(target=server.run, daemon=True).start()

    import httpx
    for _ in range(50):
        try:
            if httpx.get(f"http://127.0.0.1:{port}/health", timeout=0.5).status_code == 200:
                return
        except Exception:
            pass
        time.sleep(0.1)
    raise RuntimeError("server never came up")


def run_episode(base_url: str) -> float:
    from client import ProtocolOneEnv
    from models import ProtocolOneAction

    auth_full = {"Authorization": "Bearer token_full"}
    auth_read = {"Authorization": "Bearer token_read"}

    # The belief graph the agent will eventually submit. We build it up
    # as probes confirm expectations, so it only contains things we've
    # actually observed (no hallucinated endpoints = no false-claim penalty).
    belief: dict = {"endpoints": [], "resources": [], "auth": {"type": "bearer", "scopes_observed": []}}

    def add_ep(method: str, path: str, **extra) -> None:
        belief["endpoints"].append({"method": method, "path": path, **extra})

    env = ProtocolOneEnv(base_url=base_url).sync()
    with env:
        env.reset()

        # --- Discovery phase: cheap probes to learn the surface --- #
        probes: list[tuple[str, str, dict, dict | None]] = [
            # Auth probes (unauth then authed)
            ("GET", "/users", {}, None),
            ("GET", "/users", auth_full, None),
            ("GET", "/_/health", {}, None),
            ("GET", "/auth/whoami", auth_full, None),
            ("GET", "/users/me", auth_full, None),
            ("GET", "/users/u_alice", auth_full, None),
            ("GET", "/users/u_alice/documents", auth_full, None),
            ("GET", "/docs", auth_full, None),
            ("GET", "/docs/d_intro", auth_full, None),
            # Scope probes β€” discover 403 behavior for scopes
            ("GET", "/auth/scopes", auth_read, None),  # should 403
            # State-machine probes
            ("POST", "/docs/d_specs/publish", auth_full, None),  # draft -> published
            ("POST", "/docs/d_specs/archive", auth_full, None),  # published -> archived
            ("POST", "/docs/d_old/publish", auth_full, None),  # archived -> invalid 409
            ("POST", "/users/u_alice/suspend", auth_full, None),  # active -> suspended
            ("POST", "/users/u_alice/restore", auth_full, None),  # suspended -> active
            # Deletion + idempotency
            ("DELETE", "/docs/d_old", auth_full, None),  # 200
            ("DELETE", "/docs/d_old", auth_full, None),  # 404 (already-gone)
            # Creation / validation
            ("POST", "/docs", auth_full, {"title": "smoke"}),
            ("POST", "/docs", auth_full, {}),  # 422 missing_title
        ]

        scopes_observed: set[str] = set()
        for method, path, headers, body in probes:
            result = env.step(ProtocolOneAction(
                tool="probe",
                args={"method": method, "path": path, "headers": headers, "body": body},
            ))
            text = result.observation.text or ""
            # Parse status code from "[Probe N/M] HTTP XXX"
            if "HTTP 401" in text:
                pass
            if "HTTP 403" in text and "required" in text:
                # A 403 tells us *what scope* was required β€” extract it
                for line in text.splitlines():
                    if '"required"' in line:
                        # crude parse: extract value after "required":
                        import re
                        m = re.search(r'"required"\s*:\s*"([^"]+)"', line)
                        if m:
                            scopes_observed.add(m.group(1))

        # --- Build the belief graph to simulate a stock-LLM baseline.
        # A real untrained LLM in ~30 probes would find the obvious endpoints,
        # infer auth + 1-2 scopes, partially describe resources, and miss
        # state-machine nuances. We deliberately include a plausible hallucination
        # to exercise the false-claim penalty. Target: ~0.25-0.45 reward.

        # Core endpoints the agent would find quickly (10/18), with
        # minimal/partial details:
        add_ep("GET", "/users", auth_required=True, auth_scope="users:read")
        add_ep("POST", "/users", auth_required=True,
               params=[{"name": "email", "type": "string", "location": "body"}])
        add_ep("GET", "/users/{id}", auth_required=True)
        add_ep("DELETE", "/users/{id}", auth_required=True)
        add_ep("GET", "/docs", auth_required=True, auth_scope="docs:read")
        add_ep("POST", "/docs", auth_required=True)
        add_ep("GET", "/docs/{id}", auth_required=True)
        add_ep("POST", "/docs/{id}/publish", auth_required=True)
        add_ep("GET", "/auth/whoami", auth_required=True)
        add_ep("GET", "/_/health", auth_required=False)
        # Realistic hallucination: untrained LLM guesses endpoint that feels RESTful
        add_ep("PUT", "/users/{id}", auth_required=True)  # spec has PATCH, not PUT

        # Resources β€” only User, partial fields, no state machine:
        belief["resources"].append({
            "name": "User",
            "fields": [
                {"name": "id", "type": "string"},
                {"name": "email", "type": "string"},
                {"name": "role", "type": "string"},
            ],
        })

        # Auth inference β€” type right, only 2 of 5 scopes
        belief["auth"] = {
            "type": "bearer",
            "scopes_observed": sorted({"users:read", "docs:read"} | scopes_observed),
        }

        # Push it all via update_model then finalize
        env.step(ProtocolOneAction(tool="update_model", args={"delta": belief}))
        result = env.step(ProtocolOneAction(tool="finalize", args={}))

    reward = float(result.reward or 0.0)
    return reward


def main() -> int:
    space_url = os.environ.get("SPACE_URL")
    if space_url:
        print(f"Using remote SPACE_URL={space_url}")
        base_url = space_url
    else:
        port = 8766
        print(f"Booting local uvicorn on :{port} …")
        _start_local_server(port)
        base_url = f"http://127.0.0.1:{port}"

    reward = run_episode(base_url)
    print(f"\nScripted-heuristic reward: {reward:.3f}")

    if not (0.2 <= reward <= 0.6):
        # Smoke test band: 0.2-0.5 for a stock LLM per master plan, widening
        # to 0.6 for the scripted heuristic which is a bit stronger at
        # hard-coding endpoint details.
        print(f"⚠ reward {reward:.3f} outside expected [0.2, 0.6] β€” check matcher/env")
        return 1

    print(f"βœ“ reward in expected band β€” env end-to-end loop works")
    return 0


if __name__ == "__main__":
    sys.exit(main())