Learning and Evaluation Mode - implementation report
This page is the ground truth for what the GView client implements for Learning and Evaluation Mode (protocol v2).
It is kept identical to plans/LEARNING_MODE_IMPLEMENTED.md (the copy consumed by the course-server team) and
complements the design contract plans/LEARNING_MODE_PROTOCOL_SPEC.md. Where the client differs from that
contract, the difference is listed in 14. Deviations from the protocol spec and the spec has been updated.
1. Overview and threat model
A teacher runs a course server that hands out tasks (problems), resources and a signed policy. The student
pastes a connection string into GView (Options > Learning and Evaluation Mode, or GView learn <connectionString>).
GView then:
connects over verified TLS and authenticates with a secret access token;
receives a policy, verifies its Ed25519 signature over the raw bytes, then validates schema, digest, subject, server and validity window - and only then applies it;
enforces the policy: disabled features, plugin whitelist, watermark, memory-only storage, screen protection;
lists Weeks > Problems/Resources, delivers task content either into page-locked memory (
memory) or to a file chosen by the student (file), verifying SHA-256 in both cases;submits flags (with an optional explanation) using idempotency keys;
sends privacy-preserving telemetry (event-level + counters) in the background.
What is guaranteed (against a student using an unmodified GView build):
no policy is applied unless it verifies with the public key from the connection string (or the INI fallback); a missing key means the policy is refused, never silently ignored;
a verified policy is never downgraded by a later unverifiable or legacy answer;
memory-only task content is never written to disk by GView itself (no temp files, no disassembly cache, no exports, no “drop to disk” for signature checks) and is wiped when its window closes;
every policy-gated GView entry point listed in 7. Feature enforcement matrix refuses the action, says so, and records a
feature_blockedtelemetry event;telemetry never contains user-authored text, file paths, clipboard content or keystrokes.
What is not guaranteed (best effort, see CCIS sec. 4.5 / 9.3): a photo of the screen, retyping content, a patched/rebuilt GView (it is open source), external tools reading process memory, OS-level screenshot tools on platforms without capture exclusion, and the clipboard paths of AppCUI’s own controls (see Limitations).
2. Connection string
v1: base64( base64(token) "#" base64(serverUrl) )
v2: base64( base64(token) "#" base64(serverUrl) "#" base64(policyPublicKeyHex) [ "#" base64(extraJson) ] )
Standard base64 alphabet. Decoding is strict: no whitespace inside, no foreign characters, padding optional, non-canonical trailing bits rejected. Surrounding whitespace (copy/paste) is trimmed.
token: 16..256 printable ASCII characters (0x21-0x7E). Anything else (CR/LF in particular) is rejected because the token travels in an HTTP header.serverUrl:https://host[:port][/path], no user-info, query or fragment; trailing/removed, scheme and host lower-cased.http://is accepted only forlocalhost,127.0.0.1or[::1]and only when[GView] LearningAllowPlainHttpLocalhost=true(teacher-laptop labs).policyPublicKeyHex: exactly 64 hex characters (raw 32-byte Ed25519 key).extraJson(optional JSON object):label(string, shown in the window) andcaPem(base64 of a PEM certificate used as the only trust anchor for that deployment, e.g. a self-signed server certificate).
Worked example (token tok_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcd, server https://re.example.edu, the stub
server’s public key 7fd806c7f2a9e88ec89bb6cca8ba6a2b4b225aa85e488e8222be9dc2acadf094):
v1 = ZEc5clgwRkNRMFJGUmtkSVNVcExURTFPVDFCUlVsTlVWVlpYV0ZsYU1ERXlNelExTmpjNE9XRmlZMlE9I2FIUjBjSE02THk5eVpTNWxlR0Z0Y0d4bExtVmtkUT09
v2 = ZEc5clgwRkNRMFJGUmtkSVNVcExURTFPVDFCUlVsTlVWVlpYV0ZsYU1ERXlNelExTmpjNE9XRmlZMlE9I2FIUjBjSE02THk5eVpTNWxlR0Z0Y0d4bExtVmtkUT09I04yWmtPREEyWXpkbU1tRTVaVGc0WldNNE9XSmlObU5qWVRoaVlUWmhNbUkwWWpJeU5XRmhPRFZsTkRnNFpUZ3lNakppWlRsa1l6SmhZMkZrWmpBNU5BPT0=
v2 + extra {"label":"RE 2026 - Group A"} =
ZEc5clgwRkNRMFJGUmtkSVNVcExURTFPVDFCUlVsTlVWVlpYV0ZsYU1ERXlNelExTmpjNE9XRmlZMlE9I2FIUjBjSE02THk5eVpTNWxlR0Z0Y0d4bExtVmtkUT09I04yWmtPREEyWXpkbU1tRTVaVGc0WldNNE9XSmlObU5qWVRoaVlUWmhNbUkwWWpJeU5XRmhPRFZsTkRnNFpUZ3lNakppWlRsa1l6SmhZMkZrWmpBNU5BPT0jZXlKc1lXSmxiQ0k2SWxKRklESXdNallnTFNCSGNtOTFjQ0JCSW4wPQ==
INI keys (section [GView] of gview.ini):
Key |
Meaning |
|---|---|
|
Saved only after a successful, verified connect; used to reconnect on open. |
|
64 hex chars; used when the connection string has no key (v1). |
|
|
|
Default folder for |
|
Shortcut of “Submit flag for this task” in a task window (default Ctrl+Alt+F). |
3. Access token and request headers
The token is a secret bearer credential. Every request is an HTTPS POST with:
Header |
Value |
|---|---|
|
the access token |
|
client version ( |
|
|
|
|
|
id of the policy currently in force (omitted before the first policy) |
|
|
|
|
Transport rules (libcurl): CURLOPT_SSL_VERIFYPEER=1, CURLOPT_SSL_VERIFYHOST=2, TLS >= 1.2, protocols
restricted to https (or http for the localhost exception), redirects are never followed (the token must
only reach the configured server), the Windows certificate store is trusted (CURLSSLOPT_NATIVE_CA) unless
caPem is supplied, in which case it is the only trust anchor (CURLOPT_CAINFO_BLOB). Timeouts: connect 10 s,
JSON requests 60 s, downloads 300 s, telemetry 15 s (2 s for the final flush at exit). Response caps: Connect 256 KiB
(decoded policy 64 KiB), JSON 4 MiB, binaries 256 MiB, telemetry/submit replies 64 KiB. All network I/O runs on a
worker thread; results are applied on the UI thread. Ending the session or exiting cancels the running request
(checked about every second) instead of waiting for its timeout.
4. Endpoints
All endpoints are POST. Error envelope for every non-2xx answer:
{ "status": "error", "code": "POLICY_EXPIRED", "details": "human readable", "retryable": false }
Codes understood (HTTP mapping as in the spec): BAD_REQUEST 400, UNAUTHORIZED 401, FORBIDDEN 403,
NOT_FOUND 404, RATE_LIMITED 429 (+ Retry-After seconds), POLICY_EXPIRED 403, POLICY_NOT_STARTED 403,
ALREADY_SOLVED 409, ITEM_DISABLED 404, UNSUPPORTED_CLIENT 426, SERVER_ERROR 500. When the body is not an
envelope the code is derived from the HTTP status. TRANSPORT_ERROR and PROTOCOL_ERROR are client-side only.
Endpoint |
Client behaviour |
|---|---|
|
Sent first. |
|
Legacy connect. Empty/non-JSON |
|
Catalogue. |
|
Problem download (bytes + delivery headers). |
|
Resource download ( |
|
Submission (10. Submission). |
|
Batches (9. Telemetry). |
|
Not used by the client. |
Connect response (protocol 2):
{ "status": "ok", "protocolVersion": 2, "serverVersion": "2.0.0", "serverTime": 1759400000,
"policy": "<base64 of the exact policy bytes>", "policySignature": "<base64 of a 64-byte Ed25519 signature>",
"user": { "displayName": "Student 17", "score": 120 } }
A JSON object without policy and without protocolVersion >= 2 is treated as legacy. A protocol-2 answer
without policy/policySignature is an error. serverTime is informational (the client validates with its
own clock and 120 s skew).
Item names are validated before they are placed in a URL: [A-Za-z0-9_-]{1,64}.
5. Policy (schema 2)
Signature: Ed25519.verify(publicKey, policyBytes, signature) with OpenSSL EVP_DigestVerify over the exact
decoded bytes, before any parsing. Validation order and error codes (the code prefixes the message and is the
reason of policy_rejected):
POLICY_KEY_MISSING- no 32-byte public key (connection string v2 orPolicyPublicKey);POLICY_SIGNATURE_INVALID- signature does not verify;POLICY_SCHEMA_UNSUPPORTED-schemais not2;POLICY_MALFORMED- structure/types (see below);POLICY_DIGEST_MISMATCH- digest check;POLICY_SUBJECT_MISMATCH-subject != hex(sha256(token))[0:16](constant-time compare);POLICY_SERVER_MISMATCH-serverUrl(normalised) differs from the connection string’s server;POLICY_NOT_STARTED/POLICY_EXPIRED-startsAt - 120 <= now <= endsAt + 120;platform:
requireScreenProtectand capture exclusion unavailable => state Error (restrictions applied, no task can be opened).
Fields (unknown top-level fields are ignored; a present field with a wrong type is an error):
Field |
Required / default |
Rules |
|---|---|---|
|
required |
must be 2 |
|
required |
|
|
required |
64 lowercase hex |
|
“” |
<= 256 chars, no control characters |
|
0 |
unsigned integer |
|
required |
unsigned, |
|
required |
16 lowercase hex |
|
“” |
when present it must match the connection string’s server |
|
[] |
|
|
[] |
<= 256 names |
|
|
|
|
“” |
<= 128 chars, shown in every file window title |
|
true |
|
|
false |
|
|
false |
nothing is collected or sent when false |
|
60 |
clamped to [5, 3600] |
|
500 |
clamped to [1, 5000]; the queue holds 4x this |
|
120 |
clamped to [10, 86400] |
|
true |
false => counters only, no events |
|
true |
false => Submit hidden |
|
false |
explanation mandatory for every problem |
|
2000 |
clamped to [0, 100000] |
|
“” |
“” or |
|
“” |
16 lowercase hex; required with |
Digest: the server serialises the policy with "digest":"" (compact separators, sorted keys), takes
sha256 of those bytes and embeds the hex value. The client verifies it by blanking the (unique) occurrence of
"digest":"<hex>" (or "digest": "<hex>") in the received bytes and hashing the result. The digest must appear
exactly once.
Re-entrancy: a connect while a policy is in force validates the new policy completely; only a valid policy
replaces the current one (a different id emits policy_applied). Failed reconnects and legacy answers leave
the current policy in force.
Implementation: GViewCore/src/Security/RestrictedMode.cpp (VerifyAndParsePolicy, Internal::Activate
returning an ActivationReport). The policy object is signed public data and is not page-locked; secrets are
(see below).
6. Content delivery
Download response headers: X-GView-Delivery: memory|file (required in v2), X-GView-SHA256 (hex of the plaintext,
required in v2), X-GView-Encrypted: 0|1, X-GView-Filename (sanitised: base name only, no reserved device names,
no <>:"|?* or control characters, <= 128 chars), X-GView-Item-Version.
Effective mode = most restrictive of policy.storageMode, the catalogue deliveryMode and X-GView-Delivery
(memory wins). Legacy sessions always use memory mode (historic behaviour).
aes-256-gcm-hkdf-v1:
key = HKDF-SHA256(ikm = UTF8(accessToken), salt = UTF8(policy.id), info = "GView-Content-v1", L = 32)
keyId = hex(sha256(key))[0:16] -> must equal policy.contentKeyId (checked before decrypting)
aad = UTF8(itemName) || 0x00 || UTF8(policy.id)
blob = "GVE1" || iv(12) || ciphertext || tag(16)
Rules: an encrypted blob requires an active policy; a memory delivery that is not encrypted is refused when the
policy declares contentEncryption; the SHA-256 of the plaintext is verified in all v2 cases.
Memory mode: the blob is decrypted directly into a LockedBuffer (VirtualAlloc + VirtualLock - the
working set is grown if needed - / mmap + mlock + MADV_DONTDUMP), the HTTP buffers use an allocator that
zeroes memory before freeing it, and the viewer reads straight from the locked buffer through a read-only
LockedMemoryDataObject (no MemoryFile copy). The buffer is wiped when the window closes. The window is bound
to the item; for memory items GView never reads or writes the disassembly cache, never exports, and skips WinTrust
(which would need the file on disk; the embedded OpenSSL signature check still runs).
Derived objects: GView does not track which window an in-memory object (Object::Type::MemoryBuffer, e.g. an entry
extracted from an archive) was opened from. While a memory item - or an object derived from one - is open, every new
in-memory object is therefore treated as memory-only as well (LearningSession::OnObjectCreated), until its own
window closes. Objects opened from disk are never affected.
Known residual plaintext copies (honest list): GView’s DataCache chunk cache (up to CacheSize, 10 MiB by
default, ordinary heap), parser/viewer-derived structures (strings, disassembly text, images), AppCUI screen buffers.
Pinning can fail silently (Windows working-set quota, Linux RLIMIT_MEMLOCK); content is then still wiped but may
have been paged.
File mode: the student picks the destination in a Save dialog (default LearningDownloadFolder or
<Documents>/GView/<week>); the content is written to <name>.part, re-hashed from disk, then atomically moved
into place (MoveFileExW(REPLACE_EXISTING|WRITE_THROUGH) / rename). Any failure removes the .part file.
Secrets: the access token is held in a SecureString (wiped allocator) and the content key in a LockedBuffer;
both are erased by End session and at exit. libcurl’s own copy of the header list is freed without wiping, the
connection field’s text and the saved ServerConnectionString (which contains the token) remain - the latter is
stored in gview.ini in plain text, as before this work.
7. Feature enforcement matrix
Every gate goes through GView::App::IsBlockedByPolicy (or IsExportBlockedFor(object, ...)), which shows a
“Blocked by course policy” notice (at most once per 5 s per feature) and records feature_blocked. Commands are
also hidden from the command bar where GView builds it. Under a memory storage policy, Export and SaveAs
are restricted even if not listed.
Feature |
Gated locations |
|---|---|
Copy |
|
Clipboard |
|
Export |
GridViewer export cell/column (hidden + gated); |
SaveAs |
|
Plugins |
runtime filter in |
LLMHints |
|
Screenshots |
|
Watermark |
|
What remains possible: see 1. Overview and threat model and Limitations.
8. Screen protection
Protection is requested when bestEffortScreenProtect or requireScreenProtect is true or Screenshots is
disabled. It is applied when capture exclusion succeeds on at least one GView-owned window.
Platform / frontend |
Behaviour |
|---|---|
Windows, SDL frontend |
|
Windows, console frontend |
The console window belongs to conhost/Windows Terminal, so capture exclusion cannot be applied => not applied; the keyboard hook is still installed. |
Linux |
No capture-exclusion API; only |
macOS |
Not implemented for the terminal frontend => not applied. |
requireScreenProtect=true and not applied => session state Error, tasks cannot be opened, the window explains
the platform limitation and screen_protect_failed{reason:"unsupported_platform"} is recorded. Everything is
reverted on deactivation (hook thread stopped, affinity reset).
9. Telemetry
Only windows opened from the course catalogue produce item events. Batches are posted by a background thread every
flushIntervalSeconds and immediately after submit, task_close and policy_applied; at exit
session_end is flushed synchronously with a 2 s cap.
Batch:
{ "sessionId": "<uuid4, one per session, kept across reconnects>", "policyId": "...", "policyDigest": "...",
"clientVersion": "0.390.0", "platform": "windows-x64", "sentAt": 1759403100,
"events": [ { "seq": 1, "t": 1759401000, "type": "task_open", "item": "w1_stack_var", "itemVersion": 3, "mode": "memory" } ],
"counters": { "w1_stack_var": { "jump_follow": 12, "...": 0, "viewer_open": {"Dissasm": 1}, "feature_blocked": {"Copy": 1} } } }
Event whitelist (any other field, or an item on an event that does not allow it, is refused by the client):
Event |
item |
fields |
|---|---|---|
|
no |
|
|
no |
|
|
no |
|
|
no |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
yes |
|
|
no |
|
|
no |
|
Counters (totals since session start, per item; the server replaces them): jump_follow, jump_back,
jump_forward, goto_entrypoint, goto_dialog, comment_add, comment_edit, comment_remove,
label_rename, note_add, idle_seconds, active_seconds, dissasm_seconds and the maps viewer_open
and feature_blocked.
Activity model: GView repaints only in response to input, so a repaint of a file window is the activity signal.
Idle starts when no activity happened for idleThresholdSeconds; one idle{seconds} event is recorded when
activity resumes. Active and Dissasm seconds accrue while a course window has focus and the user is not idle.
Delivery: seq is monotonic per sessionId and preserved across retries (the server de-duplicates on
(sessionId, seq)). 2xx => removed; 429/5xx/network error => kept and retried at the next flush;
any other 4xx => dropped. The queue holds maxBatchEvents x 4 events (oldest dropped). A 403
POLICY_EXPIRED on telemetry expires the session. Batches stay below 900 KiB.
Privacy: never sent - comment text, label names, note contents, clipboard content, keystrokes, file paths, user
files opened outside the catalogue, screenshots. telemetry.enabled=false or no policy => nothing is recorded and
no thread runs. policy_rejected is only ever sent when a previously verified policy with telemetry enabled is in
force (a rejected policy cannot grant consent).
10. Submission
Body (application/json):
{ "problem": "w1_stack_var", "flag": "3", "explanation": "...", "clientSubmissionId": "<uuid4>",
"policyId": "pol_...", "policyDigest": "...", "clientVersion": "0.390.0", "clientTime": 1759403000 }
The flag is trimmed, must be non-empty and <= 512 bytes; the explanation is required when the policy or the problem requires it and is limited to
explanationMaxChars.clientSubmissionIdis a UUIDv4 created once per new answer (problem + flag + explanation fingerprint). A retry after a transport failure, 408, 429 or 5xx reuses it; any answer from the server that grades or refuses the attempt consumes it, so the next submission is a new attempt; editing the answer creates a new id.submit{clientSubmissionId}telemetry is recorded and a flush requested.Response:
correct,points,attempts,alreadySolved,duplicate,details,totalScoreare shown.409 ALREADY_SOLVEDis informational.403 POLICY_EXPIREDexpires the session.Legacy servers (no
correctfield):correct = (status == "ok").UI: catalogue Submit flag button, or Ctrl+Alt+F (
Key.LearningSubmitFlag) in a task window.
11. Catalogue model
GetWeeks is parsed defensively: weeks/items with enabled:false are hidden (the server should never send
them), items with invalid names, unknown kind/deliveryMode or non-http(s) links are dropped and counted
(“N entries hidden”), duplicates are dropped, strings are capped and control characters removed. Weeks are sorted by
order then id; items by order then name. kind: file and text are downloaded (text opens in
the Text/Lexical viewer), link shows the URL read-only (Copy only if Clipboard is allowed).
Legacy servers: /GView/ with an empty 200 => Legacy state: no restrictions are applied (there is nothing to
verify), the catalogue comes from the flat GetProblems list as a single week, submissions use the legacy verdict.
The window says so explicitly.
12. Session state machine
Disconnected --Connect--> Connecting --verified policy------------> Active --endsAt+120s / POLICY_EXPIRED--> Expired
^ | \--legacy (empty 200)-------------> Legacy (catalogue + submit, no restrictions)
| \---any failure (no policy yet)-----> Error (no task can be opened)
| Active --requireScreenProtect unmet--> Error (restrictions applied, tasks blocked)
| Active/Expired/Error --Reconnect--> Connecting (current policy stays in force until a newer validates)
+--End session (only when no learning window is open) / exit-- any state
Expired keeps the restrictions applied (already open windows stay protected); downloads and submissions are refused;
one final session_end is flushed and flushing stops.
13. Verification
Environment: Windows 11 Enterprise 10.0.26200, MSVC 19.44 (VS 2022 17.14), Debug, Catch2 3.10.0, OpenSSL 3.5.2, libcurl 8.16.0 (OpenSSL backend), Python 3.11.9 + cryptography 50.0.1 for the stub server.
Case |
Tests ( |
Result |
|---|---|---|
T1 |
|
passed |
T2 |
|
passed |
T3 |
|
passed |
T4 |
|
passed (state); entry points by review + UI |
T5 |
|
passed |
T6 |
|
passed |
T7 |
|
passed |
T8 |
|
passed |
Totals on this machine: 29 learning unit test cases / 646 assertions pass (the two end-to-end cases
are hidden and need the stub server: 58 + 12 assertions pass, 716 in total); the full ctest suite (80 cases) passes. Cross-implementation
vectors (Test vectors) are asserted by cross-implementation test vectors.
How to reproduce:
cmake -S . -B build-test -DENABLE_TESTS=ON && cmake --build build-test --config Debug
ctest --test-dir build-test -C Debug
python tools/learning_mode_stub_server.py --quiet --require-explanation # prints the connection string
set GVIEW_LEARNING_STUB_CS=<printed connection string>
bin\Debug\libGViewCore.exe "[integration]"
Manual check of the UI: GView learn <connection string printed by the stub>.
Test vectors
The stub key is derived from a public seed and is for tests only.
Input / output |
Value |
|---|---|
token |
|
subject |
|
content key (pol id |
|
|
|
contentKeyId |
|
AAD (item |
|
|
|
public key |
|
Signed policy (serverUrl https://re.example.edu, verified at now = 1759400000):
{"allowedPlugins":[],"bestEffortScreenProtect":true,"digest":"5e6a9108d33760ebce5fc15585d40a7081d3eddaaa743eab771d0e535039eff8","disabledFeatures":["Copy","Export"],"endsAt":1760004800,"id":"pol_2026w1_ab12cd","issuedAt":1759400000,"purpose":"Evaluation week 1","requireScreenProtect":false,"schema":2,"serverUrl":"https://re.example.edu","startsAt":1759399880,"storageMode":"file","subject":"f1988ba4bd709819","submission":{"allowInTool":true,"explanationMaxChars":2000,"requireExplanation":false},"telemetry":{"enabled":true,"eventLevel":true,"flushIntervalSeconds":60,"idleThresholdSeconds":120,"maxBatchEvents":500},"watermark":"Student 17"}
signature (base64) = LnnH52/44aGmbhfBVmmv7LwdQoUSTaNppaRMhuE4W4tBy0rKPLyUIbVAlUa2Jw/cuy3jjOSjIkWPkFzbPNdWBQ==
Limitations
AppCUI controls write to the clipboard on their own (Ctrl+C in
ListView/TreeView/Gridpanels,TextField/TextAreaselections, theme editor, window manager notes) throughAppCUI::OS::Clipboardand are not gated, because the AppCUI submodule was not modified. Paste is not blocked either. Proposed upstream fix: a process-wide clipboard policy callback inAppCUI::OS::Clipboard(SetText/GetTextconsult it), installed by GView on activation.Screen capture cannot be prevented on the Windows console frontend, Linux or macOS (8. Screen protection).
Frontends without frame updates (ncurses on Linux/macOS) run network operations synchronously on the UI thread, bounded by the timeouts above. GView enables AppCUI’s FPS mode so that the Windows console and SDL frontends deliver
OnFrameUpdate(~30 Hz, no repaint unless something changed); on those frontends the automatic connect when the Learning window opens is deferred to its first frame so that it runs on the worker.Idle/active time is derived from repaints (approximation).
Retry-Afteris shown to the user but telemetry retries simply wait for the next flush interval.Plugins outside GViewCore can still write files through code paths not listed in the matrix; the plugin whitelist (
Plugins+allowedPlugins) is the control for those (e.g.FileDownloader).Residual plaintext copies and secrets: see 6. Content delivery.
14. Deviations from the protocol spec
All of these are additional strictness or clarifications; plans/LEARNING_MODE_PROTOCOL_SPEC.md was updated.
Extra validation step
POLICY_SERVER_MISMATCH: a non-emptyserverUrlmust equal the connection string’s server (scheme/host case-insensitive, trailing/ignored).Course-server policies must be
schema: 2(schema 1 is only accepted by the file-basedLoadPolicyFromFiles).storageMode: memoryrequirescontentEncryptionandcontentKeyId;contentEncryptionrequirescontentKeyId; present fields with wrong JSON types are rejected (fail closed).Unencrypted
memorydeliveries are refused when the policy declarescontentEncryption;X-GView-SHA256andX-GView-Deliveryare mandatory in protocol 2.Telemetry: explicit per-event field whitelist (table above);
note_addcounter;eventLevel:falsemeans counters only; item events only for catalogue windows;policy_rejectedonly with prior consent.Screen protection is requested by
bestEffortScreenProtectorrequireScreenProtector a disabledScreenshotsfeature.Under a
memorystorage policyExportandSaveAsare restricted for every object.Telemetry numeric parameters and
explanationMaxCharsare clamped to the ranges listed in 5. Policy (schema 2).Redirects are never followed;
caPemreplaces (does not extend) the system trust store.Item names must match
[A-Za-z0-9_-]{1,64}(DjangoSlugFieldcompatible)./GView/Heartbeatis not used by the client.
15. Open questions for the server implementer
Browser clients need CORS and a platform-aware policy: see 16. Browser clients.
Please make sure
policy.serverUrlis exactly the URL put in connection strings (otherwise use an emptyserverUrl) - the client rejects mismatches.Accept the
note_addcounter and thereasonvalues listed forsession_end.Should
eventLevel:falsealso suppress session lifecycle events server-side? The client sends counters only.The client treats any
4xxexcept408/429onSubmitFlagas consuming the idempotency key (e.g. a400for a missing explanation). Confirm the server does not store aSubmissionrow in that case.Confirm that a protocol-2 download of a
kind=linkresource is never needed (the client uses the catalogue URL).Legacy flat
GetProblemsitems carry nodeliveryMode/sha256; for v2 clients please always serveGetWeeks.
16. Browser clients
GView also runs inside a web page (docs/source/wasm.rst, GVIEW_ENABLE_BROWSER_NETWORK, ON by default for the
WebAssembly build). The protocol is unchanged; the transport and the platform are different.
Transport. A page has no sockets: requests go through fetch() (LearningHttpFetch.cpp) and the browser owns
TLS. The desktop rules map as follows:
Desktop (libcurl) |
Browser (fetch) |
|---|---|
|
enforced by the browser (a page can not skip certificate validation) |
TLS >= 1.2 |
|
|
|
exception) |
is served from the local machine and |
redirects never followed |
|
|
impossible - the browser always uses the system store. A connection string with
|
response caps, timeouts, cancel |
|
|
CORS (server side, required). Every endpoint answers the OPTIONS preflight of the allowed GView web origins
(exact origins, e.g. the GitHub Pages site or the self-hosted one; no * is needed since no cookie is sent):
Access-Control-Allow-Origin: <origin of the GView page> (and Vary: Origin)
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: XAppUserID, X-GView-Client, X-GView-Protocol, X-GView-Policy, X-GView-Platform,
Content-Type, Accept
Access-Control-Expose-Headers: X-GView-Delivery, X-GView-SHA256, X-GView-Encrypted, X-GView-Filename,
X-GView-Item-Version, Retry-After, Content-Length
Access-Control-Max-Age: 600
Without Expose-Headers the delivery headers are invisible to the page and every download is refused (they are
mandatory in protocol 2). tools/learning_mode_stub_server.py --cors-origin <origin> implements all of this.
Platform-aware policies (server decides, client fails closed). Every request carries
X-GView-Platform: web-wasm32. A page can not exclude itself from screen capture, can not pin memory and is open to
the developer tools of the browser, so:
requireScreenProtectis never met in a browser: the session goes to Error and no task can be opened (as on the other platforms without capture exclusion);LockedBufferreportsIsLocked() == false(the memory of the page is a JavaScriptArrayBuffer): memory mode still decrypts in place, wipes on close and never touches the file system, but cannot prevent paging;the server may issue a dedicated policy for
web-wasm32(e.g.bestEffortScreenProtect: false), refuse with426 UNSUPPORTED_CLIENT, or keep the same policy and accept the weaker guarantees.
No threads. A page has none (they would need COOP/COEP headers). A network job runs on the UI thread and is
re-run from the start each time one of its responses arrives (JobReplay.hpp, BackgroundWorkerWasm.cpp); the
side effects before a request (a telemetry batch) run once. The UI keeps running while requests are in flight;
End session aborts the request in flight. Telemetry is flushed every flushIntervalSeconds from the session tick,
and once more when the page is hidden; at End session the last batch is sent with keepalive without waiting for
the answer. A tab in the background is throttled by the browser (down to one timer per minute after a few minutes),
so its network jobs complete only when it becomes visible again.
Secrets at rest. ServerConnectionString is not stored in GView.ini by the web build (the page keeps
GView.ini in clear in its localStorage): it is kept AES-GCM encrypted by the persistent storage of the page,
under a non-extractable key (File > Stored files > Forget Learning connection). Learning task files saved in
file mode land in the home folder and are kept like any other file; memory content is never written to the
file system and therefore never stored.