Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# 更新日志 / Changelog

## 0.4.0 - 2026-10-23

**授权决策 `2026-10-auth-1` 落地:原始数据导出回归免费,更新资格改由
发布时刻判定。**(issue #35 的仓内切片)

### Added

- **原始数据导出移出旧 Pro 门禁**:`pcap-export` 与 Wireshark 回放
(retained)不再要求授权——免费层即可导出自己的捕获数据(PRICING
免费范围的「raw-data export」落地)。工程报告生成(`report-export`)
成为付费证据面:按付费范围的「带证据的报告」补上门禁(旧 UI 文案
一直宣称 Pro,但实现从未门禁)。
- **更新资格 = 发布时刻语义**:候选版本的认证 `published_at`(签名
feed 携带)与 entitlement 的 `updates_through` 判定,**当前日期不再
是输入**——窗口内的版本永远可获取,窗口过期不使已装版本失效、
不降级、不影响启动。此前按「今天 vs 窗口」判定,截止日次日连窗口
内发布的最后一个版本也无法下载。日期粒度的截止扩展到当日结束;
无可读发布时刻的候选对受门禁安装一律拒绝(fail-closed)。
`docs/update-contract.md` 记录契约;固定 UTC 边界测试覆盖
截止前 1 秒/恰在截止/截止后 1 秒与日期粒度截止。

### Changed

- 产品站(docs/index.html)补 AppImage 安装卡,对照实际发布资产;
`gate-check` 的 `export` 动作由 `report` 取代(原始导出免费后报告
是唯一付费导出面)。

## 0.3.1 - 2026-10-23

**Linux 分发面对齐 taskly 标杆 + 发布 rivet 切 pin。** 修复 v0.3.0 流水线
Expand Down
2 changes: 1 addition & 1 deletion PRICING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion PRICING.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

## 免费与付费边界

免费 / 评估:被动抓包、协议解码、离线导入、模拟器、参数预览、已有诊断与原始数据导出应免费。原始捕获导出从旧 Pro 门禁移到 Free 的实现尚待完成;不能据此称当前安装包已放开。Apache-2.0 源码权利不变。
免费 / 评估:被动抓包、协议解码、离线导入、模拟器、参数预览、已有诊断与原始数据导出应免费。原始捕获导出(pcap 导出与 Wireshark 回放)自 0.4.0 起移出旧 Pro 门禁;工程报告生成保持付费证据面。Apache-2.0 源码权利不变。

付费:官方 Pro 的 GM/从钟运行控制、参考源管理、重复测试编排、带证据的比较报告与已验证参考组合;席位是命名用户,不是网卡数/设备数/并发数。仅交付并验证的能力可列在正式 offer。

Expand Down
45 changes: 29 additions & 16 deletions app/backend.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 "")
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down
31 changes: 11 additions & 20 deletions app/gate.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
;;
Expand All @@ -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
Expand Down Expand Up @@ -178,32 +178,23 @@
(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)
(if (gate-pro?)
#t
(case action
[(engine) "引擎控制(GM / 从钟)是 Pro 功能。可开始 14 天免费试用或输入许可证。"]
[(export) "报文导出与报告生成是 Pro 功能。"]
[(report) "工程报告生成是 Pro 功能。"]
[(reference) "外接参考源管理是 Pro 功能。"]
[else "此功能需要 Pro 许可证。"])))

Expand Down
2 changes: 1 addition & 1 deletion app/state.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
settings-set!
app-version)

(define app-version "0.3.1")
(define app-version "0.4.0")

;; ---- stores (created once) -----------------------------------------------------

Expand Down
4 changes: 4 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ <h2 class="section-title"><span data-en="Start with" data-zh="第一步先运行
cd &lt;extracted-directory&gt;
./bin/gptp-studio --doctor
./bin/gptp-studio</pre><p data-en="Self-contained; installs nothing outside its directory. Useful for lab hosts where the payload should stay outside the package database." data-zh="自包含,不在目录外安装任何内容。适合希望应用 payload 与系统包数据库解耦的实验室主机。">Self-contained; installs nothing outside its directory.</p></div>
<div class="download-card"><h3 data-en="AppImage (any distribution)" data-zh="AppImage(任意发行版)">AppImage</h3><pre class="command">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</pre><p data-en="Bundles its GTK4 dependency closure, so it runs on older hosts than the packages require; no install step." data-zh="自带 GTK4 依赖闭包,比包管理器版本能跑在更老的宿主上;无需安装。">Bundles its GTK4 dependency closure; no install step.</p></div>
</div>
<div class="hero-buttons" style="margin-top:28px">
<a href="https://github.com/turinglambdaai/gptp-studio/releases/latest" class="btn btn-primary" data-en="Open latest release" data-zh="打开最新 Release">Open latest release</a>
Expand Down
24 changes: 15 additions & 9 deletions docs/update-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@
(state: idle | downloading | staged | applying | applied | failed) and
shows progress with the signed percent.
- **The updates window gates staging, not use**: Free and trial installs
update always; a perpetual Pro install updates through its entitlement's
`updates_through` day (PRICING.md's "含 1 年官方更新"). When the window
has passed, `start-download` refuses before any network leg and the flow
lands on `failed` with the renewal message — the customer's version
keeps working forever (perpetual rights, `docs/licensing-model.md`).
A newer version is still reported honestly: the pill stays, the refusal
names the renewal.
update always; a perpetual Pro install stages exactly the releases its
entitlement covers. Eligibility is a pure function of the candidate
release's authenticated `published_at` (the signed feed) against the
entitlement's `updates_through` — the current date is not an input, so
a release published inside the window stays stageable forever no
matter when the customer checks (PRICING.md's authorization decision).
A release published after the cutoff is refused at `start-download`
before any network leg and the flow lands on `failed` with the renewal
message — the customer's installed version keeps working forever
(perpetual rights, `docs/licensing-model.md`). A newer version is
still reported honestly: the pill stays, the refusal names the renewal.
- **Bounded transfer**: a stalled download surfaces a retryable error after
a finite timeout (no byte progress for 120 s, wall clock capped at
30 min); a slow or stalled manifest fetch is bounded the same way (60 s).
Expand All @@ -52,8 +56,10 @@
| relocatable tarball (everything else) | `linux/<arch>` `targz` entry | same stage dir | in-place swap: extract to a temp dir beside the stage, rename the running payload root to `<root>.old`, move the extracted payload in, chmod the launch executables with the integer mode `#o755`; a failed swap restores `<root>.old` so the previous install always stays runnable; the host then offers `重启应用 / Quit to Restart` |

The arm64 feed entries are the same shapes (`gptp-studio-<v>-linux-arm64.*`
assets); `pick-artifact` keys on the running architecture. There is no
AppImage (deliberately not ported) and no Windows/macOS channel.
assets); `pick-artifact` keys on the running architecture. The AppImage
is a distribution format, not an update shape: its users update by
re-downloading from Releases (the in-app updater serves deb and tarball
shapes only). There is no Windows/macOS channel.

## The feed: `update-stable.json`

Expand Down
2 changes: 1 addition & 1 deletion info.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

(define collection "gptp-studio")
(define pkg-desc "gPTP Studio - gPTP / IEEE 802.1AS debugging workstation (Racket + Glaze)")
(define version "0.3.1")
(define version "0.4.0")
(define pkg-authors '("turinglambdaai"))
(define license 'Apache-2.0)

Expand Down
2 changes: 1 addition & 1 deletion rivet.rktd
Original file line number Diff line number Diff line change
@@ -1 +1 @@
#hasheq((name . "gptp-studio") (display-name . "gPTP Studio") (version . "0.3.1") (build . 4) (identifier . "site.jrtx.gptpstudio") (release-channel . stable) (url-schemes . ()) (file-associations . ()) (resources . ("assets")) (linux-icon . "assets/icon.png") (linux-formats . ("deb" "rpm" "appimage")) (device-rpcs . ()) (macos-min-version . "14.0") (windows-min-version . "10.0.19041.0") (macos-icon . "assets/icon.icns") (windows-icon . "assets/icon.ico") (linux-binary-name . "gptp-studio") (backend . "app/backend.rkt") (module . "backend") (entry . "start") (protocol . 1))
#hasheq((name . "gptp-studio") (display-name . "gPTP Studio") (version . "0.4.0") (build . 5) (identifier . "site.jrtx.gptpstudio") (release-channel . stable) (url-schemes . ()) (file-associations . ()) (resources . ("assets")) (linux-icon . "assets/icon.png") (linux-formats . ("deb" "rpm" "appimage")) (device-rpcs . ()) (macos-min-version . "14.0") (windows-min-version . "10.0.19041.0") (macos-icon . "assets/icon.icns") (windows-icon . "assets/icon.ico") (linux-binary-name . "gptp-studio") (backend . "app/backend.rkt") (module . "backend") (entry . "start") (protocol . 1))
48 changes: 36 additions & 12 deletions support/updates.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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 -------------------------------------------------------------
Expand Down Expand Up @@ -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 -------------------------------------------------------------

Expand Down
19 changes: 18 additions & 1 deletion tests/backend-wire-test.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Loading
Loading