Skip to content

Commit 5915c74

Browse files
authored
chore: harden hosted dashboard demo (#30)
1 parent c51456d commit 5915c74

6 files changed

Lines changed: 220 additions & 1 deletion

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ receipt. The manual Next/Back controls and optional auto-play reveal one result
189189
by the running gateway. Security decisions are live local computations; settlement is
190190
labelled simulation unless separate verified devnet evidence exists.
191191

192+
For an optional public review deployment, use the checked-in Render Blueprint and follow
193+
the [hosted demonstration guide](docs/DEPLOYMENT.md). The hosted fallback requires no
194+
wallet credentials and continues to label settlement as simulated.
195+
192196
After installing the official Pay CLI, attempt one external ephemeral-wallet sandbox purchase through the same gateway:
193197

194198
```bash
@@ -248,6 +252,8 @@ This is an early product thesis, not evidence of existing customers, revenue, or
248252
- [Threat model](docs/THREAT_MODEL.md)
249253
- [Metadata sanitization](docs/PRIVACY.md)
250254
- [Local security dashboard](docs/DASHBOARD.md)
255+
- [Hosted demonstration](docs/DEPLOYMENT.md)
256+
- [Judge demonstration runbook](docs/JUDGE_RUNBOOK.md)
251257
- [Audit receipts and local event stream](docs/AUDIT.md)
252258
- [Request integrity and replay protection](docs/INTEGRITY.md)
253259
- [Single-use wallet authorization](docs/AUTHORIZATION.md)

docs/DEPLOYMENT.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Hosted demonstration
2+
3+
The public dashboard is a demonstration surface for the running SolGuard gateway. It is
4+
not a production payment service. Every security decision is computed by the Python
5+
gateway; settlement remains explicitly labelled `SIMULATED` unless separately verified
6+
devnet evidence is supplied.
7+
8+
## Render deployment
9+
10+
The repository includes a Render Blueprint in `render.yaml`. It creates one Python web
11+
service in Frankfurt, installs the package, starts the dashboard on Render's assigned
12+
port, and checks `/healthz` before routing traffic. Automatic deployments wait for the
13+
linked commit's checks to pass.
14+
15+
New Render services currently default to Python 3.14, which is outside this project's
16+
supported range. The existing committed `.python-version` intentionally selects Python
17+
3.11.
18+
19+
1. In Render, choose **New > Blueprint**.
20+
2. Connect `ShieldTech-Ltd/SolGuard`.
21+
3. For a temporary review deployment, select `feature/stage-dashboard`. After the pull
22+
request is reviewed and merged, deploy the protected integration branch instead.
23+
4. Confirm the Blueprint path is `render.yaml` and create the service.
24+
5. Wait for the health check to pass, then open the generated `onrender.com` URL.
25+
6. Run all four dashboard scenarios and confirm the page still says
26+
**SIMULATED SETTLEMENT**.
27+
28+
No wallet key, RPC credential, Pay.sh credential, or x402 secret is required or permitted
29+
for this hosted fallback. Do not add secrets to the Blueprint. A real devnet exercise is
30+
a separate, opt-in CLI path documented in `X402_LIVE_DEVNET.md`.
31+
32+
## Local production-shaped check
33+
34+
Run the same command shape used by Render:
35+
36+
```bash
37+
uv run solguard-dashboard --host 0.0.0.0 --port 10000
38+
```
39+
40+
Then verify:
41+
42+
```bash
43+
curl http://127.0.0.1:10000/healthz
44+
```
45+
46+
Expected response:
47+
48+
```json
49+
{"service":"solguard-dashboard","settlement":"simulated","status":"ok"}
50+
```
51+
52+
The dashboard state is intentionally process-local and resets on restart. This keeps the
53+
fallback deterministic and avoids presenting the prototype as durable production
54+
infrastructure.

docs/JUDGE_RUNBOOK.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Judge demonstration runbook
2+
3+
This runbook presents the live dashboard as evidence of the running gateway, not as a
4+
mock product animation. Use the guided mode so each screen is tied to one gateway result.
5+
6+
## Before presenting
7+
8+
1. Start `uv run solguard-dashboard` or open the verified hosted fallback.
9+
2. Confirm `/healthz` returns `status: ok`.
10+
3. Open the dashboard at a desktop width and select **Reset demo**.
11+
4. Confirm the header says **LIVE SECURITY ENGINE** and **SIMULATED SETTLEMENT**.
12+
5. Keep `uv run solguard-demo --skip-paysh` ready as a terminal fallback.
13+
14+
Do not describe a settlement as real, devnet, or on-chain unless its transaction signature
15+
has been independently verified for that run.
16+
17+
## Two-minute story
18+
19+
### 1. State the problem
20+
21+
> An autonomous agent can hold valid wallet access and still make a dangerous payment.
22+
> SolGuard decides whether that payment should reach the signer before money can move.
23+
24+
Select **Start guided demo**. Point to the separation between the agent, SolGuard, the
25+
wallet boundary, and settlement.
26+
27+
### 2. Allow ordinary commerce
28+
29+
Run **Normal payment** and step through the request, integrity, mandate, behaviour, and
30+
authorization stages.
31+
32+
Visible proof:
33+
34+
- decision is `ALLOW`;
35+
- the request and policy digests come from the current event;
36+
- signing state is `SIGNED`; and
37+
- settlement is visibly labelled `SIMULATED`.
38+
39+
Say: "The owner did not approve this payment manually. The agent operated inside a
40+
pre-approved mandate, so the gateway allowed it autonomously."
41+
42+
### 3. Escalate uncertainty without stopping everything
43+
44+
Run **First-seen recipient**.
45+
46+
Visible proof:
47+
48+
- decision is `REQUIRE_APPROVAL`;
49+
- the reason identifies recipient novelty; and
50+
- no signing authorization reaches the wallet.
51+
52+
Say: "Autonomy is bounded, not removed. An unfamiliar but otherwise plausible payment is
53+
paused for approval instead of being silently signed."
54+
55+
### 4. Prove integrity enforcement
56+
57+
Run **Replay attack**.
58+
59+
Visible proof:
60+
61+
- the reused request is `BLOCK`;
62+
- the reason is `REQUEST_REPLAYED`; and
63+
- signing state remains `NOT_SIGNED`.
64+
65+
Say: "A valid-looking payment cannot be reused with the same per-agent nonce."
66+
67+
### 5. Trigger the memorable attack
68+
69+
Run **Compound drain** and advance to the behaviour, wallet, and evidence stages.
70+
71+
Visible proof:
72+
73+
- the decision is `BLOCK`;
74+
- the reasons include compound drain, new recipient, and high velocity;
75+
- the wallet is `NOT_SIGNED` and has no settlement reference;
76+
- the simulated wallet balance does not fall for the blocked request;
77+
- the email and bearer token are redacted; and
78+
- the receipt chain verifies.
79+
80+
Say: "The important output is not the red alert. It is the missing signature. The blocked
81+
request never crossed the wallet boundary."
82+
83+
### 6. Close with the product
84+
85+
> Payment protocols answer how an agent can pay. SolGuard answers whether that agent
86+
> should be allowed to pay. We are looking for an agent platform or wallet partner to
87+
> validate this pre-signing boundary with its own sandbox payment intents.
88+
89+
## If the network or hosted page fails
90+
91+
1. Start the local dashboard and repeat the same four controls.
92+
2. If a browser is unavailable, run `uv run solguard-demo --skip-paysh` and show the
93+
emitted decisions and invariants.
94+
3. Use the recorded evidence package only as clearly labelled backup footage.
95+
96+
Never replace a failed external integration with an unlabelled simulation. Reliability is
97+
part of the demonstration; accurate labels preserve reviewer trust.
98+
99+
## Fast reviewer questions
100+
101+
- **Is settlement real?** The hosted fallback uses simulated settlement. The security
102+
decisions, signing-state outcome, sanitized receipt, and dashboard metrics are computed
103+
by the running local gateway.
104+
- **Can blocked traffic poison the baseline?** No. The behaviour engine learns recipients
105+
and amounts only from approved traffic.
106+
- **What stops replay?** Freshness and per-agent nonce checks run before policy and signing.
107+
- **What happens if a control fails?** The gateway fails closed and does not issue signing
108+
authorization.
109+
- **Is this production-ready?** No. Durable replay coordination, multi-tenant isolation,
110+
authenticated administration, hardened key custody, high availability, and independent
111+
security review remain production work.

render.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
- type: web
3+
name: solguard-demo
4+
runtime: python
5+
plan: free
6+
region: frankfurt
7+
buildCommand: pip install .
8+
startCommand: solguard-dashboard --host 0.0.0.0 --port $PORT
9+
healthCheckPath: /healthz
10+
autoDeployTrigger: checksPass

src/solguard/dashboard.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
392392
"""Serve static dashboard assets and local scenario endpoints."""
393393

394394
server: DashboardServer
395+
server_version = "SolGuard"
396+
sys_version = ""
395397

396398
_ASSETS: ClassVar[dict[str, tuple[str, str]]] = {
397399
"/": ("index.html", "text/html; charset=utf-8"),
@@ -402,6 +404,15 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
402404

403405
def do_GET(self) -> None:
404406
path = urlparse(self.path).path
407+
if path == "/healthz":
408+
self._send_json(
409+
{
410+
"service": "solguard-dashboard",
411+
"settlement": "simulated",
412+
"status": "ok",
413+
}
414+
)
415+
return
405416
if path == "/api/state":
406417
self._send_json(self.server.runtime.snapshot())
407418
return
@@ -469,7 +480,15 @@ def _send_bytes(
469480
self.send_header("Content-Length", str(len(body)))
470481
self.send_header("Cache-Control", "no-store")
471482
self.send_header("X-Content-Type-Options", "nosniff")
472-
self.send_header("Content-Security-Policy", "default-src 'self'; connect-src 'self'")
483+
self.send_header(
484+
"Content-Security-Policy",
485+
"default-src 'self'; base-uri 'none'; connect-src 'self'; "
486+
"form-action 'none'; frame-ancestors 'none'; object-src 'none'",
487+
)
488+
self.send_header("Cross-Origin-Resource-Policy", "same-origin")
489+
self.send_header("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
490+
self.send_header("Referrer-Policy", "no-referrer")
491+
self.send_header("X-Frame-Options", "DENY")
473492
self.end_headers()
474493
self.wfile.write(body)
475494

tests/test_dashboard.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,25 @@ def test_server_delivers_dashboard_assets(path: str, content_type: str, marker:
283283
assert headers["Cache-Control"] == "no-store"
284284
assert headers["X-Content-Type-Options"] == "nosniff"
285285
assert "default-src 'self'" in headers["Content-Security-Policy"]
286+
assert "frame-ancestors 'none'" in headers["Content-Security-Policy"]
287+
assert headers["Cross-Origin-Resource-Policy"] == "same-origin"
288+
assert headers["Permissions-Policy"] == "camera=(), microphone=(), geolocation=()"
289+
assert headers["Referrer-Policy"] == "no-referrer"
290+
assert headers["X-Frame-Options"] == "DENY"
291+
assert headers["Server"].strip() == "SolGuard"
292+
293+
294+
def test_server_exposes_dependency_free_health_check() -> None:
295+
with running_server(runtime()) as base:
296+
status, body, headers = get(f"{base}/healthz")
297+
298+
assert status == 200
299+
assert json.loads(body) == {
300+
"service": "solguard-dashboard",
301+
"settlement": "simulated",
302+
"status": "ok",
303+
}
304+
assert headers["Cache-Control"] == "no-store"
286305

287306

288307
def test_stage_dashboard_packages_visible_enforcement_proof() -> None:

0 commit comments

Comments
 (0)