From 8f31a866523d42799058c8f252a0fa23959cfe42 Mon Sep 17 00:00:00 2001
From: turinglambdaai
Date: Fri, 23 Oct 2026 11:53:35 +0800
Subject: [PATCH 1/3] feat: implement the 2026-10-auth-1 in-repo slice (free
raw export, gated report, release-instant eligibility)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Raw capture export (pcap-export, Wireshark retained replay) leaves the
legacy Pro gate — the free scope's raw-data access, asserted over the
wire with no entitlement. The engineering report becomes the gated paid
evidence surface (the old UI copy claimed Pro, but no gate existed);
gate-check's export action becomes report.
Update eligibility moves to the authorization decision's rule: the
candidate release's authenticated published_at against the entitlement's
updates_through, never the current date. A release inside the window
stays stageable forever, an expired window never invalidates the
installed tier, a date-only cutoff extends to that day's end, and a
candidate without a readable instant fails closed. Fixed UTC boundary
tests cover cutoff-1s / at-cutoff / cutoff+1s and the past-window
install keeps its tier and rights.
---
app/backend.rkt | 45 +++++++++++------
app/gate.rkt | 31 +++++-------
support/updates.rkt | 48 +++++++++++++-----
tests/backend-wire-test.rkt | 19 ++++++-
tests/gate-test.rkt | 99 +++++++++++++++++++++++++++----------
5 files changed, 167 insertions(+), 75 deletions(-)
diff --git a/app/backend.rkt b/app/backend.rkt
index 1f221be..a5cf383 100644
--- a/app/backend.rkt
+++ b/app/backend.rkt
@@ -655,17 +655,16 @@
(count (lambda (d) (hash-ref d 'is_ptp #f)) decoded)))
(ok-action (length decoded))))
+;; Raw capture export is free scope (the authorization decision moves it
+;; out of the legacy Pro gate); only the engineering report is gated.
(define-rpc (pcap-export [path : String] : ActionResult)
- (cond
- [(not (eq? (gate-check 'export) #t)) (deny-pro (gate-check 'export))]
- [else
- (with-handlers ([exn:fail? (lambda (e) (fail-action (exn-message e)))])
- (define frames
- (reverse
- (for/list ([f (in-list (packet-store-snapshot app-packets 999999))])
- (packet->capture-file-frame f))))
- (write-capture-file path frames)
- (ok-action (length frames)))]))
+ (with-handlers ([exn:fail? (lambda (e) (fail-action (exn-message e)))])
+ (define frames
+ (reverse
+ (for/list ([f (in-list (packet-store-snapshot app-packets 999999))])
+ (packet->capture-file-frame f))))
+ (write-capture-file path frames)
+ (ok-action (length frames))))
(define-rpc (wireshark-launch [action : String] [iface : String] : ActionResult)
(cond
@@ -680,8 +679,7 @@
(log-add! app-logs 'app 'info
(format "Wireshark live: iface=~a filter=~a" iface filter))
(ok-action result))]
- [(not (eq? (gate-check 'export) #t)) (deny-pro (gate-check 'export))]
- [else (ok-action (open-retained-in-wireshark))]))
+ [(string=? action "retained") (ok-action (open-retained-in-wireshark))]))
(define-rpc (series-window [series-id : String] : SeriesWindow)
(cond
@@ -795,8 +793,13 @@
(write-diagnostic-snapshot! path snapshot)
(ok-action (hasheq 'path (path->string path) 'redacted #t))))
+;; The engineering report is the paid evidence surface (PRICING.md's paid
+;; scope); raw data access around it stays free.
(define-rpc (report-export [path : String] [iface : String] [role : String] [reference : String] : ActionResult)
- (with-handlers ([exn:fail? (lambda (e) (fail-action (exn-message e)))])
+ (cond
+ [(not (eq? (gate-check 'report) #t)) (deny-pro (gate-check 'report))]
+ [else
+ (with-handlers ([exn:fail? (lambda (e) (fail-action (exn-message e)))])
(define nics (detect-interfaces))
(define chosen-iface
(if (string=? iface "")
@@ -835,7 +838,7 @@
(write-engineering-report-json! path report)
(write-engineering-report-markdown! markdown-path report)
(ok-action (hasheq 'json (path->string path)
- 'markdown (path->string markdown-path)))))
+ 'markdown (path->string markdown-path))))]))
(define-rpc (params-merge
[domain : (Optional Int64)]
@@ -930,8 +933,18 @@
(with-handlers
([exn:fail?
(lambda (e) (flow-set! 'failed #:error (exn-message e)))])
- (update-prepare! 'download (unbox update-manifest-box)
- #:window-open? (gate-update-window-open?)))
+ (define candidate (unbox update-manifest-box))
+ ;; Eligibility is the candidate's authenticated release instant
+ ;; against the entitlement cutoff — never the current date
+ ;; (docs/update-contract.md).
+ (define published-at
+ (and candidate
+ (update-manifest-published-at
+ (update-candidate-manifest candidate))))
+ (update-prepare! 'download candidate
+ #:window-open?
+ (gate-update-window-open?
+ #:release-published-at published-at)))
(update-state-now))
;; Applies what is staged, per install shape (deb: pkexec with the
diff --git a/app/gate.rkt b/app/gate.rkt
index d07629d..080f428 100644
--- a/app/gate.rkt
+++ b/app/gate.rkt
@@ -3,9 +3,10 @@
;; Commercial tier gate built on Permit (github.com/turinglambdaai/permit)
;;
;; free — passive listening (capture+decode), offline pcap analysis,
-;; simulator, config editor/preview, presets (watch free)
+;; simulator, config editor/preview, presets, raw capture
+;; export and Wireshark replay
;; pro — engine control (GM/slave), reference-source management,
-;; unlimited packet store, pcap/report exports (drive paid)
+;; unlimited packet store, engineering reports (drive paid)
;; trial — 14 days of Pro from the moment the user starts it, tracked
;; as permit local-trial state (last_seen high-water mark)
;;
@@ -16,7 +17,6 @@
;; (ins_ + 32 hex) so the vendor can issue credentials bound to it.
(require json
- racket/date
racket/file
racket/string
permit
@@ -178,24 +178,15 @@
(define info (unbox gate-entitlement-box))
(and info (hash-ref info 'updates-through #f)))
-;; Whether staging an official update is part of what this install holds.
-;; Free and trial always update; a perpetual Pro install updates through
-;; its entitlement's updates_through day.
-(define (iso-today)
- (define d (current-date))
- (format "~a-~a-~a"
- (date-year d)
- (~r2 (date-month d))
- (~r2 (date-day d))))
-
-(define (~r2 n)
- (define s (number->string n))
- (if (< (string-length s) 2) (string-append "0" s) s))
-
-(define (gate-update-window-open? [today (iso-today)])
+;; Whether staging an official update with the given authenticated
+;; release instant is part of what this install holds. Free and trial
+;; always update; a perpetual Pro install stages releases published at or
+;; before its entitlement's updates_through — the current date plays no
+;; role (see updates-window-open?).
+(define (gate-update-window-open? #:release-published-at published-at)
(updates-window-open? #:pro? (eq? (unbox gate-cache) 'pro)
#:updates-through (gate-updates-through)
- #:today today))
+ #:release-published-at published-at))
;; Action-level check; returns #t or an error message string.
(define (gate-check action)
@@ -203,7 +194,7 @@
#t
(case action
[(engine) "引擎控制(GM / 从钟)是 Pro 功能。可开始 14 天免费试用或输入许可证。"]
- [(export) "报文导出与报告生成是 Pro 功能。"]
+ [(report) "工程报告生成是 Pro 功能。"]
[(reference) "外接参考源管理是 Pro 功能。"]
[else "此功能需要 Pro 许可证。"])))
diff --git a/support/updates.rkt b/support/updates.rkt
index c624b33..d57c79f 100644
--- a/support/updates.rkt
+++ b/support/updates.rkt
@@ -59,6 +59,11 @@
update-info
update-info-box
update-manifest-box
+ ;; re-exports the start-download eligibility needs: the
+ ;; candidate's manifest carries the authenticated release
+ ;; instant the window decision consumes
+ update-candidate-manifest
+ update-manifest-published-at
call-under-watchdog!)
;; ---- product facts -------------------------------------------------------------
@@ -132,21 +137,40 @@
;; ---- the updates window (the commercial contract in code) ----------------------
;;
;; A perpetual entitlement carries updates_through — the instant official
-;; updates stop being part of the purchase. When it passes the application
-;; never degrades (perpetual rights: the last obtained official version
-;; keeps working); only the in-app updater stops staging newer official
-;; builds, and the renewal message says why. Free and trial installs always
-;; update. The end day itself counts as entitled (date-prefix comparison),
-;; so a host east of UTC still updates on the morning the instant has
-;; technically passed — a one-year window does not need sharper edges.
+;; updates stop being part of the purchase. Eligibility answers one
+;; question about the candidate release: was it published at or before
+;; that instant? The inputs are the signed feed's authenticated
+;; published_at and the entitlement's cutoff — never the current date
+;; (PRICING.md's authorization decision: "Use release time and
+;; updates_through for update eligibility; the current date must not
+;; suddenly invalidate an acquired version"). A release inside the window
+;; therefore stays stageable forever, an expired window never degrades
+;; the running install (perpetual rights: the last obtained official
+;; version keeps working), and the renewal message explains a refusal.
+;; Free and trial installs always update.
+
+;; UTC RFC 3339 values in this shape ("YYYY-MM-DD" or
+;; "YYYY-MM-DDTHH:MM:SSZ") compare chronologically as plain strings, so
+;; no date parsing is involved. A date-only cutoff extends to that day's
+;; end: the entitlement's day counts as entitled, and a host east of UTC
+;; still updates on that morning — a one-year window does not need
+;; sharper edges.
+(define (normalize-cutoff through)
+ (if (= (string-length through) 10)
+ (string-append through "T23:59:59Z")
+ through))
(define (updates-window-open? #:pro? pro?
#:updates-through through
- #:today today)
- (or (not pro?)
- (not (string? through))
- (string<=? today
- (substring through 0 (min 10 (string-length through))))))
+ #:release-published-at published-at)
+ (cond
+ [(not pro?) #t]
+ [(not (string? through)) #t]
+ ;; An authenticated candidate without a readable instant cannot prove
+ ;; it is inside the window: refuse staging for gated installs
+ ;; (fail-closed). The rivet manifest schema always carries one.
+ [(not (string? published-at)) #f]
+ [else (string<=? published-at (normalize-cutoff through))]))
;; ---- feed location -------------------------------------------------------------
diff --git a/tests/backend-wire-test.rkt b/tests/backend-wire-test.rkt
index f8dd03c..fe3d412 100644
--- a/tests/backend-wire-test.rkt
+++ b/tests/backend-wire-test.rkt
@@ -101,7 +101,24 @@
(define-values (stopped stop-error) (call-rpc "engine-stop"))
(check-false stop-error)
- (check-equal? (list-ref stopped 0) "#f"))
+ (check-equal? (list-ref stopped 0) "#f")
+
+ ;; Raw-data access is free scope (the 2026-10-auth-1 decision): pcap
+ ;; export answers on the wire with no entitlement at all, while the
+ ;; engineering report refuses with need-pro before doing any work.
+ (define export-path (build-path scratch "free-export.pcap"))
+ (define-values (exported export-error)
+ (call-rpc "pcap-export" (path->string export-path)))
+ (check-false export-error)
+ (check-equal? (list-ref exported 0) #t)
+ (check-true (file-exists? export-path) "the free export wrote a capture file")
+
+ (define report-path (build-path scratch "free-report.json"))
+ (define-values (report report-error)
+ (call-rpc "report-export" (path->string report-path) "" "" ""))
+ (check-false report-error)
+ (check-equal? (take report 2) (list #f #t))
+ (check-false (file-exists? report-path) "the refused report wrote nothing"))
(lambda ()
(kill-thread server-thread)
(delete-directory/files scratch)))
diff --git a/tests/gate-test.rkt b/tests/gate-test.rkt
index 6d1eeec..5b38292 100644
--- a/tests/gate-test.rkt
+++ b/tests/gate-test.rkt
@@ -119,9 +119,11 @@
(check-equal? (hash-ref ok-info 'issuer) "test.issuer" "issuer comes from the credential")
;; ---- the updates window (the commercial contract in gate form) ----
-;; A perpetual Pro purchase carries updates_through; the window decision
-;; never re-parses the file (the gate caches what the tier came from) and
-;; the end day itself still counts as entitled.
+;; A perpetual Pro purchase carries updates_through. Eligibility is a
+;; pure function of the candidate release's authenticated instant
+;; against that cutoff — the current date is not an input — so a release
+;; inside the window stays stageable forever and no check date can
+;; invalidate an acquired version.
(define window-path
(issue-to-file
@@ -133,27 +135,40 @@
"updates_through surfaces from the credential")
(check-true (updates-window-open? #:pro? #f #:updates-through #f
- #:today "2026-10-22")
+ #:release-published-at "2099-01-01T00:00:00Z")
"a free install always updates")
(check-true (updates-window-open? #:pro? #t #:updates-through #f
- #:today "2099-01-01")
+ #:release-published-at "2099-01-01T00:00:00Z")
"a Pro entitlement without the constraint updates forever")
-(check-true (updates-window-open? #:pro? #t
- #:updates-through "2027-10-16T00:00:00Z"
- #:today "2027-10-15")
- "the day before the instant is entitled")
-(check-true (updates-window-open? #:pro? #t
- #:updates-through "2027-10-16T00:00:00Z"
- #:today "2027-10-16")
- "the end day itself is entitled")
-(check-false (updates-window-open? #:pro? #t
- #:updates-through "2027-10-16T00:00:00Z"
- #:today "2027-10-17")
- "the day after the instant is not")
-(check-false (updates-window-open? #:pro? #t
- #:updates-through "2026-10-22"
- #:today "2026-10-23")
- "a date-only constraint behaves the same")
+
+;; fixed UTC boundaries around the cutoff instant
+(define window-cutoff "2027-10-16T00:00:00Z")
+(check-true (updates-window-open? #:pro? #t #:updates-through window-cutoff
+ #:release-published-at "2027-10-15T23:59:59Z")
+ "one second before the cutoff is inside the window")
+(check-true (updates-window-open? #:pro? #t #:updates-through window-cutoff
+ #:release-published-at window-cutoff)
+ "a release published exactly at the cutoff is entitled")
+(check-false (updates-window-open? #:pro? #t #:updates-through window-cutoff
+ #:release-published-at "2027-10-16T00:00:01Z")
+ "one second after the cutoff is outside")
+(check-false (updates-window-open? #:pro? #t #:updates-through window-cutoff
+ #:release-published-at #f)
+ "a candidate without an authenticated instant fails closed")
+;; a date-only cutoff covers through the end of that day
+(check-true (updates-window-open? #:pro? #t #:updates-through "2027-10-16"
+ #:release-published-at "2027-10-16T23:59:59Z")
+ "a date-only cutoff extends to that day's end")
+(check-false (updates-window-open? #:pro? #t #:updates-through "2027-10-16"
+ #:release-published-at "2027-10-17T00:00:00Z")
+ "the day after a date-only cutoff is outside")
+;; whatever the wall clock says, the same inputs answer the same way —
+;; the decision reads no clock at all
+(check-equal? (updates-window-open? #:pro? #t #:updates-through window-cutoff
+ #:release-published-at "2020-01-01T00:00:00Z")
+ (updates-window-open? #:pro? #t #:updates-through window-cutoff
+ #:release-published-at "2020-01-01T00:00:00Z")
+ "eligibility is deterministic in its two inputs")
;; The gate-level decision, driven through a real activation. The seam
;; points the gate at this test's throwaway store before the replacement
@@ -166,15 +181,47 @@
(check-equal? (gate-init!) 'pro)
(check-equal? (gate-updates-through) "2027-10-16T00:00:00Z"
"the gate carries the window from the cached verification")
- (check-false (gate-update-window-open? "2027-10-17")
- "a closed window refuses official updates")
- (check-true (gate-update-window-open? "2027-10-16")
- "the end day still updates")
+ (check-false (gate-update-window-open?
+ #:release-published-at "2027-10-16T00:00:01Z")
+ "a release after the cutoff is not staged")
+ (check-true (gate-update-window-open?
+ #:release-published-at "2027-10-16T00:00:00Z")
+ "the cutoff-instant release still stages")
+ ;; An old release published inside a window that has since passed stays
+ ;; eligible, and the install keeps every right it had — the window
+ ;; gates staging, never use.
+ (check-true (gate-update-window-open?
+ #:release-published-at "2026-10-01T00:00:00Z")
+ "an in-window release stays eligible after the window passes")
+ (check-equal? (gate-tier) 'pro "the tier outlives the window")
+ (check-true (gate-pro?) "an expired window never invalidates the install")
+ (check-true (gate-check 'engine) "Pro actions keep working past the window")
(gate-deactivate!)
(check-equal? (gate-init!) 'free)
- (check-true (gate-update-window-open? "2099-01-01")
+ (check-true (gate-update-window-open?
+ #:release-published-at "2099-01-01T00:00:00Z")
"a free install updates again after deactivation"))
+;; A credential whose window is already entirely past is still a valid
+;; perpetual Pro entitlement: the install upgrades and keeps working.
+(let ([scratch (make-temporary-file "gate-past-~a" 'directory)])
+ (putenv "GPTP_STUDIO_HOME" (path->string scratch))
+ (putenv "GPTP_STUDIO_TRUST_STORE" (path->string trust-store-path))
+ (define past-path
+ (issue-to-file
+ (gate-payload #:updates-through "2020-01-01T00:00:00Z")
+ (make-temporary-file "gate-pastwin-~a.permit.json")))
+ (define-values (p-act-ok? _p-subject) (gate-activate! past-path))
+ (check-true p-act-ok? "a past-window credential is still a valid purchase")
+ (check-equal? (gate-init!) 'pro "an expired window still grants the pro tier")
+ (check-true (gate-check 'engine) "an expired window install keeps Pro actions")
+ (check-false (gate-update-window-open?
+ #:release-published-at "2021-01-01T00:00:00Z")
+ "nothing published after the cutoff stages")
+ (check-true (gate-update-window-open?
+ #:release-published-at "2019-12-31T23:59:59Z")
+ "anything published inside the cutoff stays eligible"))
+
;; ---- expired use window ----
(define expired-path
From c04e173a64484911e7a0110be2d9c4ba1afbe5bb Mon Sep 17 00:00:00 2001
From: turinglambdaai
Date: Fri, 23 Oct 2026 11:53:35 +0800
Subject: [PATCH 2/3] docs: reconcile the update contract, landing page and
pricing status with the decision
The update contract documents the release-instant eligibility and
corrects the AppImage note (a distribution format, not an update
shape); the landing page gains the AppImage install card its release
assets already ship; PRICING's free-scope paragraph now states the
0.4.0 implementation status instead of the pending-move caveat.
---
PRICING.md | 2 +-
PRICING.zh-CN.md | 2 +-
docs/index.html | 4 ++++
docs/update-contract.md | 24 +++++++++++++++---------
4 files changed, 21 insertions(+), 11 deletions(-)
diff --git a/PRICING.md b/PRICING.md
index b10ac0a..99dc06d 100644
--- a/PRICING.md
+++ b/PRICING.md
@@ -18,7 +18,7 @@ Engineering value and support effort are higher than for an everyday utility. A
## Free and paid scope
-Free / evaluation: Passive capture, protocol decoding, offline import, simulator, parameter previews, existing diagnostics and raw-data export are intended to be free. Moving raw capture export out of its legacy Pro gate still requires implementation; current installers must not be described as already unrestricted. Apache-2.0 source rights remain unchanged.
+Free / evaluation: Passive capture, protocol decoding, offline import, simulator, parameter previews, existing diagnostics and raw-data export are intended to be free. Raw capture export (pcap export and Wireshark replay) moved out of its legacy Pro gate in 0.4.0; engineering report generation remains the gated paid evidence surface. Apache-2.0 source rights remain unchanged.
Paid: Official Pro GM/slave runtime control, reference-source management, repeatable test orchestration, evidence-backed comparison reports and validated reference combinations. Seats mean named users, not network adapters, devices or concurrent sessions. Only delivered and verified capabilities may appear in an active offer.
diff --git a/PRICING.zh-CN.md b/PRICING.zh-CN.md
index 673f886..b5ea5bf 100644
--- a/PRICING.zh-CN.md
+++ b/PRICING.zh-CN.md
@@ -18,7 +18,7 @@
## 免费与付费边界
-免费 / 评估:被动抓包、协议解码、离线导入、模拟器、参数预览、已有诊断与原始数据导出应免费。原始捕获导出从旧 Pro 门禁移到 Free 的实现尚待完成;不能据此称当前安装包已放开。Apache-2.0 源码权利不变。
+免费 / 评估:被动抓包、协议解码、离线导入、模拟器、参数预览、已有诊断与原始数据导出应免费。原始捕获导出(pcap 导出与 Wireshark 回放)自 0.4.0 起移出旧 Pro 门禁;工程报告生成保持付费证据面。Apache-2.0 源码权利不变。
付费:官方 Pro 的 GM/从钟运行控制、参考源管理、重复测试编排、带证据的比较报告与已验证参考组合;席位是命名用户,不是网卡数/设备数/并发数。仅交付并验证的能力可列在正式 offer。
diff --git a/docs/index.html b/docs/index.html
index be79f86..41bfb2f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -116,6 +116,10 @@ Self-contained; installs nothing outside its directory.
+ AppImage
curl -LO https://github.com/turinglambdaai/gptp-studio/releases/latest/download/gptp-studio-linux-x64.AppImage
+chmod +x gptp-studio-linux-x64.AppImage
+./gptp-studio-linux-x64.AppImage --doctor
+./gptp-studio-linux-x64.AppImage
Bundles its GTK4 dependency closure; no install step.