Skip to content

Commit c33bb1e

Browse files
authored
feat(tools): add --json output and expand invariant coverage in gauntlet.py (#17)
gauntlet.py verified a running bundle over raw CDP but only checked a small set of surfaces and emitted human-readable text, so it could not be consumed by CI or a dashboard. Keep the zero-deps, raw-CDP design and add: - A --json flag that emits {surfaces, invariants, ok} on stdout and sets the exit code accordingly. Without it the text report is unchanged. - Seven new invariants, all already spoofed by the default persona the bundle launcher injects: - navigator.hardwareConcurrency present and plausible (1-128) - navigator.deviceMemory present - navigator.languages well-formed (a real list, length >= 2) - Intl.DateTimeFormat().resolvedOptions().timeZone is an IANA zone - canvas 2D noise present (a flat grey fill reads back perturbed) - navigator.gpu adapter present (WebGPU, via the swiftshader adapter) - navigator.plugins non-empty The surface-collection expression becomes an async IIFE so it can await navigator.gpu.requestAdapter(); ws_eval already awaits the promise. Document --json in --help and the module header, and commit a reference docs/gauntlet-sample.json showing a passing report. Closes #12
1 parent c2f7c0e commit c33bb1e

2 files changed

Lines changed: 101 additions & 12 deletions

File tree

docs/gauntlet-sample.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"surfaces": {
3+
"webdriver": false,
4+
"platform": "Win32",
5+
"uaWindows": true,
6+
"mp4": "probably",
7+
"emoji": true,
8+
"webglRenderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)",
9+
"hardwareConcurrency": 16,
10+
"deviceMemory": 8,
11+
"languages": [
12+
"en-US",
13+
"en"
14+
],
15+
"timezone": "America/New_York",
16+
"canvasNoisePixels": 37,
17+
"webgpuAdapter": true,
18+
"webgpuVendor": "nvidia",
19+
"plugins": 5
20+
},
21+
"invariants": {
22+
"webdriver is false": true,
23+
"platform is Win32": true,
24+
"UA is Windows": true,
25+
"mp4 codec works": true,
26+
"emoji font present": true,
27+
"WebGL renderer spoofed": true,
28+
"hardwareConcurrency plausible": true,
29+
"deviceMemory present": true,
30+
"languages well-formed": true,
31+
"timezone is an IANA zone": true,
32+
"canvas 2D noise present": true,
33+
"WebGPU adapter present": true,
34+
"plugins non-empty": true
35+
},
36+
"ok": true
37+
}

tools/gauntlet.py

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
stealth invariants. Exit 0 = clean, non-zero = a surface regressed.
88
99
Usage:
10-
tools/gauntlet.py --bundle /path/to/tilion-fortress [--port 9333] [--keep]
10+
tools/gauntlet.py --bundle /path/to/tilion-fortress [--port 9333] [--keep] [--json]
11+
12+
With --json it prints a machine-readable {surfaces, invariants, ok} report on stdout
13+
(and still sets the exit code) so CI and dashboards can consume it; see
14+
docs/gauntlet-sample.json. Without it, the human-readable report is unchanged.
1115
1216
No third-party deps — raw CDP over a hand-rolled WebSocket so it runs anywhere
1317
Python 3 does.
@@ -66,6 +70,9 @@ def main():
6670
ap.add_argument("--bundle", required=True, help="path to extracted tilion-fortress/")
6771
ap.add_argument("--port", type=int, default=9333)
6872
ap.add_argument("--keep", action="store_true", help="leave the browser running")
73+
ap.add_argument("--json", action="store_true",
74+
help="emit a machine-readable {surfaces, invariants, ok} JSON report on "
75+
"stdout (and set the exit code) instead of the human text report")
6976
args = ap.parse_args()
7077

7178
launcher = os.path.join(args.bundle, "tilion.cmd" if os.name == "nt" else "tilion")
@@ -91,36 +98,81 @@ def main():
9198
except Exception:
9299
time.sleep(0.5)
93100

94-
checks = ws_eval(args.port, """JSON.stringify((function(){
101+
# One async pass over the page. WebGPU adapter info needs an awaited requestAdapter(),
102+
# and canvas 2D noise is measured by filling a flat grey rect and counting how many
103+
# read-back pixels drift off it — a clean engine leaves it perfectly flat.
104+
checks = ws_eval(args.port, """(async function(){
105+
function canvasNoisePixels(){
106+
try{
107+
var cv=document.createElement('canvas'); cv.width=64; cv.height=16;
108+
var ctx=cv.getContext('2d'); ctx.fillStyle='rgb(128,128,128)'; ctx.fillRect(0,0,64,16);
109+
var d=ctx.getImageData(0,0,cv.width,cv.height).data, n=0;
110+
for(var i=0;i<d.length;i+=4){ if(d[i]!==128||d[i+1]!==128||d[i+2]!==128) n++; }
111+
return n;
112+
}catch(e){ return -1; }
113+
}
114+
var webgpuAdapter=false, webgpuVendor='';
115+
try{
116+
if(navigator.gpu){
117+
var a=await navigator.gpu.requestAdapter();
118+
webgpuAdapter=!!a;
119+
if(a && a.requestAdapterInfo){ try{ webgpuVendor=(await a.requestAdapterInfo()).vendor||''; }catch(e){} }
120+
}
121+
}catch(e){}
95122
var g=document.createElement('canvas').getContext('webgl');
96123
var dbg=g&&g.getExtension('WEBGL_debug_renderer_info');
97-
return {
124+
return JSON.stringify({
98125
webdriver: navigator.webdriver,
99126
platform: navigator.platform,
100127
uaWindows: /Windows NT/.test(navigator.userAgent),
101128
mp4: document.createElement('video').canPlayType('video/mp4; codecs="avc1.42E01E"'),
102129
emoji: document.fonts.check('32px "Segoe UI Emoji"'),
103-
webglRenderer: dbg ? g.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : ''
104-
};
105-
})())""")
130+
webglRenderer: dbg ? g.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '',
131+
hardwareConcurrency: navigator.hardwareConcurrency,
132+
deviceMemory: navigator.deviceMemory,
133+
languages: navigator.languages,
134+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
135+
canvasNoisePixels: canvasNoisePixels(),
136+
webgpuAdapter: webgpuAdapter,
137+
webgpuVendor: webgpuVendor,
138+
plugins: navigator.plugins.length
139+
});
140+
})()""")
106141
r = json.loads(checks)
107-
print("Gauntlet surfaces:", json.dumps(r, indent=2))
108142

143+
hc = r["hardwareConcurrency"]
144+
dm = r["deviceMemory"]
145+
langs = r["languages"]
109146
invariants = {
110147
"webdriver is false": r["webdriver"] is False,
111148
"platform is Win32": r["platform"] == "Win32",
112149
"UA is Windows": r["uaWindows"] is True,
113150
"mp4 codec works": r["mp4"] == "probably",
114151
"emoji font present": r["emoji"] is True,
115152
"WebGL renderer spoofed": "NVIDIA" in (r["webglRenderer"] or ""),
153+
"hardwareConcurrency plausible": isinstance(hc, int) and 1 <= hc <= 128,
154+
"deviceMemory present": isinstance(dm, (int, float)) and dm > 0,
155+
"languages well-formed": isinstance(langs, list) and len(langs) >= 2,
156+
"timezone is an IANA zone": isinstance(r["timezone"], str) and "/" in r["timezone"],
157+
"canvas 2D noise present": isinstance(r["canvasNoisePixels"], int) and r["canvasNoisePixels"] > 0,
158+
"WebGPU adapter present": r["webgpuAdapter"] is True,
159+
"plugins non-empty": isinstance(r["plugins"], int) and r["plugins"] > 0,
116160
}
117161
failed = [k for k, v in invariants.items() if not v]
118-
for k, v in invariants.items():
119-
print(f" [{'PASS' if v else 'FAIL'}] {k}")
120-
if failed:
121-
print(f"\nGAUNTLET FAILED: {len(failed)} regression(s).")
162+
ok = not failed
163+
164+
if args.json:
165+
print(json.dumps({"surfaces": r, "invariants": invariants, "ok": ok}, indent=2))
166+
else:
167+
print("Gauntlet surfaces:", json.dumps(r, indent=2))
168+
for k, v in invariants.items():
169+
print(f" [{'PASS' if v else 'FAIL'}] {k}")
170+
if ok:
171+
print("\nGAUNTLET PASS — Fortress is stealth-clean.")
172+
else:
173+
print(f"\nGAUNTLET FAILED: {len(failed)} regression(s).")
174+
if not ok:
122175
sys.exit(1)
123-
print("\nGAUNTLET PASS — Fortress is stealth-clean.")
124176
finally:
125177
if not args.keep:
126178
proc.terminate()

0 commit comments

Comments
 (0)